mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
feat: complete offline write support with mutation queue + runtime SW cache config
- Add offline CRUD to todoRepo, budgetRepo, reservationRepo, accommodationRepo, dayRepo, tripRepo, fileRepo with optimistic Dexie writes and mutation queue - Wire all store slices (todo, budget, reservations, files, dayNotes, assignments, tripStore) through repos for offline-aware writes - Cover archive/unarchive, file toggleStar/update/delete, assignment create/delete, day title/notes update offline paths - Migrate service worker from generateSW to injectManifest (custom sw.ts) with runtime-configurable api-data (7d/500) and map-tiles (30d/1000) cache policies - Add Settings → Offline cache configuration UI with save/reset and live SW postMessage - Extend mutationQueue flush to cover all writable Dexie tables
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { accommodationsApi } from '../api/client'
|
||||
import { offlineDb, upsertAccommodations } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Accommodation } from '../types'
|
||||
|
||||
export const accommodationRepo = {
|
||||
@@ -13,4 +14,77 @@ export const accommodationRepo = {
|
||||
upsertAccommodations(result.accommodations || []).catch(() => {})
|
||||
return result
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ accommodation: Accommodation }> {
|
||||
if (!navigator.onLine) {
|
||||
const tempId = -(Date.now())
|
||||
const tempAccommodation: Accommodation = {
|
||||
...(data as Partial<Accommodation>),
|
||||
id: tempId,
|
||||
trip_id: Number(tripId),
|
||||
name: (data.name as string) ?? 'New accommodation',
|
||||
address: null,
|
||||
check_in: null,
|
||||
check_in_end: null,
|
||||
check_out: null,
|
||||
confirmation_number: null,
|
||||
notes: null,
|
||||
url: null,
|
||||
created_at: new Date().toISOString(),
|
||||
} as Accommodation
|
||||
await offlineDb.accommodations.put(tempAccommodation)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'POST',
|
||||
url: `/trips/${tripId}/accommodations`,
|
||||
body: data,
|
||||
resource: 'accommodations',
|
||||
tempId,
|
||||
})
|
||||
return { accommodation: tempAccommodation }
|
||||
}
|
||||
const result = await accommodationsApi.create(tripId, data)
|
||||
offlineDb.accommodations.put(result.accommodation)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<{ accommodation: Accommodation }> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.accommodations.get(id)
|
||||
const optimistic: Accommodation = { ...(existing ?? {} as Accommodation), ...(data as Partial<Accommodation>), id }
|
||||
await offlineDb.accommodations.put(optimistic)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/accommodations/${id}`,
|
||||
body: data,
|
||||
resource: 'accommodations',
|
||||
})
|
||||
return { accommodation: optimistic }
|
||||
}
|
||||
const result = await accommodationsApi.update(tripId, id, data)
|
||||
offlineDb.accommodations.put(result.accommodation)
|
||||
return result
|
||||
},
|
||||
|
||||
async delete(tripId: number | string, id: number): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
await offlineDb.accommodations.delete(id)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'DELETE',
|
||||
url: `/trips/${tripId}/accommodations/${id}`,
|
||||
body: undefined,
|
||||
resource: 'accommodations',
|
||||
entityId: id,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await accommodationsApi.delete(tripId, id)
|
||||
offlineDb.accommodations.delete(id)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { budgetApi } from '../api/client'
|
||||
import { offlineDb, upsertBudgetItems } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { BudgetItem } from '../types'
|
||||
|
||||
export const budgetRepo = {
|
||||
@@ -15,4 +16,72 @@ export const budgetRepo = {
|
||||
upsertBudgetItems(result.items)
|
||||
return result
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ item: BudgetItem }> {
|
||||
if (!navigator.onLine) {
|
||||
const tempId = -(Date.now())
|
||||
const tempItem: BudgetItem = {
|
||||
...(data as Partial<BudgetItem>),
|
||||
id: tempId,
|
||||
trip_id: Number(tripId),
|
||||
name: (data.name as string) ?? 'New expense',
|
||||
amount: (data.amount as number) ?? 0,
|
||||
currency: (data.currency as string) ?? 'USD',
|
||||
members: [],
|
||||
} as BudgetItem
|
||||
await offlineDb.budgetItems.put(tempItem)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'POST',
|
||||
url: `/trips/${tripId}/budget`,
|
||||
body: data,
|
||||
resource: 'budgetItems',
|
||||
tempId,
|
||||
})
|
||||
return { item: tempItem }
|
||||
}
|
||||
const result = await budgetApi.create(tripId, data)
|
||||
offlineDb.budgetItems.put(result.item)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<{ item: BudgetItem }> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.budgetItems.get(id)
|
||||
const optimistic: BudgetItem = { ...(existing ?? {} as BudgetItem), ...(data as Partial<BudgetItem>), id }
|
||||
await offlineDb.budgetItems.put(optimistic)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/budget/${id}`,
|
||||
body: data,
|
||||
resource: 'budgetItems',
|
||||
})
|
||||
return { item: optimistic }
|
||||
}
|
||||
const result = await budgetApi.update(tripId, id, data)
|
||||
offlineDb.budgetItems.put(result.item)
|
||||
return result
|
||||
},
|
||||
|
||||
async delete(tripId: number | string, id: number): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
await offlineDb.budgetItems.delete(id)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'DELETE',
|
||||
url: `/trips/${tripId}/budget/${id}`,
|
||||
body: undefined,
|
||||
resource: 'budgetItems',
|
||||
entityId: id,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await budgetApi.delete(tripId, id)
|
||||
offlineDb.budgetItems.delete(id)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { daysApi } from '../api/client'
|
||||
import { offlineDb, upsertDays } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Day } from '../types'
|
||||
|
||||
export const dayRepo = {
|
||||
@@ -15,4 +16,24 @@ export const dayRepo = {
|
||||
upsertDays(result.days)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, dayId: number | string, data: Record<string, unknown>): Promise<{ day: Day }> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.days.get(Number(dayId))
|
||||
const optimistic: Day = { ...(existing ?? {} as Day), ...(data as Partial<Day>), id: Number(dayId) }
|
||||
await offlineDb.days.put(optimistic)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/days/${dayId}`,
|
||||
body: data,
|
||||
resource: 'days',
|
||||
})
|
||||
return { day: optimistic }
|
||||
}
|
||||
const result = await daysApi.update(tripId, dayId, data)
|
||||
offlineDb.days.put(result.day)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { filesApi } from '../api/client'
|
||||
import { offlineDb, upsertTripFiles } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { TripFile } from '../types'
|
||||
|
||||
export const fileRepo = {
|
||||
@@ -15,4 +16,61 @@ export const fileRepo = {
|
||||
upsertTripFiles(result.files)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.tripFiles.get(id)
|
||||
if (existing) await offlineDb.tripFiles.put({ ...existing, ...(data as Partial<TripFile>) })
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/files/${id}`,
|
||||
body: data,
|
||||
resource: 'tripFiles',
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await filesApi.update(tripId, id, data)
|
||||
const file = (result as { file?: TripFile }).file
|
||||
if (file) offlineDb.tripFiles.put(file)
|
||||
return result
|
||||
},
|
||||
|
||||
async toggleStar(tripId: number | string, id: number): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.tripFiles.get(id)
|
||||
if (existing) {
|
||||
await offlineDb.tripFiles.put({ ...existing, starred: existing.starred ? 0 : 1 })
|
||||
}
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PATCH',
|
||||
url: `/trips/${tripId}/files/${id}/star`,
|
||||
body: undefined,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
return filesApi.toggleStar(tripId, id)
|
||||
},
|
||||
|
||||
async delete(tripId: number | string, id: number): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
await offlineDb.tripFiles.delete(id)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'DELETE',
|
||||
url: `/trips/${tripId}/files/${id}`,
|
||||
body: undefined,
|
||||
resource: 'tripFiles',
|
||||
entityId: id,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await filesApi.delete(tripId, id)
|
||||
offlineDb.tripFiles.delete(id)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { reservationsApi } from '../api/client'
|
||||
import { offlineDb, upsertReservations } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Reservation } from '../types'
|
||||
|
||||
export const reservationRepo = {
|
||||
@@ -15,4 +16,77 @@ export const reservationRepo = {
|
||||
upsertReservations(result.reservations)
|
||||
return result
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ reservation: Reservation }> {
|
||||
if (!navigator.onLine) {
|
||||
const tempId = -(Date.now())
|
||||
const tempReservation: Reservation = {
|
||||
...(data as Partial<Reservation>),
|
||||
id: tempId,
|
||||
trip_id: Number(tripId),
|
||||
name: (data.name as string) ?? 'New reservation',
|
||||
type: (data.type as string) ?? 'other',
|
||||
status: 'pending',
|
||||
date: (data.date as string) ?? null,
|
||||
time: null,
|
||||
confirmation_number: null,
|
||||
notes: null,
|
||||
url: null,
|
||||
created_at: new Date().toISOString(),
|
||||
} as Reservation
|
||||
await offlineDb.reservations.put(tempReservation)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'POST',
|
||||
url: `/trips/${tripId}/reservations`,
|
||||
body: data,
|
||||
resource: 'reservations',
|
||||
tempId,
|
||||
})
|
||||
return { reservation: tempReservation }
|
||||
}
|
||||
const result = await reservationsApi.create(tripId, data)
|
||||
offlineDb.reservations.put(result.reservation)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<{ reservation: Reservation }> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.reservations.get(id)
|
||||
const optimistic: Reservation = { ...(existing ?? {} as Reservation), ...(data as Partial<Reservation>), id }
|
||||
await offlineDb.reservations.put(optimistic)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/reservations/${id}`,
|
||||
body: data,
|
||||
resource: 'reservations',
|
||||
})
|
||||
return { reservation: optimistic }
|
||||
}
|
||||
const result = await reservationsApi.update(tripId, id, data)
|
||||
offlineDb.reservations.put(result.reservation)
|
||||
return result
|
||||
},
|
||||
|
||||
async delete(tripId: number | string, id: number): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
await offlineDb.reservations.delete(id)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'DELETE',
|
||||
url: `/trips/${tripId}/reservations/${id}`,
|
||||
body: undefined,
|
||||
resource: 'reservations',
|
||||
entityId: id,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await reservationsApi.delete(tripId, id)
|
||||
offlineDb.reservations.delete(id)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { todoApi } from '../api/client'
|
||||
import { offlineDb, upsertTodoItems } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { TodoItem } from '../types'
|
||||
|
||||
export const todoRepo = {
|
||||
@@ -15,4 +16,75 @@ export const todoRepo = {
|
||||
upsertTodoItems(result.items)
|
||||
return result
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ item: TodoItem }> {
|
||||
if (!navigator.onLine) {
|
||||
const tempId = -(Date.now())
|
||||
const tempItem: TodoItem = {
|
||||
...(data as Partial<TodoItem>),
|
||||
id: tempId,
|
||||
trip_id: Number(tripId),
|
||||
name: (data.name as string) ?? 'New todo',
|
||||
checked: 0,
|
||||
sort_order: 0,
|
||||
due_date: null,
|
||||
description: null,
|
||||
assigned_user_id: null,
|
||||
priority: 0,
|
||||
} as TodoItem
|
||||
await offlineDb.todoItems.put(tempItem)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'POST',
|
||||
url: `/trips/${tripId}/todo`,
|
||||
body: data,
|
||||
resource: 'todoItems',
|
||||
tempId,
|
||||
})
|
||||
return { item: tempItem }
|
||||
}
|
||||
const result = await todoApi.create(tripId, data)
|
||||
offlineDb.todoItems.put(result.item)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<{ item: TodoItem }> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.todoItems.get(id)
|
||||
const optimistic: TodoItem = { ...(existing ?? {} as TodoItem), ...(data as Partial<TodoItem>), id }
|
||||
await offlineDb.todoItems.put(optimistic)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/todo/${id}`,
|
||||
body: data,
|
||||
resource: 'todoItems',
|
||||
})
|
||||
return { item: optimistic }
|
||||
}
|
||||
const result = await todoApi.update(tripId, id, data)
|
||||
offlineDb.todoItems.put(result.item)
|
||||
return result
|
||||
},
|
||||
|
||||
async delete(tripId: number | string, id: number): Promise<unknown> {
|
||||
if (!navigator.onLine) {
|
||||
await offlineDb.todoItems.delete(id)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'DELETE',
|
||||
url: `/trips/${tripId}/todo/${id}`,
|
||||
body: undefined,
|
||||
resource: 'todoItems',
|
||||
entityId: id,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await todoApi.delete(tripId, id)
|
||||
offlineDb.todoItems.delete(id)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { tripsApi } from '../api/client'
|
||||
import { offlineDb, upsertTrip } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Trip } from '../types'
|
||||
|
||||
export const tripRepo = {
|
||||
@@ -30,4 +31,24 @@ export const tripRepo = {
|
||||
upsertTrip(result.trip)
|
||||
return result
|
||||
},
|
||||
|
||||
async update(tripId: number | string, data: Partial<Trip>): Promise<{ trip: Trip }> {
|
||||
if (!navigator.onLine) {
|
||||
const existing = await offlineDb.trips.get(Number(tripId))
|
||||
const optimistic: Trip = { ...(existing ?? {} as Trip), ...(data as Partial<Trip>), id: Number(tripId) }
|
||||
await offlineDb.trips.put(optimistic)
|
||||
await mutationQueue.enqueue({
|
||||
id: generateUUID(),
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}`,
|
||||
body: data as Record<string, unknown>,
|
||||
resource: 'trips',
|
||||
})
|
||||
return { trip: optimistic }
|
||||
}
|
||||
const result = await tripsApi.update(tripId, data as Record<string, unknown>)
|
||||
upsertTrip(result.trip)
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user