diff --git a/app/Http/Controllers/Parking/Operasional/TransaksiParkirController.php b/app/Http/Controllers/Parking/Operasional/TransaksiParkirController.php index 4233745..e1e6664 100644 --- a/app/Http/Controllers/Parking/Operasional/TransaksiParkirController.php +++ b/app/Http/Controllers/Parking/Operasional/TransaksiParkirController.php @@ -27,10 +27,18 @@ public function index() * - tgl_awal / tgl_akhir (Y-m-d): periode kolom tanggal * - status_kendaraan: 1 = masuk, 0 = keluar * - status_transaksi: 0 = casual, 3 = member, 32 = denda + * - sort_by + sort_dir (asc/desc): klik header kolom * - export=csv: unduh hasil filter sebagai CSV */ public function data(Request $request) { + $sortable = [ + 'id', 'no_pol', 'no_barcode', 'id_kendaraan', 'nama_kendaraan', + 'waktu_masuk', 'waktu_keluar', 'tanggal', + 'id_op_masuk', 'id_op_keluar', 'id_pintu_masuk', 'id_pintu_keluar', + 'bayar_keluar', 'status', 'status_transaksi', + ]; + $request->validate([ 'field' => 'nullable|in:id,no_pol,no_barcode', 'q' => 'nullable|string|max:100', @@ -38,6 +46,8 @@ public function data(Request $request) 'tgl_akhir' => 'nullable|date_format:Y-m-d|after_or_equal:tgl_awal', 'status_kendaraan' => 'nullable|in:0,1', 'status_transaksi' => 'nullable|in:0,3,32', + 'sort_by' => 'nullable|in:' . implode(',', $sortable), + 'sort_dir' => 'nullable|in:asc,desc', 'per_page' => 'nullable|integer|min:1|max:500', ]); @@ -53,6 +63,7 @@ public function data(Request $request) return response()->json([ 'status' => true, 'data' => $rows->items(), + 'summary' => $this->summary($request), 'meta' => [ 'current_page' => $rows->currentPage(), 'per_page' => $rows->perPage(), @@ -62,6 +73,45 @@ public function data(Request $request) ]); } + /** + * Rekap sedetail logic report (per segmen) dari filter yang sedang aktif. + * - keluar = status 0 ; tunai = cara_bayar null/0 ; cashless = cara_bayar 3 + * - casual/member/denda/manual/masalah = subset tunai (ikut reportOfficer) + */ + protected function summary(Request $request): array + { + $tunai = "(tp.cara_bayar IS NULL OR tp.cara_bayar = 0)"; + $keluar = "tp.status = 0"; + + $query = $this->filteredQuery($request); + $query->reorder(); + $query->columns = []; + $row = $query + ->selectRaw(" + count(*) as total_rows, + coalesce(sum(tp.bayar_keluar), 0) as total_bayar, + count(case when {$keluar} then 1 end) as keluar_rows, + coalesce(sum(case when {$keluar} then tp.bayar_keluar end), 0) as keluar_bayar, + count(case when {$keluar} and {$tunai} then 1 end) as tunai_rows, + coalesce(sum(case when {$keluar} and {$tunai} then tp.bayar_keluar end), 0) as tunai_bayar, + count(case when {$keluar} and tp.cara_bayar = 3 then 1 end) as cashless_rows, + coalesce(sum(case when {$keluar} and tp.cara_bayar = 3 then tp.bayar_keluar end), 0) as cashless_bayar, + count(case when {$keluar} and {$tunai} and tp.status_transaksi = '0' and tp.id_op_masuk is not null then 1 end) as casual_rows, + coalesce(sum(case when {$keluar} and {$tunai} and tp.status_transaksi = '0' and tp.id_op_masuk is not null then tp.bayar_keluar end), 0) as casual_bayar, + count(case when {$keluar} and {$tunai} and tp.status_transaksi = '3' then 1 end) as member_rows, + coalesce(sum(case when {$keluar} and {$tunai} and tp.status_transaksi = '3' then tp.bayar_keluar end), 0) as member_bayar, + count(case when {$keluar} and {$tunai} and tp.status_transaksi = '32' then 1 end) as denda_rows, + coalesce(sum(case when {$keluar} and {$tunai} and tp.status_transaksi = '32' then tp.bayar_keluar end), 0) as denda_bayar, + count(case when {$keluar} and {$tunai} and tp.id_op_masuk is null then 1 end) as manual_rows, + coalesce(sum(case when {$keluar} and {$tunai} and tp.id_op_masuk is null then tp.bayar_keluar end), 0) as manual_bayar, + count(case when {$keluar} and {$tunai} and tp.status_transaksi = '-1' then 1 end) as masalah_rows, + coalesce(sum(case when {$keluar} and {$tunai} and tp.status_transaksi = '-1' then tp.bayar_keluar end), 0) as masalah_bayar + ") + ->first(); + + return (array) $row; + } + protected function filteredQuery(Request $request) { $query = DB::table('transaksi_parkir as tp') @@ -76,7 +126,12 @@ protected function filteredQuery(Request $request) 'tp.status_transaksi', 'tp.waktu_masuk', 'tp.waktu_keluar', - 'tp.tanggal' + 'tp.tanggal', + 'tp.id_op_masuk', + 'tp.id_op_keluar', + 'tp.id_pintu_masuk', + 'tp.id_pintu_keluar', + 'tp.bayar_keluar' ); // 1. Pencarian: id / no_pol / no_barcode @@ -103,7 +158,13 @@ protected function filteredQuery(Request $request) $query->where('tp.status_transaksi', (int) $request->input('status_transaksi')); } - return $query->orderBy('tp.tanggal', 'desc'); + // 5. Sorting klik header (default: tanggal terbaru) + $sortMap = ['nama_kendaraan' => 'jm.nama']; + $sortBy = $request->input('sort_by', 'tanggal'); + $sortDir = $request->input('sort_dir', 'desc'); + $query->orderBy($sortMap[$sortBy] ?? "tp.{$sortBy}", $sortDir); + + return $query; } protected function exportCsv($query): StreamedResponse @@ -112,7 +173,7 @@ protected function exportCsv($query): StreamedResponse return response()->streamDownload(function () use ($query) { $out = fopen('php://output', 'w'); - fputcsv($out, ['ID', 'No. Pol', 'Barcode', 'Kendaraan', 'Status Kendaraan', 'Status Transaksi', 'Waktu Masuk', 'Waktu Keluar']); + fputcsv($out, ['ID', 'No. Pol', 'Barcode', 'Kendaraan', 'Status Kendaraan', 'Status Transaksi', 'Waktu Masuk', 'Waktu Keluar', 'Op Masuk', 'Op Keluar', 'Pintu Masuk', 'Pintu Keluar', 'Bayar Keluar']); $query->chunk(1000, function ($rows) use ($out) { foreach ($rows as $r) { fputcsv($out, [ @@ -124,6 +185,11 @@ protected function exportCsv($query): StreamedResponse $r->status_transaksi, $r->waktu_masuk, $r->waktu_keluar, + $r->id_op_masuk, + $r->id_op_keluar, + $r->id_pintu_masuk, + $r->id_pintu_keluar, + $r->bayar_keluar, ]); } }); diff --git a/app/Http/Controllers/Parking/Setup/Petugas/MasterPetugasController.php b/app/Http/Controllers/Parking/Setup/Petugas/MasterPetugasController.php new file mode 100644 index 0000000..b1ff256 --- /dev/null +++ b/app/Http/Controllers/Parking/Setup/Petugas/MasterPetugasController.php @@ -0,0 +1,186 @@ +get(); + $levels = DB::table('level_user')->orderBy('level_code')->get(); + + return view('setup.petugas.petugas', compact('petugas', 'levels')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $data = $request->validate($this->rules(), $this->messages()); + // Normalisasi: string kosong jadi null (kolom date/int tidak terima ''). + $data = array_map(fn ($v) => $v === '' ? null : $v, $data); + + $pegawai = Pegawai::create(collect($data)->except('foto')->all()); + + if ($request->hasFile('foto')) { + $this->saveFoto($pegawai->id, $request->file('foto')); + } + + return response()->json([ + 'success' => true, + 'message' => 'Petugas berhasil ditambahkan.', + 'data' => $pegawai->fresh()->makeHidden('foto'), + ], 201); + } + + /** + * Display the specified resource (tanpa binary foto). + */ + public function show($id) + { + $pegawai = Pegawai::select( + 'id', 'nomer', 'nama', 'alamat', 'tgl_lahir', 'tgl_masuk', + 'username', 'password', 'bisalogin', 'status', 'jeniskelamin', + 'agama', 'telepon', 'alamat_lahir', 'alamat_sekarang', 'nip', + 'statusabsen', 'level_pegawai', 'email' + )->findOrFail($id); + + $hasFoto = (bool) DB::table('pegawai')->where('id', $id)->whereNotNull('foto')->exists(); + + return response()->json([ + 'success' => true, + 'data' => $pegawai, + 'has_foto' => $hasFoto, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, $id) + { + $pegawai = Pegawai::findOrFail($id); + + $data = $request->validate($this->rules($pegawai->id), $this->messages()); + // Normalisasi: string kosong jadi null (kolom date/int tidak terima ''). + $data = array_map(fn ($v) => $v === '' ? null : $v, $data); + + // Password kosong = tidak diubah (tetap pakai yang lama, plain-text ikut gaya legacy). + if (empty($data['password'])) { + unset($data['password']); + } + + $pegawai->update(collect($data)->except('foto')->all()); + + if ($request->hasFile('foto')) { + $this->saveFoto($pegawai->id, $request->file('foto')); + } + + return response()->json([ + 'success' => true, + 'message' => 'Petugas berhasil diperbarui.', + 'data' => $pegawai->fresh()->makeHidden('foto'), + ]); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($id) + { + Pegawai::findOrFail($id)->delete(); + + return response()->json(['success' => true, 'message' => 'Petugas berhasil dihapus.']); + } + + /** + * Stream foto dari kolom bytea. Fallback ke placeholder bila kosong. + */ + public function foto($id) + { + $row = DB::table('pegawai')->select('foto')->where('id', $id)->first(); + + $binary = $row?->foto; + if (is_resource($binary)) { + $binary = stream_get_contents($binary); + } + + if (empty($binary)) { + return response()->file(public_path('images/no-image.jpg')); + } + + $mime = (new \finfo(FILEINFO_MIME_TYPE))->buffer($binary) ?: 'image/jpeg'; + + return response($binary, 200, [ + 'Content-Type' => $mime, + 'Content-Length' => strlen($binary), + 'Cache-Control' => 'private, max-age=3600', + ]); + } + + protected function messages(): array + { + return [ + 'nomer.unique' => 'Nomer sudah dipakai petugas lain, ganti dengan yang unik.', + 'username.unique' => 'Username sudah dipakai petugas lain, ganti dengan yang unik.', + 'foto.image' => 'File foto harus berupa gambar.', + 'foto.max' => 'Ukuran foto maksimal 2MB.', + ]; + } + + protected function rules(?int $ignoreId = null): array + { + $uniqueUsername = Rule::unique('pegawai', 'username'); + $uniqueNomer = Rule::unique('pegawai', 'nomer'); + if ($ignoreId) { + $uniqueUsername->ignore($ignoreId, 'id'); + $uniqueNomer->ignore($ignoreId, 'id'); + } + + return [ + 'nomer' => ['required', 'string', 'max:50', $uniqueNomer], + 'nama' => 'required|string|max:50', + 'username' => ['required', 'string', 'max:50', $uniqueUsername], + // Plain-text ikut gaya data legacy. Wajib saat tambah, opsional saat ubah. + 'password' => ($ignoreId ? 'nullable' : 'required') . '|string|max:50', + 'level_pegawai' => 'nullable|string|exists:level_user,level_code', + 'bisalogin' => 'nullable|integer|in:0,1', + 'status' => 'nullable|integer|in:0,1', + 'jeniskelamin' => 'nullable|integer|in:0,1', + 'statusabsen' => 'nullable|integer|in:0,1', + 'agama' => 'nullable|string|max:50', + 'telepon' => 'nullable|string|max:255', + 'alamat' => 'nullable|string|max:100', + 'alamat_lahir' => 'nullable|string|max:255', + 'alamat_sekarang' => 'nullable|string|max:255', + 'nip' => 'nullable|string|max:50', + 'email' => 'nullable|string|max:64', + 'tgl_lahir' => 'nullable|date', + 'tgl_masuk' => 'nullable|date', + 'foto' => 'nullable|image|mimes:jpg,jpeg,png,bmp|max:2048', + ]; + } + + /** + * Simpan binary ke kolom bytea tanpa nambah kolom baru. + * bin2hex menghindari masalah escaping bytea. + */ + protected function saveFoto(int $id, $file): void + { + $binary = file_get_contents($file->getRealPath()); + + DB::table('pegawai')->where('id', $id)->update([ + 'foto' => DB::raw("decode('" . bin2hex($binary) . "', 'hex')"), + ]); + } +} diff --git a/app/Http/Controllers/Parking/Setup/Vehicle/ModifiedVehiclepriceController.php b/app/Http/Controllers/Parking/Setup/Vehicle/ModifiedVehiclepriceController.php index f2ce4ef..0e5a34b 100644 --- a/app/Http/Controllers/Parking/Setup/Vehicle/ModifiedVehiclepriceController.php +++ b/app/Http/Controllers/Parking/Setup/Vehicle/ModifiedVehiclepriceController.php @@ -115,13 +115,6 @@ public function update(Request $request, $id) $data['denda_only'] = $request->boolean('denda_only') ? 1 : 0; $dataQuery->fill($data); - - if (!$dataQuery->isDirty()) { - return response()->json([ - "status" => false, - "message" => "Tidak ada perubahan data", - ]); - } $dataQuery->save(); return response()->json([ diff --git a/app/Models/Pegawai.php b/app/Models/Pegawai.php index 12c9858..6ae1737 100644 --- a/app/Models/Pegawai.php +++ b/app/Models/Pegawai.php @@ -10,6 +10,7 @@ class Pegawai extends Model implements AuthenticatableContract { protected $table = 'pegawai'; // Nama tabel protected $primaryKey = 'id'; // Primary key tabel + public $timestamps = false; // Tabel tidak punya created_at / updated_at protected $fillable = [ 'nomer', 'nama', 'alamat', 'tgl_lahir', 'tgl_masuk', 'username', 'password', 'foto', 'bisalogin', 'status', 'jeniskelamin', 'agama', 'telepon', 'alamat_lahir', 'alamat_sekarang', 'nip', 'statusabsen', 'level_pegawai' diff --git a/app/Services/MakeStikerServices.php b/app/Services/MakeStikerServices.php index 36f7d05..bdb1b49 100755 --- a/app/Services/MakeStikerServices.php +++ b/app/Services/MakeStikerServices.php @@ -77,7 +77,7 @@ public function stikerCreate($request, Carbon $awal, Carbon $akhir) if (!empty($dataCard)) { foreach ($dataCard as $cardNumber) { $insertCard = [ - "notrans" => $cardNumber["no_id"], + "notrans" => $cardNumber["notrans"], "no_card" => $cardNumber["nocard"], "no_urut" => $cardNumber["nourut"], ]; @@ -99,7 +99,7 @@ public function stikerCreate($request, Carbon $awal, Carbon $akhir) "status" => true, "message" => "Data berhasil di simpan", ]); - } catch (Exception $e) { + } catch (\Exception $e) { return response()->json([ "status" => false, "message" => "Gagal menyimpan data " . $e->getMessage() . $e->getLine(), diff --git a/public/js/member/member-register.js b/public/js/member/member-register.js index 62dd3a2..87bd6c8 100644 --- a/public/js/member/member-register.js +++ b/public/js/member/member-register.js @@ -108,7 +108,7 @@ $(document).ready(function () { $(".nopol-add").next(".invalid-feedback").hide(); if (!vehicle) { - alert("Wajib pilih jenis kendaraan"); + Swal.fire({icon: 'warning', title: 'Belum lengkap', text: "Wajib pilih jenis kendaraan"}); return; } @@ -122,7 +122,7 @@ $(document).ready(function () { var rowhtml = ` ${nopol} - ${vehicleName} + ${vehicleName} ${member} ${merk} @@ -144,8 +144,8 @@ $(document).ready(function () { console.log(r.message); } }, - error: function () { - alert("Terjadi kesalahan saat memeriksa nopol"); + error: function (xhr) { + Swal.fire({icon: 'error', title: 'Gagal memeriksa', text: xhr.responseJSON?.message || "Terjadi kesalahan saat memeriksa nopol"}); }, }); }); @@ -246,6 +246,9 @@ $(document).ready(function () { console.log("FORM ADD:", allData); if ($(".tambah-member").valid()) { + const saveBtn = tombol; + const saveHtml = saveBtn.html(); + saveBtn.prop('disabled', true).html('Menyimpan...'); $.ajax({ url: $("#add-member").attr("action"), type: "POST", @@ -255,10 +258,11 @@ $(document).ready(function () { // tombol.attr("disabled", true); }, success: function (r) { - // alert('Data berhasil dikirim'); - if (r.response == true) { + const ok = r.status == true || r.response == true; + if (ok) { $("#modal-member").modal("hide"); Swal.fire({ + icon: "success", title: "Berhasil!!", text: "Member berhasil di daftarkan.. !!", timer: 5000, @@ -267,7 +271,7 @@ $(document).ready(function () { // Reload halaman setelah SweetAlert ditutup // location.reload(); // Ini akan me-reload halaman }); - } else if (r.response == false) { + } else { Swal.fire({ icon: "error", title: "Oops...", @@ -277,7 +281,10 @@ $(document).ready(function () { console.log(r); }, error: function (xhr, status, error) { - alert("Terjadi kesalahan: " + error); + Swal.fire({icon: 'error', title: 'Gagal menyimpan', text: xhr.responseJSON?.message || ("Terjadi kesalahan: " + error)}); + }, + complete: function () { + saveBtn.prop('disabled', false).html(saveHtml); }, }); } diff --git a/public/js/setup/gate.js b/public/js/setup/gate.js index 044d64c..7cf1791 100644 --- a/public/js/setup/gate.js +++ b/public/js/setup/gate.js @@ -137,7 +137,7 @@ }); $('.btn-cancel').click(); - alert('Berhasil disimpan 🔥'); + if (window.Swal) Swal.fire({icon: 'success', title: 'Berhasil', text: 'Konfigurasi hardware tersimpan.'}); } }); }); diff --git a/public/js/setup/vehicle.js b/public/js/setup/vehicle.js index c99345f..03f41fc 100644 --- a/public/js/setup/vehicle.js +++ b/public/js/setup/vehicle.js @@ -34,9 +34,13 @@ $(document).ready(function () { $(document).on('click', '.vehicle-price', function () { - let id = $(this).data('id'); - let nama = $(this).data('nama'); + let btn = $(this); + let btnHtml = btn.html(); + let id = btn.data('id'); + let nama = btn.data('nama'); console.log(id); + + btn.prop('disabled', true).html('Memuat...'); $.ajax({ url: '/setup/master/price/' + id, @@ -70,6 +74,9 @@ $(document).ready(function () { }, error: function (xhr) { Swal.fire({icon: 'error', title: 'Gagal memuat tarif', text: xhr.responseJSON?.message || 'Data tarif tidak ditemukan.'}); + }, + complete: function () { + btn.prop('disabled', false).html(btnHtml); } }); }); @@ -105,6 +112,10 @@ $(document).ready(function () { url = url.replace(':id', encodeURIComponent(id)); console.log(url); + const saveBtn = $(this); + const saveHtml = saveBtn.html(); + saveBtn.prop('disabled', true).html('Menyimpan...'); + $.ajax({ url: url, type: 'PUT', @@ -126,6 +137,9 @@ $(document).ready(function () { return; } Swal.fire({icon: 'error', title: 'Gagal memperbarui tarif', text: xhr.responseJSON?.message || 'Silakan periksa kembali data tarif.'}); + }, + complete: function() { + saveBtn.prop('disabled', false).html(saveHtml); } }); }); diff --git a/public/js/stikerMember/member-register.js b/public/js/stikerMember/member-register.js index 0620b95..ce8de25 100644 --- a/public/js/stikerMember/member-register.js +++ b/public/js/stikerMember/member-register.js @@ -108,7 +108,7 @@ $(document).ready(function () { $(".nopol-add").next(".invalid-feedback").hide(); if (!vehicle) { - alert("Wajib pilih jenis kendaraan"); + Swal.fire({icon: 'warning', title: 'Belum lengkap', text: "Wajib pilih jenis kendaraan"}); return; } @@ -122,7 +122,7 @@ $(document).ready(function () { var rowhtml = ` ${nopol} - ${vehicleName} + ${vehicleName} ${member} ${merk} @@ -144,8 +144,8 @@ $(document).ready(function () { console.log(r.message); } }, - error: function () { - alert("Terjadi kesalahan saat memeriksa nopol"); + error: function (xhr) { + Swal.fire({icon: 'error', title: 'Gagal memeriksa', text: xhr.responseJSON?.message || "Terjadi kesalahan saat memeriksa nopol"}); }, }); }); @@ -244,21 +244,23 @@ $(document).ready(function () { }; if ($(".tambah-member").valid()) { + const saveHtml = tombol.html(); $.ajax({ url: $("#add-member").attr("action"), type: "POST", data: allData, headers: { "X-CSRF-token": $('meta[name="csrf-token"]').attr("content") }, beforeSend: function () { - tombol.attr("disabled", true); + tombol.prop("disabled", true).html('Menyimpan...'); }, success: function (r) { // alert('Data berhasil dikirim'); $("#modal-member").modal("hide"); if (r.status == true) { Swal.fire({ + icon: "success", title: "Berhasil!!", - text: "Member anda telah terdaftar !!", + text: r.message || "Member anda telah terdaftar !!", timer: 5000, showConfirmButton: !1, }).then(function () { @@ -266,12 +268,15 @@ $(document).ready(function () { location.reload(); // Ini akan me-reload halaman }); } else if (r.status == false) { - Swal.fire("Oops...", "Terjadi kendala !!", "silahkan hubungi admin", "error"); + Swal.fire({icon: "error", title: "Oops...", text: r.message || "Terjadi kendala, silakan hubungi admin."}); } console.log(r); }, error: function (xhr, status, error) { - alert("Terjadi kesalahan: " + error); + Swal.fire({icon: 'error', title: 'Gagal menyimpan', text: xhr.responseJSON?.message || ("Terjadi kesalahan: " + error)}); + }, + complete: function () { + tombol.prop('disabled', false).html(saveHtml); }, }); } diff --git a/resources/views/Parking/setup/member/stasiunkerja.blade.php b/resources/views/Parking/setup/member/stasiunkerja.blade.php index d83f4b6..69d8a55 100644 --- a/resources/views/Parking/setup/member/stasiunkerja.blade.php +++ b/resources/views/Parking/setup/member/stasiunkerja.blade.php @@ -139,5 +139,6 @@ // tidak hardcode URL (menghindari masalah beda host/port seperti sebelumnya). window.STASIUNKERJA_ENDPOINT = "{{ url('/setup/master/stasiunkerja') }}"; + @endsection diff --git a/resources/views/Parking/setup/member/tarifstiker.blade.php b/resources/views/Parking/setup/member/tarifstiker.blade.php index a312843..410c8c8 100644 --- a/resources/views/Parking/setup/member/tarifstiker.blade.php +++ b/resources/views/Parking/setup/member/tarifstiker.blade.php @@ -146,5 +146,6 @@ + @endsection diff --git a/resources/views/layouts/partial/horizontal-menu.blade.php b/resources/views/layouts/partial/horizontal-menu.blade.php index 88d6936..a8e6d68 100644 --- a/resources/views/layouts/partial/horizontal-menu.blade.php +++ b/resources/views/layouts/partial/horizontal-menu.blade.php @@ -29,7 +29,7 @@
- +
diff --git a/resources/views/operasional/transaksi-parkir.blade.php b/resources/views/operasional/transaksi-parkir.blade.php index ad19e4c..b105877 100644 --- a/resources/views/operasional/transaksi-parkir.blade.php +++ b/resources/views/operasional/transaksi-parkir.blade.php @@ -25,6 +25,17 @@ .table td { text-align: right; } */ + #tabel-parkir th[data-sort] { + cursor: pointer; + white-space: nowrap; + user-select: none; + } + #tabel-parkir th[data-sort]:hover { + background-color: #f2f6ff; + } + #tabel-parkir .sort-ind { + font-size: 11px; + } @endsection @@ -117,6 +128,17 @@ class="btn btn-sm btn-icon btn-danger btn-export-pdf" title="Cetak">
+
+
Total (filter aktif)
-
+
Keluar (status 0)
-
+
Tunai
-
+
Cashless
-
+
Casual
-
+
Member
-
+
Denda
-
+
Manual
-
+
Masalah (-1)
-
+
Hasil Transaksi
@@ -126,18 +148,23 @@ class="btn btn-sm btn-icon btn-danger btn-export-pdf" title="Cetak"> No. - ID - No. Pol - Barcode - Kendaraan - Waktu Masuk - Waktu Keluar - Status Kendaraan - Status Transaksi + ID + No. Pol + Barcode + Kendaraan + Waktu Masuk + Waktu Keluar + Op Masuk + Op Keluar + Pintu Masuk + Pintu Keluar + Bayar Keluar + Status Kendaraan + Status Transaksi - Memuat data... + Memuat data...
@@ -162,6 +189,8 @@ class="btn btn-sm btn-icon btn-danger btn-export-pdf" title="Cetak"> $(function () { const dataUrl = '{{ route('transaksiparkir.data', [], false) }}'; let page = 1; + let sortBy = 'tanggal'; + let sortDir = 'desc'; const labelKendaraan = {1: 'Masuk', 0: 'Keluar'}; const labelTransaksi = {0: 'Casual', 3: 'Member', 32: 'Denda'}; @@ -196,23 +225,62 @@ function params(extra) { tgl_akhir: $('#tgl_akhir').val(), status_kendaraan: $('#status_kendaraan').val(), status_transaksi: $('#status_transaksi').val(), + sort_by: sortBy, + sort_dir: sortDir, page: page }; return $.param(Object.assign(p, extra || {})); } + function paintSort() { + $('#tabel-parkir th[data-sort] .sort-ind').text(''); + const th = $('#tabel-parkir th[data-sort="' + sortBy + '"] .sort-ind'); + if (th.length) th.text(sortDir === 'asc' ? '▲' : '▼'); + } + + $('#tabel-parkir').on('click', 'th[data-sort]', function () { + const col = $(this).data('sort'); + if (sortBy === col) { + sortDir = sortDir === 'asc' ? 'desc' : 'asc'; + } else { + sortBy = col; + sortDir = 'asc'; + } + page = 1; + paintSort(); + load(); + }); + + function fmtRp(v) { + if (v === null || v === undefined || v === '') return ''; + return 'Rp ' + Number(v).toLocaleString('id-ID'); + } + + function paintSummary(s) { + if (!s) return; + const segs = ['total', 'keluar', 'tunai', 'cashless', 'casual', 'member', 'denda', 'manual', 'masalah']; + segs.forEach(function (k) { + $('#sum-' + k).text((Number(s[k + '_rows']) || 0).toLocaleString('id-ID') + ' trx'); + $('#sum-' + k + '-rp').text(fmtRp(s[k + '_bayar'])); + }); + } + function load() { - $('#body-parkir').html('Memuat data...'); + $('#body-parkir').html('Memuat data...'); + $('#btn-cari').prop('disabled', true).html('Mencari...'); + $('#btn-prev, #btn-next').prop('disabled', true); $.ajax({ url: dataUrl + '?' + params(), type: 'GET', success: function (r) { + paintSummary(r.summary); if (!r.data.length) { - $('#body-parkir').html('Data tidak ditemukan.'); + $('#body-parkir').html('Data tidak ditemukan.'); } else { let html = ''; const offset = (r.meta.current_page - 1) * r.meta.per_page; r.data.forEach(function (row, i) { + const bayar = row.bayar_keluar === null || row.bayar_keluar === undefined || row.bayar_keluar === '' ? '' : Number(row.bayar_keluar).toLocaleString('id-ID'); html += '' + '' + (offset + i + 1) + '' + '' + esc(row.id) + '' @@ -221,6 +289,11 @@ function load() { + '' + esc([row.id_kendaraan, row.nama_kendaraan].filter(Boolean).join(' - ')) + '' + '' + esc(row.waktu_masuk) + '' + '' + esc(row.waktu_keluar) + '' + + '' + esc(row.id_op_masuk) + '' + + '' + esc(row.id_op_keluar) + '' + + '' + esc(row.id_pintu_masuk) + '' + + '' + esc(row.id_pintu_keluar) + '' + + '' + bayar + '' + '' + statusKendaraan(row.status) + '' + '' + statusTransaksi(row.status_transaksi) + '' + ''; @@ -238,7 +311,11 @@ function load() { if (xhr.status === 422 && xhr.responseJSON?.errors) { msg = Object.values(xhr.responseJSON.errors).flat().join(' '); } - $('#body-parkir').html('' + esc(msg) + ''); + $('#body-parkir').html('' + esc(msg) + ''); + Swal.fire({icon: 'error', title: 'Gagal memuat data', text: msg}); + }, + complete: function () { + $('#btn-cari').prop('disabled', false).html('Cari'); } }); } @@ -258,6 +335,7 @@ function load() { }); $('.btn-export-pdf').on('click', function () { window.print(); }); + paintSort(); load(); }); diff --git a/resources/views/setup/UserLevel/level-user.blade.php b/resources/views/setup/UserLevel/level-user.blade.php index 278f40c..5409067 100644 --- a/resources/views/setup/UserLevel/level-user.blade.php +++ b/resources/views/setup/UserLevel/level-user.blade.php @@ -178,7 +178,7 @@ }); $('.btn-cancel').click(); - alert('Berhasil disimpan 🔥'); + Swal.fire({icon: 'success', title: 'Berhasil', text: 'Level pengguna tersimpan.'}); } }); }); diff --git a/resources/views/setup/gate/gate.blade.php b/resources/views/setup/gate/gate.blade.php index f0be237..08c0f56 100644 --- a/resources/views/setup/gate/gate.blade.php +++ b/resources/views/setup/gate/gate.blade.php @@ -155,6 +155,8 @@ $(document).ready(function () { $(document).on('click', '.hardware-button', function () { + let btn = $(this); + let btnHtml = btn.html(); let row = $(this).closest('tr'); // kalau sudah ada baris edit, hapus dulu @@ -166,6 +168,8 @@ // # base url tanpa parameter biar tidak dobel slash let baseUrl = "{{ url('setup/master/gate-hardware') }}"; + btn.prop('disabled', true).html('Memuat...'); + $.ajax({ url: baseUrl + "/" + id, type: 'GET', @@ -244,6 +248,10 @@ class="form-control form-control-sm edit-mode d-none" }, error: function (xhr) { console.error(xhr.responseText); + Swal.fire({icon: 'error', title: 'Gagal memuat hardware', text: xhr.responseJSON?.message || 'Cek koneksi / hubungi admin.'}); + }, + complete: function () { + btn.prop('disabled', false).html(btnHtml); } }); }); @@ -275,6 +283,8 @@ class="form-control form-control-sm edit-mode d-none" // SAVE $(document).on('click', '.btn-save', function () { + const btn = $(this); + const origHtml = btn.html(); let dataToSend = {}; let gateId = $('.edit-row').data('id'); @@ -289,6 +299,8 @@ class="form-control form-control-sm edit-mode d-none" // # base url tanpa parameter biar tidak dobel slash let baseUrl = "{{ url('setup/master/gate-hardware') }}"; + btn.prop('disabled', true).html('Menyimpan...'); + $.ajax({ url: baseUrl + "/" + gateId, type: "POST", @@ -308,11 +320,14 @@ class="form-control form-control-sm edit-mode d-none" }); $('.btn-cancel').click(); - alert('Berhasil disimpan'); + Swal.fire({icon: 'success', title: 'Berhasil', text: 'Konfigurasi hardware tersimpan.'}); }, error: function (xhr) { console.error(xhr.responseText); - alert('Gagal menyimpan. Cek koneksi / hubungi admin.'); + Swal.fire({icon: 'error', title: 'Gagal menyimpan', text: xhr.responseJSON?.message || 'Cek koneksi / hubungi admin.'}); + }, + complete: function () { + btn.prop('disabled', false).html(origHtml); } }); }); diff --git a/resources/views/setup/index.blade.php b/resources/views/setup/index.blade.php index a77d689..b5bb204 100644 --- a/resources/views/setup/index.blade.php +++ b/resources/views/setup/index.blade.php @@ -598,6 +598,7 @@ class="btn btn-primary" logsUrl: @json(route('dhcp.logs')), }; + @endsection diff --git a/resources/views/setup/petugas/petugas.blade.php b/resources/views/setup/petugas/petugas.blade.php new file mode 100644 index 0000000..1bd2b33 --- /dev/null +++ b/resources/views/setup/petugas/petugas.blade.php @@ -0,0 +1,286 @@ +@extends('layouts.master') + +@section('styles') + +@endsection + +@section('content') +
+
+
+
+
+
+
+
+

Master Petugas / Pegawai

+ +
+
+ + + + + + + + + + + + + + + @forelse ($petugas as $p) + + + + + + + + + + + @empty + + @endforelse + +
FotoNomerNamaUsernameLevelBisa LoginStatusAksi
+ foto + {{ $p->nomer }}{{ $p->nama }}{{ $p->username }}{{ $levels->firstWhere('level_code', $p->level_pegawai)->level_name ?? $p->level_pegawai }} + + {{ $p->bisalogin == 1 ? 'Ya' : 'Tidak' }} + + + + {{ $p->status == 1 ? 'Aktif' : 'Nonaktif' }} + + + + + +
Belum ada data petugas.
+
+
+
+
+
+
+
+ + + + + + +
+@endsection + +@section('javascript') + + + +@endsection diff --git a/resources/views/setup/vehicle/vehicle.blade.php b/resources/views/setup/vehicle/vehicle.blade.php index 635089e..cee086d 100644 --- a/resources/views/setup/vehicle/vehicle.blade.php +++ b/resources/views/setup/vehicle/vehicle.blade.php @@ -173,11 +173,21 @@ function formRow(mode, vehicle) { $(document).on('submit', '.vehicle-inline-form', function (event) { event.preventDefault(); const form = $(this); const id = form.data('id'); const edit = form.data('mode') === 'edit'; - $.ajax({ url: edit ? routes.update.replace(':id', encodeURIComponent(id)) : routes.store, type: edit ? 'PUT' : 'POST', data: form.serialize(), headers: {'X-CSRF-TOKEN': csrf}, success: () => window.location.reload(), error: xhr => alert(xhr.responseJSON?.message || Object.values(xhr.responseJSON?.errors || {}).flat().join('\n') || 'Gagal menyimpan kendaraan.') }); + const btn = form.find('[type="submit"]'); const btnHtml = btn.html(); + btn.prop('disabled', true).html('Menyimpan...'); + $.ajax({ + url: edit ? routes.update.replace(':id', encodeURIComponent(id)) : routes.store, + type: edit ? 'PUT' : 'POST', data: form.serialize(), headers: {'X-CSRF-TOKEN': csrf}, + success: () => window.location.reload(), + error: xhr => { + btn.prop('disabled', false).html(btnHtml); + Swal.fire({icon: 'error', title: 'Gagal menyimpan', text: xhr.responseJSON?.message || Object.values(xhr.responseJSON?.errors || {}).flat().join('\n') || 'Gagal menyimpan kendaraan.'}); + } + }); }); - $('#btnDeleteVehicle').on('click', function () { if (!selected) return; Swal.fire({title:'Hapus kendaraan?', text:`${selected.vehicleName} beserta tarif terkait akan dihapus.`, icon:'warning', showCancelButton:true, confirmButtonText:'Ya, hapus', cancelButtonText:'Batal', confirmButtonColor:'#dc3545'}).then(result => { if (!result.isConfirmed) return; $.ajax({url: routes.destroy.replace(':id', encodeURIComponent(selected.vehicleId)), type:'DELETE', headers:{'X-CSRF-TOKEN':csrf}, success:() => window.location.reload(), error:xhr => Swal.fire({icon:'error', title:'Gagal menghapus', text:xhr.responseJSON?.message || 'Kendaraan tidak dapat dihapus.'})}); }); }); - $('#btnDetail').on('click', function () { if (!selected) return; $('#vehicle-detail-body').html('
Memuat detail...
'); new bootstrap.Modal(document.getElementById('vehicle-detail-modal')).show(); $.get(routes.show.replace(':id', encodeURIComponent(selected.vehicleId)), function(r) { const t = r.tarif; $('#vehicle-detail-body').html(`
ID Kendaraan
${r.data.id}
Nama
${r.data.nama}
Shortcut
${r.data.short_cut}
Tarif Awal
${t ? Number(t.tarif || 0).toLocaleString('id-ID') : '-'}
Tarif Interval
${t ? Number(t.tarif_interval || 0).toLocaleString('id-ID') : '-'}
Waktu Gratis
${t?.waktu_gratis ?? '-' } menit
`); }); }); + $('#btnDeleteVehicle').on('click', function () { if (!selected) return; Swal.fire({title:'Hapus kendaraan?', text:`${selected.vehicleName} beserta tarif terkait akan dihapus.`, icon:'warning', showCancelButton:true, confirmButtonText:'Ya, hapus', cancelButtonText:'Batal', confirmButtonColor:'#dc3545', showLoaderOnConfirm: true, preConfirm: () => $.ajax({url: routes.destroy.replace(':id', encodeURIComponent(selected.vehicleId)), type:'DELETE', headers:{'X-CSRF-TOKEN':csrf}}).catch(xhr => { Swal.showValidationMessage(xhr.responseJSON?.message || 'Kendaraan tidak dapat dihapus.'); })}).then(result => { if (result.isConfirmed && result.value) { Swal.fire({icon: 'success', title: 'Berhasil', text: 'Kendaraan dihapus.', timer: 1500, showConfirmButton: false}).then(() => window.location.reload()); } }); }); + $('#btnDetail').on('click', function () { if (!selected) return; const btn = $(this); const btnHtml = btn.html(); btn.prop('disabled', true).html('Memuat...'); $('#vehicle-detail-body').html('
Memuat detail...
'); new bootstrap.Modal(document.getElementById('vehicle-detail-modal')).show(); $.get(routes.show.replace(':id', encodeURIComponent(selected.vehicleId)), function(r) { const t = r.tarif; $('#vehicle-detail-body').html(`
ID Kendaraan
${r.data.id}
Nama
${r.data.nama}
Shortcut
${r.data.short_cut}
Tarif Awal
${t ? Number(t.tarif || 0).toLocaleString('id-ID') : '-'}
Tarif Interval
${t ? Number(t.tarif_interval || 0).toLocaleString('id-ID') : '-'}
Waktu Gratis
${t?.waktu_gratis ?? '-' } menit
`); }).fail(xhr => { $('#vehicle-detail-body').html('
' + (xhr.responseJSON?.message || 'Gagal memuat detail.') + '
'); }).always(() => btn.prop('disabled', false).html(btnHtml)); }); }); diff --git a/resources/views/stiker/extend-stiker.blade.php b/resources/views/stiker/extend-stiker.blade.php index 97ed96a..cb98b62 100644 --- a/resources/views/stiker/extend-stiker.blade.php +++ b/resources/views/stiker/extend-stiker.blade.php @@ -54,7 +54,7 @@
- +
diff --git a/resources/views/stiker/stiker.blade.php b/resources/views/stiker/stiker.blade.php index b824f0a..b644a08 100644 --- a/resources/views/stiker/stiker.blade.php +++ b/resources/views/stiker/stiker.blade.php @@ -39,6 +39,33 @@ .table-selectable tbody tr.selected td { color: #fff !important; } + + /* Tegaskan garis input di modal member (tambah/ubah/extend/detail). + Cuma warna border (tetap 1px), layout tidak berubah. */ + #modal-member .form-control, + #modal-member .form-select, + #modal-extend .form-control, + #modal-extend .form-select, + #ubah-member .form-control, + #ubah-member .form-select, + #modal-details .form-control { + border-color: #9aa5b1; + } + + /* Template global bikin border .table-bordered 5px — balikin ke 1px + khusus tabel di modal member. */ + #modal-member .table-bordered > :not(caption) > *, + #modal-extend .table-bordered > :not(caption) > *, + #ubah-member .table-bordered > :not(caption) > *, + #modal-details .table-bordered > :not(caption) > * { + border-width: 1px 0; + } + #modal-member .table-bordered > :not(caption) > * > *, + #modal-extend .table-bordered > :not(caption) > * > *, + #ubah-member .table-bordered > :not(caption) > * > *, + #modal-details .table-bordered > :not(caption) > * > * { + border-width: 0 1px; + } @endsection diff --git a/resources/views/stiker/tambah-stiker.blade.php b/resources/views/stiker/tambah-stiker.blade.php index 3efb164..3b53cfa 100644 --- a/resources/views/stiker/tambah-stiker.blade.php +++ b/resources/views/stiker/tambah-stiker.blade.php @@ -54,7 +54,7 @@
- +
diff --git a/resources/views/stiker/ubah-stiker.blade.php b/resources/views/stiker/ubah-stiker.blade.php index e3014c4..c0f1bf5 100644 --- a/resources/views/stiker/ubah-stiker.blade.php +++ b/resources/views/stiker/ubah-stiker.blade.php @@ -49,7 +49,7 @@
- +
diff --git a/routes/web.php b/routes/web.php index 0dd03a1..c5de9b1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,6 +12,7 @@ use App\Http\Controllers\Parking\Setup\Gate\MasterGateParameterController; use App\Http\Controllers\Parking\Setup\Vehicle\MasterVehicleController; use App\Http\Controllers\Parking\Setup\Vehicle\ModifiedVehiclepriceController; +use App\Http\Controllers\Parking\Setup\Petugas\MasterPetugasController; use App\Http\Controllers\Parking\Setup\UserLevel\LevelUserController; use App\Http\Controllers\Parking\Setup\UserLevel\LevelMenuController; use App\Http\Controllers\Parking\Setup\Member\StasiunKerjaController; @@ -81,6 +82,8 @@ Route::get("/dhcp/logs", [MasterConfigNetworkController::class, "logs"])->name("dhcp.logs"); Route::resource("vehicle", MasterVehicleController::class); Route::resource("price", ModifiedVehiclepriceController::class)->names("vehiclePrice"); + Route::get("petugas/{petugas}/foto", [MasterPetugasController::class, "foto"])->name("petugas.foto"); + Route::resource("petugas", MasterPetugasController::class)->names("petugas"); Route::resource("level-user", LevelUserController::class)->names("levelUser"); Route::resource("level-menu", LevelMenuController::class)->names("levelMenu"); Route::get("stasiunkerja/data", [StasiunKerjaController::class, "data"])->name("stasiunkerja.data");