Files
mechanic-pwa/driver-pwa/frontend/src/pages/FinesPage.tsx
T
tremble7681andClaude Opus 5 3bdd5ce818 feat(pwa): переход в штраф из выписки, заметные даты, минус раздел пополнений
Дата в выписке и в списке штрафов встала первой строкой чернильным цветом: в
этих списках ищут «что было 12-го», а бледная подпись под заголовком для этого
не годилась.

Строка штрафа в выписке стала кнопкой в карточку — там фотография и подробности.
Остальные строки не притворяются ссылками.

Раздел «Мои пополнения» удалён вместе с кнопкой: он показывал только наши
платежи и у большинства водителей был пуст (1С проводит пополнения сама), а те
же операции видны в выписке фильтром «Пополнения».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:58:28 +10:00

100 lines
4.4 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 [tab, setTab] = useState<"open" | "all">("open");
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["fines", tab],
queryFn: () => getFines(tab),
});
const items = data?.items ?? [];
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 ${
tab === key ? "border-line 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">
{tab === "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>
);
}