polish(driver-pwa): error states, typed catch, drop dead avatar, terminal poll

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-18 19:59:38 +10:00
co-authored by claude-flow
parent 0fba967cd8
commit 15d7f34689
6 changed files with 30 additions and 13 deletions
@@ -1,12 +1,7 @@
export function AppHeader({ initials }: { initials?: string }) { export function AppHeader() {
return ( return (
<header className="flex items-center justify-between mb-4"> <header className="mb-4">
<span className="text-sm font-bold">Премиум Водитель</span> <span className="text-sm font-bold">Премиум Водитель</span>
{initials && (
<span className="w-7 h-7 rounded-full bg-ink text-cream text-[10px] font-bold flex items-center justify-center">
{initials}
</span>
)}
</header> </header>
); );
} }
@@ -1,10 +1,12 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getPaymentState } from "@/api/driver"; import { getPaymentState } from "@/api/driver";
const TERMINAL = new Set(["PAID", "CONFIRMED", "REJECTED", "REVERSED", "REFUNDED", "CANCELED", "AUTH_FAIL"]);
export function usePaymentStatus(orderId: string) { export function usePaymentStatus(orderId: string) {
return useQuery({ return useQuery({
queryKey: ["payment", orderId], queryKey: ["payment", orderId],
queryFn: () => getPaymentState(orderId), queryFn: () => getPaymentState(orderId),
refetchInterval: (q) => (q.state.data?.status === "PAID" || q.state.data?.status === "REJECTED" ? false : 1500), refetchInterval: (q) => (q.state.data && TERMINAL.has(q.state.data.status) ? false : 1500),
}); });
} }
@@ -32,4 +32,16 @@ describe("BalancePage", () => {
expect(screen.getAllByText("3 200 ₽").length).toBeGreaterThan(0); expect(screen.getAllByText("3 200 ₽").length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: /Пополнить/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Пополнить/i })).toBeInTheDocument();
}); });
it("shows an error state when balance fails to load", async () => {
const driver = await import("@/api/driver");
(driver.getBalance as any).mockRejectedValueOnce(new Error("boom"));
// re-render fresh
const { QueryClient, QueryClientProvider } = await import("@tanstack/react-query");
const { MemoryRouter } = await import("react-router-dom");
const { render, screen } = await import("@testing-library/react");
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={qc}><MemoryRouter><BalancePage /></MemoryRouter></QueryClientProvider>);
expect(await screen.findByText(/Не удалось загрузить/i)).toBeInTheDocument();
});
}); });
@@ -10,14 +10,14 @@ import { Spinner } from "@/components/Spinner";
export function BalancePage() { export function BalancePage() {
const nav = useNavigate(); const nav = useNavigate();
const clear = useAuth((s) => s.clear); const clear = useAuth((s) => s.clear);
const { data, isLoading } = useQuery({ queryKey: ["balance"], queryFn: getBalance }); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ["balance"], queryFn: getBalance });
const accounts = data?.accounts ?? []; const accounts = data?.accounts ?? [];
const totalDebt = accounts.filter((a) => a.balance < 0).reduce((s, a) => s + a.balance, 0); const totalDebt = accounts.filter((a) => a.balance < 0).reduce((s, a) => s + a.balance, 0);
return ( return (
<div> <div>
<AppHeader initials="" /> <AppHeader />
<div className="card p-4 mb-4 text-center"> <div className="card p-4 mb-4 text-center">
<p className="text-muted text-xs mb-1">Общий долг</p> <p className="text-muted text-xs mb-1">Общий долг</p>
<p className={`text-2xl font-extrabold ${totalDebt < 0 ? "text-neg" : ""}`}>{formatMoney(totalDebt)}</p> <p className={`text-2xl font-extrabold ${totalDebt < 0 ? "text-neg" : ""}`}>{formatMoney(totalDebt)}</p>
@@ -25,6 +25,11 @@ export function BalancePage() {
{isLoading ? ( {isLoading ? (
<div className="flex justify-center py-10"><Spinner /></div> <div className="flex justify-center py-10"><Spinner /></div>
) : isError ? (
<div className="text-center py-8">
<p className="text-muted text-sm mb-3">Не удалось загрузить баланс</p>
<button className="btn-ghost" onClick={() => refetch()}>Повторить</button>
</div>
) : ( ) : (
<div className="mb-4"> <div className="mb-4">
{accounts.map((a) => ( {accounts.map((a) => (
@@ -10,7 +10,7 @@ const STATUS_RU: Record<string, string> = {
export function HistoryPage() { export function HistoryPage() {
const nav = useNavigate(); const nav = useNavigate();
const { data, isLoading } = useQuery({ queryKey: ["payments"], queryFn: getPayments }); const { data, isLoading, isError } = useQuery({ queryKey: ["payments"], queryFn: getPayments });
const rows = data?.payments ?? []; const rows = data?.payments ?? [];
return ( return (
@@ -19,6 +19,8 @@ export function HistoryPage() {
<h1 className="text-lg font-extrabold mb-4">История пополнений</h1> <h1 className="text-lg font-extrabold mb-4">История пополнений</h1>
{isLoading ? ( {isLoading ? (
<div className="flex justify-center py-10"><Spinner /></div> <div className="flex justify-center py-10"><Spinner /></div>
) : isError ? (
<p className="text-muted text-sm">Не удалось загрузить историю</p>
) : rows.length === 0 ? ( ) : rows.length === 0 ? (
<p className="text-muted text-sm">Пока нет пополнений</p> <p className="text-muted text-sm">Пока нет пополнений</p>
) : ( ) : (
+3 -2
View File
@@ -28,8 +28,9 @@ export function LoginPage() {
} }
setChannel(r.channel); setChannel(r.channel);
setStep("code"); setStep("code");
} catch (err: any) { } catch (err: unknown) {
toast.error(err?.response?.status === 404 ? "Номер не найден. Обратитесь в парк." : "Не удалось отправить код"); const status = (err as { response?: { status?: number } })?.response?.status;
toast.error(status === 404 ? "Номер не найден. Обратитесь в парк." : "Не удалось отправить код");
} finally { } finally {
setBusy(false); setBusy(false);
} }