693 lines
32 KiB
PHP
693 lines
32 KiB
PHP
@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 6–23 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
|