mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-23 07:11:46 +00:00
Merge pull request #653 from mauriceboe/fix/pwa-offline-session-and-file-downloads
fix(offline): complete offline data coverage after initial PWA implementation
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
import Dexie, { type Table } from 'dexie';
|
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 ────────────────────────────────────────────────────────
|
// ── Queue + sync types ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -52,6 +57,10 @@ class TrekOfflineDb extends Dexie {
|
|||||||
budgetItems!: Table<BudgetItem, number>;
|
budgetItems!: Table<BudgetItem, number>;
|
||||||
reservations!: Table<Reservation, number>;
|
reservations!: Table<Reservation, number>;
|
||||||
tripFiles!: Table<TripFile, 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>;
|
mutationQueue!: Table<QueuedMutation, string>;
|
||||||
syncMeta!: Table<SyncMeta, number>;
|
syncMeta!: Table<SyncMeta, number>;
|
||||||
blobCache!: Table<BlobCacheEntry, string>;
|
blobCache!: Table<BlobCacheEntry, string>;
|
||||||
@@ -72,6 +81,13 @@ class TrekOfflineDb extends Dexie {
|
|||||||
syncMeta: 'tripId',
|
syncMeta: 'tripId',
|
||||||
blobCache: 'url, cachedAt',
|
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);
|
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> {
|
export async function upsertSyncMeta(meta: SyncMeta): Promise<void> {
|
||||||
await offlineDb.syncMeta.put(meta);
|
await offlineDb.syncMeta.put(meta);
|
||||||
}
|
}
|
||||||
@@ -129,6 +162,8 @@ export async function clearTripData(tripId: number): Promise<void> {
|
|||||||
offlineDb.budgetItems,
|
offlineDb.budgetItems,
|
||||||
offlineDb.reservations,
|
offlineDb.reservations,
|
||||||
offlineDb.tripFiles,
|
offlineDb.tripFiles,
|
||||||
|
offlineDb.accommodations,
|
||||||
|
offlineDb.tripMembers,
|
||||||
offlineDb.mutationQueue,
|
offlineDb.mutationQueue,
|
||||||
offlineDb.syncMeta,
|
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.budgetItems.where('trip_id').equals(tripId).delete();
|
||||||
await offlineDb.reservations.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.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.mutationQueue.where('tripId').equals(tripId).delete();
|
||||||
await offlineDb.syncMeta.where('tripId').equals(tripId).delete();
|
await offlineDb.syncMeta.where('tripId').equals(tripId).delete();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'الإشعارات',
|
'settings.tabs.notifications': 'الإشعارات',
|
||||||
'settings.tabs.integrations': 'التكاملات',
|
'settings.tabs.integrations': 'التكاملات',
|
||||||
'settings.tabs.account': 'الحساب',
|
'settings.tabs.account': 'الحساب',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'حول',
|
'settings.tabs.about': 'حول',
|
||||||
'settings.map': 'الخريطة',
|
'settings.map': 'الخريطة',
|
||||||
'settings.mapTemplate': 'قالب الخريطة',
|
'settings.mapTemplate': 'قالب الخريطة',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Notificações',
|
'settings.tabs.notifications': 'Notificações',
|
||||||
'settings.tabs.integrations': 'Integrações',
|
'settings.tabs.integrations': 'Integrações',
|
||||||
'settings.tabs.account': 'Conta',
|
'settings.tabs.account': 'Conta',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'Sobre',
|
'settings.tabs.about': 'Sobre',
|
||||||
'settings.map': 'Mapa',
|
'settings.map': 'Mapa',
|
||||||
'settings.mapTemplate': 'Modelo de mapa',
|
'settings.mapTemplate': 'Modelo de mapa',
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Oznámení',
|
'settings.tabs.notifications': 'Oznámení',
|
||||||
'settings.tabs.integrations': 'Integrace',
|
'settings.tabs.integrations': 'Integrace',
|
||||||
'settings.tabs.account': 'Účet',
|
'settings.tabs.account': 'Účet',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'O aplikaci',
|
'settings.tabs.about': 'O aplikaci',
|
||||||
'settings.map': 'Mapy',
|
'settings.map': 'Mapy',
|
||||||
'settings.mapTemplate': 'Šablona mapy',
|
'settings.mapTemplate': 'Šablona mapy',
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Benachrichtigungen',
|
'settings.tabs.notifications': 'Benachrichtigungen',
|
||||||
'settings.tabs.integrations': 'Integrationen',
|
'settings.tabs.integrations': 'Integrationen',
|
||||||
'settings.tabs.account': 'Konto',
|
'settings.tabs.account': 'Konto',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'Über',
|
'settings.tabs.about': 'Über',
|
||||||
'settings.map': 'Karte',
|
'settings.map': 'Karte',
|
||||||
'settings.mapTemplate': 'Karten-Vorlage',
|
'settings.mapTemplate': 'Karten-Vorlage',
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Notifications',
|
'settings.tabs.notifications': 'Notifications',
|
||||||
'settings.tabs.integrations': 'Integrations',
|
'settings.tabs.integrations': 'Integrations',
|
||||||
'settings.tabs.account': 'Account',
|
'settings.tabs.account': 'Account',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'About',
|
'settings.tabs.about': 'About',
|
||||||
'settings.map': 'Map',
|
'settings.map': 'Map',
|
||||||
'settings.mapTemplate': 'Map Template',
|
'settings.mapTemplate': 'Map Template',
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ const es: Record<string, string> = {
|
|||||||
'settings.tabs.notifications': 'Notificaciones',
|
'settings.tabs.notifications': 'Notificaciones',
|
||||||
'settings.tabs.integrations': 'Integraciones',
|
'settings.tabs.integrations': 'Integraciones',
|
||||||
'settings.tabs.account': 'Cuenta',
|
'settings.tabs.account': 'Cuenta',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'Acerca de',
|
'settings.tabs.about': 'Acerca de',
|
||||||
'settings.map': 'Mapa',
|
'settings.map': 'Mapa',
|
||||||
'settings.mapTemplate': 'Plantilla del mapa',
|
'settings.mapTemplate': 'Plantilla del mapa',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const fr: Record<string, string> = {
|
|||||||
'settings.tabs.notifications': 'Notifications',
|
'settings.tabs.notifications': 'Notifications',
|
||||||
'settings.tabs.integrations': 'Intégrations',
|
'settings.tabs.integrations': 'Intégrations',
|
||||||
'settings.tabs.account': 'Compte',
|
'settings.tabs.account': 'Compte',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'À propos',
|
'settings.tabs.about': 'À propos',
|
||||||
'settings.map': 'Carte',
|
'settings.map': 'Carte',
|
||||||
'settings.mapTemplate': 'Modèle de carte',
|
'settings.mapTemplate': 'Modèle de carte',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Értesítések',
|
'settings.tabs.notifications': 'Értesítések',
|
||||||
'settings.tabs.integrations': 'Integrációk',
|
'settings.tabs.integrations': 'Integrációk',
|
||||||
'settings.tabs.account': 'Fiók',
|
'settings.tabs.account': 'Fiók',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'Névjegy',
|
'settings.tabs.about': 'Névjegy',
|
||||||
'settings.map': 'Térkép',
|
'settings.map': 'Térkép',
|
||||||
'settings.mapTemplate': 'Térkép sablon',
|
'settings.mapTemplate': 'Térkép sablon',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Notifiche',
|
'settings.tabs.notifications': 'Notifiche',
|
||||||
'settings.tabs.integrations': 'Integrazioni',
|
'settings.tabs.integrations': 'Integrazioni',
|
||||||
'settings.tabs.account': 'Account',
|
'settings.tabs.account': 'Account',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'Informazioni',
|
'settings.tabs.about': 'Informazioni',
|
||||||
'settings.map': 'Mappa',
|
'settings.map': 'Mappa',
|
||||||
'settings.mapTemplate': 'Modello Mappa',
|
'settings.mapTemplate': 'Modello Mappa',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const nl: Record<string, string> = {
|
|||||||
'settings.tabs.notifications': 'Meldingen',
|
'settings.tabs.notifications': 'Meldingen',
|
||||||
'settings.tabs.integrations': 'Integraties',
|
'settings.tabs.integrations': 'Integraties',
|
||||||
'settings.tabs.account': 'Account',
|
'settings.tabs.account': 'Account',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'Over',
|
'settings.tabs.about': 'Over',
|
||||||
'settings.map': 'Kaart',
|
'settings.map': 'Kaart',
|
||||||
'settings.mapTemplate': 'Kaartsjabloon',
|
'settings.mapTemplate': 'Kaartsjabloon',
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.tabs.notifications': 'Powiadomienia',
|
'settings.tabs.notifications': 'Powiadomienia',
|
||||||
'settings.tabs.integrations': 'Integracje',
|
'settings.tabs.integrations': 'Integracje',
|
||||||
'settings.tabs.account': 'Konto',
|
'settings.tabs.account': 'Konto',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'O aplikacji',
|
'settings.tabs.about': 'O aplikacji',
|
||||||
'settings.map': 'Mapa',
|
'settings.map': 'Mapa',
|
||||||
'settings.mapTemplate': 'Szablon mapy',
|
'settings.mapTemplate': 'Szablon mapy',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const ru: Record<string, string> = {
|
|||||||
'settings.tabs.notifications': 'Уведомления',
|
'settings.tabs.notifications': 'Уведомления',
|
||||||
'settings.tabs.integrations': 'Интеграции',
|
'settings.tabs.integrations': 'Интеграции',
|
||||||
'settings.tabs.account': 'Аккаунт',
|
'settings.tabs.account': 'Аккаунт',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': 'О приложении',
|
'settings.tabs.about': 'О приложении',
|
||||||
'settings.map': 'Карта',
|
'settings.map': 'Карта',
|
||||||
'settings.mapTemplate': 'Шаблон карты',
|
'settings.mapTemplate': 'Шаблон карты',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const zh: Record<string, string> = {
|
|||||||
'settings.tabs.notifications': '通知',
|
'settings.tabs.notifications': '通知',
|
||||||
'settings.tabs.integrations': '集成',
|
'settings.tabs.integrations': '集成',
|
||||||
'settings.tabs.account': '账户',
|
'settings.tabs.account': '账户',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': '关于',
|
'settings.tabs.about': '关于',
|
||||||
'settings.map': '地图',
|
'settings.map': '地图',
|
||||||
'settings.mapTemplate': '地图模板',
|
'settings.mapTemplate': '地图模板',
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const zhTw: Record<string, string> = {
|
|||||||
'settings.tabs.notifications': '通知',
|
'settings.tabs.notifications': '通知',
|
||||||
'settings.tabs.integrations': '整合',
|
'settings.tabs.integrations': '整合',
|
||||||
'settings.tabs.account': '帳戶',
|
'settings.tabs.account': '帳戶',
|
||||||
|
'settings.tabs.offline': 'Offline',
|
||||||
'settings.tabs.about': '關於',
|
'settings.tabs.about': '關於',
|
||||||
'settings.map': '地圖',
|
'settings.map': '地圖',
|
||||||
'settings.mapTemplate': '地圖模板',
|
'settings.mapTemplate': '地圖模板',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react'
|
import React, { useEffect, useState, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { tripsApi } from '../api/client'
|
import { tripsApi } from '../api/client'
|
||||||
|
import { tripRepo } from '../repo/tripRepo'
|
||||||
import { useAuthStore } from '../store/authStore'
|
import { useAuthStore } from '../store/authStore'
|
||||||
import { useSettingsStore } from '../store/settingsStore'
|
import { useSettingsStore } from '../store/settingsStore'
|
||||||
import { useTranslation } from '../i18n'
|
import { useTranslation } from '../i18n'
|
||||||
@@ -713,12 +714,9 @@ export default function DashboardPage(): React.ReactElement {
|
|||||||
const loadTrips = async () => {
|
const loadTrips = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const [active, archived] = await Promise.all([
|
const { trips, archivedTrips } = await tripRepo.list()
|
||||||
tripsApi.list(),
|
setTrips(sortTrips(trips))
|
||||||
tripsApi.list({ archived: 1 }),
|
setArchivedTrips(sortTrips(archivedTrips))
|
||||||
])
|
|
||||||
setTrips(sortTrips(active.trips))
|
|
||||||
setArchivedTrips(sortTrips(archived.trips))
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('dashboard.toast.loadError'))
|
toast.error(t('dashboard.toast.loadError'))
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||||
import { useTripStore } from '../store/tripStore'
|
import { useTripStore } from '../store/tripStore'
|
||||||
import { tripsApi, placesApi } from '../api/client'
|
import { tripRepo } from '../repo/tripRepo'
|
||||||
|
import { placeRepo } from '../repo/placeRepo'
|
||||||
import Navbar from '../components/Layout/Navbar'
|
import Navbar from '../components/Layout/Navbar'
|
||||||
import FileManager from '../components/Files/FileManager'
|
import FileManager from '../components/Files/FileManager'
|
||||||
import { ArrowLeft } from 'lucide-react'
|
import { ArrowLeft } from 'lucide-react'
|
||||||
@@ -27,8 +28,8 @@ export default function FilesPage(): React.ReactElement {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const [tripData, placesData] = await Promise.all([
|
const [tripData, placesData] = await Promise.all([
|
||||||
tripsApi.get(tripId),
|
tripRepo.get(tripId),
|
||||||
placesApi.list(tripId),
|
placeRepo.list(tripId),
|
||||||
])
|
])
|
||||||
setTrip(tripData.trip)
|
setTrip(tripData.trip)
|
||||||
setPlaces(placesData.places)
|
setPlaces(placesData.places)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
{ id: 'map', label: t('settings.tabs.map') },
|
{ id: 'map', label: t('settings.tabs.map') },
|
||||||
{ id: 'notifications', label: t('settings.tabs.notifications') },
|
{ id: 'notifications', label: t('settings.tabs.notifications') },
|
||||||
...(hasIntegrations ? [{ id: 'integrations', label: t('settings.tabs.integrations') }] : []),
|
...(hasIntegrations ? [{ id: 'integrations', label: t('settings.tabs.integrations') }] : []),
|
||||||
{ id: 'offline', label: t('settings.tabs.offline', 'Offline') },
|
{ id: 'offline', label: t('settings.tabs.offline') },
|
||||||
{ id: 'account', label: t('settings.tabs.account') },
|
{ id: 'account', label: t('settings.tabs.account') },
|
||||||
...(appVersion ? [{ id: 'about', label: t('settings.tabs.about') }] : []),
|
...(appVersion ? [{ id: 'about', label: t('settings.tabs.about') }] : []),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Ticket, PackageCheck, Wallet, FolderOpen, Users } from 'lucide-react'
|
||||||
import { useTranslation } from '../i18n'
|
import { useTranslation } from '../i18n'
|
||||||
import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi, mapsApi } from '../api/client'
|
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 ConfirmDialog from '../components/shared/ConfirmDialog'
|
||||||
import { useResizablePanels } from '../hooks/useResizablePanels'
|
import { useResizablePanels } from '../hooks/useResizablePanels'
|
||||||
import { useTripWebSocket } from '../hooks/useTripWebSocket'
|
import { useTripWebSocket } from '../hooks/useTripWebSocket'
|
||||||
@@ -104,7 +106,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
|||||||
|
|
||||||
const loadAccommodations = useCallback(() => {
|
const loadAccommodations = useCallback(() => {
|
||||||
if (tripId) {
|
if (tripId) {
|
||||||
accommodationsApi.list(tripId).then(d => setTripAccommodations(d.accommodations || [])).catch(() => {})
|
accommodationRepo.list(tripId).then(d => setTripAccommodations(d.accommodations || [])).catch(() => {})
|
||||||
tripActions.loadReservations(tripId)
|
tripActions.loadReservations(tripId)
|
||||||
}
|
}
|
||||||
}, [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.loadTrip(tripId).catch(() => { toast.error(t('trip.toast.loadError')); navigate('/dashboard') })
|
||||||
tripActions.loadFiles(tripId)
|
tripActions.loadFiles(tripId)
|
||||||
loadAccommodations()
|
loadAccommodations()
|
||||||
tripsApi.getMembers(tripId).then(d => {
|
if (!navigator.onLine) {
|
||||||
// Combine owner + members into one list
|
offlineDb.tripMembers.where('tripId').equals(Number(tripId)).toArray()
|
||||||
const all = [d.owner, ...(d.members || [])].filter(Boolean)
|
.then(rows => setTripMembers(rows))
|
||||||
setTripMembers(all)
|
.catch(() => {})
|
||||||
}).catch(() => {})
|
} else {
|
||||||
|
tripsApi.getMembers(tripId).then(d => {
|
||||||
|
const all = [d.owner, ...(d.members || [])].filter(Boolean)
|
||||||
|
setTripMembers(all)
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [tripId])
|
}, [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
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -3,6 +3,23 @@ import { offlineDb, upsertTrip } from '../db/offlineDb'
|
|||||||
import type { Trip } from '../types'
|
import type { Trip } from '../types'
|
||||||
|
|
||||||
export const tripRepo = {
|
export const tripRepo = {
|
||||||
|
async list(): Promise<{ trips: Trip[]; archivedTrips: Trip[] }> {
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
const all = await offlineDb.trips.toArray()
|
||||||
|
return {
|
||||||
|
trips: all.filter(t => !t.is_archived),
|
||||||
|
archivedTrips: all.filter(t => t.is_archived),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [active, archived] = await Promise.all([
|
||||||
|
tripsApi.list(),
|
||||||
|
tripsApi.list({ archived: 1 }),
|
||||||
|
])
|
||||||
|
active.trips.forEach(t => upsertTrip(t))
|
||||||
|
archived.trips.forEach(t => upsertTrip(t))
|
||||||
|
return { trips: active.trips, archivedTrips: archived.trips }
|
||||||
|
},
|
||||||
|
|
||||||
async get(tripId: number | string): Promise<{ trip: Trip }> {
|
async get(tripId: number | string): Promise<{ trip: Trip }> {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
const cached = await offlineDb.trips.get(Number(tripId))
|
const cached = await offlineDb.trips.get(Number(tripId))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { budgetApi } from '../../api/client'
|
import { budgetApi } from '../../api/client'
|
||||||
|
import { budgetRepo } from '../../repo/budgetRepo'
|
||||||
import type { StoreApi } from 'zustand'
|
import type { StoreApi } from 'zustand'
|
||||||
import type { TripStoreState } from '../tripStore'
|
import type { TripStoreState } from '../tripStore'
|
||||||
import type { BudgetItem, BudgetMember } from '../../types'
|
import type { BudgetItem, BudgetMember } from '../../types'
|
||||||
@@ -21,7 +22,7 @@ export interface BudgetSlice {
|
|||||||
export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice => ({
|
export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice => ({
|
||||||
loadBudgetItems: async (tripId) => {
|
loadBudgetItems: async (tripId) => {
|
||||||
try {
|
try {
|
||||||
const data = await budgetApi.list(tripId)
|
const data = await budgetRepo.list(tripId)
|
||||||
set({ budgetItems: data.items })
|
set({ budgetItems: data.items })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Failed to load budget items:', err)
|
console.error('Failed to load budget items:', err)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { filesApi } from '../../api/client'
|
import { filesApi } from '../../api/client'
|
||||||
|
import { fileRepo } from '../../repo/fileRepo'
|
||||||
import type { StoreApi } from 'zustand'
|
import type { StoreApi } from 'zustand'
|
||||||
import type { TripStoreState } from '../tripStore'
|
import type { TripStoreState } from '../tripStore'
|
||||||
import type { TripFile } from '../../types'
|
import type { TripFile } from '../../types'
|
||||||
@@ -16,7 +17,7 @@ export interface FilesSlice {
|
|||||||
export const createFilesSlice = (set: SetState, get: GetState): FilesSlice => ({
|
export const createFilesSlice = (set: SetState, get: GetState): FilesSlice => ({
|
||||||
loadFiles: async (tripId) => {
|
loadFiles: async (tripId) => {
|
||||||
try {
|
try {
|
||||||
const data = await filesApi.list(tripId)
|
const data = await fileRepo.list(tripId)
|
||||||
set({ files: data.files })
|
set({ files: data.files })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Failed to load files:', err)
|
console.error('Failed to load files:', err)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { reservationsApi } from '../../api/client'
|
import { reservationsApi } from '../../api/client'
|
||||||
|
import { reservationRepo } from '../../repo/reservationRepo'
|
||||||
import type { StoreApi } from 'zustand'
|
import type { StoreApi } from 'zustand'
|
||||||
import type { TripStoreState } from '../tripStore'
|
import type { TripStoreState } from '../tripStore'
|
||||||
import type { Reservation } from '../../types'
|
import type { Reservation } from '../../types'
|
||||||
@@ -18,7 +19,7 @@ export interface ReservationsSlice {
|
|||||||
export const createReservationsSlice = (set: SetState, get: GetState): ReservationsSlice => ({
|
export const createReservationsSlice = (set: SetState, get: GetState): ReservationsSlice => ({
|
||||||
loadReservations: async (tripId) => {
|
loadReservations: async (tripId) => {
|
||||||
try {
|
try {
|
||||||
const data = await reservationsApi.list(tripId)
|
const data = await reservationRepo.list(tripId)
|
||||||
set({ reservations: data.reservations })
|
set({ reservations: data.reservations })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Failed to load reservations:', err)
|
console.error('Failed to load reservations:', err)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { StoreApi } from 'zustand'
|
import type { StoreApi } from 'zustand'
|
||||||
import { tripsApi, tagsApi, categoriesApi } from '../api/client'
|
import { tripsApi, tagsApi, categoriesApi } from '../api/client'
|
||||||
|
import { offlineDb } from '../db/offlineDb'
|
||||||
import { tripRepo } from '../repo/tripRepo'
|
import { tripRepo } from '../repo/tripRepo'
|
||||||
import { dayRepo } from '../repo/dayRepo'
|
import { dayRepo } from '../repo/dayRepo'
|
||||||
import { placeRepo } from '../repo/placeRepo'
|
import { placeRepo } from '../repo/placeRepo'
|
||||||
@@ -94,8 +95,12 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
|
|||||||
placeRepo.list(tripId),
|
placeRepo.list(tripId),
|
||||||
packingRepo.list(tripId),
|
packingRepo.list(tripId),
|
||||||
todoRepo.list(tripId),
|
todoRepo.list(tripId),
|
||||||
tagsApi.list().catch(() => ({ tags: [] })),
|
navigator.onLine
|
||||||
categoriesApi.list().catch(() => ({ categories: [] })),
|
? 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 = {}
|
const assignmentsMap: AssignmentsMap = {}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* - trip list refresh (DashboardPage)
|
* - trip list refresh (DashboardPage)
|
||||||
* - WS reconnect (phase 7)
|
* - WS reconnect (phase 7)
|
||||||
*/
|
*/
|
||||||
import { tripsApi } from '../api/client'
|
import { tripsApi, tagsApi, categoriesApi } from '../api/client'
|
||||||
import {
|
import {
|
||||||
offlineDb,
|
offlineDb,
|
||||||
upsertTrip,
|
upsertTrip,
|
||||||
@@ -21,12 +21,16 @@ import {
|
|||||||
upsertBudgetItems,
|
upsertBudgetItems,
|
||||||
upsertReservations,
|
upsertReservations,
|
||||||
upsertTripFiles,
|
upsertTripFiles,
|
||||||
|
upsertAccommodations,
|
||||||
|
upsertTripMembers,
|
||||||
|
upsertTags,
|
||||||
|
upsertCategories,
|
||||||
upsertSyncMeta,
|
upsertSyncMeta,
|
||||||
clearTripData,
|
clearTripData,
|
||||||
} from '../db/offlineDb'
|
} from '../db/offlineDb'
|
||||||
import { prefetchTilesForTrip } from './tilePrefetcher'
|
import { prefetchTilesForTrip } from './tilePrefetcher'
|
||||||
import { useSettingsStore } from '../store/settingsStore'
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -39,6 +43,8 @@ interface TripBundle {
|
|||||||
budgetItems: BudgetItem[]
|
budgetItems: BudgetItem[]
|
||||||
reservations: Reservation[]
|
reservations: Reservation[]
|
||||||
files: TripFile[]
|
files: TripFile[]
|
||||||
|
accommodations: Accommodation[]
|
||||||
|
members: TripMember[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
@@ -77,6 +83,8 @@ async function syncTrip(tripId: number): Promise<void> {
|
|||||||
await upsertBudgetItems(bundle.budgetItems)
|
await upsertBudgetItems(bundle.budgetItems)
|
||||||
await upsertReservations(bundle.reservations)
|
await upsertReservations(bundle.reservations)
|
||||||
await upsertTripFiles(bundle.files)
|
await upsertTripFiles(bundle.files)
|
||||||
|
await upsertAccommodations(bundle.accommodations || [])
|
||||||
|
await upsertTripMembers(tripId, bundle.members || [])
|
||||||
await upsertSyncMeta({
|
await upsertSyncMeta({
|
||||||
tripId,
|
tripId,
|
||||||
lastSyncedAt: Date.now(),
|
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)
|
// Cache file blobs + map tiles in background (don't block syncAll)
|
||||||
const tileUrl = useSettingsStore.getState().settings.map_tile_url || undefined
|
const tileUrl = useSettingsStore.getState().settings.map_tile_url || undefined
|
||||||
for (const trip of toSync) {
|
for (const trip of toSync) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
TRIP_SELECT,
|
TRIP_SELECT,
|
||||||
} from '../services/tripService';
|
} from '../services/tripService';
|
||||||
import { listDays } from '../services/dayService';
|
import { listDays, listAccommodations } from '../services/dayService';
|
||||||
import { listPlaces } from '../services/placeService';
|
import { listPlaces } from '../services/placeService';
|
||||||
import { listItems as listPackingItems } from '../services/packingService';
|
import { listItems as listPackingItems } from '../services/packingService';
|
||||||
import { listItems as listTodoItems } from '../services/todoService';
|
import { listItems as listTodoItems } from '../services/todoService';
|
||||||
@@ -318,6 +318,9 @@ router.get('/:id/bundle', authenticate, (req: Request, res: Response) => {
|
|||||||
const budgetItems = listBudgetItems(tripId);
|
const budgetItems = listBudgetItems(tripId);
|
||||||
const reservations = listReservations(tripId);
|
const reservations = listReservations(tripId);
|
||||||
const files = listFiles(tripId, false);
|
const files = listFiles(tripId, false);
|
||||||
|
const accommodations = listAccommodations(tripId);
|
||||||
|
const { owner, members } = listMembers(tripId, trip.user_id);
|
||||||
|
const allMembers = [owner, ...(members || [])].filter(Boolean);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
trip,
|
trip,
|
||||||
@@ -328,6 +331,8 @@ router.get('/:id/bundle', authenticate, (req: Request, res: Response) => {
|
|||||||
budgetItems,
|
budgetItems,
|
||||||
reservations,
|
reservations,
|
||||||
files,
|
files,
|
||||||
|
accommodations,
|
||||||
|
members: allMembers,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user