feat(pwa): вход для сотрудника, просмотр от лица водителя и вкладка «Отладка»
Приложение обкатывается на боевом контуре, поэтому интерфейс надо проверять на живых данных — а живые данные лежат у настоящих водителей. * «Вход для сотрудника» (/dev) — логин учётки CRM, поддержан второй шаг 2FA; дальше поиск водителя по ФИО/телефону/id и просмотр его экранов. * Плашка «Смотрю как …» над всеми экранами: интерфейс в этом режиме неотличим от водительского, и чужой баланс легко принять за свой. * Вкладка «Отладка» (/debug, водителю недоступна): смена водителя, driver_id, режим, срок токена, адрес API, коммит и время сборки, журнал последних 40 ответивших запросов, сброс кэша сервис-воркера. * Пополнение под просмотром идёт в учебный контур: экран /mock-pay/:orderId выбирает, что «ответит» банк, а настоящий экран оплаты разбирает эти статусы своим боевым кодом. Ни рубля, ни чека 54-ФЗ. Заодно доезжает незакоммиченная работа прошлой сессии: дев-прокси в vite.config.ts, его тест и разобранный .env.example. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { apiErrorText, mockPayOutcome } from "@/api/driver";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
|
||||
/**
|
||||
* Учебная «касса»: чем закончится платёж, который сотрудник создал под просмотром.
|
||||
*
|
||||
* Это подмена банка, а не экрана приложения. Настоящий экран оплаты
|
||||
* (`/pay/:orderId`) остаётся тем же самым и разбирает те же статусы — иначе
|
||||
* отладка проверяла бы поведение, которого у водителя не будет.
|
||||
*/
|
||||
const OUTCOMES = [
|
||||
{ key: "paid", label: "Оплата прошла", hint: "PAID — экран «Оплачено»" },
|
||||
{ key: "rejected", label: "Банк отклонил", hint: "REJECTED — «Оплата не прошла»" },
|
||||
{ key: "expired", label: "Счёт протух", hint: "DEADLINE_EXPIRED — «Срок оплаты истёк»" },
|
||||
{ key: "reversed", label: "Деньги вернулись", hint: "REVERSED — «Платёж возвращён»" },
|
||||
] as const;
|
||||
|
||||
export function MockPayPage() {
|
||||
const { orderId = "" } = useParams();
|
||||
const nav = useNavigate();
|
||||
const impersonated = useAuth((s) => s.impersonated);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
async function choose(outcome: (typeof OUTCOMES)[number]["key"]) {
|
||||
setBusy(outcome);
|
||||
try {
|
||||
await mockPayOutcome(orderId, outcome);
|
||||
nav(`/pay/${orderId}`, { replace: true });
|
||||
} catch (e: unknown) {
|
||||
toast.error(await apiErrorText(e, "Не удалось разыграть исход"));
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!impersonated) {
|
||||
return (
|
||||
<div className="pt-24 text-center">
|
||||
<p className="text-sm font-semibold mb-1">Учебная оплата недоступна</p>
|
||||
<p className="text-muted text-xs mb-6">Этот экран работает только при просмотре сотрудником.</p>
|
||||
<button className="btn-ghost" onClick={() => nav("/")}>На главную</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-8">
|
||||
<button className="text-muted text-xs mb-3" onClick={() => nav(-1)}>‹ Назад</button>
|
||||
<h1 className="text-lg font-extrabold mb-1">Учебная оплата</h1>
|
||||
<p className="text-muted text-xs mb-5">
|
||||
Банка в этом платеже нет: ни списания, ни чека, ни проводки. Выберите, что
|
||||
«ответит» банк — приложение покажет ровно то же, что увидел бы водитель.
|
||||
</p>
|
||||
<div className="card divide-y divide-line">
|
||||
{OUTCOMES.map((o) => (
|
||||
<button
|
||||
key={o.key}
|
||||
disabled={busy !== null}
|
||||
onClick={() => choose(o.key)}
|
||||
className="w-full text-left px-4 py-3 flex items-center justify-between gap-3 disabled:opacity-50"
|
||||
>
|
||||
<span>
|
||||
<span className="block text-sm font-semibold">{o.label}</span>
|
||||
<span className="block text-muted text-xs">{o.hint}</span>
|
||||
</span>
|
||||
{busy === o.key && <Spinner />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-muted text-xs mt-4">Заказ: {orderId}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user