feat(pwa): главный экран из двух половин — деньги сверху, выписка снизу

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

Разбивка по статьям переехала в шторку «Пополнить». Она нужна ровно в момент
выбора, куда платить; всё остальное время это шесть строк, отодвигающих выписку
за нижний край телефона. В шторке только те статьи, в которые платёж проводится.

Экран суммы, придя из шторки, показывает выбранную категорию строкой, а не рядом
чипов: второй выбор того же самого сбивает с толку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 16:35:01 +10:00
co-authored by Claude Opus 5
parent 8b0ce4cf45
commit 2d4b66d51f
7 changed files with 323 additions and 85 deletions
@@ -0,0 +1,90 @@
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>
);
}