diff --git a/resources/views/operator/dashboard.blade.php b/resources/views/operator/dashboard.blade.php index e89e2cb..ec65aa4 100644 --- a/resources/views/operator/dashboard.blade.php +++ b/resources/views/operator/dashboard.blade.php @@ -29,11 +29,7 @@ transition:opacity .18s; border-radius:2px 0 0 2px; } - .lot-picker-card:hover { - border-color:#a5b4fc; - box-shadow:0 4px 20px rgba(99,102,241,.12); - transform:translateY(-2px); - } + .lot-picker-card:hover { border-color:#a5b4fc; box-shadow:0 4px 20px rgba(99,102,241,.12); transform:translateY(-2px); } .lot-picker-card:hover::before { opacity:1; } .lot-picker-card.highlighted { border-color:#6366f1; box-shadow:0 0 0 3px rgba(99,102,241,.15); } .lot-picker-card.highlighted::before { opacity:1; } @@ -70,28 +66,71 @@ margin-bottom:1.5rem; } - /* ── Tabs ─────────────────────────────────────────────────────────── */ - .op-tabs { display:flex; gap:.375rem; margin-bottom:1.25rem; border-bottom:2px solid #e2e8f0; padding-bottom:0; } - .op-tab { - padding:.55rem 1.125rem; + /* ── Booking cards ───────────────────────────────────────────────── */ + .booking-card { + background:#fff; border:none; - border-bottom:3px solid transparent; - margin-bottom:-2px; - background:none; - font-family:'Cairo',sans-serif; - font-weight:600; - font-size:.875rem; + border-radius:1rem; + box-shadow:0 4px 16px rgba(0,0,0,.06); + overflow:hidden; + transition:transform .2s, box-shadow .2s; + height:100%; + } + .booking-card:hover { + transform:translateY(-5px); + box-shadow:0 10px 28px rgba(0,0,0,.1); + } + .booking-card-header { + padding:.75rem 1rem; + font-weight:700; + font-size:.8rem; + color:#fff; + text-align:center; + letter-spacing:.02em; + } + .booking-card-header.reservation { background:linear-gradient(90deg,#1e1b4b,#4338ca); } + .booking-card-header.walkin { background:linear-gradient(90deg,#064e3b,#059669); } + + .plate-display { + font-size:1.6rem; + font-weight:900; + letter-spacing:.12em; + color:#0f172a; + background:#f8fafc; + border:2px solid #e2e8f0; + border-radius:.625rem; + padding:.6rem 1rem; + text-align:center; + margin:.875rem; + font-family:monospace; + direction:ltr; + } + + .booking-card-body { + padding:0 .875rem .875rem; + } + + .booking-card-body .customer-name { + text-align:center; color:#64748b; - cursor:pointer; - border-radius:.5rem .5rem 0 0; - transition:color .15s, border-color .15s; + font-size:.82rem; + margin-bottom:.5rem; + } + + .time-badge { display:flex; align-items:center; - gap:.4rem; + justify-content:center; + gap:.375rem; + background:#f1f5f9; + border-radius:.5rem; + padding:.35rem .625rem; + font-size:.78rem; + color:#475569; + margin-bottom:.75rem; } - .op-tab:hover { color:#0f172a; } - .op-tab.active { color:#6366f1; border-bottom-color:#6366f1; } - .op-tab .badge { font-size:.68rem; padding:.2em .5em; } + + .booking-card-divider { border:none; border-top:1px dashed #e2e8f0; margin:.625rem 0; } /* ── Receipt modal ────────────────────────────────────────────────── */ .fee-row { display:flex; justify-content:space-between; padding:.35rem 0; border-bottom:1px dashed #f1f5f9; font-size:.875rem; } @@ -101,6 +140,10 @@ .pay-method:hover { border-color:#a5b4fc; } .pay-method.selected { border-color:#6366f1; background:#f0f4ff; } .pay-method i { font-size:1.5rem; display:block; margin-bottom:.25rem; } + + /* Empty state */ + .empty-state { text-align:center; padding:3rem 1rem; color:#94a3b8; } + .empty-state i { font-size:3rem; display:block; margin-bottom:.75rem; opacity:.3; } @endpush @@ -154,9 +197,7 @@
-
- {{ $lot['name'] }} -
+
{{ $lot['name'] }}
{{ $lot['address'] }}
@@ -228,6 +269,7 @@ @php $pct = $selectedLot->usage_percentage; $barColor = $pct >= 90 ? '#ef4444' : ($pct >= 60 ? '#f59e0b' : '#10b981'); + $totalCards = $activeCars->count() + $reservations->count(); @endphp {{-- ── Selected lot header ─────────────────────────────────────────── --}} @@ -243,7 +285,6 @@
- {{-- Live stats --}}
{{ $selectedLot->available_spaces }}
@@ -261,7 +302,6 @@
- {{-- Capacity bar (inline) --}}
الإشغال{{ round($pct) }}% @@ -271,7 +311,6 @@
- {{-- Change lot --}} @@ -280,241 +319,249 @@
+{{-- ── Toolbar: search + new entry ─────────────────────────────────── --}} +
+
+ + + + + +
+ +
+ + {{ $reservations->count() }} حجز مسبق + + + {{ $activeCars->count() }} داخل الآن + +
+ + +
+ +{{-- ── Booking cards grid ───────────────────────────────────────────── --}} +
+ + {{-- Reservation cards (not yet arrived) --}} + @foreach($reservations as $res) +
+
+
+ حجز مسبق #{{ $res->id }} +
+ +
{{ $res->vehicle_plate ?? '—' }}
+ +
+
+ {{ $res->customer_name ?? 'غير محدد' }} + @if($res->phone) +
{{ $res->phone }}
+ @endif +
+ +
+ + {{ $res->start_time->format('H:i') }} — {{ $res->end_time->format('H:i') }} + {{ $res->start_time->format('d/m') }} +
+ +
+ + + +
+
+
+ @endforeach + + {{-- Walk-in cards (already inside) --}} + @foreach($activeCars as $car) + @php + $elapsedMin = $car->start_time->diffInMinutes(now()); + $remainMins = now()->diffInMinutes($car->end_time, false); + $isOverdue = $remainMins < 0; + @endphp +
+
+
+ داخل الموقف +
+ +
{{ $car->vehicle_plate ?? '—' }}
+ +
+
+ {{ $car->customer_name ?? 'غير محدد' }} + @if($car->phone) +
{{ $car->phone }}
+ @endif +
+ +
+ + دخل {{ $car->start_time->format('H:i') }} +  ·  + {{ floor($elapsedMin/60) }}س {{ $elapsedMin%60 }}د +
+ + @if($isOverdue) +
+ + + تجاوز {{ floor(abs($remainMins)/60) }}س {{ abs($remainMins)%60 }}د + +
+ @else +
+ + متبقي {{ floor($remainMins/60) }}س {{ $remainMins%60 }}د + +
+ @endif + +
+ + +
+
+
+ @endforeach + + {{-- Empty state --}} + @if($totalCards === 0) +
+
+ +

لا توجد سيارات أو حجوزات نشطة

+

سجّل دخول سيارة جديدة للبدء

+ +
+
+ @endif + + {{-- No search results --}} + +
+ {{-- Auto-refresh countdown --}} -
+
-{{-- ── Tabs ───────────────────────────────────────────────────────────── --}} -
- - - -
- -{{-- ───────────────────────────────────────────────────────────────────── - TAB 1: Active walk-in cars -──────────────────────────────────────────────────────────────────────── --}} -
-
-
- - السيارات داخل الموقف - -
- - @if($activeCars->isEmpty()) -
- -

لا توجد سيارات داخل الموقف

- -
- @else -
- - - - - - - - - - - - - @foreach($activeCars as $car) - @php - $elapsedMin = $car->start_time->diffInMinutes(now()); - $remainMins = now()->diffInMinutes($car->end_time, false); - $isOverdue = $remainMins < 0; - @endphp - - - - - - - - - @endforeach - -
رقم اللوحةالسائقوقت الدخولالمدةالمتبقيإجراء
- - {{ $car->vehicle_plate ?? '—' }} - - - {{ $car->customer_name ?? 'غير محدد' }} - @if($car->phone) -
{{ $car->phone }}
- @endif -
- {{ $car->start_time->format('H:i') }} -
{{ $car->start_time->format('Y/m/d') }}
-
- - {{ floor($elapsedMin/60) }}س {{ $elapsedMin%60 }}د - - - @if($isOverdue) - - تجاوز {{ floor(abs($remainMins)/60) }}س {{ abs($remainMins)%60 }}د - - @else - - {{ floor($remainMins/60) }}س {{ $remainMins%60 }}د - - @endif - - -
-
- @endif -
-
- -{{-- ───────────────────────────────────────────────────────────────────── - TAB 2: Manual check-in form -──────────────────────────────────────────────────────────────────────── --}} - - -{{-- ───────────────────────────────────────────────────────────────────── - TAB 3: Pre-reservations -──────────────────────────────────────────────────────────────────────── --}} - - @endif {{-- end selectedLot --}} +{{-- ══════════════════════════════════════════════════════════════════════ + DIRECT ENTRY MODAL +══════════════════════════════════════════════════════════════════════ --}} + + + {{-- ══════════════════════════════════════════════════════════════════════ RECEIPT & PAYMENT MODAL ══════════════════════════════════════════════════════════════════════ --}} @@ -540,7 +587,7 @@
-
+
@@ -599,7 +646,6 @@
- {{-- File upload (hidden until upload selected) --}} `); - m.on('click', () => highlightCard(l.id)); }); @@ -679,21 +722,16 @@ if (lots.length) { map.fitBounds(g.getBounds().pad(.15)); } -// ── Card highlight ─────────────────────────────────────────────────────────── let hlId = null; function highlightCard(id) { if (hlId) document.getElementById('lcard-' + hlId)?.classList.remove('highlighted'); hlId = id; const card = document.getElementById('lcard-' + id); - if (card) { - card.classList.add('highlighted'); - card.scrollIntoView({ behavior:'smooth', block:'nearest' }); - } + if (card) { card.classList.add('highlighted'); card.scrollIntoView({ behavior:'smooth', block:'nearest' }); } const l = lots.find(x => x.id === id); if (l) map.setView([l.lat, l.lng], 15, { animate:true }); } -// ── Search ─────────────────────────────────────────────────────────────────── const searchEl = document.getElementById('lotSearch'); const clearBtn = document.getElementById('clearSearch'); const noResults = document.getElementById('noResults'); @@ -716,7 +754,6 @@ clearBtn.addEventListener('click', () => { searchEl.value = ''; searchEl.dispatchEvent(new Event('input')); searchEl.focus(); }); -// ── Mobile tab ─────────────────────────────────────────────────────────────── function mobileTab(tab) { const cc = document.getElementById('cardsCol'); const mc = document.getElementById('mapCol'); @@ -737,56 +774,81 @@ if (window.innerWidth < 992) { document.getElementById('mapCol').style.display = 'none'; } -// Highlight duration radio default -document.querySelectorAll('input[name="duration_hours"]').forEach(r => { - if (r.checked) r.closest('label').style.cssText='border:2px solid #6366f1;cursor:pointer;font-size:.82rem;font-weight:700;color:#4338ca;background:#f0f4ff;transition:all .15s;'; -}); - @else // ════════════════════════════════════════════════════════════════════════════ // OPERATOR PANEL SCRIPTS // ════════════════════════════════════════════════════════════════════════════ -// ── Tabs ───────────────────────────────────────────────────────────────────── -function switchTab(name) { - ['active','checkin','reservations'].forEach(t => { - document.getElementById('panel-' + t).style.display = t === name ? '' : 'none'; - document.getElementById('tab-' + t).classList.toggle('active', t === name); - }); -} +// ── Card search ─────────────────────────────────────────────────────────────── +const cardSearch = document.getElementById('cardSearch'); +const clearCardSearch = document.getElementById('clearCardSearch'); -// ── Check-in form ───────────────────────────────────────────────────────────── -// Default duration highlight -document.querySelectorAll('input[name="duration_hours"]').forEach(r => { - if (r.checked) r.closest('label').style.cssText='border:2px solid #6366f1;cursor:pointer;font-size:.82rem;font-weight:700;color:#4338ca;background:#f0f4ff;transition:all .15s;'; - r.addEventListener('change', () => { - document.querySelectorAll('input[name="duration_hours"]').forEach(x => - x.closest('label').style.cssText='border:2px solid #e2e8f0;cursor:pointer;font-size:.82rem;font-weight:600;color:#475569;transition:all .15s;' - ); - r.closest('label').style.cssText='border:2px solid #6366f1;cursor:pointer;font-size:.82rem;font-weight:700;color:#4338ca;background:#f0f4ff;transition:all .15s;'; +cardSearch.addEventListener('input', () => { + const q = cardSearch.value.trim().toLowerCase(); + clearCardSearch.style.display = q ? 'block' : 'none'; + let vis = 0; + document.querySelectorAll('.booking-card-wrap').forEach(w => { + const match = !q || w.dataset.plate.includes(q) || w.dataset.name.includes(q); + w.style.display = match ? '' : 'none'; + if (match) vis++; }); + document.getElementById('noCardResults').style.display = + (vis === 0 && q) ? 'block' : 'none'; +}); +clearCardSearch.addEventListener('click', () => { + cardSearch.value = ''; cardSearch.dispatchEvent(new Event('input')); cardSearch.focus(); }); -document.getElementById('checkInForm')?.addEventListener('submit', async e => { - e.preventDefault(); - const btn = document.getElementById('checkInBtn'); +// ── Duration tile highlight ─────────────────────────────────────────────────── +function initDurationTiles() { + document.querySelectorAll('.duration-tile').forEach(label => { + const radio = label.querySelector('input[type="radio"]'); + if (radio.checked) label.style.cssText = 'border:2px solid #10b981;cursor:pointer;font-size:.82rem;font-weight:700;color:#059669;background:#f0fdf4;transition:all .15s;border-radius:.75rem;'; + radio.addEventListener('change', () => { + document.querySelectorAll('.duration-tile').forEach(l => + l.style.cssText = 'border:2px solid #e2e8f0;cursor:pointer;font-size:.82rem;font-weight:600;color:#475569;transition:all .15s;border-radius:.75rem;' + ); + label.style.cssText = 'border:2px solid #10b981;cursor:pointer;font-size:.82rem;font-weight:700;color:#059669;background:#f0fdf4;transition:all .15s;border-radius:.75rem;'; + }); + }); +} +initDurationTiles(); + +// ── Check-in form submit ────────────────────────────────────────────────────── +async function submitCheckIn() { + const form = document.getElementById('checkInForm'); + const btn = document.getElementById('checkInBtn'); + if (!form.vehicle_plate.value.trim()) { + form.vehicle_plate.focus(); return; + } btn.disabled = true; btn.innerHTML = 'جاري التسجيل...'; try { - const res = await fetch('/operator/check-in', { method:'POST', body: new FormData(e.target) }); + const res = await fetch('/operator/check-in', { method:'POST', body: new FormData(form) }); const data = await res.json(); if (data.success) { btn.style.background = '#059669'; - btn.innerHTML = 'تم التسجيل — يتم التحديث...'; - setTimeout(() => location.reload(), 900); + btn.innerHTML = 'تم التسجيل!'; + setTimeout(() => location.reload(), 700); } else { - alert(data.message || 'حدث خطأ'); resetCheckInBtn(); + alert(data.message || 'حدث خطأ'); + btn.disabled = false; + btn.innerHTML = 'تأكيد الدخول'; } - } catch { alert('خطأ في الاتصال'); resetCheckInBtn(); } - function resetCheckInBtn() { + } catch { + alert('خطأ في الاتصال'); btn.disabled = false; - btn.innerHTML = 'تسجيل الدخول'; + btn.innerHTML = 'تأكيد الدخول'; } +} + +// Reset modal form when closed +document.getElementById('newEntryModal').addEventListener('hidden.bs.modal', () => { + document.getElementById('checkInForm').reset(); + document.getElementById('checkInBtn').disabled = false; + document.getElementById('checkInBtn').innerHTML = 'تأكيد الدخول'; + document.getElementById('checkInBtn').style.background = '#10b981'; + initDurationTiles(); }); // ── Activate reservation ────────────────────────────────────────────────────── @@ -801,15 +863,30 @@ async function activateRes(id, btn) { }); const data = await res.json(); if (data.success) { - document.getElementById('res-' + id)?.remove(); - alert(data.message); - location.reload(); - } else { alert(data.message || 'حدث خطأ'); btn.innerHTML=orig; btn.disabled=false; } - } catch { alert('خطأ في الاتصال'); btn.innerHTML=orig; btn.disabled=false; } + // swap check-in button to success state, enable checkout + btn.style.background = '#059669'; + btn.innerHTML = 'تم الدخول'; + const checkoutBtn = document.getElementById('checkout-res-' + id); + if (checkoutBtn) { + checkoutBtn.disabled = false; + checkoutBtn.style.opacity = '1'; + checkoutBtn.style.cursor = 'pointer'; + checkoutBtn.onclick = () => openReceipt(id); + } + } else { + alert(data.message || 'حدث خطأ'); + btn.innerHTML = orig; + btn.disabled = false; + } + } catch { + alert('خطأ في الاتصال'); + btn.innerHTML = orig; + btn.disabled = false; + } } // ── Receipt modal ───────────────────────────────────────────────────────────── -let receiptModal = null; +let receiptModal = null; let currentBookingId = null; let selectedPayment = 'cash'; @@ -823,15 +900,12 @@ async function openReceipt(id) { selectedPayment = 'cash'; selectPayment('cash'); - // Reset breakdown document.getElementById('rcpt-breakdown').innerHTML = '
'; - document.getElementById('rcpt-plate').textContent = '—'; - document.getElementById('rcpt-name').textContent = '—'; - document.getElementById('rcpt-duration').textContent = '—'; - document.getElementById('rcpt-entry').textContent = '—'; - document.getElementById('rcpt-exit').textContent = '—'; - document.getElementById('rcpt-total').textContent = '—'; + ['rcpt-plate','rcpt-name','rcpt-duration','rcpt-entry','rcpt-exit','rcpt-total'].forEach(i => + document.getElementById(i).textContent = '—' + ); + document.getElementById('rcpt-lot').textContent = ''; getModal().show(); @@ -842,33 +916,31 @@ async function openReceipt(id) { const d = data.data; document.getElementById('rcpt-lot').textContent = d.lot_name; - document.getElementById('rcpt-plate').textContent = d.plate || '—'; + document.getElementById('rcpt-plate').textContent = d.plate || '—'; document.getElementById('rcpt-name').textContent = d.customer_name || 'غير محدد'; document.getElementById('rcpt-duration').textContent = d.duration_label; 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') + ' ل.س'; - // Breakdown table const rows = d.fee_details.map(r => `
${r.day} ${r.date} ${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')} ${Number(r.subtotal).toLocaleString('ar-SA')} ل.س
`).join(''); - document.getElementById('rcpt-breakdown').innerHTML = rows || '

لا تفاصيل

'; + document.getElementById('rcpt-breakdown').innerHTML = + rows || '

لا تفاصيل

'; } catch { alert('تعذّر تحميل بيانات الفاتورة'); } } -// Payment method toggle function selectPayment(method) { selectedPayment = method; - document.getElementById('pm-cash').classList.toggle('selected', method === 'cash'); + document.getElementById('pm-cash').classList.toggle('selected', method === 'cash'); document.getElementById('pm-upload').classList.toggle('selected', method === 'upload'); document.getElementById('uploadArea').style.display = method === 'upload' ? 'block' : 'none'; } -// Confirm payment document.getElementById('confirmPayBtn').addEventListener('click', async () => { if (!currentBookingId) return; if (selectedPayment === 'upload' && !document.getElementById('paymentProofFile').files.length) { @@ -879,7 +951,6 @@ document.getElementById('confirmPayBtn').addEventListener('click', async () => { btn.innerHTML = 'جاري المعالجة...'; const fd = new FormData(); - fd.append('_method', 'POST'); fd.append('payment_method', selectedPayment); if (selectedPayment === 'upload') { fd.append('payment_proof', document.getElementById('paymentProofFile').files[0]); @@ -887,29 +958,33 @@ document.getElementById('confirmPayBtn').addEventListener('click', async () => { try { const res = await fetch(`/operator/${currentBookingId}/payment`, { - method:'POST', - headers:{'X-CSRF-TOKEN':csrf}, - body: fd + method:'POST', headers:{'X-CSRF-TOKEN':csrf}, body: fd }); const data = await res.json(); if (data.success) { getModal().hide(); - document.getElementById('row-' + currentBookingId)?.remove(); - const row = document.getElementById('row-' + currentBookingId); - if (row) { row.style.transition='opacity .4s'; row.style.opacity='0'; setTimeout(()=>row.remove(),400); } + const card = document.getElementById('card-' + currentBookingId); + if (card) { card.style.transition='opacity .4s'; card.style.opacity='0'; setTimeout(()=>card.remove(),400); } setTimeout(() => location.reload(), 600); } else { alert(data.message || 'حدث خطأ'); - btn.disabled = false; + btn.disabled = false; btn.innerHTML = 'تأكيد الدفع وإغلاق البوابة'; } } catch { alert('خطأ في الاتصال'); - btn.disabled = false; + btn.disabled = false; btn.innerHTML = 'تأكيد الدفع وإغلاق البوابة'; } }); +// Reset receipt modal on close +document.getElementById('receiptModal').addEventListener('hidden.bs.modal', () => { + document.getElementById('confirmPayBtn').disabled = false; + document.getElementById('confirmPayBtn').innerHTML = 'تأكيد الدفع وإغلاق البوابة'; + document.getElementById('paymentProofFile').value = ''; +}); + // ── Auto-refresh countdown ──────────────────────────────────────────────────── let t = 30; const badge = document.getElementById('refresh-badge');