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() {
- {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)} со скидкой