Пуш живёт до первого смахивания, сообщение в боте тонет в переписке — и разговор «мне ничего не приходило» упирался в слово против слова. Теперь у водителя есть место, куда посмотреть, а у парка — отметка о прочтении, которая ставится ровно тогда, когда человек открыл ленту или нажал на уведомление. Отмечаем ПОСЛЕ показа списка, а не при загрузке: иначе счётчик гаснет у того, кто мазнул по экрану и ушёл. Время первого прочтения бэкенд не переписывает. Нажатие по пушу: воркер сам отметить не может (токен лежит в хранилище страницы, ему недоступном), поэтому номер уведомления приезжает в пуше, воркер добавляет его к адресу параметром, а страница отмечает и параметр убирает. Колокольчик по умолчанию выключен в общей шапке и включён только на главной: шапка стоит на каждом экране, и незачем ходить за счётчиком с оплаты. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
4.1 KiB
TypeScript
82 lines
4.1 KiB
TypeScript
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>
|
||
);
|
||
}
|