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
+61 -51
View File
@@ -36,6 +36,24 @@ export const useAdminState = () => {
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 ---
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<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 ---
@@ -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
const processOfflineQueue = async () => {
const queue = await localforage.getItem<any[]>('sync-queue') || [];
records.forEach(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) {
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 => {
queue.push({ type: 'SET_ATTENDANCE', payload: record });
});
await localforage.setItem('sync-queue', queue);
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<any[]>('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
};
};