Files
app/app/admin/AttendanceTab.tsx
T

171 lines
11 KiB
TypeScript

// app/admin/AttendanceTab.tsx
"use client";
import { CalendarRange, CheckCircle, ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { type AttendanceDataMap, getAttendanceKey, type Period, toIsoDate } from './adminTypes';
import { TeamAttendanceCard } from './TeamAttendanceCard';
interface Props {
periods: Period[]; attendance: AttendanceDataMap;
setManualAttendance: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON', status: 'Absent' | 'Late' | 'Present', hours: number, note?: string) => void;
bulkSetManualAttendance: (records: any[]) => void;
addPendingAttendance: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => void;
removeAttendanceEntry: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => void;
activePeriodId: string; setActivePeriodId: (id: string) => void;
scheduleData: any[];
}
export const getTeamShiftInfo = (daySchedule: any, team: 'PF' | 'TU') => {
if (!daySchedule) return { time: 'Ledig', shiftId: 'MORNING' as 'MORNING' | 'AFTERNOON' };
const pfTime = daySchedule.pilgrimsfalkarna?.time || 'Ledig';
const tuTime = daySchedule.tumlarna?.time || 'Ledig';
const pfStart = pfTime !== 'Ledig' ? parseInt(pfTime.match(/(\d+):/)?.[1] || '99') : 99;
const tuStart = tuTime !== 'Ledig' ? parseInt(tuTime.match(/(\d+):/)?.[1] || '99') : 99;
if (team === 'PF') return { time: pfTime, shiftId: (pfStart > tuStart) ? 'AFTERNOON' : 'MORNING' as 'MORNING' | 'AFTERNOON' };
else return { time: tuTime, shiftId: (tuStart > pfStart) ? 'AFTERNOON' : (pfStart === tuStart ? 'AFTERNOON' : 'MORNING') as 'MORNING' | 'AFTERNOON' };
};
const getDaysInPeriod = (start: string, end: string) => {
const days = []; let curr = new Date(start + 'T12:00:00'); const endDate = new Date(end + 'T12:00:00');
while (curr <= endDate) { days.push(toIsoDate(curr)); curr.setDate(curr.getDate() + 1); }
return days;
};
const getDailyCompletionStats = (date: string, period: Period, attendance: AttendanceDataMap, scheduleData: any[]) => {
const dayNameStr = new Date(date + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase();
const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr);
if (!daySchedule) return { expected: 0, completed: 0, isComplete: true, hasWork: false };
let expected = 0; let completed = 0; let hasWork = false;
if (daySchedule.pilgrimsfalkarna && daySchedule.pilgrimsfalkarna.time !== 'Ledig') {
hasWork = true; const { shiftId } = getTeamShiftInfo(daySchedule, 'PF');
const pfYouth = period.youthList.filter(y => y.team === 'PF'); expected += pfYouth.length;
pfYouth.forEach(y => { const entry = attendance[getAttendanceKey(date, y.id, shiftId)]; if (entry && entry.status !== 'Pending') completed++; });
}
if (daySchedule.tumlarna && daySchedule.tumlarna.time !== 'Ledig') {
hasWork = true; const { shiftId } = getTeamShiftInfo(daySchedule, 'TU');
const tuYouth = period.youthList.filter(y => y.team === 'TU'); expected += tuYouth.length;
tuYouth.forEach(y => { const entry = attendance[getAttendanceKey(date, y.id, shiftId)]; if (entry && entry.status !== 'Pending') completed++; });
}
return { expected, completed, isComplete: expected > 0 && completed >= expected, hasWork };
};
export const AttendanceTab: React.FC<Props> = ({ periods, attendance, setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry, activePeriodId, setActivePeriodId, scheduleData }) => {
const activePeriod = periods.find(p => p.id === activePeriodId);
const [currentDate, setCurrentDate] = useState<string>(() => {
const today = toIsoDate(new Date());
if (activePeriod && today >= activePeriod.startDate && today <= activePeriod.endDate) return today;
return activePeriod ? activePeriod.startDate : today;
});
useEffect(() => {
if (activePeriod) {
setCurrentDate(prevDate => {
if (prevDate >= activePeriod.startDate && prevDate <= activePeriod.endDate) {
return prevDate;
}
const today = toIsoDate(new Date());
if (today >= activePeriod.startDate && today <= activePeriod.endDate) return today;
return activePeriod.startDate;
});
}
}, [activePeriodId, activePeriod]);
if (!activePeriod) return <p className="text-center font-bold text-slate-teal mt-10">Ingen period aktiv.</p>;
const changeDate = (days: number) => {
const newDateObj = new Date(currentDate + 'T12:00:00'); newDateObj.setDate(newDateObj.getDate() + days);
const startObj = new Date(activePeriod.startDate + 'T12:00:00'); const endObj = new Date(activePeriod.endDate + 'T12:00:00');
if (newDateObj >= startObj && newDateObj <= endObj) setCurrentDate(toIsoDate(newDateObj));
};
const dayNameStr = new Date(currentDate + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' });
const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr.toLowerCase());
const timelineDays = getDaysInPeriod(activePeriod.startDate, activePeriod.endDate);
const todayIso = toIsoDate(new Date());
const currentDayStats = getDailyCompletionStats(currentDate, activePeriod, attendance, scheduleData);
const teamsToRender = ['PF', 'TU'].sort((a, b) => {
const timeA = getTeamShiftInfo(daySchedule, a as 'PF' | 'TU').time;
const timeB = getTeamShiftInfo(daySchedule, b as 'PF' | 'TU').time;
const startA = timeA !== 'Ledig' ? parseInt(timeA.match(/(\d+):/)?.[1] || '99') : 99;
const startB = timeB !== 'Ledig' ? parseInt(timeB.match(/(\d+):/)?.[1] || '99') : 99;
return startA - startB;
});
return (
<div className="space-y-6 animate-fade-in">
<div className="rounded-3xl shadow-sm transition-all duration-300 bg-white/40 backdrop-blur-md border border-white/40 p-3 sm:p-5 hover:shadow-md">
<div className="flex justify-between items-center mb-3">
<h2 className="text-sm font-black text-ebony uppercase tracking-widest flex items-center mb-2">
<CalendarRange className="mr-2 text-slate-teal" size={20} /> Periodöversikt
</h2>
<select value={activePeriodId} onChange={(e) => setActivePeriodId(e.target.value)} className="bg-white border border-slate-teal/10 py-1.5 px-3 rounded-xl font-bold text-slate-teal text-xs cursor-pointer focus:outline-none shadow-sm">
{periods.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div className="flex overflow-x-auto gap-2.5 pb-3 pt-1 px-2 scrollbar-hide">
{timelineDays.map(day => {
const stats = getDailyCompletionStats(day, activePeriod, attendance, scheduleData);
const isSelected = day === currentDate;
let bgClass = "bg-white text-ebony border-slate-teal/10";
if (!stats.hasWork) bgClass = "bg-slate-teal/5 text-ebony/40 border-transparent";
else if (stats.isComplete) bgClass = "bg-moss text-white border-moss shadow-inner";
else if (day < todayIso || day === todayIso) bgClass = "bg-goldenrod text-white border-goldenrod shadow-inner";
return (
<button key={day} onClick={() => setCurrentDate(day)} className={`flex flex-col items-center justify-center min-w-14 p-2 rounded-xl border transition-colors ${bgClass} ${isSelected ? 'ring-2 ring-slate-teal ring-offset-2 ring-offset-eggshell' : 'hover:brightness-95 shadow-sm'}`}>
<span className="text-[10px] font-black uppercase tracking-wider">{new Date(day + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'short' })}</span>
<span className="text-sm font-black">{new Date(day + 'T12:00:00').getDate()}/{new Date(day + 'T12:00:00').getMonth() + 1}</span>
</button>
);
})}
</div>
</div>
<div className="flex flex-row justify-between items-center rounded-3xl shadow-sm transition-all duration-300 bg-white/40 backdrop-blur-md border border-white/40 p-3 sm:p-5 hover:shadow-md gap-4">
<button onClick={() => changeDate(-1)} className="p-3 text-slate-teal bg-slate-teal/10 hover:bg-slate-teal/30 rounded-2xl transition-colors"><ChevronLeft size={24} /></button>
<div className="text-center flex-1">
<h2 className="text-lg font-black text-ebony capitalize mb-0.5">{dayNameStr}</h2>
<p className="text-[11px] font-black uppercase tracking-widest text-moss">{currentDate}</p>
</div>
<button onClick={() => changeDate(1)} className="p-3 text-slate-teal bg-slate-teal/10 hover:bg-slate-teal/30 rounded-2xl transition-colors"><ChevronRight size={24} /></button>
</div>
{currentDayStats.hasWork && (
<div className={`px-5 py-3 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center shadow-sm border transition-colors ${currentDayStats.isComplete ? 'bg-moss/10 text-moss border-moss/20' : 'bg-goldenrod/10 text-goldenrod border-goldenrod/20'}`}>
{currentDayStats.isComplete ? <><CheckCircle size={16} className="mr-2" /> All närvaro rapporterad</> : `Ifyllt: ${currentDayStats.completed} av ${currentDayStats.expected} pers`}
</div>
)}
{!daySchedule || !currentDayStats.hasWork ? (
<div className="bg-eggshell border border-slate-teal/10 p-8 rounded-3xl text-center shadow-sm">
<p className="font-black text-sm text-ebony">Inga schemalagda pass denna dag.</p>
</div>
) : (
teamsToRender.map(teamStr => {
const team = teamStr as 'PF' | 'TU';
const { time: standardTime, shiftId } = getTeamShiftInfo(daySchedule, team);
if (standardTime === 'Ledig') return null;
return (
<TeamAttendanceCard
key={team}
team={team}
currentDate={currentDate}
shiftId={shiftId}
standardTime={standardTime}
periodYouthList={activePeriod.youthList}
attendance={attendance}
setManualAttendance={setManualAttendance}
bulkSetManualAttendance={bulkSetManualAttendance}
addPendingAttendance={addPendingAttendance}
removeAttendanceEntry={removeAttendanceEntry}
/>
);
})
)}
</div>
);
};