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é]
|
||||
|
||||
## [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é
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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é
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue