From 3bdd5ce8188e635a1b4238f759bdc72e5af0ab52 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Tue, 18 Aug 2026 17:58:28 +1000 Subject: [PATCH] =?UTF-8?q?feat(pwa):=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=B2=20=D1=88=D1=82=D1=80=D0=B0=D1=84=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=B2=D1=8B=D0=BF=D0=B8=D1=81=D0=BA=D0=B8,=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=82=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D1=8B,=20=D0=BC=D0=B8=D0=BD=D1=83=D1=81=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=20=D0=BF=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дата в выписке и в списке штрафов встала первой строкой чернильным цветом: в этих списках ищут «что было 12-го», а бледная подпись под заголовком для этого не годилась. Строка штрафа в выписке стала кнопкой в карточку — там фотография и подробности. Остальные строки не притворяются ссылками. Раздел «Мои пополнения» удалён вместе с кнопкой: он показывал только наши платежи и у большинства водителей был пуст (1С проводит пополнения сама), а те же операции видны в выписке фильтром «Пополнения». Co-Authored-By: Claude Opus 5 --- driver-pwa/frontend/src/App.tsx | 2 - driver-pwa/frontend/src/api/driver.ts | 2 + .../frontend/src/components/Statement.tsx | 49 +++++++++----- .../frontend/src/pages/BalancePage.test.tsx | 9 +++ driver-pwa/frontend/src/pages/BalancePage.tsx | 9 ++- driver-pwa/frontend/src/pages/FinesPage.tsx | 10 +-- .../frontend/src/pages/HistoryPage.test.tsx | 25 ------- driver-pwa/frontend/src/pages/HistoryPage.tsx | 67 ------------------- 8 files changed, 55 insertions(+), 118 deletions(-) delete mode 100644 driver-pwa/frontend/src/pages/HistoryPage.test.tsx delete mode 100644 driver-pwa/frontend/src/pages/HistoryPage.tsx diff --git a/driver-pwa/frontend/src/App.tsx b/driver-pwa/frontend/src/App.tsx index c497d5d..ea88670 100644 --- a/driver-pwa/frontend/src/App.tsx +++ b/driver-pwa/frontend/src/App.tsx @@ -9,7 +9,6 @@ import { OnboardingPage } from "@/pages/OnboardingPage"; import { BalancePage } from "@/pages/BalancePage"; import { TopupPage } from "@/pages/TopupPage"; import { PaymentPage } from "@/pages/PaymentPage"; -import { HistoryPage } from "@/pages/HistoryPage"; import { DevLoginPage } from "@/pages/DevLoginPage"; import { DebugPage } from "@/pages/DebugPage"; import { MockPayPage } from "@/pages/MockPayPage"; @@ -45,7 +44,6 @@ export default function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/driver-pwa/frontend/src/api/driver.ts b/driver-pwa/frontend/src/api/driver.ts index 9d347a0..3187c5c 100644 --- a/driver-pwa/frontend/src/api/driver.ts +++ b/driver-pwa/frontend/src/api/driver.ts @@ -127,6 +127,8 @@ export const StatementSchema = z.object({ description: z.string().default(""), amount: z.number(), kind: z.enum(["charges", "payments", "fines"]), + // Карточка штрафа, если постановление у нас есть. null — строка не ссылка. + fine_id: z.number().nullable().default(null), })), from: z.string(), to: z.string(), diff --git a/driver-pwa/frontend/src/components/Statement.tsx b/driver-pwa/frontend/src/components/Statement.tsx index 9a3ee9e..041a706 100644 --- a/driver-pwa/frontend/src/components/Statement.tsx +++ b/driver-pwa/frontend/src/components/Statement.tsx @@ -1,4 +1,6 @@ import { useQuery } from "@tanstack/react-query"; +import { useNavigate } from "react-router-dom"; +import { ChevronRight } from "lucide-react"; import { getStatement, type StatementKind } from "@/api/driver"; import { formatDate, formatMoney } from "@/lib/format"; import { OperationIcon } from "@/components/operationIcon"; @@ -25,6 +27,7 @@ export function Statement({ from, to, kind, onFrom, onTo, onKind }: { onTo: (v: string) => void; onKind: (k: StatementKind) => void; }) { + const nav = useNavigate(); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ["statement", from, to, kind], queryFn: () => getStatement(from, to, kind), @@ -69,22 +72,38 @@ export function Statement({ from, to, kind, onFrom, onTo, onKind }: {

Нет операций за выбранный период

) : (
- {items.map((op, idx) => ( -
- - - {op.title} - - {formatDate(op.date)} - {op.description ? ` · ${op.description}` : ""} + {items.map((op, idx) => { + const row = ( + <> + + + {/* Дата — первой строкой и чернильным цветом: в выписке ищут + «что было 12-го», а не название операции, и бледная подпись + под заголовком для этого не годится. */} + {formatDate(op.date)} + {op.title} + {op.description && ( + {op.description} + )} - - - {formatMoney(op.amount)} - -
- ))} + + {formatMoney(op.amount)} + + + ); + const cls = "w-full flex items-center gap-3 py-2.5 border-b border-line last:border-0 text-left"; + // Строка штрафа ведёт в карточку: в выписке видно «Штраф ГИБДД + // −618,75 ₽», а за что именно — только там, вместе с фотографией. + return op.fine_id != null ? ( + + ) : ( +
{row}
+ ); + })}
)} diff --git a/driver-pwa/frontend/src/pages/BalancePage.test.tsx b/driver-pwa/frontend/src/pages/BalancePage.test.tsx index 734189c..69a8cce 100644 --- a/driver-pwa/frontend/src/pages/BalancePage.test.tsx +++ b/driver-pwa/frontend/src/pages/BalancePage.test.tsx @@ -112,6 +112,15 @@ describe("главный экран", () => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); + it("отдельного раздела пополнений нет — они в выписке", async () => { + // Раздел «Мои пополнения» показывал наши платежи и у большинства водителей был + // пуст: 1С проводит пополнения сама, и они уже видны фильтром в выписке. + wrap(); + await screen.findByText("−8 976,25 ₽"); + expect(screen.queryByText(/Мои пополнения/)).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Пополнения" })).toBeInTheDocument(); + }); + it("баланс не загрузился — экран говорит об этом, а не рисует ноль", async () => { vi.mocked(driver.getBalance).mockRejectedValueOnce(new Error("boom")); wrap(); diff --git a/driver-pwa/frontend/src/pages/BalancePage.tsx b/driver-pwa/frontend/src/pages/BalancePage.tsx index 1b0348b..4991f1e 100644 --- a/driver-pwa/frontend/src/pages/BalancePage.tsx +++ b/driver-pwa/frontend/src/pages/BalancePage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; -import { ArrowUp, ChevronRight, History, LogOut } from "lucide-react"; +import { ArrowUp, ChevronRight, LogOut } from "lucide-react"; import { getBalance, type StatementKind } from "@/api/driver"; import { formatMoney, isoDaysAgo, isoToday } from "@/lib/format"; import { useAuth } from "@/store/auth"; @@ -108,10 +108,9 @@ export function BalancePage() { -
- +
+ {/* Отдельного раздела «Мои пополнения» нет намеренно: пополнения видны в + выписке выше по фильтру, а дублирующий пустой экран сбивал с толку. */} diff --git a/driver-pwa/frontend/src/pages/FinesPage.tsx b/driver-pwa/frontend/src/pages/FinesPage.tsx index 1cb4164..eaa6c63 100644 --- a/driver-pwa/frontend/src/pages/FinesPage.tsx +++ b/driver-pwa/frontend/src/pages/FinesPage.tsx @@ -76,12 +76,14 @@ export function FinesPage() { > - {f.article} - + {formatDate(f.date)} - {f.car_number ? ` · ${f.car_number}` : ""} - {f.has_photo && } + {f.has_photo && } + {f.article} + {f.car_number && ( + {f.car_number} + )} {formatMoney(f.amount)} diff --git a/driver-pwa/frontend/src/pages/HistoryPage.test.tsx b/driver-pwa/frontend/src/pages/HistoryPage.test.tsx deleted file mode 100644 index f866132..0000000 --- a/driver-pwa/frontend/src/pages/HistoryPage.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { MemoryRouter } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; - -vi.mock("@/api/driver", () => ({ - getPayments: vi.fn(async () => ({ payments: [ - { order_id: "o1", bucket: "Долг аренда", amount: 1000, commission: 52, method: "qr", status: "PAID", created_at: "2026-06-18T09:00:00Z", paid_at: "2026-06-18T09:01:00Z" }, - ] })), -})); -import { HistoryPage } from "./HistoryPage"; - -function wrap() { - const qc = new QueryClient(); - return render(); -} - -describe("HistoryPage", () => { - it("lists payments with amount + status", async () => { - wrap(); - expect(await screen.findByText("Долг аренда")).toBeInTheDocument(); - expect(screen.getByText("1 000 ₽")).toBeInTheDocument(); - expect(screen.getByText(/Оплачено/i)).toBeInTheDocument(); - }); -}); diff --git a/driver-pwa/frontend/src/pages/HistoryPage.tsx b/driver-pwa/frontend/src/pages/HistoryPage.tsx deleted file mode 100644 index 683b84a..0000000 --- a/driver-pwa/frontend/src/pages/HistoryPage.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useNavigate } from "react-router-dom"; -import { getPayments } from "@/api/driver"; -import { formatMoney } from "@/lib/format"; -import { Spinner } from "@/components/Spinner"; - -/** - * Все статусы, которые может прислать бэкенд. Раньше их было пять из тринадцати, - * и водитель видел в истории «DEADLINE_EXPIRED» или «ATTEMPTS_EXPIRED» латиницей, - * а возврат денег выглядел ровно как обычный отказ. - */ -const STATUS_RU: Record = { - NEW: "Ожидает оплаты", - FORM: "Ожидает оплаты", - PAID: "Оплачено", - CONFIRMED: "Оплачено", - REJECTED: "Отклонён банком", - AUTH_FAIL: "Отклонён банком", - CANCELED: "Отменён", - DEADLINE_EXPIRED: "Истёк срок оплаты", - ATTEMPTS_EXPIRED: "Исчерпаны попытки оплаты", - ERROR: "Ошибка", - REVERSED: "Деньги возвращены", - REFUNDED: "Деньги возвращены", - PARTIAL_REVERSED: "Возвращена часть", - PARTIAL_REFUNDED: "Возвращена часть", -}; - -const RETURNED = new Set(["REVERSED", "REFUNDED", "PARTIAL_REVERSED", "PARTIAL_REFUNDED"]); - -export function HistoryPage() { - const nav = useNavigate(); - const { data, isLoading, isError } = useQuery({ queryKey: ["payments"], queryFn: getPayments }); - const rows = data?.payments ?? []; - - return ( -
- -

История пополнений

- {isLoading ? ( -
- ) : isError ? ( -

Не удалось загрузить историю

- ) : rows.length === 0 ? ( -

Пока нет пополнений

- ) : ( - rows.map((p) => { - const returned = RETURNED.has(p.status) || !!p.reversed_at; - return ( -
-
-

{p.bucket}

-

- {p.created_at ? new Date(p.created_at).toLocaleDateString("ru-RU") : ""} ·{" "} - {STATUS_RU[p.status] ?? p.status} -

-
- - {formatMoney(p.amount)} - -
- ); - }) - )} -
- ); -}