Compare commits
No commits in common. "ecbcf44f86c91903d2a8f663cfa28197fd6c9393" and "501051f9ed48c0167351dfb7ac8c8d3a93682756" have entirely different histories.
ecbcf44f86
...
501051f9ed
7 changed files with 12 additions and 91 deletions
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [Non publié]
|
||||||
|
|
||||||
### Modifié
|
|
||||||
- Tableau de budget : la colonne année précédente affiche maintenant le réel (transactions) au lieu du budget planifié (#34)
|
|
||||||
|
|
||||||
## [0.6.5]
|
## [0.6.5]
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Budget table: previous year column now shows actual transactions instead of planned budget (#34)
|
|
||||||
|
|
||||||
## [0.6.5]
|
## [0.6.5]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ 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; // actuals are already signed in the DB
|
prevYearTotal += row.previousYearTotal * sign;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCols = 15; // category + prev year + annual + 12 months
|
const totalCols = 15; // category + prev year + annual + 12 months
|
||||||
|
|
@ -197,7 +197,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={`py-2 px-2 text-right text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"} text-[var(--muted-foreground)]`}>
|
<td className={`py-2 px-2 text-right text-xs ${isIntermediateParent ? "font-medium" : "font-semibold"} text-[var(--muted-foreground)]`}>
|
||||||
{formatSigned(row.previousYearTotal)}
|
{formatSigned(row.previousYearTotal * sign)}
|
||||||
</td>
|
</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)}
|
||||||
|
|
@ -230,7 +230,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
{/* Previous year total — read-only */}
|
{/* Previous year total — read-only */}
|
||||||
<td className="py-2 px-2 text-right text-[var(--muted-foreground)]">
|
<td className="py-2 px-2 text-right text-[var(--muted-foreground)]">
|
||||||
<span className="text-xs px-1 py-0.5">
|
<span className="text-xs px-1 py-0.5">
|
||||||
{formatSigned(row.previousYearTotal)}
|
{formatSigned(row.previousYearTotal * sign)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{/* Annual total — editable */}
|
{/* Annual total — editable */}
|
||||||
|
|
@ -340,7 +340,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
||||||
sectionMonthTotals[m] += row.months[m] * sign;
|
sectionMonthTotals[m] += row.months[m] * sign;
|
||||||
}
|
}
|
||||||
sectionAnnualTotal += row.annual * sign;
|
sectionAnnualTotal += row.annual * sign;
|
||||||
sectionPrevYearTotal += row.previousYearTotal; // actuals are already signed in the DB
|
sectionPrevYearTotal += row.previousYearTotal * sign;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment key={type}>
|
<Fragment key={type}>
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { buildPrevYearTotalMap } from "./useBudget";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for the previous-year actuals normalization logic used in useBudget.
|
|
||||||
*
|
|
||||||
* Transaction amounts in the database use signed values (expenses are negative,
|
|
||||||
* income is positive). The buildPrevYearTotalMap function preserves these signs
|
|
||||||
* as-is, because the budget display layer does NOT apply a sign multiplier to
|
|
||||||
* previous year actuals (unlike planned budget amounts).
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe("buildPrevYearTotalMap", () => {
|
|
||||||
it("should preserve negative sign for expense actuals", () => {
|
|
||||||
const actuals = [{ category_id: 1, actual: -500 }]; // expense: negative in DB
|
|
||||||
const map = buildPrevYearTotalMap(actuals);
|
|
||||||
expect(map.get(1)).toBe(-500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve positive sign for income actuals", () => {
|
|
||||||
const actuals = [{ category_id: 2, actual: 3000 }]; // income: positive in DB
|
|
||||||
const map = buildPrevYearTotalMap(actuals);
|
|
||||||
expect(map.get(2)).toBe(3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should skip null category_id entries", () => {
|
|
||||||
const actuals = [{ category_id: null, actual: -100 }];
|
|
||||||
const map = buildPrevYearTotalMap(actuals);
|
|
||||||
expect(map.size).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle zero actuals", () => {
|
|
||||||
const actuals = [{ category_id: 3, actual: 0 }];
|
|
||||||
const map = buildPrevYearTotalMap(actuals);
|
|
||||||
expect(map.get(3)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple categories", () => {
|
|
||||||
const actuals = [
|
|
||||||
{ category_id: 1, actual: -200 },
|
|
||||||
{ category_id: 2, actual: 1500 },
|
|
||||||
{ category_id: 3, actual: -75.5 },
|
|
||||||
];
|
|
||||||
const map = buildPrevYearTotalMap(actuals);
|
|
||||||
expect(map.size).toBe(3);
|
|
||||||
expect(map.get(1)).toBe(-200);
|
|
||||||
expect(map.get(2)).toBe(1500);
|
|
||||||
expect(map.get(3)).toBe(-75.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -3,7 +3,6 @@ import type { BudgetYearRow, BudgetTemplate } from "../shared/types";
|
||||||
import {
|
import {
|
||||||
getAllActiveCategories,
|
getAllActiveCategories,
|
||||||
getBudgetEntriesForYear,
|
getBudgetEntriesForYear,
|
||||||
getActualTotalsForYear,
|
|
||||||
upsertBudgetEntry,
|
upsertBudgetEntry,
|
||||||
upsertBudgetEntriesForYear,
|
upsertBudgetEntriesForYear,
|
||||||
getAllTemplates,
|
getAllTemplates,
|
||||||
|
|
@ -63,21 +62,6 @@ function reducer(state: BudgetState, action: BudgetAction): BudgetState {
|
||||||
|
|
||||||
const TYPE_ORDER: Record<string, number> = { expense: 0, income: 1, transfer: 2 };
|
const TYPE_ORDER: Record<string, number> = { expense: 0, income: 1, transfer: 2 };
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a map of category_id -> annual actual total from raw actuals.
|
|
||||||
* Transaction amounts are already signed (expenses negative, income positive),
|
|
||||||
* so they are stored as-is without normalization.
|
|
||||||
*/
|
|
||||||
export function buildPrevYearTotalMap(
|
|
||||||
actuals: Array<{ category_id: number | null; actual: number }>
|
|
||||||
): Map<number, number> {
|
|
||||||
const prevYearTotalMap = new Map<number, number>();
|
|
||||||
for (const a of actuals) {
|
|
||||||
if (a.category_id != null) prevYearTotalMap.set(a.category_id, a.actual);
|
|
||||||
}
|
|
||||||
return prevYearTotalMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBudget() {
|
export function useBudget() {
|
||||||
const [state, dispatch] = useReducer(reducer, undefined, initialState);
|
const [state, dispatch] = useReducer(reducer, undefined, initialState);
|
||||||
const fetchIdRef = useRef(0);
|
const fetchIdRef = useRef(0);
|
||||||
|
|
@ -88,10 +72,10 @@ export function useBudget() {
|
||||||
dispatch({ type: "SET_ERROR", payload: null });
|
dispatch({ type: "SET_ERROR", payload: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [allCategories, entries, prevYearActuals, templates] = await Promise.all([
|
const [allCategories, entries, prevYearEntries, templates] = await Promise.all([
|
||||||
getAllActiveCategories(),
|
getAllActiveCategories(),
|
||||||
getBudgetEntriesForYear(year),
|
getBudgetEntriesForYear(year),
|
||||||
getActualTotalsForYear(year - 1),
|
getBudgetEntriesForYear(year - 1),
|
||||||
getAllTemplates(),
|
getAllTemplates(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -104,8 +88,11 @@ 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 actuals: categoryId -> annual actual total
|
// Build a map for previous year totals: categoryId -> annual total
|
||||||
const prevYearTotalMap = buildPrevYearTotalMap(prevYearActuals);
|
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) => {
|
||||||
|
|
|
||||||
|
|
@ -178,16 +178,6 @@ export async function deleteTemplate(templateId: number): Promise<void> {
|
||||||
await db.execute("DELETE FROM budget_templates WHERE id = $1", [templateId]);
|
await db.execute("DELETE FROM budget_templates WHERE id = $1", [templateId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Actuals helpers ---
|
|
||||||
|
|
||||||
export async function getActualTotalsForYear(
|
|
||||||
year: number
|
|
||||||
): Promise<Array<{ category_id: number | null; actual: number }>> {
|
|
||||||
const dateFrom = `${year}-01-01`;
|
|
||||||
const dateTo = `${year}-12-31`;
|
|
||||||
return getActualsByCategoryRange(dateFrom, dateTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Budget vs Actual ---
|
// --- Budget vs Actual ---
|
||||||
|
|
||||||
async function getActualsByCategoryRange(
|
async function getActualsByCategoryRange(
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export interface BudgetYearRow {
|
||||||
depth?: number;
|
depth?: number;
|
||||||
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; // actual (transactions) total from the previous year
|
previousYearTotal: number; // total budget from the previous year
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportConfigTemplate {
|
export interface ImportConfigTemplate {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue