mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
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:
@@ -14,6 +14,8 @@ export interface BudgetSlice {
|
||||
deleteBudgetItem: (tripId: number | string, id: number) => Promise<void>
|
||||
setBudgetItemMembers: (tripId: number | string, itemId: number, userIds: number[]) => Promise<{ members: BudgetMember[]; item: BudgetItem }>
|
||||
toggleBudgetMemberPaid: (tripId: number | string, itemId: number, userId: number, paid: boolean) => Promise<void>
|
||||
reorderBudgetItems: (tripId: number | string, orderedIds: number[]) => Promise<void>
|
||||
reorderBudgetCategories: (tripId: number | string, orderedCategories: string[]) => Promise<void>
|
||||
}
|
||||
|
||||
export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice => ({
|
||||
@@ -82,4 +84,52 @@ export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice =>
|
||||
)
|
||||
}));
|
||||
},
|
||||
|
||||
reorderBudgetItems: async (tripId, orderedIds) => {
|
||||
// Optimistic: reorder locally
|
||||
set(state => {
|
||||
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)
|
||||
// Keep items not in orderedIds at the end
|
||||
const remaining = state.budgetItems.filter(i => !orderedIds.includes(i.id))
|
||||
return { budgetItems: [...reordered, ...remaining] }
|
||||
})
|
||||
try {
|
||||
await budgetApi.reorderItems(tripId, orderedIds)
|
||||
} catch {
|
||||
// Reload on failure
|
||||
const data = await budgetApi.list(tripId)
|
||||
set({ budgetItems: data.items })
|
||||
}
|
||||
},
|
||||
|
||||
reorderBudgetCategories: async (tripId, orderedCategories) => {
|
||||
// Optimistic: reorder items by new category order (Map preserves insertion order for numeric keys)
|
||||
set(state => {
|
||||
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 }
|
||||
})
|
||||
try {
|
||||
await budgetApi.reorderCategories(tripId, orderedCategories)
|
||||
} catch {
|
||||
const data = await budgetApi.list(tripId)
|
||||
set({ budgetItems: data.items })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user