Fixed offline queue caching

This commit is contained in:
2026-03-18 14:06:59 +01:00 Verified
parent 965d01fb32
commit 83de05505c
2 changed files with 58 additions and 48 deletions
+58 -48
View File
@@ -36,6 +36,24 @@ export const useAdminState = () => {
return { loadedPeriods, loadedAttendance }; return { loadedPeriods, loadedAttendance };
}; };
const applyQueueToAttendance = async (baseAttendance: AttendanceDataMap) => {
const queue = await localforage.getItem<any[]>('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 --- // --- 2. OFFLINE QUEUE FLUSHER ---
const flushOfflineQueue = useCallback(async () => { const flushOfflineQueue = useCallback(async () => {
try { try {
@@ -43,10 +61,8 @@ export const useAdminState = () => {
if (queue.length > 0) { if (queue.length > 0) {
console.log(`Flushing ${queue.length} items from offline queue...`, queue); console.log(`Flushing ${queue.length} items from offline queue...`, queue);
await syncOfflineQueueDb(queue); await syncOfflineQueueDb(queue);
// ONLY clear the queue if the server action succeeds without throwing an error
await localforage.setItem('sync-queue', []); await localforage.setItem('sync-queue', []);
return true; // Indicates we successfully synced something return true;
} }
} catch (error) { } catch (error) {
console.error("Server sync failed, keeping items in offline queue for later.", 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 --- // --- 3. MAIN DATA FETCHING ---
const refreshData = useCallback(async (isInitialLoad = false) => { const refreshData = useCallback(async (isInitialLoad = false) => {
try { try {
// First, check if we are online and have a backlog to sync before pulling fresh data
if (navigator.onLine) { if (navigator.onLine) {
await flushOfflineQueue(); await flushOfflineQueue();
} }
@@ -67,10 +82,12 @@ export const useAdminState = () => {
setIsOffline(false); setIsOffline(false);
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(dbPeriods); 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) { if (isInitialLoad && loadedPeriods.length > 0) {
const today = toIsoDate(new Date()); const today = toIsoDate(new Date());
const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate); const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate);
@@ -88,8 +105,11 @@ export const useAdminState = () => {
if (cachedData) { if (cachedData) {
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(cachedData); const { loadedPeriods, loadedAttendance } = mapDbDataToUI(cachedData);
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
setPeriods(loadedPeriods); setPeriods(loadedPeriods);
setAttendance(loadedAttendance); setAttendance(finalAttendance);
if (isInitialLoad && loadedPeriods.length > 0) setActivePeriodId(loadedPeriods[0].id); if (isInitialLoad && loadedPeriods.length > 0) setActivePeriodId(loadedPeriods[0].id);
} }
} finally { } finally {
@@ -97,18 +117,15 @@ export const useAdminState = () => {
} }
}, [flushOfflineQueue]); }, [flushOfflineQueue]);
// Initial load
useEffect(() => { useEffect(() => {
refreshData(true); refreshData(true);
}, [refreshData]); }, [refreshData]);
// --- 4. OFFLINE/ONLINE EVENT LISTENERS ---
useEffect(() => { useEffect(() => {
const handleOnline = async () => { const handleOnline = async () => {
setIsOffline(false); setIsOffline(false);
const didSync = await flushOfflineQueue(); const didSync = await flushOfflineQueue();
if (didSync) { if (didSync) {
// If we successfully pushed old data, pull the fresh DB state so the UI updates
refreshData(); refreshData();
} }
}; };
@@ -122,11 +139,18 @@ export const useAdminState = () => {
}; };
}, [flushOfflineQueue, refreshData]); }, [flushOfflineQueue, refreshData]);
// Helper to add actions to local queue
const addToOfflineQueue = async (action: any) => { const addToOfflineQueue = async (action: any) => {
const queue = await localforage.getItem<any[]>('sync-queue') || []; const queue = await localforage.getItem<any[]>('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 --- // --- 5. DATABASE WRITE ACTIONS ---
@@ -168,7 +192,6 @@ export const useAdminState = () => {
let hoursWorked = status === 'Late' || status === 'Present' ? hoursManual : 0; let hoursWorked = status === 'Late' || status === 'Present' ? hoursManual : 0;
const noteStr = note || ''; const noteStr = note || '';
// Optimistic UI update
setAttendance(prev => ({ setAttendance(prev => ({
...prev, [getAttendanceKey(date, youthId, shiftId)]: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } ...prev, [getAttendanceKey(date, youthId, shiftId)]: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr }
})); }));
@@ -176,15 +199,13 @@ export const useAdminState = () => {
if (!navigator.onLine) { if (!navigator.onLine) {
await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } }); await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } });
} else { } else {
await setAttendanceDb(date, youthId, shiftId, hoursWorked, weightedHours, status, noteStr).catch(() => { await setAttendanceDb(date, youthId, shiftId, hoursWorked, weightedHours, status, noteStr).catch(async () => {
// If it fails (e.g., server timeout despite being "online"), put it in the queue await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } });
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 }[]) => { 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 => { setAttendance(prev => {
const next = { ...prev }; const next = { ...prev };
records.forEach(record => { records.forEach(record => {
@@ -193,23 +214,23 @@ export const useAdminState = () => {
return next; return next;
}); });
// 2. Offline Queue Handling const processOfflineQueue = async () => {
if (!navigator.onLine) {
const queue = await localforage.getItem<any[]>('sync-queue') || []; const queue = await localforage.getItem<any[]>('sync-queue') || [];
// We can safely push them as individual actions to the queue to re-use the existing sync logic
records.forEach(record => { 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); await localforage.setItem('sync-queue', queue);
setIsOffline(true);
};
if (!navigator.onLine) {
await processOfflineQueue();
} else { } else {
// 3. Single Network Request
await bulkSetAttendanceDb(records).catch(async () => { await bulkSetAttendanceDb(records).catch(async () => {
// Fallback to queue if the server times out await processOfflineQueue();
const queue = await localforage.getItem<any[]>('sync-queue') || [];
records.forEach(record => {
queue.push({ type: 'SET_ATTENDANCE', payload: record });
});
await localforage.setItem('sync-queue', queue);
}); });
} }
}; };
@@ -220,8 +241,8 @@ export const useAdminState = () => {
if (!navigator.onLine) { if (!navigator.onLine) {
await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }); await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } });
} else { } else {
await setAttendanceDb(date, youthId, shiftId, 0, 0, 'Pending', '').catch(() => { await setAttendanceDb(date, youthId, shiftId, 0, 0, 'Pending', '').catch(async () => {
addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }); 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) { if (!navigator.onLine) {
await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } });
} else { } else {
await removeAttendanceDb(date, youthId, shiftId).catch(() => { await removeAttendanceDb(date, youthId, shiftId).catch(async () => {
addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } });
}); });
} }
}; };
return { return {
periods, periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline,
attendance, createPeriod, deletePeriod, bulkAddYouth, removeYouth,
activePeriodId, setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry
setActivePeriodId,
isLoadingData,
isOffline,
createPeriod,
deletePeriod,
bulkAddYouth,
removeYouth,
setManualAttendance,
bulkSetManualAttendance,
addPendingAttendance,
removeAttendanceEntry
}; };
}; };