Fixed offline queue caching
This commit is contained in:
+58
-48
@@ -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
|
||||
if (!navigator.onLine) {
|
||||
const processOfflineQueue = async () => {
|
||||
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 });
|
||||
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<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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user