feat(ui): add Logistics/Tickets tabbed navigation
- Wrap the existing analytics dashboard as the Logistics tab - Add a scaffolded Tickets tab (per-tab KPIs, recent-tickets + by-status cards, informative empty state) - Shared header KPI strip swaps per tab; tickets load lazily on first open - Ticket data source left as a dashboard_api integration point — no S3 credentials embedded in the static SPA Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
21bca24cee
commit
2611212fcd
1 changed files with 94 additions and 1 deletions
|
|
@ -46,7 +46,7 @@
|
||||||
}
|
}
|
||||||
.app {
|
.app {
|
||||||
display: grid; min-height: 100vh;
|
display: grid; min-height: 100vh;
|
||||||
grid-template-rows: auto auto 1fr; /* header · filter bar · content */
|
grid-template-rows: auto 1fr; /* header · content (tabs/filters live inside each view) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Top bar (mirrors FleetNow) ──────────────────────────────────────── */
|
/* ── Top bar (mirrors FleetNow) ──────────────────────────────────────── */
|
||||||
|
|
@ -83,6 +83,23 @@
|
||||||
.clock .label { font-size: 9.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
|
.clock .label { font-size: 9.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
|
||||||
.clock b { font-weight: 600; }
|
.clock b { font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Tab nav (segmented control) ─────────────────────────────────────── */
|
||||||
|
.tabs {
|
||||||
|
display: flex; gap: 4px; background: var(--bg);
|
||||||
|
border: 1px solid var(--border); border-radius: 8px; padding: 3px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
background: transparent; color: var(--muted); border: 0; border-radius: 6px;
|
||||||
|
padding: 6px 14px; font-size: 12.5px; font-weight: 700; letter-spacing: .3px;
|
||||||
|
cursor: pointer; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tab:hover { color: var(--text); }
|
||||||
|
.tab.active { background: var(--accent); color: #1a1009; }
|
||||||
|
|
||||||
|
/* ── Tabbed views ────────────────────────────────────────────────────── */
|
||||||
|
.view { display: none; }
|
||||||
|
.view.active { display: block; }
|
||||||
|
|
||||||
/* ── Filter bar ──────────────────────────────────────────────────────── */
|
/* ── Filter bar ──────────────────────────────────────────────────────── */
|
||||||
.filterbar {
|
.filterbar {
|
||||||
padding: 10px 18px; background: var(--panel-2);
|
padding: 10px 18px; background: var(--panel-2);
|
||||||
|
|
@ -163,11 +180,17 @@
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header>
|
<header>
|
||||||
<div class="brand"><span class="mark"></span>FLEET<span class="nm">OPS</span></div>
|
<div class="brand"><span class="mark"></span>FLEET<span class="nm">OPS</span></div>
|
||||||
|
<nav class="tabs" id="tabs">
|
||||||
|
<button class="tab active" data-tab="logistics" type="button">Logistics</button>
|
||||||
|
<button class="tab" data-tab="tickets" type="button">Tickets</button>
|
||||||
|
</nav>
|
||||||
<div id="kpis"></div>
|
<div id="kpis"></div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="clock"><span class="label">EAT</span><b id="clock-time">—</b></div>
|
<div class="clock"><span class="label">EAT</span><b id="clock-time">—</b></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<section class="view active" id="view-logistics">
|
||||||
<div class="filterbar">
|
<div class="filterbar">
|
||||||
<div class="ff">
|
<div class="ff">
|
||||||
<label for="f-cc">Cost centre</label>
|
<label for="f-cc">Cost centre</label>
|
||||||
|
|
@ -213,6 +236,31 @@
|
||||||
<div class="tbl-scroll" id="drv-wrap"><div class="empty">Loading…</div></div>
|
<div class="tbl-scroll" id="drv-wrap"><div class="empty">Loading…</div></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="view" id="view-tickets">
|
||||||
|
<main id="main-tickets">
|
||||||
|
<div class="card span12">
|
||||||
|
<div class="banner" id="tk-banner">
|
||||||
|
Tickets data source not connected yet — this tab is scaffolded and ready to wire.
|
||||||
|
<ul>
|
||||||
|
<li>Ticket data must be served via <code>dashboard_api</code> (proxied / presigned from the rustfs <code>tickets</code> bucket); credentials are never embedded in this static SPA.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card span8">
|
||||||
|
<h2>Recent tickets <span class="count" id="tk-count"></span></h2>
|
||||||
|
<div class="tbl-scroll" id="tk-wrap"><div class="empty">Loading…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card span4">
|
||||||
|
<h2>By status</h2>
|
||||||
|
<div id="tk-status"><div class="empty">Loading…</div></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -432,6 +480,7 @@ async function loadAll() {
|
||||||
api(`/analytics/fuel?${q}`),
|
api(`/analytics/fuel?${q}`),
|
||||||
]);
|
]);
|
||||||
const fuelL = ((fuel && fuel.rows) || []).reduce((s, r) => s + Number(r.actual_fuel_l || 0), 0);
|
const fuelL = ((fuel && fuel.rows) || []).reduce((s, r) => s + Number(r.actual_fuel_l || 0), 0);
|
||||||
|
lastTotals = summary.totals; lastFuelL = fuelL;
|
||||||
renderKpis(summary.totals, fuelL);
|
renderKpis(summary.totals, fuelL);
|
||||||
renderTrend(util.daily_trend);
|
renderTrend(util.daily_trend);
|
||||||
renderVehicles(summary.rows);
|
renderVehicles(summary.rows);
|
||||||
|
|
@ -446,6 +495,50 @@ async function loadAll() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TABS (Logistics ↔ Tickets)
|
||||||
|
// ============================================================================
|
||||||
|
// The header KPI strip is shared, so we cache the last logistics totals and
|
||||||
|
// re-render them when switching back from Tickets.
|
||||||
|
let lastTotals = null, lastFuelL = 0;
|
||||||
|
const ticketStats = {}; // populated once a tickets data source is wired
|
||||||
|
|
||||||
|
function renderTicketKpis() {
|
||||||
|
const k = [
|
||||||
|
['accent', ticketStats.open ?? '—', 'Open'],
|
||||||
|
['warn', ticketStats.in_progress ?? '—', 'In progress'],
|
||||||
|
['live', ticketStats.resolved ?? '—', 'Resolved'],
|
||||||
|
['', ticketStats.avg_resolution_h != null ? num(ticketStats.avg_resolution_h, 1) + 'h' : '—', 'Avg resolution'],
|
||||||
|
];
|
||||||
|
$('kpis').innerHTML = k.map(([c, v, l]) =>
|
||||||
|
`<div class="kpi"><b class="${c}">${v}</b><span>${l}</span></div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
let ticketsLoaded = false;
|
||||||
|
async function loadTickets() {
|
||||||
|
// Integration point. FleetOps is a credential-less static SPA, so ticket data
|
||||||
|
// must arrive through dashboard_api (proxied / presigned from the rustfs
|
||||||
|
// `tickets` bucket) — never by embedding S3 keys here. Wire the fetch + the
|
||||||
|
// renderers below once that endpoint exists. Until then, show empty states.
|
||||||
|
ticketsLoaded = true;
|
||||||
|
$('tk-count').textContent = '';
|
||||||
|
$('tk-wrap').innerHTML = '<div class="empty">No ticket data source connected yet.</div>';
|
||||||
|
$('tk-status').innerHTML = '<div class="empty">—</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(name) {
|
||||||
|
document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === name));
|
||||||
|
document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === `view-${name}`));
|
||||||
|
if (name === 'tickets') {
|
||||||
|
renderTicketKpis();
|
||||||
|
if (!ticketsLoaded) loadTickets();
|
||||||
|
} else {
|
||||||
|
renderKpis(lastTotals, lastFuelL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.tab').forEach(b =>
|
||||||
|
b.addEventListener('click', () => switchTab(b.dataset.tab)));
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// BOOT
|
// BOOT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue