This commit is contained in:
root committed 2026-05-11 17:58:21 +00:00
1 parent b03c1d4619
commit b07423aef3
42 files changed
+1431 -394

No files matched your search

+4 -4
View File
@@ -241,7 +241,7 @@ $gradients = [
<div class="lot-card-stats">
<span><i class="bi bi-car-front"></i>{{ $lot['total'] }} مكان</span>
<span><i class="bi bi-currency-exchange"></i>{{ number_format($lot['price']) }} ل.س/س</span>
<span><i class="bi bi-currency-exchange"></i>{{ number_format($lot['price']) }} ليرة سورية/س</span>
<span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span>
</div>
@@ -290,7 +290,7 @@ $gradients = [
<div class="d-flex align-items-center gap-3">
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
@if(!$assignedLotId)
@if(count($assignedLotIds) > 1)
<a href="{{ route('operator.dashboard') }}"
class="btn btn-sm fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
@@ -962,13 +962,13 @@ async function openReceipt(id) {
document.getElementById('rcpt-name').textContent = d.customer_name || 'غير محدد';
document.getElementById('rcpt-entry').textContent = d.entry_time;
document.getElementById('rcpt-exit').textContent = d.exit_time;
document.getElementById('rcpt-total').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل.س';
document.getElementById('rcpt-total').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ليرة سورية';
const rows = d.fee_details.map(r => `
<div class="fee-row">
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
</div>`).join('');
document.getElementById('rcpt-breakdown').innerHTML =
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
+692
View File
@@ -0,0 +1,692 @@
@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/[email protected]/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