mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(offline): cache accommodations, trip members, tags, and categories for full offline support
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import type { Trip, Day, Place, PackingItem, TodoItem, BudgetItem, Reservation, TripFile } from '../types';
|
||||
import type { Trip, Day, Place, PackingItem, TodoItem, BudgetItem, Reservation, TripFile, Accommodation, TripMember, Tag, Category } from '../types';
|
||||
|
||||
/** TripMember enriched with tripId so we can index by trip. */
|
||||
export interface CachedTripMember extends TripMember {
|
||||
tripId: number;
|
||||
}
|
||||
|
||||
// ── Queue + sync types ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -52,6 +57,10 @@ class TrekOfflineDb extends Dexie {
|
||||
budgetItems!: Table<BudgetItem, number>;
|
||||
reservations!: Table<Reservation, number>;
|
||||
tripFiles!: Table<TripFile, number>;
|
||||
accommodations!: Table<Accommodation, number>;
|
||||
tripMembers!: Table<CachedTripMember, [number, number]>;
|
||||
tags!: Table<Tag, number>;
|
||||
categories!: Table<Category, number>;
|
||||
mutationQueue!: Table<QueuedMutation, string>;
|
||||
syncMeta!: Table<SyncMeta, number>;
|
||||
blobCache!: Table<BlobCacheEntry, string>;
|
||||
@@ -72,6 +81,13 @@ class TrekOfflineDb extends Dexie {
|
||||
syncMeta: 'tripId',
|
||||
blobCache: 'url, cachedAt',
|
||||
});
|
||||
|
||||
this.version(2).stores({
|
||||
accommodations: 'id, trip_id',
|
||||
tripMembers: '[tripId+id], tripId',
|
||||
tags: 'id',
|
||||
categories: 'id',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +127,23 @@ export async function upsertTripFiles(files: TripFile[]): Promise<void> {
|
||||
await offlineDb.tripFiles.bulkPut(files);
|
||||
}
|
||||
|
||||
export async function upsertAccommodations(items: Accommodation[]): Promise<void> {
|
||||
await offlineDb.accommodations.bulkPut(items);
|
||||
}
|
||||
|
||||
export async function upsertTripMembers(tripId: number, members: TripMember[]): Promise<void> {
|
||||
const rows: CachedTripMember[] = members.map(m => ({ ...m, tripId }));
|
||||
await offlineDb.tripMembers.bulkPut(rows);
|
||||
}
|
||||
|
||||
export async function upsertTags(tags: Tag[]): Promise<void> {
|
||||
await offlineDb.tags.bulkPut(tags);
|
||||
}
|
||||
|
||||
export async function upsertCategories(categories: Category[]): Promise<void> {
|
||||
await offlineDb.categories.bulkPut(categories);
|
||||
}
|
||||
|
||||
export async function upsertSyncMeta(meta: SyncMeta): Promise<void> {
|
||||
await offlineDb.syncMeta.put(meta);
|
||||
}
|
||||
@@ -129,6 +162,8 @@ export async function clearTripData(tripId: number): Promise<void> {
|
||||
offlineDb.budgetItems,
|
||||
offlineDb.reservations,
|
||||
offlineDb.tripFiles,
|
||||
offlineDb.accommodations,
|
||||
offlineDb.tripMembers,
|
||||
offlineDb.mutationQueue,
|
||||
offlineDb.syncMeta,
|
||||
],
|
||||
@@ -140,6 +175,8 @@ export async function clearTripData(tripId: number): Promise<void> {
|
||||
await offlineDb.budgetItems.where('trip_id').equals(tripId).delete();
|
||||
await offlineDb.reservations.where('trip_id').equals(tripId).delete();
|
||||
await offlineDb.tripFiles.where('trip_id').equals(tripId).delete();
|
||||
await offlineDb.accommodations.where('trip_id').equals(tripId).delete();
|
||||
await offlineDb.tripMembers.where('tripId').equals(tripId).delete();
|
||||
await offlineDb.mutationQueue.where('tripId').equals(tripId).delete();
|
||||
await offlineDb.syncMeta.where('tripId').equals(tripId).delete();
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ import { useToast } from '../components/shared/Toast'
|
||||
import { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Ticket, PackageCheck, Wallet, FolderOpen, Users } from 'lucide-react'
|
||||
import { useTranslation } from '../i18n'
|
||||
import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi, mapsApi } from '../api/client'
|
||||
import { accommodationRepo } from '../repo/accommodationRepo'
|
||||
import { offlineDb } from '../db/offlineDb'
|
||||
import ConfirmDialog from '../components/shared/ConfirmDialog'
|
||||
import { useResizablePanels } from '../hooks/useResizablePanels'
|
||||
import { useTripWebSocket } from '../hooks/useTripWebSocket'
|
||||
@@ -104,7 +106,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
||||
|
||||
const loadAccommodations = useCallback(() => {
|
||||
if (tripId) {
|
||||
accommodationsApi.list(tripId).then(d => setTripAccommodations(d.accommodations || [])).catch(() => {})
|
||||
accommodationRepo.list(tripId).then(d => setTripAccommodations(d.accommodations || [])).catch(() => {})
|
||||
tripActions.loadReservations(tripId)
|
||||
}
|
||||
}, [tripId])
|
||||
@@ -192,11 +194,16 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
||||
tripActions.loadTrip(tripId).catch(() => { toast.error(t('trip.toast.loadError')); navigate('/dashboard') })
|
||||
tripActions.loadFiles(tripId)
|
||||
loadAccommodations()
|
||||
tripsApi.getMembers(tripId).then(d => {
|
||||
// Combine owner + members into one list
|
||||
const all = [d.owner, ...(d.members || [])].filter(Boolean)
|
||||
setTripMembers(all)
|
||||
}).catch(() => {})
|
||||
if (!navigator.onLine) {
|
||||
offlineDb.tripMembers.where('tripId').equals(Number(tripId)).toArray()
|
||||
.then(rows => setTripMembers(rows))
|
||||
.catch(() => {})
|
||||
} else {
|
||||
tripsApi.getMembers(tripId).then(d => {
|
||||
const all = [d.owner, ...(d.members || [])].filter(Boolean)
|
||||
setTripMembers(all)
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}, [tripId])
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { accommodationsApi } from '../api/client'
|
||||
import { offlineDb, upsertAccommodations } from '../db/offlineDb'
|
||||
import type { Accommodation } from '../types'
|
||||
|
||||
export const accommodationRepo = {
|
||||
async list(tripId: number | string): Promise<{ accommodations: Accommodation[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const accommodations = await offlineDb.accommodations
|
||||
.where('trip_id').equals(Number(tripId)).toArray()
|
||||
return { accommodations }
|
||||
}
|
||||
const result = await accommodationsApi.list(tripId)
|
||||
upsertAccommodations(result.accommodations || []).catch(() => {})
|
||||
return result
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
import type { StoreApi } from 'zustand'
|
||||
import { tripsApi, tagsApi, categoriesApi } from '../api/client'
|
||||
import { offlineDb } from '../db/offlineDb'
|
||||
import { tripRepo } from '../repo/tripRepo'
|
||||
import { dayRepo } from '../repo/dayRepo'
|
||||
import { placeRepo } from '../repo/placeRepo'
|
||||
@@ -94,8 +95,12 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
|
||||
placeRepo.list(tripId),
|
||||
packingRepo.list(tripId),
|
||||
todoRepo.list(tripId),
|
||||
tagsApi.list().catch(() => ({ tags: [] })),
|
||||
categoriesApi.list().catch(() => ({ categories: [] })),
|
||||
navigator.onLine
|
||||
? tagsApi.list().catch(() => offlineDb.tags.toArray().then(tags => ({ tags })))
|
||||
: offlineDb.tags.toArray().then(tags => ({ tags })),
|
||||
navigator.onLine
|
||||
? categoriesApi.list().catch(() => offlineDb.categories.toArray().then(categories => ({ categories })))
|
||||
: offlineDb.categories.toArray().then(categories => ({ categories })),
|
||||
])
|
||||
|
||||
const assignmentsMap: AssignmentsMap = {}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - trip list refresh (DashboardPage)
|
||||
* - WS reconnect (phase 7)
|
||||
*/
|
||||
import { tripsApi } from '../api/client'
|
||||
import { tripsApi, tagsApi, categoriesApi } from '../api/client'
|
||||
import {
|
||||
offlineDb,
|
||||
upsertTrip,
|
||||
@@ -21,12 +21,16 @@ import {
|
||||
upsertBudgetItems,
|
||||
upsertReservations,
|
||||
upsertTripFiles,
|
||||
upsertAccommodations,
|
||||
upsertTripMembers,
|
||||
upsertTags,
|
||||
upsertCategories,
|
||||
upsertSyncMeta,
|
||||
clearTripData,
|
||||
} from '../db/offlineDb'
|
||||
import { prefetchTilesForTrip } from './tilePrefetcher'
|
||||
import { useSettingsStore } from '../store/settingsStore'
|
||||
import type { Trip, Day, Place, PackingItem, TodoItem, BudgetItem, Reservation, TripFile } from '../types'
|
||||
import type { Trip, Day, Place, PackingItem, TodoItem, BudgetItem, Reservation, TripFile, Accommodation, TripMember } from '../types'
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -39,6 +43,8 @@ interface TripBundle {
|
||||
budgetItems: BudgetItem[]
|
||||
reservations: Reservation[]
|
||||
files: TripFile[]
|
||||
accommodations: Accommodation[]
|
||||
members: TripMember[]
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -77,6 +83,8 @@ async function syncTrip(tripId: number): Promise<void> {
|
||||
await upsertBudgetItems(bundle.budgetItems)
|
||||
await upsertReservations(bundle.reservations)
|
||||
await upsertTripFiles(bundle.files)
|
||||
await upsertAccommodations(bundle.accommodations || [])
|
||||
await upsertTripMembers(tripId, bundle.members || [])
|
||||
await upsertSyncMeta({
|
||||
tripId,
|
||||
lastSyncedAt: Date.now(),
|
||||
@@ -145,6 +153,10 @@ export const tripSyncManager = {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache global user data (tags + categories) — fire-and-forget
|
||||
tagsApi.list().then(d => upsertTags(d.tags)).catch(() => {})
|
||||
categoriesApi.list().then(d => upsertCategories(d.categories)).catch(() => {})
|
||||
|
||||
// Cache file blobs + map tiles in background (don't block syncAll)
|
||||
const tileUrl = useSettingsStore.getState().settings.map_tile_url || undefined
|
||||
for (const trip of toSync) {
|
||||
|
||||
Reference in New Issue
Block a user