Водитель, оплативший штраф сам, видел пустой экран: список открывался на «Не погашены», а там у него ноль (Морозов, штраф от 16.08 — оплачен им, парк его не выставлял). Выглядело как «приложение потеряло мои штрафы». Теперь вкладка выбирается по данным: есть непогашенные — открываем их, нет — открываем «Все». Заодно один запрос вместо двух: список короткий, фильтруем на месте, и переключение вкладок стало мгновенным. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
5.1 KiB
TypeScript
106 lines
5.1 KiB
TypeScript
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>
|
||
);
|
||
}
|