Files
backend_parkir/resources/views/report/statistik-pengunjung.blade.php
T

287 lines
15 KiB
PHP

@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