197 lines
10 KiB
TypeScript
197 lines
10 KiB
TypeScript
// app/admin/page.tsx
|
|
|
|
"use client";
|
|
|
|
import { CalendarRange, ClipboardCheck, Loader2, Lock, Unlock, Users as UsersIcon } from 'lucide-react';
|
|
import React, { useEffect, useState, useRef } from 'react'; // <-- Added useRef
|
|
import { verifyLogin } from '../actions/admin';
|
|
import { OfflineBadge } from '../components/ui/OfflineBadge';
|
|
import { AppUser } from './adminTypes';
|
|
import { AttendanceTab } from './AttendanceTab';
|
|
import { ReportTab } from './ReportTab';
|
|
import { SetupTab } from './SetupTab';
|
|
import { useAdminState } from './useAdminState';
|
|
|
|
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);
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [loginError, setLoginError] = useState(false);
|
|
|
|
const [activeTab, setActiveTab] = useState<'setup' | 'today' | 'report'>('report');
|
|
const adminState = useAdminState();
|
|
|
|
useEffect(() => {
|
|
const savedSession = localStorage.getItem('kullaberg_admin_session');
|
|
if (savedSession) {
|
|
const user = JSON.parse(savedSession) as AppUser;
|
|
setCurrentUser(user);
|
|
if (user.role === 'Viewer') setActiveTab('report');
|
|
else if (user.role === 'Staff') setActiveTab('today');
|
|
else setActiveTab('today');
|
|
}
|
|
setIsCheckingSession(false);
|
|
}, []);
|
|
|
|
const handleLogin = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
|
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;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
const result = await verifyLogin(usernameVal, pinVal);
|
|
if (result.success && result.user) {
|
|
const user = result.user as AppUser;
|
|
setCurrentUser(user);
|
|
localStorage.setItem('kullaberg_admin_session', JSON.stringify(user));
|
|
if (user.role === 'Viewer') setActiveTab('report');
|
|
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);
|
|
}
|
|
setIsLoading(false);
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
setCurrentUser(null);
|
|
localStorage.removeItem('kullaberg_admin_session');
|
|
};
|
|
|
|
if (isCheckingSession) return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;
|
|
|
|
if (!currentUser) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-20 animate-fade-in px-4">
|
|
<div className="bg-eggshell border-2 border-slate-teal/20 p-6 md:p-8 rounded-2xl shadow-sm w-full max-w-sm text-center">
|
|
<div className="bg-slate-teal/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Lock size={32} className="text-slate-teal" />
|
|
</div>
|
|
<h2 className="text-2xl font-black text-ebony uppercase mb-6 tracking-widest">Admin Login</h2>
|
|
|
|
<form onSubmit={handleLogin} className="space-y-3 text-left">
|
|
<div className="relative">
|
|
<input
|
|
id="username"
|
|
name="username"
|
|
autoComplete="username"
|
|
type="text"
|
|
ref={usernameRef} // <-- Uncontrolled Ref
|
|
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
|
|
/>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<input
|
|
id="pin"
|
|
name="pin"
|
|
autoComplete="current-password"
|
|
type="password"
|
|
ref={pinRef} // <-- Uncontrolled Ref
|
|
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)}
|
|
/>
|
|
</div>
|
|
|
|
{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>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const availableTabs = [];
|
|
if (currentUser.role === 'Admin') availableTabs.push({ id: 'setup', icon: CalendarRange, label: 'Perioder' });
|
|
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') availableTabs.push({ id: 'today', icon: ClipboardCheck, label: 'Närvaro' });
|
|
availableTabs.push({ id: 'report', icon: UsersIcon, label: 'Rapport' });
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fade-in w-full">
|
|
<div className="flex flex-col md:flex-row justify-between md:items-end border-b border-slate-teal/20 pb-3 gap-2">
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center gap-3">
|
|
<Unlock className="text-seafoam" size={24} />
|
|
<h1 className="text-2xl font-black text-slate-teal uppercase tracking-widest">Admin</h1>
|
|
</div>
|
|
{adminState.isOffline && <OfflineBadge className="mt-2" />}
|
|
</div>
|
|
<div className="flex items-center gap-3 text-sm md:text-right bg-eggshell md:bg-transparent p-2 md:p-0 rounded-lg">
|
|
<span className="font-bold text-ebony">Inloggad: {currentUser.name}</span>
|
|
<span className="text-slate-teal/30">|</span>
|
|
<button onClick={handleLogout} className="font-bold text-slate-teal hover:text-goldenrod transition-colors">Logga ut</button>
|
|
</div>
|
|
</div>
|
|
|
|
{adminState.isLoadingData && adminState.periods.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-20 text-slate-teal">
|
|
<Loader2 size={40} className="animate-spin mb-4" />
|
|
<p className="font-bold text-sm animate-pulse">Hämtar data...</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{availableTabs.length > 1 && (
|
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
|
{availableTabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as any)}
|
|
className={`flex items-center px-4 py-2.5 rounded-lg font-bold text-xs uppercase tracking-widest transition-colors ${activeTab === tab.id ? 'bg-slate-teal text-eggshell' : 'bg-white/60 text-slate-teal hover:bg-slate-teal/10'}`}
|
|
>
|
|
<tab.icon size={16} className="mr-2 hidden md:block shrink-0" /> {tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'setup' && currentUser.role === 'Admin' && <SetupTab {...adminState} />}
|
|
{activeTab === 'today' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && (
|
|
<AttendanceTab
|
|
periods={adminState.periods}
|
|
attendance={adminState.attendance}
|
|
setManualAttendance={adminState.setManualAttendance}
|
|
bulkSetManualAttendance={adminState.bulkSetManualAttendance}
|
|
addPendingAttendance={adminState.addPendingAttendance}
|
|
removeAttendanceEntry={adminState.removeAttendanceEntry}
|
|
activePeriodId={adminState.activePeriodId}
|
|
setActivePeriodId={adminState.setActivePeriodId}
|
|
/>
|
|
)}
|
|
{activeTab === 'report' && (
|
|
<ReportTab
|
|
periods={adminState.periods}
|
|
attendance={adminState.attendance}
|
|
activePeriodId={adminState.activePeriodId}
|
|
setActivePeriodId={adminState.setActivePeriodId}
|
|
currentUserRole={currentUser.role}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
} |