CRUD area & gate, statistik pengunjung per pintu, laporan okupansi area
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Parking\Report;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MasterLokasi;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReportOkupansiAreaController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$area = MasterLokasi::orderBy("prioritas")->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";
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user