Auto update admin page and better cached status for files
This commit is contained in:
+4
-9
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user