Files
app/app/admin/useAdminState.ts
T

283 lines
15 KiB
TypeScript

// app/admin/useAdminState.ts
"use client";
import localforage from 'localforage';
import { useCallback, useEffect, useState } from 'react';
import { bulkAddYouthDb, bulkSetAttendanceDb, createPeriodDb, deletePeriodDb, getAdminData, removeAttendanceDb, removeYouthDb, setAttendanceDb, syncOfflineQueueDb, getDailyLogsDb, setDailyLogDb } from '../actions/admin';
import { readJsonFile } from '../actions/jsonEditor';
import { AttendanceDataMap, Period, Youth, getAttendanceKey, isWeekend, toIsoDate } from './adminTypes';
export const useAdminState = () => {
const [periods, setPeriods] = useState<Period[]>([]);
const [attendance, setAttendance] = useState<AttendanceDataMap>({});
const [scheduleData, setScheduleData] = useState<any[]>([]);
const [dailyLogs, setDailyLogs] = useState<Record<string, string>>({});
const [activePeriodId, setActivePeriodId] = useState<string>('');
const [isLoadingData, setIsLoadingData] = useState<boolean>(true);
const [isOffline, setIsOffline] = useState<boolean>(false);
const [isSyncing, setIsSyncing] = useState<boolean>(false);
const mapDbDataToUI = (dbPeriods: any[]) => {
const loadedPeriods: Period[] = [];
const loadedAttendance: AttendanceDataMap = {};
for (const dbPeriod of dbPeriods) {
const youthList: Youth[] = dbPeriod.youths.map((y: any) => ({ id: y.id, name: y.name, team: y.team as 'PF' | 'TU' }));
loadedPeriods.push({ id: dbPeriod.id, name: dbPeriod.name, startDate: dbPeriod.startDate, endDate: dbPeriod.endDate, youthList });
for (const youth of dbPeriod.youths) {
for (const att of youth.attendance) {
const key = getAttendanceKey(att.date, att.youthId, att.shiftId);
loadedAttendance[key] = {
date: att.date, youthId: att.youthId, shiftId: att.shiftId as 'MORNING' | 'AFTERNOON',
hoursWorked: att.hoursWorked, weightedHours: att.weightedHours, status: att.status as any, note: att.note || ''
};
}
}
}
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;
};
const applyQueueToLogs = async (baseLogs: Record<string, string>) => {
const queue = await localforage.getItem<any[]>('sync-queue') || [];
const nextLogs = { ...baseLogs };
for (const action of queue) {
if (action.type === 'SET_DAILY_LOG') {
nextLogs[action.payload.date] = action.payload.content;
}
}
return nextLogs;
};
const flushOfflineQueue = useCallback(async () => {
try {
const queue = await localforage.getItem<any[]>('sync-queue') || [];
if (queue.length > 0) {
await syncOfflineQueueDb(queue);
await localforage.setItem('sync-queue', []);
return true;
}
} catch (error) {
console.error("Server sync failed, keeping items in offline queue for later.", error);
}
return false;
}, []);
const refreshData = useCallback(async (isInitialLoad = false) => {
if (!isInitialLoad) setIsSyncing(true);
if (isInitialLoad) {
try {
const cachedSched = await localforage.getItem<any[]>('cachedScheduleData');
const cachedData = await localforage.getItem<any[]>('cachedAdminData');
const cachedLogs = await localforage.getItem<Record<string, string>>('cachedDailyLogs');
if (cachedData) {
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(cachedData);
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
const finalLogs = await applyQueueToLogs(cachedLogs || {});
if (cachedSched) setScheduleData(cachedSched);
setPeriods(loadedPeriods);
setAttendance(finalAttendance);
setDailyLogs(finalLogs);
if (loadedPeriods.length > 0) {
const today = toIsoDate(new Date());
const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate);
if (activePeriods.length > 0) setActivePeriodId(activePeriods[0].id);
else setActivePeriodId(loadedPeriods[0].id);
}
setIsLoadingData(false);
}
} catch (e) {
console.warn("Kunde inte ladda lokal cache", e);
}
}
const withTimeout = <T>(promise: Promise<T>, ms: number = 5000): Promise<T> => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('NETWORK_TIMEOUT')), ms);
promise
.then(val => { clearTimeout(timer); resolve(val); })
.catch(err => { clearTimeout(timer); reject(err); });
});
};
try {
if (navigator.onLine) await flushOfflineQueue();
const schedRes = await withTimeout(readJsonFile('schedule.json'));
if (schedRes.success && schedRes.data) {
setScheduleData(schedRes.data);
await localforage.setItem('cachedScheduleData', schedRes.data);
}
const dbPeriods = await withTimeout(getAdminData());
await localforage.setItem('cachedAdminData', dbPeriods);
const dbLogs = await withTimeout(getDailyLogsDb());
const mappedLogs: Record<string, string> = {};
dbLogs.forEach((l: { date: string | number; content: string; }) => mappedLogs[l.date] = l.content);
await localforage.setItem('cachedDailyLogs', mappedLogs);
setIsOffline(false);
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(dbPeriods);
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
const finalLogs = await applyQueueToLogs(mappedLogs);
setPeriods(loadedPeriods);
setAttendance(finalAttendance);
setDailyLogs(finalLogs);
if (isInitialLoad && periods.length === 0 && loadedPeriods.length > 0) {
const today = toIsoDate(new Date());
const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate);
if (activePeriods.length > 0) setActivePeriodId(activePeriods[0].id);
else setActivePeriodId(loadedPeriods[0].id);
}
} catch (error) {
console.warn("Nätverkstimeout eller offline - faller tillbaka på cache.", error);
setIsOffline(true);
} finally {
setIsLoadingData(false);
setIsSyncing(false);
}
}, [flushOfflineQueue, periods.length]);
useEffect(() => { refreshData(true); }, [refreshData]);
useEffect(() => {
const interval = setInterval(() => { if (navigator.onLine) refreshData(); }, 10000);
const handleVisibilityChange = () => { if (document.visibilityState === 'visible' && navigator.onLine) refreshData(); };
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('focus', handleVisibilityChange);
return () => { clearInterval(interval); document.removeEventListener('visibilitychange', handleVisibilityChange); window.removeEventListener('focus', handleVisibilityChange); };
}, [refreshData]);
useEffect(() => {
const handleOnline = async () => { setIsOffline(false); const didSync = await flushOfflineQueue(); if (didSync) refreshData(); };
window.addEventListener('online', handleOnline); window.addEventListener('offline', () => setIsOffline(true));
return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', () => setIsOffline(true)); };
}, [flushOfflineQueue, refreshData]);
const addToOfflineQueue = async (action: any) => {
const queue = await localforage.getItem<any[]>('sync-queue') || [];
let filteredQueue = queue;
if (action.type === 'SET_DAILY_LOG') {
filteredQueue = queue.filter(q => !(q.type === 'SET_DAILY_LOG' && q.payload.date === action.payload.date));
} else if (action.type === 'SET_ATTENDANCE' || action.type === 'REMOVE_ATTENDANCE') {
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);
};
const createPeriod = async (name: string, startDateStr: string) => {
setIsLoadingData(true);
const start = new Date(startDateStr + 'T12:00:00');
const end = new Date(start); end.setDate(start.getDate() + 20);
const newPeriod = await createPeriodDb(name, toIsoDate(start), toIsoDate(end));
await refreshData();
setActivePeriodId(newPeriod.id);
};
const deletePeriod = async (periodId: string) => { setPeriods(periods.filter(p => p.id !== periodId)); await deletePeriodDb(periodId); await refreshData(); };
const bulkAddYouth = async (periodId: string, text: string, defaultTeam: 'PF' | 'TU') => {
setIsLoadingData(true);
const newYouthData = text.split('\n').map(l => l.trim()).filter(l => l.length > 0).map((line) => {
const parts = line.split(/[,|-]/).map(p => p.trim());
return { name: parts[0], team: (parts[1]?.toUpperCase() === 'PF' || parts[1]?.toUpperCase() === 'TU') ? parts[1].toUpperCase() : defaultTeam };
});
await bulkAddYouthDb(periodId, newYouthData);
await refreshData();
};
const removeYouth = async (periodId: string, youthId: string) => { setPeriods(periods.map(p => p.id === periodId ? { ...p, youthList: p.youthList.filter(y => y.id !== youthId) } : p)); await removeYouthDb(youthId); await refreshData(); };
const setManualAttendance = async (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON', status: 'Absent' | 'Late' | 'Present', hoursManual: number = 0, note?: string) => {
const weight = isWeekend(date) ? 1.5 : 1.0;
let weightedHours = status === 'Late' || status === 'Present' ? hoursManual * weight : 0;
let hoursWorked = status === 'Late' || status === 'Present' ? hoursManual : 0;
const noteStr = note || '';
setAttendance(prev => ({ ...prev, [getAttendanceKey(date, youthId, shiftId)]: { date, youthId, shiftId, hoursWorked, weightedHours, status, note: noteStr } }));
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(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 }[]) => {
setAttendance(prev => { const next = { ...prev }; records.forEach(record => { next[getAttendanceKey(record.date, record.youthId, record.shiftId)] = record; }); return next; });
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) await processOfflineQueue();
else await bulkSetAttendanceDb(records).catch(async () => { await processOfflineQueue(); });
};
const addPendingAttendance = async (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => {
setAttendance(prev => ({ ...prev, [getAttendanceKey(date, youthId, shiftId)]: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }));
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(async () => { await addToOfflineQueue({ type: 'SET_ATTENDANCE', payload: { date, youthId, shiftId, hoursWorked: 0, weightedHours: 0, status: 'Pending', note: '' } }); });
};
const removeAttendanceEntry = async (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => {
setAttendance(prev => { const next = { ...prev }; delete next[getAttendanceKey(date, youthId, shiftId)]; return next; });
if (!navigator.onLine) await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } });
else await removeAttendanceDb(date, youthId, shiftId).catch(async () => { await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); });
};
const setDailyLog = async (date: string, content: string) => {
setDailyLogs(prev => ({ ...prev, [date]: content }));
if (!navigator.onLine) {
await addToOfflineQueue({ type: 'SET_DAILY_LOG', payload: { date, content } });
} else {
await setDailyLogDb(date, content).catch(async () => {
await addToOfflineQueue({ type: 'SET_DAILY_LOG', payload: { date, content } });
});
}
};
return {
periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline, isSyncing, scheduleData,
dailyLogs, setDailyLog,
createPeriod, deletePeriod, bulkAddYouth, removeYouth,
setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry, refreshData
};
};