fix(todo): tasks added, ticked, edited, reordered or deleted offline wait in the queue and sync once the connection is back

Every task write went straight to the API, so offline a new task failed with
"Network Error" and was lost, and a tick flipped back. Tasks now go through
todoRepo and the mutation queue like packing items: a temporary row at the
end of the list offline, the edits and the delete of it wait for its real id,
and an online write to a task that still has an older one queued joins the
queue behind it, since task writes carry no version token. A reorder offline
is queued without the tasks that have no server id yet. Desktop and phone
both write through the store, so both are covered.

Refs #2601
This commit is contained in:
Maurice
2026-10-12 18:24:18 +02:00
parent cfdbfb49c0
commit 5dbcc4af40
4 changed files with 333 additions and 6 deletions
+6 -6
View File
@@ -1,4 +1,4 @@
import { todoApi } from '../../api/client'
import { todoRepo } from '../../repo/todoRepo'
import type { StoreApi } from 'zustand'
import type { TripStoreState } from '../tripStore'
import type { TodoItem } from '../../types'
@@ -20,7 +20,7 @@ export interface TodoSlice {
export const createTodoSlice = (set: SetState, get: GetState): TodoSlice => ({
addTodoItem: async (tripId, data) => {
try {
const result = await todoApi.create(tripId, data)
const result = await todoRepo.create(tripId, data)
set(state => ({ todoItems: [...state.todoItems, result.item] }))
return result.item
} catch (err: unknown) {
@@ -30,7 +30,7 @@ export const createTodoSlice = (set: SetState, get: GetState): TodoSlice => ({
updateTodoItem: async (tripId, id, data) => {
try {
const result = await todoApi.update(tripId, id, data)
const result = await todoRepo.update(tripId, id, data)
set(state => ({
todoItems: state.todoItems.map(item => item.id === id ? result.item : item)
}))
@@ -44,7 +44,7 @@ export const createTodoSlice = (set: SetState, get: GetState): TodoSlice => ({
const prev = get().todoItems
set(state => ({ todoItems: state.todoItems.filter(item => item.id !== id) }))
try {
await todoApi.delete(tripId, id)
await todoRepo.delete(tripId, id)
} catch (err: unknown) {
set({ todoItems: prev })
throw new Error(getApiErrorMessage(err, 'Error deleting todo'))
@@ -58,7 +58,7 @@ export const createTodoSlice = (set: SetState, get: GetState): TodoSlice => ({
)
}))
try {
await todoApi.update(tripId, id, { checked })
await todoRepo.update(tripId, id, { checked })
} catch (err: unknown) {
// The caller fires this optimistically and doesn't await, so rolling back
// silently would just flip the checkbox with no explanation. Surface it.
@@ -85,7 +85,7 @@ export const createTodoSlice = (set: SetState, get: GetState): TodoSlice => ({
return { todoItems: [...reordered, ...remaining] }
})
try {
await todoApi.reorder(tripId, orderedIds)
await todoRepo.reorder(tripId, orderedIds)
} catch (err: unknown) {
set({ todoItems: prev })
notify(getApiErrorMessage(err, 'Error reordering todos'), 'error')