diff --git a/package-lock.json b/package-lock.json index a0bc8d5..ea6b02d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simpl_result_scaffold", - "version": "0.3.0", + "version": "0.3.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simpl_result_scaffold", - "version": "0.3.0", + "version": "0.3.7", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/src/components/budget/BudgetTable.tsx b/src/components/budget/BudgetTable.tsx index 7fe3981..4e6386b 100644 --- a/src/components/budget/BudgetTable.tsx +++ b/src/components/budget/BudgetTable.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, Fragment } from "react"; import { useTranslation } from "react-i18next"; -import { AlertTriangle } from "lucide-react"; +import { AlertTriangle, ArrowUpDown } from "lucide-react"; import type { BudgetYearRow } from "../../shared/types"; const fmt = new Intl.NumberFormat("en-CA", { @@ -16,6 +16,32 @@ const MONTH_KEYS = [ "months.sep", "months.oct", "months.nov", "months.dec", ] as const; +const STORAGE_KEY = "subtotals-position"; + +function reorderRows( + rows: T[], + subtotalsOnTop: boolean, +): T[] { + if (subtotalsOnTop) return rows; + const groups: { parent: T | null; children: T[] }[] = []; + let current: { parent: T | null; children: T[] } | null = null; + for (const row of rows) { + if (row.is_parent) { + if (current) groups.push(current); + current = { parent: row, children: [] }; + } else if (current && row.parent_id === current.parent?.category_id) { + current.children.push(row); + } else { + if (current) groups.push(current); + current = { parent: null, children: [row] }; + } + } + if (current) groups.push(current); + return groups.flatMap(({ parent, children }) => + parent ? [...children, parent] : children, + ); +} + interface BudgetTableProps { rows: BudgetYearRow[]; onUpdatePlanned: (categoryId: number, month: number, amount: number) => void; @@ -27,6 +53,18 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu const [editingCell, setEditingCell] = useState<{ categoryId: number; monthIdx: number } | null>(null); const [editingAnnual, setEditingAnnual] = useState<{ categoryId: number } | null>(null); const [editingValue, setEditingValue] = useState(""); + const [subtotalsOnTop, setSubtotalsOnTop] = useState(() => { + const stored = localStorage.getItem(STORAGE_KEY); + return stored === null ? true : stored === "top"; + }); + + const toggleSubtotals = () => { + setSubtotalsOnTop((prev) => { + const next = !prev; + localStorage.setItem(STORAGE_KEY, next ? "top" : "bottom"); + return next; + }); + }; const inputRef = useRef(null); const annualInputRef = useRef(null); @@ -258,7 +296,17 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu }; return ( -
+
+
+ +
+
@@ -289,7 +337,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu {t(typeLabelKeys[type])} - {group.map((row) => renderRow(row))} + {reorderRows(group, subtotalsOnTop).map((row) => renderRow(row))} ); })} @@ -305,6 +353,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
+
); } diff --git a/src/components/reports/BudgetVsActualTable.tsx b/src/components/reports/BudgetVsActualTable.tsx index 18b4a68..3fdf4ac 100644 --- a/src/components/reports/BudgetVsActualTable.tsx +++ b/src/components/reports/BudgetVsActualTable.tsx @@ -1,5 +1,6 @@ -import { Fragment } from "react"; +import { Fragment, useState } from "react"; import { useTranslation } from "react-i18next"; +import { ArrowUpDown } from "lucide-react"; import type { BudgetVsActualRow } from "../../shared/types"; const cadFormatter = (value: number) => @@ -22,8 +23,46 @@ interface BudgetVsActualTableProps { data: BudgetVsActualRow[]; } +const STORAGE_KEY = "subtotals-position"; + +function reorderRows( + rows: T[], + subtotalsOnTop: boolean, +): T[] { + if (subtotalsOnTop) return rows; + const groups: { parent: T | null; children: T[] }[] = []; + let current: { parent: T | null; children: T[] } | null = null; + for (const row of rows) { + if (row.is_parent) { + if (current) groups.push(current); + current = { parent: row, children: [] }; + } else if (current && row.parent_id === current.parent?.category_id) { + current.children.push(row); + } else { + if (current) groups.push(current); + current = { parent: null, children: [row] }; + } + } + if (current) groups.push(current); + return groups.flatMap(({ parent, children }) => + parent ? [...children, parent] : children, + ); +} + export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) { const { t } = useTranslation(); + const [subtotalsOnTop, setSubtotalsOnTop] = useState(() => { + const stored = localStorage.getItem(STORAGE_KEY); + return stored === null ? true : stored === "top"; + }); + + const toggleSubtotals = () => { + setSubtotalsOnTop((prev) => { + const next = !prev; + localStorage.setItem(STORAGE_KEY, next ? "top" : "bottom"); + return next; + }); + }; if (data.length === 0) { return ( @@ -68,7 +107,17 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) const totalYtdPct = totals.ytdBudget !== 0 ? totals.ytdVariation / Math.abs(totals.ytdBudget) : null; return ( -
+
+
+ +
+
@@ -117,7 +166,7 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) {section.label} - {section.rows.map((row) => { + {reorderRows(section.rows, subtotalsOnTop).map((row) => { const isParent = row.is_parent; const isChild = row.parent_id !== null && !row.is_parent; return ( @@ -187,6 +236,7 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
+
); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index da4cf38..e768145 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -26,8 +26,12 @@ "6months": "6 months", "12months": "12 months", "year": "This year", - "all": "All" + "all": "All", + "custom": "Custom" }, + "dateFrom": "From", + "dateTo": "To", + "apply": "Apply", "help": { "title": "How to use the Dashboard", "tips": [ @@ -343,6 +347,8 @@ "overTime": "Category Over Time", "trends": "Monthly Trends", "budgetVsActual": "Budget vs Actual", + "subtotalsOnTop": "Subtotals on top", + "subtotalsOnBottom": "Subtotals on bottom", "bva": { "monthly": "Monthly", "ytd": "Year-to-Date", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 0e54f33..030bc3e 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -26,8 +26,12 @@ "6months": "6 mois", "12months": "12 mois", "year": "Cette année", - "all": "Tout" + "all": "Tout", + "custom": "Personnalisé" }, + "dateFrom": "Du", + "dateTo": "Au", + "apply": "Appliquer", "help": { "title": "Comment utiliser le tableau de bord", "tips": [ @@ -343,6 +347,8 @@ "overTime": "Catégories dans le temps", "trends": "Tendances mensuelles", "budgetVsActual": "Budget vs R\u00e9el", + "subtotalsOnTop": "Sous-totaux en haut", + "subtotalsOnBottom": "Sous-totaux en bas", "bva": { "monthly": "Mensuel", "ytd": "Cumul annuel",