Spaces:
Sleeping
Sleeping
| {% extends "base.html" %} | |
| {% block title %}Admin Dashboard β Ain El Aql{% endblock %} | |
| {% block body %} | |
| <div class="app-layout"> | |
| {% with role='admin', active='dashboard' %} | |
| {% include "partials/sidebar.html" %} | |
| {% endwith %} | |
| <main class="main-content"> | |
| <div class="page-header fade-in flex-between"> | |
| <div> | |
| <h1 class="page-title">Admin Dashboard</h1> | |
| <p class="page-subtitle">Parking facility management & analytics</p> | |
| </div> | |
| <div class="pulse-badge"> | |
| <div class="pulse-dot"></div> | |
| System Live | |
| </div> | |
| </div> | |
| <div class="stats-grid fade-in" id="statsGrid"> | |
| <div class="stat-card blue"> | |
| <div class="stat-label">Total Capacity</div> | |
| <div class="stat-value blue" id="statCapacity">β</div> | |
| </div> | |
| <div class="stat-card green"> | |
| <div class="stat-label">Currently Inside</div> | |
| <div class="stat-value green" id="statInside">β</div> | |
| </div> | |
| <div class="stat-card purple"> | |
| <div class="stat-label">Available Spots</div> | |
| <div class="stat-value purple" id="statAvailable">β</div> | |
| </div> | |
| <div class="stat-card red"> | |
| <div class="stat-label">Occupancy Rate</div> | |
| <div class="stat-value red" id="statOccRate">β</div> | |
| </div> | |
| </div> | |
| <div class="content-grid" style="grid-template-columns: 1fr;"> | |
| <!-- Inside Cars --> | |
| <div class="card fade-in"> | |
| <div class="card-header"> | |
| <span class="card-title">π Vehicles Inside</span> | |
| <span class="card-badge badge-live">LIVE</span> | |
| </div> | |
| <div class="table-wrapper" style="max-height:500px;overflow-y:auto;"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Plate</th> | |
| <th>Registration</th> | |
| <th>Entry</th> | |
| <th>Duration</th> | |
| <th>Est. Fee</th> | |
| <th>Status</th> | |
| <th>Payment</th> | |
| </tr> | |
| </thead> | |
| <tbody id="insideBody"> | |
| <tr><td colspan="7" class="text-center" style="padding:2rem;color:var(--text-muted);">Loading live data...</td></tr></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quick Admin Actions --> | |
| <div class="card fade-in"> | |
| <div class="card-header"> | |
| <span class="card-title">β‘ Administration</span> | |
| </div> | |
| <div style="display:flex;gap:.75rem;flex-wrap:wrap;"> | |
| <a href="{{ url_for('admin.garage') }}" class="btn btn-outline" style="background:var(--accent-blue);color:white;border:none;"> | |
| <span class="icon">ποΈ</span> Garage Status | |
| </a> | |
| <a href="{{ url_for('admin.users') }}" class="btn btn-outline"> | |
| <span class="icon">π₯</span> Manage Users | |
| </a> | |
| <a href="{{ url_for('admin.vehicles') }}" class="btn btn-outline"> | |
| <span class="icon">π</span> Manage Vehicles | |
| </a> | |
| <a href="{{ url_for('admin.history') }}" class="btn btn-outline"> | |
| <span class="icon">π</span> Parking History | |
| </a> | |
| <button class="btn btn-danger" onclick="runMaintenance()"><span>π§Ή</span> Refresh Storage (Photos)</button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| function updateStats(occ) { | |
| if (!occ) return; | |
| const g = occ.garage || occ; | |
| const cap = g.total_capacity || g.capacity || 0; | |
| const inside = g.inside || g.occupied || g.current_count || 0; | |
| document.getElementById('statCapacity').textContent = cap || 'β'; | |
| document.getElementById('statInside').textContent = inside; | |
| document.getElementById('statAvailable').textContent = cap ? cap - inside : 'β'; | |
| document.getElementById('statOccRate').textContent = cap ? Math.round((inside/cap)*100) + '%' : 'β'; | |
| } | |
| 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 renderInside(data) { | |
| const body = document.getElementById('insideBody'); | |
| // Backend returns { items: [...] } where each item has { session, vehicle, pricing, payment_status } | |
| let items = Array.isArray(data) ? data : (data.items || data.data || data.cars || []); | |
| if (!items.length) { body.innerHTML = '<tr><td colspan="6" class="text-center" style="padding:2rem;color:var(--text-muted);">No vehicles inside</td></tr>'; return; } | |
| body.innerHTML = items.map(item => { | |
| const s = item.session || item; | |
| const v = item.vehicle || {}; | |
| const p = item.pricing || {}; | |
| const ps = item.payment_status || {}; | |
| const plate = s.plate_arabic || s.plate_english || s.plate_key || v.plate_letters_ar || 'β'; | |
| const entry = s.check_in_at ? new Date(s.check_in_at).toLocaleString() : (s.entry_time ? new Date(s.entry_time).toLocaleString() : 'β'); | |
| const duration = formatDuration(p.duration_minutes); | |
| const fee = p.total_fee_egp != null ? p.total_fee_egp + ' EGP' : 'β'; | |
| const status = s.status || 'entered'; | |
| const statusClass = status === 'paid' ? 'status-registered' : 'status-active'; | |
| const statusLabel = status === 'paid' ? 'β Paid' : status === 'entered' ? 'π΅ Inside' : status; | |
| // Payment method badge | |
| const notes = (s.notes || '').toLowerCase(); | |
| const hasCashIntent = notes.includes('intent_cash') || notes.includes('cash'); | |
| const isGuest = !v.id && !v.plate_letters_ar; | |
| let registration = isGuest | |
| ? '<span style="color:var(--accent-red);font-size:0.8rem;font-weight:bold;">Unregistered</span>' | |
| : '<span style="color:var(--accent-green);font-size:0.8rem;font-weight:bold;">Registered</span>'; | |
| 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>'; | |
| return `<tr> | |
| <td class="mono">${plate}</td> | |
| <td>${registration}</td> | |
| <td>${entry}</td> | |
| <td>${duration}</td><td>${fee}</td> | |
| <td><span class="status ${statusClass}">${statusLabel}</span></td> | |
| <td>${payBadge}</td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| async function refreshData() { | |
| try { | |
| const [oR, iR] = await Promise.all([ | |
| fetch('/admin/api/occupancy'), fetch('/admin/api/inside-cars') | |
| ]); | |
| updateStats(await oR.json()); renderInside(await iR.json()); | |
| } catch(e) { console.error(e); } | |
| } | |
| async function runMaintenance() { | |
| if (!confirm('Clear database photo storage? (Profiles and tables will not be affected)')) return; | |
| try { | |
| const r = await fetch('/admin/api/maintenance', {method:'POST'}); | |
| const d = await r.json(); | |
| alert(d.message || d.error || 'Done'); | |
| } catch(e) { alert('Error: ' + e); } | |
| } | |
| refreshData(); | |
| setInterval(refreshData, 2000); // 2 seconds for high-tier real-time feel | |
| </script> | |
| {% endblock %} | |