CRUD petugas/pegawai + filter-sort-rekap transaksi parkir + fix save member kartu + hilangkan alert ganti Swal-loading

This commit is contained in:
2026-10-14 11:21:07 +07:00
parent ee1f43977a
commit f3818f03f5
23 changed files with 747 additions and 53 deletions
@@ -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,
]);
}
});
@@ -0,0 +1,186 @@
<?php
namespace App\Http\Controllers\Parking\Setup\Petugas;
use App\Http\Controllers\Controller;
use App\Models\Pegawai;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
class MasterPetugasController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$petugas = Pegawai::orderBy('nomer')->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')"),
]);
}
}
@@ -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([
+1
View File
@@ -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'
+2 -2
View File
@@ -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(),
+15 -8
View File
@@ -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 = `
<tr>
<td class="text-center">${nopol}</td>
<td class="text-center" data-value="${vehicle}"">${vehicleName}</td>
<td class="text-center" data-value="${vehicle}">${vehicleName}</td>
<td class="text-center">${member}</td>
<td class="text-center">${merk}</td>
<td class="text-center p-1" style="text-align: center;">
@@ -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('<span class="spinner-border spinner-border-sm me-1"></span>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);
},
});
}
+1 -1
View File
@@ -137,7 +137,7 @@
});
$('.btn-cancel').click();
alert('Berhasil disimpan 🔥');
if (window.Swal) Swal.fire({icon: 'success', title: 'Berhasil', text: 'Konfigurasi hardware tersimpan.'});
}
});
});
+16 -2
View File
@@ -34,10 +34,14 @@ $(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('<span class="spinner-border spinner-border-sm me-1"></span>Memuat...');
$.ajax({
url: '/setup/master/price/' + id,
type: 'GET',
@@ -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('<span class="spinner-border spinner-border-sm me-1"></span>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);
}
});
});
+13 -8
View File
@@ -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 = `
<tr>
<td class="text-center">${nopol}</td>
<td class="text-center" data-value="${vehicle}"">${vehicleName}</td>
<td class="text-center" data-value="${vehicle}">${vehicleName}</td>
<td class="text-center">${member}</td>
<td class="text-center">${merk}</td>
<td class="text-center p-1" style="text-align: center;">
@@ -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('<span class="spinner-border spinner-border-sm me-1"></span>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);
},
});
}
@@ -139,5 +139,6 @@
// tidak hardcode URL (menghindari masalah beda host/port seperti sebelumnya).
window.STASIUNKERJA_ENDPOINT = "{{ url('/setup/master/stasiunkerja') }}";
</script>
<script src="{{ asset('vendor/sweetalert2/dist/sweetalert2.min.js') }}"></script>
<script src="{{ asset('js/setup/stasiunkerja.js') }}"></script>
@endsection
@@ -146,5 +146,6 @@
<script>
window.TARIFSTIKER_ENDPOINT = "{{ url('/setup/master/tarif-stiker') }}";
</script>
<script src="{{ asset('vendor/sweetalert2/dist/sweetalert2.min.js') }}"></script>
<script src="{{ asset('js/setup/tarifstiker.js') }}"></script>
@endsection
@@ -29,7 +29,7 @@
<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Shift</a></li>
<hr>
<li class="nav-item"><a class="nav-link" href="/setup/master/level-user">Master Level Pengguna</a></li>
<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Petugas/Pegawai</a></li>
<li class="nav-item"><a class="nav-link" href="/setup/master/petugas">Master Petugas/Pegawai</a></li>
<hr>
<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Stiker</a></li>
</ul>
@@ -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;
}
</style>
@endsection
@@ -117,6 +128,17 @@ class="btn btn-sm btn-icon btn-danger btn-export-pdf" title="Cetak">
</div>
</form>
<hr class="my-3">
<div class="row g-2 mb-3" id="rekap-parkir">
<div class="col-md-3"><div class="border rounded p-2 h-100"><small class="text-muted">Total (filter aktif)</small><div class="fw-bold" id="sum-total">-</div><small class="text-muted" id="sum-total-rp"></small></div></div>
<div class="col-md-3"><div class="border rounded p-2 h-100"><small class="text-muted">Keluar (status 0)</small><div class="fw-bold" id="sum-keluar">-</div><small class="text-muted" id="sum-keluar-rp"></small></div></div>
<div class="col-md-3"><div class="border rounded p-2 h-100"><small class="text-muted">Tunai</small><div class="fw-bold text-success" id="sum-tunai">-</div><small class="text-muted" id="sum-tunai-rp"></small></div></div>
<div class="col-md-3"><div class="border rounded p-2 h-100"><small class="text-muted">Cashless</small><div class="fw-bold text-info" id="sum-cashless">-</div><small class="text-muted" id="sum-cashless-rp"></small></div></div>
<div class="col-md-2"><div class="border rounded p-2 h-100"><small class="text-muted">Casual</small><div class="fw-bold" id="sum-casual">-</div><small class="text-muted" id="sum-casual-rp"></small></div></div>
<div class="col-md-2"><div class="border rounded p-2 h-100"><small class="text-muted">Member</small><div class="fw-bold" id="sum-member">-</div><small class="text-muted" id="sum-member-rp"></small></div></div>
<div class="col-md-2"><div class="border rounded p-2 h-100"><small class="text-muted">Denda</small><div class="fw-bold" id="sum-denda">-</div><small class="text-muted" id="sum-denda-rp"></small></div></div>
<div class="col-md-3"><div class="border rounded p-2 h-100"><small class="text-muted">Manual</small><div class="fw-bold" id="sum-manual">-</div><small class="text-muted" id="sum-manual-rp"></small></div></div>
<div class="col-md-3"><div class="border rounded p-2 h-100"><small class="text-muted">Masalah (-1)</small><div class="fw-bold text-warning" id="sum-masalah">-</div><small class="text-muted" id="sum-masalah-rp"></small></div></div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="card-title mb-0">Hasil Transaksi</h5>
<small class="text-muted" id="info-paging"></small>
@@ -126,18 +148,23 @@ class="btn btn-sm btn-icon btn-danger btn-export-pdf" title="Cetak">
<thead>
<tr>
<th width="4%">No.</th>
<th>ID</th>
<th>No. Pol</th>
<th>Barcode</th>
<th>Kendaraan</th>
<th>Waktu Masuk</th>
<th>Waktu Keluar</th>
<th>Status Kendaraan</th>
<th>Status Transaksi</th>
<th data-sort="id">ID <span class="sort-ind"></span></th>
<th data-sort="no_pol">No. Pol <span class="sort-ind"></span></th>
<th data-sort="no_barcode">Barcode <span class="sort-ind"></span></th>
<th data-sort="nama_kendaraan">Kendaraan <span class="sort-ind"></span></th>
<th data-sort="waktu_masuk">Waktu Masuk <span class="sort-ind"></span></th>
<th data-sort="waktu_keluar">Waktu Keluar <span class="sort-ind"></span></th>
<th data-sort="id_op_masuk">Op Masuk <span class="sort-ind"></span></th>
<th data-sort="id_op_keluar">Op Keluar <span class="sort-ind"></span></th>
<th data-sort="id_pintu_masuk">Pintu Masuk <span class="sort-ind"></span></th>
<th data-sort="id_pintu_keluar">Pintu Keluar <span class="sort-ind"></span></th>
<th data-sort="bayar_keluar">Bayar Keluar <span class="sort-ind"></span></th>
<th data-sort="status">Status Kendaraan <span class="sort-ind"></span></th>
<th data-sort="status_transaksi">Status Transaksi <span class="sort-ind"></span></th>
</tr>
</thead>
<tbody id="body-parkir">
<tr><td colspan="9" class="text-center">Memuat data...</td></tr>
<tr><td colspan="14" class="text-center">Memuat data...</td></tr>
</tbody>
</table>
</div>
@@ -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('<tr><td colspan="9" class="text-center">Memuat data...</td></tr>');
$('#body-parkir').html('<tr><td colspan="14" class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>Memuat data...</td></tr>');
$('#btn-cari').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>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('<tr><td colspan="9" class="text-center">Data tidak ditemukan.</td></tr>');
$('#body-parkir').html('<tr><td colspan="14" class="text-center">Data tidak ditemukan.</td></tr>');
} 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 += '<tr>'
+ '<td class="text-center">' + (offset + i + 1) + '</td>'
+ '<td>' + esc(row.id) + '</td>'
@@ -221,6 +289,11 @@ function load() {
+ '<td>' + esc([row.id_kendaraan, row.nama_kendaraan].filter(Boolean).join(' - ')) + '</td>'
+ '<td>' + esc(row.waktu_masuk) + '</td>'
+ '<td>' + esc(row.waktu_keluar) + '</td>'
+ '<td>' + esc(row.id_op_masuk) + '</td>'
+ '<td>' + esc(row.id_op_keluar) + '</td>'
+ '<td>' + esc(row.id_pintu_masuk) + '</td>'
+ '<td>' + esc(row.id_pintu_keluar) + '</td>'
+ '<td class="text-end">' + bayar + '</td>'
+ '<td class="text-center">' + statusKendaraan(row.status) + '</td>'
+ '<td class="text-center">' + statusTransaksi(row.status_transaksi) + '</td>'
+ '</tr>';
@@ -238,7 +311,11 @@ function load() {
if (xhr.status === 422 && xhr.responseJSON?.errors) {
msg = Object.values(xhr.responseJSON.errors).flat().join(' ');
}
$('#body-parkir').html('<tr><td colspan="9" class="text-center text-danger">' + esc(msg) + '</td></tr>');
$('#body-parkir').html('<tr><td colspan="14" class="text-center text-danger">' + esc(msg) + '</td></tr>');
Swal.fire({icon: 'error', title: 'Gagal memuat data', text: msg});
},
complete: function () {
$('#btn-cari').prop('disabled', false).html('<i class="fa fa-search me-1"></i>Cari');
}
});
}
@@ -258,6 +335,7 @@ function load() {
});
$('.btn-export-pdf').on('click', function () { window.print(); });
paintSort();
load();
});
</script>
@@ -178,7 +178,7 @@
});
$('.btn-cancel').click();
alert('Berhasil disimpan 🔥');
Swal.fire({icon: 'success', title: 'Berhasil', text: 'Level pengguna tersimpan.'});
}
});
});
+17 -2
View File
@@ -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('<span class="spinner-border spinner-border-sm me-1"></span>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('<span class="spinner-border spinner-border-sm me-1"></span>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);
}
});
});
+1
View File
@@ -598,6 +598,7 @@ class="btn btn-primary"
logsUrl: @json(route('dhcp.logs')),
};
</script>
<script src="{{ asset('vendor/sweetalert2/dist/sweetalert2.min.js') }}"></script>
<script src="{{ asset('js/setup/dhcp.js') }}"></script>
@endsection
@@ -0,0 +1,286 @@
@extends('layouts.master')
@section('styles')
<style>
.foto-thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; border: 1px solid #dee2e6; }
.foto-preview { width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #dee2e6; }
.card-body label { font-size: 13px; color: #6c757d; }
</style>
@endsection
@section('content')
<div class="container-fluid page-body-wrapper">
<div class="main-panel">
<div class="content-wrapper">
<div class="row">
<div class="col-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="card-title mb-0">Master Petugas / Pegawai</h4>
<button class="btn btn-primary btn-sm" id="btnAdd" type="button">
<i class="fa fa-plus me-1"></i>Tambah
</button>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped" id="petugas-table">
<thead>
<tr>
<th class="text-center" width="5%">Foto</th>
<th>Nomer</th>
<th>Nama</th>
<th>Username</th>
<th>Level</th>
<th class="text-center">Bisa Login</th>
<th class="text-center">Status</th>
<th class="text-center" width="18%">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($petugas as $p)
<tr>
<td class="text-center">
<img class="foto-thumb" src="{{ route('petugas.foto', $p->id, false) }}" alt="foto" loading="lazy">
</td>
<td>{{ $p->nomer }}</td>
<td>{{ $p->nama }}</td>
<td>{{ $p->username }}</td>
<td>{{ $levels->firstWhere('level_code', $p->level_pegawai)->level_name ?? $p->level_pegawai }}</td>
<td class="text-center">
<span class="badge {{ $p->bisalogin == 1 ? 'bg-success' : 'bg-secondary' }}">
{{ $p->bisalogin == 1 ? 'Ya' : 'Tidak' }}
</span>
</td>
<td class="text-center">
<span class="badge {{ $p->status == 1 ? 'bg-success' : 'bg-danger' }}">
{{ $p->status == 1 ? 'Aktif' : 'Nonaktif' }}
</span>
</td>
<td class="text-center">
<button class="btn btn-info btn-sm btn-detail" type="button" data-id="{{ $p->id }}">Detail</button>
<button class="btn btn-warning btn-sm btn-edit" type="button" data-id="{{ $p->id }}">Ubah</button>
<button class="btn btn-danger btn-sm btn-delete" type="button" data-id="{{ $p->id }}" data-nama="{{ $p->nama }}">Hapus</button>
</td>
</tr>
@empty
<tr><td colspan="8" class="text-center">Belum ada data petugas.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- MODAL FORM -->
<div class="modal fade" id="modal-petugas" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-title">Tambah Petugas</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="form-petugas" enctype="multipart/form-data">
@csrf
<input type="hidden" name="_method" id="form-method" value="POST">
<input type="hidden" id="form-id" value="">
<div class="modal-body">
<div class="row g-2">
<div class="col-md-3 text-center">
<img class="foto-preview mb-2" id="foto-preview" src="{{ asset('images/no-image.jpg') }}" alt="preview">
<input type="file" class="form-control form-control-sm" name="foto" id="foto" accept="image/*">
<small class="text-muted">JPG/PNG/BMP, maks 2MB</small>
</div>
<div class="col-md-9">
<div class="row g-2">
<div class="col-md-6"><label class="form-label">Nomer *</label><input class="form-control" name="nomer" id="f-nomer" maxlength="50" required></div>
<div class="col-md-6"><label class="form-label">Nama *</label><input class="form-control" name="nama" id="f-nama" maxlength="50" required></div>
<div class="col-md-6"><label class="form-label">Username *</label><input class="form-control" name="username" id="f-username" maxlength="50" required></div>
<div class="col-md-6"><label class="form-label">Password <span id="pw-hint">*</span></label><input type="text" class="form-control" name="password" id="f-password" maxlength="50" placeholder="Kosongkan jika tidak diubah"></div>
<div class="col-md-6"><label class="form-label">Level</label>
<select class="form-select" name="level_pegawai" id="f-level">
<option value="">-- Pilih --</option>
@foreach ($levels as $lv)
<option value="{{ $lv->level_code }}">{{ $lv->level_name }}</option>
@endforeach
</select>
</div>
<div class="col-md-6"><label class="form-label">NIP</label><input class="form-control" name="nip" id="f-nip" maxlength="50"></div>
<div class="col-md-4"><label class="form-label">Bisa Login</label>
<select class="form-select" name="bisalogin" id="f-bisalogin"><option value="1">Ya</option><option value="0">Tidak</option></select>
</div>
<div class="col-md-4"><label class="form-label">Status</label>
<select class="form-select" name="status" id="f-status"><option value="1">Aktif</option><option value="0">Nonaktif</option></select>
</div>
<div class="col-md-4"><label class="form-label">Jenis Kelamin</label>
<select class="form-select" name="jeniskelamin" id="f-jk"><option value="0">Laki-laki</option><option value="1">Perempuan</option></select>
</div>
<div class="col-md-6"><label class="form-label">Agama</label><input class="form-control" name="agama" id="f-agama" maxlength="50"></div>
<div class="col-md-6"><label class="form-label">Telepon</label><input class="form-control" name="telepon" id="f-telepon" maxlength="255"></div>
<div class="col-md-6"><label class="form-label">Tgl Lahir</label><input type="date" class="form-control" name="tgl_lahir" id="f-tgl-lahir"></div>
<div class="col-md-6"><label class="form-label">Tgl Masuk</label><input type="date" class="form-control" name="tgl_masuk" id="f-tgl-masuk"></div>
<div class="col-md-6"><label class="form-label">Email</label><input class="form-control" name="email" id="f-email" maxlength="64"></div>
<div class="col-md-6"><label class="form-label">Alamat</label><input class="form-control" name="alamat" id="f-alamat" maxlength="100"></div>
<div class="col-md-6"><label class="form-label">Alamat Lahir</label><input class="form-control" name="alamat_lahir" id="f-alamat-lahir" maxlength="255"></div>
<div class="col-md-6"><label class="form-label">Alamat Sekarang</label><input class="form-control" name="alamat_sekarang" id="f-alamat-sekarang" maxlength="255"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Simpan</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">Batal</button>
</div>
</form>
</div>
</div>
</div>
<!-- MODAL DETAIL -->
<div class="modal fade" id="modal-detail" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title">Detail Petugas</h5><button class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body" id="detail-body"></div>
</div>
</div>
</div>
</div>
@endsection
@section('javascript')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="{{ asset('vendor/sweetalert2/dist/sweetalert2.min.js') }}"></script>
<script>
$(function () {
const csrf = $('meta[name="csrf-token"]').attr('content');
const noImage = '{{ asset('images/no-image.jpg') }}';
const levelMap = @json($levels->pluck('level_name', 'level_code'));
const routes = {
store: '{{ route('petugas.store', [], false) }}',
show: '{{ route('petugas.show', ':id', false) }}',
update: '{{ route('petugas.update', ':id', false) }}',
destroy: '{{ route('petugas.destroy', ':id', false) }}',
foto: '{{ route('petugas.foto', ':id', false) }}'
};
const modal = new bootstrap.Modal(document.getElementById('modal-petugas'));
function errMsg(xhr) {
if (xhr.status === 419) return 'Sesi berakhir, silakan muat ulang halaman.';
if (xhr.status === 422 && xhr.responseJSON?.errors) {
return Object.values(xhr.responseJSON.errors).flat().join('\n');
}
return xhr.responseJSON?.message || 'Operasi gagal.';
}
$('#foto').on('change', function () {
const f = this.files[0];
$('#foto-preview').attr('src', f ? URL.createObjectURL(f) : noImage);
});
$('#btnAdd').on('click', function () {
$('#form-petugas')[0].reset();
$('#form-id').val('');
$('#form-method').val('POST');
$('#pw-hint').text('*');
$('#f-password').prop('required', true);
$('#foto-preview').attr('src', noImage);
$('#modal-title').text('Tambah Petugas');
modal.show();
});
$(document).on('click', '.btn-edit', function () {
const id = $(this).data('id');
const btn = $(this); const btnHtml = btn.html();
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span>');
$.get(routes.show.replace(':id', id), function (r) {
const d = r.data;
$('#form-id').val(d.id);
$('#form-method').val('PUT');
$('#pw-hint').text('(kosongkan jika tidak diubah)');
$('#f-password').prop('required', false).val('');
$('#f-nomer').val(d.nomer);
$('#f-nama').val(d.nama);
$('#f-username').val(d.username);
$('#f-level').val(d.level_pegawai);
$('#f-nip').val(d.nip);
$('#f-bisalogin').val(d.bisalogin ?? 1);
$('#f-status').val(d.status ?? 1);
$('#f-jk').val(d.jeniskelamin ?? 0);
$('#f-agama').val(d.agama);
$('#f-telepon').val(d.telepon);
$('#f-tgl-lahir').val(d.tgl_lahir);
$('#f-tgl-masuk').val(d.tgl_masuk);
$('#f-email').val(d.email);
$('#f-alamat').val(d.alamat);
$('#f-alamat-lahir').val(d.alamat_lahir);
$('#f-alamat-sekarang').val(d.alamat_sekarang);
$('#foto').val('');
$('#foto-preview').attr('src', r.has_foto ? routes.foto.replace(':id', d.id) + '?t=' + Date.now() : noImage);
$('#modal-title').text('Ubah Petugas');
modal.show();
}).fail(xhr => Swal.fire({icon: 'error', title: 'Gagal memuat data', text: errMsg(xhr)})).always(() => btn.prop('disabled', false).html(btnHtml));
});
$('#form-petugas').on('submit', function (e) {
e.preventDefault();
const id = $('#form-id').val();
const isEdit = !!id;
const url = isEdit ? routes.update.replace(':id', encodeURIComponent(id)) : routes.store;
const fd = new FormData(this);
if (isEdit) fd.set('_method', 'PUT');
const saveBtn = $(this).find('[type="submit"]'); const saveHtml = saveBtn.html();
saveBtn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Menyimpan...');
$.ajax({
url: url, type: 'POST', data: fd, processData: false, contentType: false,
headers: {'X-CSRF-TOKEN': csrf},
success: () => window.location.reload(),
error: xhr => {
saveBtn.prop('disabled', false).html(saveHtml);
Swal.fire({icon: 'error', title: 'Gagal menyimpan', text: errMsg(xhr)});
}
});
});
$(document).on('click', '.btn-delete', function () {
const id = $(this).data('id');
const nama = $(this).data('nama');
Swal.fire({title: 'Hapus petugas?', text: nama, icon: 'warning', showCancelButton: true, confirmButtonText: 'Ya, hapus', cancelButtonText: 'Batal', confirmButtonColor: '#dc3545', showLoaderOnConfirm: true, preConfirm: () => $.ajax({url: routes.destroy.replace(':id', encodeURIComponent(id)), type: 'DELETE', headers: {'X-CSRF-TOKEN': csrf}}).catch(xhr => { Swal.showValidationMessage(errMsg(xhr)); })})
.then(res => {
if (res.isConfirmed && res.value) {
Swal.fire({icon: 'success', title: 'Berhasil', text: 'Petugas dihapus.', timer: 1500, showConfirmButton: false}).then(() => window.location.reload());
}
});
});
$(document).on('click', '.btn-detail', function () {
const id = $(this).data('id');
const btn = $(this); const btnHtml = btn.html();
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span>');
$('#detail-body').html('<div class="text-center py-3"><span class="spinner-border spinner-border-sm me-2"></span>Memuat...</div>');
new bootstrap.Modal(document.getElementById('modal-detail')).show();
$.get(routes.show.replace(':id', id), function (r) {
const d = r.data;
const img = r.has_foto ? routes.foto.replace(':id', d.id) : noImage;
$('#detail-body').html(
'<div class="text-center mb-3"><img src="' + img + '" class="foto-preview" alt="foto"></div>' +
'<dl class="row mb-0">' +
'<dt class="col-4">Nomer</dt><dd class="col-8">' + (d.nomer ?? '-') + '</dd>' +
'<dt class="col-4">Nama</dt><dd class="col-8">' + (d.nama ?? '-') + '</dd>' +
'<dt class="col-4">Username</dt><dd class="col-8">' + (d.username ?? '-') + '</dd>' +
'<dt class="col-4">Level</dt><dd class="col-8">' + (levelMap[d.level_pegawai] ?? d.level_pegawai ?? '-') + '</dd>' +
'<dt class="col-4">NIP</dt><dd class="col-8">' + (d.nip ?? '-') + '</dd>' +
'<dt class="col-4">Telepon</dt><dd class="col-8">' + (d.telepon ?? '-') + '</dd>' +
'<dt class="col-4">Alamat</dt><dd class="col-8">' + (d.alamat ?? '-') + '</dd>' +
'<dt class="col-4">Tgl Lahir</dt><dd class="col-8">' + (d.tgl_lahir ?? '-') + '</dd>' +
'<dt class="col-4">Tgl Masuk</dt><dd class="col-8">' + (d.tgl_masuk ?? '-') + '</dd>' +
'</dl>'
);
}).fail(xhr => $('#detail-body').html('<div class="text-danger text-center">' + errMsg(xhr) + '</div>')).always(() => btn.prop('disabled', false).html(btnHtml));
});
});
</script>
@endsection
@@ -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('<span class="spinner-border spinner-border-sm me-1"></span>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('<div class="text-center py-3">Memuat detail...</div>'); 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(`<dl class="row mb-0"><dt class="col-5">ID Kendaraan</dt><dd class="col-7">${r.data.id}</dd><dt class="col-5">Nama</dt><dd class="col-7">${r.data.nama}</dd><dt class="col-5">Shortcut</dt><dd class="col-7">${r.data.short_cut}</dd><dt class="col-5">Tarif Awal</dt><dd class="col-7">${t ? Number(t.tarif || 0).toLocaleString('id-ID') : '-'}</dd><dt class="col-5">Tarif Interval</dt><dd class="col-7">${t ? Number(t.tarif_interval || 0).toLocaleString('id-ID') : '-'}</dd><dt class="col-5">Waktu Gratis</dt><dd class="col-7">${t?.waktu_gratis ?? '-' } menit</dd></dl>`); }); });
$('#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('<span class="spinner-border spinner-border-sm me-1"></span>Memuat...'); $('#vehicle-detail-body').html('<div class="text-center py-3"><span class="spinner-border spinner-border-sm me-2"></span>Memuat detail...</div>'); 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(`<dl class="row mb-0"><dt class="col-5">ID Kendaraan</dt><dd class="col-7">${r.data.id}</dd><dt class="col-5">Nama</dt><dd class="col-7">${r.data.nama}</dd><dt class="col-5">Shortcut</dt><dd class="col-7">${r.data.short_cut}</dd><dt class="col-5">Tarif Awal</dt><dd class="col-7">${t ? Number(t.tarif || 0).toLocaleString('id-ID') : '-'}</dd><dt class="col-5">Tarif Interval</dt><dd class="col-7">${t ? Number(t.tarif_interval || 0).toLocaleString('id-ID') : '-'}</dd><dt class="col-5">Waktu Gratis</dt><dd class="col-7">${t?.waktu_gratis ?? '-' } menit</dd></dl>`); }).fail(xhr => { $('#vehicle-detail-body').html('<div class="text-danger text-center">' + (xhr.responseJSON?.message || 'Gagal memuat detail.') + '</div>'); }).always(() => btn.prop('disabled', false).html(btnHtml)); });
});
</script>
@@ -54,7 +54,7 @@
<div class="mb-3 row">
<label class="col-lg-3 col-form-label form-label">Telepon</label>
<div class="col-lg-8">
<input type="number" class="form-control form-control-sm" phone="phone" value="{{ old('phone')}}">
<input type="text" inputmode="tel" class="form-control form-control-sm" name="phone" value="{{ old('phone')}}">
</div>
</div>
<div class="mb-3 row">
+27
View File
@@ -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;
}
</style>
@endsection
@@ -54,7 +54,7 @@
<div class="mb-3 row">
<label class="col-lg-3 col-form-label form-label">Telepon</label>
<div class="col-lg-8">
<input type="text" class="form-control form-control-sm" phone="phone" placeholder="+62 81/081...">
<input type="text" class="form-control form-control-sm" name="phone" placeholder="+62 81/081...">
</div>
</div>
<div class="mb-3 row">
+1 -1
View File
@@ -49,7 +49,7 @@
<div class="mb-3 row">
<label class="col-lg-3 col-form-label form-label">Telepon</label>
<div class="col-lg-8">
<input type="number" class="form-control" phone="phone" placeholder="+62 81/081...">
<input type="text" inputmode="tel" class="form-control" name="phone" placeholder="+62 81/081...">
</div>
</div>
<div class="mb-3 row">
+3
View File
@@ -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");