/* Overview.jsx — головний екран: KPI + алерти + активність (live API). */ const { useState: useStateO, useEffect: useEffectO } = React; function Sparkline({ data, color = "var(--accent)", height = 32 }) { if (!data || !data.length) return null; const w = 80, h = height; const max = Math.max(...data), min = Math.min(...data); const range = max - min || 1; const pts = data.map((v, i) => [ (i / Math.max(1, data.length - 1)) * w, h - ((v - min) / range) * (h - 4) - 2, ]); const path = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(" "); const last = pts[pts.length - 1]; return ( ); } function KpiCard({ label, value, sub, sparkData, sparkColor, alert }) { return (
{label}
{value}
{sparkData && }
{sub &&
{sub}
}
); } function ActionLabel(action) { const map = { "passenger.stops.create": "Створено зупинку", "passenger.stops.update": "Оновлено зупинку", "passenger.stops.delete": "Видалено зупинку", "passenger.stops.publish": "Опубліковано зупинку", "passenger.routes.create": "Створено маршрут", "passenger.routes.update": "Оновлено маршрут", "passenger.routes.publish": "Опубліковано маршрут", "passenger.routes.pause": "Призупинено маршрут", "passenger.routes.resume": "Відновлено маршрут", "passenger.routes.delete": "Видалено маршрут", "passenger.settings.update": "Оновлено налаштування", "passenger.monitoring.ack": "Підтверджено алерт", }; return map[action] || action; } function ActionIcon(action) { if (action.endsWith(".create")) return "➕"; if (action.endsWith(".update")) return "✏️"; if (action.endsWith(".delete")) return "🗑"; if (action.endsWith(".publish")) return "✅"; if (action.endsWith(".pause")) return "⏸"; if (action.endsWith(".resume")) return "▶"; if (action.endsWith(".ack")) return "✓"; if (action.includes("settings")) return "⚙"; return "•"; } function formatTime(ts) { if (!ts) return ""; return new Date(ts).toLocaleTimeString("uk-UA", { hour: "2-digit", minute: "2-digit" }); } function AlertRow({ alert, onGoTo }) { return (
{alert.route_id || alert.central_id || "—"} {alert.kind || alert.code}
{alert.message || "—"}
{alert.age_label || ""}
); } function ActivityItem({ item }) { return (
{ActionIcon(item.action || "")}
{ActionLabel(item.action)} {item.target_id ? #{item.target_id} : null}
{formatTime(item.ts)}
); } function Overview({ onNav }) { const [data, setData] = useStateO(null); const [loading, setLoading] = useStateO(true); const [error, setError] = useStateO(null); const reload = async () => { try { const res = await window.passengerApi.overview.get(); setData(res); setError(null); } catch (e) { setError(e); window.showToast && window.showToast("Не вдалося завантажити Огляд: " + (e.message || ""), "bad"); } finally { setLoading(false); } }; useEffectO(() => { reload(); }, []); window.passengerApi.usePolling(reload, 30000, []); const kpi = (data && data.kpi) || {}; const alerts = (data && data.alerts) || []; const activity = (data && data.activity) || []; return ( <> { await reload(); window.showToast && window.showToast("Дані оновлено", "ok"); }}> ⟳ Оновити } />
{error && (
Помилка завантаження.
)} {loading && !data ? (
Завантаження…
) : ( <> {/* Alert banner */} {alerts.length > 0 && (
{kpi.alerts_total || alerts.length} алертів у мережі · {alerts.slice(0, 5).map(a => a.central_id).filter(Boolean).join(", ")}
)} {/* KPI grid */}
0} /> 0} />
Активні алерти {alerts.length}
{alerts.length === 0 ? (
Немає активних алертів
) : ( alerts.map(a => ) )}
Активність
{activity.length === 0 ? (
Немає подій
) : ( activity.map(item => ) )}
{[ { icon: "📍", label: "Зупинки", sub: "CRUD-таблиця", screen: "stops" }, { icon: "🛤", label: "Маршрути", sub: `${kpi.active_routes || 0} активних`, screen: "routes" }, { icon: "📡", label: "Моніторинг", sub: kpi.vehicles_online != null ? `${kpi.vehicles_online} ТЗ онлайн` : "—", screen: "monitoring" }, ].map(q => ( ))}
)}
); } Object.assign(window, { Overview, Sparkline });