From 83de05505c11080549543efd64840d1baabc1198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20S=C3=B6derberg?= Date: Wed, 18 Mar 2026 14:06:59 +0100 Subject: [PATCH] Fixed offline queue caching --- app/admin/useAdminState.ts | 106 ++++++++++-------- ...aberge.pdf => Badplatser på Kullaberg.pdf} | Bin 2 files changed, 58 insertions(+), 48 deletions(-) rename public/files/{Badplatser på Kullaberge.pdf => Badplatser på Kullaberg.pdf} (100%) diff --git a/app/admin/useAdminState.ts b/app/admin/useAdminState.ts index 036d416..bdd686f 100644 --- a/app/admin/useAdminState.ts +++ b/app/admin/useAdminState.ts @@ -36,6 +36,24 @@ export const useAdminState = () => { return { loadedPeriods, loadedAttendance }; }; + const applyQueueToAttendance = async (baseAttendance: AttendanceDataMap) => { + const queue = await localforage.getItem('sync-queue') || []; + const nextAttendance = { ...baseAttendance }; + + for (const action of queue) { + if (action.type === 'SET_ATTENDANCE') { + const { date, youthId, shiftId, hoursWorked, weightedHours, status, note } = action.payload; + nextAttendance[getAttendanceKey(date, youthId, shiftId)] = { + date, youthId, shiftId, hoursWorked, weightedHours, status, note + }; + } else if (action.type === 'REMOVE_ATTENDANCE') { + const { date, youthId, shiftId } = action.payload; + delete nextAttendance[getAttendanceKey(date, youthId, shiftId)]; + } + } + return nextAttendance; + }; + // --- 2. OFFLINE QUEUE FLUSHER --- const flushOfflineQueue = useCallback(async () => { try { @@ -43,10 +61,8 @@ export const useAdminState = () => { if (queue.length > 0) { console.log(`Flushing ${queue.length} items from offline queue...`, queue); await syncOfflineQueueDb(queue); - - // ONLY clear the queue if the server action succeeds without throwing an error await localforage.setItem('sync-queue', []); - return true; // Indicates we successfully synced something + return true; } } catch (error) { console.error("Server sync failed, keeping items in offline queue for later.", error); @@ -57,7 +73,6 @@ export const useAdminState = () => { // --- 3. MAIN DATA FETCHING --- const refreshData = useCallback(async (isInitialLoad = false) => { try { - // First, check if we are online and have a backlog to sync before pulling fresh data if (navigator.onLine) { await flushOfflineQueue(); } @@ -67,10 +82,12 @@ export const useAdminState = () => { setIsOffline(false); const { loadedPeriods, loadedAttendance } = mapDbDataToUI(dbPeriods); - setPeriods(loadedPeriods); - setAttendance(loadedAttendance); - // Auto-select the active period if it's the initial load + const finalAttendance = await applyQueueToAttendance(loadedAttendance); + + setPeriods(loadedPeriods); + setAttendance(finalAttendance); + if (isInitialLoad && loadedPeriods.length > 0) { const today = toIsoDate(new Date()); const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate); @@ -88,8 +105,11 @@ export const useAdminState = () => { if (cachedData) { const { loadedPeriods, loadedAttendance } = mapDbDataToUI(cachedData); + const finalAttendance = await applyQueueToAttendance(loadedAttendance); + setPeriods(loadedPeriods); - setAttendance(loadedAttendance); + setAttendance(finalAttendance); + if (isInitialLoad && loadedPeriods.length > 0) setActivePeriodId(loadedPeriods[0].id); } } finally { @@ -97,18 +117,15 @@ export const useAdminState = () => { } }, [flushOfflineQueue]); - // Initial load useEffect(() => { refreshData(true); }, [refreshData]); - // --- 4. OFFLINE/ONLINE EVENT LISTENERS --- useEffect(() => { const handleOnline = async () => { setIsOffline(false); const didSync = await flushOfflineQueue(); if (didSync) { - // If we successfully pushed old data, pull the fresh DB state so the UI updates refreshData(); } }; @@ -122,11 +139,18 @@ export const useAdminState = () => { }; }, [flushOfflineQueue, refreshData]); - // Helper to add actions to local queue const addToOfflineQueue = async (action: any) => { const queue = await localforage.getItem('sync-queue') || []; - queue.push(action); - await localforage.setItem('sync-queue', queue); + + const filteredQueue = queue.filter(q => + !(q.payload.date === action.payload.date && + q.payload.youthId === action.payload.youthId && + q.payload.shiftId === action.payload.shiftId) + ); + + filteredQueue.push(action); + await localforage.setItem('sync-queue', filteredQueue); + setIsOffline(true); }; // --- 5. DATABASE WRITE ACTIONS --- @@ -168,7 +192,6 @@ export const useAdminState = () => { let hoursWorked = status === 'Late' || status === 'Present' ? hoursManual : 0; const noteStr = note || ''; - // Optimistic UI update setAttendance(prev => ({ ...prev, [getAttendanceKey(date, youthId, shiftId)]: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } })); @@ -176,15 +199,13 @@ export const useAdminState = () => { if (!navigator.onLine) { await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } }); } else { - await setAttendanceDb(date, youthId, shiftId, hoursWorked, weightedHours, status, noteStr).catch(() => { - // If it fails (e.g., server timeout despite being "online"), put it in the queue - addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } }); + await setAttendanceDb(date, youthId, shiftId, hoursWorked, weightedHours, status, noteStr).catch(async () => { + await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } }); }); } }; const bulkSetManualAttendance = async (records: { date: string; youthId: string; shiftId: 'MORNING' | 'AFTERNOON'; hoursWorked: number; weightedHours: number; status: 'Absent' | 'Late' | 'Present'; note: string }[]) => { - // 1. Optimistic UI update (Instant feedback for all youths) setAttendance(prev => { const next = { ...prev }; records.forEach(record => { @@ -193,23 +214,23 @@ export const useAdminState = () => { return next; }); - // 2. Offline Queue Handling - if (!navigator.onLine) { + const processOfflineQueue = async () => { const queue = await localforage.getItem('sync-queue') || []; - // We can safely push them as individual actions to the queue to re-use the existing sync logic records.forEach(record => { - queue.push({ type: 'SET_ATTENDANCE', payload: record }); + const idx = queue.findIndex(q => q.payload.date === record.date && q.payload.youthId === record.youthId && q.payload.shiftId === record.shiftId); + const action = { type: 'SET_ATTENDANCE', payload: record }; + if (idx > -1) queue[idx] = action; + else queue.push(action); }); await localforage.setItem('sync-queue', queue); + setIsOffline(true); + }; + + if (!navigator.onLine) { + await processOfflineQueue(); } else { - // 3. Single Network Request await bulkSetAttendanceDb(records).catch(async () => { - // Fallback to queue if the server times out - const queue = await localforage.getItem('sync-queue') || []; - records.forEach(record => { - queue.push({ type: 'SET_ATTENDANCE', payload: record }); - }); - await localforage.setItem('sync-queue', queue); + await processOfflineQueue(); }); } }; @@ -220,8 +241,8 @@ export const useAdminState = () => { if (!navigator.onLine) { await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }); } else { - await setAttendanceDb(date, youthId, shiftId, 0, 0, 'Pending', '').catch(() => { - addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }); + await setAttendanceDb(date, youthId, shiftId, 0, 0, 'Pending', '').catch(async () => { + await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }); }); } }; @@ -232,26 +253,15 @@ export const useAdminState = () => { if (!navigator.onLine) { await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); } else { - await removeAttendanceDb(date, youthId, shiftId).catch(() => { - addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); + await removeAttendanceDb(date, youthId, shiftId).catch(async () => { + await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); }); } }; return { - periods, - attendance, - activePeriodId, - setActivePeriodId, - isLoadingData, - isOffline, - createPeriod, - deletePeriod, - bulkAddYouth, - removeYouth, - setManualAttendance, - bulkSetManualAttendance, - addPendingAttendance, - removeAttendanceEntry + periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline, + createPeriod, deletePeriod, bulkAddYouth, removeYouth, + setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry }; }; \ No newline at end of file diff --git a/public/files/Badplatser på Kullaberge.pdf b/public/files/Badplatser på Kullaberg.pdf similarity index 100% rename from public/files/Badplatser på Kullaberge.pdf rename to public/files/Badplatser på Kullaberg.pdf