@@ -67,7 +67,7 @@ export default function CategoryPieChart({
)}
+
{data.map((item, index) => {
const isHidden = hiddenCategories.has(item.category_name);
return (
diff --git a/src/hooks/useBudget.ts b/src/hooks/useBudget.ts
index 9ee0899..ce70a5f 100644
--- a/src/hooks/useBudget.ts
+++ b/src/hooks/useBudget.ts
@@ -317,6 +317,7 @@ export function useBudget() {
}
// Sort by type, then within each type: keep hierarchy groups together
+ // Sub-group logic: depth-2 children must stay under their intermediate parent
function getTopGroupId(r: BudgetYearRow): number {
if ((r.depth ?? 0) === 0) return r.category_id;
if (r.is_parent && r.parent_id === null) return r.category_id;
@@ -328,6 +329,11 @@ export function useBudget() {
}
return r.category_id;
}
+ function getSubGroupId(r: BudgetYearRow): number {
+ if ((r.depth ?? 0) === 2 && r.parent_id !== null) return r.parent_id;
+ if ((r.depth ?? 0) === 1 && r.is_parent) return r.category_id;
+ return r.category_id;
+ }
rows.sort((a, b) => {
const typeA = TYPE_ORDER[a.category_type] ?? 9;
@@ -343,9 +349,23 @@ export function useBudget() {
if (orderA !== orderB) return orderA - orderB;
return (catA?.name ?? "").localeCompare(catB?.name ?? "");
}
- // Same group: sort by depth, then parent before children at same depth
- if (a.is_parent !== b.is_parent && (a.depth ?? 0) === (b.depth ?? 0)) return a.is_parent ? -1 : 1;
- if ((a.depth ?? 0) !== (b.depth ?? 0)) return (a.depth ?? 0) - (b.depth ?? 0);
+ // Within same group: depth-0 parent always first
+ if (a.is_parent && (a.depth ?? 0) === 0) return -1;
+ if (b.is_parent && (b.depth ?? 0) === 0) return 1;
+ // "(direct)" of top-level parent always second
+ if (!a.is_parent && (a.depth ?? 0) === 1 && a.parent_id === a.category_id) return -1;
+ if (!b.is_parent && (b.depth ?? 0) === 1 && b.parent_id === b.category_id) return 1;
+ // Sub-group: keep depth-2 children with their intermediate parent
+ const subA = getSubGroupId(a);
+ const subB = getSubGroupId(b);
+ if (subA !== subB) {
+ const nameA = catById.get(subA)?.name ?? "";
+ const nameB = catById.get(subB)?.name ?? "";
+ return nameA.localeCompare(nameB);
+ }
+ // Same sub-group: parent before children
+ if (a.is_parent !== b.is_parent) return a.is_parent ? -1 : 1;
+ // "(direct)" within sub-group first
if (a.parent_id && a.category_id === a.parent_id) return -1;
if (b.parent_id && b.category_id === b.parent_id) return 1;
return a.category_name.localeCompare(b.category_name);
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
index d484fbf..e6ccfe9 100644
--- a/src/pages/DashboardPage.tsx
+++ b/src/pages/DashboardPage.tsx
@@ -126,7 +126,7 @@ export default function DashboardPage() {
))}
-
+
{t("dashboard.expensesByCategory")}
{
const typeA = TYPE_ORDER[a.category_type] ?? 9;
const typeB = TYPE_ORDER[b.category_type] ?? 9;
if (typeA !== typeB) return typeA - typeB;
- // Find the top-level group id
- function getGroupId(r: BudgetVsActualRow): number {
- if (r.depth === 0) return r.category_id;
- if (r.is_parent && r.parent_id === null) return r.category_id;
- // Walk up to find the root
- let pid = r.parent_id;
- while (pid !== null) {
- const pCat = catById.get(pid);
- if (!pCat || !pCat.parent_id) return pid;
- pid = pCat.parent_id;
- }
- return r.category_id;
- }
const groupA = getGroupId(a);
const groupB = getGroupId(b);
if (groupA !== groupB) {
@@ -442,9 +446,23 @@ export async function getBudgetVsActualData(
if (orderA !== orderB) return orderA - orderB;
return (catA?.name ?? "").localeCompare(catB?.name ?? "");
}
- // Within same group: sort by depth, then parent before children
- if (a.is_parent !== b.is_parent && (a.depth ?? 0) === (b.depth ?? 0)) return a.is_parent ? -1 : 1;
- if ((a.depth ?? 0) !== (b.depth ?? 0)) return (a.depth ?? 0) - (b.depth ?? 0);
+ // Within same group: depth-0 parent always first
+ if (a.is_parent && (a.depth ?? 0) === 0) return -1;
+ if (b.is_parent && (b.depth ?? 0) === 0) return 1;
+ // "(direct)" of top-level parent always second
+ if (!a.is_parent && (a.depth ?? 0) === 1 && a.parent_id === a.category_id) return -1;
+ if (!b.is_parent && (b.depth ?? 0) === 1 && b.parent_id === b.category_id) return 1;
+ // Sub-group: keep depth-2 children with their intermediate parent
+ const subA = getSubGroupId(a);
+ const subB = getSubGroupId(b);
+ if (subA !== subB) {
+ const nameA = catById.get(subA)?.name ?? "";
+ const nameB = catById.get(subB)?.name ?? "";
+ return nameA.localeCompare(nameB);
+ }
+ // Same sub-group: parent before children
+ if (a.is_parent !== b.is_parent) return a.is_parent ? -1 : 1;
+ // "(direct)" within sub-group first
if (a.parent_id && a.category_id === a.parent_id) return -1;
if (b.parent_id && b.category_id === b.parent_id) return 1;
return a.category_name.localeCompare(b.category_name);