diff --git a/app/Http/Controllers/Parking/Report/ReportOkupansiAreaController.php b/app/Http/Controllers/Parking/Report/ReportOkupansiAreaController.php new file mode 100644 index 0000000..4522eef --- /dev/null +++ b/app/Http/Controllers/Parking/Report/ReportOkupansiAreaController.php @@ -0,0 +1,273 @@ +orderBy("id_lokasi")->get(); + + return view("report.okupansi-area", compact("area")); + } + + public function data(Request $request) + { + $request->validate( + [ + "tgl_awal" => ["nullable", "date_format:Y-m-d"], + "tgl_akhir" => ["nullable", "date_format:Y-m-d", "after_or_equal:tgl_awal", "before_or_equal:today"], + "area" => ["nullable", "string", "max:10"], + ], + [ + "tgl_akhir.after_or_equal" => "Periode akhir tidak boleh lebih awal dari periode awal.", + "tgl_akhir.before_or_equal" => "Periode akhir tidak boleh melewati hari ini.", + ] + ); + + $tglAkhir = $request->input("tgl_akhir", Carbon::today()->toDateString()); + $tglAwal = $request->input("tgl_awal", Carbon::parse($tglAkhir)->subDays(6)->toDateString()); + $start = Carbon::parse($tglAwal)->startOfDay(); + $end = Carbon::parse($tglAkhir)->startOfDay(); + + if ($start->diffInDays($end) > 370) { + return response()->json(["message" => "Rentang maksimal 370 hari, persempit periodenya."], 422); + } + + $areaFilter = $request->input("area"); + $areas = MasterLokasi::query() + ->when($areaFilter && $areaFilter !== "all", function ($query) use ($areaFilter) { + $query->where("id_lokasi", $areaFilter); + }) + ->orderBy("prioritas") + ->orderBy("id_lokasi") + ->get(); + + $dateList = []; + for ($date = $start->copy(); $date->lte($end); $date->addDay()) { + $dateList[] = $date->toDateString(); + } + + $masukMap = $this->eventMap($request, $areaFilter, $tglAwal, $tglAkhir, 0, "waktu_masuk", [0, 1]); + $keluarMap = $this->eventMap($request, $areaFilter, $tglAwal, $tglAkhir, 1, "waktu_keluar", [0]); + + $today = Carbon::today()->toDateString(); + $currentHour = (int) Carbon::now()->format("G"); + $areaRows = []; + + foreach ($areas as $area) { + $capacity = max(0, (int) $area->max_space); + $stats = []; + $totalMasuk = 0; + $totalKeluar = 0; + $fullDays = 0; + + for ($hour = 0; $hour < 24; $hour++) { + $stats[$hour] = [ + "sum" => 0, + "count" => 0, + "max" => 0, + "min" => null, + "masuk" => 0, + "keluar" => 0, + "hari_penuh" => 0, + ]; + } + + foreach ($dateList as $date) { + $occupancy = 0; + $isFull = false; + $maxHour = $date === $today ? $currentHour : 23; + + for ($hour = 0; $hour <= $maxHour; $hour++) { + $masuk = (int) ($masukMap[$area->id_lokasi][$date][$hour] ?? 0); + $keluar = (int) ($keluarMap[$area->id_lokasi][$date][$hour] ?? 0); + $occupancy = max(0, min($capacity, $occupancy + $masuk - $keluar)); + + $stats[$hour]["sum"] += $occupancy; + $stats[$hour]["count"]++; + $stats[$hour]["max"] = max($stats[$hour]["max"], $occupancy); + $stats[$hour]["min"] = $stats[$hour]["min"] === null + ? $occupancy + : min($stats[$hour]["min"], $occupancy); + $stats[$hour]["masuk"] += $masuk; + $stats[$hour]["keluar"] += $keluar; + $totalMasuk += $masuk; + $totalKeluar += $keluar; + + if ($capacity > 0 && $occupancy >= $capacity) { + $stats[$hour]["hari_penuh"]++; + $isFull = true; + } + } + + if ($isFull) { + $fullDays++; + } + } + + $hourRows = []; + $totalSum = 0; + $totalCount = 0; + + for ($hour = 0; $hour < 24; $hour++) { + $stat = $stats[$hour]; + $count = max(1, $stat["count"]); + $average = round($stat["sum"] / $count, 1); + $percent = $capacity > 0 ? round(($average / $capacity) * 100, 1) : 0; + + $hourRows[] = [ + "jam" => $hour, + "label" => str_pad((string) $hour, 2, "0", STR_PAD_LEFT) . ":00", + "rata" => $average, + "maks" => $stat["max"], + "min" => $stat["min"] ?? 0, + "persen" => $percent, + "status" => $this->statusOkupansi($percent, $capacity), + "hari_penuh" => $stat["hari_penuh"], + "hari_dianalisis" => $stat["count"], + "masuk" => $stat["masuk"], + "keluar" => $stat["keluar"], + ]; + + $totalSum += $stat["sum"]; + $totalCount += $stat["count"]; + } + + $peak = $hourRows[0]; + $quiet = $hourRows[0]; + $mostFull = null; + + foreach ($hourRows as $row) { + if ($row["rata"] > $peak["rata"]) { + $peak = $row; + } + if ($row["rata"] < $quiet["rata"]) { + $quiet = $row; + } + if ($row["hari_penuh"] > 0 && ($mostFull === null || $row["hari_penuh"] > $mostFull["hari_penuh"])) { + $mostFull = $row; + } + } + + $averageOccupancy = $totalCount > 0 ? round($totalSum / $totalCount, 1) : 0; + $averagePercent = $capacity > 0 ? round(($averageOccupancy / $capacity) * 100, 1) : 0; + + $areaRows[] = [ + "id_lokasi" => $area->id_lokasi, + "nama_lokasi" => $area->nama_lokasi, + "kapasitas" => $capacity, + "sisa" => (int) $area->sisa, + "total_masuk" => $totalMasuk, + "total_keluar" => $totalKeluar, + "rata_okupansi" => $averageOccupancy, + "rata_persen" => $averagePercent, + "status" => $this->statusOkupansi($peak["persen"], $capacity), + "puncak" => $peak, + "kosong" => $quiet, + "jam_penuh" => $mostFull, + "hari_penuh" => $fullDays, + "jam" => $hourRows, + ]; + } + + $areaTerpadat = null; + foreach ($areaRows as $row) { + if ($areaTerpadat === null + || $row["puncak"]["persen"] > $areaTerpadat["puncak"]["persen"] + || ($row["puncak"]["persen"] === $areaTerpadat["puncak"]["persen"] + && $row["puncak"]["rata"] > $areaTerpadat["puncak"]["rata"])) { + $areaTerpadat = $row; + } + } + + $hourAggregate = array_fill(0, 24, ["sum" => 0, "count" => 0]); + foreach ($areaRows as $row) { + foreach ($row["jam"] as $hour) { + $weight = $hour["hari_dianalisis"]; + $hourAggregate[$hour["jam"]]["sum"] += $hour["rata"] * $weight; + $hourAggregate[$hour["jam"]]["count"] += $weight; + } + } + + $jamPadat = null; + $jamLengang = null; + foreach ($hourAggregate as $hour => $aggregate) { + $average = $aggregate["count"] > 0 ? $aggregate["sum"] / $aggregate["count"] : 0; + if ($jamPadat === null || $average > $jamPadat["rata"]) { + $jamPadat = ["jam" => $hour, "label" => str_pad((string) $hour, 2, "0", STR_PAD_LEFT) . ":00", "rata" => round($average, 1)]; + } + if ($jamLengang === null || $average < $jamLengang["rata"]) { + $jamLengang = ["jam" => $hour, "label" => str_pad((string) $hour, 2, "0", STR_PAD_LEFT) . ":00", "rata" => round($average, 1)]; + } + } + + return response()->json([ + "status" => true, + "tgl_awal" => $tglAwal, + "tgl_akhir" => $tglAkhir, + "data" => $areaRows, + "ringkasan" => [ + "jumlah_area" => count($areaRows), + "area_pernah_penuh" => count(array_filter($areaRows, fn ($row) => $row["hari_penuh"] > 0)), + "area_terpadat" => $areaTerpadat, + "jam_padat" => $jamPadat, + "jam_lengang" => $jamLengang, + ], + ]); + } + + private function eventMap(Request $request, ?string $areaFilter, string $tglAwal, string $tglAkhir, int $jenis, string $timeColumn, array $statuses): array + { + $rows = DB::table("transaksi_parkir as tp") + ->join("nama_pos as np", "np.id", "=", $jenis === 0 ? "tp.id_pintu_masuk" : "tp.id_pintu_keluar") + ->join("master_lokasi as ml", "ml.id_lokasi", "=", "np.area") + ->whereIn("tp.status", $statuses) + ->where("np.jenis", $jenis) + ->whereNotNull("tp.{$timeColumn}") + ->whereBetween("tp.{$timeColumn}", [$tglAwal . " 00:00:00", $tglAkhir . " 23:59:59"]) + ->when($areaFilter && $areaFilter !== "all", function ($query) use ($areaFilter) { + $query->where("ml.id_lokasi", $areaFilter); + }) + ->selectRaw("ml.id_lokasi as area_id") + ->selectRaw("tp.{$timeColumn}::date as tanggal") + ->selectRaw("extract(hour from tp.{$timeColumn})::int as jam") + ->selectRaw("count(*) as jumlah") + ->groupByRaw("ml.id_lokasi, tp.{$timeColumn}::date, extract(hour from tp.{$timeColumn})") + ->get(); + + $map = []; + foreach ($rows as $row) { + $map[$row->area_id][substr((string) $row->tanggal, 0, 10)][(int) $row->jam] = (int) $row->jumlah; + } + + return $map; + } + + private function statusOkupansi(float $percent, int $capacity): string + { + if ($capacity <= 0) { + return "Kapasitas belum diisi"; + } + if ($percent >= 100) { + return "Penuh"; + } + if ($percent >= 80) { + return "Hampir penuh"; + } + if ($percent >= 50) { + return "Ramai"; + } + if ($percent > 0) { + return "Lengang"; + } + + return "Kosong"; + } +} diff --git a/app/Http/Controllers/Parking/Report/ReportTransaksiController.php b/app/Http/Controllers/Parking/Report/ReportTransaksiController.php index 52f64a8..bfd8f63 100644 --- a/app/Http/Controllers/Parking/Report/ReportTransaksiController.php +++ b/app/Http/Controllers/Parking/Report/ReportTransaksiController.php @@ -31,10 +31,84 @@ public function statistik() return view("report.statistik"); } + public function statistikPengunjung() + { + $pintu = DB::table("nama_pos")->select("id", "nama")->orderBy("id")->get(); + + return view("report.statistik-pengunjung", compact("pintu")); + } + /** - * Statistik transaksi parkir dipecah per hari / minggu / bulan. - * Periode berdasarkan kolom tanggal. Response: rows per periode + ringkasan. + * Statistik jumlah pengunjung (kendaraan masuk) per jam / hari / minggu. + * Fokus qty saja, periode berdasarkan waktu_masuk. */ + public function statistikPengunjungData(Request $request) + { + $request->validate([ + "grup" => "nullable|in:jam,hari,minggu", + "tgl_awal" => "nullable|date_format:Y-m-d", + "tgl_akhir" => "nullable|date_format:Y-m-d|after_or_equal:tgl_awal", + "pintu_masuk" => "nullable|string|max:10", + ]); + + $grup = $request->input("grup", "hari"); + $tglAkhir = $request->input("tgl_akhir", date("Y-m-d")); + $tglAwal = $request->input("tgl_awal", date("Y-m-d", strtotime("-6 days", strtotime($tglAkhir)))); + + if ((strtotime($tglAkhir) - strtotime($tglAwal)) / 86400 > 370) { + return response()->json(["message" => "Rentang maksimal 370 hari, persempit periodenya."], 422); + } + + $bucket = match ($grup) { + "jam" => "to_char(date_trunc('hour', tp.waktu_masuk), 'YYYY-MM-DD HH24:00:00')", + "minggu" => "to_char(date_trunc('week', tp.waktu_masuk), 'YYYY-MM-DD')", + default => "to_char(date_trunc('day', tp.waktu_masuk), 'YYYY-MM-DD')", + }; + $labelPintu = "case when tp.id_pintu_masuk is null then 'Tanpa Pintu' when np.nama is null then 'Pintu ' || tp.id_pintu_masuk else np.nama || ' (' || tp.id_pintu_masuk || ')' end"; + + $query = DB::table("transaksi_parkir as tp") + ->leftJoin("nama_pos as np", "np.id", "=", "tp.id_pintu_masuk") + ->whereNotNull("tp.waktu_masuk") + ->whereDate("tp.waktu_masuk", ">=", $tglAwal) + ->whereDate("tp.waktu_masuk", "<=", $tglAkhir); + + if ($request->filled("pintu_masuk") && $request->input("pintu_masuk") !== "all") { + if ($request->input("pintu_masuk") === "__none__") { + $query->whereNull("tp.id_pintu_masuk"); + } else { + $query->where("tp.id_pintu_masuk", $request->input("pintu_masuk")); + } + } + + $rows = $query + ->selectRaw("{$bucket} as periode") + ->selectRaw("tp.id_pintu_masuk") + ->selectRaw("{$labelPintu} as pintu") + ->selectRaw("count(*) as jumlah") + ->groupByRaw("{$bucket}, tp.id_pintu_masuk, np.nama") + ->orderBy("periode") + ->orderBy("pintu") + ->get(); + + $total = (int) $rows->sum("jumlah"); + $puncak = $rows->sortByDesc("jumlah")->first(); + + return response()->json([ + "status" => true, + "grup" => $grup, + "tgl_awal" => $tglAwal, + "tgl_akhir" => $tglAkhir, + "data" => $rows, + "ringkasan" => [ + "total" => $total, + "rata_rata" => $rows->count() ? round($total / $rows->count(), 1) : 0, + "puncak" => $puncak ? (int) $puncak->jumlah : 0, + "puncak_periode" => $puncak ? $puncak->periode : null, + "puncak_pintu" => $puncak ? $puncak->pintu : null, + ], + ]); + } + public function statistikData(Request $request) { $request->validate([ @@ -55,14 +129,16 @@ public function statistikData(Request $request) } $bucket = match ($grup) { - "minggu" => "date_trunc('week', tp.tanggal)::date", - "bulan" => "date_trunc('month', tp.tanggal)::date", - default => "tp.tanggal::date", + "minggu" => "date_trunc('week', tp.waktu_keluar)::date", + "bulan" => "date_trunc('month', tp.waktu_keluar)::date", + default => "tp.waktu_keluar::date", }; $query = DB::table("transaksi_parkir as tp") - ->whereDate("tp.tanggal", ">=", $tglAwal) - ->whereDate("tp.tanggal", "<=", $tglAkhir); + ->where("tp.status", 0) + ->whereNotNull("tp.waktu_keluar") + ->whereDate("tp.waktu_keluar", ">=", $tglAwal) + ->whereDate("tp.waktu_keluar", "<=", $tglAkhir); if ($request->filled("status_transaksi")) { $query->where("tp.status_transaksi", $request->input("status_transaksi")); @@ -78,10 +154,9 @@ public function statistikData(Request $request) $rows = $query ->selectRaw("({$bucket}) as periode") ->selectRaw("count(*) as transaksi") - ->selectRaw("count(case when tp.status = 0 then 1 end) as keluar") - ->selectRaw("coalesce(sum(case when tp.status = 0 then tp.bayar_keluar end), 0) as pendapatan") - ->selectRaw("coalesce(sum(case when tp.status = 0 and (tp.cara_bayar is null or tp.cara_bayar = 0) then tp.bayar_keluar end), 0) as tunai") - ->selectRaw("coalesce(sum(case when tp.status = 0 and tp.cara_bayar = 3 then tp.bayar_keluar end), 0) as cashless") + ->selectRaw("coalesce(sum(tp.bayar_keluar), 0) as pendapatan") + ->selectRaw("coalesce(sum(case when tp.cara_bayar is null or tp.cara_bayar = 0 then tp.bayar_keluar end), 0) as tunai") + ->selectRaw("coalesce(sum(case when tp.cara_bayar = 3 then tp.bayar_keluar end), 0) as cashless") ->groupByRaw($bucket) ->orderBy("periode") ->get(); @@ -94,7 +169,6 @@ public function statistikData(Request $request) "data" => $rows, "ringkasan" => [ "transaksi" => $rows->sum("transaksi"), - "keluar" => $rows->sum("keluar"), "pendapatan" => (float) $rows->sum("pendapatan"), "tunai" => (float) $rows->sum("tunai"), "cashless" => (float) $rows->sum("cashless"), diff --git a/app/Http/Controllers/Parking/Setup/Gate/MasterGateController.php b/app/Http/Controllers/Parking/Setup/Gate/MasterGateController.php index 38f8b8b..d5b23f0 100644 --- a/app/Http/Controllers/Parking/Setup/Gate/MasterGateController.php +++ b/app/Http/Controllers/Parking/Setup/Gate/MasterGateController.php @@ -3,86 +3,162 @@ namespace App\Http\Controllers\Parking\Setup\Gate; use App\Http\Controllers\Controller; +use App\Models\MasterLokasi; use App\Models\NamaPos; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; class MasterGateController extends Controller { - /** - * Display a listing of the resource. - * - * @return \Illuminate\Http\Response - */ public function index() { + $Gate = NamaPos::with("areaParkir")->orderBy("id")->get(); + $Area = MasterLokasi::orderBy("prioritas")->orderBy("id_lokasi")->get(); - $Gate = NamaPos::orderBy('id')->get(); - return view ('setup.gate.gate', compact('Gate')); + return view("setup.gate.gate", compact("Gate", "Area")); } - /** - * Show the form for creating a new resource. - * - * @return \Illuminate\Http\Response - */ public function create() { // } - /** - * Store a newly created resource in storage. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ public function store(Request $request) { - // + $request->merge([ + "id" => strtoupper(trim((string) $request->input("id"))), + "nama" => trim((string) $request->input("nama")), + ]); + + $data = $request->validate( + [ + "id" => ["required", "string", "max:10", Rule::unique("nama_pos", "id")], + "nama" => ["required", "string", "max:100"], + "jenis" => ["required", "integer", "in:0,1,2,3"], + "grace_period" => ["nullable", "integer", "in:0,1"], + "manless" => ["nullable", "integer", "in:0,1"], + "area" => ["nullable", "string", "max:10", Rule::exists("master_lokasi", "id_lokasi")], + ], + [ + "id.unique" => "ID pintu/pos sudah dipakai, gunakan kode lain.", + "area.exists" => "Area parkir tidak ditemukan.", + ] + ); + + $gate = NamaPos::create([ + "id" => $data["id"], + "nama" => $data["nama"], + "jenis" => $data["jenis"], + "tanggal" => now(), + "lokasi" => null, + "grace_period" => $data["grace_period"] ?? 1, + "manless" => $data["manless"] ?? 0, + "area" => $data["area"] ?? null, + ]); + $gate->load("areaParkir"); + + return response()->json([ + "success" => true, + "message" => "Pintu/pos berhasil ditambahkan.", + "data" => $gate, + ], 201); } - /** - * Display the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response - */ public function show($id) { - // + $gate = NamaPos::with("areaParkir")->find($id); + + if (!$gate) { + return response()->json([ + "success" => false, + "message" => "Pintu/pos tidak ditemukan.", + ], 404); + } + + return response()->json(["success" => true, "data" => $gate]); } - /** - * Show the form for editing the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response - */ public function edit($id) { - // + return $this->show($id); } - /** - * Update the specified resource in storage. - * - * @param \Illuminate\Http\Request $request - * @param int $id - * @return \Illuminate\Http\Response - */ public function update(Request $request, $id) { - // + $gate = NamaPos::find($id); + + if (!$gate) { + return response()->json([ + "success" => false, + "message" => "Pintu/pos tidak ditemukan.", + ], 404); + } + + $request->merge([ + "nama" => trim((string) $request->input("nama")), + ]); + + $data = $request->validate( + [ + "nama" => ["required", "string", "max:100"], + "jenis" => ["required", "integer", "in:0,1,2,3"], + "grace_period" => ["nullable", "integer", "in:0,1"], + "manless" => ["nullable", "integer", "in:0,1"], + "area" => ["nullable", "string", "max:10", Rule::exists("master_lokasi", "id_lokasi")], + ], + [ + "area.exists" => "Area parkir tidak ditemukan.", + ] + ); + + $data["area"] = $request->filled("area") ? $request->input("area") : null; + $gate->update($data); + $gate->load("areaParkir"); + + return response()->json([ + "success" => true, + "message" => "Pintu/pos berhasil diperbarui.", + "data" => $gate, + ]); } - /** - * Remove the specified resource from storage. - * - * @param int $id - * @return \Illuminate\Http\Response - */ public function destroy($id) { - // + $gate = NamaPos::find($id); + + if (!$gate) { + return response()->json([ + "success" => false, + "message" => "Pintu/pos tidak ditemukan.", + ], 404); + } + + $usedTrx = DB::table("transaksi_parkir") + ->where("id_pintu_masuk", $id) + ->orWhere("id_pintu_keluar", $id) + ->count(); + if ($usedTrx > 0) { + return response()->json([ + "success" => false, + "message" => "Pintu/pos {$id} dipakai {$usedTrx} transaksi, tidak bisa dihapus.", + ], 409); + } + + $usedConfig = DB::table("config_pos")->where("id_pos", $id)->count() + + DB::table("config_pos_hardware")->where("id_pos", $id)->count(); + if ($usedConfig > 0) { + return response()->json([ + "success" => false, + "message" => "Pintu/pos {$id} masih punya konfigurasi Pos/Hardware, hapus konfigurasinya dulu.", + ], 409); + } + + $gate->delete(); + + return response()->json([ + "success" => true, + "message" => "Pintu/pos berhasil dihapus.", + ]); } } diff --git a/app/Http/Controllers/Parking/Setup/Lokasi/MasterLokasiController.php b/app/Http/Controllers/Parking/Setup/Lokasi/MasterLokasiController.php new file mode 100644 index 0000000..1475a8f --- /dev/null +++ b/app/Http/Controllers/Parking/Setup/Lokasi/MasterLokasiController.php @@ -0,0 +1,144 @@ +orderBy("id_lokasi")->get(); + + return view("setup.lokasi.lokasi", compact("lokasi")); + } + + public function create() + { + // + } + + public function store(Request $request) + { + $request->merge([ + "id_lokasi" => strtoupper(trim((string) $request->input("id_lokasi"))), + "nama_lokasi" => trim((string) $request->input("nama_lokasi")), + ]); + + $data = $request->validate( + [ + "id_lokasi" => ["required", "string", "max:10", Rule::unique("master_lokasi", "id_lokasi")], + "nama_lokasi" => ["required", "string", "max:100"], + "max_space" => ["required", "integer", "min:0", "max:32767"], + "sisa" => ["required", "integer", "min:0", "max:32767", "lte:max_space"], + "prioritas" => ["nullable", "integer"], + ], + [ + "id_lokasi.unique" => "ID lokasi sudah dipakai, gunakan kode lain.", + "sisa.lte" => "Sisa kapasitas tidak boleh melebihi kapasitas maksimal.", + ] + ); + + $lokasi = MasterLokasi::create($data); + + return response()->json([ + "success" => true, + "message" => "Area parkir berhasil ditambahkan.", + "data" => $lokasi, + ], 201); + } + + public function show($id_lokasi) + { + $lokasi = MasterLokasi::find($id_lokasi); + + if (!$lokasi) { + return response()->json([ + "success" => false, + "message" => "Area parkir tidak ditemukan.", + ], 404); + } + + return response()->json(["success" => true, "data" => $lokasi]); + } + + public function edit($id_lokasi) + { + return $this->show($id_lokasi); + } + + public function update(Request $request, $id_lokasi) + { + $lokasi = MasterLokasi::find($id_lokasi); + + if (!$lokasi) { + return response()->json([ + "success" => false, + "message" => "Area parkir tidak ditemukan.", + ], 404); + } + + $request->merge([ + "nama_lokasi" => trim((string) $request->input("nama_lokasi")), + ]); + + $data = $request->validate( + [ + "nama_lokasi" => ["required", "string", "max:100"], + "max_space" => ["required", "integer", "min:0", "max:32767"], + "sisa" => ["required", "integer", "min:0", "max:32767", "lte:max_space"], + "prioritas" => ["nullable", "integer"], + ], + [ + "sisa.lte" => "Sisa kapasitas tidak boleh melebihi kapasitas maksimal.", + ] + ); + + $lokasi->update($data); + + return response()->json([ + "success" => true, + "message" => "Area parkir berhasil diperbarui.", + "data" => $lokasi, + ]); + } + + public function destroy($id_lokasi) + { + $lokasi = MasterLokasi::find($id_lokasi); + + if (!$lokasi) { + return response()->json([ + "success" => false, + "message" => "Area parkir tidak ditemukan.", + ], 404); + } + + $dipakai = DB::table("stasiunkerja")->where("id_lokasi", $lokasi->id_lokasi)->count(); + if ($dipakai > 0) { + return response()->json([ + "success" => false, + "message" => "Area {$lokasi->id_lokasi} dipakai {$dipakai} stasiun kerja, tidak bisa dihapus.", + ], 409); + } + + $dipakaiGate = DB::table("nama_pos")->where("area", $lokasi->id_lokasi)->count(); + if ($dipakaiGate > 0) { + return response()->json([ + "success" => false, + "message" => "Area {$lokasi->id_lokasi} dipakai {$dipakaiGate} pintu/pos, tidak bisa dihapus.", + ], 409); + } + + $lokasi->delete(); + + return response()->json([ + "success" => true, + "message" => "Area parkir berhasil dihapus.", + ]); + } +} diff --git a/app/Models/MasterLokasi.php b/app/Models/MasterLokasi.php new file mode 100644 index 0000000..354d15a --- /dev/null +++ b/app/Models/MasterLokasi.php @@ -0,0 +1,32 @@ + 'integer', + 'sisa' => 'integer', + 'prioritas' => 'integer', + ]; +} diff --git a/app/Models/NamaPos.php b/app/Models/NamaPos.php index e4c573d..2cd3d96 100644 --- a/app/Models/NamaPos.php +++ b/app/Models/NamaPos.php @@ -12,12 +12,18 @@ class NamaPos extends Model protected $table = 'nama_pos'; protected $primaryKey = 'id'; public $incrementing = false; + public $timestamps = false; protected $keyType = 'string'; protected $fillable = [ 'id','nama','jenis','tanggal','lokasi','grace_period','manless','area' ]; + public function areaParkir() + { + return $this->belongsTo(MasterLokasi::class, 'area', 'id_lokasi'); + } + public function posHardware() { return $this->hasOne(ConfigHardware::class, 'id_pos', 'id'); } diff --git a/resources/views/layouts/partial/horizontal-menu.blade.php b/resources/views/layouts/partial/horizontal-menu.blade.php index 085a984..8754719 100644 --- a/resources/views/layouts/partial/horizontal-menu.blade.php +++ b/resources/views/layouts/partial/horizontal-menu.blade.php @@ -25,6 +25,7 @@ diff --git a/resources/views/report/okupansi-area.blade.php b/resources/views/report/okupansi-area.blade.php new file mode 100644 index 0000000..67f5674 --- /dev/null +++ b/resources/views/report/okupansi-area.blade.php @@ -0,0 +1,387 @@ +@extends('layouts.master') + +@section('styles') + +@endsection + +@section('content') +
+
+
+
+
+
+
+

Okupansi Area Parkir

+ Menentukan jam area cenderung penuh dan cenderung kosong berdasarkan arus keluar-masuk pintu yang terhubung ke area. +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + Simulasi dimulai dari 0 lot terisi pada pukul 00:00 setiap hari. Pintu Masuk (jenis 0) mengurangi sisa lot sehingga okupansi naik, pintu Keluar (jenis 1) menambah sisa lot sehingga okupansi turun, dan nilai dibatasi 0 sampai kapasitas area. Transaksi berstatus arsip/void tidak dihitung. +
+
+
+
+
+
+ Area Dianalisis +
-
+ +
+
+
+
+
+
+
+ Area Pernah Penuh +
-
+ Mencapai kapasitas lot +
+
+
+
+
+
+
+ Jam Paling Padat +
-
+ +
+
+
+
+
+
+
+ Jam Paling Lengang +
-
+ +
+
+
+
+
+ +
+
Ringkasan per Area
+
+ + + + + + + + + + + + + + + +
AreaKapasitasRata-rata OkupansiJam Paling PadatJam Paling LengangPaling Sering PenuhStatus
Pilih filter lalu klik Proses.
+
+
Rincian per Jam
+
+ + + + + + + + + + + + + + + + +
AreaJamRata-rata LotOkupansiMasukKeluarHari PenuhStatus
Belum ada data.
+
+
+
+
+
+
+
+
+@endsection + +@section('javascript') + + + + +@endsection diff --git a/resources/views/report/statistik-pengunjung.blade.php b/resources/views/report/statistik-pengunjung.blade.php new file mode 100644 index 0000000..077fdf0 --- /dev/null +++ b/resources/views/report/statistik-pengunjung.blade.php @@ -0,0 +1,286 @@ +@extends('layouts.master') + +@section('styles') + +@endsection + +@section('content') +
+
+
+
+
+
+
+

Statistik Pengunjung

+ Jumlah kendaraan masuk berdasarkan waktu masuk. Fokus qty, tanpa data pendapatan. +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ Total Pengunjung +
-
+
+
+
+
+
+
+
+ Rata-rata / Periode +
-
+
+
+
+
+
+
+
+ Puncak +
-
+ +
+
+
+
+
+
+ + + + + + + + + + + +
PeriodePintu MasukJumlah Pengunjung
Pilih filter lalu klik Proses.
+
+
+
+
+
+
+
+
+@endsection + +@section('javascript') + + + + +@endsection diff --git a/resources/views/report/statistik.blade.php b/resources/views/report/statistik.blade.php index f7a397f..d39b9c5 100644 --- a/resources/views/report/statistik.blade.php +++ b/resources/views/report/statistik.blade.php @@ -15,8 +15,8 @@
-

Statistik Transaksi

- Periode berdasarkan tanggal masuk. Pecah per hari / minggu / bulan. +

Statistik Transaksi Keluar

+ Hanya transaksi yang sudah keluar. Periode berdasarkan waktu keluar, dipecah per hari / minggu / bulan.
@@ -60,9 +60,8 @@
-
Transaksi
-
-
Keluar
-
-
Pendapatan
-
+
Transaksi Keluar
-
+
Pendapatan
-
Tunai
-
Cashless
-
@@ -72,15 +71,14 @@ Periode - Transaksi - Keluar + Transaksi Keluar Pendapatan Tunai Cashless - Pilih filter lalu klik Proses. + Pilih filter lalu klik Proses.
@@ -145,7 +143,7 @@ function drawChart(rows, grup) { $('#filter-statistik').on('submit', function () { const btn = $('#btn-proses'); const btnHtml = btn.html(); btn.prop('disabled', true).html('Memproses...'); - $('#body-statistik').html('Memuat statistik...'); + $('#body-statistik').html('Memuat statistik...'); const grup = $('#grup').val(); $.ajax({ url: dataUrl, @@ -160,12 +158,11 @@ function drawChart(rows, grup) { success: function (r) { const s = r.ringkasan || {}; $('#sum-trx').text(fmtN(s.transaksi) + ' trx'); - $('#sum-keluar').text(fmtN(s.keluar) + ' trx'); $('#sum-pendapatan').text(fmtRp(s.pendapatan)); $('#sum-tunai').text(fmtRp(s.tunai)); $('#sum-cashless').text(fmtRp(s.cashless)); if (!r.data.length) { - $('#body-statistik').html('Data tidak ditemukan.'); + $('#body-statistik').html('Data tidak ditemukan.'); if (chart) chart.destroy(); return; } @@ -173,7 +170,6 @@ function drawChart(rows, grup) { r.data.forEach(function (row) { html += '' + labelPeriode(row.periode, grup) + '' + '' + fmtN(row.transaksi) + '' - + '' + fmtN(row.keluar) + '' + '' + fmtRp(row.pendapatan) + '' + '' + fmtRp(row.tunai) + '' + '' + fmtRp(row.cashless) + ''; @@ -185,7 +181,7 @@ function drawChart(rows, grup) { let msg = 'Gagal memuat statistik.'; if (xhr.responseJSON?.message) msg = xhr.responseJSON.message; else if (xhr.status === 422 && xhr.responseJSON?.errors) msg = Object.values(xhr.responseJSON.errors).flat().join(' '); - $('#body-statistik').html('' + msg + ''); + $('#body-statistik').html('' + msg + ''); Swal.fire({icon: 'error', title: 'Gagal memuat', text: msg}); }, complete: function () { diff --git a/resources/views/setup/gate/gate.blade.php b/resources/views/setup/gate/gate.blade.php index 08c0f56..db6ca2a 100644 --- a/resources/views/setup/gate/gate.blade.php +++ b/resources/views/setup/gate/gate.blade.php @@ -24,6 +24,14 @@ .text-label { color: #000000; } + + #example tbody tr:not(.edit-row) { + cursor: pointer; + } + + #example tbody tr.table-active td { + background-color: #e7f1ff; + } /* .table-selectable tbody tr { cursor: pointer; @@ -66,6 +74,7 @@ Mode Pintu Grace Periode Manless? + Area Parkir Pengaturan @@ -82,10 +91,17 @@ ? '' : ''; @endphp - + {{ $loop->iteration }} {{-- {{ $pintu->id }} --}} {{ $pintu->nama }} - ({{ $pintu->id }}) + {{ match($pintu->jenis) { 0 => 'Masuk', @@ -104,7 +120,18 @@ style="width:12px;height:12px;"> - + + @if ($pintu->areaParkir) + {{ $pintu->areaParkir->nama_lokasi }} + {{ $pintu->areaParkir->id_lokasi }} + @elseif ($pintu->area) + {{ $pintu->area }} + Area tidak ditemukan + @else + Belum diset + @endif + + Pos @@ -115,10 +142,10 @@
- - - - + + + +
@@ -134,6 +161,78 @@
+ + @endsection @section('javascript') @@ -154,6 +253,130 @@ + + +@endsection diff --git a/routes/web.php b/routes/web.php index cf3b6a7..ea93038 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\Lokasi\MasterLokasiController; use App\Http\Controllers\Parking\Setup\Petugas\MasterPetugasController; use App\Http\Controllers\Parking\Setup\UserLevel\LevelUserController; use App\Http\Controllers\Parking\Setup\UserLevel\LevelMenuController; @@ -22,6 +23,7 @@ use App\Http\Controllers\Parking\MemberStikerController; use App\Http\Controllers\Parking\Operasional\TransaksiParkirController; use App\Http\Controllers\Parking\Report\LaporanStiker; +use App\Http\Controllers\Parking\Report\ReportOkupansiAreaController; use App\Http\Controllers\Parking\Report\ReportTransaksiController; use App\Http\Controllers\Parking\SetupController; // use App\Http\Controllers\Parking\StikerController; @@ -81,6 +83,9 @@ ); Route::get("/dhcp/logs", [MasterConfigNetworkController::class, "logs"])->name("dhcp.logs"); Route::resource("vehicle", MasterVehicleController::class); + Route::resource("lokasi", MasterLokasiController::class) + ->parameters(["lokasi" => "id_lokasi"]) + ->names("lokasi"); 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"); @@ -121,6 +126,10 @@ Route::get("/laporan-per-pintu/data", [ReportTransaksiController::class, "gateData"])->name("gateData"); Route::get("/statistik", [ReportTransaksiController::class, "statistik"])->name("statistik"); Route::get("/statistik/data", [ReportTransaksiController::class, "statistikData"])->name("statistik.data"); + Route::get("/statistik-pengunjung", [ReportTransaksiController::class, "statistikPengunjung"])->name("statistik.pengunjung"); + Route::get("/statistik-pengunjung/data", [ReportTransaksiController::class, "statistikPengunjungData"])->name("statistik.pengunjung.data"); + Route::get("/okupansi-area", [ReportOkupansiAreaController::class, "index"])->name("okupansi.area"); + Route::get("/okupansi-area/data", [ReportOkupansiAreaController::class, "data"])->name("okupansi.area.data"); Route::prefix("stiker")->group(function () { Route::get("/harian", [LaporanStiker::class, "laporanStikerHarian"]);