diff --git a/driver-pwa/frontend/src/App.tsx b/driver-pwa/frontend/src/App.tsx
index ea88670..9426bb0 100644
--- a/driver-pwa/frontend/src/App.tsx
+++ b/driver-pwa/frontend/src/App.tsx
@@ -12,7 +12,6 @@ import { PaymentPage } from "@/pages/PaymentPage";
import { DevLoginPage } from "@/pages/DevLoginPage";
import { DebugPage } from "@/pages/DebugPage";
import { MockPayPage } from "@/pages/MockPayPage";
-import { FinesPage } from "@/pages/FinesPage";
import { FineDetailPage } from "@/pages/FineDetailPage";
function RequireAuth({ children }: { children: ReactNode }) {
@@ -44,7 +43,6 @@ export default function App() {
} />
} />
} />
- } />
} />
} />
} />
diff --git a/driver-pwa/frontend/src/components/Statement.tsx b/driver-pwa/frontend/src/components/Statement.tsx
index 041a706..94f2a20 100644
--- a/driver-pwa/frontend/src/components/Statement.tsx
+++ b/driver-pwa/frontend/src/components/Statement.tsx
@@ -1,7 +1,8 @@
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 { ChevronRight, Image as ImageIcon } from "lucide-react";
+import { getFines, getStatement, type StatementKind } from "@/api/driver";
+import { fineStatusText, isOwed } from "@/lib/fineStatus";
import { formatDate, formatMoney } from "@/lib/format";
import { OperationIcon } from "@/components/operationIcon";
import { Spinner } from "@/components/Spinner";
@@ -14,10 +15,13 @@ const FILTERS: { key: StatementKind; label: string }[] = [
];
/**
- * Выписка по счёту за период — нижняя половина главного экрана.
+ * Нижняя половина главного экрана: за что списаны деньги.
*
- * Это ответ на вопрос «за что списано», который иначе задают звонком в парк.
- * Строки те же, по которым 1С считает долг: аренда, пени, штрафы, пополнения.
+ * Три фильтра показывают проводки 1С, а «Штрафы» — сами постановления, а не
+ * проводки по ним. Разница существенная: штраф, который водитель оплатил сам,
+ * парк не выставлял, проводки по нему нет, и в выписке он бы не появился вовсе.
+ * Отдельного экрана штрафов нет намеренно — два списка одного и того же в разных
+ * местах сбивают с толку.
*/
export function Statement({ from, to, kind, onFrom, onTo, onKind }: {
from: string;
@@ -28,11 +32,28 @@ export function Statement({ from, to, kind, onFrom, onTo, onKind }: {
onKind: (k: StatementKind) => void;
}) {
const nav = useNavigate();
- const { data, isLoading, isError, refetch } = useQuery({
+ const showFines = kind === "fines";
+
+ const ops = useQuery({
queryKey: ["statement", from, to, kind],
queryFn: () => getStatement(from, to, kind),
+ enabled: !showFines,
});
- const items = data?.items ?? [];
+ const fines = useQuery({
+ queryKey: ["fines"],
+ queryFn: () => getFines("all"),
+ enabled: showFines,
+ });
+
+ const active = showFines ? fines : ops;
+ // Период распространяется и на штрафы: иначе календарь над списком стоял бы,
+ // ничего не меняя, — это хуже, чем его отсутствие.
+ const fineRows = (fines.data?.items ?? []).filter(
+ (f) => !f.date || (f.date >= from && f.date <= to),
+ );
+ const opRows = ops.data?.items ?? [];
+ const isEmpty = showFines ? fineRows.length === 0 : opRows.length === 0;
+ const rowCls = "w-full flex items-center gap-3 py-2.5 border-b border-line last:border-0 text-left";
return (
+ {showFines ? "Штрафов за период нет" : "Нет операций за выбранный период"}
+
+ ) : showFines ? (
+
+ {fineRows.map((f) => (
+ // Штраф ведёт в карточку: там фотография с камеры, место и скорость.
+
+ ))}
- ) : items.length === 0 ? (
-
Нет операций за выбранный период
) : (
- {items.map((op, idx) => {
+ {opRows.map((op, idx) => {
const row = (
<>
- {/* Дата — первой строкой и чернильным цветом: в выписке ищут
- «что было 12-го», а не название операции, и бледная подпись
- под заголовком для этого не годится. */}
+ {/* Дата — первой строкой: в выписке ищут «что было 12-го», а не
+ название операции. */}
{formatDate(op.date)}{op.title}
{op.description && (
@@ -91,17 +136,14 @@ export function Statement({ from, to, kind, onFrom, onTo, onKind }: {
>
);
- 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 ? (
-
) : (
-
} />
- экран штрафов} />
@@ -64,9 +71,18 @@ describe("главный экран", () => {
it("фильтр перезапрашивает операции выбранного вида", async () => {
wrap();
- await userEvent.click(await screen.findByRole("button", { name: "Штрафы" }));
+ await userEvent.click(await screen.findByRole("button", { name: "Начисления" }));
const calls = vi.mocked(driver.getStatement).mock.calls;
- expect(calls[calls.length - 1][2]).toBe("fines");
+ expect(calls[calls.length - 1][2]).toBe("charges");
+ });
+
+ it("фильтр «Штрафы» показывает постановления, а не проводки", async () => {
+ // Штраф, оплаченный водителем самостоятельно, парк не выставлял — проводки по
+ // нему нет, и в выписке он бы не появился вовсе (Морозов, 18.08.2026).
+ wrap();
+ await userEvent.click(await screen.findByRole("button", { name: "Штрафы" }));
+ expect(await screen.findByText(/Превышение скорости/)).toBeInTheDocument();
+ expect(driver.getFines).toHaveBeenCalled();
});
it("аренда и штрафы видны сразу, остальные статьи — нет", async () => {
@@ -86,10 +102,11 @@ describe("главный экран", () => {
expect(await screen.findByText("экран пополнения")).toBeInTheDocument();
});
- it("строка штрафов открывает список, а не оплату: «за что» важнее кнопки", async () => {
+ it("строка штрафов включает фильтр внизу, а не уводит на другой экран", async () => {
wrap();
await userEvent.click(await screen.findByRole("button", { name: /Долг по штрафам/ }));
- expect(await screen.findByText("экран штрафов")).toBeInTheDocument();
+ expect(await screen.findByText(/Превышение скорости/)).toBeInTheDocument();
+ expect(driver.getFines).toHaveBeenCalled();
});
it("категории для выбора живут в шторке «Пополнить»", async () => {
diff --git a/driver-pwa/frontend/src/pages/BalancePage.tsx b/driver-pwa/frontend/src/pages/BalancePage.tsx
index 4991f1e..6226a0f 100644
--- a/driver-pwa/frontend/src/pages/BalancePage.tsx
+++ b/driver-pwa/frontend/src/pages/BalancePage.tsx
@@ -65,15 +65,16 @@ export function BalancePage() {
{main.length > 0 && (
{main.map((a) => (
- // Аренда ведёт к оплате — за тем её и открывают. Штрафы ведут в свой
- // список: «−5 156 ₽» не отвечает на вопрос «за что», а платить,
- // не поняв за что, человек не станет.
+ // Аренда ведёт к оплате — за тем её и открывают. Штрафы включают
+ // фильтр в списке ниже: «−5 156 ₽» не отвечает на вопрос «за что»,
+ // а отдельного экрана штрафов нет — два списка одного и того же в
+ // разных местах сбивали с толку.