mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(bookings): bookings, transports and stays saved offline wait in the queue and sync once the connection is back
Every booking and stay write went straight to the API, so offline a new
booking failed with "Network Error" and was lost, and a status tick, the
travellers or a moved transport snapped back. Bookings (add, edit, status,
travellers, delete, day order) and stays (add, edit, delete) now go through
reservationRepo and accommodationRepo and the mutation queue like places and
costs: a temporary row offline, held behind an older queued write online,
since neither carries a version token. Desktop, the phone sheets, the day
plan, the road trip nights and the PDF all write and read through them.
The replay reads the booking from `reservation` only, so the travellers
answer ({ travelers, reservation }) lands on the booking it names. A booking
or its stay that names a place or stay created offline is sent with the real
id once that one has synced, inside create_accommodation too, so an imported
hotel reviewed offline can be linked to a place made for it.
What only works online says so instead of failing: an AirTrail import, a
booking file import, the AI retry and an attachment on a saved booking show
that they need a connection, and a save with newly picked files is stopped
with a hint, since an upload cannot wait in the queue.
Refs #2601
This commit is contained in:
@@ -15,7 +15,7 @@ import type { QueuedMutation } from '../db/offlineDb'
|
||||
import type { Table } from 'dexie'
|
||||
import { roadtripPreferencesResponseSchema, assignmentSchema, tourCreateResponseSchema, type TourCreateResponse } from '@trek/shared'
|
||||
import { cacheAssignment } from '../db/cacheAssignment'
|
||||
import type { BudgetItem, BudgetItemMember } from '../types'
|
||||
import type { BudgetItem, BudgetItemMember, Reservation } from '../types'
|
||||
|
||||
// Map Dexie table names used in `resource` field → actual Dexie tables.
|
||||
function getTable(resource: string): Table | undefined {
|
||||
@@ -25,6 +25,7 @@ function getTable(resource: string): Table | undefined {
|
||||
todoItems: offlineDb.todoItems,
|
||||
budgetItems: offlineDb.budgetItems,
|
||||
reservations: offlineDb.reservations,
|
||||
accommodations: offlineDb.accommodations,
|
||||
tripFiles: offlineDb.tripFiles,
|
||||
}
|
||||
return map[resource]
|
||||
@@ -219,8 +220,12 @@ async function remapTempId(mutation: QueuedMutation, realId: number, idMap: Map<
|
||||
}
|
||||
|
||||
// Body fields that can name an entity created offline earlier in the queue: an
|
||||
// expense saved from a place or a booking that has not synced yet.
|
||||
const TEMP_REF_FIELDS = ['place_id', 'reservation_id'] as const
|
||||
// expense saved from a place or a booking that has not synced yet, a booking
|
||||
// linked to such a place or stay.
|
||||
const TEMP_REF_FIELDS = ['place_id', 'reservation_id', 'accommodation_id'] as const
|
||||
// Objects inside a body that carry such fields of their own: the stay a hotel
|
||||
// booking creates alongside names its place.
|
||||
const TEMP_REF_OBJECTS = ['create_accommodation'] as const
|
||||
|
||||
/**
|
||||
* The body with every link to an offline-created entity pointed at the real id
|
||||
@@ -237,6 +242,13 @@ function remapBodyRefs(body: unknown, idMap: Map<number, number>): unknown {
|
||||
remapped ??= { ...fields }
|
||||
remapped[field] = realId
|
||||
}
|
||||
for (const key of TEMP_REF_OBJECTS) {
|
||||
const nested = fields[key]
|
||||
const next = remapBodyRefs(nested, idMap)
|
||||
if (next === nested) continue
|
||||
remapped ??= { ...fields }
|
||||
remapped[key] = next
|
||||
}
|
||||
return remapped ?? body
|
||||
}
|
||||
|
||||
@@ -259,6 +271,24 @@ async function reconcileBudgetItem(mutation: QueuedMutation, data: unknown, idMa
|
||||
await offlineDb.budgetItems.put({ ...local, ...item, ...(answer.members ? { members: answer.members } : {}) })
|
||||
}
|
||||
|
||||
/**
|
||||
* A replayed booking write. Only `reservation` names the booking: the travelers
|
||||
* route answers `{ travelers, reservation }`, which the first-value rule of the
|
||||
* generic branch would read as the traveler list. The travelers the answer
|
||||
* lists replace the local ones; a row that does not list them keeps them.
|
||||
*/
|
||||
async function reconcileReservation(mutation: QueuedMutation, data: unknown, idMap: Map<number, number>): Promise<void> {
|
||||
const answer = (data && typeof data === 'object' ? data : {}) as { reservation?: Reservation; travelers?: Reservation['travelers'] }
|
||||
const reservation = answer.reservation
|
||||
if (!reservation || typeof reservation.id !== 'number') return
|
||||
if (mutation.tempId !== undefined && mutation.tempId !== reservation.id) {
|
||||
await offlineDb.reservations.delete(mutation.tempId)
|
||||
await remapTempId(mutation, reservation.id, idMap)
|
||||
}
|
||||
const local = await offlineDb.reservations.get(reservation.id)
|
||||
await offlineDb.reservations.put({ ...local, ...reservation, ...(answer.travelers ? { travelers: answer.travelers } : {}) })
|
||||
}
|
||||
|
||||
/**
|
||||
* A replayed tour write. The answer names the tour by its place, so the
|
||||
* offline place and facet move from the temporary id to the real one. The
|
||||
@@ -497,6 +527,8 @@ export const mutationQueue = {
|
||||
await reconcileTour(mutation, tourCreateResponseSchema.parse(response.data), idMap)
|
||||
} else if (mutation.resource === 'budgetItems' && mutation.method !== 'DELETE') {
|
||||
await reconcileBudgetItem(mutation, response.data, idMap)
|
||||
} else if (mutation.resource === 'reservations' && mutation.method !== 'DELETE') {
|
||||
await reconcileReservation(mutation, response.data, idMap)
|
||||
} else if (mutation.method !== 'DELETE' && mutation.resource) {
|
||||
const table = getTable(mutation.resource)
|
||||
if (table && response.data && typeof response.data === 'object') {
|
||||
|
||||
Reference in New Issue
Block a user