Fixed offline queue caching
This commit is contained in:
+61
-51
@@ -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 () => {
|
||||||
|
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) {
|
if (!navigator.onLine) {
|
||||||
const queue = await localforage.getItem<any[]>('sync-queue') || [];
|
await processOfflineQueue();
|
||||||
// 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);
|
|
||||||
} 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
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user