Files
app/app/admin/page.tsx
T

267 lines
14 KiB
TypeScript

// app/admin/page.tsx
"use client";
import { CalendarRange, ClipboardCheck, Loader2, Lock, Unlock, Users as UsersIcon, BellRing, NotebookText } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
import { verifyLogin } from '../actions/admin';
import { OfflineBadge } from '../components/ui/OfflineBadge';
import { AppUser, toIsoDate, getAttendanceKey } from './adminTypes';
import { AttendanceTab, getTeamShiftInfo } from './AttendanceTab';
import { ReportTab } from './ReportTab';
import { SetupTab } from './SetupTab';
import { NoticeTab } from './NoticeTab';
import { LogTab } from './LogTab';
import { useAdminState } from './useAdminState';
export default function Admin() {
const [currentUser, setCurrentUser] = useState<AppUser | null>(null);
const [isCheckingSession, setIsCheckingSession] = useState(true);
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' | 'notice' | 'today' | 'log' | '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);
const usernameVal = usernameRef.current?.value || '';
const pinVal = pinRef.current?.value || '';
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 {
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}
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)}
/>
</div>
<div className="relative">
<input
id="pin"
name="pin"
autoComplete="current-password"
type="password"
ref={pinRef}
placeholder="•••••"
className={`w-full bg-white border text-center text-2xl text-ebony font-mono p-3 rounded-xl tracking-widest 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>}
<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>
);
}
// SMART NOTIS-LOGIK FÖR BÅDE DAGBOK OCH NÄRVARO
let missingLogsCount = 0;
let missingAttendanceCount = 0;
if (adminState.periods.length > 0 && adminState.scheduleData.length > 0) {
const todayIso = toIsoDate(new Date());
const activePeriod = adminState.periods.find(p => p.id === adminState.activePeriodId) || adminState.periods[0];
let curr = new Date(activePeriod.startDate + 'T12:00:00');
const end = new Date((activePeriod.endDate < todayIso ? activePeriod.endDate : todayIso) + 'T12:00:00');
while (curr <= end) {
const dateStr = toIsoDate(curr);
const dayName = curr.toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase();
const daySchedule = adminState.scheduleData.find(d => d.day.toLowerCase() === dayName);
if (daySchedule) {
const pfWork = daySchedule.pilgrimsfalkarna?.time && daySchedule.pilgrimsfalkarna.time !== 'Ledig';
const tuWork = daySchedule.tumlarna?.time && daySchedule.tumlarna.time !== 'Ledig';
if (pfWork || tuWork) {
// 1. Kolla Dagboken
const hasLog = !!adminState.dailyLogs?.[dateStr] && adminState.dailyLogs[dateStr].trim() !== "";
if (!hasLog) missingLogsCount++;
// 2. Kolla Närvaron
if (pfWork) {
const { shiftId } = getTeamShiftInfo(daySchedule, 'PF');
activePeriod.youthList.filter(y => y.team === 'PF').forEach(y => {
const entry = adminState.attendance[getAttendanceKey(dateStr, y.id, shiftId)];
if (!entry || entry.status === 'Pending') missingAttendanceCount++;
});
}
if (tuWork) {
const { shiftId } = getTeamShiftInfo(daySchedule, 'TU');
activePeriod.youthList.filter(y => y.team === 'TU').forEach(y => {
const entry = adminState.attendance[getAttendanceKey(dateStr, y.id, shiftId)];
if (!entry || entry.status === 'Pending') missingAttendanceCount++;
});
}
}
}
curr.setDate(curr.getDate() + 1);
}
}
const availableTabs = [];
if (currentUser.role === 'Admin') availableTabs.push({ id: 'setup', icon: CalendarRange, label: 'Perioder' });
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') availableTabs.push({ id: 'notice', icon: BellRing, label: 'Notis' });
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') {
availableTabs.push({ id: 'today', icon: ClipboardCheck, label: 'Närvaro', badge: missingAttendanceCount });
availableTabs.push({ id: 'log', icon: NotebookText, label: 'Journal', badge: missingLogsCount });
}
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 || adminState.scheduleData.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 pt-3 pb-2 px-1 -mx-1">
{availableTabs.map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`relative flex items-center px-4 py-2.5 rounded-lg font-bold text-xs uppercase tracking-widest transition-colors ${isActive ? 'bg-slate-teal text-eggshell' : 'bg-white/60 text-slate-teal hover:bg-slate-teal/10'}`}
>
<Icon size={16} className="mr-2 hidden md:block shrink-0" /> {tab.label}
{!!tab.badge && tab.badge > 0 && (
<div className="absolute -top-1.5 -right-1.5 bg-emergency text-white text-[10px] font-black w-5 h-5 flex items-center justify-center rounded-full shadow-sm ring-[1.5px] ring-white">
{tab.badge}
</div>
)}
</button>
);
})}
</div>
)}
{activeTab === 'setup' && currentUser.role === 'Admin' && <SetupTab {...adminState} />}
{activeTab === 'notice' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && <NoticeTab isOffline={adminState.isOffline} />}
{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}
scheduleData={adminState.scheduleData}
/>
)}
{activeTab === 'log' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && (
<LogTab
periods={adminState.periods}
dailyLogs={adminState.dailyLogs}
setDailyLog={adminState.setDailyLog}
activePeriodId={adminState.activePeriodId}
setActivePeriodId={adminState.setActivePeriodId}
scheduleData={adminState.scheduleData}
/>
)}
{activeTab === 'report' && (
<ReportTab
periods={adminState.periods}
attendance={adminState.attendance}
activePeriodId={adminState.activePeriodId}
setActivePeriodId={adminState.setActivePeriodId}
currentUserRole={currentUser.role}
scheduleData={adminState.scheduleData}
/>
)}
</>
)}
</div>
);
}