feat(todo): add To-Do list feature with 3-column layout

- New todo_items DB table with priority, due date, description, user assignment
- Full CRUD API with WebSocket real-time sync
- 3-column UI: sidebar filters (All, My Tasks, Overdue, Done, by Priority),
  task list with inline badges, and detail/create pane
- Apple-inspired design with custom dropdowns, date picker, priority system (P1-P3)
- Mobile responsive: icon-only sidebar, bottom-sheet modals for detail/create
- Lists tab with sub-tabs (Packing List + To-Do), persisted selection
- Addon renamed from "Packing List" to "Lists"
- i18n keys for all 13 languages
- UI polish: notification colors use system theme, mobile navbar cleanup,
  settings page responsive buttons
This commit is contained in:
mauriceboe
2026-04-04 16:58:24 +02:00
parent 1ea0eb9965
commit 0b36427c09
31 changed files with 1732 additions and 102 deletions
+14 -1
View File
@@ -1,6 +1,6 @@
import type { StoreApi } from 'zustand'
import type { TripStoreState } from '../tripStore'
import type { Assignment, Place, Day, DayNote, PackingItem, BudgetItem, BudgetMember, Reservation, Trip, TripFile, WebSocketEvent } from '../../types'
import type { Assignment, Place, Day, DayNote, PackingItem, TodoItem, BudgetItem, BudgetMember, Reservation, Trip, TripFile, WebSocketEvent } from '../../types'
type SetState = StoreApi<TripStoreState>['setState']
@@ -175,6 +175,19 @@ export function handleRemoteEvent(set: SetState, event: WebSocketEvent): void {
packingItems: state.packingItems.filter(i => i.id !== payload.itemId),
}
// Todo
case 'todo:created':
if (state.todoItems.some(i => i.id === (payload.item as TodoItem).id)) return {}
return { todoItems: [...state.todoItems, payload.item as TodoItem] }
case 'todo:updated':
return {
todoItems: state.todoItems.map(i => i.id === (payload.item as TodoItem).id ? payload.item as TodoItem : i),
}
case 'todo:deleted':
return {
todoItems: state.todoItems.filter(i => i.id !== payload.itemId),
}
// Budget
case 'budget:created':
if (state.budgetItems.some(i => i.id === (payload.item as BudgetItem).id)) return {}
+67
View File
@@ -0,0 +1,67 @@
import { todoApi } from '../../api/client'
import type { StoreApi } from 'zustand'
import type { TripStoreState } from '../tripStore'
import type { TodoItem } from '../../types'
import { getApiErrorMessage } from '../../types'
type SetState = StoreApi<TripStoreState>['setState']
type GetState = StoreApi<TripStoreState>['getState']
export interface TodoSlice {
addTodoItem: (tripId: number | string, data: Partial<TodoItem>) => Promise<TodoItem>
updateTodoItem: (tripId: number | string, id: number, data: Partial<TodoItem>) => Promise<TodoItem>
deleteTodoItem: (tripId: number | string, id: number) => Promise<void>
toggleTodoItem: (tripId: number | string, id: number, checked: boolean) => Promise<void>
}
export const createTodoSlice = (set: SetState, get: GetState): TodoSlice => ({
addTodoItem: async (tripId, data) => {
try {
const result = await todoApi.create(tripId, data)
set(state => ({ todoItems: [...state.todoItems, result.item] }))
return result.item
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error adding todo'))
}
},
updateTodoItem: async (tripId, id, data) => {
try {
const result = await todoApi.update(tripId, id, data)
set(state => ({
todoItems: state.todoItems.map(item => item.id === id ? result.item : item)
}))
return result.item
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error updating todo'))
}
},
deleteTodoItem: async (tripId, id) => {
const prev = get().todoItems
set(state => ({ todoItems: state.todoItems.filter(item => item.id !== id) }))
try {
await todoApi.delete(tripId, id)
} catch (err: unknown) {
set({ todoItems: prev })
throw new Error(getApiErrorMessage(err, 'Error deleting todo'))
}
},
toggleTodoItem: async (tripId, id, checked) => {
set(state => ({
todoItems: state.todoItems.map(item =>
item.id === id ? { ...item, checked: checked ? 1 : 0 } : item
)
}))
try {
await todoApi.update(tripId, id, { checked })
} catch {
set(state => ({
todoItems: state.todoItems.map(item =>
item.id === id ? { ...item, checked: checked ? 0 : 1 } : item
)
}))
}
},
})