EALPR_FLASK / templates /admin /dashboard.html
Pant0x's picture
Final synced deploy v2 - Fix session and timeouts
cd01269
Raw
History Blame Contribute Delete
7.04 kB
{% 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 %}