Section subtotals: text-sm font-semibold with more padding. Grand totals: text-sm font-bold with border-t-2 and extra padding. Applied consistently across BudgetTable, BudgetVsActualTable, MonthlyTrendsTable, CategoryOverTimeTable, CategoryTable, and DynamicReportTable. Closes #14 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
111 lines
4.8 KiB
TypeScript
111 lines
4.8 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import type { CategoryOverTimeData } from "../../shared/types";
|
|
|
|
const cadFormatter = (value: number) =>
|
|
new Intl.NumberFormat("en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0 }).format(value);
|
|
|
|
function formatMonth(month: string): string {
|
|
const [year, m] = month.split("-");
|
|
const date = new Date(Number(year), Number(m) - 1);
|
|
return date.toLocaleDateString("default", { month: "short", year: "2-digit" });
|
|
}
|
|
|
|
interface CategoryOverTimeTableProps {
|
|
data: CategoryOverTimeData;
|
|
hiddenCategories?: Set<string>;
|
|
}
|
|
|
|
export default function CategoryOverTimeTable({ data, hiddenCategories }: CategoryOverTimeTableProps) {
|
|
const { t } = useTranslation();
|
|
|
|
if (data.data.length === 0) {
|
|
return (
|
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-8 text-center text-[var(--muted-foreground)]">
|
|
{t("dashboard.noData")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const visibleCategories = hiddenCategories?.size
|
|
? data.categories.filter((name) => !hiddenCategories.has(name))
|
|
: data.categories;
|
|
|
|
const months = data.data.map((d) => d.month);
|
|
|
|
return (
|
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl overflow-hidden">
|
|
<div className="overflow-x-auto overflow-y-auto" style={{ maxHeight: "calc(100vh - 220px)" }}>
|
|
<table className="w-full text-sm whitespace-nowrap">
|
|
<thead className="sticky top-0 z-20">
|
|
<tr className="border-b border-[var(--border)] bg-[var(--card)]">
|
|
<th className="text-left px-3 py-2 font-medium text-[var(--muted-foreground)] bg-[var(--card)] sticky left-0 z-30 min-w-[140px]">
|
|
{t("budget.category")}
|
|
</th>
|
|
{months.map((month) => (
|
|
<th key={month} className="text-right px-3 py-2 font-medium text-[var(--muted-foreground)] bg-[var(--card)] min-w-[90px]">
|
|
{formatMonth(month)}
|
|
</th>
|
|
))}
|
|
<th className="text-right px-3 py-2 font-medium text-[var(--muted-foreground)] bg-[var(--card)] border-l border-[var(--border)] min-w-[90px]">
|
|
{t("common.total")}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{visibleCategories.map((category) => {
|
|
const rowTotal = data.data.reduce((sum, d) => sum + ((d as Record<string, unknown>)[category] as number || 0), 0);
|
|
return (
|
|
<tr key={category} className="border-b border-[var(--border)]/50">
|
|
<td className="px-3 py-1.5 sticky left-0 bg-[var(--card)] z-10">
|
|
<span className="flex items-center gap-2">
|
|
<span
|
|
className="w-2.5 h-2.5 rounded-full shrink-0"
|
|
style={{ backgroundColor: data.colors[category] }}
|
|
/>
|
|
{category}
|
|
</span>
|
|
</td>
|
|
{months.map((month) => {
|
|
const monthData = data.data.find((d) => d.month === month);
|
|
const value = (monthData as Record<string, unknown>)?.[category] as number || 0;
|
|
return (
|
|
<td key={month} className="text-right px-3 py-1.5">
|
|
{value ? cadFormatter(value) : "—"}
|
|
</td>
|
|
);
|
|
})}
|
|
<td className="text-right px-3 py-1.5 font-semibold border-l border-[var(--border)]/50">
|
|
{cadFormatter(rowTotal)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
|
<td className="px-3 py-3 sticky left-0 bg-[var(--muted)]/20 z-10">{t("common.total")}</td>
|
|
{months.map((month) => {
|
|
const monthData = data.data.find((d) => d.month === month);
|
|
const monthTotal = visibleCategories.reduce(
|
|
(sum, cat) => sum + ((monthData as Record<string, unknown>)?.[cat] as number || 0),
|
|
0,
|
|
);
|
|
return (
|
|
<td key={month} className="text-right px-3 py-3">
|
|
{cadFormatter(monthTotal)}
|
|
</td>
|
|
);
|
|
})}
|
|
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
|
{cadFormatter(
|
|
visibleCategories.reduce(
|
|
(sum, cat) => sum + data.data.reduce((s, d) => s + ((d as Record<string, unknown>)[cat] as number || 0), 0),
|
|
0,
|
|
),
|
|
)}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|