feat: drag-and-drop reorder for budget categories and items (#479)

Add reordering support for budget categories and line items within
categories. Changes persist via new DB table (budget_category_order)
and existing sort_order column. Live sync via WebSocket budget:reordered
event. Use Map instead of plain objects for category grouping to
preserve insertion order with numeric category names.
This commit is contained in:
Maurice
2026-04-09 19:21:43 +02:00
parent 1f3e27765a
commit 5c0d819fc1
7 changed files with 293 additions and 24 deletions
@@ -214,6 +214,37 @@ export function handleRemoteEvent(set: SetState, event: WebSocketEvent): void {
: i
),
}
case 'budget:reordered': {
if (payload.orderedIds) {
const orderedIds = payload.orderedIds as number[]
const byId = new Map(state.budgetItems.map(i => [i.id, i]))
const reordered = orderedIds.map((id, idx) => {
const item = byId.get(id)
return item ? { ...item, sort_order: idx } : null
}).filter((i): i is BudgetItem => i !== null)
const remaining = state.budgetItems.filter(i => !orderedIds.includes(i.id))
return { budgetItems: [...reordered, ...remaining] }
}
if (payload.orderedCategories) {
const orderedCategories = payload.orderedCategories as string[]
const grouped = new Map<string, BudgetItem[]>()
for (const item of state.budgetItems) {
const cat = item.category || 'Other'
if (!grouped.has(cat)) grouped.set(cat, [])
grouped.get(cat)!.push(item)
}
const reordered: BudgetItem[] = []
for (const cat of orderedCategories) {
const items = grouped.get(cat)
if (items) reordered.push(...items)
}
for (const [cat, items] of grouped) {
if (!orderedCategories.includes(cat)) reordered.push(...items)
}
return { budgetItems: reordered }
}
return {}
}
// Reservations
case 'reservation:created':