Spaces:
Sleeping
Sleeping
| {% extends "base.html" %} | |
| {% block title %}Vehicles β Ain El Aql{% endblock %} | |
| {% block body %} | |
| <div class="app-layout"> | |
| {% with role='admin', active='vehicles' %} | |
| {% include "partials/sidebar.html" %} | |
| {% endwith %} | |
| <main class="main-content"> | |
| <div class="page-header fade-in flex-between"> | |
| <div> | |
| <h1 class="page-title">Vehicle Management</h1> | |
| <p class="page-subtitle">All vehicles currently in the system</p> | |
| </div> | |
| <button class="btn btn-outline" onclick="loadVehicles()"><span>π</span> Refresh</button> | |
| </div> | |
| <div class="stats-grid fade-in"> | |
| <div class="stat-card blue"> | |
| <div class="stat-label">Total Vehicles</div> | |
| <div class="stat-value blue" id="statTotal">β</div> | |
| </div> | |
| <div class="stat-card green"> | |
| <div class="stat-label">Currently Inside</div> | |
| <div class="stat-value green" id="statActive">β</div> | |
| </div> | |
| <div class="stat-card purple"> | |
| <div class="stat-label">Registered</div> | |
| <div class="stat-value purple" id="statRegistered">β</div> | |
| </div> | |
| <div class="stat-card red"> | |
| <div class="stat-label">Unregistered</div> | |
| <div class="stat-value red" id="statUnregistered">β</div> | |
| </div> | |
| </div> | |
| <div class="card fade-in"> | |
| <div class="table-wrapper" style="max-height:500px;overflow-y:auto;"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Plate (AR)</th> | |
| <th>Owner / User</th> | |
| <th>Status</th> | |
| <th>Duration</th> | |
| <th>Fee</th> | |
| <th>Payment</th> | |
| </tr> | |
| </thead> | |
| <tbody id="vehBody"> | |
| <tr><td colspan="6" class="text-center" style="padding:2rem;color:var(--text-muted);">Loading...</td></tr></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| async function loadVehicles() { | |
| try { | |
| // Use inside-cars feed + history to show all known vehicles | |
| const [insideR, histR] = await Promise.all([ | |
| fetch('/admin/api/inside-cars'), | |
| fetch('/admin/api/history') | |
| ]); | |
| const insideData = await insideR.json(); | |
| const histData = await histR.json(); | |
| const insideItems = insideData.items || insideData.data || []; | |
| const histItems = histData.history || histData.items || histData.data || []; | |
| // Deduplicate by session id | |
| const seen = new Set(); | |
| const allItems = []; | |
| // Inside cars first (active) | |
| for (const item of insideItems) { | |
| const s = item.session || item; | |
| const sid = s.id || ''; | |
| if (sid && !seen.has(sid)) { seen.add(sid); allItems.push({...item, _isInside: true}); } | |
| } | |
| // Then history (completed) | |
| for (const item of histItems) { | |
| const s = item.session || item; | |
| const sid = s.id || ''; | |
| if (sid && !seen.has(sid)) { seen.add(sid); allItems.push({...item, _isInside: false}); } | |
| } | |
| // Update stats | |
| // Update stats | |
| const unregistered = allItems.filter(i => { | |
| const v = i.vehicle || {}; | |
| return !v.id && !v.plate_letters_ar; | |
| }).length; | |
| const registered = allItems.length - unregistered; | |
| const insideCount = allItems.filter(i => i._isInside).length; | |
| document.getElementById('statTotal').textContent = allItems.length; | |
| document.getElementById('statActive').textContent = insideCount; | |
| document.getElementById('statRegistered').textContent = registered; | |
| document.getElementById('statUnregistered').textContent = unregistered; | |
| renderVehicles(allItems); | |
| } catch(e) { console.error(e); } | |
| } | |
| function formatDuration(mins) { | |
| if (mins == null) return 'β'; | |
| const h = Math.floor(mins / 60); | |
| const m = Math.floor(mins % 60); | |
| return h > 0 ? `${h}h ${m}m` : `${m}m`; | |
| } | |
| function renderVehicles(items) { | |
| const body = document.getElementById('vehBody'); | |
| if (!items || !items.length) { | |
| body.innerHTML = '<tr><td colspan="6" class="text-center" style="padding:2rem;color:var(--text-muted);">No vehicles found</td></tr>'; | |
| return; | |
| } | |
| body.innerHTML = items.map(item => { | |
| const s = item.session || item; | |
| const v = item.vehicle || {}; | |
| const p = item.pricing || {}; | |
| const plateAr = s.plate_arabic || ((v.plate_letters_ar||'') + ' ' + (v.plate_numbers_ar||'')).trim() || 'β'; | |
| // Attempt to extract username from backend's profile if available (depends on API return shape) | |
| // Sometimes backend attaches `owner` to the vehicle object | |
| let ownerName = 'β'; | |
| if (v.owner) { | |
| ownerName = v.owner.username || v.owner.full_name || v.owner.first_name || 'β'; | |
| } else if (v.owner_id) { | |
| ownerName = 'Registered'; | |
| } | |
| const isGuest = !v.id && !v.plate_letters_ar; | |
| if (isGuest) { | |
| ownerName = '<span class="status status-unregistered" style="font-size:0.7rem;padding:2px 6px;">Unregistered</span>'; | |
| } | |
| const isInside = item._isInside; | |
| const status = isInside ? 'Inside' : 'Left'; | |
| const stClass = isInside ? 'status-active' : 'status-unregistered'; | |
| const duration = formatDuration(p.duration_minutes); | |
| const fee = p.total_fee_egp != null ? p.total_fee_egp + ' EGP' : 'β'; | |
| // Payment badge | |
| const notes = (s.notes || '').toLowerCase(); | |
| const hasCashIntent = notes.includes('intent_cash') || notes.includes('cash'); | |
| let isCash = isGuest || hasCashIntent; | |
| let isPaidVisa = s.payment_confirmed_at != null && !isCash; | |
| let payBadge = 'β'; | |
| if (isPaidVisa) payBadge = '<span style="color:var(--accent-blue);">π³ Visa</span>'; | |
| else if (isCash) payBadge = '<span style="color:var(--accent-green);">π΅ Cash</span>'; | |
| else if (isInside) payBadge = 'β³ Pending'; | |
| return `<tr> | |
| <td class="mono">${plateAr}</td> | |
| <td>${ownerName}</td> | |
| <td><span class="status ${stClass}">${status}</span></td> | |
| <td>${duration}</td><td>${fee}</td> | |
| <td>${payBadge}</td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| loadVehicles(); | |
| </script> | |
| {% endblock %} | |