Files
mechanic-pwa/driver-pwa/frontend/src/App.tsx
T
tremble7681andClaude Opus 5 00006ed51b Лента уведомлений: колокольчик, экран и честная отметка «прочитано»
Пуш живёт до первого смахивания, сообщение в боте тонет в переписке — и
разговор «мне ничего не приходило» упирался в слово против слова. Теперь у
водителя есть место, куда посмотреть, а у парка — отметка о прочтении, которая
ставится ровно тогда, когда человек открыл ленту или нажал на уведомление.

Отмечаем ПОСЛЕ показа списка, а не при загрузке: иначе счётчик гаснет у того,
кто мазнул по экрану и ушёл. Время первого прочтения бэкенд не переписывает.

Нажатие по пушу: воркер сам отметить не может (токен лежит в хранилище
страницы, ему недоступном), поэтому номер уведомления приезжает в пуше, воркер
добавляет его к адресу параметром, а страница отмечает и параметр убирает.

Колокольчик по умолчанию выключен в общей шапке и включён только на главной:
шапка стоит на каждом экране, и незачем ходить за счётчиком с оплаты.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:24:55 +10:00

82 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from "react";
import { useEffect } from "react";
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "@/store/auth";
import { useStaff } from "@/store/staff";
import { ImpersonationBar } from "@/components/ImpersonationBar";
import { LoginPage } from "@/pages/LoginPage";
import { TgWaitPage } from "@/pages/TgWaitPage";
import { OnboardingPage } from "@/pages/OnboardingPage";
import { BalancePage } from "@/pages/BalancePage";
import { TopupPage } from "@/pages/TopupPage";
import { PaymentPage } from "@/pages/PaymentPage";
import { DevLoginPage } from "@/pages/DevLoginPage";
import { DebugPage } from "@/pages/DebugPage";
import { MockPayPage } from "@/pages/MockPayPage";
import { FineDetailPage } from "@/pages/FineDetailPage";
import { NotificationsPage } from "@/pages/NotificationsPage";
import { markNotificationsRead } from "@/api/driver";
function RequireAuth({ children }: { children: ReactNode }) {
const token = useAuth((s) => s.token);
return token ? <>{children}</> : <Navigate to="/login" replace />;
}
/** Отладка существует только для сотрудника: либо он вошёл учёткой CRM, либо
* прямо сейчас смотрит от лица водителя. Обычному водителю адрес /debug ничего
* не даёт — его уводит на главную. */
function RequireStaff({ children }: { children: ReactNode }) {
const impersonated = useAuth((s) => s.impersonated);
const staffToken = useStaff((s) => s.token);
return impersonated || staffToken ? <>{children}</> : <Navigate to="/" replace />;
}
/**
* Нажатие по пушу — единственный честный признак, что уведомление увидели.
* Сам воркер отметить его не может: токен водителя лежит в хранилище страницы,
* ему недоступном. Поэтому воркер добавляет к адресу `?n=<номер>`, а отмечает
* страница — и тут же убирает параметр, чтобы он не остался в истории.
*/
function MarkPushRead() {
const loc = useLocation();
const nav = useNavigate();
useEffect(() => {
const params = new URLSearchParams(loc.search);
const raw = params.get("n");
if (!raw) return;
const id = Number(raw);
if (Number.isFinite(id) && id > 0) void markNotificationsRead([id], "push");
params.delete("n");
const rest = params.toString();
nav(loc.pathname + (rest ? `?${rest}` : ""), { replace: true });
}, [loc.search, loc.pathname, nav]);
return null;
}
export default function App() {
return (
<div className="mx-auto max-w-md min-h-full px-4 pt-3 pb-8">
{/* Плашка живёт над маршрутами: чей экран открыт, видно на каждом из них,
а не только там, где про неё вспомнили. Сама решает, показываться ли. */}
<ImpersonationBar />
<MarkPushRead />
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/dev" element={<DevLoginPage />} />
<Route path="/tg-wait" element={<TgWaitPage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
<Route path="/" element={<RequireAuth><BalancePage /></RequireAuth>} />
<Route path="/topup" element={<RequireAuth><TopupPage /></RequireAuth>} />
<Route path="/pay/:orderId" element={<RequireAuth><PaymentPage /></RequireAuth>} />
<Route path="/mock-pay/:orderId" element={<RequireAuth><MockPayPage /></RequireAuth>} />
<Route path="/fines/:fineId" element={<RequireAuth><FineDetailPage /></RequireAuth>} />
<Route path="/notifications" element={<RequireAuth><NotificationsPage /></RequireAuth>} />
<Route path="/debug" element={<RequireAuth><RequireStaff><DebugPage /></RequireStaff></RequireAuth>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
);
}