CRUD petugas/pegawai + filter-sort-rekap transaksi parkir + fix save member kartu + hilangkan alert ganti Swal-loading
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user