scp-syria/resources/views/operator/stats.blade.php
2026-05-11 17:58:21 +00:00

693 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.admin')
@section('title', 'إحصائيات المشغّل — دمشق باركينغ')
@section('page-title', 'إحصائيات المشغّل')
@section('styles')
<style>
/* ── KPI Cards ───────────────────────────────────────────────────────────── */
.kpi-card {
border-radius: 1rem;
padding: 1.25rem 1.375rem;
position: relative;
overflow: hidden;
border: none;
height: 100%;
}
.kpi-card::after {
content: '';
position: absolute;
inset-inline-end: -18px;
bottom: -18px;
width: 90px;
height: 90px;
border-radius: 50%;
opacity: .08;
background: currentColor;
}
.kpi-icon {
width: 44px; height: 44px;
border-radius: .75rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem;
margin-bottom: .875rem;
flex-shrink: 0;
}
.kpi-value {
font-size: 2rem;
font-weight: 900;
line-height: 1.1;
margin-bottom: .2rem;
}
.kpi-label {
font-size: .8rem;
font-weight: 600;
opacity: .75;
}
.kpi-sub {
font-size: .7rem;
opacity: .55;
margin-top: .125rem;
}
/* ── Chart cards ─────────────────────────────────────────────────────────── */
.chart-card {
border-radius: 1rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.06);
height: 100%;
display: flex; flex-direction: column;
}
.chart-card .card-header {
background: transparent;
border-bottom: 1px solid #f1f5f9;
padding: 1rem 1.25rem .75rem;
border-radius: 1rem 1rem 0 0;
flex-shrink: 0;
}
/* ── Lot status cards ────────────────────────────────────────────────────── */
.lot-stat-card {
background: #fff;
border-radius: 1rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.06);
padding: 1.125rem 1.25rem;
transition: transform .2s, box-shadow .2s;
}
.lot-stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 28px rgba(0,0,0,.1);
}
.lot-occ-bar { height: 6px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .625rem 0; }
.lot-occ-fill { height: 100%; border-radius: 4px; transition: width .6s cubic-bezier(.4,0,.2,1); }
/* ── Recent table ────────────────────────────────────────────────────────── */
.recent-table th { font-size: .75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.recent-table td { font-size: .85rem; vertical-align: middle; }
.plate-badge {
display: inline-block;
font-family: monospace;
font-weight: 800;
letter-spacing: 2px;
font-size: .82rem;
direction: ltr;
background: #f1f5f9;
color: #1e293b;
padding: .2em .6em;
border-radius: .4rem;
}
.pay-badge {
font-size: .7rem; font-weight: 700; padding: .25em .65em; border-radius: 20px;
}
.pay-cash { background: rgba(16,185,129,.1); color: #059669; }
.pay-upload { background: rgba(99,102,241,.1); color: #4f46e5; }
/* ── Work progress card ──────────────────────────────────────────────────── */
.work-card {
background: #fff;
border-radius: 1rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.06);
padding: 1.25rem 1.5rem;
}
.day-bar-track {
height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .5rem 0 .25rem;
}
.day-bar-fill {
height: 100%; border-radius: 4px;
background: linear-gradient(90deg, #6366f1, #10b981);
transition: width .8s cubic-bezier(.4,0,.2,1);
}
.work-stat {
display: flex; flex-direction: column; align-items: center;
padding: .75rem 1rem; border-radius: .75rem; flex: 1; min-width: 90px;
}
.work-stat-value { font-size: 1.5rem; font-weight: 900; line-height: 1.1; }
.work-stat-label { font-size: .72rem; font-weight: 600; opacity: .65; margin-top: .15rem; text-align: center; }
.trend-chip {
display: inline-flex; align-items: center; gap: .2rem;
font-size: .7rem; font-weight: 700; padding: .15em .55em; border-radius: 20px;
}
.trend-up { background: rgba(16,185,129,.1); color: #059669; }
.trend-down { background: rgba(239,68,68,.1); color: #dc2626; }
.trend-flat { background: rgba(100,116,139,.1); color: #64748b; }
/* ── No-lots empty state ─────────────────────────────────────────────────── */
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 4rem 2rem; text-align: center;
}
.empty-icon {
width: 80px; height: 80px;
background: rgba(99,102,241,.08);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 2.5rem; color: #6366f1; margin-bottom: 1.25rem;
}
</style>
@endsection
@section('content')
@if(empty($assignedLotIds))
{{-- ── No lots assigned ──────────────────────────────────────────────────── --}}
<div class="card border-0 shadow-sm" style="border-radius:1rem;">
<div class="empty-state">
<div class="empty-icon"><i class="bi bi-bar-chart"></i></div>
<h5 class="fw-800 mb-2" style="color:#0f172a;">لا توجد مواقف مخصصة</h5>
<p class="mb-0" style="color:#64748b;max-width:340px;">
لم يتم تعيينك في أي موقف بعد. تواصل مع المدير لتخصيص مواقف لحسابك.
</p>
</div>
</div>
@else
{{-- ── KPI Row ───────────────────────────────────────────────────────────────── --}}
<div class="row g-3 mb-4">
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);color:#e2e8f0;">
<div class="kpi-icon" style="background:rgba(99,102,241,.2);">
<i class="bi bi-car-front" style="color:#818cf8;"></i>
</div>
<div class="kpi-value" id="kpi-active" style="color:#fff;">--</div>
<div class="kpi-label">السيارات داخل المواقف</div>
<div class="kpi-sub">الآن · مشغول فعلياً</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#064e3b 0%,#065f46 100%);color:#d1fae5;">
<div class="kpi-icon" style="background:rgba(16,185,129,.2);">
<i class="bi bi-box-arrow-in-down" style="color:#34d399;"></i>
</div>
<div class="kpi-value" id="kpi-checkins" style="color:#fff;">--</div>
<div class="kpi-label">دخول اليوم</div>
<div class="kpi-sub">منذ منتصف الليل</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#1e1b4b 0%,#312e81 100%);color:#e0e7ff;">
<div class="kpi-icon" style="background:rgba(129,140,248,.2);">
<i class="bi bi-cash-stack" style="color:#a5b4fc;"></i>
</div>
<div class="kpi-value" id="kpi-revenue" style="color:#fff;font-size:1.4rem;">--</div>
<div class="kpi-label">إيرادات اليوم</div>
<div class="kpi-sub">ليرة سورية · مدفوع</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#4a044e 0%,#6b21a8 100%);color:#f3e8ff;">
<div class="kpi-icon" style="background:rgba(192,132,252,.2);">
<i class="bi bi-p-square" style="color:#c084fc;"></i>
</div>
<div class="kpi-value" id="kpi-available" style="color:#fff;">--</div>
<div class="kpi-label">أماكن متاحة</div>
<div class="kpi-sub">مجموع المواقف</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#451a03 0%,#92400e 100%);color:#fef3c7;">
<div class="kpi-icon" style="background:rgba(251,191,36,.2);">
<i class="bi bi-calendar-event" style="color:#fcd34d;"></i>
</div>
<div class="kpi-value" id="kpi-reservations" style="color:#fff;">--</div>
<div class="kpi-label">حجوزات منتظرة</div>
<div class="kpi-sub">لم تُفعَّل بعد</div>
</div>
</div>
</div>
{{-- ── Work Progress ────────────────────────────────────────────────────────── --}}
<div class="work-card mb-4">
{{-- Header row --}}
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h6 class="fw-800 mb-0" style="color:#0f172a;font-size:.95rem;">
<i class="bi bi-person-check me-2" style="color:#6366f1;"></i>
تقدم العمل اليوم
</h6>
<p class="mb-0 mt-1" style="font-size:.75rem;color:#94a3b8;" id="work-date">{{ now()->translatedFormat('l، j F Y') }}</p>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="badge" id="completion-badge" style="font-size:.75rem;padding:.4em .8em;"></span>
<span style="font-size:.75rem;color:#64748b;">معدل الإنجاز</span>
</div>
</div>
{{-- Day progress bar --}}
<div class="mb-1" style="font-size:.72rem;color:#94a3b8;display:flex;justify-content:space-between;">
<span>بداية اليوم 00:00</span>
<span id="work-now-label"></span>
<span>نهاية اليوم 24:00</span>
</div>
<div class="day-bar-track">
<div class="day-bar-fill" id="day-progress-fill" style="width:0%;"></div>
</div>
<p class="mb-3" style="font-size:.7rem;color:#94a3b8;text-align:center;" id="day-pct-label"></p>
{{-- Work stats row --}}
<div class="d-flex flex-wrap gap-2">
<div class="work-stat" style="background:rgba(16,185,129,.07);color:#065f46;">
<div class="work-stat-value" id="ws-checkins">--</div>
<div class="work-stat-label">دخول اليوم</div>
<div id="ws-vs-yesterday" class="mt-1"></div>
</div>
<div class="work-stat" style="background:rgba(99,102,241,.07);color:#312e81;">
<div class="work-stat-value" id="ws-checkouts">--</div>
<div class="work-stat-label">خروج مكتمل</div>
</div>
<div class="work-stat" style="background:rgba(239,68,68,.07);color:#7f1d1d;">
<div class="work-stat-value" id="ws-cancelled">--</div>
<div class="work-stat-label">ملغي</div>
</div>
<div class="work-stat" style="background:rgba(245,158,11,.07);color:#78350f;">
<div class="work-stat-value" id="ws-avg-stay">--</div>
<div class="work-stat-label">متوسط المدة</div>
</div>
<div class="work-stat" style="background:rgba(14,165,233,.07);color:#0c4a6e;">
<div class="work-stat-value" id="ws-peak-hour">--</div>
<div class="work-stat-label">الساعة الأكثر ازدحاماً</div>
</div>
</div>
</div>
{{-- ── Charts ────────────────────────────────────────────────────────────────── --}}
<div class="row g-3 mb-4">
{{-- Daily check-ins bar chart --}}
<div class="col-lg-5">
<div class="chart-card card">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-bar-chart-line me-2" style="color:#10b981;"></i>
الدخول آخر 7 أيام
</span>
<span class="badge" style="background:rgba(16,185,129,.1);color:#059669;font-size:.72rem;">دخول</span>
</div>
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
<canvas id="checkinsChart"></canvas>
</div>
</div>
</div>
{{-- Hourly activity today --}}
<div class="col-lg-4">
<div class="chart-card card">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-clock me-2" style="color:#f59e0b;"></i>
توزيع الدخول بالساعة
</span>
<span class="badge" style="background:rgba(245,158,11,.1);color:#d97706;font-size:.72rem;">اليوم</span>
</div>
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
<canvas id="hourlyChart"></canvas>
</div>
</div>
</div>
{{-- Daily revenue line chart --}}
<div class="col-lg-3">
<div class="chart-card card">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-graph-up-arrow me-2" style="color:#818cf8;"></i>
الإيرادات
</span>
<span class="badge" style="background:rgba(99,102,241,.1);color:#6366f1;font-size:.72rem;">ليرة سورية</span>
</div>
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
<canvas id="revenueChart"></canvas>
</div>
</div>
</div>
</div>
{{-- ── Per-lot status (only when operator has >1 lot) ────────────────────────── --}}
@if(count($lots) > 1)
<div class="mb-4">
<h6 class="fw-700 mb-3" style="color:#0f172a;font-size:.9rem;">
<i class="bi bi-buildings me-2" style="color:#6366f1;"></i>
حالة المواقف المخصصة
</h6>
<div class="row g-3" id="lotCards">
@foreach($lots as $lot)
<div class="col-lg-4 col-sm-6">
<div class="lot-stat-card">
<div class="d-flex align-items-start justify-content-between mb-2">
<div>
<div class="fw-800" style="font-size:.95rem;color:#0f172a;">{{ $lot->name }}</div>
<div style="font-size:.75rem;color:#94a3b8;">{{ $lot->address }}</div>
</div>
<span class="lot-status-badge-{{ $lot->id }} badge" style="font-size:.7rem;"></span>
</div>
<div class="lot-occ-bar">
<div class="lot-occ-fill lot-fill-{{ $lot->id }}" style="width:0;background:#6366f1;"></div>
</div>
<div class="d-flex justify-content-between" style="font-size:.78rem;color:#64748b;">
<span><i class="bi bi-car-front me-1"></i><span class="lot-active-{{ $lot->id }}"></span> مشغول</span>
<span><span class="lot-avail-{{ $lot->id }}"></span> / {{ $lot->total_capacity }} متاح</span>
</div>
<div class="mt-2 pt-2 border-top d-flex gap-3" style="font-size:.75rem;color:#64748b;border-color:#f1f5f9!important;">
<span><i class="bi bi-box-arrow-in-down me-1"></i>دخول اليوم: <strong class="lot-today-{{ $lot->id }}"></strong></span>
<span><i class="bi bi-cash me-1"></i>إيراد: <strong class="lot-rev-{{ $lot->id }}"></strong></span>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
{{-- ── Recent completions table ─────────────────────────────────────────────── --}}
<div class="card border-0 shadow-sm" style="border-radius:1rem;overflow:hidden;">
<div class="card-header bg-transparent d-flex align-items-center justify-content-between"
style="padding:1rem 1.25rem .75rem;border-bottom:1px solid #f1f5f9;">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-clock-history me-2" style="color:#0ea5e9;"></i>
آخر المدفوعات المنجزة
</span>
<span class="badge" id="last-refresh" style="background:#f1f5f9;color:#64748b;font-size:.7rem;"></span>
</div>
<div class="table-responsive">
<table class="table recent-table mb-0">
<thead style="background:#f8fafc;">
<tr>
<th class="px-4 py-3">اللوحة</th>
<th class="py-3">الموقف</th>
<th class="py-3">العميل</th>
<th class="py-3">وقت الدفع</th>
<th class="py-3 text-center">الرسوم</th>
<th class="py-3 text-center">الدفع</th>
</tr>
</thead>
<tbody id="recent-tbody">
<tr>
<td colspan="6" class="text-center py-5" style="color:#94a3b8;">
<div class="spinner-border spinner-border-sm me-2"></div>
جاري التحميل...
</td>
</tr>
</tbody>
</table>
</div>
</div>
@endif {{-- assignedLotIds not empty --}}
@endsection
@push('scripts')
@if(!empty($assignedLotIds))
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
let checkinsChart, revenueChart, hourlyChart;
const LOTS = @json($lots->map(fn($l) => ['id' => $l->id, 'name' => $l->name]));
const MULTI_LOT = LOTS.length > 1;
// ── Day progress bar (runs on its own clock) ──────────────────────────────────
function updateDayProgress() {
const now = new Date();
const pct = Math.round((now.getHours() * 60 + now.getMinutes()) / 1440 * 100);
const fill = document.getElementById('day-progress-fill');
const lbl = document.getElementById('day-pct-label');
const now2 = document.getElementById('work-now-label');
if (fill) fill.style.width = pct + '%';
if (lbl) lbl.textContent = `${pct}% من اليوم مضى`;
if (now2) now2.textContent = now.toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'}) + ' الآن';
}
updateDayProgress();
setInterval(updateDayProgress, 60000);
// ── Duration formatter ────────────────────────────────────────────────────────
function fmtDuration(mins) {
if (!mins) return '—';
const h = Math.floor(mins / 60);
const m = mins % 60;
return h > 0 ? `${h}س ${m}د` : `${m}د`;
}
// ── Load & render ─────────────────────────────────────────────────────────────
async function loadStats() {
try {
const { success, data } = await fetch('{{ route("operator.statsData") }}').then(r => r.json());
if (!success) return;
// KPI cards
document.getElementById('kpi-active').textContent = data.active_cars;
document.getElementById('kpi-checkins').textContent = data.checkins_today;
document.getElementById('kpi-revenue').textContent = data.revenue_today.toLocaleString('ar-SA') + ' ليرة سورية';
document.getElementById('kpi-available').textContent = data.available_spaces;
document.getElementById('kpi-reservations').textContent = data.pending_reservations;
// ── Work progress ──────────────────────────────────────────────────────
document.getElementById('ws-checkins').textContent = data.checkins_today;
document.getElementById('ws-checkouts').textContent = data.checkouts_today;
document.getElementById('ws-cancelled').textContent = data.cancelled_today;
document.getElementById('ws-avg-stay').textContent = fmtDuration(data.avg_duration_min);
const peak = data.peak_hour;
const peakStr = peak !== null && peak !== undefined
? new Date(2000,0,1,peak).toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'})
: '—';
document.getElementById('ws-peak-hour').textContent = peakStr;
// Completion rate badge
const cr = data.completion_rate ?? 100;
const crEl = document.getElementById('completion-badge');
if (crEl) {
crEl.textContent = cr + '%';
crEl.style.cssText = cr >= 80
? 'background:rgba(16,185,129,.12);color:#059669;font-size:.75rem;padding:.4em .8em;'
: cr >= 50
? 'background:rgba(245,158,11,.12);color:#d97706;font-size:.75rem;padding:.4em .8em;'
: 'background:rgba(239,68,68,.12);color:#dc2626;font-size:.75rem;padding:.4em .8em;';
}
// vs yesterday trend chip
const vsel = document.getElementById('ws-vs-yesterday');
if (vsel) {
const diff = data.checkins_today - data.checkins_yesterday;
const cls = diff > 0 ? 'trend-up' : diff < 0 ? 'trend-down' : 'trend-flat';
const icon = diff > 0 ? 'bi-arrow-up' : diff < 0 ? 'bi-arrow-down' : 'bi-dash';
const lbl = diff > 0 ? `+${diff} عن أمس` : diff < 0 ? `${diff} عن أمس` : 'مثل أمس';
vsel.innerHTML = `<span class="trend-chip ${cls}"><i class="bi ${icon}"></i>${lbl}</span>`;
}
// Charts
renderCheckinsChart(data.daily_checkins, data.daily_dates);
renderRevenueChart(data.daily_revenue, data.daily_dates);
renderHourlyChart(data.hourly_activity ?? []);
// Per-lot cards
if (MULTI_LOT && data.lot_stats) {
data.lot_stats.forEach(lot => {
const pct = lot.pct;
const fill = document.querySelector(`.lot-fill-${lot.id}`);
const badge = document.querySelector(`.lot-status-badge-${lot.id}`);
if (fill) { fill.style.width = pct + '%'; fill.style.background = pct >= 90 ? '#ef4444' : pct >= 60 ? '#f59e0b' : '#10b981'; }
if (badge) {
badge.textContent = pct >= 90 ? 'ممتلئ' : pct >= 60 ? 'مشغول' : 'متاح';
badge.style.cssText = pct >= 90
? 'background:rgba(239,68,68,.1);color:#dc2626;'
: pct >= 60
? 'background:rgba(245,158,11,.1);color:#d97706;'
: 'background:rgba(16,185,129,.1);color:#059669;';
}
const setText = (sel, val) => { const el = document.querySelector(sel); if (el) el.textContent = val; };
setText(`.lot-active-${lot.id}`, lot.active);
setText(`.lot-avail-${lot.id}`, lot.available);
setText(`.lot-today-${lot.id}`, lot.today_ins);
setText(`.lot-rev-${lot.id}`, lot.today_rev.toLocaleString('ar-SA'));
});
}
// Recent completions table
renderRecentTable(data.recent_completions ?? []);
document.getElementById('last-refresh').textContent = 'تحديث ' + new Date().toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'});
} catch (e) {
console.error(e);
}
}
// ── Bar chart: check-ins ──────────────────────────────────────────────────────
function renderCheckinsChart(counts, dates) {
const labels = dates.map(d => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
});
const fullLabels = dates.map(d => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('ar-SA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
});
if (checkinsChart) { checkinsChart.destroy(); }
checkinsChart = new Chart(document.getElementById('checkinsChart'), {
type: 'bar',
data: {
labels,
datasets: [{
data: counts,
backgroundColor: counts.map((_, i) => i === counts.length - 1 ? 'rgba(16,185,129,.9)' : 'rgba(16,185,129,.25)'),
borderColor: counts.map((_, i) => i === counts.length - 1 ? '#10b981' : 'rgba(16,185,129,.4)'),
borderWidth: 2,
borderRadius: 8,
hoverBackgroundColor: 'rgba(16,185,129,.7)',
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
title: ctx => fullLabels[ctx[0].dataIndex],
label: ctx => ` ${ctx.parsed.y} دخول`,
}},
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#f8fafc' } },
x: { grid: { display: false } },
},
},
});
}
// ── Line chart: revenue ───────────────────────────────────────────────────────
function renderRevenueChart(amounts, dates) {
const labels = dates.map(d => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
});
if (revenueChart) { revenueChart.destroy(); }
revenueChart = new Chart(document.getElementById('revenueChart'), {
type: 'line',
data: {
labels,
datasets: [{
data: amounts,
borderColor: '#818cf8',
backgroundColor: 'rgba(99,102,241,.08)',
borderWidth: 2.5,
pointBackgroundColor: '#6366f1',
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.4,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed.y.toLocaleString('ar-SA')} ليرة سورية`,
}},
},
scales: {
y: { beginAtZero: true, grid: { color: '#f8fafc' }, ticks: { callback: v => v.toLocaleString('ar-SA') } },
x: { grid: { display: false } },
},
},
});
}
// ── Hourly activity chart ─────────────────────────────────────────────────────
function renderHourlyChart(hourly) {
// Show only hours 623 to keep the chart readable
const hours = Array.from({length: 18}, (_, i) => i + 6);
const counts = hours.map(h => hourly[h] ?? 0);
const labels = hours.map(h => h + ':00');
const nowH = new Date().getHours();
const colors = counts.map((_, i) => {
const h = hours[i];
if (h === nowH) return 'rgba(245,158,11,.9)';
return counts[i] === Math.max(...counts) && counts[i] > 0 ? 'rgba(99,102,241,.8)' : 'rgba(99,102,241,.2)';
});
if (hourlyChart) { hourlyChart.destroy(); }
hourlyChart = new Chart(document.getElementById('hourlyChart'), {
type: 'bar',
data: {
labels,
datasets: [{
data: counts,
backgroundColor: colors,
borderColor: colors.map(c => c.replace(/[\d.]+\)$/, '1)')),
borderWidth: 1,
borderRadius: 4,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
title: ctx => `${hours[ctx[0].dataIndex]}:00`,
label: ctx => ` ${ctx.parsed.y} دخول`,
}},
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0, font: { size: 10 } }, grid: { color: '#f8fafc' } },
x: { grid: { display: false }, ticks: { font: { size: 9 }, maxRotation: 0 } },
},
},
});
}
// ── Recent completions ────────────────────────────────────────────────────────
function renderRecentTable(rows) {
const tbody = document.getElementById('recent-tbody');
if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center py-5" style="color:#94a3b8;">
<i class="bi bi-inbox d-block mb-2" style="font-size:2rem;opacity:.3;"></i>
لا توجد مدفوعات منجزة بعد
</td></tr>`;
return;
}
const fmt = iso => {
if (!iso) return '—';
return new Date(iso).toLocaleString('ar-SA', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
tbody.innerHTML = rows.map(r => {
const payBadge = r.payment_method === 'cash'
? '<span class="pay-badge pay-cash"><i class="bi bi-cash me-1"></i>نقدي</span>'
: '<span class="pay-badge pay-upload"><i class="bi bi-upload me-1"></i>تحويل</span>';
const fee = r.total_fee ? r.total_fee.toLocaleString('ar-SA') + ' ليرة سورية' : '—';
return `<tr>
<td class="px-4 py-3"><span class="plate-badge">${r.vehicle_plate ?? '—'}</span></td>
<td class="py-3" style="color:#475569;">${r.parking_lot?.name ?? '—'}</td>
<td class="py-3" style="color:#475569;">${r.customer_name ?? '—'}</td>
<td class="py-3" style="color:#64748b;font-size:.8rem;">${fmt(r.paid_at)}</td>
<td class="py-3 text-center fw-700" style="color:#0f172a;">${fee}</td>
<td class="py-3 text-center">${payBadge}</td>
</tr>`;
}).join('');
}
// ── Boot ──────────────────────────────────────────────────────────────────────
loadStats();
setInterval(loadStats, 30000);
</script>
@endif
@endpush