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(` +
+ 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
+