/* 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 (
);
}
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 });