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 { useQuery } from "@tanstack/react-query";
|
||||||
import { getStatement, type StatementKind } from "@/api/driver";
|
import { getStatement, type StatementKind } from "@/api/driver";
|
||||||
import { formatDate, formatMoney } from "@/lib/format";
|
import { formatDate, formatMoney } from "@/lib/format";
|
||||||
|
import { OperationIcon } from "@/components/operationIcon";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
|
|
||||||
const FILTERS: { key: StatementKind; label: string }[] = [
|
const FILTERS: { key: StatementKind; label: string }[] = [
|
||||||
@@ -70,8 +71,9 @@ export function Statement({ from, to, kind, onFrom, onTo, onKind }: {
|
|||||||
<div>
|
<div>
|
||||||
{items.map((op, idx) => (
|
{items.map((op, idx) => (
|
||||||
<div key={`${op.date}-${idx}`}
|
<div key={`${op.date}-${idx}`}
|
||||||
className="flex justify-between items-start gap-3 py-3 border-b border-line last:border-0">
|
className="flex items-center gap-3 py-2.5 border-b border-line last:border-0">
|
||||||
<span className="min-w-0">
|
<OperationIcon title={op.title} />
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block text-sm">{op.title}</span>
|
<span className="block text-sm">{op.title}</span>
|
||||||
<span className="block text-muted text-xs">
|
<span className="block text-muted text-xs">
|
||||||
{formatDate(op.date)}
|
{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