diff --git a/driver-pwa/frontend/src/components/Statement.tsx b/driver-pwa/frontend/src/components/Statement.tsx index 90f6999..3a108b0 100644 --- a/driver-pwa/frontend/src/components/Statement.tsx +++ b/driver-pwa/frontend/src/components/Statement.tsx @@ -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 }: {
{items.map((op, idx) => (
- + className="flex items-center gap-3 py-2.5 border-b border-line last:border-0"> + + {op.title} {formatDate(op.date)} diff --git a/driver-pwa/frontend/src/components/operationIcon.test.ts b/driver-pwa/frontend/src/components/operationIcon.test.ts new file mode 100644 index 0000000..f4978dd --- /dev/null +++ b/driver-pwa/frontend/src/components/operationIcon.test.ts @@ -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(); + }); +}); diff --git a/driver-pwa/frontend/src/components/operationIcon.tsx b/driver-pwa/frontend/src/components/operationIcon.tsx new file mode 100644 index 0000000..c9be2c7 --- /dev/null +++ b/driver-pwa/frontend/src/components/operationIcon.tsx @@ -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 ( + + + + ); +}