Bump version to 0.6.2 — Section subtotals and category detail fix
All checks were successful
Release / build-and-release (push) Successful in 30m13s
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:
parent
6ca62db4a9
commit
420506b074
12 changed files with 118 additions and 6 deletions
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [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]
|
## [0.6.1]
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [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]
|
## [0.6.1]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "simpl_result_scaffold",
|
"name": "simpl_result_scaffold",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [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]
|
## [0.6.1]
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [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]
|
## [0.6.1]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "simpl-result"
|
name = "simpl-result"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
description = "Personal finance management app"
|
description = "Personal finance management app"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Simpl Resultat",
|
"productName": "Simpl Resultat",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"identifier": "com.simpl.resultat",
|
"identifier": "com.simpl.resultat",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,11 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
income: "budget.income",
|
income: "budget.income",
|
||||||
transfer: "budget.transfers",
|
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)
|
// Column totals with sign convention (only count leaf rows to avoid double-counting parents)
|
||||||
const monthTotals: number[] = Array(12).fill(0);
|
const monthTotals: number[] = Array(12).fill(0);
|
||||||
|
|
@ -360,6 +365,16 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
{typeOrder.map((type) => {
|
{typeOrder.map((type) => {
|
||||||
const group = grouped[type];
|
const group = grouped[type];
|
||||||
if (!group || group.length === 0) return null;
|
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 (
|
return (
|
||||||
<Fragment key={type}>
|
<Fragment key={type}>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -371,6 +386,17 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{reorderRows(group, subtotalsOnTop).map((row) => renderRow(row))}
|
{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>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,11 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
income: t("budget.income"),
|
income: t("budget.income"),
|
||||||
transfer: t("budget.transfers"),
|
transfer: t("budget.transfers"),
|
||||||
};
|
};
|
||||||
|
const typeTotalKeys: Record<SectionType, string> = {
|
||||||
|
expense: "budget.totalExpenses",
|
||||||
|
income: "budget.totalIncome",
|
||||||
|
transfer: "budget.totalTransfers",
|
||||||
|
};
|
||||||
|
|
||||||
let currentType: SectionType | null = null;
|
let currentType: SectionType | null = null;
|
||||||
for (const row of data) {
|
for (const row of data) {
|
||||||
|
|
@ -184,7 +189,22 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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}>
|
<Fragment key={section.type}>
|
||||||
<tr className="bg-[var(--muted)]/50">
|
<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">
|
<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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<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>
|
</Fragment>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{/* Grand totals */}
|
{/* Grand totals */}
|
||||||
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
||||||
<td className="px-3 py-2">{t("common.total")}</td>
|
<td className="px-3 py-2">{t("common.total")}</td>
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,9 @@
|
||||||
"expenses": "Expenses",
|
"expenses": "Expenses",
|
||||||
"income": "Income",
|
"income": "Income",
|
||||||
"transfers": "Transfers",
|
"transfers": "Transfers",
|
||||||
|
"totalExpenses": "Total Expenses",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalTransfers": "Total Transfers",
|
||||||
"totalPlanned": "Total Planned",
|
"totalPlanned": "Total Planned",
|
||||||
"totalActual": "Total Actual",
|
"totalActual": "Total Actual",
|
||||||
"totalDifference": "Difference",
|
"totalDifference": "Difference",
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,9 @@
|
||||||
"expenses": "Dépenses",
|
"expenses": "Dépenses",
|
||||||
"income": "Revenus",
|
"income": "Revenus",
|
||||||
"transfers": "Transferts",
|
"transfers": "Transferts",
|
||||||
|
"totalExpenses": "Total des dépenses",
|
||||||
|
"totalIncome": "Total des revenus",
|
||||||
|
"totalTransfers": "Total des transferts",
|
||||||
"totalPlanned": "Total prévu",
|
"totalPlanned": "Total prévu",
|
||||||
"totalActual": "Total réel",
|
"totalActual": "Total réel",
|
||||||
"totalDifference": "Écart",
|
"totalDifference": "Écart",
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ export default function CategoriesPage() {
|
||||||
onRemove={removeKeyword}
|
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">
|
<div className="w-1/3 bg-[var(--card)] rounded-xl border border-[var(--border)] p-3 overflow-y-auto">
|
||||||
<CategoryTree
|
<CategoryTree
|
||||||
tree={state.tree}
|
tree={state.tree}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue