Auto update admin page and better cached status for files

This commit is contained in:
2026-03-18 14:35:14 +01:00 Verified
parent 83de05505c
commit 99ff2f94b4
3 changed files with 54 additions and 26 deletions
+4 -9
View File
@@ -3,7 +3,7 @@
"use client";
import { CalendarRange, ClipboardCheck, Loader2, Lock, Unlock, Users as UsersIcon } from 'lucide-react';
import React, { useEffect, useState, useRef } from 'react'; // <-- Added useRef
import React, { useEffect, useRef, useState } from 'react';
import { verifyLogin } from '../actions/admin';
import { OfflineBadge } from '../components/ui/OfflineBadge';
import { AppUser } from './adminTypes';
@@ -16,7 +16,6 @@ export default function Admin() {
const [currentUser, setCurrentUser] = useState<AppUser | null>(null);
const [isCheckingSession, setIsCheckingSession] = useState(true);
// Replaced username/pin state with refs to stop React from fighting the autofill
const usernameRef = useRef<HTMLInputElement>(null);
const pinRef = useRef<HTMLInputElement>(null);
@@ -42,11 +41,9 @@ export default function Admin() {
e.preventDefault();
setLoginError(false);
// Read values directly from the DOM nodes on submit
const usernameVal = usernameRef.current?.value || '';
const pinVal = pinRef.current?.value || '';
// Basic validation before hitting the server
if (!usernameVal || !pinVal) {
setLoginError(true);
return;
@@ -63,7 +60,6 @@ export default function Admin() {
else if (user.role === 'Staff') setActiveTab('today');
else setActiveTab(adminState.periods.length > 0 ? 'today' : 'setup');
} else {
// Clear the pin field on failure
if (pinRef.current) pinRef.current.value = '';
setLoginError(true);
}
@@ -93,10 +89,10 @@ export default function Admin() {
name="username"
autoComplete="username"
type="text"
ref={usernameRef} // <-- Uncontrolled Ref
ref={usernameRef}
placeholder="Användarnamn"
className="w-full bg-white border border-slate-teal/20 text-center text-ebony font-bold p-3 rounded-xl focus:outline-none focus:border-slate-teal"
onChange={() => setLoginError(false)} // Just clear error on type, don't trigger full re-render
onChange={() => setLoginError(false)}
/>
</div>
@@ -106,7 +102,7 @@ export default function Admin() {
name="pin"
autoComplete="current-password"
type="password"
ref={pinRef} // <-- Uncontrolled Ref
ref={pinRef}
placeholder="•••••"
className={`w-full bg-white border text-center text-2xl text-ebony font-mono p-3 rounded-xl focus:outline-none ${loginError ? 'border-emergency/50 bg-emergency/5' : 'border-slate-teal/20 focus:border-slate-teal'}`}
onChange={() => setLoginError(false)}
@@ -115,7 +111,6 @@ export default function Admin() {
{loginError && <p className="text-emergency text-xs font-bold text-center mt-1">Fel namn eller lösenord.</p>}
{/* Removed the disabled state logic since we don't track live values anymore */}
<button type="submit" disabled={isLoading} className="w-full bg-slate-teal text-eggshell font-black uppercase tracking-widest py-3 mt-2 rounded-xl hover:bg-ebony transition-colors disabled:opacity-50">
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : 'Logga in'}
</button>
+29 -9
View File
@@ -13,6 +13,7 @@ export const useAdminState = () => {
const [activePeriodId, setActivePeriodId] = useState<string>('');
const [isLoadingData, setIsLoadingData] = useState<boolean>(true);
const [isOffline, setIsOffline] = useState<boolean>(false);
const [isSyncing, setIsSyncing] = useState<boolean>(false);
// --- 1. DATA MAPPING ---
const mapDbDataToUI = (dbPeriods: any[]) => {
@@ -59,7 +60,6 @@ export const useAdminState = () => {
try {
const queue = await localforage.getItem<any[]>('sync-queue') || [];
if (queue.length > 0) {
console.log(`Flushing ${queue.length} items from offline queue...`, queue);
await syncOfflineQueueDb(queue);
await localforage.setItem('sync-queue', []);
return true;
@@ -72,6 +72,8 @@ export const useAdminState = () => {
// --- 3. MAIN DATA FETCHING ---
const refreshData = useCallback(async (isInitialLoad = false) => {
if (!isInitialLoad) setIsSyncing(true);
try {
if (navigator.onLine) {
await flushOfflineQueue();
@@ -82,7 +84,6 @@ export const useAdminState = () => {
setIsOffline(false);
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(dbPeriods);
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
setPeriods(loadedPeriods);
@@ -99,7 +100,6 @@ export const useAdminState = () => {
}
}
} catch (error) {
console.error("Offline or Server Error! Loading from cache...");
setIsOffline(true);
const cachedData = await localforage.getItem<any[]>('cachedAdminData');
@@ -114,6 +114,7 @@ export const useAdminState = () => {
}
} finally {
setIsLoadingData(false);
setIsSyncing(false);
}
}, [flushOfflineQueue]);
@@ -121,13 +122,33 @@ export const useAdminState = () => {
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]);
// --- 4. OFFLINE/ONLINE EVENT LISTENERS ---
useEffect(() => {
const handleOnline = async () => {
setIsOffline(false);
const didSync = await flushOfflineQueue();
if (didSync) {
refreshData();
}
if (didSync) refreshData();
};
window.addEventListener('online', handleOnline);
@@ -141,7 +162,6 @@ export const useAdminState = () => {
const addToOfflineQueue = async (action: any) => {
const queue = await localforage.getItem<any[]>('sync-queue') || [];
const filteredQueue = queue.filter(q =>
!(q.payload.date === action.payload.date &&
q.payload.youthId === action.payload.youthId &&
@@ -185,7 +205,7 @@ export const useAdminState = () => {
await refreshData();
};
// --- 6. ATTENDANCE ACTIONS (WITH OFFLINE QUEUE SUPPORT) ---
// --- 6. ATTENDANCE ACTIONS ---
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;
@@ -260,7 +280,7 @@ export const useAdminState = () => {
};
return {
periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline,
periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline, isSyncing,
createPeriod, deletePeriod, bulkAddYouth, removeYouth,
setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry
};
+21 -8
View File
@@ -2,7 +2,7 @@
"use client";
import { BookCopy, CheckCircle, CloudDownload, Folder, Loader2, Map, MapPinned, MapPlus, WavesLadder, WifiOff } from 'lucide-react';
import { BookCopy, CheckCircle, CloudDownload, Folder, Loader2, Map, MapPinned, MapPlus, WavesLadder } from 'lucide-react';
import { useEffect, useState } from 'react';
import { DocumentCard, ExternalLinkCard, DocumentItem, LinkItem } from '../components/ui/Cards';
import { OfflineBadge } from '../components/ui/OfflineBadge';
@@ -70,15 +70,28 @@ export default function Documents() {
const handleSyncAll = async () => {
setSyncStatus('syncing');
const updatedCache = new Set(cachedFiles);
for (const doc of docs) {
try { await fetch(doc.file); } catch (error) { console.error("Kunde inte ladda ner:", doc.file); }
try {
const response = await fetch(doc.file);
if (response.ok) {
updatedCache.add(doc.file);
setCachedFiles(new Set(updatedCache));
}
} catch (error) {
console.error("Kunde inte ladda ner:", doc.file);
}
}
await checkCacheStatus();
setSyncStatus('done');
setShowNotification(true);
setTimeout(() => {
setShowNotification(false);
}, 4000);
setTimeout(async () => {
await checkCacheStatus();
setSyncStatus('done');
setShowNotification(true);
setTimeout(() => {
setShowNotification(false);
}, 4000);
}, 500);
};
if (!isHydrated) return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;