- 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>
54 lines
1.9 KiB
TypeScript
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>
|
|
);
|
|
}
|