CRUD area & gate, statistik pengunjung per pintu, laporan okupansi area
This commit is contained in:
@@ -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="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Statistik Transaksi</h4>
|
||||
<small class="text-muted d-block mb-3">Periode berdasarkan tanggal masuk. Pecah per hari / minggu / bulan.</small>
|
||||
<h4 class="card-title">Statistik Transaksi Keluar</h4>
|
||||
<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)">
|
||||
<div class="row align-items-end g-2">
|
||||
<div class="col-md-2">
|
||||
@@ -60,9 +60,8 @@
|
||||
</form>
|
||||
<hr class="my-3">
|
||||
<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-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-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">Transaksi Keluar</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">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">Cashless</small><div class="fw-bold text-info" id="sum-cashless">-</div></div></div>
|
||||
</div>
|
||||
@@ -72,15 +71,14 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Periode</th>
|
||||
<th class="text-end">Transaksi</th>
|
||||
<th class="text-end">Keluar</th>
|
||||
<th class="text-end">Transaksi Keluar</th>
|
||||
<th class="text-end">Pendapatan</th>
|
||||
<th class="text-end">Tunai</th>
|
||||
<th class="text-end">Cashless</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
@@ -145,7 +143,7 @@ function drawChart(rows, grup) {
|
||||
$('#filter-statistik').on('submit', function () {
|
||||
const btn = $('#btn-proses'); const btnHtml = btn.html();
|
||||
btn.prop('disabled', true).html('<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();
|
||||
$.ajax({
|
||||
url: dataUrl,
|
||||
@@ -160,12 +158,11 @@ function drawChart(rows, grup) {
|
||||
success: function (r) {
|
||||
const s = r.ringkasan || {};
|
||||
$('#sum-trx').text(fmtN(s.transaksi) + ' trx');
|
||||
$('#sum-keluar').text(fmtN(s.keluar) + ' trx');
|
||||
$('#sum-pendapatan').text(fmtRp(s.pendapatan));
|
||||
$('#sum-tunai').text(fmtRp(s.tunai));
|
||||
$('#sum-cashless').text(fmtRp(s.cashless));
|
||||
if (!r.data.length) {
|
||||
$('#body-statistik').html('<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();
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +170,6 @@ function drawChart(rows, grup) {
|
||||
r.data.forEach(function (row) {
|
||||
html += '<tr><td>' + labelPeriode(row.periode, grup) + '</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.tunai) + '</td>'
|
||||
+ '<td class="text-end">' + fmtRp(row.cashless) + '</td></tr>';
|
||||
@@ -185,7 +181,7 @@ function drawChart(rows, grup) {
|
||||
let msg = 'Gagal memuat statistik.';
|
||||
if (xhr.responseJSON?.message) msg = xhr.responseJSON.message;
|
||||
else if (xhr.status === 422 && xhr.responseJSON?.errors) msg = Object.values(xhr.responseJSON.errors).flat().join(' ');
|
||||
$('#body-statistik').html('<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});
|
||||
},
|
||||
complete: function () {
|
||||
|
||||
Reference in New Issue
Block a user