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

+14 -10
View File
@@ -396,10 +396,12 @@
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
<div id="endStep2Pay" style="display:none;" class="p-4">
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-sm fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-left me-1"></i>رجوع
</button>
</div>
{{-- Times --}}
<div class="row g-2 mb-3">
@@ -469,10 +471,12 @@
{{-- ── STEP 2B : force close ───────────────────────────────── --}}
<div id="endStep2Force" style="display:none;" class="p-4">
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-sm fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-left me-1"></i>رجوع
</button>
</div>
<div class="text-center py-2 mb-3">
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
@@ -609,12 +613,12 @@ async function endChoose(type) {
const d = data.data;
document.getElementById('payRcptEntry').textContent = d.entry_time;
document.getElementById('payRcptExit').textContent = d.exit_time;
document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل.س';
document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ليرة سورية';
const rows = d.fee_details.map(r => `
<div style="display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px dashed #f1f5f9;font-size:.82rem;">
<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('payRcptBreakdown').innerHTML =
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
+1 -1
View File
@@ -274,7 +274,7 @@ async function loadStats() {
document.getElementById('total-bookings').textContent = data.total_bookings ?? 0;
document.getElementById('active-bookings').textContent = data.active_bookings ?? 0;
document.getElementById('occupancy-rate').textContent = (data.occupancy_rate ?? 0) + '%';
document.getElementById('estimated-revenue').textContent = (data.estimated_revenue ?? 0).toLocaleString('ar-SA') + ' ر.س';
document.getElementById('estimated-revenue').textContent = (data.estimated_revenue ?? 0).toLocaleString('ar-SA') + ' ليرة سورية';
document.getElementById('available-spots').textContent = data.available_spots ?? 0;
} catch {}
}
+58 -32
View File
@@ -22,6 +22,15 @@
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600;
background:#f1f5f9; color:#94a3b8;
}
.lot-checkbox-list {
border:1px solid #e2e8f0; border-radius:.625rem; max-height:180px; overflow-y:auto; padding:.5rem;
}
.lot-checkbox-item {
display:flex; align-items:center; gap:.6rem; padding:.375rem .5rem; border-radius:.4rem; cursor:pointer;
font-size:.875rem; color:#374151; transition:background .15s;
}
.lot-checkbox-item:hover { background:#f8fafc; }
.lot-checkbox-item input[type=checkbox] { width:1rem; height:1rem; cursor:pointer; flex-shrink:0; }
</style>
@endsection
@@ -61,11 +70,15 @@
<td class="py-3" style="color:#475569;">{{ $op->email }}</td>
<td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td>
<td class="py-3">
@if($op->assignedLot)
<span class="lot-pill">
<i class="bi bi-buildings"></i>
{{ $op->assignedLot->name }}
</span>
@if($op->assignedLots->isNotEmpty())
<div class="d-flex flex-wrap gap-1">
@foreach($op->assignedLots as $assignedLot)
<span class="lot-pill">
<i class="bi bi-buildings"></i>
{{ $assignedLot->name }}
</span>
@endforeach
</div>
@else
<span class="no-lot-pill">
<i class="bi bi-dash-circle"></i>
@@ -81,7 +94,7 @@
data-name="{{ $op->name }}"
data-email="{{ $op->email }}"
data-phone="{{ $op->phone ?? '' }}"
data-lot="{{ $op->parking_lot_id ?? '' }}">
data-lots="{{ json_encode($op->assignedLots->pluck('id')) }}">
<i class="bi bi-pencil me-1"></i>تعديل
</button>
<button class="btn btn-sm fw-600 btn-delete-op"
@@ -134,13 +147,17 @@
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
</div>
<div class="mb-1">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
<select id="createLot" class="form-select">
<option value="">— بدون تخصيص —</option>
@foreach($lots as $lot)
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
@endforeach
</select>
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
<div class="lot-checkbox-list" id="createLotList">
@forelse($lots as $lot)
<label class="lot-checkbox-item">
<input type="checkbox" class="create-lot-cb" value="{{ $lot->id }}">
<span>{{ $lot->name }}</span>
</label>
@empty
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
@endforelse
</div>
</div>
</div>
<div class="modal-footer border-0 pt-1">
@@ -186,13 +203,17 @@
<input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
</div>
<div class="mb-1">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص</label>
<select id="editLot" class="form-select">
<option value="">— بدون تخصيص —</option>
@foreach($lots as $lot)
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
@endforeach
</select>
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة</label>
<div class="lot-checkbox-list" id="editLotList">
@forelse($lots as $lot)
<label class="lot-checkbox-item">
<input type="checkbox" class="edit-lot-cb" value="{{ $lot->id }}">
<span>{{ $lot->name }}</span>
</label>
@empty
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
@endforelse
</div>
</div>
</div>
<div class="modal-footer border-0 pt-1">
@@ -271,7 +292,7 @@ document.addEventListener('DOMContentLoaded', function () {
// ── Open create ───────────────────────────────────────────────────────────
document.getElementById('btnOpenCreate').addEventListener('click', function () {
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
document.getElementById('createLot').value = '';
document.querySelectorAll('.create-lot-cb').forEach(cb => cb.checked = false);
document.getElementById('createError').classList.add('d-none');
modal('createModal').show();
});
@@ -284,7 +305,10 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('editEmail').value = this.dataset.email;
document.getElementById('editPhone').value = this.dataset.phone || '';
document.getElementById('editPassword').value = '';
document.getElementById('editLot').value = this.dataset.lot || '';
const assignedIds = JSON.parse(this.dataset.lots || '[]');
document.querySelectorAll('.edit-lot-cb').forEach(cb => {
cb.checked = assignedIds.includes(parseInt(cb.value));
});
document.getElementById('editError').classList.add('d-none');
modal('editModal').show();
});
@@ -309,15 +333,16 @@ document.addEventListener('DOMContentLoaded', function () {
spinner.classList.remove('d-none');
errEl.classList.add('d-none');
try {
const createLotIds = [...document.querySelectorAll('.create-lot-cb:checked')].map(cb => parseInt(cb.value));
const res = await fetch('{{ route("admin.operators.store") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({
name: document.getElementById('createName').value.trim(),
email: document.getElementById('createEmail').value.trim(),
phone: document.getElementById('createPhone').value.trim() || null,
password: document.getElementById('createPassword').value,
parking_lot_id: document.getElementById('createLot').value || null,
name: document.getElementById('createName').value.trim(),
email: document.getElementById('createEmail').value.trim(),
phone: document.getElementById('createPhone').value.trim() || null,
password: document.getElementById('createPassword').value,
lot_ids: createLotIds,
}),
});
const json = await res.json();
@@ -343,15 +368,16 @@ document.addEventListener('DOMContentLoaded', function () {
spinner.classList.remove('d-none');
errEl.classList.add('d-none');
try {
const editLotIds = [...document.querySelectorAll('.edit-lot-cb:checked')].map(cb => parseInt(cb.value));
const res = await fetch(`/admin/operators/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({
name: document.getElementById('editName').value.trim(),
email: document.getElementById('editEmail').value.trim(),
phone: document.getElementById('editPhone').value.trim() || null,
password: document.getElementById('editPassword').value || null,
parking_lot_id: document.getElementById('editLot').value || null,
name: document.getElementById('editName').value.trim(),
email: document.getElementById('editEmail').value.trim(),
phone: document.getElementById('editPhone').value.trim() || null,
password: document.getElementById('editPassword').value || null,
lot_ids: editLotIds,
}),
});
const json = await res.json();
@@ -209,7 +209,7 @@ $gradients = [
</span>
<span class="lot-card-stat" style="{{ !empty($lot->pricing_rules) ? 'border-color:#fbbf24;color:#92400e;' : '' }}">
<i class="bi bi-{{ !empty($lot->pricing_rules) ? 'tags' : 'tag' }}" style="color:{{ !empty($lot->pricing_rules) ? '#f59e0b' : '#10b981' }};"></i>
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ر.س/س
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ليرة سورية/س
@if(!empty($lot->pricing_rules))
<span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span>
@endif
@@ -385,7 +385,7 @@ $gradients = [
<input type="number" id="p_base" class="form-control"
step="0.01" min="0" placeholder="0.00"
oninput="syncBaseHints()">
<span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ر.س / ساعة</span>
<span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ليرة سورية / ساعة</span>
</div>
<p class="text-xs mt-1 mb-0" style="color:#94a3b8;">
الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً.
@@ -425,7 +425,7 @@ $gradients = [
step="0.01" min="0"
placeholder="مثل السعر الأساسي"
style="font-family:'Cairo',sans-serif;">
<span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ر.س</span>
<span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ليرة سورية</span>
</div>
</div>
<div style="width:70px;text-align:center;">
-11
View File
@@ -72,16 +72,5 @@
إنشاء حساب جديد
</a>
<div class="mt-4 p-3 rounded-3" style="background:#f0f9ff;border:1px solid #bae6fd;">
<p class="text-xs fw-700 mb-2" style="color:#0369a1;">
<i class="bi bi-key-fill me-1"></i>حسابات الاختبار
</p>
<p class="text-xs mb-1" style="color:#0369a1;">
<strong>مدير:</strong> admin@damascusparking.com / admin123
</p>
<p class="text-xs mb-0" style="color:#0369a1;">
<strong>مشغّل:</strong> operator@damascusparking.com / operator123
</p>
</div>
@endsection
+62 -26
View File
@@ -15,33 +15,69 @@
/* Bootstrap resets max-width:100% on all <img> which breaks Leaflet tiles */
.leaflet-container img { max-width: none !important; box-shadow: none !important; }
.leaflet-container { direction: ltr; }
/* ── Desktop: map + list fill remaining viewport height ─────────────── */
@media (min-width: 992px) {
html, body { height: 100%; overflow: hidden; }
body { display: flex; flex-direction: column; }
.app-topbar { flex-shrink: 0; }
.mob-hero-compact { flex-shrink: 0; }
#mainContent {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding-bottom: 0.75rem !important;
}
#mainContent > .row {
flex: 1;
min-height: 0;
}
#mainContent .col-lg-8,
#mainContent .col-lg-4 {
display: flex;
flex-direction: column;
}
/* Map card */
#mainContent .col-lg-8 > .card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
#mainContent .col-lg-8 .card-body {
flex: 1;
min-height: 0;
overflow: hidden;
}
#map { height: 100% !important; }
/* List card */
#mainContent .col-lg-4 > .card {
flex: 1;
min-height: 0;
position: static !important;
display: flex;
flex-direction: column;
}
#parkingList {
flex: 1;
min-height: 0;
max-height: none !important;
overflow-y: auto;
}
}
</style>
</head>
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
<header class="public-header">
<div class="container">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div style="width:40px;height:40px;background:#6366f1;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-p-square-fill text-white" style="font-size:1.25rem;"></i>
</div>
<div>
<div class="fw-800" style="color:#f8fafc;font-size:1.05rem;line-height:1.2;">دمشق باركينغ</div>
<div style="color:#94a3b8;font-size:.72rem;">مواقف السيارات في دمشق</div>
</div>
</div>
<div class="d-flex align-items-center gap-2">
@include('partials.user-dropdown')
</div>
</div>
</div>
</header>
@include('partials.topbar')
{{-- ══ HERO SEARCH ═════════════════════════════════════════════════════════ --}}
<div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;">
<div class="container text-center">
<div class="container-fluid text-center">
<h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);">
ابحث عن موقف سيارات في دمشق
</h1>
@@ -72,7 +108,7 @@
</div>
{{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}}
<div class="container py-4 mob-nav-pad">
<div id="mainContent" class="container-fluid py-4 mob-nav-pad">
<div class="row g-3">
{{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}}
@@ -81,7 +117,7 @@
{{-- MAP --}}
<div class="col-lg-8 order-lg-1 mob-section-map">
<div class="card h-100" style="min-height:520px;">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700 text-sm" style="color:#0f172a;">
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
@@ -90,7 +126,7 @@
<span class="badge badge-soft-primary text-xs" id="map-count">--</span>
</div>
<div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;">
<div id="map" style="height:500px;"></div>
<div id="map"></div>
</div>
</div>
</div>
@@ -105,7 +141,7 @@
</span>
<span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span>
</div>
<div style="max-height:500px;overflow-y:auto;scrollbar-width:thin;" id="parkingList">
<div style="overflow-y:auto;scrollbar-width:thin;" id="parkingList">
@forelse($lots as $lot)
@php
$avail = $lot['avail'];
@@ -130,7 +166,7 @@
<p class="text-xs mb-2" style="color:#94a3b8;">{{ $lot['address'] }}</p>
<div class="d-flex gap-3 text-xs" style="color:#64748b;">
<span><i class="bi bi-car-front me-1"></i>{{ $total }}</span>
<span><i class="bi bi-currency-exchange me-1"></i>{{ number_format($lot['price']) }} ر.س</span>
<span><i class="bi bi-currency-exchange me-1"></i>{{ number_format($lot['price']) }} ليرة سورية</span>
<span><i class="bi bi-clock me-1"></i>{{ $lot['hours'] }}</span>
</div>
</div>
@@ -370,7 +406,7 @@
if (l.avail < l.total * .2) return { cls: 'avail-limited', text: `${l.avail} محدود`, pct: Math.round((l.total - l.avail) / l.total * 100) };
return { cls: 'avail-open', text: `${l.avail} متاح`, pct: Math.round((l.total - l.avail) / l.total * 100) };
}
const fmtPrice = p => new Intl.NumberFormat('ar-SY').format(p) + ' ر.س';
const fmtPrice = p => new Intl.NumberFormat('ar-SY').format(p) + ' ليرة سورية';
// ── Map (lazy-init on mobile) ─────────────────────────────────────────
let map = null, mapReady = false;
@@ -379,7 +415,7 @@
if (mapReady) { map?.invalidateSize(); return; }
mapReady = true;
map = L.map('map').setView([33.5138, 36.2765], 12);
map = L.map('map', { attributionControl: false }).setView([33.5138, 36.2765], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
+100 -136
View File
@@ -17,152 +17,103 @@
<div class="app-layout">
{{-- ════════════════════════════════════════
SIDEBAR (appears on the RIGHT in RTL
because it is the first flex child)
TOPBAR — full width, topmost
════════════════════════════════════════ --}}
<aside class="app-sidebar" id="appSidebar">
{{-- Logo --}}
<a href="{{ route('admin.dashboard') }}" class="sidebar-logo">
<div class="logo-icon">
<i class="bi bi-p-square-fill"></i>
</div>
<div>
<div class="logo-text">دمشق باركينغ</div>
<div class="logo-sub">لوحة الإدارة</div>
</div>
</a>
{{-- Navigation --}}
<nav class="sidebar-nav">
<div class="sidebar-section">الرئيسية</div>
<a href="{{ route('admin.dashboard') }}"
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-speedometer2 sidebar-icon"></i>
<span>لوحة التحكم</span>
</a>
<div class="sidebar-section">المواقف والحجوزات</div>
<a href="{{ route('admin.parking-lots.index') }}"
class="sidebar-link {{ request()->routeIs('admin.parking-lots.*') ? 'active' : '' }}">
<i class="bi bi-buildings sidebar-icon"></i>
<span>المواقف</span>
</a>
<a href="{{ route('admin.bookings.active') }}"
class="sidebar-link {{ request()->routeIs('admin.bookings.*') ? 'active' : '' }}">
<i class="bi bi-calendar-check sidebar-icon"></i>
<span>الحجوزات النشطة</span>
</a>
<div class="sidebar-section">التشغيل</div>
<a href="{{ route('admin.operators.index') }}"
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
<i class="bi bi-people sidebar-icon"></i>
<span>المشغّلون</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}">
<i class="bi bi-person-badge sidebar-icon"></i>
<span>لوحة المشغّل</span>
</a>
</nav>
{{-- Footer: user info + logout --}}
<div class="sidebar-footer">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="user-avatar">
{{ mb_substr(auth()->user()?->name ?? 'م', 0, 1) }}
</div>
<div style="min-width:0">
<div class="user-name text-truncate">{{ auth()->user()?->name ?? 'المستخدم' }}</div>
<div class="user-role">
{{ auth()->user()?->role === 'admin' ? 'مدير النظام' : 'مشغّل' }}
</div>
</div>
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="btn btn-sm w-100 mt-1 text-start"
style="background:rgba(239,68,68,.12);color:#f87171;border:none;border-radius:.5rem;padding:.45rem .75rem;font-family:'Cairo',sans-serif;font-size:.82rem;">
<i class="bi bi-box-arrow-left me-2"></i>تسجيل الخروج
</button>
</form>
</div>
</aside>
@include('partials.topbar', ['isAdminLayout' => true])
{{-- ════════════════════════════════════════
MAIN BODY
INNER ROW — sidebar + content
════════════════════════════════════════ --}}
<div class="app-body">
<div class="app-inner">
{{-- Topbar --}}
<header class="app-topbar">
<div class="d-flex align-items-center gap-3">
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
<i class="bi bi-list"></i>
</button>
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
</div>
<div class="topbar-actions d-flex align-items-center gap-2">
{{-- Desktop: link to public site --}}
<a href="{{ route('parking.index') }}"
class="btn btn-sm d-none d-md-inline-flex align-items-center"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
>
<i class="bi bi-globe2 me-1"></i>الموقع العام
<aside class="app-sidebar" id="appSidebar">
@php $isAdmin = auth()->user()?->role === 'admin'; @endphp
{{-- Navigation --}}
<nav class="sidebar-nav">
@if($isAdmin)
<div class="sidebar-section">الرئيسية</div>
<a href="{{ route('admin.dashboard') }}"
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-speedometer2 sidebar-icon"></i>
<span>لوحة التحكم</span>
</a>
{{-- Mobile: user avatar + logout --}}
<div class="d-flex d-md-none align-items-center gap-2">
<div style="width:30px;height:30px;background:rgba(99,102,241,.12);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#6366f1;font-weight:800;font-size:.82rem;flex-shrink:0;">
{{ mb_substr(auth()->user()?->name ?? 'م', 0, 1) }}
</div>
<form method="POST" action="{{ route('logout') }}" style="margin:0;">
@csrf
<button type="submit"
style="background:none;border:none;color:#94a3b8;padding:4px 6px;font-size:1.15rem;cursor:pointer;line-height:1;"
title="تسجيل الخروج">
<i class="bi bi-box-arrow-left"></i>
</button>
</form>
<div class="sidebar-section">المواقف والحجوزات</div>
<a href="{{ route('admin.parking-lots.index') }}"
class="sidebar-link {{ request()->routeIs('admin.parking-lots.*') ? 'active' : '' }}">
<i class="bi bi-buildings sidebar-icon"></i>
<span>المواقف</span>
</a>
<a href="{{ route('admin.bookings.active') }}"
class="sidebar-link {{ request()->routeIs('admin.bookings.*') ? 'active' : '' }}">
<i class="bi bi-calendar-check sidebar-icon"></i>
<span>الحجوزات النشطة</span>
</a>
<div class="sidebar-section">التشغيل</div>
<a href="{{ route('admin.operators.index') }}"
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
<i class="bi bi-people sidebar-icon"></i>
<span>المشغّلون</span>
</a>
@endif
<a href="{{ route('operator.stats') }}"
class="sidebar-link {{ request()->routeIs('operator.stats') ? 'active' : '' }}">
<i class="bi bi-graph-up sidebar-icon"></i>
<span>الإحصائيات</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="sidebar-link {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
<i class="bi bi-person-badge sidebar-icon"></i>
<span>{{ $isAdmin ? 'لوحة المشغّل' : 'الموقف' }}</span>
</a>
</nav>
</aside>
{{-- ════════════════════════════════════════
MAIN BODY
════════════════════════════════════════ --}}
<div class="app-body">
{{-- Flash Messages --}}
<div class="px-4 pt-3">
@if(session('success'))
<div class="alert alert-success d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
role="alert">
<i class="bi bi-check-circle-fill flex-shrink-0"></i>
<span>{{ session('success') }}</span>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0"></i>
<span>{{ session('error') }}</span>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div>
@endif
</div>
</header>
{{-- Flash Messages --}}
<div class="px-4 pt-3">
@if(session('success'))
<div class="alert alert-success d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
role="alert">
<i class="bi bi-check-circle-fill flex-shrink-0"></i>
<span>{{ session('success') }}</span>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0"></i>
<span>{{ session('error') }}</span>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div>
@endif
</div>
{{-- Page Content --}}
<main class="app-content">
@yield('content')
</main>
{{-- Page Content --}}
<main class="app-content">
@yield('content')
</main>
</div>{{-- /app-body --}}
</div>{{-- /app-body --}}
</div>{{-- /app-inner --}}
</div>{{-- /app-layout --}}
@@ -171,6 +122,7 @@
{{-- ══ MOBILE BOTTOM NAVIGATION ═══════════════════════════════════════════════ --}}
<nav class="mobile-bottom-nav" aria-label="التنقل">
@if($isAdmin)
<a href="{{ route('admin.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-speedometer2"></i>
@@ -192,10 +144,22 @@
<span>المشغّلون</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i>
<span>التشغيل</span>
</a>
@else
<a href="{{ route('operator.stats') }}"
class="mob-nav-item {{ request()->routeIs('operator.stats') ? 'active' : '' }}">
<i class="bi bi-graph-up"></i>
<span>الإحصائيات</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i>
<span>الموقف</span>
</a>
@endif
</nav>
<script>
+2 -22
View File
@@ -14,30 +14,10 @@
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
<header class="public-header">
<div class="container">
<div class="d-flex align-items-center justify-content-between">
{{-- Logo --}}
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
<div style="width:40px;height:40px;background:#6366f1;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-p-square-fill text-white" style="font-size:1.25rem;"></i>
</div>
<div>
<div class="fw-800" style="color:#f8fafc;font-size:1.05rem;line-height:1.2;">دمشق باركينغ</div>
<div style="color:#94a3b8;font-size:.72rem;">مواقف السيارات في دمشق</div>
</div>
</a>
{{-- User Dropdown --}}
@include('partials.user-dropdown')
</div>
</div>
</header>
@include('partials.topbar')
{{-- ══ CONTENT ═════════════════════════════════════════════════════════════ --}}
<main class="container py-4" style="max-width:820px;">
<main style="padding:1.5rem;">
{{-- Flash messages --}}
@if(session('success'))
+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
+33
View File
@@ -0,0 +1,33 @@
<header class="app-topbar">
{{-- Left: logo + optional admin controls --}}
<div class="d-flex align-items-center gap-3">
{{-- Brand logo (always shown, links to public site) --}}
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
<div style="width:36px;height:36px;background:#6366f1;border-radius:.5rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-p-square-fill text-white" style="font-size:1.1rem;"></i>
</div>
<div class="d-none d-sm-block">
<div class="fw-800" style="color:#f8fafc;font-size:1rem;line-height:1.2;">دمشق باركينغ</div>
<div style="color:#94a3b8;font-size:.68rem;">مواقف السيارات في دمشق</div>
</div>
</a>
@if($isAdminLayout ?? false)
{{-- Sidebar toggle (admin/operator pages only) --}}
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
<i class="bi bi-list"></i>
</button>
{{-- Page title --}}
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
@endif
</div>
{{-- Right: user dropdown --}}
<div class="d-flex align-items-center gap-2">
@include('partials.user-dropdown')
</div>
</header>
@@ -6,8 +6,7 @@
data-bs-toggle="dropdown"
aria-expanded="false"
style="background:rgba(255,255,255,.1);color:#f8fafc;border:1px solid rgba(255,255,255,.18);border-radius:.625rem;padding:.35rem .75rem;font-family:'Cairo',sans-serif;">
{{-- Avatar circle --}}
<div style="width:32px;height:32px;background:#6366f1;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;flex-shrink:0;color:#fff;border:2px solid rgba(255,255,255,.25);">
<div style="width:32px;height:32px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;flex-shrink:0;color:#fff;border:2px solid rgba(255,255,255,.25);">
{{ mb_substr(auth()->user()->name, 0, 1) }}
</div>
<span class="d-none d-sm-inline fw-600" style="font-size:.875rem;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
@@ -32,7 +31,6 @@
<div style="color:#64748b;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ auth()->user()->email }}
</div>
{{-- Role badge --}}
@php
$role = auth()->user()->role;
$roleLabel = match($role) {
@@ -53,6 +51,16 @@
<li><hr class="dropdown-divider my-1" style="border-color:#f1f5f9;"></li>
{{-- Public site --}}
<li>
<a href="{{ route('parking.index') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
style="color:#374151;font-size:.875rem;padding:.5rem .75rem;">
<i class="bi bi-globe2" style="color:#6366f1;font-size:1rem;width:18px;text-align:center;"></i>
الموقع العام
</a>
</li>
{{-- My profile --}}
<li>
<a href="{{ route('profile.show') }}"
@@ -63,7 +71,8 @@
</a>
</li>
{{-- My reservations --}}
{{-- My reservations (regular users) --}}
@if($role === 'user')
<li>
<a href="{{ route('user.dashboard') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
@@ -72,9 +81,10 @@
حجوزاتي
</a>
</li>
@endif
{{-- Operator panel (operators & admins) --}}
@if(in_array(auth()->user()->role, ['operator', 'admin']))
@if(in_array($role, ['operator', 'admin']))
<li>
<a href="{{ route('operator.dashboard') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
@@ -86,7 +96,7 @@
@endif
{{-- Admin panel (admins only) --}}
@if(auth()->user()->role === 'admin')
@if($role === 'admin')
<li>
<a href="{{ route('admin.dashboard') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
+17 -17
View File
@@ -5,15 +5,15 @@
{{-- Page header --}}
<div class="d-flex align-items-center gap-3 mb-4">
<a href="{{ route('parking.index') }}"
class="btn btn-sm"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>العودة
</a>
<div>
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">حجوزاتي</h1>
<p class="text-sm mb-0" style="color:#64748b;">سجل حجوزاتك في مواقف السيارات</p>
</div>
<a href="{{ route('parking.index') }}"
class="btn btn-sm ms-auto"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-left me-1"></i>العودة
</a>
</div>
{{-- ── Debt banner ─────────────────────────────────────────────────────────── --}}
@@ -27,12 +27,12 @@
<div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div>
<div class="text-sm" style="color:#b45309;">
لديك رسوم إلغاء متراكمة بقيمة
<strong>{{ number_format($pendingDebt) }} ل.س</strong>
<strong>{{ number_format($pendingDebt) }} ليرة سورية</strong>
— ستُضاف تلقائياً إلى حجزك القادم.
</div>
</div>
<div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;">
{{ number_format($pendingDebt) }} ل.س
{{ number_format($pendingDebt) }} ليرة سورية
</div>
</div>
@endif
@@ -130,13 +130,13 @@
{{-- Fee column --}}
<td>
@if($hasDebt)
<span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ل.س</span>
<span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
<div class="text-xs fw-600" style="color:#ef4444;">مستحق</div>
@elseif($booking->total_fee > 0 && !is_null($booking->paid_at))
<span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ل.س</span>
<span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
<div class="text-xs" style="color:#10b981;">مدفوع</div>
@elseif($booking->total_fee > 0)
<span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ل.س</span>
<span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
@elseif($isFree && $booking->status === 'cancelled')
<span class="text-xs" style="color:#94a3b8;">مجاني</span>
@else
@@ -304,16 +304,16 @@
{{-- After pay-now chosen: back + confirm --}}
<div id="cancelPayNowActions" style="display:none;" class="d-flex gap-2">
<button type="button" class="btn btn-light fw-600"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
onclick="cancelBackToActions()">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
<button type="button" id="cancelConfirmPayBtn"
class="btn btn-success fw-bold flex-grow-1"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء
</button>
<button type="button" class="btn btn-light fw-600"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
onclick="cancelBackToActions()">
<i class="bi bi-arrow-left me-1"></i>رجوع
</button>
</div>
</div>
@@ -382,12 +382,12 @@ async function openCancelModal(id, lotName, startIso) {
<div style="display:flex;justify-content:space-between;padding:.25rem 0;border-bottom:1px dashed #f1f5f9;">
<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('cancelFeeBreakdown').innerHTML =
rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>';
document.getElementById('cancelFeeTotal').textContent =
Number(d.fee).toLocaleString('ar-SA') + ' ل.س';
Number(d.fee).toLocaleString('ar-SA') + ' ليرة سورية';
// Reset UI
document.getElementById('cancelPayMethodWrap').style.display = 'none';
+5 -5
View File
@@ -5,15 +5,15 @@
{{-- Page header --}}
<div class="d-flex align-items-center gap-3 mb-4">
<a href="{{ route('parking.index') }}"
class="btn btn-sm"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>العودة
</a>
<div>
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">معلوماتي</h1>
<p class="text-sm mb-0" style="color:#64748b;">إدارة بيانات حسابك الشخصي</p>
</div>
<a href="{{ route('parking.index') }}"
class="btn btn-sm ms-auto"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-left me-1"></i>العودة
</a>
</div>
<div class="row g-4">