From 5c4be7635bbf38e239921d60923c3457530ed7b4 Mon Sep 17 00:00:00 2001 From: arifal Date: Fri, 21 Feb 2025 00:46:33 +0700 Subject: [PATCH] partial update crud customer data --- .../Controllers/Api/CustomersController.php | 130 +++++++++++++++ app/Http/Controllers/CustomersController.php | 26 +++ app/Http/Requests/CustomersRequest.php | 33 ++++ app/Http/Resources/CustomersResource.php | 19 +++ app/Imports/CustomersImport.php | 52 ++++++ app/Models/Customer.php | 20 +++ ...25_02_20_154106_create_customers_table.php | 33 ++++ database/seeders/UsersRoleMenuSeeder.php | 9 ++ resources/js/customers/create.js | 65 ++++++++ resources/js/customers/edit.js | 65 ++++++++ resources/js/customers/index.js | 151 ++++++++++++++++++ resources/js/customers/upload.js | 78 +++++++++ resources/views/customers/create.blade.php | 32 ++++ resources/views/customers/edit.blade.php | 33 ++++ resources/views/customers/form.blade.php | 24 +++ resources/views/customers/index.blade.php | 31 ++++ resources/views/customers/upload.blade.php | 80 ++++++++++ .../views/spatial-plannings/index.blade.php | 1 - routes/api.php | 11 +- routes/web.php | 8 + 20 files changed, 898 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Api/CustomersController.php create mode 100644 app/Http/Controllers/CustomersController.php create mode 100644 app/Http/Requests/CustomersRequest.php create mode 100644 app/Http/Resources/CustomersResource.php create mode 100644 app/Imports/CustomersImport.php create mode 100644 app/Models/Customer.php create mode 100644 database/migrations/2025_02_20_154106_create_customers_table.php create mode 100644 resources/js/customers/create.js create mode 100644 resources/js/customers/edit.js create mode 100644 resources/js/customers/index.js create mode 100644 resources/js/customers/upload.js create mode 100644 resources/views/customers/create.blade.php create mode 100644 resources/views/customers/edit.blade.php create mode 100644 resources/views/customers/form.blade.php create mode 100644 resources/views/customers/index.blade.php create mode 100644 resources/views/customers/upload.blade.php diff --git a/app/Http/Controllers/Api/CustomersController.php b/app/Http/Controllers/Api/CustomersController.php new file mode 100644 index 0000000..d759a8d --- /dev/null +++ b/app/Http/Controllers/Api/CustomersController.php @@ -0,0 +1,130 @@ +orderBy('id', 'desc'); + if ($request->has('search') &&!empty($request->get('search'))) { + $query = $query->where('nomor_pelanggan', 'LIKE', '%'.$request->get('search').'%') + ->orWhere('nama', 'LIKE', '%'.$request->get('search').'%') + ->orWhere('kota_palayanan', 'LIKE', '%'.$request->get('search').'%'); + } + return CustomersResource::collection($query->paginate()); + } + + /** + * Store a newly created resource in storage. + */ + public function store(CustomersRequest $request) + { + try{ + $customer = Customer::create($request->validated()); + return response()->json(['message' => 'Successfully created', new CustomersResource($customer)]); + }catch(\Exception $e){ + return response()->json([ + 'message' => 'Failed to create customer', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + try{ + $customer = Customer::find($id); + if($customer){ + return new CustomersResource($customer); + } else { + return response()->json(['message' => 'Customer not found'], 404); + } + }catch(\Exception $e){ + return response()->json([ + 'message' => 'Failed to retrieve customer', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(CustomersRequest $request, string $id) + { + try{ + $customer = Customer::find($id); + if($customer){ + $customer->update($request->validated()); + return response()->json(['message' => 'Successfully updated', new CustomersResource($customer)]); + } else { + return response()->json(['message' => 'Customer not found'], 404); + } + }catch(\Exception $e) { + return response()->json([ + 'message' => 'Failed to update customer', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + try{ + $customer = Customer::find($id); + if($customer){ + $customer->delete(); + return response()->json(['message' => 'Successfully deleted']); + }else { + return response()->json(['message' => 'Customer not found'], 404); + } + }catch(\Exception $e) { + return response()->json([ + 'message' => 'Failed to delete customer', + 'error' => $e->getMessage() + ], 500); + } + } + + public function upload(ExcelUploadRequest $request){ + try{ + if(!$request->hasFile('file')){ + return response()->json([ + 'error' => 'No file provided' + ], 400); + } + + $file = $request->file('file'); + Excel::import(new CustomersImport, $file); + + return response()->json([ + 'message' => 'File uploaded successfully', + ]); + }catch(\Exception $e){ + \Log::info($e->getMessage()); + return response()->json([ + 'error' => 'Failed to upload file', + 'message' => $e->getMessage() + ], 500); + } + } +} diff --git a/app/Http/Controllers/CustomersController.php b/app/Http/Controllers/CustomersController.php new file mode 100644 index 0000000..9c0c8ec --- /dev/null +++ b/app/Http/Controllers/CustomersController.php @@ -0,0 +1,26 @@ +|string> + */ + public function rules(): array + { + return [ + 'nomor_pelanggan' => ['required', 'string'], + 'kota_pelayanan' => ['required', 'string'], + 'nama' => ['required', 'string'], + 'alamat' => ['required', 'string'], + 'latitude' => ['required', 'numeric', 'between:-90,90'], + 'longitude' => ['required', 'numeric', 'between:-180,180'], + ]; + } +} diff --git a/app/Http/Resources/CustomersResource.php b/app/Http/Resources/CustomersResource.php new file mode 100644 index 0000000..2b48c64 --- /dev/null +++ b/app/Http/Resources/CustomersResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Imports/CustomersImport.php b/app/Imports/CustomersImport.php new file mode 100644 index 0000000..e448faa --- /dev/null +++ b/app/Imports/CustomersImport.php @@ -0,0 +1,52 @@ +skip(1) as $row) { + if (!isset($row[0]) || empty($row[0])) { + continue; + } + + $latitude = filter_var($row[4], FILTER_VALIDATE_FLOAT) ? bcadd($row[4], '0', 18) : null; + $longitude = filter_var($row[5], FILTER_VALIDATE_FLOAT) ? bcadd($row[5], '0', 18) : null; + + $batchData[] = [ + 'nomor_pelanggan' => $row[0], + 'kota_pelayanan' => $row[1], + 'nama' => $row[2], + 'alamat' => $row[3], + 'latitude' => $latitude, + 'longitude' => $longitude, + ]; + } + + if (!empty($batchData)) { + Customer::upsert($batchData, ['nomor_pelanggan'], ['kota_pelayanan', 'nama', 'alamat', 'latitude', 'longitude']); + } + } + + public function sheets(): array { + return [ + 0 => $this + ]; + } +} diff --git a/app/Models/Customer.php b/app/Models/Customer.php new file mode 100644 index 0000000..b50d890 --- /dev/null +++ b/app/Models/Customer.php @@ -0,0 +1,20 @@ +id(); + $table->string('nomor_pelanggan')->unique(); + $table->string('kota_pelayanan'); + $table->string('nama'); + $table->text('alamat'); + $table->decimal('latitude', 22,18); + $table->decimal('longitude', 22,18); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('customers'); + } +}; diff --git a/database/seeders/UsersRoleMenuSeeder.php b/database/seeders/UsersRoleMenuSeeder.php index d8a4c9f..875061e 100644 --- a/database/seeders/UsersRoleMenuSeeder.php +++ b/database/seeders/UsersRoleMenuSeeder.php @@ -193,6 +193,13 @@ class UsersRoleMenuSeeder extends Seeder "parent_id" => $data->id, "sort_order" => 6, ], + [ + "name" => "PDAM", + "url" => "customers", + "icon" => null, + "parent_id" => $data->id, + "sort_order" => 7, + ], [ "name" => "Lap Pariwisata", "url" => "tourisms.index", @@ -221,6 +228,7 @@ class UsersRoleMenuSeeder extends Seeder $umkm = Menu::where('name', 'UMKM')->first(); $lack_of_potentials = Menu::where('name', 'Dashboard Potensi')->first(); $spatial_plannings = Menu::where('name', 'Tata Ruang')->first(); + $pdam = Menu::where('name', 'PDAM')->first(); // Superadmin gets all menus $superadmin->menus()->sync([ @@ -247,6 +255,7 @@ class UsersRoleMenuSeeder extends Seeder $umkm->id => ["allow_show" => true, "allow_create" => true, "allow_update" => true, "allow_destroy" => true], $lack_of_potentials->id => ["allow_show" => true, "allow_create" => true, "allow_update" => true, "allow_destroy" => true], $spatial_plannings->id => ["allow_show" => true, "allow_create" => true, "allow_update" => true, "allow_destroy" => true], + $pdam->id => ["allow_show" => true, "allow_create" => true, "allow_update" => true, "allow_destroy" => true], ]); // Admin gets limited menus diff --git a/resources/js/customers/create.js b/resources/js/customers/create.js new file mode 100644 index 0000000..23aa7af --- /dev/null +++ b/resources/js/customers/create.js @@ -0,0 +1,65 @@ +class CreateCustomer { + constructor() { + this.initCreateCustomer(); + } + + initCreateCustomer() { + const toastNotification = document.getElementById("toastNotification"); + const toast = new bootstrap.Toast(toastNotification); + document + .getElementById("btnCreateCustomer") + .addEventListener("click", async function () { + let submitButton = this; + let spinner = document.getElementById("spinner"); + let form = document.getElementById("formCreateCustomer"); + + if (!form) { + console.error("Form element not found!"); + return; + } + // Get form data + let formData = new FormData(form); + + // Disable button and show spinner + submitButton.disabled = true; + spinner.classList.remove("d-none"); + + try { + let response = await fetch(form.action, { + method: "POST", + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + }, + body: formData, + }); + + if (response.ok) { + let result = await response.json(); + document.getElementById("toast-message").innerText = + result.message; + toast.show(); + setTimeout(() => { + window.location.href = "/data/customers"; + }, 2000); + } else { + let error = await response.json(); + document.getElementById("toast-message").innerText = + error.message; + toast.show(); + console.error("Error:", error); + } + } catch (error) { + console.error("Request failed:", error); + document.getElementById("toast-message").innerText = + error.message; + toast.show(); + } + }); + } +} + +document.addEventListener("DOMContentLoaded", function (e) { + new CreateCustomer(); +}); diff --git a/resources/js/customers/edit.js b/resources/js/customers/edit.js new file mode 100644 index 0000000..b59b4b8 --- /dev/null +++ b/resources/js/customers/edit.js @@ -0,0 +1,65 @@ +class UpdateCustomer { + constructor() { + this.initUpdateSpatial(); + } + + initUpdateSpatial() { + const toastNotification = document.getElementById("toastNotification"); + const toast = new bootstrap.Toast(toastNotification); + document + .getElementById("btnUpdateCustomer") + .addEventListener("click", async function () { + let submitButton = this; + let spinner = document.getElementById("spinner"); + let form = document.getElementById("formUpdateCustomer"); + + if (!form) { + console.error("Form element not found!"); + return; + } + // Get form data + let formData = new FormData(form); + + // Disable button and show spinner + submitButton.disabled = true; + spinner.classList.remove("d-none"); + + try { + let response = await fetch(form.action, { + method: "POST", + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + }, + body: formData, + }); + + if (response.ok) { + let result = await response.json(); + document.getElementById("toast-message").innerText = + result.message; + toast.show(); + setTimeout(() => { + window.location.href = "/data/customers"; + }, 2000); + } else { + let error = await response.json(); + document.getElementById("toast-message").innerText = + error.message; + toast.show(); + console.error("Error:", error); + } + } catch (error) { + console.error("Request failed:", error); + document.getElementById("toast-message").innerText = + error.message; + toast.show(); + } + }); + } +} + +document.addEventListener("DOMContentLoaded", function (e) { + new UpdateCustomer(); +}); diff --git a/resources/js/customers/index.js b/resources/js/customers/index.js new file mode 100644 index 0000000..9141ae5 --- /dev/null +++ b/resources/js/customers/index.js @@ -0,0 +1,151 @@ +import { Grid } from "gridjs/dist/gridjs.umd.js"; +import gridjs from "gridjs/dist/gridjs.umd.js"; +import "gridjs/dist/gridjs.umd.js"; +import GlobalConfig from "../global-config"; +import Swal from "sweetalert2"; + +class Customers { + constructor() { + this.toastMessage = document.getElementById("toast-message"); + this.toastElement = document.getElementById("toastNotification"); + this.toast = new bootstrap.Toast(this.toastElement); + this.table = null; + + // Initialize functions + this.initTableSpatialPlannings(); + this.initEvents(); + } + initEvents() { + document.body.addEventListener("click", async (event) => { + const deleteButton = event.target.closest(".btn-delete-customers"); + if (deleteButton) { + event.preventDefault(); + await this.handleDelete(deleteButton); + } + }); + } + + initTableSpatialPlannings() { + let tableContainer = document.getElementById("table-customers"); + // Create a new Grid.js instance only if it doesn't exist + this.table = new Grid({ + columns: [ + "ID", + "Nomor Pelanggan", + "Nama", + "Kota Pelayanan", + "Alamat", + "Latitude", + "Longitude", + { + name: "Action", + formatter: (cell) => + gridjs.html(` +
+ + + + +
+ `), + }, + ], + pagination: { + limit: 15, + server: { + url: (prev, page) => + `${prev}${prev.includes("?") ? "&" : "?"}page=${ + page + 1 + }`, + }, + }, + sort: true, + search: { + server: { + url: (prev, keyword) => `${prev}?search=${keyword}`, + }, + }, + server: { + url: `${GlobalConfig.apiHost}/api/customers`, + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + "Content-Type": "application/json", + }, + then: (data) => + data.data.map((item) => [ + item.id, + item.nomor_pelanggan, + item.nama, + item.kota_pelayanan, + item.alamat, + item.latitude, + item.longitude, + item.id, + ]), + total: (data) => data.meta.total, + }, + }).render(tableContainer); + } + + async handleDelete(deleteButton) { + const id = deleteButton.getAttribute("data-id"); + + const result = await Swal.fire({ + title: "Are you sure?", + text: "You won't be able to revert this!", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, delete it!", + }); + + if (result.isConfirmed) { + try { + let response = await fetch( + `${GlobalConfig.apiHost}/api/customers/${id}`, + { + method: "DELETE", + credentials: "include", + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + let result = await response.json(); + this.toastMessage.innerText = + result.message || "Deleted successfully!"; + this.toast.show(); + + // Refresh Grid.js table + if (typeof this.table !== "undefined") { + this.table.updateConfig({}).forceRender(); + } + } else { + let error = await response.json(); + console.error("Delete failed:", error); + this.toastMessage.innerText = + error.message || "Delete failed!"; + this.toast.show(); + } + } catch (error) { + console.error("Error deleting item:", error); + this.toastMessage.innerText = "An error occurred!"; + this.toast.show(); + } + } + } +} + +document.addEventListener("DOMContentLoaded", function (e) { + new Customers(); +}); diff --git a/resources/js/customers/upload.js b/resources/js/customers/upload.js new file mode 100644 index 0000000..8133c35 --- /dev/null +++ b/resources/js/customers/upload.js @@ -0,0 +1,78 @@ +import { Dropzone } from "dropzone"; +Dropzone.autoDiscover = false; + +class UploadCustomers { + constructor() { + this.spatialDropzone = null; + this.formElement = document.getElementById("formUploadCustomers"); + this.uploadButton = document.getElementById("submit-upload"); + this.spinner = document.getElementById("spinner"); + if (!this.formElement) { + console.error("Element formUploadCustomers tidak ditemukan!"); + } + } + + init() { + this.initDropzone(); + this.setupUploadButton(); + } + + initDropzone() { + const toastNotification = document.getElementById("toastNotification"); + const toast = new bootstrap.Toast(toastNotification); + var previewTemplate, + dropzonePreviewNode = document.querySelector( + "#dropzone-preview-list" + ); + (dropzonePreviewNode.id = ""), + dropzonePreviewNode && + ((previewTemplate = dropzonePreviewNode.parentNode.innerHTML), + dropzonePreviewNode.parentNode.removeChild(dropzonePreviewNode), + (this.spatialDropzone = new Dropzone(".dropzone", { + url: this.formElement.action, + method: "post", + acceptedFiles: ".xls,.xlsx", + previewTemplate: previewTemplate, + previewsContainer: "#dropzone-preview", + autoProcessQueue: false, + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + }, + init: function () { + this.on("success", function (file, response) { + document.getElementById("toast-message").innerText = + response.message; + toast.show(); + setTimeout(() => { + window.location.href = "/data/customers"; + }, 2000); + }); + this.on("error", function (file, errorMessage) { + document.getElementById("toast-message").innerText = + errorMessage.message; + toast.show(); + this.uploadButton.disabled = false; + this.spinner.classList.add("d-none"); + }); + }, + }))); + } + + setupUploadButton() { + this.uploadButton.addEventListener("click", (e) => { + if (this.spatialDropzone.files.length > 0) { + this.spatialDropzone.processQueue(); + this.uploadButton.disabled = true; + this.spinner.classList.remove("d-none"); + } else { + return; + } + }); + } +} + +document.addEventListener("DOMContentLoaded", function (e) { + new UploadCustomers().init(); +}); diff --git a/resources/views/customers/create.blade.php b/resources/views/customers/create.blade.php new file mode 100644 index 0000000..33cac97 --- /dev/null +++ b/resources/views/customers/create.blade.php @@ -0,0 +1,32 @@ +@extends('layouts.vertical', ['subtitle' => 'Data']) + +@section('content') + +@include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'PDAM']) + + +
+
+
+
+ Back +
+
+
+ @csrf + @include('customers.form') + +
+
+
+
+
+ +@endsection + +@section('scripts') +@vite(['resources/js/customers/create.js']) +@endsection diff --git a/resources/views/customers/edit.blade.php b/resources/views/customers/edit.blade.php new file mode 100644 index 0000000..1cff9ae --- /dev/null +++ b/resources/views/customers/edit.blade.php @@ -0,0 +1,33 @@ +@extends('layouts.vertical', ['subtitle' => 'Data']) + +@section('content') + +@include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'PDAM']) + + +
+
+
+
+ Back +
+
+
+ @csrf + @method('put') + @include('customers.form') + +
+
+
+
+
+ +@endsection + +@section('scripts') +@vite(['resources/js/customers/edit.js']) +@endsection diff --git a/resources/views/customers/form.blade.php b/resources/views/customers/form.blade.php new file mode 100644 index 0000000..c08f165 --- /dev/null +++ b/resources/views/customers/form.blade.php @@ -0,0 +1,24 @@ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/resources/views/customers/index.blade.php b/resources/views/customers/index.blade.php new file mode 100644 index 0000000..bab6ebf --- /dev/null +++ b/resources/views/customers/index.blade.php @@ -0,0 +1,31 @@ +@extends('layouts.vertical', ['subtitle' => 'Data']) + +@section('css') +@vite(['node_modules/gridjs/dist/theme/mermaid.min.css']) +@endsection + +@section('content') + +@include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'PDAM']) + + + +
+
+
+
+
+ Create + Upload +
+
+
+
+
+
+ +@endsection + +@section('scripts') +@vite(['resources/js/customers/index.js']) +@endsection \ No newline at end of file diff --git a/resources/views/customers/upload.blade.php b/resources/views/customers/upload.blade.php new file mode 100644 index 0000000..fb56b8c --- /dev/null +++ b/resources/views/customers/upload.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.vertical', ['subtitle' => 'Data']) + +@section('content') + +@include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'Tata Ruang']) + + +
+
+
+
+
Upload Data
+

+ Please upload a file with the extension .xls or .xlsx with a maximum size of 10 MB. +
+ For .xls and .xlsx files, ensure that the data is contained within a single sheet with the following columns: + Nomor Pelanggan, Kota Pelayanan, Nama, Alamat, Latitude, Longitude +

+
+ +
+ +
+ +
+
+
+ +
+
+
+ +

Drop files here or click to upload.

+
+
+ +
    +
  • + +
    +
    +
    +
    + +
    +
    +
    +
    +
      +
    +

    + +
    +
    +
    + +
    +
    +
    +
  • +
+ +
+
+ +
+
+
+
+
+ +@endsection + +@section('scripts') +@vite(['resources/js/customers/upload.js']) +@endsection \ No newline at end of file diff --git a/resources/views/spatial-plannings/index.blade.php b/resources/views/spatial-plannings/index.blade.php index 44414db..3aa2fcb 100644 --- a/resources/views/spatial-plannings/index.blade.php +++ b/resources/views/spatial-plannings/index.blade.php @@ -9,7 +9,6 @@ @include('layouts.partials/page-title', ['title' => 'Data', 'subtitle' => 'Tata Ruang']) -
diff --git a/routes/api.php b/routes/api.php index 886f4d0..20f9d9c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ 'auth:sanctum'], function (){ Route::delete('/spatial-plannings/{id}', 'destroy')->name('api.spatial-plannings.destroy'); Route::post('/spatial-plannings/upload', 'upload')->name('api.spatial-plannings.upload'); }); -}); - + Route::controller(CustomersController::class)->group( function (){ + Route::get('/customers', 'index')->name('api.customers'); + Route::post('/customers', 'store')->name('api.customers.store'); + Route::put('/customers/{id}', 'update')->name('api.customers.update'); + Route::delete('/customers/{id}', 'destroy')->name('api.customers.destroy'); + Route::post('/customers/upload', 'upload')->name('api.customers.upload'); + }); +}); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index adee5ea..2a57280 100755 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ 'auth'], function(){ Route::get('/spatial-plannings/{spatial_planning_id}/edit', 'edit')->name('spatial-plannings.edit'); Route::get('/spatial-plannings/upload', 'upload')->name('spatial-plannings.upload'); }); + + Route::controller(CustomersController::class)->group( function (){ + Route::get('/customers', 'index')->name('customers'); + Route::get('/customers/create', 'create')->name('customers.create'); + Route::get('/customers/{customer_id}/edit', 'edit')->name('customers.edit'); + Route::get('/customers/upload', 'upload')->name('customers.upload'); + }); }); // Report