From a0ada7de6718c1eaa66ca28fccb7293223385240 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Tue, 18 Aug 2026 15:00:22 +1000 Subject: [PATCH] =?UTF-8?q?feat(pwa):=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC?= =?UTF-8?q?=D0=BE=D1=82=D1=80=20=D1=87=D1=83=D0=B6=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=B8=D0=B7=20=D1=81?= =?UTF-8?q?=D0=B2=D0=BE=D0=B5=D0=B3=D0=BE=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83?= =?UTF-8?q?=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сотрудник, вошедший водителем, при переключении на чужую карточку терял собственный вход: токен один на приложение, и «посмотреть, как выглядит у другого» означало заново входить через Telegram. Теперь свой сеанс откладывается и возвращается кнопкой «Вернуться к своему аккаунту». Откладывается ровно один раз — на переходе из обычного сеанса: смена водителя внутри просмотра не должна подменять отложенное чужим токеном. Заодно вход для сотрудника достижим изнутри приложения: длинный тап по названию в шапке ведёт на /dev. Ссылкой на экране это не делаем — водителю она ни к чему, а сотруднику иначе приходится набирать адрес руками. Co-Authored-By: Claude Opus 5 --- .../frontend/src/components/AppHeader.tsx | 30 ++++++++++++- .../frontend/src/components/DriverPicker.tsx | 4 +- .../frontend/src/pages/DebugPage.test.tsx | 32 +++++++++++++ driver-pwa/frontend/src/pages/DebugPage.tsx | 13 ++++-- driver-pwa/frontend/src/store/auth.ts | 45 ++++++++++++++++++- 5 files changed, 116 insertions(+), 8 deletions(-) diff --git a/driver-pwa/frontend/src/components/AppHeader.tsx b/driver-pwa/frontend/src/components/AppHeader.tsx index b34d92c..224ce07 100644 --- a/driver-pwa/frontend/src/components/AppHeader.tsx +++ b/driver-pwa/frontend/src/components/AppHeader.tsx @@ -1,7 +1,35 @@ +import { useNavigate } from "react-router-dom"; +import { useRef } from "react"; + +/** Долгое нажатие, после которого открывается вход для сотрудника. */ +const LONG_PRESS_MS = 1500; + export function AppHeader() { + const nav = useNavigate(); + const timer = useRef(null); + + // Ссылки «для сотрудника» на экранах водителя нет намеренно: водителю она + // ничего не даёт, а место занимает. Но сотруднику, уже вошедшему водителем, + // адрес /dev иначе приходится набирать руками — отсюда долгий тап по названию. + const start = () => { + timer.current = window.setTimeout(() => nav("/dev"), LONG_PRESS_MS); + }; + const cancel = () => { + if (timer.current) window.clearTimeout(timer.current); + timer.current = null; + }; + return (
- Премиум Водитель + e.preventDefault()} + > + Премиум Водитель +
); } diff --git a/driver-pwa/frontend/src/components/DriverPicker.tsx b/driver-pwa/frontend/src/components/DriverPicker.tsx index 741aa43..8804293 100644 --- a/driver-pwa/frontend/src/components/DriverPicker.tsx +++ b/driver-pwa/frontend/src/components/DriverPicker.tsx @@ -11,7 +11,7 @@ import { Spinner } from "@/components/Spinner"; * первый вход сотрудника (/dev) и смена водителя на вкладке «Отладка». */ export function DriverPicker({ onPicked }: { onPicked: (d: DriverBrief) => void }) { - const setSession = useAuth((s) => s.setSession); + const startImpersonation = useAuth((s) => s.startImpersonation); const [q, setQ] = useState(""); const [list, setList] = useState([]); const [loading, setLoading] = useState(true); @@ -35,7 +35,7 @@ export function DriverPicker({ onPicked }: { onPicked: (d: DriverBrief) => void setBusyId(d.id); try { const r = await impersonate(d.id); - setSession(r.token, r.driver.id, { impersonated: true, driverName: r.driver.fio }); + startImpersonation(r.token, r.driver.id, r.driver.fio); onPicked(r.driver); } catch (e: unknown) { toast.error(await apiErrorText(e, "Не удалось открыть приложение за водителя")); diff --git a/driver-pwa/frontend/src/pages/DebugPage.test.tsx b/driver-pwa/frontend/src/pages/DebugPage.test.tsx index 4f08bcc..38ab8d0 100644 --- a/driver-pwa/frontend/src/pages/DebugPage.test.tsx +++ b/driver-pwa/frontend/src/pages/DebugPage.test.tsx @@ -65,3 +65,35 @@ describe("вкладка отладки", () => { expect(screen.getByText("123 мс")).toBeInTheDocument(); }); }); + +describe("возврат к своему аккаунту", () => { + beforeEach(() => { useAuth.getState().clear(); useStaff.getState().clear(); clearLog(); }); + + it("свой вход не теряется при просмотре чужого экрана", () => { + useAuth.getState().setSession("мой-токен", 91); + useAuth.getState().startImpersonation("чужой-токен", 6866, "Тест Тестовый тестович"); + let s = useAuth.getState(); + expect([s.token, s.driverId, s.impersonated]).toEqual(["чужой-токен", 6866, true]); + + // смена водителя внутри просмотра не должна подменять отложенный свой вход + useAuth.getState().startImpersonation("третий-токен", 2869, "Тестик Николай"); + expect(useAuth.getState().own).toEqual({ token: "мой-токен", driverId: 91 }); + + expect(useAuth.getState().stopImpersonation()).toBe(true); + s = useAuth.getState(); + expect([s.token, s.driverId, s.impersonated, s.own]).toEqual(["мой-токен", 91, false, null]); + }); + + it("если своего входа не было — просмотр просто заканчивается выходом", () => { + useAuth.getState().startImpersonation("чужой-токен", 6866, "Тест"); + expect(useAuth.getState().stopImpersonation()).toBe(false); + expect(useAuth.getState().token).toBeNull(); + }); + + it("кнопка предлагает вернуться к себе, когда есть куда", async () => { + useAuth.getState().setSession("мой-токен", 91); + useAuth.getState().startImpersonation("чужой-токен", 6866, "Тест Тестовый тестович"); + renderAt("/debug"); + expect(screen.getByRole("button", { name: /Вернуться к своему аккаунту/ })).toBeInTheDocument(); + }); +}); diff --git a/driver-pwa/frontend/src/pages/DebugPage.tsx b/driver-pwa/frontend/src/pages/DebugPage.tsx index 56d886e..2d4b731 100644 --- a/driver-pwa/frontend/src/pages/DebugPage.tsx +++ b/driver-pwa/frontend/src/pages/DebugPage.tsx @@ -27,7 +27,7 @@ function Row({ k, v }: { k: string; v: string }) { */ export function DebugPage() { const nav = useNavigate(); - const { token, driverId, driverName, impersonated, clear } = useAuth(); + const { token, driverId, driverName, impersonated, own, clear, stopImpersonation } = useAuth(); const staff = useStaff(); const log = useSyncExternalStore(subscribe, getLog); const [switching, setSwitching] = useState(false); @@ -112,9 +112,16 @@ export function DebugPage() { {staff.token && (