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");
|
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.
|
* Statistik jumlah pengunjung (kendaraan masuk) per jam / hari / minggu.
|
||||||
* Periode berdasarkan kolom tanggal. Response: rows per periode + ringkasan.
|
* 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)
|
public function statistikData(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
@@ -55,14 +129,16 @@ public function statistikData(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$bucket = match ($grup) {
|
$bucket = match ($grup) {
|
||||||
"minggu" => "date_trunc('week', tp.tanggal)::date",
|
"minggu" => "date_trunc('week', tp.waktu_keluar)::date",
|
||||||
"bulan" => "date_trunc('month', tp.tanggal)::date",
|
"bulan" => "date_trunc('month', tp.waktu_keluar)::date",
|
||||||
default => "tp.tanggal::date",
|
default => "tp.waktu_keluar::date",
|
||||||
};
|
};
|
||||||
|
|
||||||
$query = DB::table("transaksi_parkir as tp")
|
$query = DB::table("transaksi_parkir as tp")
|
||||||
->whereDate("tp.tanggal", ">=", $tglAwal)
|
->where("tp.status", 0)
|
||||||
->whereDate("tp.tanggal", "<=", $tglAkhir);
|
->whereNotNull("tp.waktu_keluar")
|
||||||
|
->whereDate("tp.waktu_keluar", ">=", $tglAwal)
|
||||||
|
->whereDate("tp.waktu_keluar", "<=", $tglAkhir);
|
||||||
|
|
||||||
if ($request->filled("status_transaksi")) {
|
if ($request->filled("status_transaksi")) {
|
||||||
$query->where("tp.status_transaksi", $request->input("status_transaksi"));
|
$query->where("tp.status_transaksi", $request->input("status_transaksi"));
|
||||||
@@ -78,10 +154,9 @@ public function statistikData(Request $request)
|
|||||||
$rows = $query
|
$rows = $query
|
||||||
->selectRaw("({$bucket}) as periode")
|
->selectRaw("({$bucket}) as periode")
|
||||||
->selectRaw("count(*) as transaksi")
|
->selectRaw("count(*) as transaksi")
|
||||||
->selectRaw("count(case when tp.status = 0 then 1 end) as keluar")
|
->selectRaw("coalesce(sum(tp.bayar_keluar), 0) as pendapatan")
|
||||||
->selectRaw("coalesce(sum(case when tp.status = 0 then tp.bayar_keluar end), 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.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.cara_bayar = 3 then tp.bayar_keluar end), 0) as cashless")
|
||||||
->selectRaw("coalesce(sum(case when tp.status = 0 and tp.cara_bayar = 3 then tp.bayar_keluar end), 0) as cashless")
|
|
||||||
->groupByRaw($bucket)
|
->groupByRaw($bucket)
|
||||||
->orderBy("periode")
|
->orderBy("periode")
|
||||||
->get();
|
->get();
|
||||||
@@ -94,7 +169,6 @@ public function statistikData(Request $request)
|
|||||||
"data" => $rows,
|
"data" => $rows,
|
||||||
"ringkasan" => [
|
"ringkasan" => [
|
||||||
"transaksi" => $rows->sum("transaksi"),
|
"transaksi" => $rows->sum("transaksi"),
|
||||||
"keluar" => $rows->sum("keluar"),
|
|
||||||
"pendapatan" => (float) $rows->sum("pendapatan"),
|
"pendapatan" => (float) $rows->sum("pendapatan"),
|
||||||
"tunai" => (float) $rows->sum("tunai"),
|
"tunai" => (float) $rows->sum("tunai"),
|
||||||
"cashless" => (float) $rows->sum("cashless"),
|
"cashless" => (float) $rows->sum("cashless"),
|
||||||
|
|||||||
@@ -3,86 +3,162 @@
|
|||||||
namespace App\Http\Controllers\Parking\Setup\Gate;
|
namespace App\Http\Controllers\Parking\Setup\Gate;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\MasterLokasi;
|
||||||
use App\Models\NamaPos;
|
use App\Models\NamaPos;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class MasterGateController extends Controller
|
class MasterGateController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function index()
|
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", "Area"));
|
||||||
return view ('setup.gate.gate', compact('Gate'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for creating a new resource.
|
|
||||||
*
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
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)
|
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)
|
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)
|
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)
|
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.",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Parking\Setup\Lokasi;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\MasterLokasi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class MasterLokasiController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$lokasi = MasterLokasi::orderBy("prioritas")->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.",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class MasterLokasi extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'master_lokasi';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id_lokasi';
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id_lokasi',
|
||||||
|
'nama_lokasi',
|
||||||
|
'max_space',
|
||||||
|
'sisa',
|
||||||
|
'prioritas',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'max_space' => 'integer',
|
||||||
|
'sisa' => 'integer',
|
||||||
|
'prioritas' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -12,12 +12,18 @@ class NamaPos extends Model
|
|||||||
protected $table = 'nama_pos';
|
protected $table = 'nama_pos';
|
||||||
protected $primaryKey = 'id';
|
protected $primaryKey = 'id';
|
||||||
public $incrementing = false;
|
public $incrementing = false;
|
||||||
|
public $timestamps = false;
|
||||||
protected $keyType = 'string';
|
protected $keyType = 'string';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'id','nama','jenis','tanggal','lokasi','grace_period','manless','area'
|
'id','nama','jenis','tanggal','lokasi','grace_period','manless','area'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function areaParkir()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(MasterLokasi::class, 'area', 'id_lokasi');
|
||||||
|
}
|
||||||
|
|
||||||
public function posHardware() {
|
public function posHardware() {
|
||||||
return $this->hasOne(ConfigHardware::class, 'id_pos', 'id');
|
return $this->hasOne(ConfigHardware::class, 'id_pos', 'id');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li class="nav-item"><a class="nav-link" href="/setup/master/gate">Master Pintu/Pos</a></li>
|
<li class="nav-item"><a class="nav-link" href="/setup/master/gate">Master Pintu/Pos</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/setup/master/vehicle">Master Kendaraan</a></li>
|
<li class="nav-item"><a class="nav-link" href="/setup/master/vehicle">Master Kendaraan</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/setup/master/lokasi">Master Area Parkir</a></li>
|
||||||
<!--<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Tarif</a></li>-->
|
<!--<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Tarif</a></li>-->
|
||||||
<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Shift</a></li>
|
<li class="nav-item"><a class="nav-link" href="/transaksi/member">Master Shift</a></li>
|
||||||
<hr>
|
<hr>
|
||||||
@@ -65,6 +66,8 @@
|
|||||||
<li class="nav-item"><a class="nav-link" href="/report/laporan-semua-transaksi">Transaksi Harian</a></li>
|
<li class="nav-item"><a class="nav-link" href="/report/laporan-semua-transaksi">Transaksi Harian</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/report/stiker/harian">Member Harian</a></li>
|
<li class="nav-item"><a class="nav-link" href="/report/stiker/harian">Member Harian</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/report/statistik">Statistik Transaksi</a></li>
|
<li class="nav-item"><a class="nav-link" href="/report/statistik">Statistik Transaksi</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/report/statistik-pengunjung">Statistik Pengunjung</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/report/okupansi-area">Okupansi Area Parkir</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.occ-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .8rem;
|
||||||
|
height: 100%;
|
||||||
|
padding: .9rem 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-left: 4px solid var(--accent, #0d6efd);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, var(--soft, #f8f9fa) 100%);
|
||||||
|
}
|
||||||
|
.occ-card::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -28px;
|
||||||
|
right: -28px;
|
||||||
|
width: 92px;
|
||||||
|
height: 92px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--soft, #f8f9fa);
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
.occ-card .occ-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--accent, #0d6efd);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.occ-card .occ-body { position: relative; z-index: 1; min-width: 0; }
|
||||||
|
.occ-card .occ-value { color: var(--accent, #0d6efd); font-size: 1.05rem; line-height: 1.2; }
|
||||||
|
.occ-card .occ-sub { display: block; margin-top: .15rem; font-size: .75rem; }
|
||||||
|
.occ-card--primary { --accent: #0d6efd; --soft: #e7f1ff; }
|
||||||
|
.occ-card--danger { --accent: #dc3545; --soft: #fdecee; }
|
||||||
|
.occ-card--warning { --accent: #fd7e14; --soft: #fff1e6; }
|
||||||
|
.occ-card--success { --accent: #198754; --soft: #e8f5ee; }
|
||||||
|
.occ-chart-wrap { position: relative; height: 360px; }
|
||||||
|
.occ-table td, .occ-table th { vertical-align: middle; }
|
||||||
|
.occ-scroll { max-height: 540px; overflow-y: auto; }
|
||||||
|
.occ-scroll thead th { position: sticky; top: 0; z-index: 2; background: #fff; }
|
||||||
|
</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-md-12 grid-margin stretch-card">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Okupansi Area Parkir</h4>
|
||||||
|
<small class="text-muted d-block mb-3">Menentukan jam area cenderung penuh dan cenderung kosong berdasarkan arus keluar-masuk pintu yang terhubung ke area.</small>
|
||||||
|
<form id="filter-okupansi" action="javascript:void(0)">
|
||||||
|
<div class="row align-items-end g-2">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" for="okupansi-area">Area Parkir</label>
|
||||||
|
<select class="form-select form-select-sm" id="okupansi-area">
|
||||||
|
<option value="all" selected>Semua Area</option>
|
||||||
|
@foreach($area as $a)
|
||||||
|
<option value="{{ $a->id_lokasi }}">{{ $a->nama_lokasi }} ({{ $a->id_lokasi }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label" for="okupansi-awal">Periode Awal</label>
|
||||||
|
<input type="date" class="form-control form-control-sm" id="okupansi-awal">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label" for="okupansi-akhir">Periode Akhir</label>
|
||||||
|
<input type="date" class="form-control form-control-sm" id="okupansi-akhir">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary" id="btn-proses-okupansi">Proses</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="alert alert-light border small mt-3 mb-3">
|
||||||
|
<i class="mdi mdi-information-outline"></i>
|
||||||
|
Simulasi dimulai dari 0 lot terisi pada pukul 00:00 setiap hari. Pintu <strong>Masuk (jenis 0)</strong> mengurangi sisa lot sehingga okupansi naik, pintu <strong>Keluar (jenis 1)</strong> menambah sisa lot sehingga okupansi turun, dan nilai dibatasi 0 sampai kapasitas area. Transaksi berstatus arsip/void tidak dihitung.
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="occ-card occ-card--primary">
|
||||||
|
<div class="occ-icon"><i class="mdi mdi-parking"></i></div>
|
||||||
|
<div class="occ-body">
|
||||||
|
<small class="text-muted">Area Dianalisis</small>
|
||||||
|
<div class="fw-bold occ-value" id="occ-jumlah-area">-</div>
|
||||||
|
<small class="text-muted occ-sub" id="occ-area-terpadat"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="occ-card occ-card--danger">
|
||||||
|
<div class="occ-icon"><i class="mdi mdi-alert-octagon-outline"></i></div>
|
||||||
|
<div class="occ-body">
|
||||||
|
<small class="text-muted">Area Pernah Penuh</small>
|
||||||
|
<div class="fw-bold occ-value" id="occ-area-penuh">-</div>
|
||||||
|
<small class="text-muted occ-sub">Mencapai kapasitas lot</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="occ-card occ-card--warning">
|
||||||
|
<div class="occ-icon"><i class="mdi mdi-clock-fast"></i></div>
|
||||||
|
<div class="occ-body">
|
||||||
|
<small class="text-muted">Jam Paling Padat</small>
|
||||||
|
<div class="fw-bold occ-value" id="occ-jam-padat">-</div>
|
||||||
|
<small class="text-muted occ-sub" id="occ-jam-padat-sub"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="occ-card occ-card--success">
|
||||||
|
<div class="occ-icon"><i class="mdi mdi-weather-night"></i></div>
|
||||||
|
<div class="occ-body">
|
||||||
|
<small class="text-muted">Jam Paling Lengang</small>
|
||||||
|
<div class="fw-bold occ-value" id="occ-jam-lengang">-</div>
|
||||||
|
<small class="text-muted occ-sub" id="occ-jam-lengang-sub"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="occ-chart-wrap mb-4">
|
||||||
|
<canvas id="chart-okupansi"></canvas>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title mb-2">Ringkasan per Area</h5>
|
||||||
|
<div class="table-responsive mb-4">
|
||||||
|
<table class="table table-bordered table-striped occ-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Area</th>
|
||||||
|
<th class="text-end">Kapasitas</th>
|
||||||
|
<th class="text-end">Rata-rata Okupansi</th>
|
||||||
|
<th>Jam Paling Padat</th>
|
||||||
|
<th>Jam Paling Lengang</th>
|
||||||
|
<th>Paling Sering Penuh</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="body-ringkasan-okupansi">
|
||||||
|
<tr><td colspan="7" class="text-center text-muted">Pilih filter lalu klik Proses.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title mb-2">Rincian per Jam</h5>
|
||||||
|
<div class="table-responsive occ-scroll">
|
||||||
|
<table class="table table-sm table-bordered table-striped occ-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Area</th>
|
||||||
|
<th>Jam</th>
|
||||||
|
<th class="text-end">Rata-rata Lot</th>
|
||||||
|
<th class="text-end">Okupansi</th>
|
||||||
|
<th class="text-end">Masuk</th>
|
||||||
|
<th class="text-end">Keluar</th>
|
||||||
|
<th class="text-end">Hari Penuh</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="body-detail-okupansi">
|
||||||
|
<tr><td colspan="8" class="text-center text-muted">Belum ada data.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 src="{{ asset('vendors/chart.js/Chart.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
const dataUrl = '{{ route('okupansi.area.data', [], false) }}';
|
||||||
|
let chart = null;
|
||||||
|
|
||||||
|
function localDate(offset) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + offset);
|
||||||
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#okupansi-akhir').val(localDate(0));
|
||||||
|
$('#okupansi-awal').val(localDate(-6));
|
||||||
|
|
||||||
|
function fmtN(value, decimals) {
|
||||||
|
return Number(value || 0).toLocaleString('id-ID', {
|
||||||
|
minimumFractionDigits: decimals || 0,
|
||||||
|
maximumFractionDigits: decimals || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(value) {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(status) {
|
||||||
|
const map = {
|
||||||
|
'Penuh': 'bg-danger',
|
||||||
|
'Hampir penuh': 'bg-warning text-dark',
|
||||||
|
'Ramai': 'bg-primary',
|
||||||
|
'Lengang': 'bg-success',
|
||||||
|
'Kosong': 'bg-secondary',
|
||||||
|
'Kapasitas belum diisi': 'bg-light text-dark border'
|
||||||
|
};
|
||||||
|
return '<span class="badge ' + (map[status] || 'bg-light text-dark border') + '">' + esc(status) + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart(rows) {
|
||||||
|
const canvas = document.getElementById('chart-okupansi');
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
if (!rows.length) return;
|
||||||
|
|
||||||
|
const labels = rows[0].jam.map(h => h.label);
|
||||||
|
const colors = ['#0d6efd', '#dc3545', '#198754', '#fd7e14', '#6f42c1', '#20c997', '#0dcaf0', '#ffc107', '#6c757d'];
|
||||||
|
const datasets = rows.map(function (area, index) {
|
||||||
|
return {
|
||||||
|
label: area.nama_lokasi,
|
||||||
|
data: area.jam.map(h => h.persen),
|
||||||
|
capacity: area.kapasitas,
|
||||||
|
borderColor: colors[index % colors.length],
|
||||||
|
backgroundColor: colors[index % colors.length],
|
||||||
|
fill: false,
|
||||||
|
tension: 0.25,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 2,
|
||||||
|
pointHoverRadius: 4
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
chart = new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: { labels: labels, datasets: datasets },
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
legend: { display: true },
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 110,
|
||||||
|
ticks: { callback: value => value + '%' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (item, data) {
|
||||||
|
const area = rows[item.datasetIndex];
|
||||||
|
const hour = area.jam[item.index];
|
||||||
|
return ' ' + data.datasets[item.datasetIndex].label + ': '
|
||||||
|
+ fmtN(hour.persen, 1) + '% (' + fmtN(hour.rata, 1) + '/' + fmtN(area.kapasitas) + ' lot)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(rows) {
|
||||||
|
if (!rows.length) {
|
||||||
|
$('#body-ringkasan-okupansi').html('<tr><td colspan="7" class="text-center">Data tidak ditemukan.</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
rows.forEach(function (area) {
|
||||||
|
const full = area.jam_penuh
|
||||||
|
? esc(area.jam_penuh.label) + '<small class="d-block text-muted">' + fmtN(area.jam_penuh.hari_penuh) + '/' + fmtN(area.jam_penuh.hari_dianalisis) + ' hari</small>'
|
||||||
|
: '<span class="text-muted">Belum pernah</span>';
|
||||||
|
html += '<tr>'
|
||||||
|
+ '<td><strong>' + esc(area.nama_lokasi) + '</strong><small class="d-block text-muted">' + esc(area.id_lokasi) + '</small></td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(area.kapasitas) + ' lot</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(area.rata_okupansi, 1) + ' lot<small class="d-block text-muted">' + fmtN(area.rata_persen, 1) + '%</small></td>'
|
||||||
|
+ '<td><strong>' + esc(area.puncak.label) + '</strong><small class="d-block text-muted">' + fmtN(area.puncak.rata, 1) + ' lot (' + fmtN(area.puncak.persen, 1) + '%)</small></td>'
|
||||||
|
+ '<td><strong>' + esc(area.kosong.label) + '</strong><small class="d-block text-muted">' + fmtN(area.kosong.rata, 1) + ' lot (' + fmtN(area.kosong.persen, 1) + '%)</small></td>'
|
||||||
|
+ '<td>' + full + '</td>'
|
||||||
|
+ '<td>' + statusBadge(area.status) + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
});
|
||||||
|
$('#body-ringkasan-okupansi').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetail(rows) {
|
||||||
|
if (!rows.length) {
|
||||||
|
$('#body-detail-okupansi').html('<tr><td colspan="8" class="text-center">Data tidak ditemukan.</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
rows.forEach(function (area) {
|
||||||
|
area.jam.forEach(function (hour) {
|
||||||
|
html += '<tr>'
|
||||||
|
+ '<td><strong>' + esc(area.nama_lokasi) + '</strong></td>'
|
||||||
|
+ '<td>' + esc(hour.label) + '</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(hour.rata, 1) + '</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(hour.persen, 1) + '%</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(hour.masuk) + '</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(hour.keluar) + '</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(hour.hari_penuh) + '/' + fmtN(hour.hari_dianalisis) + '</td>'
|
||||||
|
+ '<td>' + statusBadge(hour.status) + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$('#body-detail-okupansi').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCards(summary) {
|
||||||
|
const topArea = summary.area_terpadat;
|
||||||
|
$('#occ-jumlah-area').text(fmtN(summary.jumlah_area) + ' area');
|
||||||
|
$('#occ-area-terpadat').text(topArea ? 'Terpadat: ' + topArea.nama_lokasi + ' (' + fmtN(topArea.puncak.persen, 1) + '%)' : '-');
|
||||||
|
$('#occ-area-penuh').text(fmtN(summary.area_pernah_penuh) + ' area');
|
||||||
|
$('#occ-jam-padat').text(summary.jam_padat ? summary.jam_padat.label : '-');
|
||||||
|
$('#occ-jam-padat-sub').text(summary.jam_padat ? 'Rata-rata ' + fmtN(summary.jam_padat.rata, 1) + ' lot terisi' : '-');
|
||||||
|
$('#occ-jam-lengang').text(summary.jam_lengang ? summary.jam_lengang.label : '-');
|
||||||
|
$('#occ-jam-lengang-sub').text(summary.jam_lengang ? 'Rata-rata ' + fmtN(summary.jam_lengang.rata, 1) + ' lot terisi' : '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#filter-okupansi').on('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const btn = $('#btn-proses-okupansi');
|
||||||
|
const btnHtml = btn.html();
|
||||||
|
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Memproses...');
|
||||||
|
$('#body-ringkasan-okupansi').html('<tr><td colspan="7" class="text-center py-4"><span class="spinner-border spinner-border-sm me-2"></span>Menghitung okupansi...</td></tr>');
|
||||||
|
$('#body-detail-okupansi').html('<tr><td colspan="8" class="text-center py-4"><span class="spinner-border spinner-border-sm me-2"></span>Memuat rincian...</td></tr>');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: dataUrl,
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
area: $('#okupansi-area').val(),
|
||||||
|
tgl_awal: $('#okupansi-awal').val(),
|
||||||
|
tgl_akhir: $('#okupansi-akhir').val()
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
const rows = response.data || [];
|
||||||
|
renderCards(response.ringkasan || {});
|
||||||
|
renderChart(rows);
|
||||||
|
renderSummary(rows);
|
||||||
|
renderDetail(rows);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
let message = 'Gagal menghitung okupansi area.';
|
||||||
|
if (xhr.responseJSON?.message) message = xhr.responseJSON.message;
|
||||||
|
else if (xhr.status === 422 && xhr.responseJSON?.errors) message = Object.values(xhr.responseJSON.errors).flat().join(' ');
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
$('#body-ringkasan-okupansi').html('<tr><td colspan="7" class="text-center text-danger">' + esc(message) + '</td></tr>');
|
||||||
|
$('#body-detail-okupansi').html('<tr><td colspan="8" class="text-center text-danger">' + esc(message) + '</td></tr>');
|
||||||
|
Swal.fire({ icon: 'error', title: 'Gagal memuat', text: message });
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
btn.prop('disabled', false).html(btnHtml);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#filter-okupansi').submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.stat-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .8rem;
|
||||||
|
height: 100%;
|
||||||
|
padding: .9rem 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-left: 4px solid var(--accent, #0d6efd);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, var(--soft, #f8f9fa) 100%);
|
||||||
|
transition: transform .2s ease, box-shadow .2s ease;
|
||||||
|
}
|
||||||
|
.stat-card::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -28px;
|
||||||
|
right: -28px;
|
||||||
|
width: 92px;
|
||||||
|
height: 92px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--soft, #f8f9fa);
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, .08);
|
||||||
|
}
|
||||||
|
.stat-card .stat-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--accent, #0d6efd);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, .12);
|
||||||
|
}
|
||||||
|
.stat-card .stat-body { position: relative; z-index: 1; min-width: 0; }
|
||||||
|
.stat-card .stat-body small { display: block; margin-bottom: .15rem; }
|
||||||
|
.stat-card .stat-value { color: var(--accent, #0d6efd); font-size: 1.05rem; line-height: 1.2; }
|
||||||
|
.stat-card .stat-sub { display: block; margin-top: .15rem; font-size: .75rem; }
|
||||||
|
.stat-card--primary { --accent: #0d6efd; --soft: #e7f1ff; }
|
||||||
|
.stat-card--success { --accent: #198754; --soft: #e8f5ee; }
|
||||||
|
.stat-card--warning { --accent: #fd7e14; --soft: #fff1e6; }
|
||||||
|
</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-md-12 grid-margin stretch-card">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Statistik Pengunjung</h4>
|
||||||
|
<small class="text-muted d-block mb-3">Jumlah kendaraan masuk berdasarkan waktu masuk. Fokus qty, tanpa data pendapatan.</small>
|
||||||
|
<form id="filter-pengunjung" action="javascript:void(0)">
|
||||||
|
<div class="row align-items-end g-2">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label" for="grup-pengunjung">Pecah Per</label>
|
||||||
|
<select class="form-select form-select-sm" id="grup-pengunjung">
|
||||||
|
<option value="jam">Jam</option>
|
||||||
|
<option value="hari" selected>Hari</option>
|
||||||
|
<option value="minggu">Minggu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label" for="pengunjung-awal">Periode Awal</label>
|
||||||
|
<input type="date" class="form-control form-control-sm" id="pengunjung-awal">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label" for="pengunjung-akhir">Periode Akhir</label>
|
||||||
|
<input type="date" class="form-control form-control-sm" id="pengunjung-akhir">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label" for="pintu-pengunjung">Pintu Masuk</label>
|
||||||
|
<select class="form-select form-select-sm" id="pintu-pengunjung">
|
||||||
|
<option value="all" selected>Semua Pintu</option>
|
||||||
|
@foreach($pintu as $p)
|
||||||
|
<option value="{{ $p->id }}">{{ $p->nama }} ({{ $p->id }})</option>
|
||||||
|
@endforeach
|
||||||
|
<option value="__none__">Tanpa Pintu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary" id="btn-proses-pengunjung">Proses</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<hr class="my-3">
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card stat-card--primary">
|
||||||
|
<div class="stat-icon"><i class="mdi mdi-account-multiple-outline"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<small class="text-muted">Total Pengunjung</small>
|
||||||
|
<div class="fw-bold stat-value" id="peng-total">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card stat-card--success">
|
||||||
|
<div class="stat-icon"><i class="mdi mdi-chart-line"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<small class="text-muted">Rata-rata / Periode</small>
|
||||||
|
<div class="fw-bold stat-value" id="peng-rata">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card stat-card--warning">
|
||||||
|
<div class="stat-icon"><i class="mdi mdi-trophy-outline"></i></div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<small class="text-muted">Puncak</small>
|
||||||
|
<div class="fw-bold stat-value" id="peng-puncak">-</div>
|
||||||
|
<small class="text-muted stat-sub" id="peng-puncak-periode"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" style="max-height: 320px;"><canvas id="chart-pengunjung" height="120"></canvas></div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Periode</th>
|
||||||
|
<th>Pintu Masuk</th>
|
||||||
|
<th class="text-end">Jumlah Pengunjung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="body-pengunjung">
|
||||||
|
<tr><td colspan="3" class="text-center text-muted">Pilih filter lalu klik Proses.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 src="{{ asset('vendors/chart.js/Chart.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
const pengunjungUrl = '{{ route('statistik.pengunjung.data', [], false) }}';
|
||||||
|
let chartPengunjung = null;
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
$('#pengunjung-akhir').val(today);
|
||||||
|
$('#pengunjung-awal').val(new Date(Date.now() - 6 * 864e5).toISOString().split('T')[0]);
|
||||||
|
|
||||||
|
function fmtN(v) {
|
||||||
|
return Number(v || 0).toLocaleString('id-ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelPengunjung(p, grup) {
|
||||||
|
if (grup === 'jam') {
|
||||||
|
const d = new Date(p.replace(' ', 'T'));
|
||||||
|
return d.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' }) + ' ' + String(d.getHours()).padStart(2, '0') + ':00';
|
||||||
|
}
|
||||||
|
if (grup === 'minggu') return 'Minggu ' + p;
|
||||||
|
const d = new Date(p + 'T00:00:00');
|
||||||
|
return d.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPengunjungChart(rows, grup) {
|
||||||
|
const ctx = document.getElementById('chart-pengunjung');
|
||||||
|
if (chartPengunjung) chartPengunjung.destroy();
|
||||||
|
|
||||||
|
const labels = [...new Set(rows.map(r => r.periode))];
|
||||||
|
const perPintu = new Map();
|
||||||
|
rows.forEach(function (row) {
|
||||||
|
if (!perPintu.has(row.pintu)) perPintu.set(row.pintu, new Map());
|
||||||
|
perPintu.get(row.pintu).set(row.periode, Number(row.jumlah));
|
||||||
|
});
|
||||||
|
const colors = ['#0d6efd', '#198754', '#fd7e14', '#6f42c1', '#dc3545', '#20c997', '#0dcaf0', '#ffc107', '#6c757d'];
|
||||||
|
const datasets = [...perPintu.entries()].map(function (entry, i) {
|
||||||
|
const pintu = entry[0]; const data = entry[1];
|
||||||
|
return {
|
||||||
|
label: pintu,
|
||||||
|
data: labels.map(p => data.get(p) || 0),
|
||||||
|
borderColor: colors[i % colors.length],
|
||||||
|
backgroundColor: colors[i % colors.length],
|
||||||
|
fill: false,
|
||||||
|
tension: 0.25,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: labels.length > 60 ? 0 : 3,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
chartPengunjung = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels.map(p => labelPengunjung(p, grup)),
|
||||||
|
datasets: datasets,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
legend: { display: datasets.length > 1 },
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { callback: v => Number(v).toLocaleString('id-ID') },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (item, data) {
|
||||||
|
return ' ' + data.datasets[item.datasetIndex].label + ': ' + fmtN(data.datasets[item.datasetIndex].data[item.index]) + ' pengunjung';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#filter-pengunjung').on('submit', function () {
|
||||||
|
const btn = $('#btn-proses-pengunjung'); const btnHtml = btn.html();
|
||||||
|
const grup = $('#grup-pengunjung').val();
|
||||||
|
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Memproses...');
|
||||||
|
$('#body-pengunjung').html('<tr><td colspan="3" class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>Memuat pengunjung...</td></tr>');
|
||||||
|
$.ajax({
|
||||||
|
url: pengunjungUrl,
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
grup: grup,
|
||||||
|
tgl_awal: $('#pengunjung-awal').val(),
|
||||||
|
tgl_akhir: $('#pengunjung-akhir').val(),
|
||||||
|
pintu_masuk: $('#pintu-pengunjung').val(),
|
||||||
|
},
|
||||||
|
success: function (r) {
|
||||||
|
const s = r.ringkasan || {};
|
||||||
|
$('#peng-total').text(fmtN(s.total) + ' pengunjung');
|
||||||
|
$('#peng-rata').text(fmtN(s.rata_rata) + ' pengunjung');
|
||||||
|
$('#peng-puncak').text(fmtN(s.puncak) + ' pengunjung');
|
||||||
|
const puncakInfo = s.puncak_periode ? labelPengunjung(s.puncak_periode, grup) + (s.puncak_pintu ? ' · ' + s.puncak_pintu : '') : '-';
|
||||||
|
$('#peng-puncak-periode').text(puncakInfo);
|
||||||
|
if (!r.data.length) {
|
||||||
|
$('#body-pengunjung').html('<tr><td colspan="3" class="text-center">Data tidak ditemukan.</td></tr>');
|
||||||
|
if (chartPengunjung) chartPengunjung.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = '';
|
||||||
|
r.data.forEach(function (row) {
|
||||||
|
html += '<tr><td>' + labelPengunjung(row.periode, grup) + '</td>'
|
||||||
|
+ '<td>' + row.pintu + '</td>'
|
||||||
|
+ '<td class="text-end">' + fmtN(row.jumlah) + '</td></tr>';
|
||||||
|
});
|
||||||
|
$('#body-pengunjung').html(html);
|
||||||
|
drawPengunjungChart(r.data, grup);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
let msg = 'Gagal memuat statistik pengunjung.';
|
||||||
|
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-pengunjung').html('<tr><td colspan="3" class="text-center text-danger">' + msg + '</td></tr>');
|
||||||
|
Swal.fire({icon: 'error', title: 'Gagal memuat', text: msg});
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
btn.prop('disabled', false).html(btnHtml);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#filter-pengunjung').submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<div class="col-md-12 grid-margin stretch-card">
|
<div class="col-md-12 grid-margin stretch-card">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">Statistik Transaksi</h4>
|
<h4 class="card-title">Statistik Transaksi Keluar</h4>
|
||||||
<small class="text-muted d-block mb-3">Periode berdasarkan tanggal masuk. Pecah per hari / minggu / bulan.</small>
|
<small class="text-muted d-block mb-3">Hanya transaksi yang sudah keluar. Periode berdasarkan waktu keluar, dipecah per hari / minggu / bulan.</small>
|
||||||
<form id="filter-statistik" action="javascript:void(0)">
|
<form id="filter-statistik" action="javascript:void(0)">
|
||||||
<div class="row align-items-end g-2">
|
<div class="row align-items-end g-2">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
@@ -60,9 +60,8 @@
|
|||||||
</form>
|
</form>
|
||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="row g-2 mb-3" id="ringkasan">
|
<div class="row g-2 mb-3" id="ringkasan">
|
||||||
<div class="col-md-2"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Transaksi</small><div class="fw-bold" id="sum-trx">-</div></div></div>
|
<div class="col-md-3"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Transaksi Keluar</small><div class="fw-bold" id="sum-trx">-</div></div></div>
|
||||||
<div class="col-md-2"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Keluar</small><div class="fw-bold" id="sum-keluar">-</div></div></div>
|
<div class="col-md-3"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Pendapatan</small><div class="fw-bold text-success" id="sum-pendapatan">-</div></div></div>
|
||||||
<div class="col-md-2"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Pendapatan</small><div class="fw-bold text-success" id="sum-pendapatan">-</div></div></div>
|
|
||||||
<div class="col-md-3"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Tunai</small><div class="fw-bold" id="sum-tunai">-</div></div></div>
|
<div class="col-md-3"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Tunai</small><div class="fw-bold" id="sum-tunai">-</div></div></div>
|
||||||
<div class="col-md-3"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Cashless</small><div class="fw-bold text-info" id="sum-cashless">-</div></div></div>
|
<div class="col-md-3"><div class="border rounded p-2 h-100 stat-card"><small class="text-muted">Cashless</small><div class="fw-bold text-info" id="sum-cashless">-</div></div></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,15 +71,14 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Periode</th>
|
<th>Periode</th>
|
||||||
<th class="text-end">Transaksi</th>
|
<th class="text-end">Transaksi Keluar</th>
|
||||||
<th class="text-end">Keluar</th>
|
|
||||||
<th class="text-end">Pendapatan</th>
|
<th class="text-end">Pendapatan</th>
|
||||||
<th class="text-end">Tunai</th>
|
<th class="text-end">Tunai</th>
|
||||||
<th class="text-end">Cashless</th>
|
<th class="text-end">Cashless</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="body-statistik">
|
<tbody id="body-statistik">
|
||||||
<tr><td colspan="6" class="text-center text-muted">Pilih filter lalu klik Proses.</td></tr>
|
<tr><td colspan="5" class="text-center text-muted">Pilih filter lalu klik Proses.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +143,7 @@ function drawChart(rows, grup) {
|
|||||||
$('#filter-statistik').on('submit', function () {
|
$('#filter-statistik').on('submit', function () {
|
||||||
const btn = $('#btn-proses'); const btnHtml = btn.html();
|
const btn = $('#btn-proses'); const btnHtml = btn.html();
|
||||||
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Memproses...');
|
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Memproses...');
|
||||||
$('#body-statistik').html('<tr><td colspan="6" class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>Memuat statistik...</td></tr>');
|
$('#body-statistik').html('<tr><td colspan="5" class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>Memuat statistik...</td></tr>');
|
||||||
const grup = $('#grup').val();
|
const grup = $('#grup').val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: dataUrl,
|
url: dataUrl,
|
||||||
@@ -160,12 +158,11 @@ function drawChart(rows, grup) {
|
|||||||
success: function (r) {
|
success: function (r) {
|
||||||
const s = r.ringkasan || {};
|
const s = r.ringkasan || {};
|
||||||
$('#sum-trx').text(fmtN(s.transaksi) + ' trx');
|
$('#sum-trx').text(fmtN(s.transaksi) + ' trx');
|
||||||
$('#sum-keluar').text(fmtN(s.keluar) + ' trx');
|
|
||||||
$('#sum-pendapatan').text(fmtRp(s.pendapatan));
|
$('#sum-pendapatan').text(fmtRp(s.pendapatan));
|
||||||
$('#sum-tunai').text(fmtRp(s.tunai));
|
$('#sum-tunai').text(fmtRp(s.tunai));
|
||||||
$('#sum-cashless').text(fmtRp(s.cashless));
|
$('#sum-cashless').text(fmtRp(s.cashless));
|
||||||
if (!r.data.length) {
|
if (!r.data.length) {
|
||||||
$('#body-statistik').html('<tr><td colspan="6" class="text-center">Data tidak ditemukan.</td></tr>');
|
$('#body-statistik').html('<tr><td colspan="5" class="text-center">Data tidak ditemukan.</td></tr>');
|
||||||
if (chart) chart.destroy();
|
if (chart) chart.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -173,7 +170,6 @@ function drawChart(rows, grup) {
|
|||||||
r.data.forEach(function (row) {
|
r.data.forEach(function (row) {
|
||||||
html += '<tr><td>' + labelPeriode(row.periode, grup) + '</td>'
|
html += '<tr><td>' + labelPeriode(row.periode, grup) + '</td>'
|
||||||
+ '<td class="text-end">' + fmtN(row.transaksi) + '</td>'
|
+ '<td class="text-end">' + fmtN(row.transaksi) + '</td>'
|
||||||
+ '<td class="text-end">' + fmtN(row.keluar) + '</td>'
|
|
||||||
+ '<td class="text-end">' + fmtRp(row.pendapatan) + '</td>'
|
+ '<td class="text-end">' + fmtRp(row.pendapatan) + '</td>'
|
||||||
+ '<td class="text-end">' + fmtRp(row.tunai) + '</td>'
|
+ '<td class="text-end">' + fmtRp(row.tunai) + '</td>'
|
||||||
+ '<td class="text-end">' + fmtRp(row.cashless) + '</td></tr>';
|
+ '<td class="text-end">' + fmtRp(row.cashless) + '</td></tr>';
|
||||||
@@ -185,7 +181,7 @@ function drawChart(rows, grup) {
|
|||||||
let msg = 'Gagal memuat statistik.';
|
let msg = 'Gagal memuat statistik.';
|
||||||
if (xhr.responseJSON?.message) msg = xhr.responseJSON.message;
|
if (xhr.responseJSON?.message) msg = xhr.responseJSON.message;
|
||||||
else if (xhr.status === 422 && xhr.responseJSON?.errors) msg = Object.values(xhr.responseJSON.errors).flat().join(' ');
|
else if (xhr.status === 422 && xhr.responseJSON?.errors) msg = Object.values(xhr.responseJSON.errors).flat().join(' ');
|
||||||
$('#body-statistik').html('<tr><td colspan="6" class="text-center text-danger">' + msg + '</td></tr>');
|
$('#body-statistik').html('<tr><td colspan="5" class="text-center text-danger">' + msg + '</td></tr>');
|
||||||
Swal.fire({icon: 'error', title: 'Gagal memuat', text: msg});
|
Swal.fire({icon: 'error', title: 'Gagal memuat', text: msg});
|
||||||
},
|
},
|
||||||
complete: function () {
|
complete: function () {
|
||||||
|
|||||||
@@ -24,6 +24,14 @@
|
|||||||
.text-label {
|
.text-label {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#example tbody tr:not(.edit-row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#example tbody tr.table-active td {
|
||||||
|
background-color: #e7f1ff;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
.table-selectable tbody tr {
|
.table-selectable tbody tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -66,6 +74,7 @@
|
|||||||
<th style="text-align: center;">Mode Pintu</th>
|
<th style="text-align: center;">Mode Pintu</th>
|
||||||
<th style="text-align: center;">Grace Periode</th>
|
<th style="text-align: center;">Grace Periode</th>
|
||||||
<th style="text-align: center;">Manless?</th>
|
<th style="text-align: center;">Manless?</th>
|
||||||
|
<th style="text-align: center;" width="15%">Area Parkir</th>
|
||||||
<th style="text-align: center;">Pengaturan</th>
|
<th style="text-align: center;">Pengaturan</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -82,10 +91,17 @@
|
|||||||
? '<span class="d-inline-block rounded-circle bg-success" style="width:12px;height:12px;"></span>'
|
? '<span class="d-inline-block rounded-circle bg-success" style="width:12px;height:12px;"></span>'
|
||||||
: '<span class="d-inline-block rounded-circle bg-danger" style="width:12px;height:12px;"></span>';
|
: '<span class="d-inline-block rounded-circle bg-danger" style="width:12px;height:12px;"></span>';
|
||||||
@endphp
|
@endphp
|
||||||
<tr>
|
<tr data-gate-id="{{ $pintu->id }}"
|
||||||
|
data-gate-nama="{{ $pintu->nama }}"
|
||||||
|
data-gate-jenis="{{ $pintu->jenis }}"
|
||||||
|
data-gate-grace="{{ $pintu->grace_period }}"
|
||||||
|
data-gate-manless="{{ $pintu->manless }}"
|
||||||
|
data-gate-area="{{ $pintu->area }}"
|
||||||
|
data-gate-tanggal="{{ $pintu->tanggal }}">
|
||||||
<td class="text-center">{{ $loop->iteration }}</td>
|
<td class="text-center">{{ $loop->iteration }}</td>
|
||||||
{{-- <td class="text-center" width="7%">{{ $pintu->id }}</td> --}}
|
{{-- <td class="text-center" width="7%">{{ $pintu->id }}</td> --}}
|
||||||
<td>{{ $pintu->nama }} <strong> - ({{ $pintu->id }})</strong></td>
|
<td>{{ $pintu->nama }} <strong> - ({{ $pintu->id }})</strong></td>
|
||||||
|
|
||||||
<td width="10%">{{
|
<td width="10%">{{
|
||||||
match($pintu->jenis) {
|
match($pintu->jenis) {
|
||||||
0 => 'Masuk',
|
0 => 'Masuk',
|
||||||
@@ -104,7 +120,18 @@
|
|||||||
style="width:12px;height:12px;">
|
style="width:12px;height:12px;">
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" width="12%">
|
<td class="text-center">
|
||||||
|
@if ($pintu->areaParkir)
|
||||||
|
<span class="badge bg-primary">{{ $pintu->areaParkir->nama_lokasi }}</span>
|
||||||
|
<small class="text-muted d-block">{{ $pintu->areaParkir->id_lokasi }}</small>
|
||||||
|
@elseif ($pintu->area)
|
||||||
|
<span class="badge bg-warning text-dark">{{ $pintu->area }}</span>
|
||||||
|
<small class="text-muted d-block">Area tidak ditemukan</small>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Belum diset</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center text-nowrap" width="18%">
|
||||||
<a class="btn btn-warning btn-sm parameter-button" id="parameter" data-bs-toggle="modal" data-bs-target=".bd-ubah-modal-lg">Pos</a>
|
<a class="btn btn-warning btn-sm parameter-button" id="parameter" data-bs-toggle="modal" data-bs-target=".bd-ubah-modal-lg">Pos</a>
|
||||||
<button class="btn btn-primary btn-sm hardware-button" data-id="{{ $pintu->id }}" data-nama="{{ $pintu->nama }}">Hardware</button>
|
<button class="btn btn-primary btn-sm hardware-button" data-id="{{ $pintu->id }}" data-nama="{{ $pintu->nama }}">Hardware</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -115,10 +142,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-center align-items-center mt-2">
|
<div class="d-flex justify-content-center align-items-center mt-2">
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary btn-sm add-button" data-bs-toggle="modal" data-bs-target=".bd-example-modal-lg">Tambah</button>
|
<button class="btn btn-primary btn-sm add-button" id="btnAddGate" type="button">Tambah</button>
|
||||||
<button class="btn btn-warning btn-sm edit-button" id="btnEdit" data-bs-toggle="modal" data-bs-target=".bd-ubah-modal-lg">Ubah</button>
|
<button class="btn btn-warning btn-sm" id="btnEdit" type="button" disabled>Ubah</button>
|
||||||
<button class="btn btn-danger btn-sm" id="btnDelete">Hapus</button>
|
<button class="btn btn-danger btn-sm" id="btnDeleteGate" type="button" disabled>Hapus</button>
|
||||||
<button class="btn btn-danger btn-sm" id="btnDelete">Detail</button>
|
<button class="btn btn-info btn-sm" id="btnDetailGate" type="button" disabled>Detail</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ADD MEMMBER MODAL -->
|
<!-- ADD MEMMBER MODAL -->
|
||||||
@@ -134,6 +161,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="gate-form-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="form-gate">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="gate-form-title">Tambah Pintu/Pos</h5>
|
||||||
|
<button class="btn-close" data-bs-dismiss="modal" type="button"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="gate-mode">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="gate-id">ID Pintu/Pos</label>
|
||||||
|
<input class="form-control text-uppercase" id="gate-id" name="id" maxlength="10" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label" for="gate-nama">Nama Pintu/Pos</label>
|
||||||
|
<input class="form-control" id="gate-nama" name="nama" maxlength="100" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="gate-jenis">Mode Pintu</label>
|
||||||
|
<select class="form-select" id="gate-jenis" name="jenis" required>
|
||||||
|
<option value="0">Masuk</option>
|
||||||
|
<option value="1">Keluar</option>
|
||||||
|
<option value="2">Transit</option>
|
||||||
|
<option value="3">Emergency</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="gate-grace">Grace Periode</label>
|
||||||
|
<select class="form-select" id="gate-grace" name="grace_period">
|
||||||
|
<option value="1">Aktif</option>
|
||||||
|
<option value="0">Nonaktif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="gate-manless">Manless</label>
|
||||||
|
<select class="form-select" id="gate-manless" name="manless">
|
||||||
|
<option value="1">Ya</option>
|
||||||
|
<option value="0">Tidak</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="gate-area">Area Parkir</label>
|
||||||
|
<select class="form-select" id="gate-area" name="area">
|
||||||
|
<option value="">-- Tanpa Area --</option>
|
||||||
|
@foreach ($Area as $area)
|
||||||
|
<option value="{{ $area->id_lokasi }}">{{ $area->nama_lokasi }} ({{ $area->id_lokasi }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-light border" data-bs-dismiss="modal" type="button">Batal</button>
|
||||||
|
<button class="btn btn-primary" id="btn-save-gate" type="submit">Simpan</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="gate-detail-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Detail Pintu/Pos</h5>
|
||||||
|
<button class="btn-close" data-bs-dismiss="modal" type="button"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="gate-detail-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('javascript')
|
@section('javascript')
|
||||||
@@ -154,6 +253,130 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
store: "{{ route('gate.store', [], false) }}",
|
||||||
|
show: "{{ route('gate.show', ':id', false) }}",
|
||||||
|
update: "{{ route('gate.update', ':id', false) }}",
|
||||||
|
destroy: "{{ route('gate.destroy', ':id', false) }}"
|
||||||
|
};
|
||||||
|
const csrfToken = "{{ csrf_token() }}";
|
||||||
|
let selectedGate = null;
|
||||||
|
|
||||||
|
function openGateForm(mode, gate) {
|
||||||
|
const isEdit = mode === 'edit';
|
||||||
|
$('#form-gate')[0].reset();
|
||||||
|
$('#gate-mode').val(mode);
|
||||||
|
$('#gate-form-title').text(isEdit ? 'Ubah Pintu/Pos' : 'Tambah Pintu/Pos');
|
||||||
|
$('#gate-id').val(gate?.gateId || '').prop('readonly', isEdit);
|
||||||
|
$('#gate-nama').val(gate?.gateNama || '');
|
||||||
|
$('#gate-jenis').val(gate?.gateJenis ?? 0);
|
||||||
|
$('#gate-grace').val(gate?.gateGrace ?? 1);
|
||||||
|
$('#gate-manless').val(gate?.gateManless ?? 0);
|
||||||
|
$('#gate-area').val(gate?.gateArea || '');
|
||||||
|
new bootstrap.Modal(document.getElementById('gate-form-modal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#example tbody').on('click', 'tr:not(.edit-row)', function () {
|
||||||
|
$('#example tbody tr').removeClass('table-active');
|
||||||
|
$(this).addClass('table-active');
|
||||||
|
selectedGate = $(this).data();
|
||||||
|
$('#btnEdit, #btnDeleteGate, #btnDetailGate').prop('disabled', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnAddGate').on('click', function () {
|
||||||
|
openGateForm('create');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnEdit').on('click', function () {
|
||||||
|
if (!selectedGate) return;
|
||||||
|
openGateForm('edit', selectedGate);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#form-gate').on('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const isEdit = $('#gate-mode').val() === 'edit';
|
||||||
|
const id = $('#gate-id').val();
|
||||||
|
const btn = $('#btn-save-gate');
|
||||||
|
const btnHtml = btn.html();
|
||||||
|
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Menyimpan...');
|
||||||
|
$.ajax({
|
||||||
|
url: isEdit ? routes.update.replace(':id', encodeURIComponent(id)) : routes.store,
|
||||||
|
type: isEdit ? 'PUT' : 'POST',
|
||||||
|
data: $('#form-gate').serialize(),
|
||||||
|
headers: { 'X-CSRF-TOKEN': csrfToken },
|
||||||
|
success: function (r) {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('gate-form-modal'))?.hide();
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: r.message || 'Data pintu/pos disimpan.',
|
||||||
|
timer: 1300,
|
||||||
|
showConfirmButton: false
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
},
|
||||||
|
error: function (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 pintu/pos.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnDeleteGate').on('click', function () {
|
||||||
|
if (!selectedGate) return;
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Hapus pintu/pos?',
|
||||||
|
text: `${selectedGate.gateId} - ${selectedGate.gateNama}`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Ya, hapus',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
confirmButtonColor: '#dc3545',
|
||||||
|
showLoaderOnConfirm: true,
|
||||||
|
preConfirm: () => $.ajax({
|
||||||
|
url: routes.destroy.replace(':id', encodeURIComponent(selectedGate.gateId)),
|
||||||
|
type: 'DELETE',
|
||||||
|
headers: { 'X-CSRF-TOKEN': csrfToken }
|
||||||
|
}).catch(xhr => {
|
||||||
|
Swal.showValidationMessage(xhr.responseJSON?.message || 'Pintu/pos tidak dapat dihapus.');
|
||||||
|
})
|
||||||
|
}).then(result => {
|
||||||
|
if (result.isConfirmed && result.value) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: 'Pintu/pos dihapus.',
|
||||||
|
timer: 1300,
|
||||||
|
showConfirmButton: false
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnDetailGate').on('click', function () {
|
||||||
|
if (!selectedGate) return;
|
||||||
|
const btn = $(this);
|
||||||
|
const btnHtml = btn.html();
|
||||||
|
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Memuat...');
|
||||||
|
$('#gate-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('gate-detail-modal')).show();
|
||||||
|
$.get(routes.show.replace(':id', encodeURIComponent(selectedGate.gateId)), function (r) {
|
||||||
|
const d = r.data;
|
||||||
|
const jenis = { 0: 'Masuk', 1: 'Keluar', 2: 'Transit', 3: 'Emergency' };
|
||||||
|
const area = d.area_parkir
|
||||||
|
? `${esc(d.area_parkir.nama_lokasi)} (${esc(d.area_parkir.id_lokasi)})`
|
||||||
|
: (d.area ? esc(d.area) + ' (tidak ditemukan)' : 'Tanpa Area');
|
||||||
|
$('#gate-detail-body').html(`<dl class="row mb-0"><dt class="col-5">ID Pintu/Pos</dt><dd class="col-7">${esc(d.id)}</dd><dt class="col-5">Nama</dt><dd class="col-7">${esc(d.nama)}</dd><dt class="col-5">Mode Pintu</dt><dd class="col-7">${jenis[d.jenis] ?? '-'}</dd><dt class="col-5">Grace Periode</dt><dd class="col-7">${Number(d.grace_period) === 1 ? 'Aktif' : 'Nonaktif'}</dd><dt class="col-5">Manless</dt><dd class="col-7">${Number(d.manless) === 1 ? 'Ya' : 'Tidak'}</dd><dt class="col-5">Area Parkir</dt><dd class="col-7">${area}</dd><dt class="col-5">Dibuat</dt><dd class="col-7">${esc(d.tanggal || '-')}</dd></dl>`);
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
$('#gate-detail-body').html('<div class="text-danger text-center">' + (xhr.responseJSON?.message || 'Gagal memuat detail.') + '</div>');
|
||||||
|
}).always(function () {
|
||||||
|
btn.prop('disabled', false).html(btnHtml);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '.hardware-button', function () {
|
$(document).on('click', '.hardware-button', function () {
|
||||||
let btn = $(this);
|
let btn = $(this);
|
||||||
let btnHtml = btn.html();
|
let btnHtml = btn.html();
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
@extends('layouts.master')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.card-body label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body .form-control,
|
||||||
|
.card-body .form-select {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-selectable tbody tr:not(.lokasi-form-row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-selectable tbody tr:not(.lokasi-form-row):hover {
|
||||||
|
background-color: #f2f6ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid page-body-wrapper">
|
||||||
|
<div class="main-panel">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-md-10 grid-margin stretch-card">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Master Area Parkir</h4>
|
||||||
|
<div class="table-responsive mb-2">
|
||||||
|
<table class="table table-bordered table-selectable" id="lokasi-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="p-3" width="5%">No.</th>
|
||||||
|
<th style="text-align: center;" width="12%">ID Lokasi</th>
|
||||||
|
<th style="text-align: center;">Nama Area</th>
|
||||||
|
<th style="text-align: center;" width="12%">Kapasitas</th>
|
||||||
|
<th style="text-align: center;" width="12%">Sisa</th>
|
||||||
|
<th style="text-align: center;" width="10%">Prioritas</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($lokasi as $item)
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ $loop->iteration }}</td>
|
||||||
|
<td class="text-center"
|
||||||
|
data-lokasi-id="{{ $item->id_lokasi }}"
|
||||||
|
data-lokasi-nama="{{ $item->nama_lokasi }}"
|
||||||
|
data-lokasi-max="{{ $item->max_space }}"
|
||||||
|
data-lokasi-sisa="{{ $item->sisa }}"
|
||||||
|
data-lokasi-prioritas="{{ $item->prioritas }}">
|
||||||
|
{{ $item->id_lokasi }}
|
||||||
|
</td>
|
||||||
|
<td>{{ $item->nama_lokasi }}</td>
|
||||||
|
<td class="text-end">{{ number_format((int) $item->max_space, 0, ',', '.') }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<span class="badge {{ (int) $item->sisa > 0 ? 'bg-success' : 'bg-danger' }}">
|
||||||
|
{{ number_format((int) $item->sisa, 0, ',', '.') }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{{ $item->prioritas ?? '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr class="empty-lokasi-row">
|
||||||
|
<td colspan="6" class="text-center text-muted">Belum ada area parkir.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<button class="btn btn-primary btn-sm" id="btnAddLokasi" type="button"><i class="fa fa-plus me-1"></i>Tambah</button>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-info btn-sm" id="btnDetailLokasi" type="button" disabled><i class="fa fa-circle-info me-1"></i>Detail</button>
|
||||||
|
<button class="btn btn-warning btn-sm" id="btnEditLokasi" type="button" disabled><i class="fa fa-pen me-1"></i>Ubah</button>
|
||||||
|
<button class="btn btn-danger btn-sm" id="btnDeleteLokasi" type="button" disabled><i class="fa fa-trash me-1"></i>Hapus</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="lokasi-detail-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Detail Area Parkir</h5>
|
||||||
|
<button class="btn-close" data-bs-dismiss="modal" type="button"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="lokasi-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 () {
|
||||||
|
let selected = null;
|
||||||
|
const csrf = $('meta[name="csrf-token"]').attr('content');
|
||||||
|
const routes = {
|
||||||
|
store: '{{ route('lokasi.store', [], false) }}',
|
||||||
|
show: '{{ route('lokasi.show', ':id', false) }}',
|
||||||
|
update: '{{ route('lokasi.update', ':id', false) }}',
|
||||||
|
destroy: '{{ route('lokasi.destroy', ':id', false) }}'
|
||||||
|
};
|
||||||
|
|
||||||
|
function setSelected(row) {
|
||||||
|
$('#lokasi-table tbody tr').removeClass('table-active');
|
||||||
|
selected = row ? $(row).find('[data-lokasi-id]').data() : null;
|
||||||
|
if (row) $(row).addClass('table-active');
|
||||||
|
$('#btnDetailLokasi, #btnEditLokasi, #btnDeleteLokasi').prop('disabled', !selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formRow(mode, lokasi) {
|
||||||
|
const isEdit = mode === 'edit';
|
||||||
|
return `<tr class="lokasi-form-row"><td colspan="6"><form class="lokasi-inline-form" data-mode="${mode}" data-id="${lokasi?.lokasiId || ''}"><div class="row g-2 align-items-end"><div class="col-md-2"><label class="form-label mb-1">ID Lokasi</label><input class="form-control text-uppercase" name="id_lokasi" maxlength="10" value="${lokasi?.lokasiId || ''}" ${isEdit ? 'readonly' : ''} required></div><div class="col-md-3"><label class="form-label mb-1">Nama Area</label><input class="form-control" name="nama_lokasi" maxlength="100" value="${lokasi?.lokasiNama || ''}" required></div><div class="col-md-2"><label class="form-label mb-1">Kapasitas</label><input class="form-control" type="number" name="max_space" min="0" max="32767" value="${lokasi?.lokasiMax ?? 0}" required></div><div class="col-md-2"><label class="form-label mb-1">Sisa</label><input class="form-control" type="number" name="sisa" min="0" max="32767" value="${lokasi?.lokasiSisa ?? 0}" required></div><div class="col-md-1"><label class="form-label mb-1">Prioritas</label><input class="form-control" type="number" name="prioritas" value="${lokasi?.lokasiPrioritas ?? ''}"></div><div class="col-md-2 d-flex gap-2"><button class="btn btn-success" type="submit">Simpan</button><button class="btn btn-light border cancel-lokasi-form" type="button">Batal</button></div></div></form></td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#lokasi-table tbody').on('click', 'tr:not(.lokasi-form-row):not(.empty-lokasi-row)', function () {
|
||||||
|
setSelected(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnAddLokasi').on('click', function () {
|
||||||
|
$('.lokasi-form-row').remove();
|
||||||
|
$('#lokasi-table tbody').prepend(formRow('create'));
|
||||||
|
setSelected(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnEditLokasi').on('click', function () {
|
||||||
|
if (!selected) return;
|
||||||
|
$('.lokasi-form-row').remove();
|
||||||
|
$('#lokasi-table tbody tr').filter(function () {
|
||||||
|
return $(this).find('[data-lokasi-id]').data('lokasiId') === selected.lokasiId;
|
||||||
|
}).after(formRow('edit', selected));
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.cancel-lokasi-form', function () {
|
||||||
|
$(this).closest('tr').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('input', '.lokasi-inline-form [name="max_space"]', function () {
|
||||||
|
const form = $(this).closest('form');
|
||||||
|
if (form.data('mode') === 'create') form.find('[name="sisa"]').val($(this).val());
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('submit', '.lokasi-inline-form', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = $(this);
|
||||||
|
const id = form.data('id');
|
||||||
|
const isEdit = form.data('mode') === 'edit';
|
||||||
|
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: isEdit ? routes.update.replace(':id', encodeURIComponent(id)) : routes.store,
|
||||||
|
type: isEdit ? 'PUT' : 'POST',
|
||||||
|
data: form.serialize(),
|
||||||
|
headers: { 'X-CSRF-TOKEN': csrf },
|
||||||
|
success: function (r) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: r.message || 'Data area parkir disimpan.',
|
||||||
|
timer: 1300,
|
||||||
|
showConfirmButton: false
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
},
|
||||||
|
error: function (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 area parkir.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnDeleteLokasi').on('click', function () {
|
||||||
|
if (!selected) return;
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Hapus area parkir?',
|
||||||
|
text: `${selected.lokasiId} - ${selected.lokasiNama}`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Ya, hapus',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
confirmButtonColor: '#dc3545',
|
||||||
|
showLoaderOnConfirm: true,
|
||||||
|
preConfirm: () => $.ajax({
|
||||||
|
url: routes.destroy.replace(':id', encodeURIComponent(selected.lokasiId)),
|
||||||
|
type: 'DELETE',
|
||||||
|
headers: { 'X-CSRF-TOKEN': csrf }
|
||||||
|
}).catch(xhr => {
|
||||||
|
Swal.showValidationMessage(xhr.responseJSON?.message || 'Area parkir tidak dapat dihapus.');
|
||||||
|
})
|
||||||
|
}).then(result => {
|
||||||
|
if (result.isConfirmed && result.value) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: 'Area parkir dihapus.',
|
||||||
|
timer: 1300,
|
||||||
|
showConfirmButton: false
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnDetailLokasi').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...');
|
||||||
|
$('#lokasi-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('lokasi-detail-modal')).show();
|
||||||
|
$.get(routes.show.replace(':id', encodeURIComponent(selected.lokasiId)), function (r) {
|
||||||
|
const d = r.data;
|
||||||
|
$('#lokasi-detail-body').html(`<dl class="row mb-0"><dt class="col-5">ID Lokasi</dt><dd class="col-7">${d.id_lokasi}</dd><dt class="col-5">Nama Area</dt><dd class="col-7">${d.nama_lokasi}</dd><dt class="col-5">Kapasitas</dt><dd class="col-7">${Number(d.max_space || 0).toLocaleString('id-ID')}</dd><dt class="col-5">Sisa</dt><dd class="col-7">${Number(d.sisa || 0).toLocaleString('id-ID')}</dd><dt class="col-5">Prioritas</dt><dd class="col-7">${d.prioritas ?? '-'}</dd></dl>`);
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
$('#lokasi-detail-body').html('<div class="text-danger text-center">' + (xhr.responseJSON?.message || 'Gagal memuat detail.') + '</div>');
|
||||||
|
}).always(function () {
|
||||||
|
btn.prop('disabled', false).html(btnHtml);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
use App\Http\Controllers\Parking\Setup\Gate\MasterGateParameterController;
|
use App\Http\Controllers\Parking\Setup\Gate\MasterGateParameterController;
|
||||||
use App\Http\Controllers\Parking\Setup\Vehicle\MasterVehicleController;
|
use App\Http\Controllers\Parking\Setup\Vehicle\MasterVehicleController;
|
||||||
use App\Http\Controllers\Parking\Setup\Vehicle\ModifiedVehiclepriceController;
|
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\Petugas\MasterPetugasController;
|
||||||
use App\Http\Controllers\Parking\Setup\UserLevel\LevelUserController;
|
use App\Http\Controllers\Parking\Setup\UserLevel\LevelUserController;
|
||||||
use App\Http\Controllers\Parking\Setup\UserLevel\LevelMenuController;
|
use App\Http\Controllers\Parking\Setup\UserLevel\LevelMenuController;
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
use App\Http\Controllers\Parking\MemberStikerController;
|
use App\Http\Controllers\Parking\MemberStikerController;
|
||||||
use App\Http\Controllers\Parking\Operasional\TransaksiParkirController;
|
use App\Http\Controllers\Parking\Operasional\TransaksiParkirController;
|
||||||
use App\Http\Controllers\Parking\Report\LaporanStiker;
|
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\Report\ReportTransaksiController;
|
||||||
use App\Http\Controllers\Parking\SetupController;
|
use App\Http\Controllers\Parking\SetupController;
|
||||||
// use App\Http\Controllers\Parking\StikerController;
|
// use App\Http\Controllers\Parking\StikerController;
|
||||||
@@ -81,6 +83,9 @@
|
|||||||
);
|
);
|
||||||
Route::get("/dhcp/logs", [MasterConfigNetworkController::class, "logs"])->name("dhcp.logs");
|
Route::get("/dhcp/logs", [MasterConfigNetworkController::class, "logs"])->name("dhcp.logs");
|
||||||
Route::resource("vehicle", MasterVehicleController::class);
|
Route::resource("vehicle", MasterVehicleController::class);
|
||||||
|
Route::resource("lokasi", MasterLokasiController::class)
|
||||||
|
->parameters(["lokasi" => "id_lokasi"])
|
||||||
|
->names("lokasi");
|
||||||
Route::resource("price", ModifiedVehiclepriceController::class)->names("vehiclePrice");
|
Route::resource("price", ModifiedVehiclepriceController::class)->names("vehiclePrice");
|
||||||
Route::get("petugas/{petugas}/foto", [MasterPetugasController::class, "foto"])->name("petugas.foto");
|
Route::get("petugas/{petugas}/foto", [MasterPetugasController::class, "foto"])->name("petugas.foto");
|
||||||
Route::resource("petugas", MasterPetugasController::class)->names("petugas");
|
Route::resource("petugas", MasterPetugasController::class)->names("petugas");
|
||||||
@@ -121,6 +126,10 @@
|
|||||||
Route::get("/laporan-per-pintu/data", [ReportTransaksiController::class, "gateData"])->name("gateData");
|
Route::get("/laporan-per-pintu/data", [ReportTransaksiController::class, "gateData"])->name("gateData");
|
||||||
Route::get("/statistik", [ReportTransaksiController::class, "statistik"])->name("statistik");
|
Route::get("/statistik", [ReportTransaksiController::class, "statistik"])->name("statistik");
|
||||||
Route::get("/statistik/data", [ReportTransaksiController::class, "statistikData"])->name("statistik.data");
|
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::prefix("stiker")->group(function () {
|
||||||
Route::get("/harian", [LaporanStiker::class, "laporanStikerHarian"]);
|
Route::get("/harian", [LaporanStiker::class, "laporanStikerHarian"]);
|
||||||
|
|||||||
Reference in New Issue
Block a user