From eac59b823c989a578424e1c8c471d474b26d7af0 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Tue, 18 Aug 2026 17:36:01 +1000 Subject: [PATCH] =?UTF-8?q?fix(pwa):=20=D0=B2=20=D1=88=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=84=D0=B0=D1=85=20=C2=AB=D0=BF=D0=BE=D0=B3=D0=B0=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=C2=BB,=20=D0=B0=20=D0=BD=D0=B5=20=C2=AB=D0=BE=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D1=87=D0=B5=D0=BD=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Слово «оплачен» водитель читает как «я ничего не должен», а постановление к тому моменту оплачено парком — не им. Поэтому в списке и карточке состояние теперь «не погашен / погашен / не выставлялся / уточняется», долг в шапке берётся из баланса 1С, а карточка объясняет расхождение сумм: «постановление — 750 ₽, парк оплатил его со скидкой и выставил вам фактическую сумму». Co-Authored-By: Claude Opus 5 --- driver-pwa/frontend/src/api/driver.ts | 12 +++++- driver-pwa/frontend/src/lib/fineStatus.ts | 30 ++++++++++++++ .../frontend/src/pages/FineDetailPage.tsx | 28 ++++++++----- .../frontend/src/pages/FinesPage.test.tsx | 39 +++++++++++++------ driver-pwa/frontend/src/pages/FinesPage.tsx | 27 ++++++++----- 5 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 driver-pwa/frontend/src/lib/fineStatus.ts diff --git a/driver-pwa/frontend/src/api/driver.ts b/driver-pwa/frontend/src/api/driver.ts index c46669a..b08a079 100644 --- a/driver-pwa/frontend/src/api/driver.ts +++ b/driver-pwa/frontend/src/api/driver.ts @@ -147,10 +147,17 @@ const FineBriefSchema = z.object({ article: z.string(), address: z.string().default(""), car_number: z.string().default(""), + // Сумма, начисленная ВОДИТЕЛЮ (парк платит ГИБДД со скидкой и выставляет то, + // что заплатил). fine_amount — номинал постановления, он бывает другим. amount: z.number(), + billed: z.boolean().default(false), + fine_amount: z.number().default(0), discount_amount: z.number().default(0), discount_until: z.string().nullable().default(null), - paid: z.boolean(), + // Погашен ли штраф ВОДИТЕЛЕМ перед парком. Не путать с closed_with_gibdd: + // постановление парк гасит авансом, долг водителя от этого не исчезает. + settled: z.enum(["owed", "paid", "not_charged", "cancelled", "unknown"]), + closed_with_gibdd: z.boolean().default(false), cancelled: z.boolean().default(false), has_photo: z.boolean().default(false), }); @@ -159,7 +166,8 @@ export type FineBrief = z.infer; export const FinesSchema = z.object({ items: z.array(FineBriefSchema), open_count: z.number(), - open_sum: z.number(), + // Долг по штрафам из 1С — тот же, что на главном экране (отрицательный). + debt: z.number(), }); export const FineDetailSchema = FineBriefSchema.extend({ diff --git a/driver-pwa/frontend/src/lib/fineStatus.ts b/driver-pwa/frontend/src/lib/fineStatus.ts new file mode 100644 index 0000000..70f2773 --- /dev/null +++ b/driver-pwa/frontend/src/lib/fineStatus.ts @@ -0,0 +1,30 @@ +import type { FineBrief } from "@/api/driver"; + +/** + * Что написать водителю о штрафе. + * + * Ключевое различие, которое интерфейс обязан удерживать: постановление в ГИБДД + * гасит ПАРК, обычно авансом и со скидкой, — и с этого момента штраф закрыт для + * государства, но не для водителя. Водитель возвращает деньги парку, и это видно + * только по балансу «Долг по штрафам». Поэтому слово «оплачен» здесь не + * используется вовсе: оно означало бы для человека «я ничего не должен». + */ +export function fineStatusText(f: Pick): string { + switch (f.settled) { + case "owed": + return "не погашен"; + case "paid": + return "погашен"; + case "not_charged": + return "не выставлялся"; + case "cancelled": + return "отменён"; + default: + return "уточняется"; + } +} + +/** Долг именно этого штрафа перед парком — по нему красим сумму. */ +export function isOwed(f: Pick): boolean { + return f.settled === "owed"; +} diff --git a/driver-pwa/frontend/src/pages/FineDetailPage.tsx b/driver-pwa/frontend/src/pages/FineDetailPage.tsx index ab82b6d..3e4b686 100644 --- a/driver-pwa/frontend/src/pages/FineDetailPage.tsx +++ b/driver-pwa/frontend/src/pages/FineDetailPage.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useNavigate, useParams } from "react-router-dom"; import { FileText, Gauge } from "lucide-react"; import { getFine } from "@/api/driver"; +import { fineStatusText, isOwed } from "@/lib/fineStatus"; import { formatDate, formatMoney } from "@/lib/format"; import { Spinner } from "@/components/Spinner"; @@ -39,7 +40,7 @@ export function FineDetailPage() { } const discountLive = - f.discount_amount > 0 && !!f.discount_until && !f.paid && + f.discount_amount > 0 && !!f.discount_until && !f.closed_with_gibdd && new Date(f.discount_until) >= new Date(new Date().toDateString()); return ( @@ -72,15 +73,24 @@ export function FineDetailPage() {

-

+

Начислено вам

+

{formatMoney(f.amount)}

-

- {f.cancelled ? "Постановление отменено" : f.paid ? "Оплачен" : "Не оплачен"} -

- {discountLive && ( - // Половина суммы в первые 20 дней — то, ради чего в карточку заходят - // вовремя. Дату показываем прямо здесь, а не в реквизитах ниже. +

{fineStatusText(f)}

+ {f.billed && f.fine_amount > 0 && f.fine_amount !== f.amount && ( + // Номинал постановления и начисление расходятся почти всегда: парк + // платит со скидкой. Не показать этого — значит получить вопрос «почему + // с меня 618, если в постановлении 750». +

+ Постановление — {formatMoney(f.fine_amount)}, парк оплатил его + {f.fine_amount > f.amount ? " со скидкой" : ""} и выставил вам фактическую сумму +

+ )} + {f.closed_with_gibdd && ( +

В ГИБДД постановление закрыто

+ )} + {discountLive && !f.billed && (

До {formatDate(f.discount_until)} — {formatMoney(f.discount_amount)} со скидкой

@@ -111,7 +121,7 @@ export function FineDetailPage() { )} - {!f.paid && !f.cancelled && ( + {isOwed(f) && (
) : items.length === 0 ? (

- {tab === "open" ? "Неоплаченных штрафов нет" : "Штрафов не найдено"} + {tab === "open" ? "Непогашенных штрафов нет" : "Штрафов не найдено"}

) : (
@@ -78,10 +89,8 @@ export function FinesPage() { - {formatMoney(f.amount)} - - {f.cancelled ? "отменён" : f.paid ? "оплачен" : "не оплачен"} - + {formatMoney(f.amount)} + {fineStatusText(f)}