По образцу приложения вендора: баланс в парке и «зачисляется», под ними крупная кнопка «Пополнить», ниже — период с календарями, фильтры и список операций (аренда, пени, штрафы, пополнения). Разбивка по статьям переехала в шторку «Пополнить». Она нужна ровно в момент выбора, куда платить; всё остальное время это шесть строк, отодвигающих выписку за нижний край телефона. В шторке только те статьи, в которые платёж проводится. Экран суммы, придя из шторки, показывает выбранную категорию строкой, а не рядом чипов: второй выбор того же самого сбивает с толку. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { getStatement, type StatementKind } from "@/api/driver";
|
|
import { formatDate, formatMoney } from "@/lib/format";
|
|
import { Spinner } from "@/components/Spinner";
|
|
|
|
const FILTERS: { key: StatementKind; label: string }[] = [
|
|
{ key: "all", label: "Все операции" },
|
|
{ key: "charges", label: "Начисления" },
|
|
{ key: "payments", label: "Пополнения" },
|
|
{ key: "fines", label: "Штрафы" },
|
|
];
|
|
|
|
/**
|
|
* Выписка по счёту за период — нижняя половина главного экрана.
|
|
*
|
|
* Это ответ на вопрос «за что списано», который иначе задают звонком в парк.
|
|
* Строки те же, по которым 1С считает долг: аренда, пени, штрафы, пополнения.
|
|
*/
|
|
export function Statement({ from, to, kind, onFrom, onTo, onKind }: {
|
|
from: string;
|
|
to: string;
|
|
kind: StatementKind;
|
|
onFrom: (v: string) => void;
|
|
onTo: (v: string) => void;
|
|
onKind: (k: StatementKind) => void;
|
|
}) {
|
|
const { data, isLoading, isError, refetch } = useQuery({
|
|
queryKey: ["statement", from, to, kind],
|
|
queryFn: () => getStatement(from, to, kind),
|
|
});
|
|
const items = data?.items ?? [];
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
{/* Нативный date-input: календарь телефона знакомее любого нашего. */}
|
|
<input type="date" value={from} max={to} onChange={(e) => onFrom(e.target.value)}
|
|
aria-label="Начало периода"
|
|
className="card flex-1 px-3 py-2 text-xs bg-white" />
|
|
<span className="text-muted text-xs">—</span>
|
|
<input type="date" value={to} min={from} onChange={(e) => onTo(e.target.value)}
|
|
aria-label="Конец периода"
|
|
className="card flex-1 px-3 py-2 text-xs bg-white" />
|
|
</div>
|
|
|
|
<div className="flex gap-1.5 mb-3 overflow-x-auto">
|
|
{FILTERS.map((f) => (
|
|
<button
|
|
key={f.key}
|
|
onClick={() => onKind(f.key)}
|
|
className={`shrink-0 px-3 py-2 rounded-xl2 text-xs border ${
|
|
f.key === kind ? "border-line bg-surface font-semibold" : "border-line text-muted"
|
|
}`}
|
|
>
|
|
{f.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex justify-center py-8"><Spinner /></div>
|
|
) : isError ? (
|
|
<div className="text-center py-6">
|
|
<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-8">Нет операций за выбранный период</p>
|
|
) : (
|
|
<div>
|
|
{items.map((op, idx) => (
|
|
<div key={`${op.date}-${idx}`}
|
|
className="flex justify-between items-start gap-3 py-3 border-b border-line last:border-0">
|
|
<span className="min-w-0">
|
|
<span className="block text-sm">{op.title}</span>
|
|
<span className="block text-muted text-xs">
|
|
{formatDate(op.date)}
|
|
{op.description ? ` · ${op.description}` : ""}
|
|
</span>
|
|
</span>
|
|
<b className={`shrink-0 ${op.amount < 0 ? "text-neg" : "text-pos"}`}>
|
|
{formatMoney(op.amount)}
|
|
</b>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|