Auto update admin page and better cached status for files
This commit is contained in:
+4
-9
@@ -3,7 +3,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CalendarRange, ClipboardCheck, Loader2, Lock, Unlock, Users as UsersIcon } from 'lucide-react';
|
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 { verifyLogin } from '../actions/admin';
|
||||||
import { OfflineBadge } from '../components/ui/OfflineBadge';
|
import { OfflineBadge } from '../components/ui/OfflineBadge';
|
||||||
import { AppUser } from './adminTypes';
|
import { AppUser } from './adminTypes';
|
||||||
@@ -16,7 +16,6 @@ export default function Admin() {
|
|||||||
const [currentUser, setCurrentUser] = useState<AppUser | null>(null);
|
const [currentUser, setCurrentUser] = useState<AppUser | null>(null);
|
||||||
const [isCheckingSession, setIsCheckingSession] = useState(true);
|
const [isCheckingSession, setIsCheckingSession] = useState(true);
|
||||||
|
|
||||||
// Replaced username/pin state with refs to stop React from fighting the autofill
|
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
const pinRef = useRef<HTMLInputElement>(null);
|
const pinRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -42,11 +41,9 @@ export default function Admin() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoginError(false);
|
setLoginError(false);
|
||||||
|
|
||||||
// Read values directly from the DOM nodes on submit
|
|
||||||
const usernameVal = usernameRef.current?.value || '';
|
const usernameVal = usernameRef.current?.value || '';
|
||||||
const pinVal = pinRef.current?.value || '';
|
const pinVal = pinRef.current?.value || '';
|
||||||
|
|
||||||
// Basic validation before hitting the server
|
|
||||||
if (!usernameVal || !pinVal) {
|
if (!usernameVal || !pinVal) {
|
||||||
setLoginError(true);
|
setLoginError(true);
|
||||||
return;
|
return;
|
||||||
@@ -63,7 +60,6 @@ export default function Admin() {
|
|||||||
else if (user.role === 'Staff') setActiveTab('today');
|
else if (user.role === 'Staff') setActiveTab('today');
|
||||||
else setActiveTab(adminState.periods.length > 0 ? 'today' : 'setup');
|
else setActiveTab(adminState.periods.length > 0 ? 'today' : 'setup');
|
||||||
} else {
|
} else {
|
||||||
// Clear the pin field on failure
|
|
||||||
if (pinRef.current) pinRef.current.value = '';
|
if (pinRef.current) pinRef.current.value = '';
|
||||||
setLoginError(true);
|
setLoginError(true);
|
||||||
}
|
}
|
||||||
@@ -93,10 +89,10 @@ export default function Admin() {
|
|||||||
name="username"
|
name="username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
type="text"
|
type="text"
|
||||||
ref={usernameRef} // <-- Uncontrolled Ref
|
ref={usernameRef}
|
||||||
placeholder="Användarnamn"
|
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"
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -106,7 +102,7 @@ export default function Admin() {
|
|||||||
name="pin"
|
name="pin"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
type="password"
|
type="password"
|
||||||
ref={pinRef} // <-- Uncontrolled Ref
|
ref={pinRef}
|
||||||
placeholder="•••••"
|
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'}`}
|
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)}
|
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>}
|
{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">
|
<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'}
|
{isLoading ? <Loader2 className="animate-spin mx-auto" /> : 'Logga in'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const useAdminState = () => {
|
|||||||
const [activePeriodId, setActivePeriodId] = useState<string>('');
|
const [activePeriodId, setActivePeriodId] = useState<string>('');
|
||||||
const [isLoadingData, setIsLoadingData] = useState<boolean>(true);
|
const [isLoadingData, setIsLoadingData] = useState<boolean>(true);
|
||||||
const [isOffline, setIsOffline] = useState<boolean>(false);
|
const [isOffline, setIsOffline] = useState<boolean>(false);
|
||||||
|
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
||||||
|
|
||||||
// --- 1. DATA MAPPING ---
|
// --- 1. DATA MAPPING ---
|
||||||
const mapDbDataToUI = (dbPeriods: any[]) => {
|
const mapDbDataToUI = (dbPeriods: any[]) => {
|
||||||
@@ -59,7 +60,6 @@ export const useAdminState = () => {
|
|||||||
try {
|
try {
|
||||||
const queue = await localforage.getItem<any[]>('sync-queue') || [];
|
const queue = await localforage.getItem<any[]>('sync-queue') || [];
|
||||||
if (queue.length > 0) {
|
if (queue.length > 0) {
|
||||||
console.log(`Flushing ${queue.length} items from offline queue...`, queue);
|
|
||||||
await syncOfflineQueueDb(queue);
|
await syncOfflineQueueDb(queue);
|
||||||
await localforage.setItem('sync-queue', []);
|
await localforage.setItem('sync-queue', []);
|
||||||
return true;
|
return true;
|
||||||
@@ -72,6 +72,8 @@ export const useAdminState = () => {
|
|||||||
|
|
||||||
// --- 3. MAIN DATA FETCHING ---
|
// --- 3. MAIN DATA FETCHING ---
|
||||||
const refreshData = useCallback(async (isInitialLoad = false) => {
|
const refreshData = useCallback(async (isInitialLoad = false) => {
|
||||||
|
if (!isInitialLoad) setIsSyncing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (navigator.onLine) {
|
if (navigator.onLine) {
|
||||||
await flushOfflineQueue();
|
await flushOfflineQueue();
|
||||||
@@ -82,7 +84,6 @@ export const useAdminState = () => {
|
|||||||
setIsOffline(false);
|
setIsOffline(false);
|
||||||
|
|
||||||
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(dbPeriods);
|
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(dbPeriods);
|
||||||
|
|
||||||
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
|
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
|
||||||
|
|
||||||
setPeriods(loadedPeriods);
|
setPeriods(loadedPeriods);
|
||||||
@@ -99,7 +100,6 @@ export const useAdminState = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Offline or Server Error! Loading from cache...");
|
|
||||||
setIsOffline(true);
|
setIsOffline(true);
|
||||||
const cachedData = await localforage.getItem<any[]>('cachedAdminData');
|
const cachedData = await localforage.getItem<any[]>('cachedAdminData');
|
||||||
|
|
||||||
@@ -114,6 +114,7 @@ export const useAdminState = () => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingData(false);
|
setIsLoadingData(false);
|
||||||
|
setIsSyncing(false);
|
||||||
}
|
}
|
||||||
}, [flushOfflineQueue]);
|
}, [flushOfflineQueue]);
|
||||||
|
|
||||||
@@ -121,13 +122,33 @@ export const useAdminState = () => {
|
|||||||
refreshData(true);
|
refreshData(true);
|
||||||
}, [refreshData]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const handleOnline = async () => {
|
const handleOnline = async () => {
|
||||||
setIsOffline(false);
|
setIsOffline(false);
|
||||||
const didSync = await flushOfflineQueue();
|
const didSync = await flushOfflineQueue();
|
||||||
if (didSync) {
|
if (didSync) refreshData();
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('online', handleOnline);
|
window.addEventListener('online', handleOnline);
|
||||||
@@ -141,7 +162,6 @@ export const useAdminState = () => {
|
|||||||
|
|
||||||
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') || [];
|
||||||
|
|
||||||
const filteredQueue = queue.filter(q =>
|
const filteredQueue = queue.filter(q =>
|
||||||
!(q.payload.date === action.payload.date &&
|
!(q.payload.date === action.payload.date &&
|
||||||
q.payload.youthId === action.payload.youthId &&
|
q.payload.youthId === action.payload.youthId &&
|
||||||
@@ -185,7 +205,7 @@ export const useAdminState = () => {
|
|||||||
await refreshData();
|
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 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;
|
const weight = isWeekend(date) ? 1.5 : 1.0;
|
||||||
let weightedHours = status === 'Late' || status === 'Present' ? hoursManual * weight : 0;
|
let weightedHours = status === 'Late' || status === 'Present' ? hoursManual * weight : 0;
|
||||||
@@ -260,7 +280,7 @@ export const useAdminState = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline,
|
periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline, isSyncing,
|
||||||
createPeriod, deletePeriod, bulkAddYouth, removeYouth,
|
createPeriod, deletePeriod, bulkAddYouth, removeYouth,
|
||||||
setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry
|
setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry
|
||||||
};
|
};
|
||||||
|
|||||||
+21
-8
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
"use client";
|
"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 { useEffect, useState } from 'react';
|
||||||
import { DocumentCard, ExternalLinkCard, DocumentItem, LinkItem } from '../components/ui/Cards';
|
import { DocumentCard, ExternalLinkCard, DocumentItem, LinkItem } from '../components/ui/Cards';
|
||||||
import { OfflineBadge } from '../components/ui/OfflineBadge';
|
import { OfflineBadge } from '../components/ui/OfflineBadge';
|
||||||
@@ -70,15 +70,28 @@ export default function Documents() {
|
|||||||
|
|
||||||
const handleSyncAll = async () => {
|
const handleSyncAll = async () => {
|
||||||
setSyncStatus('syncing');
|
setSyncStatus('syncing');
|
||||||
|
const updatedCache = new Set(cachedFiles);
|
||||||
|
|
||||||
for (const doc of docs) {
|
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');
|
setTimeout(async () => {
|
||||||
setShowNotification(true);
|
await checkCacheStatus();
|
||||||
setTimeout(() => {
|
setSyncStatus('done');
|
||||||
setShowNotification(false);
|
setShowNotification(true);
|
||||||
}, 4000);
|
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>;
|
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