mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
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:
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}))
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,16 +1,17 @@
|
||||
import { create } from 'zustand'
|
||||
import type { StoreApi } from 'zustand'
|
||||
import { tripsApi, daysApi, placesApi, packingApi, tagsApi, categoriesApi } from '../api/client'
|
||||
import { tripsApi, daysApi, placesApi, packingApi, todoApi, tagsApi, categoriesApi } from '../api/client'
|
||||
import { createPlacesSlice } from './slices/placesSlice'
|
||||
import { createAssignmentsSlice } from './slices/assignmentsSlice'
|
||||
import { createDayNotesSlice } from './slices/dayNotesSlice'
|
||||
import { createPackingSlice } from './slices/packingSlice'
|
||||
import { createTodoSlice } from './slices/todoSlice'
|
||||
import { createBudgetSlice } from './slices/budgetSlice'
|
||||
import { createReservationsSlice } from './slices/reservationsSlice'
|
||||
import { createFilesSlice } from './slices/filesSlice'
|
||||
import { handleRemoteEvent } from './slices/remoteEventHandler'
|
||||
import type {
|
||||
Trip, Day, Place, Assignment, DayNote, PackingItem,
|
||||
Trip, Day, Place, Assignment, DayNote, PackingItem, TodoItem,
|
||||
Tag, Category, BudgetItem, TripFile, Reservation,
|
||||
AssignmentsMap, DayNotesMap, WebSocketEvent,
|
||||
} from '../types'
|
||||
@@ -19,6 +20,7 @@ import type { PlacesSlice } from './slices/placesSlice'
|
||||
import type { AssignmentsSlice } from './slices/assignmentsSlice'
|
||||
import type { DayNotesSlice } from './slices/dayNotesSlice'
|
||||
import type { PackingSlice } from './slices/packingSlice'
|
||||
import type { TodoSlice } from './slices/todoSlice'
|
||||
import type { BudgetSlice } from './slices/budgetSlice'
|
||||
import type { ReservationsSlice } from './slices/reservationsSlice'
|
||||
import type { FilesSlice } from './slices/filesSlice'
|
||||
@@ -28,6 +30,7 @@ export interface TripStoreState
|
||||
AssignmentsSlice,
|
||||
DayNotesSlice,
|
||||
PackingSlice,
|
||||
TodoSlice,
|
||||
BudgetSlice,
|
||||
ReservationsSlice,
|
||||
FilesSlice {
|
||||
@@ -37,6 +40,7 @@ export interface TripStoreState
|
||||
assignments: AssignmentsMap
|
||||
dayNotes: DayNotesMap
|
||||
packingItems: PackingItem[]
|
||||
todoItems: TodoItem[]
|
||||
tags: Tag[]
|
||||
categories: Category[]
|
||||
budgetItems: BudgetItem[]
|
||||
@@ -62,6 +66,7 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
|
||||
assignments: {},
|
||||
dayNotes: {},
|
||||
packingItems: [],
|
||||
todoItems: [],
|
||||
tags: [],
|
||||
categories: [],
|
||||
budgetItems: [],
|
||||
@@ -78,11 +83,12 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
|
||||
loadTrip: async (tripId: number | string) => {
|
||||
set({ isLoading: true, error: null })
|
||||
try {
|
||||
const [tripData, daysData, placesData, packingData, tagsData, categoriesData] = await Promise.all([
|
||||
const [tripData, daysData, placesData, packingData, todoData, tagsData, categoriesData] = await Promise.all([
|
||||
tripsApi.get(tripId),
|
||||
daysApi.list(tripId),
|
||||
placesApi.list(tripId),
|
||||
packingApi.list(tripId),
|
||||
todoApi.list(tripId),
|
||||
tagsApi.list(),
|
||||
categoriesApi.list(),
|
||||
])
|
||||
@@ -101,6 +107,7 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
|
||||
assignments: assignmentsMap,
|
||||
dayNotes: dayNotesMap,
|
||||
packingItems: packingData.items,
|
||||
todoItems: todoData.items,
|
||||
tags: tagsData.tags,
|
||||
categories: categoriesData.categories,
|
||||
isLoading: false,
|
||||
@@ -169,6 +176,7 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
|
||||
...createAssignmentsSlice(set, get),
|
||||
...createDayNotesSlice(set, get),
|
||||
...createPackingSlice(set, get),
|
||||
...createTodoSlice(set, get),
|
||||
...createBudgetSlice(set, get),
|
||||
...createReservationsSlice(set, get),
|
||||
...createFilesSlice(set, get),
|
||||
|
||||
Reference in New Issue
Block a user