/* Routes.jsx — список маршрутів, live API. */ const { useState: useStateR, useEffect: useEffectR, useRef: useRefR } = React; function slugifyRouteCode(name) { const map = { "а":"a","б":"b","в":"v","г":"g","ґ":"g","д":"d","е":"e","є":"ie", "ж":"zh","з":"z","и":"y","і":"i","ї":"yi","й":"y","к":"k","л":"l","м":"m", "н":"n","о":"o","п":"p","р":"r","с":"s","т":"t","у":"u","ф":"f","х":"kh", "ц":"ts","ч":"ch","ш":"sh","щ":"shch","ъ":"","ы":"y","ь":"","э":"e","ю":"iu","я":"ia", }; let out = ""; for (const ch of (name || "").toLowerCase()) out += map[ch] !== undefined ? map[ch] : ch; out = out.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); if (!out) out = "route"; return out.slice(0, 30) + "-" + Math.floor(1000 + Math.random() * 9000); } function Routes({ onNav }) { const [routes, setRoutes] = useStateR([]); const [counts, setCounts] = useStateR({ all: 0, draft: 0, published: 0, archived: 0 }); const [loading, setLoading] = useStateR(true); const [error, setError] = useStateR(null); const [search, setSearch] = useStateR(""); const [filter, setFilter] = useStateR("all"); const [drawerOpen, setDrawerOpen] = useStateR(false); const [saving, setSaving] = useStateR(false); const [createForm, setCreateForm] = useStateR({ name: "", notes: "" }); const [createErrors, setCreateErrors] = useStateR({}); const reload = async (opts = {}) => { setLoading(true); try { const res = await window.passengerApi.routes.list({ search: opts.search ?? search, status: opts.filter ?? filter, }); setRoutes(res.items || []); setCounts(res.counts || { all: 0, draft: 0, published: 0, archived: 0 }); setError(null); } catch (e) { setError(e); window.showToast && window.showToast("Не вдалося завантажити маршрути: " + (e.message || ""), "bad"); } finally { setLoading(false); } }; useEffectR(() => { reload({ search, filter }); /* eslint-disable-next-line */ }, [search, filter]); window.passengerApi.usePolling(() => reload({ search, filter }), 30000, [search, filter]); const handlePauseResume = async (route) => { try { if (route.paused) { await window.passengerApi.routes.resume(route.id); window.showToast && window.showToast(`${route.code} відновлено`, "ok"); } else { await window.passengerApi.routes.pause(route.id); window.showToast && window.showToast( `${route.code} призупинено`, "warn", async () => { try { await window.passengerApi.routes.resume(route.id); await reload(); window.showToast && window.showToast(`${route.code} відновлено`, "ok"); } catch (e) { window.showToast && window.showToast("Помилка відновлення: " + (e.message || ""), "bad"); } } ); } await reload(); } catch (e) { window.showToast && window.showToast("Помилка: " + (e.message || ""), "bad"); } }; const handlePublish = async (route) => { try { await window.passengerApi.routes.publish(route.id); window.showToast && window.showToast(`${route.code} опубліковано`, "ok"); await reload(); } catch (e) { window.showToast && window.showToast("Помилка публікації: " + (e.message || ""), "bad"); } }; const handleDelete = async (route) => { try { await window.passengerApi.routes.remove(route.id); window.showToast && window.showToast( `Маршрут ${route.code} видалено`, "warn", async () => { try { await window.passengerApi.routes.restore(route.id); await reload(); window.showToast && window.showToast(`${route.code} відновлено`, "ok"); } catch (e) { window.showToast && window.showToast("Помилка відновлення: " + (e.message || ""), "bad"); } } ); await reload(); } catch (e) { window.showToast && window.showToast("Помилка видалення: " + (e.message || ""), "bad"); } }; const handleCreate = async () => { setCreateErrors({}); const name = (createForm.name || "").trim(); if (!name) { setCreateErrors({ name: "Введіть назву" }); return; } setSaving(true); try { await window.passengerApi.routes.create({ code: slugifyRouteCode(name), name, notes: createForm.notes || null, status: "draft", }); window.showToast && window.showToast(`Маршрут «${name}» створено (чернетка)`, "info"); setCreateForm({ name: "", notes: "" }); setDrawerOpen(false); await reload(); } catch (e) { if (e.errors) setCreateErrors(e.errors); else window.showToast && window.showToast("Помилка створення: " + (e.message || ""), "bad"); } finally { setSaving(false); } }; return ( <> setDrawerOpen(true)}> + Новий маршрут } />
{error && (
Помилка завантаження.
)}
🔍 setSearch(e.target.value)} placeholder="Пошук маршруту…" style={{ paddingLeft: 32 }} />
{[ { id: "all", label: `Усі ${counts.all}` }, { id: "published", label: `Активні ${counts.published}` }, { id: "draft", label: `Чернетки ${counts.draft}` }, ].map(f => ( ))}
{loading ? (
Завантаження…
) : routes.length === 0 ? (
🛤
{search ? `Нічого за «${search}»` : "Маршрутів немає"}
{search ? : }
) : (
{routes.map(route => ( handlePauseResume(route)} onPublish={() => handlePublish(route)} onDelete={() => handleDelete(route)} onNav={onNav} /> ))}
)}
{ if (!saving) setDrawerOpen(false); }} title="Новий маршрут" width={420} footer={<> } >
{ setCreateForm(f => ({ ...f, name: e.target.value })); setCreateErrors(er => ({ ...er, name: "" })); }} placeholder="напр. Хрещатик ↔ Сирець" autoFocus /> {createErrors.name &&
{createErrors.name}
}