Simpl-Resultat/src/components/reports/HubTopTransactionsTile.tsx
le king fu ac9c8afc4a
All checks were successful
PR Check / rust (pull_request) Successful in 24m54s
PR Check / frontend (pull_request) Successful in 2m32s
PR Check / rust (push) Successful in 24m14s
PR Check / frontend (push) Successful in 2m26s
feat: reports hub with highlights panel and detailed highlights page (#71)
- Transform /reports into a hub: highlights panel + 4 nav cards
- New service: reportService.getHighlights (parameterised SQL, deterministic
  via referenceDate argument for tests, computes current-month balance, YTD,
  12-month sparkline series, top expense movers vs previous month, top recent
  transactions within configurable 30/60/90 day window)
- Extended types: HighlightsData, HighlightMover, MonthBalance
- Wired useHighlights hook with reducer + window-days state
- Hub tiles (flat naming under src/components/reports):
  HubNetBalanceTile, HubTopMoversTile, HubTopTransactionsTile,
  HubHighlightsPanel, HubReportNavCard
- Detailed ReportsHighlightsPage: balance tiles, sortable top movers table,
  diverging bar chart (Recharts + patterns SVG), top transactions list with
  30/60/90 window toggle; ViewModeToggle persistence keyed as
  reports-viewmode-highlights
- New i18n keys: reports.hub.*, reports.highlights.*
- 5 new vitest cases: empty profile, parameterised queries, window sizing,
  delta computation, zero-previous divisor handling

Fixes #71

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:47:55 -04:00

54 lines
1.9 KiB
TypeScript

import { useTranslation } from "react-i18next";
import type { RecentTransaction } from "../../shared/types";
export interface HubTopTransactionsTileProps {
transactions: RecentTransaction[];
limit?: number;
}
function formatAmount(amount: number, language: string): string {
return new Intl.NumberFormat(language === "fr" ? "fr-CA" : "en-CA", {
style: "currency",
currency: "CAD",
}).format(amount);
}
export default function HubTopTransactionsTile({
transactions,
limit = 5,
}: HubTopTransactionsTileProps) {
const { t, i18n } = useTranslation();
const visible = transactions.slice(0, limit);
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-4 flex flex-col gap-2">
<span className="text-xs font-medium text-[var(--muted-foreground)] uppercase tracking-wide">
{t("reports.highlights.topTransactions")}
</span>
{visible.length === 0 ? (
<p className="text-sm text-[var(--muted-foreground)] italic">{t("reports.empty.noData")}</p>
) : (
<ul className="flex flex-col gap-1.5">
{visible.map((tx) => (
<li key={tx.id} className="flex items-center gap-2 text-sm">
<span
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: tx.category_color ?? "#9ca3af" }}
/>
<span className="text-[var(--muted-foreground)] tabular-nums flex-shrink-0">
{tx.date}
</span>
<span className="truncate flex-1 min-w-0">{tx.description}</span>
<span
className="tabular-nums font-medium flex-shrink-0"
style={{ color: tx.amount >= 0 ? "var(--positive, #10b981)" : "var(--foreground)" }}
>
{formatAmount(tx.amount, i18n.language)}
</span>
</li>
))}
</ul>
)}
</div>
);
}