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
+
+
+
+
+ | Area |
+ Kapasitas |
+ Rata-rata Okupansi |
+ Jam Paling Padat |
+ Jam Paling Lengang |
+ Paling Sering Penuh |
+ Status |
+
+
+
+ | Pilih filter lalu klik Proses. |
+
+
+
+
Rincian per Jam
+
+
+
+
+
+
+
+
+@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.
+
+
+
+
+
+
+
+
+
Rata-rata / Periode
+
-
+
+
+
+
+
+
+
+
+
+
+ | Periode |
+ Pintu Masuk |
+ Jumlah 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.