Bump version to 0.6.2 — Section subtotals and category detail fix
All checks were successful
Release / build-and-release (push) Successful in 30m13s

Add per-section subtotals (expenses, income, transfers) to budget table
and budget vs actual report. Fix category detail panel visibility when
scrolling through long category lists.

Closes #11, closes #12

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-03-06 16:22:36 -05:00
parent 6ca62db4a9
commit 420506b074
12 changed files with 118 additions and 6 deletions

View file

@ -2,6 +2,15 @@
## [Non publié]
## [0.6.2]
### Ajouté
- Tableau de budget : sous-totaux par section (dépenses, revenus, transferts) affichés après chaque groupe (#11)
- Rapport Budget vs Réel : sous-totaux par section avec réel, prévu, écart ($) et écart (%) par type (#11)
### Corrigé
- Page catégories : le panneau de détail reste maintenant visible lors du défilement d'une longue liste de catégories (#12)
## [0.6.1]
### Ajouté

View file

@ -2,6 +2,15 @@
## [Unreleased]
## [0.6.2]
### Added
- Budget table: section subtotals for expenses, income, and transfers displayed after each group (#11)
- Budget vs Actual report: section subtotals with actual, planned, variation ($) and variation (%) per type (#11)
### Fixed
- Category page: detail panel now stays visible when scrolling through a long category list (#12)
## [0.6.1]
### Added

View file

@ -1,7 +1,7 @@
{
"name": "simpl_result_scaffold",
"private": true,
"version": "0.6.1",
"version": "0.6.2",
"license": "GPL-3.0-only",
"type": "module",
"scripts": {

View file

@ -2,6 +2,15 @@
## [Non publié]
## [0.6.2]
### Ajouté
- Tableau de budget : sous-totaux par section (dépenses, revenus, transferts) affichés après chaque groupe (#11)
- Rapport Budget vs Réel : sous-totaux par section avec réel, prévu, écart ($) et écart (%) par type (#11)
### Corrigé
- Page catégories : le panneau de détail reste maintenant visible lors du défilement d'une longue liste de catégories (#12)
## [0.6.1]
### Ajouté

View file

@ -2,6 +2,15 @@
## [Unreleased]
## [0.6.2]
### Added
- Budget table: section subtotals for expenses, income, and transfers displayed after each group (#11)
- Budget vs Actual report: section subtotals with actual, planned, variation ($) and variation (%) per type (#11)
### Fixed
- Category page: detail panel now stays visible when scrolling through a long category list (#12)
## [0.6.1]
### Added

View file

@ -1,6 +1,6 @@
[package]
name = "simpl-result"
version = "0.6.1"
version = "0.6.2"
description = "Personal finance management app"
license = "GPL-3.0-only"
authors = ["you"]

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Simpl Resultat",
"version": "0.6.1",
"version": "0.6.2",
"identifier": "com.simpl.resultat",
"build": {
"beforeDevCommand": "npm run dev",

View file

@ -184,6 +184,11 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
income: "budget.income",
transfer: "budget.transfers",
};
const typeTotalKeys: Record<string, string> = {
expense: "budget.totalExpenses",
income: "budget.totalIncome",
transfer: "budget.totalTransfers",
};
// Column totals with sign convention (only count leaf rows to avoid double-counting parents)
const monthTotals: number[] = Array(12).fill(0);
@ -360,6 +365,16 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
{typeOrder.map((type) => {
const group = grouped[type];
if (!group || group.length === 0) return null;
const sign = signFor(type);
const leaves = group.filter((r) => !r.is_parent);
const sectionMonthTotals: number[] = Array(12).fill(0);
let sectionAnnualTotal = 0;
for (const row of leaves) {
for (let m = 0; m < 12; m++) {
sectionMonthTotals[m] += row.months[m] * sign;
}
sectionAnnualTotal += row.annual * sign;
}
return (
<Fragment key={type}>
<tr>
@ -371,6 +386,17 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
</td>
</tr>
{reorderRows(group, subtotalsOnTop).map((row) => renderRow(row))}
<tr className="bg-[var(--muted)]/40 border-b border-[var(--border)]">
<td className="py-2 px-3 sticky left-0 bg-[var(--muted)]/40 z-10 text-xs font-semibold">
{t(typeTotalKeys[type])}
</td>
<td className="py-2 px-2 text-right text-xs font-semibold">{formatSigned(sectionAnnualTotal)}</td>
{sectionMonthTotals.map((total, mIdx) => (
<td key={mIdx} className="py-2 px-2 text-right text-xs font-semibold">
{formatSigned(total)}
</td>
))}
</tr>
</Fragment>
);
})}

View file

@ -105,6 +105,11 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
income: t("budget.income"),
transfer: t("budget.transfers"),
};
const typeTotalKeys: Record<SectionType, string> = {
expense: "budget.totalExpenses",
income: "budget.totalIncome",
transfer: "budget.totalTransfers",
};
let currentType: SectionType | null = null;
for (const row of data) {
@ -184,7 +189,22 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
</tr>
</thead>
<tbody>
{sections.map((section) => (
{sections.map((section) => {
const sectionLeaves = section.rows.filter((r) => !r.is_parent);
const sectionTotals = sectionLeaves.reduce(
(acc, r) => ({
monthActual: acc.monthActual + r.monthActual,
monthBudget: acc.monthBudget + r.monthBudget,
monthVariation: acc.monthVariation + r.monthVariation,
ytdActual: acc.ytdActual + r.ytdActual,
ytdBudget: acc.ytdBudget + r.ytdBudget,
ytdVariation: acc.ytdVariation + r.ytdVariation,
}),
{ monthActual: 0, monthBudget: 0, monthVariation: 0, ytdActual: 0, ytdBudget: 0, ytdVariation: 0 }
);
const sectionMonthPct = sectionTotals.monthBudget !== 0 ? sectionTotals.monthVariation / Math.abs(sectionTotals.monthBudget) : null;
const sectionYtdPct = sectionTotals.ytdBudget !== 0 ? sectionTotals.ytdVariation / Math.abs(sectionTotals.ytdBudget) : null;
return (
<Fragment key={section.type}>
<tr className="bg-[var(--muted)]/50">
<td colSpan={9} className="px-3 py-1.5 font-semibold text-[var(--muted-foreground)] uppercase text-xs tracking-wider">
@ -236,8 +256,32 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
</tr>
);
})}
<tr className="border-b border-[var(--border)] bg-[var(--muted)]/40 font-semibold">
<td className="px-3 py-2 text-xs">{t(typeTotalKeys[section.type])}</td>
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
{cadFormatter(sectionTotals.monthActual)}
</td>
<td className="text-right px-3 py-2">{cadFormatter(sectionTotals.monthBudget)}</td>
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.monthVariation)}`}>
{cadFormatter(sectionTotals.monthVariation)}
</td>
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.monthVariation)}`}>
{pctFormatter(sectionMonthPct)}
</td>
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
{cadFormatter(sectionTotals.ytdActual)}
</td>
<td className="text-right px-3 py-2">{cadFormatter(sectionTotals.ytdBudget)}</td>
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.ytdVariation)}`}>
{cadFormatter(sectionTotals.ytdVariation)}
</td>
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.ytdVariation)}`}>
{pctFormatter(sectionYtdPct)}
</td>
</tr>
</Fragment>
))}
);
})}
{/* Grand totals */}
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
<td className="px-3 py-2">{t("common.total")}</td>

View file

@ -325,6 +325,9 @@
"expenses": "Expenses",
"income": "Income",
"transfers": "Transfers",
"totalExpenses": "Total Expenses",
"totalIncome": "Total Income",
"totalTransfers": "Total Transfers",
"totalPlanned": "Total Planned",
"totalActual": "Total Actual",
"totalDifference": "Difference",

View file

@ -325,6 +325,9 @@
"expenses": "Dépenses",
"income": "Revenus",
"transfers": "Transferts",
"totalExpenses": "Total des dépenses",
"totalIncome": "Total des revenus",
"totalTransfers": "Total des transferts",
"totalPlanned": "Total prévu",
"totalActual": "Total réel",
"totalDifference": "Écart",

View file

@ -90,7 +90,7 @@ export default function CategoriesPage() {
onRemove={removeKeyword}
/>
) : (
<div className="flex gap-6" style={{ minHeight: "calc(100vh - 180px)" }}>
<div className="flex gap-6" style={{ height: "calc(100vh - 180px)" }}>
<div className="w-1/3 bg-[var(--card)] rounded-xl border border-[var(--border)] p-3 overflow-y-auto">
<CategoryTree
tree={state.tree}