fix: sticky category column and month dropdown selector (#29) #30
2 changed files with 28 additions and 26 deletions
|
|
@ -158,8 +158,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
const sectionYtdPct = sectionTotals.ytdBudget !== 0 ? sectionTotals.ytdVariation / Math.abs(sectionTotals.ytdBudget) : null;
|
const sectionYtdPct = sectionTotals.ytdBudget !== 0 ? sectionTotals.ytdVariation / Math.abs(sectionTotals.ytdBudget) : null;
|
||||||
return (
|
return (
|
||||||
<Fragment key={section.type}>
|
<Fragment key={section.type}>
|
||||||
<tr className="bg-[var(--muted)]/50">
|
<tr className="bg-[var(--muted)]">
|
||||||
<td colSpan={9} className="px-3 py-1.5 font-semibold text-[var(--muted-foreground)] uppercase text-xs tracking-wider sticky left-0">
|
<td colSpan={9} className="px-3 py-1.5 font-semibold text-[var(--muted-foreground)] uppercase text-xs tracking-wider sticky left-0 bg-[var(--muted)]">
|
||||||
{section.label}
|
{section.label}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -173,13 +173,13 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
<tr
|
<tr
|
||||||
key={`${row.category_id}-${row.is_parent}-${depth}`}
|
key={`${row.category_id}-${row.is_parent}-${depth}`}
|
||||||
className={`border-b border-[var(--border)]/50 ${
|
className={`border-b border-[var(--border)]/50 ${
|
||||||
isTopParent ? "bg-[var(--muted)]/30 font-semibold" :
|
isTopParent ? "bg-[color-mix(in_srgb,var(--muted)_30%,var(--card))] font-semibold" :
|
||||||
isIntermediateParent ? "bg-[var(--muted)]/15 font-medium" : ""
|
isIntermediateParent ? "bg-[color-mix(in_srgb,var(--muted)_15%,var(--card))] font-medium" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td className={`py-1.5 sticky left-0 z-10 ${
|
<td className={`py-1.5 sticky left-0 z-10 ${
|
||||||
isTopParent ? "px-3 bg-[var(--muted)]/30" :
|
isTopParent ? "px-3 bg-[color-mix(in_srgb,var(--muted)_30%,var(--card))]" :
|
||||||
isIntermediateParent ? `${paddingClass} bg-[var(--muted)]/15` :
|
isIntermediateParent ? `${paddingClass} bg-[color-mix(in_srgb,var(--muted)_15%,var(--card))]` :
|
||||||
`${paddingClass} bg-[var(--card)]`
|
`${paddingClass} bg-[var(--card)]`
|
||||||
}`}>
|
}`}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
|
@ -213,8 +213,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<tr className="border-b border-[var(--border)] bg-[var(--muted)]/40 font-semibold text-sm">
|
<tr className="border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--muted)_40%,var(--card))] font-semibold text-sm">
|
||||||
<td className="px-3 py-2.5 sticky left-0 bg-[var(--muted)]/40 z-10">{t(typeTotalKeys[section.type])}</td>
|
<td className="px-3 py-2.5 sticky left-0 bg-[color-mix(in_srgb,var(--muted)_40%,var(--card))] z-10">{t(typeTotalKeys[section.type])}</td>
|
||||||
<td className="text-right px-3 py-2.5 border-l border-[var(--border)]/50">
|
<td className="text-right px-3 py-2.5 border-l border-[var(--border)]/50">
|
||||||
{cadFormatter(sectionTotals.monthActual)}
|
{cadFormatter(sectionTotals.monthActual)}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -240,8 +240,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* Grand totals */}
|
{/* Grand totals */}
|
||||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[color-mix(in_srgb,var(--muted)_20%,var(--card))]">
|
||||||
<td className="px-3 py-3 sticky left-0 bg-[var(--muted)]/20 z-10">{t("common.total")}</td>
|
<td className="px-3 py-3 sticky left-0 bg-[color-mix(in_srgb,var(--muted)_20%,var(--card))] z-10">{t("common.total")}</td>
|
||||||
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
||||||
{cadFormatter(totals.monthActual)}
|
{cadFormatter(totals.monthActual)}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,19 @@ export default function ReportsPage() {
|
||||||
return [];
|
return [];
|
||||||
}, [state.tab, state.categorySpending, state.categoryOverTime]);
|
}, [state.tab, state.categorySpending, state.categoryOverTime]);
|
||||||
|
|
||||||
|
const monthOptions = useMemo(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const currentMonth = now.getMonth();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
return Array.from({ length: 24 }, (_, i) => {
|
||||||
|
const d = new Date(currentYear, currentMonth - i, 1);
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = d.getMonth() + 1;
|
||||||
|
const label = new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(d);
|
||||||
|
return { key: `${y}-${m}`, value: `${y}-${m}`, label: label.charAt(0).toUpperCase() + label.slice(1) };
|
||||||
|
});
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
const hasCategories = ["byCategory", "overTime"].includes(state.tab) && filterCategories.length > 0;
|
const hasCategories = ["byCategory", "overTime"].includes(state.tab) && filterCategories.length > 0;
|
||||||
const showFilterPanel = hasCategories || (state.tab === "trends" && sources.length > 1);
|
const showFilterPanel = hasCategories || (state.tab === "trends" && sources.length > 1);
|
||||||
|
|
||||||
|
|
@ -110,22 +123,11 @@ export default function ReportsPage() {
|
||||||
}}
|
}}
|
||||||
className="text-2xl font-bold bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-1 cursor-pointer hover:bg-[var(--muted)] transition-colors"
|
className="text-2xl font-bold bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-1 cursor-pointer hover:bg-[var(--muted)] transition-colors"
|
||||||
>
|
>
|
||||||
{(() => {
|
{monthOptions.map((opt) => (
|
||||||
const now = new Date();
|
<option key={opt.key} value={opt.value}>
|
||||||
const currentMonth = now.getMonth(); // 0-based
|
{opt.label}
|
||||||
const currentYear = now.getFullYear();
|
|
||||||
return Array.from({ length: 24 }, (_, i) => {
|
|
||||||
const d = new Date(currentYear, currentMonth - i, 1);
|
|
||||||
const y = d.getFullYear();
|
|
||||||
const m = d.getMonth() + 1;
|
|
||||||
const label = new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(d);
|
|
||||||
return (
|
|
||||||
<option key={`${y}-${m}`} value={`${y}-${m}`}>
|
|
||||||
{label.charAt(0).toUpperCase() + label.slice(1)}
|
|
||||||
</option>
|
</option>
|
||||||
);
|
))}
|
||||||
});
|
|
||||||
})()}
|
|
||||||
</select>
|
</select>
|
||||||
</h1>
|
</h1>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue