diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 303dd06..a574b6a 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -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(null); const [isCheckingSession, setIsCheckingSession] = useState(true); - // Replaced username/pin state with refs to stop React from fighting the autofill const usernameRef = useRef(null); const pinRef = useRef(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)} /> @@ -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 &&

Fel namn eller lösenord.

} - {/* Removed the disabled state logic since we don't track live values anymore */} diff --git a/app/admin/useAdminState.ts b/app/admin/useAdminState.ts index bdd686f..e5f3ae4 100644 --- a/app/admin/useAdminState.ts +++ b/app/admin/useAdminState.ts @@ -13,6 +13,7 @@ export const useAdminState = () => { const [activePeriodId, setActivePeriodId] = useState(''); const [isLoadingData, setIsLoadingData] = useState(true); const [isOffline, setIsOffline] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); // --- 1. DATA MAPPING --- const mapDbDataToUI = (dbPeriods: any[]) => { @@ -59,7 +60,6 @@ export const useAdminState = () => { try { const queue = await localforage.getItem('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('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('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 }; diff --git a/app/documents/page.tsx b/app/documents/page.tsx index fada6be..3f286c8 100644 --- a/app/documents/page.tsx +++ b/app/documents/page.tsx @@ -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
;