feat(pwa): значки операций в выписке
За неделю у активного водителя два десятка строк, и половина одинаковая по виду («аренда автомобиля» каждый день) — список сливается в стену текста. Значок даёт зацепку для глаза: нужное находится, не читая каждую строку. Подбор по тегу 1С, а не по знаку суммы: аренда, пени, штраф, ДТП, депозит, пополнение, компенсация, коррекция — у каждого свой. Общий компонент, как в складе CRM: разложенные по местам иконки разъезжаются размерами при первой правке. Монохромные намеренно — цвет в этом списке уже занят и означает направление денег, цветной значок с ним бы спорил. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getStatement, type StatementKind } from "@/api/driver";
|
||||
import { formatDate, formatMoney } from "@/lib/format";
|
||||
import { OperationIcon } from "@/components/operationIcon";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
|
||||
const FILTERS: { key: StatementKind; label: string }[] = [
|
||||
@@ -70,8 +71,9 @@ export function Statement({ from, to, kind, onFrom, onTo, onKind }: {
|
||||
<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">
|
||||
className="flex items-center gap-3 py-2.5 border-b border-line last:border-0">
|
||||
<OperationIcon title={op.title} />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm">{op.title}</span>
|
||||
<span className="block text-muted text-xs">
|
||||
{formatDate(op.date)}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { operationIcon } from "./operationIcon";
|
||||
|
||||
/** Названия — настоящие теги 1С из выписки (finance_ledger_entries.tag). */
|
||||
describe("значок операции", () => {
|
||||
it("разные виды операций различаются", () => {
|
||||
const names = [
|
||||
"Аренда автомобиля",
|
||||
"Пени",
|
||||
"Штраф ГИБДД",
|
||||
"Пополнение баланса через мобильное приложение",
|
||||
"ДТП",
|
||||
"Перевод с депозита на баланс",
|
||||
].map((t) => operationIcon(t));
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
});
|
||||
|
||||
it("штраф ГИБДД и штраф компании — один значок: для водителя это одно и то же", () => {
|
||||
expect(operationIcon("Штраф ГИБДД")).toBe(operationIcon("Штраф компании"));
|
||||
});
|
||||
|
||||
it("незнакомый тег получает нейтральный значок, а не пустоту", () => {
|
||||
expect(operationIcon("Списание за что-то новое")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Banknote, Car, Flag, Gavel, Gift, HandCoins, Percent, PiggyBank, Receipt,
|
||||
RefreshCcw, ShieldAlert, TriangleAlert, Wrench, type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
/**
|
||||
* Значок операции в выписке — по тегу 1С, а не по знаку суммы.
|
||||
*
|
||||
* Список сливается в стену текста: за неделю у активного водителя два десятка
|
||||
* строк, и половина из них — одинаковая по виду «аренда автомобиля». Значок
|
||||
* даёт зацепку для глаза, чтобы взгляд находил нужное, не читая каждую строку.
|
||||
*
|
||||
* Общий компонент, а не иконка на месте применения: тот же приём, что в складе
|
||||
* CRM (modules/warehouse/categoryIcons.tsx). Разложены по местам — и размеры с
|
||||
* толщиной линии разъезжаются при первой же правке.
|
||||
*
|
||||
* Монохромные намеренно: цвет в этом списке уже занят и означает направление
|
||||
* денег (красное — списали, зелёное — пришло). Цветной значок спорил бы с ним.
|
||||
*/
|
||||
const BY_TAG: [RegExp, LucideIcon][] = [
|
||||
[/аренда/i, Car],
|
||||
[/пени/i, Percent],
|
||||
[/штраф/i, Gavel],
|
||||
[/дтп|повреждени/i, TriangleAlert],
|
||||
[/депозит/i, PiggyBank],
|
||||
[/пополнение|оплата/i, Banknote],
|
||||
[/ремонт|запчаст/i, Wrench],
|
||||
[/компенсац/i, HandCoins],
|
||||
[/акци|бонус|подар/i, Gift],
|
||||
[/коррекц|сторно|перерасч/i, RefreshCcw],
|
||||
[/страхов/i, ShieldAlert],
|
||||
[/остаток|opening/i, Flag],
|
||||
];
|
||||
|
||||
export function operationIcon(title: string): LucideIcon {
|
||||
for (const [re, icon] of BY_TAG) {
|
||||
if (re.test(title)) return icon;
|
||||
}
|
||||
return Receipt;
|
||||
}
|
||||
|
||||
/** Значок в рамке — как в списке склада: одинаковый размер у всех строк. */
|
||||
export function OperationIcon({ title }: { title: string }) {
|
||||
const Icon = operationIcon(title);
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className="shrink-0 w-8 h-8 rounded-xl2 border border-line bg-cream flex items-center justify-center text-muted"
|
||||
>
|
||||
<Icon size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user