Compare commits
No commits in common. "8742c2594587c49b19041989bcadc2ab688c85c4" and "e32a14557f02649abc8e8d7008165b923c6dce2b" have entirely different histories.
8742c25945
...
e32a14557f
7 changed files with 9 additions and 56 deletions
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [Non publié]
|
||||||
|
|
||||||
### Ajouté
|
|
||||||
- Tableau de budget : colonne du total de l'année précédente affichée comme première colonne de données pour servir de référence (#16)
|
|
||||||
|
|
||||||
## [0.6.3]
|
## [0.6.3]
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
|
||||||
- Budget table: previous year total column displayed as first data column for baseline reference (#16)
|
|
||||||
|
|
||||||
## [0.6.3]
|
## [0.6.3]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,6 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
// 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);
|
||||||
let annualTotal = 0;
|
let annualTotal = 0;
|
||||||
let prevYearTotal = 0;
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (row.is_parent) continue; // skip parent subtotals to avoid double-counting
|
if (row.is_parent) continue; // skip parent subtotals to avoid double-counting
|
||||||
const sign = signFor(row.category_type);
|
const sign = signFor(row.category_type);
|
||||||
|
|
@ -201,10 +200,9 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
monthTotals[m] += row.months[m] * sign;
|
monthTotals[m] += row.months[m] * sign;
|
||||||
}
|
}
|
||||||
annualTotal += row.annual * sign;
|
annualTotal += row.annual * sign;
|
||||||
prevYearTotal += row.previousYearTotal * sign;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCols = 15; // category + prev year + annual + 12 months
|
const totalCols = 14; // category + annual + 12 months
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -245,9 +243,6 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
<span className={`truncate text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"}`}>{row.category_name}</span>
|
<span className={`truncate text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"}`}>{row.category_name}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={`py-2 px-2 text-right text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"} text-[var(--muted-foreground)]`}>
|
|
||||||
{formatSigned(row.previousYearTotal * sign)}
|
|
||||||
</td>
|
|
||||||
<td className={`py-2 px-2 text-right text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"}`}>
|
<td className={`py-2 px-2 text-right text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"}`}>
|
||||||
{formatSigned(row.annual * sign)}
|
{formatSigned(row.annual * sign)}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -276,12 +271,6 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
<span className="truncate text-xs">{row.category_name}</span>
|
<span className="truncate text-xs">{row.category_name}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{/* Previous year total — read-only */}
|
|
||||||
<td className="py-2 px-2 text-right text-[var(--muted-foreground)]">
|
|
||||||
<span className="text-xs px-1 py-0.5">
|
|
||||||
{formatSigned(row.previousYearTotal * sign)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
{/* Annual total — editable */}
|
{/* Annual total — editable */}
|
||||||
<td className="py-2 px-2 text-right">
|
<td className="py-2 px-2 text-right">
|
||||||
{editingAnnual?.categoryId === row.category_id ? (
|
{editingAnnual?.categoryId === row.category_id ? (
|
||||||
|
|
@ -362,9 +351,6 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
<th className="text-left py-2.5 px-3 font-medium text-[var(--muted-foreground)] sticky left-0 bg-[var(--card)] z-30 min-w-[140px]">
|
<th className="text-left py-2.5 px-3 font-medium text-[var(--muted-foreground)] sticky left-0 bg-[var(--card)] z-30 min-w-[140px]">
|
||||||
{t("budget.category")}
|
{t("budget.category")}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right py-2.5 px-2 font-medium text-[var(--muted-foreground)] min-w-[90px]">
|
|
||||||
{t("budget.previousYear")}
|
|
||||||
</th>
|
|
||||||
<th className="text-right py-2.5 px-2 font-medium text-[var(--muted-foreground)] min-w-[90px]">
|
<th className="text-right py-2.5 px-2 font-medium text-[var(--muted-foreground)] min-w-[90px]">
|
||||||
{t("budget.annual")}
|
{t("budget.annual")}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -383,13 +369,11 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
const leaves = group.filter((r) => !r.is_parent);
|
const leaves = group.filter((r) => !r.is_parent);
|
||||||
const sectionMonthTotals: number[] = Array(12).fill(0);
|
const sectionMonthTotals: number[] = Array(12).fill(0);
|
||||||
let sectionAnnualTotal = 0;
|
let sectionAnnualTotal = 0;
|
||||||
let sectionPrevYearTotal = 0;
|
|
||||||
for (const row of leaves) {
|
for (const row of leaves) {
|
||||||
for (let m = 0; m < 12; m++) {
|
for (let m = 0; m < 12; m++) {
|
||||||
sectionMonthTotals[m] += row.months[m] * sign;
|
sectionMonthTotals[m] += row.months[m] * sign;
|
||||||
}
|
}
|
||||||
sectionAnnualTotal += row.annual * sign;
|
sectionAnnualTotal += row.annual * sign;
|
||||||
sectionPrevYearTotal += row.previousYearTotal * sign;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment key={type}>
|
<Fragment key={type}>
|
||||||
|
|
@ -406,7 +390,6 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
<td className="py-2.5 px-3 sticky left-0 bg-[var(--muted)]/40 z-10 text-sm font-semibold">
|
<td className="py-2.5 px-3 sticky left-0 bg-[var(--muted)]/40 z-10 text-sm font-semibold">
|
||||||
{t(typeTotalKeys[type])}
|
{t(typeTotalKeys[type])}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-2 text-right text-sm font-semibold text-[var(--muted-foreground)]">{formatSigned(sectionPrevYearTotal)}</td>
|
|
||||||
<td className="py-2.5 px-2 text-right text-sm font-semibold">{formatSigned(sectionAnnualTotal)}</td>
|
<td className="py-2.5 px-2 text-right text-sm font-semibold">{formatSigned(sectionAnnualTotal)}</td>
|
||||||
{sectionMonthTotals.map((total, mIdx) => (
|
{sectionMonthTotals.map((total, mIdx) => (
|
||||||
<td key={mIdx} className="py-2.5 px-2 text-right text-sm font-semibold">
|
<td key={mIdx} className="py-2.5 px-2 text-right text-sm font-semibold">
|
||||||
|
|
@ -420,7 +403,6 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
{/* Totals row */}
|
{/* Totals row */}
|
||||||
<tr className="bg-[var(--muted)] font-bold border-t-2 border-[var(--border)]">
|
<tr className="bg-[var(--muted)] font-bold border-t-2 border-[var(--border)]">
|
||||||
<td className="py-3 px-3 sticky left-0 bg-[var(--muted)] z-10 text-sm">{t("common.total")}</td>
|
<td className="py-3 px-3 sticky left-0 bg-[var(--muted)] z-10 text-sm">{t("common.total")}</td>
|
||||||
<td className="py-3 px-2 text-right text-sm text-[var(--muted-foreground)]">{formatSigned(prevYearTotal)}</td>
|
|
||||||
<td className="py-3 px-2 text-right text-sm">{formatSigned(annualTotal)}</td>
|
<td className="py-3 px-2 text-right text-sm">{formatSigned(annualTotal)}</td>
|
||||||
{monthTotals.map((total, mIdx) => (
|
{monthTotals.map((total, mIdx) => (
|
||||||
<td key={mIdx} className="py-3 px-2 text-right text-sm">
|
<td key={mIdx} className="py-3 px-2 text-right text-sm">
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,9 @@ export function useBudget() {
|
||||||
dispatch({ type: "SET_ERROR", payload: null });
|
dispatch({ type: "SET_ERROR", payload: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [allCategories, entries, prevYearEntries, templates] = await Promise.all([
|
const [allCategories, entries, templates] = await Promise.all([
|
||||||
getAllActiveCategories(),
|
getAllActiveCategories(),
|
||||||
getBudgetEntriesForYear(year),
|
getBudgetEntriesForYear(year),
|
||||||
getBudgetEntriesForYear(year - 1),
|
|
||||||
getAllTemplates(),
|
getAllTemplates(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -88,12 +87,6 @@ export function useBudget() {
|
||||||
entryMap.get(e.category_id)!.set(e.month, e.amount);
|
entryMap.get(e.category_id)!.set(e.month, e.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a map for previous year totals: categoryId -> annual total
|
|
||||||
const prevYearTotalMap = new Map<number, number>();
|
|
||||||
for (const e of prevYearEntries) {
|
|
||||||
prevYearTotalMap.set(e.category_id, (prevYearTotalMap.get(e.category_id) ?? 0) + e.amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: build months array from entryMap
|
// Helper: build months array from entryMap
|
||||||
const buildMonths = (catId: number) => {
|
const buildMonths = (catId: number) => {
|
||||||
const monthMap = entryMap.get(catId);
|
const monthMap = entryMap.get(catId);
|
||||||
|
|
@ -104,8 +97,7 @@ export function useBudget() {
|
||||||
months.push(val);
|
months.push(val);
|
||||||
annual += val;
|
annual += val;
|
||||||
}
|
}
|
||||||
const previousYearTotal = prevYearTotalMap.get(catId) ?? 0;
|
return { months, annual };
|
||||||
return { months, annual, previousYearTotal };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Index categories by id and group children by parent_id
|
// Index categories by id and group children by parent_id
|
||||||
|
|
@ -125,7 +117,7 @@ export function useBudget() {
|
||||||
const grandchildren = (childrenByParent.get(cat.id) || []).filter((c) => c.is_inputable);
|
const grandchildren = (childrenByParent.get(cat.id) || []).filter((c) => c.is_inputable);
|
||||||
if (grandchildren.length === 0 && cat.is_inputable) {
|
if (grandchildren.length === 0 && cat.is_inputable) {
|
||||||
// Leaf at depth 2
|
// Leaf at depth 2
|
||||||
const { months, annual, previousYearTotal } = buildMonths(cat.id);
|
const { months, annual } = buildMonths(cat.id);
|
||||||
return [{
|
return [{
|
||||||
category_id: cat.id,
|
category_id: cat.id,
|
||||||
category_name: cat.name,
|
category_name: cat.name,
|
||||||
|
|
@ -136,7 +128,6 @@ export function useBudget() {
|
||||||
depth: 2,
|
depth: 2,
|
||||||
months,
|
months,
|
||||||
annual,
|
annual,
|
||||||
previousYearTotal,
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
if (grandchildren.length === 0 && !cat.is_inputable) {
|
if (grandchildren.length === 0 && !cat.is_inputable) {
|
||||||
|
|
@ -147,7 +138,7 @@ export function useBudget() {
|
||||||
|
|
||||||
const gcRows: BudgetYearRow[] = [];
|
const gcRows: BudgetYearRow[] = [];
|
||||||
if (cat.is_inputable) {
|
if (cat.is_inputable) {
|
||||||
const { months, annual, previousYearTotal } = buildMonths(cat.id);
|
const { months, annual } = buildMonths(cat.id);
|
||||||
gcRows.push({
|
gcRows.push({
|
||||||
category_id: cat.id,
|
category_id: cat.id,
|
||||||
category_name: `${cat.name} (direct)`,
|
category_name: `${cat.name} (direct)`,
|
||||||
|
|
@ -158,11 +149,10 @@ export function useBudget() {
|
||||||
depth: 2,
|
depth: 2,
|
||||||
months,
|
months,
|
||||||
annual,
|
annual,
|
||||||
previousYearTotal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const gc of grandchildren) {
|
for (const gc of grandchildren) {
|
||||||
const { months, annual, previousYearTotal } = buildMonths(gc.id);
|
const { months, annual } = buildMonths(gc.id);
|
||||||
gcRows.push({
|
gcRows.push({
|
||||||
category_id: gc.id,
|
category_id: gc.id,
|
||||||
category_name: gc.name,
|
category_name: gc.name,
|
||||||
|
|
@ -173,7 +163,6 @@ export function useBudget() {
|
||||||
depth: 2,
|
depth: 2,
|
||||||
months,
|
months,
|
||||||
annual,
|
annual,
|
||||||
previousYearTotal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (gcRows.length === 0) return [];
|
if (gcRows.length === 0) return [];
|
||||||
|
|
@ -181,11 +170,9 @@ export function useBudget() {
|
||||||
// Build intermediate subtotal
|
// Build intermediate subtotal
|
||||||
const subMonths = Array(12).fill(0) as number[];
|
const subMonths = Array(12).fill(0) as number[];
|
||||||
let subAnnual = 0;
|
let subAnnual = 0;
|
||||||
let subPrevYear = 0;
|
|
||||||
for (const cr of gcRows) {
|
for (const cr of gcRows) {
|
||||||
for (let m = 0; m < 12; m++) subMonths[m] += cr.months[m];
|
for (let m = 0; m < 12; m++) subMonths[m] += cr.months[m];
|
||||||
subAnnual += cr.annual;
|
subAnnual += cr.annual;
|
||||||
subPrevYear += cr.previousYearTotal;
|
|
||||||
}
|
}
|
||||||
const subtotal: BudgetYearRow = {
|
const subtotal: BudgetYearRow = {
|
||||||
category_id: cat.id,
|
category_id: cat.id,
|
||||||
|
|
@ -197,7 +184,6 @@ export function useBudget() {
|
||||||
depth: 1,
|
depth: 1,
|
||||||
months: subMonths,
|
months: subMonths,
|
||||||
annual: subAnnual,
|
annual: subAnnual,
|
||||||
previousYearTotal: subPrevYear,
|
|
||||||
};
|
};
|
||||||
gcRows.sort((a, b) => {
|
gcRows.sort((a, b) => {
|
||||||
if (a.category_id === cat.id) return -1;
|
if (a.category_id === cat.id) return -1;
|
||||||
|
|
@ -217,7 +203,7 @@ export function useBudget() {
|
||||||
|
|
||||||
if (inputableChildren.length === 0 && intermediateParents.length === 0 && cat.is_inputable) {
|
if (inputableChildren.length === 0 && intermediateParents.length === 0 && cat.is_inputable) {
|
||||||
// Standalone leaf (no children) — regular editable row
|
// Standalone leaf (no children) — regular editable row
|
||||||
const { months, annual, previousYearTotal } = buildMonths(cat.id);
|
const { months, annual } = buildMonths(cat.id);
|
||||||
rows.push({
|
rows.push({
|
||||||
category_id: cat.id,
|
category_id: cat.id,
|
||||||
category_name: cat.name,
|
category_name: cat.name,
|
||||||
|
|
@ -228,14 +214,13 @@ export function useBudget() {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
months,
|
months,
|
||||||
annual,
|
annual,
|
||||||
previousYearTotal,
|
|
||||||
});
|
});
|
||||||
} else if (inputableChildren.length > 0 || intermediateParents.length > 0) {
|
} else if (inputableChildren.length > 0 || intermediateParents.length > 0) {
|
||||||
const allChildRows: BudgetYearRow[] = [];
|
const allChildRows: BudgetYearRow[] = [];
|
||||||
|
|
||||||
// If parent is also inputable, create a "(direct)" fake-child row
|
// If parent is also inputable, create a "(direct)" fake-child row
|
||||||
if (cat.is_inputable) {
|
if (cat.is_inputable) {
|
||||||
const { months, annual, previousYearTotal } = buildMonths(cat.id);
|
const { months, annual } = buildMonths(cat.id);
|
||||||
allChildRows.push({
|
allChildRows.push({
|
||||||
category_id: cat.id,
|
category_id: cat.id,
|
||||||
category_name: `${cat.name} (direct)`,
|
category_name: `${cat.name} (direct)`,
|
||||||
|
|
@ -246,7 +231,6 @@ export function useBudget() {
|
||||||
depth: 1,
|
depth: 1,
|
||||||
months,
|
months,
|
||||||
annual,
|
annual,
|
||||||
previousYearTotal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +238,7 @@ export function useBudget() {
|
||||||
const grandchildren = childrenByParent.get(child.id) || [];
|
const grandchildren = childrenByParent.get(child.id) || [];
|
||||||
if (grandchildren.length === 0) {
|
if (grandchildren.length === 0) {
|
||||||
// Simple leaf at depth 1
|
// Simple leaf at depth 1
|
||||||
const { months, annual, previousYearTotal } = buildMonths(child.id);
|
const { months, annual } = buildMonths(child.id);
|
||||||
allChildRows.push({
|
allChildRows.push({
|
||||||
category_id: child.id,
|
category_id: child.id,
|
||||||
category_name: child.name,
|
category_name: child.name,
|
||||||
|
|
@ -265,7 +249,6 @@ export function useBudget() {
|
||||||
depth: 1,
|
depth: 1,
|
||||||
months,
|
months,
|
||||||
annual,
|
annual,
|
||||||
previousYearTotal,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Intermediate parent at depth 1 with grandchildren
|
// Intermediate parent at depth 1 with grandchildren
|
||||||
|
|
@ -284,11 +267,9 @@ export function useBudget() {
|
||||||
const leafRows = allChildRows.filter((r) => !r.is_parent);
|
const leafRows = allChildRows.filter((r) => !r.is_parent);
|
||||||
const parentMonths = Array(12).fill(0) as number[];
|
const parentMonths = Array(12).fill(0) as number[];
|
||||||
let parentAnnual = 0;
|
let parentAnnual = 0;
|
||||||
let parentPrevYear = 0;
|
|
||||||
for (const cr of leafRows) {
|
for (const cr of leafRows) {
|
||||||
for (let m = 0; m < 12; m++) parentMonths[m] += cr.months[m];
|
for (let m = 0; m < 12; m++) parentMonths[m] += cr.months[m];
|
||||||
parentAnnual += cr.annual;
|
parentAnnual += cr.annual;
|
||||||
parentPrevYear += cr.previousYearTotal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
|
|
@ -301,7 +282,6 @@ export function useBudget() {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
months: parentMonths,
|
months: parentMonths,
|
||||||
annual: parentAnnual,
|
annual: parentAnnual,
|
||||||
previousYearTotal: parentPrevYear,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort children alphabetically, but keep "(direct)" first
|
// Sort children alphabetically, but keep "(direct)" first
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,6 @@
|
||||||
"actual": "Actual",
|
"actual": "Actual",
|
||||||
"difference": "Difference",
|
"difference": "Difference",
|
||||||
"annual": "Annual",
|
"annual": "Annual",
|
||||||
"previousYear": "Prev. Year",
|
|
||||||
"splitEvenly": "Split evenly across 12 months",
|
"splitEvenly": "Split evenly across 12 months",
|
||||||
"annualMismatch": "Annual total does not match the sum of monthly amounts",
|
"annualMismatch": "Annual total does not match the sum of monthly amounts",
|
||||||
"clickToEdit": "Click to edit",
|
"clickToEdit": "Click to edit",
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,6 @@
|
||||||
"actual": "Réel",
|
"actual": "Réel",
|
||||||
"difference": "Écart",
|
"difference": "Écart",
|
||||||
"annual": "Annuel",
|
"annual": "Annuel",
|
||||||
"previousYear": "Année préc.",
|
|
||||||
"splitEvenly": "Répartir également sur 12 mois",
|
"splitEvenly": "Répartir également sur 12 mois",
|
||||||
"annualMismatch": "Le total annuel ne correspond pas à la somme des montants mensuels",
|
"annualMismatch": "Le total annuel ne correspond pas à la somme des montants mensuels",
|
||||||
"clickToEdit": "Cliquer pour modifier",
|
"clickToEdit": "Cliquer pour modifier",
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,6 @@ export interface BudgetYearRow {
|
||||||
depth?: 0 | 1 | 2;
|
depth?: 0 | 1 | 2;
|
||||||
months: number[]; // index 0-11 = Jan-Dec planned amounts
|
months: number[]; // index 0-11 = Jan-Dec planned amounts
|
||||||
annual: number; // computed sum
|
annual: number; // computed sum
|
||||||
previousYearTotal: number; // total budget from the previous year
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportConfigTemplate {
|
export interface ImportConfigTemplate {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue