fix: sticky category column and month dropdown selector (#29) #30

Merged
maximus merged 4 commits from fix/simpl-resultat-29-budget-visual-adjustments into main 2026-03-10 01:29:28 +00:00
2 changed files with 28 additions and 26 deletions
Showing only changes of commit 0d324b89c4 - Show all commits

View file

@ -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>

View file

@ -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(); </option>
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>
);
});
})()}
</select> </select>
</h1> </h1>
) : ( ) : (