Files
mechanic-pwa/driver-pwa/frontend/src/pages/FinesPage.tsx
T
tremble7681andClaude Opus 5 7204290e45 fix(pwa): штрафы не прячутся за вкладкой «Не погашены»
Водитель, оплативший штраф сам, видел пустой экран: список открывался на
«Не погашены», а там у него ноль (Морозов, штраф от 16.08 — оплачен им, парк его
не выставлял). Выглядело как «приложение потеряло мои штрафы».

Теперь вкладка выбирается по данным: есть непогашенные — открываем их, нет —
открываем «Все». Заодно один запрос вместо двух: список короткий, фильтруем на
месте, и переключение вкладок стало мгновенным.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:22:16 +10:00

106 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { ChevronRight, Image as ImageIcon } from "lucide-react";
import { getFines } 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";
/**
* Штрафы водителя списком.
*
* Строка «Долг по штрафам −5 156 ₽» на балансе не отвечает на вопрос «за что»,
* и водитель идёт выяснять звонком. Здесь он видит каждое постановление, а в
* карточке — фотографию с камеры.
*
* Долг в шапке берём из баланса 1С, а не суммой строк: постановление парк гасит
* авансом, и по бумагам ГИБДД оно закрыто в тот же день — долг перед парком от
* этого никуда не девается. Список без этой оговорки показывал бы «неоплаченных
* нет» при −5 156 ₽ на главном экране.
*/
export function FinesPage() {
const nav = useNavigate();
// Запрашиваем ВСЕ и фильтруем на месте: список короткий, зато вкладка
// переключается мгновенно и, главное, известно, есть ли непогашенные ещё до
// того, как выбрана вкладка.
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["fines"],
queryFn: () => getFines("all"),
});
const [tab, setTab] = useState<"open" | "all" | null>(null);
// Долгов нет — открываем «Все». Иначе водитель с оплаченными штрафами видел
// пустой экран и решал, что приложение их потеряло (Морозов, 18.08.2026).
const active = tab ?? (data && data.open_count > 0 ? "open" : "all");
const items = (data?.items ?? []).filter((f) => active === "all" || f.settled === "owed");
return (
<div className="pt-2">
<button className="text-muted text-xs mb-3" onClick={() => nav("/")}> Назад</button>
<h1 className="text-lg font-extrabold mb-1">Штрафы</h1>
<p className="text-muted text-xs mb-4">
{data
? data.debt !== 0 || data.open_count > 0
? `Долг по штрафам ${formatMoney(data.debt)} · не погашено: ${data.open_count}`
: "Долгов по штрафам нет"
: " "}
</p>
<div className="flex gap-1.5 mb-3">
{([["open", "Не погашены"], ["all", "Все"]] as const).map(([key, label]) => (
<button
key={key}
onClick={() => setTab(key)}
className={`px-3 py-2 rounded-xl2 text-xs border ${
active === key ? "border-ink bg-surface font-semibold" : "border-line text-muted"
}`}
>
{label}
</button>
))}
</div>
{isLoading ? (
<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>
) : items.length === 0 ? (
<p className="text-muted text-sm text-center py-10">
{active === "open" ? "Непогашенных штрафов нет" : "Штрафов не найдено"}
</p>
) : (
<div>
{items.map((f) => (
<button
key={f.id}
onClick={() => nav(`/fines/${f.id}`)}
className="w-full flex items-center gap-3 py-3 border-b border-line last:border-0 text-left"
>
<OperationIcon title="Штраф" />
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5 text-xs font-semibold">
{formatDate(f.date)}
{f.has_photo && <ImageIcon size={12} strokeWidth={1.5} className="text-muted" />}
</span>
<span className="block text-sm truncate">{f.article}</span>
{f.car_number && (
<span className="block text-muted text-xs">{f.car_number}</span>
)}
</span>
<span className="shrink-0 text-right">
<b className={isOwed(f) ? "text-neg" : "text-muted"}>{formatMoney(f.amount)}</b>
<span className="block text-muted text-[11px]">{fineStatusText(f)}</span>
</span>
<ChevronRight size={16} className="text-muted shrink-0" />
</button>
))}
</div>
)}
</div>
);
}