UI tweaks

This commit is contained in:
2026-03-17 02:44:24 +01:00 Verified
parent 9b395ef75c
commit ec7bfd7e34
10 changed files with 352 additions and 652 deletions
+108 -210
View File
@@ -11,76 +11,46 @@ import scheduleData from '../data/schedule.json';
import { type AttendanceDataMap, calculateShiftDuration, formatTimeHHMM, getAttendanceKey, isWeekend, parseTimeInput, type Period, toIsoDate } from './adminTypes'; import { type AttendanceDataMap, calculateShiftDuration, formatTimeHHMM, getAttendanceKey, isWeekend, parseTimeInput, type Period, toIsoDate } from './adminTypes';
interface Props { interface Props {
periods: Period[]; periods: Period[]; attendance: AttendanceDataMap;
attendance: AttendanceDataMap;
setManualAttendance: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON', status: 'Absent' | 'Late' | 'Present', hours: number, note?: string) => void; setManualAttendance: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON', status: 'Absent' | 'Late' | 'Present', hours: number, note?: string) => void;
bulkSetManualAttendance: (records: any[]) => void; bulkSetManualAttendance: (records: any[]) => void;
addPendingAttendance: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => void; addPendingAttendance: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => void;
removeAttendanceEntry: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => void; removeAttendanceEntry: (date: string, youthId: string, shiftId: 'MORNING' | 'AFTERNOON') => void;
activePeriodId: string; activePeriodId: string; setActivePeriodId: (id: string) => void;
setActivePeriodId: (id: string) => void;
} }
// NEW: Dynamic Shift Helper
export const getTeamShiftInfo = (daySchedule: any, team: 'PF' | 'TU') => { export const getTeamShiftInfo = (daySchedule: any, team: 'PF' | 'TU') => {
if (!daySchedule) return { time: 'Ledig', shiftId: 'MORNING' as 'MORNING' | 'AFTERNOON' }; if (!daySchedule) return { time: 'Ledig', shiftId: 'MORNING' as 'MORNING' | 'AFTERNOON' };
const pfTime = daySchedule.pilgrimsfalkarna?.time || 'Ledig'; const pfTime = daySchedule.pilgrimsfalkarna?.time || 'Ledig';
const tuTime = daySchedule.tumlarna?.time || 'Ledig'; const tuTime = daySchedule.tumlarna?.time || 'Ledig';
const pfStart = pfTime !== 'Ledig' ? parseInt(pfTime.match(/(\d+):/)?.[1] || '99') : 99; const pfStart = pfTime !== 'Ledig' ? parseInt(pfTime.match(/(\d+):/)?.[1] || '99') : 99;
const tuStart = tuTime !== 'Ledig' ? parseInt(tuTime.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' };
if (team === 'PF') { else return { time: tuTime, shiftId: (tuStart > pfStart) ? 'AFTERNOON' : (pfStart === tuStart ? 'AFTERNOON' : 'MORNING') as 'MORNING' | 'AFTERNOON' };
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 getDaysInPeriod = (start: string, end: string) => {
const days = []; const days = []; let curr = new Date(start + 'T12:00:00'); const endDate = new Date(end + 'T12:00:00');
let curr = new Date(start + 'T12:00:00'); while (curr <= endDate) { days.push(toIsoDate(curr)); curr.setDate(curr.getDate() + 1); }
const endDate = new Date(end + 'T12:00:00');
while (curr <= endDate) {
days.push(toIsoDate(curr));
curr.setDate(curr.getDate() + 1);
}
return days; return days;
}; };
const getDailyCompletionStats = (date: string, period: Period, attendance: AttendanceDataMap) => { const getDailyCompletionStats = (date: string, period: Period, attendance: AttendanceDataMap) => {
const dayNameStr = new Date(date + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase(); const dayNameStr = new Date(date + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase();
const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr); const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr);
if (!daySchedule) return { expected: 0, completed: 0, isComplete: true, hasWork: false }; if (!daySchedule) return { expected: 0, completed: 0, isComplete: true, hasWork: false };
let expected = 0; let expected = 0; let completed = 0; let hasWork = false;
let completed = 0;
let hasWork = false;
// Use dynamic shift checking
if (daySchedule.pilgrimsfalkarna && daySchedule.pilgrimsfalkarna.time !== 'Ledig') { if (daySchedule.pilgrimsfalkarna && daySchedule.pilgrimsfalkarna.time !== 'Ledig') {
hasWork = true; hasWork = true; const { shiftId } = getTeamShiftInfo(daySchedule, 'PF');
const { shiftId } = getTeamShiftInfo(daySchedule, 'PF'); const pfYouth = period.youthList.filter(y => y.team === 'PF'); expected += pfYouth.length;
const pfYouth = period.youthList.filter(y => y.team === 'PF'); pfYouth.forEach(y => { const entry = attendance[getAttendanceKey(date, y.id, shiftId)]; if (entry && entry.status !== 'Pending') completed++; });
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') { if (daySchedule.tumlarna && daySchedule.tumlarna.time !== 'Ledig') {
hasWork = true; hasWork = true; const { shiftId } = getTeamShiftInfo(daySchedule, 'TU');
const { shiftId } = getTeamShiftInfo(daySchedule, 'TU'); const tuYouth = period.youthList.filter(y => y.team === 'TU'); expected += tuYouth.length;
const tuYouth = period.youthList.filter(y => y.team === 'TU'); tuYouth.forEach(y => { const entry = attendance[getAttendanceKey(date, y.id, shiftId)]; if (entry && entry.status !== 'Pending') completed++; });
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 }; return { expected, completed, isComplete: expected > 0 && completed >= expected, hasWork };
}; };
@@ -91,25 +61,17 @@ export const AttendanceTab: React.FC<Props> = ({ periods, attendance, setManualA
useEffect(() => { useEffect(() => {
if (activePeriod) { if (activePeriod) {
const today = toIsoDate(new Date()); const today = toIsoDate(new Date());
if (today >= activePeriod.startDate && today <= activePeriod.endDate) { if (today >= activePeriod.startDate && today <= activePeriod.endDate) setCurrentDate(today);
setCurrentDate(today); else setCurrentDate(activePeriod.startDate);
} else {
setCurrentDate(activePeriod.startDate);
}
} }
}, [activePeriodId, activePeriod]); }, [activePeriodId, activePeriod]);
if (!activePeriod) return <p className="text-center font-bold text-slate-teal mt-10">Ingen period aktiv.</p>; if (!activePeriod) return <p className="text-center font-bold text-slate-teal mt-10">Ingen period aktiv.</p>;
const changeDate = (days: number) => { const changeDate = (days: number) => {
const newDateObj = new Date(currentDate + 'T12:00:00'); const newDateObj = new Date(currentDate + 'T12:00:00'); newDateObj.setDate(newDateObj.getDate() + days);
newDateObj.setDate(newDateObj.getDate() + days); const startObj = new Date(activePeriod.startDate + 'T12:00:00'); const endObj = new Date(activePeriod.endDate + 'T12:00:00');
const startObj = new Date(activePeriod.startDate + 'T12:00:00'); if (newDateObj >= startObj && newDateObj <= endObj) setCurrentDate(toIsoDate(newDateObj));
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 dayNameStr = new Date(currentDate + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' });
@@ -125,7 +87,6 @@ export const AttendanceTab: React.FC<Props> = ({ periods, attendance, setManualA
if (actualHours > 0) setManualAttendance(currentDate, youthId, shiftId, 'Present', actualHours); if (actualHours > 0) setManualAttendance(currentDate, youthId, shiftId, 'Present', actualHours);
}; };
// Sort teams chronologically
const teamsToRender = ['PF', 'TU'].sort((a, b) => { const teamsToRender = ['PF', 'TU'].sort((a, b) => {
const timeA = getTeamShiftInfo(daySchedule, a as 'PF' | 'TU').time; const timeA = getTeamShiftInfo(daySchedule, a as 'PF' | 'TU').time;
const timeB = getTeamShiftInfo(daySchedule, b as 'PF' | 'TU').time; const timeB = getTeamShiftInfo(daySchedule, b as 'PF' | 'TU').time;
@@ -136,71 +97,56 @@ export const AttendanceTab: React.FC<Props> = ({ periods, attendance, setManualA
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="space-y-6 animate-fade-in">
{/* Timeline Overview */} <div className="bg-eggshell border border-slate-teal/10 p-3 sm:p-5 rounded-3xl shadow-sm">
<div className="bg-eggshell border-2 border-slate-teal/20 p-4 md:p-6 rounded-2xl shadow-sm"> <div className="flex justify-between items-center mb-3">
<div className="flex justify-between items-center mb-1"> <h3 className="text-xs font-black text-slate-teal uppercase tracking-widest">Periodöversikt</h3>
<h3 className="text-xs font-black text-slate-teal uppercase tracking-widest ml-1">Periodöversikt</h3> <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">
<select
value={activePeriodId}
onChange={(e) => setActivePeriodId(e.target.value)}
className="bg-white border border-slate-teal/20 p-1.5 rounded-lg font-bold text-slate-teal text-sm cursor-pointer shadow-sm focus:outline-none"
>
{periods.map(p => <option key={p.id} value={p.id}>{p.name}</option>)} {periods.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select> </select>
</div> </div>
<div className="flex overflow-x-auto gap-2.5 pb-3 pt-1 px-2 scrollbar-hide">
<div className="flex overflow-x-auto gap-3 pb-6 pt-4 px-2 scrollbar-hide">
{timelineDays.map(day => { {timelineDays.map(day => {
const stats = getDailyCompletionStats(day, activePeriod, attendance); const stats = getDailyCompletionStats(day, activePeriod, attendance);
const isPast = day < todayIso;
const isToday = day === todayIso;
const isSelected = day === currentDate; const isSelected = day === currentDate;
let bgClass = "bg-white text-ebony border-slate-teal/10";
let bgClass = "bg-white text-ebony border-slate-teal/20";
if (!stats.hasWork) bgClass = "bg-slate-teal/5 text-ebony/40 border-transparent"; if (!stats.hasWork) bgClass = "bg-slate-teal/5 text-ebony/40 border-transparent";
else if (stats.isComplete) bgClass = "bg-moss text-white border-moss"; else if (stats.isComplete) bgClass = "bg-moss text-white border-moss shadow-inner";
else if (isPast || isToday) bgClass = "bg-goldenrod text-white border-goldenrod"; else if (day < todayIso || day === todayIso) bgClass = "bg-goldenrod text-white border-goldenrod shadow-inner";
return ( return (
<button <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'}`}>
key={day} <span className="text-[10px] font-black uppercase tracking-wider">{new Date(day + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'short' })}</span>
onClick={() => setCurrentDate(day)} <span className="text-sm font-black">{new Date(day + 'T12:00:00').getDate()}/{new Date(day + 'T12:00:00').getMonth() + 1}</span>
className={`flex flex-col items-center justify-center min-w-12.5 p-2 rounded-xl border-2 transition-all ${bgClass} ${isSelected ? 'ring-2 ring-slate-teal ring-offset-2 ring-offset-eggshell scale-110 shadow-md' : 'hover:brightness-95'}`}
>
<span className="text-[10px] font-bold uppercase">{new Date(day + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'short' })}</span>
<span className="text-xs font-black">{new Date(day + 'T12:00:00').getDate()}/{new Date(day + 'T12:00:00').getMonth() + 1}</span>
</button> </button>
); );
})} })}
</div> </div>
</div> </div>
{/* Date Navigation */} <div className="flex flex-row justify-between items-center bg-white border border-slate-teal/10 p-3 sm:p-4 rounded-3xl shadow-sm gap-4">
<div className="flex flex-col md:flex-row justify-between items-center bg-eggshell border-2 border-slate-teal/20 p-4 md:p-6 rounded-2xl shadow-sm gap-4"> <button onClick={() => changeDate(-1)} className="p-3 text-slate-teal bg-eggshell/50 hover:bg-slate-teal/10 rounded-2xl transition-colors"><ChevronLeft size={24} /></button>
<div className="flex items-center gap-4 w-full md:w-auto justify-between md:justify-start"> <div className="text-center flex-1">
<button onClick={() => changeDate(-1)} className="p-2 text-slate-teal hover:bg-slate-teal/10 rounded-full transition-colors"><ChevronLeft size={24} /></button> <h2 className="text-lg font-black text-ebony capitalize mb-0.5">{dayNameStr}</h2>
<div className="text-center w-40"> <p className="text-[11px] font-black uppercase tracking-widest text-moss">{currentDate}</p>
<h2 className="text-lg font-black text-ebony capitalize">{dayNameStr}</h2>
<p className="text-xs font-bold text-moss">{currentDate}</p>
</div>
<button onClick={() => changeDate(1)} className="p-2 text-slate-teal hover:bg-slate-teal/10 rounded-full transition-colors"><ChevronRight size={24} /></button>
</div> </div>
<button onClick={() => changeDate(1)} className="p-3 text-slate-teal bg-eggshell/50 hover:bg-slate-teal/10 rounded-2xl transition-colors"><ChevronRight size={24} /></button>
{currentDayStats.hasWork && (
<div className={`w-full md:w-auto px-5 py-2.5 rounded-lg font-bold text-sm flex items-center justify-center border ${currentDayStats.isComplete ? 'bg-moss/20 text-moss border-moss/30' : 'bg-goldenrod/10 text-goldenrod border-goldenrod/30'}`}>
{currentDayStats.isComplete ? <><CheckCircle size={18} className="mr-2" /> Dagen är komplett!</> : `Ifyllt: ${currentDayStats.completed} av ${currentDayStats.expected} pers`}
</div>
)}
</div> </div>
{/* Attendance Lists */} {/* FIX: Status banner is fully restored and green when done! */}
{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 ? ( {!daySchedule || !currentDayStats.hasWork ? (
<div className="bg-eggshell border-2 border-slate-teal/20 p-6 rounded-2xl text-center"><p className="font-bold text-ebony">Inga schemalagda pass denna dag.</p></div> <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 => { teamsToRender.map(teamStr => {
const team = teamStr as 'PF' | 'TU'; const team = teamStr as 'PF' | 'TU';
const { time: standardTime, shiftId } = getTeamShiftInfo(daySchedule, team); const { time: standardTime, shiftId } = getTeamShiftInfo(daySchedule, team);
if (standardTime === 'Ledig') return null; if (standardTime === 'Ledig') return null;
const rawDuration = calculateShiftDuration(standardTime); const rawDuration = calculateShiftDuration(standardTime);
@@ -211,66 +157,40 @@ export const AttendanceTab: React.FC<Props> = ({ periods, attendance, setManualA
const scheduledYouth = activePeriod.youthList.filter(y => y.team === team); const scheduledYouth = activePeriod.youthList.filter(y => y.team === team);
const extraYouth = activePeriod.youthList.filter(y => y.team !== team && attendance[getAttendanceKey(currentDate, y.id, shiftId)]); const extraYouth = activePeriod.youthList.filter(y => y.team !== team && attendance[getAttendanceKey(currentDate, y.id, shiftId)]);
// Display youth sorted by name alphabetically
const displayYouth = [...scheduledYouth, ...extraYouth].sort((a, b) => a.name.localeCompare(b.name)); const displayYouth = [...scheduledYouth, ...extraYouth].sort((a, b) => a.name.localeCompare(b.name));
const availableExtras = activePeriod.youthList.filter(y => !displayYouth.some(dy => dy.id === y.id)).sort((a, b) => a.name.localeCompare(b.name)); const availableExtras = activePeriod.youthList.filter(y => !displayYouth.some(dy => dy.id === y.id)).sort((a, b) => a.name.localeCompare(b.name));
const handleQuickLogAll = () => { const handleQuickLogAll = () => {
const recordsToUpdate: any[] = []; const recordsToUpdate: any[] = [];
displayYouth.forEach(y => { displayYouth.forEach(y => {
const entry = attendance[getAttendanceKey(currentDate, y.id, shiftId)]; const entry = attendance[getAttendanceKey(currentDate, y.id, shiftId)];
if (!entry || entry.status === 'Pending') { if (!entry || entry.status === 'Pending') {
recordsToUpdate.push({ recordsToUpdate.push({ date: currentDate, youthId: y.id, shiftId, status: 'Present', hoursWorked: actualDuration, weightedHours: weightedDuration, note: '' });
date: currentDate,
youthId: y.id,
shiftId: shiftId,
status: 'Present',
hoursWorked: actualDuration,
weightedHours: weightedDuration,
note: ''
});
} }
}); });
if (recordsToUpdate.length > 0) { if (recordsToUpdate.length > 0) bulkSetManualAttendance(recordsToUpdate);
bulkSetManualAttendance(recordsToUpdate);
}
}; };
return ( return (
<div key={team} className="bg-eggshell border-2 border-slate-teal/20 p-4 md:p-6 rounded-2xl shadow-sm"> <div key={team} className="bg-eggshell border border-slate-teal/10 p-4 md:p-5 rounded-3xl shadow-sm">
<div className="flex flex-col md:flex-row md:justify-between md:items-center mb-6 gap-3"> <div className="flex flex-wrap justify-between items-center mb-4 gap-3">
<h2 className="text-xl font-black text-ebony uppercase tracking-widest flex items-center"> <div className="flex items-center">
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${team === 'PF' ? 'bg-gold' : 'bg-seafoam'} mr-3 shrink-0 shadow-sm`}> <div className={`flex items-center justify-center w-10 h-10 rounded-xl ${team === 'PF' ? 'bg-gold' : 'bg-seafoam'} mr-3 shadow-inner shrink-0`}>
<Image <Image src={team === 'PF' ? falconIcon : porpoiseIcon} alt={team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'} width={18} height={18} />
src={team === 'PF' ? falconIcon : porpoiseIcon}
alt={team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'}
width={18}
height={18}
/>
</div> </div>
{team === 'PF' ? 'Pilgrimsfalkarna' : 'Tumlarna'} <h2 className="text-base font-black text-ebony uppercase tracking-widest">{team === 'PF' ? 'Pilgrimsfalkarna' : 'Tumlarna'}</h2>
</h2> </div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-3"> <div className="flex items-center gap-2 bg-white px-3 py-1.5 rounded-xl border border-slate-teal/5 text-xs text-ebony font-bold shadow-sm">
<div className="flex items-center gap-3 bg-slate-teal/5 px-4 py-2 rounded-lg border border-slate-teal/10 text-sm"> <FileText size={14} className="text-slate-teal" /> {standardTime} ({formatTimeHHMM(rawDuration)})
<FileText size={16} className="text-slate-teal" />
<span className="font-bold text-ebony">{standardTime} ({formatTimeHHMM(rawDuration)})</span>
{isWknd && <span className="text-xs font-bold text-goldenrod bg-goldenrod/10 px-2 py-0.5 rounded border border-goldenrod/20">Helg (-30m)</span>}
</div> </div>
<button onClick={handleQuickLogAll} className="flex items-center gap-1.5 bg-moss/10 text-moss hover:bg-moss hover:text-white px-3 py-1.5 rounded-xl font-black uppercase tracking-widest text-[10px] transition-colors shadow-sm">
<button <Zap size={14} /> Alla närvarande
onClick={handleQuickLogAll}
className="flex items-center gap-2 bg-moss/10 text-moss hover:bg-moss/20 border border-moss/20 px-4 py-2 rounded-lg font-bold text-sm transition-colors"
title="Sätt alla som inte har en ifylld tid till 'Hela passet'"
>
<Zap size={16} /> Snabblogga alla
</button> </button>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-2">
{displayYouth.map(youth => { {displayYouth.map(youth => {
const entry = attendance[getAttendanceKey(currentDate, youth.id, shiftId)]; const entry = attendance[getAttendanceKey(currentDate, youth.id, shiftId)];
const isExtra = youth.team !== team; const isExtra = youth.team !== team;
@@ -278,90 +198,68 @@ export const AttendanceTab: React.FC<Props> = ({ periods, attendance, setManualA
const isCompleted = entry && !isPending; const isCompleted = entry && !isPending;
return ( return (
<div key={youth.id} className={`flex flex-col xl:flex-row xl:justify-between xl:items-center p-3 rounded-xl border-2 transition-colors ${isCompleted ? 'bg-moss/10 border-moss/40' : (isPending ? 'bg-goldenrod/5 border-goldenrod/40' : 'bg-white border-slate-teal/10 shadow-sm')}`}> <div key={youth.id} className={`flex flex-col md:flex-row md:justify-between md:items-center p-3 rounded-2xl border transition-colors ${isCompleted ? 'bg-moss/5 border-moss/20' : (isPending ? 'bg-goldenrod/5 border-goldenrod/30' : 'bg-white border-slate-teal/5 shadow-sm')}`}>
<div className="flex items-center gap-3 mb-3 md:mb-0">
<div className="flex items-center gap-3 mb-3 xl:mb-0"> {isCompleted ? <CheckCircle size={20} className="text-moss shrink-0" /> : <div className="w-5 h-5 rounded-full border-2 border-slate-teal/20 shrink-0 bg-white"></div>}
{isCompleted ? <CheckCircle size={20} className="text-moss shrink-0" /> : <div className="w-5 h-5 rounded-full border-2 border-slate-teal/20 shrink-0"></div>} <div className={`flex items-center justify-center w-6 h-6 rounded-full ${youth.team === 'PF' ? 'bg-gold' : 'bg-seafoam'} shrink-0 shadow-inner`}>
<span className={`font-bold text-lg ${isCompleted ? 'text-moss' : 'text-ebony'}`}> <Image src={youth.team === 'PF' ? falconIcon : porpoiseIcon} alt={youth.team === 'PF' ? 'PF' : 'TU'} width={12} height={12} />
{youth.name} </div>
{isExtra && <span className="ml-2 text-[10px] text-slate-teal bg-slate-teal/10 px-1.5 py-0.5 rounded uppercase tracking-wider">Extra pass</span>} <span className={`font-bold text-base ${isCompleted ? 'text-moss' : 'text-ebony'}`}>
{youth.name} {isExtra && <span className="ml-2 text-[10px] text-slate-teal bg-slate-teal/10 px-2 py-0.5 rounded-lg uppercase tracking-widest">Extra pass</span>}
</span> </span>
</div> </div>
<div className="flex items-center justify-end gap-3 w-full xl:w-auto flex-wrap"> <div className="flex items-center justify-end gap-3 w-full md:w-auto flex-wrap">
{isPending && ( {isPending && <span className="text-[10px] font-black uppercase tracking-widest bg-goldenrod/10 text-goldenrod px-3 py-1.5 rounded-xl">Väntar...</span>}
<span className="text-xs font-bold bg-goldenrod/10 text-goldenrod px-3 py-1.5 rounded-lg border border-goldenrod/20 shadow-sm animate-pulse">
Väntar tid...
</span>
)}
{isCompleted && ( {isCompleted && (
<span className="text-xs font-bold bg-white text-moss px-3 py-1.5 rounded-lg border border-moss/20 shadow-sm"> <span className="text-xs font-black uppercase tracking-widest bg-white text-moss px-3 py-1.5 rounded-xl border border-moss/10 shadow-sm">
{entry.status === 'Absent' ? entry.note : `${formatTimeHHMM(entry.hoursWorked)} arbetat (+${entry.weightedHours.toFixed(1)}t pott)`} {entry.status === 'Absent' ? entry.note : `${formatTimeHHMM(entry.hoursWorked)} (+${entry.weightedHours.toFixed(1)}h)`}
</span> </span>
)} )}
<div className="flex flex-wrap items-center gap-2"> {(!entry || isPending) && (
{(!entry || isPending) && ( <>
<> <button onClick={() => markStandardAttendance(youth.id, team as 'PF' | 'TU', shiftId)} className="bg-slate-teal/10 text-slate-teal hover:bg-slate-teal hover:text-white px-3 py-1.5 rounded-xl font-black uppercase tracking-widest text-[10px] flex items-center gap-1.5 transition-colors shadow-sm">
<button onClick={() => markStandardAttendance(youth.id, team as 'PF' | 'TU', shiftId)} className="bg-slate-teal/10 text-slate-teal hover:bg-slate-teal/20 px-3 py-1.5 rounded-lg font-bold text-sm flex items-center gap-1.5 transition-colors"> <ClipboardCheck size={14} /> Hela passet
<ClipboardCheck size={16} /> Hela passet
</button>
<button onClick={() => {
const input = prompt(`Timmar arbetade (t.ex. 2:30 eller 2.5):`, formatTimeHHMM(actualDuration));
const hrs = input ? parseTimeInput(input) : 0;
if (hrs > 0) setManualAttendance(currentDate, youth.id, shiftId, 'Present', hrs);
}} className="bg-goldenrod/10 text-goldenrod hover:bg-goldenrod/20 px-3 py-1.5 rounded-lg font-bold text-sm transition-colors">
<Edit3 size={16} />
</button>
<select
value=""
onChange={(e) => {
if (!e.target.value) return;
let reason = e.target.value;
if (reason === 'Custom') reason = prompt('Ange anledning:') || 'Frånvarande';
setManualAttendance(currentDate, youth.id, shiftId, 'Absent', 0, reason);
}}
className="bg-goldenrod/10 text-goldenrod border border-goldenrod/20 hover:bg-goldenrod/20 px-2 py-1.5 rounded-lg font-bold text-sm transition-colors cursor-pointer appearance-none text-center outline-none"
>
<option value="">+ Frånvaro...</option>
<option value="Sjuk">Sjuk</option>
<option value="Uteblev">Uteblev</option>
<option value="Ledig">Ledig</option>
<option value="Custom">Annan...</option>
</select>
</>
)}
{entry && (
<button
onClick={() => removeAttendanceEntry(currentDate, youth.id, shiftId)}
className="text-moss hover:text-goldenrod p-2 bg-white rounded-lg border border-moss/20 shadow-sm transition-colors"
title="Ångra och ta bort närvaro"
>
<Undo size={16} />
</button> </button>
)} <button onClick={() => {
</div> const input = prompt(`Timmar arbetade:`, formatTimeHHMM(actualDuration));
const hrs = input ? parseTimeInput(input) : 0;
if (hrs > 0) setManualAttendance(currentDate, youth.id, shiftId, 'Present', hrs);
}} className="bg-goldenrod/10 text-goldenrod hover:bg-goldenrod hover:text-white p-1.5 rounded-xl transition-colors shadow-sm">
<Edit3 size={16} />
</button>
<select
value=""
onChange={(e) => {
if (!e.target.value) return;
let reason = e.target.value;
if (reason === 'Custom') reason = prompt('Ange anledning:') || 'Frånvarande';
setManualAttendance(currentDate, youth.id, shiftId, 'Absent', 0, reason);
}}
className="bg-goldenrod/10 text-goldenrod px-2 py-1.5 rounded-xl font-black uppercase tracking-widest text-[10px] outline-none shadow-sm"
>
<option value="">+ Frånvaro</option>
<option value="Sjuk">Sjuk</option><option value="Uteblev">Uteblev</option><option value="Ledig">Ledig</option><option value="Custom">Annan...</option>
</select>
</>
)}
{entry && (
<button onClick={() => removeAttendanceEntry(currentDate, youth.id, shiftId)} className="text-emergency/60 hover:text-emergency p-1.5 bg-white rounded-xl border border-emergency/10 transition-colors shadow-sm">
<Undo size={16} />
</button>
)}
</div> </div>
</div> </div>
); );
})} })}
{availableExtras.length > 0 && ( {availableExtras.length > 0 && (
<div className="pt-2 border-t border-slate-teal/10 mt-2"> <div className="pt-3 mt-3 border-t border-slate-teal/5">
<select <select value="" onChange={(e) => { if (e.target.value) addPendingAttendance(currentDate, e.target.value, shiftId); }} className="bg-white border border-slate-teal/10 p-3 rounded-2xl font-bold text-slate-teal text-sm w-full outline-none shadow-sm">
value="" <option value="">+ Lägg till extra person...</option>
onChange={(e) => { {availableExtras.map(y => <option key={y.id} value={y.id}>{y.name}</option>)}
if (e.target.value) {
addPendingAttendance(currentDate, e.target.value, shiftId);
}
}}
className="bg-white border border-slate-teal/20 p-2 rounded-lg font-bold text-slate-teal text-sm w-full md:w-auto cursor-pointer"
>
<option value="">+ Lägg till extra person detta pass...</option>
{availableExtras.map(y => (
<option key={y.id} value={y.id}>{y.name}</option>
))}
</select> </select>
</div> </div>
)} )}
+68 -116
View File
@@ -12,11 +12,7 @@ import { type AttendanceDataMap, formatTimeHHMM, isWeekend, type Period, type Ro
import { getTeamShiftInfo } from './AttendanceTab'; import { getTeamShiftInfo } from './AttendanceTab';
interface Props { interface Props {
periods: Period[]; periods: Period[]; attendance: AttendanceDataMap; activePeriodId: string; setActivePeriodId: (id: string) => void; currentUserRole: Role;
attendance: AttendanceDataMap;
activePeriodId: string;
setActivePeriodId: (id: string) => void;
currentUserRole: Role;
} }
const POT_HOUR_LIMIT = 90; const POT_HOUR_LIMIT = 90;
@@ -32,25 +28,18 @@ export const ReportTab: React.FC<Props> = ({ periods, attendance, activePeriodId
let total = 0; let total = 0;
for (const key in attendance) { for (const key in attendance) {
const entry = attendance[key]; const entry = attendance[key];
if (entry.youthId === youthId && entry.date >= activePeriod.startDate && entry.date <= activePeriod.endDate) { if (entry.youthId === youthId && entry.date >= activePeriod.startDate && entry.date <= activePeriod.endDate) total += entry.weightedHours;
total += entry.weightedHours;
}
} }
return total; return total;
}; };
const getTimelineForYouth = (youthId: string) => { const getTimelineForYouth = (youthId: string) => {
return Object.values(attendance) return Object.values(attendance).filter(a => a.youthId === youthId && a.date >= activePeriod.startDate && a.date <= activePeriod.endDate)
.filter(a => a.youthId === youthId && a.date >= activePeriod.startDate && a.date <= activePeriod.endDate) .sort((a, b) => { if (a.date !== b.date) return a.date.localeCompare(b.date); return a.shiftId === 'MORNING' ? -1 : 1; });
.sort((a, b) => {
if (a.date !== b.date) return a.date.localeCompare(b.date);
return a.shiftId === 'MORNING' ? -1 : 1;
});
}; };
const exportToCSV = () => { const exportToCSV = () => {
if (!activePeriod) return; if (!activePeriod) return;
let csvContent = "Datum;Pass;Namn;Lag;Status;Arbetad Tid (HH:MM);Viktad Pott;Anteckning\n"; let csvContent = "Datum;Pass;Namn;Lag;Status;Arbetad Tid (HH:MM);Viktad Pott;Anteckning\n";
const entries = Object.values(attendance).filter(a => a.date >= activePeriod.startDate && a.date <= activePeriod.endDate); const entries = Object.values(attendance).filter(a => a.date >= activePeriod.startDate && a.date <= activePeriod.endDate);
@@ -65,22 +54,16 @@ export const ReportTab: React.FC<Props> = ({ periods, attendance, activePeriodId
entries.forEach(entry => { entries.forEach(entry => {
const youth = activePeriod.youthList.find(y => y.id === entry.youthId); const youth = activePeriod.youthList.find(y => y.id === entry.youthId);
if (!youth) return; if (!youth) return;
const shift = isWeekend(entry.date) ? 'Hela dagen' : (entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag');
const isWknd = isWeekend(entry.date);
const shift = isWknd ? 'Hela dagen' : (entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag');
const teamName = youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'; const teamName = youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare';
const worked = formatTimeHHMM(entry.hoursWorked); const worked = formatTimeHHMM(entry.hoursWorked);
const weighted = entry.weightedHours.toFixed(2).replace('.', ','); const weighted = entry.weightedHours.toFixed(2).replace('.', ',');
const note = entry.note || ''; const note = entry.note || '';
csvContent += `${entry.date};${shift};${youth.name};${teamName};${entry.status};${worked};${weighted};${note}\n`; csvContent += `${entry.date};${shift};${youth.name};${teamName};${entry.status};${worked};${weighted};${note}\n`;
}); });
csvContent += "\nSummering (Timpott)\nNamn;Lag;Total Viktad Pott\n"; csvContent += "\nSummering (Timpott)\nNamn;Lag;Total Viktad Pott\n";
const sortedYouth = [...activePeriod.youthList].sort((a, b) => a.name.localeCompare(b.name)); const sortedYouth = [...activePeriod.youthList].sort((a, b) => a.name.localeCompare(b.name));
sortedYouth.forEach(youth => { sortedYouth.forEach(youth => {
const total = getPeriodHoursTotal(youth.id).toFixed(2).replace('.', ','); const total = getPeriodHoursTotal(youth.id).toFixed(2).replace('.', ',');
const teamName = youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'; const teamName = youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare';
@@ -90,149 +73,121 @@ export const ReportTab: React.FC<Props> = ({ periods, attendance, activePeriodId
const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
link.setAttribute("href", url); link.setAttribute("href", url); link.setAttribute("download", `Narvaro_${activePeriod.name.replace(/ /g, '_')}.csv`);
link.setAttribute("download", `Narvaro_${activePeriod.name.replace(/ /g, '_')}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}; };
const sortedActiveYouth = [...activePeriod.youthList].sort((a, b) => a.name.localeCompare(b.name)); const sortedActiveYouth = [...activePeriod.youthList].sort((a, b) => a.name.localeCompare(b.name));
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="space-y-6 animate-fade-in">
{/* Header / Period Selector */} <div className="bg-eggshell border border-slate-teal/10 p-3 sm:p-5 rounded-3xl shadow-sm flex flex-col md:flex-row justify-between items-center gap-4">
<div className="bg-eggshell border-2 border-slate-teal/20 p-4 md:p-6 rounded-2xl shadow-sm text-center flex flex-col md:flex-row justify-between items-center gap-4"> <select value={activePeriodId} onChange={(e) => { setActivePeriodId(e.target.value); setExpandedYouthId(null); }} className="bg-white border border-slate-teal/10 p-3 rounded-xl font-bold text-slate-teal text-sm w-full md:w-auto focus:outline-none shadow-sm">
<select
value={activePeriodId}
onChange={(e) => { setActivePeriodId(e.target.value); setExpandedYouthId(null); }}
className="bg-white border border-slate-teal/20 p-2.5 rounded-lg font-bold text-slate-teal w-full md:w-auto focus:outline-none"
>
{periods.map(p => <option key={p.id} value={p.id}>{p.name}</option>)} {periods.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select> </select>
<div> <div className="text-center md:text-right">
<h2 className="text-xl font-black text-ebony uppercase tracking-widest">{activePeriod.name}</h2> <h2 className="text-base font-black text-ebony uppercase tracking-widest">{activePeriod.name}</h2>
<p className="font-bold text-moss">{activePeriod.startDate} {activePeriod.endDate}</p> <p className="text-xs font-bold text-moss">{activePeriod.startDate} {activePeriod.endDate}</p>
</div> </div>
</div> </div>
{/* PERIOD HOUR POT REPORT */} <div className="bg-eggshell border border-slate-teal/10 p-3 sm:p-5 rounded-3xl shadow-sm">
<div className="bg-eggshell border-2 border-slate-teal/20 p-4 md:p-6 rounded-2xl shadow-sm"> <div className="flex flex-col md:flex-row justify-between md:items-center mb-6 gap-3">
<div className="flex flex-col md:flex-row justify-between md:items-center mb-8 gap-4 px-1 md:px-2"> <h2 className="text-base font-black text-ebony uppercase tracking-widest flex items-center">
<h2 className="text-xl font-black text-ebony uppercase tracking-widest flex items-center"> <Users className="mr-2 text-slate-teal" size={20} /> Timrapport
<Users className="mr-3 text-slate-teal" size={24} />
Timrapport
</h2> </h2>
{currentUserRole !== 'Viewer' && ( {currentUserRole !== 'Viewer' && (
<button <button onClick={exportToCSV} className="flex items-center justify-center gap-2 bg-seafoam text-eggshell font-black uppercase tracking-widest text-[10px] px-5 py-3 rounded-xl hover:bg-slate-teal transition-colors shadow-sm">
onClick={exportToCSV} <Download size={16} /> Exportera till Excel
className="flex items-center justify-center gap-2 bg-seafoam/20 hover:bg-seafoam/40 text-slate-teal font-bold px-5 py-2.5 rounded-lg transition-colors border border-seafoam/30 w-full md:w-auto"
>
<Download size={18} />
Exportera till Excel
</button> </button>
)} )}
</div> </div>
<div className="space-y-4"> <div className="space-y-3">
{sortedActiveYouth.map(youth => { {sortedActiveYouth.map(youth => {
const total = getPeriodHoursTotal(youth.id); const total = getPeriodHoursTotal(youth.id);
const warningStatus: 'none' | 'yellow' | 'red' = total > POT_HOUR_LIMIT ? 'red' : (total >= POT_HOUR_LIMIT - 10 ? 'yellow' : 'none'); const warningStatus = total > POT_HOUR_LIMIT ? 'red' : (total >= POT_HOUR_LIMIT - 10 ? 'yellow' : 'none');
const isExpanded = expandedYouthId === youth.id; const isExpanded = expandedYouthId === youth.id;
const timeline = getTimelineForYouth(youth.id); const timeline = getTimelineForYouth(youth.id);
return ( return (
<div key={youth.id} className="bg-white border border-slate-teal/10 rounded-xl shadow-inner overflow-hidden"> <div key={youth.id} className={`bg-white border transition-all duration-300 rounded-2xl overflow-hidden ${isExpanded ? 'border-slate-teal/20 shadow-md' : 'border-slate-teal/10 shadow-sm hover:border-slate-teal/20'}`}>
<div className="flex flex-col md:flex-row md:justify-between md:items-center p-6 md:px-8 gap-6">
<div className="flex flex-col gap-2"> <div
<div className="flex items-center gap-3"> onClick={() => setExpandedYouthId(isExpanded ? null : youth.id)}
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${youth.team === 'PF' ? 'bg-gold' : 'bg-seafoam'} shrink-0 shadow-sm`}> className="flex flex-col sm:flex-row sm:justify-between sm:items-center p-4 gap-4 cursor-pointer hover:bg-slate-teal/5 transition-colors"
<Image >
src={youth.team === 'PF' ? falconIcon : porpoiseIcon} <div className="flex items-center gap-4">
alt={youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'} <div className={`flex items-center justify-center w-12 h-12 rounded-2xl ${youth.team === 'PF' ? 'bg-gold' : 'bg-seafoam'} shrink-0 shadow-inner`}>
width={18} <Image src={youth.team === 'PF' ? falconIcon : porpoiseIcon} alt={youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'} width={24} height={24} />
height={18} </div>
/> <div className="flex flex-col">
<span className="font-black text-ebony text-xl leading-tight">{youth.name}</span>
<div className="flex items-center text-[10px] font-black uppercase tracking-widest text-slate-teal mt-1">
{isExpanded ? <ChevronUp size={14} className="mr-1" /> : <ChevronDown size={14} className="mr-1" />}
{isExpanded ? 'Dölj detaljer' : 'Klicka för detaljer'}
</div> </div>
<span className="font-bold text-ebony text-xl">{youth.name}</span>
</div> </div>
<button
onClick={() => setExpandedYouthId(isExpanded ? null : youth.id)}
className="flex items-center text-xs font-bold text-slate-teal hover:text-ebony transition-colors w-fit bg-slate-teal/5 px-2.5 py-1.5 rounded mt-1"
>
{isExpanded ? <ChevronUp size={14} className="mr-1" /> : <ChevronDown size={14} className="mr-1" />}
{isExpanded ? 'Dölj detaljer' : 'Visa detaljer'}
</button>
</div> </div>
<div className="flex items-center justify-end gap-5 w-full md:w-auto"> <div className="flex items-center justify-end gap-4">
{warningStatus === 'red' && <AlertTriangle className="text-goldenrod shrink-0" size={32} />} {warningStatus === 'red' && <AlertTriangle className="text-goldenrod shrink-0" size={28} />}
<div className="text-right min-w-30 md:min-w-37.5"> <div className="text-right">
<div className={`text-3xl md:text-4xl font-black tracking-tight ${warningStatus === 'red' ? 'text-goldenrod' : (warningStatus === 'yellow' ? 'text-goldenrod/80' : 'text-slate-teal')}`}> <div className={`text-3xl md:text-4xl font-black tracking-tight ${warningStatus === 'red' ? 'text-goldenrod' : (warningStatus === 'yellow' ? 'text-goldenrod/80' : 'text-slate-teal')}`}>
{formatHours(total)}<span className="text-lg font-bold text-ebony/60"> / {POT_HOUR_LIMIT}t</span> {formatHours(total)}<span className="text-sm font-bold text-ebony/40"> / {POT_HOUR_LIMIT}h</span>
</div> </div>
<p className="text-[11px] font-bold text-ebony/60 uppercase tracking-widest mt-1 pr-1">Viktade timmar</p> <p className="text-[10px] font-black uppercase tracking-widest mt-0.5 text-ebony/50">Viktade timmar</p>
</div> </div>
</div> </div>
</div> </div>
{isExpanded && ( {isExpanded && (
<div className="bg-slate-teal/5 border-t border-slate-teal/10 p-5 md:p-6"> <div className="bg-slate-teal/5 border-t border-slate-teal/10 p-4 md:p-6">
<h4 className="text-sm font-black text-ebony uppercase tracking-widest mb-4 flex items-center"> <h4 className="text-xs font-black text-ebony uppercase tracking-widest mb-4 flex items-center">
<Clock size={16} className="mr-2 text-slate-teal" /> Arbetspass & Frånvaro <Clock size={16} className="mr-2 text-slate-teal" /> Arbetspass
</h4> </h4>
{timeline.length === 0 ? ( {timeline.length === 0 ? (
<p className="text-sm font-bold text-slate-teal/60 italic">Ingen närvaro loggad ännu.</p> <p className="text-sm font-bold text-slate-teal/60 italic">Ingen närvaro loggad.</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{timeline.map((entry, idx) => { {timeline.map((entry, idx) => {
const isWknd = isWeekend(entry.date); const isWknd = isWeekend(entry.date);
// Grab schedule for this exact day to check for 'Extra' shifts accurately!
const dayNameStr = new Date(entry.date + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase(); const dayNameStr = new Date(entry.date + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase();
const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr); const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr);
// FIX: Bulletproof isExtra logic that catches weekend-workers too!
let isExtra = false; let isExtra = false;
if (!isWknd && daySchedule) { if (daySchedule) {
const expectedShiftId = getTeamShiftInfo(daySchedule, youth.team).shiftId; const expectedShift = getTeamShiftInfo(daySchedule, youth.team);
isExtra = entry.shiftId !== expectedShiftId; if (expectedShift.time.toLowerCase() === 'ledig') {
isExtra = true;
} else if (!isWknd) {
isExtra = entry.shiftId !== expectedShift.shiftId;
}
} else {
isExtra = true;
} }
// Correct display logic: "Hela dagen" for weekends const shiftLabel = isWknd ? 'Heldag' : (entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag');
const shiftLabel = isWknd ? 'Hela dagen' : (entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag');
return ( return (
<div key={idx} className="flex flex-col sm:flex-row sm:justify-between sm:items-center bg-white border border-slate-teal/10 p-3 rounded-lg text-sm"> <div key={idx} className="flex flex-col sm:flex-row sm:justify-between sm:items-center bg-white p-3 rounded-xl border border-slate-teal/5 text-sm gap-2">
<div className="flex items-center flex-wrap gap-2 mb-2 sm:mb-0"> <div className="flex items-center flex-wrap gap-2">
<span className="font-bold text-ebony min-w-25">{entry.date}</span> <span className="font-black text-ebony min-w-24">{entry.date}</span>
<span className="text-[10px] font-black uppercase tracking-widest text-slate-teal bg-slate-teal/10 px-2 py-1 rounded-lg">{shiftLabel}</span>
<span className="text-xs font-bold text-slate-teal bg-slate-teal/10 px-2 py-0.5 rounded"> {isExtra && <span className="text-[10px] font-black uppercase tracking-widest text-slate-teal bg-slate-teal/10 px-2 py-1 rounded-lg">Extra</span>}
{shiftLabel} {isWknd && <span className="text-[10px] font-black uppercase tracking-widest text-goldenrod bg-goldenrod/10 px-2 py-1 rounded-lg">Helg</span>}
</span>
{isExtra && (
<span className="text-[10px] font-bold text-slate-teal bg-slate-teal/10 px-1.5 py-0.5 rounded uppercase tracking-wider">
Extra pass
</span>
)}
{isWknd && <span className="text-xs font-bold text-goldenrod bg-goldenrod/10 px-2 py-0.5 rounded border border-goldenrod/20">Helg</span>}
</div> </div>
<div className="flex items-center justify-between sm:justify-end gap-4 w-full sm:w-auto">
{entry.status === 'Pending' ? <span className="text-[10px] font-black uppercase tracking-widest text-goldenrod bg-goldenrod/10 px-2 py-1 rounded-lg">Väntar...</span> :
entry.status === 'Absent' ? <span className="text-[10px] font-black uppercase tracking-widest text-goldenrod bg-goldenrod/10 px-2 py-1 rounded-lg">{entry.note || 'Frånvarande'}</span> :
<span className="text-[10px] font-black uppercase tracking-widest text-moss bg-moss/10 px-2 py-1 rounded-lg">{entry.status === 'Late' ? 'Manuell' : 'Närvarande'}</span>}
<div className="flex items-center gap-4"> <span className="font-black text-slate-teal text-base">
{entry.status === 'Pending' ? ( {formatTimeHHMM(entry.hoursWorked)} <span className="text-xs opacity-70">(+{entry.weightedHours.toFixed(1)}h)</span>
<span className="font-bold text-goldenrod bg-goldenrod/10 px-2 py-0.5 rounded">Väntar registrering</span> </span>
) : entry.status === 'Absent' ? (
<span className="font-bold text-goldenrod bg-goldenrod/10 px-2 py-0.5 rounded">{entry.note || 'Frånvarande'}</span>
) : (
<span className="font-bold text-moss bg-moss/10 px-2 py-0.5 rounded">{entry.status === 'Late' ? 'Manuell tid' : 'Närvarande'}</span>
)}
<div className="text-right min-w-37.5">
<span className="font-bold text-ebony">{formatTimeHHMM(entry.hoursWorked)} arbetat</span>
<span className="text-slate-teal font-black ml-2"> +{entry.weightedHours.toFixed(1)} pott</span>
</div>
</div> </div>
</div> </div>
); );
@@ -244,9 +199,6 @@ export const ReportTab: React.FC<Props> = ({ periods, attendance, activePeriodId
</div> </div>
); );
})} })}
{activePeriod.youthList.length === 0 && (
<p className="text-sm font-bold text-slate-teal/60 italic text-center">Inga ungdomar i denna period.</p>
)}
</div> </div>
</div> </div>
</div> </div>
+38 -75
View File
@@ -10,12 +10,9 @@ import porpoiseIcon from '../assets/porpoise.svg';
import { type Period, type Youth } from './adminTypes'; import { type Period, type Youth } from './adminTypes';
interface Props { interface Props {
periods: Period[]; periods: Period[]; createPeriod: (name: string, start: string) => void;
createPeriod: (name: string, start: string) => void; deletePeriod: (id: string) => void; bulkAddYouth: (periodId: string, text: string, team: 'PF' | 'TU') => void;
deletePeriod: (id: string) => void; removeYouth: (periodId: string, youthId: string) => void; isOffline: boolean;
bulkAddYouth: (periodId: string, text: string, team: 'PF' | 'TU') => void;
removeYouth: (periodId: string, youthId: string) => void;
isOffline: boolean;
} }
export const SetupTab: React.FC<Props> = ({ periods, createPeriod, deletePeriod, bulkAddYouth, removeYouth, isOffline }) => { export const SetupTab: React.FC<Props> = ({ periods, createPeriod, deletePeriod, bulkAddYouth, removeYouth, isOffline }) => {
@@ -24,40 +21,24 @@ export const SetupTab: React.FC<Props> = ({ periods, createPeriod, deletePeriod,
const [expandedPeriod, setExpandedPeriod] = useState<string | null>(null); const [expandedPeriod, setExpandedPeriod] = useState<string | null>(null);
const handleCreate = () => { const handleCreate = () => {
if (isOffline) { if (isOffline) { alert("Du måste vara ansluten till internet för att skapa en ny period."); return; }
alert("Du måste vara ansluten till internet för att skapa en ny period.");
return;
}
const name = (document.getElementById('periodName') as HTMLInputElement).value; const name = (document.getElementById('periodName') as HTMLInputElement).value;
const start = (document.getElementById('periodStart') as HTMLInputElement).value; const start = (document.getElementById('periodStart') as HTMLInputElement).value;
if (name && start) createPeriod(name, start); if (name && start) createPeriod(name, start);
}; };
const handleDeletePeriod = (id: string) => { const handleDeletePeriod = (id: string) => {
if (isOffline) { if (isOffline) { alert("Åtgärd nekad: Du måste vara ansluten till internet för att ta bort en period."); return; }
alert("Åtgärd nekad: Du måste vara ansluten till internet för att ta bort en period."); if (window.confirm('Är du säker på att du vill ta bort hela perioden?')) deletePeriod(id);
return;
}
if (window.confirm('Är du säker på att du vill ta bort hela perioden? All närvarodata kopplad till perioden kommer försvinna!')) {
deletePeriod(id);
}
}; };
const handleRemoveYouth = (periodId: string, youthId: string, youthName: string) => { const handleRemoveYouth = (periodId: string, youthId: string, youthName: string) => {
if (isOffline) { if (isOffline) { alert("Åtgärd nekad: Du måste vara ansluten till internet för att ta bort en ungdom."); return; }
alert("Åtgärd nekad: Du måste vara ansluten till internet för att ta bort en ungdom."); if (window.confirm(`Är du säker på att du vill ta bort ${youthName} från perioden?`)) removeYouth(periodId, youthId);
return;
}
if (window.confirm(`Är du säker på att du vill ta bort ${youthName} från perioden?`)) {
removeYouth(periodId, youthId);
}
}; };
const handleBulkAdd = () => { const handleBulkAdd = () => {
if (isOffline) { if (isOffline) { alert("Åtgärd nekad!"); return; }
alert("Åtgärd nekad: Du måste vara ansluten till internet för att importera ungdomar.");
return;
}
if (expandedPeriod) { if (expandedPeriod) {
const processedText = bulkText.split('\n').map(line => { const processedText = bulkText.split('\n').map(line => {
const parts = line.split(/[,|-]/).map(p => p.trim()); const parts = line.split(/[,|-]/).map(p => p.trim());
@@ -69,37 +50,25 @@ export const SetupTab: React.FC<Props> = ({ periods, createPeriod, deletePeriod,
} }
return line; return line;
}).join('\n'); }).join('\n');
bulkAddYouth(expandedPeriod, processedText, bulkTeam); bulkAddYouth(expandedPeriod, processedText, bulkTeam);
setBulkText(''); setBulkText('');
} }
}; };
// Helper to render a group of youth
const renderYouthGroup = (youthList: Youth[], periodId: string) => { const renderYouthGroup = (youthList: Youth[], periodId: string) => {
if (youthList.length === 0) return null; if (youthList.length === 0) return null;
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
{youthList.map(youth => ( {youthList.map(youth => (
<div key={youth.id} className="flex justify-between items-center bg-white border border-slate-teal/10 p-3 rounded-lg"> <div key={youth.id} className="flex justify-between items-center bg-white border border-slate-teal/10 p-3 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${youth.team === 'PF' ? 'bg-gold' : 'bg-seafoam'} shrink-0 shadow-sm`}> <div className={`flex items-center justify-center w-8 h-8 rounded-xl ${youth.team === 'PF' ? 'bg-gold' : 'bg-seafoam'} shrink-0 shadow-inner`}>
<Image <Image src={youth.team === 'PF' ? falconIcon : porpoiseIcon} alt={youth.team === 'PF' ? 'PF' : 'TU'} width={16} height={16} />
src={youth.team === 'PF' ? falconIcon : porpoiseIcon}
alt={youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'}
width={18}
height={18}
/>
</div> </div>
<span className="font-bold text-ebony text-lg">{youth.name}</span> <span className="font-bold text-ebony text-base">{youth.name}</span>
</div> </div>
<button <button onClick={() => handleRemoveYouth(periodId, youth.id, youth.name)} className={`p-2 transition-colors rounded-lg ${isOffline ? 'text-goldenrod/40 cursor-not-allowed' : 'text-goldenrod hover:bg-emergency/10 hover:text-emergency'}`}>
onClick={() => handleRemoveYouth(periodId, youth.id, youth.name)} <Trash2 size={18} />
className={`transition-colors ${isOffline ? 'text-goldenrod/40 cursor-not-allowed' : 'text-goldenrod hover:text-red-500'}`}
title={isOffline ? "Kräver internet" : "Ta bort ungdom"}
>
<Trash2 size={16} />
</button> </button>
</div> </div>
))} ))}
@@ -109,70 +78,64 @@ export const SetupTab: React.FC<Props> = ({ periods, createPeriod, deletePeriod,
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="space-y-6 animate-fade-in">
{/* Create New Period */} <div className="bg-eggshell border border-slate-teal/10 p-5 rounded-3xl shadow-sm">
<div className="bg-eggshell border-2 border-slate-teal/20 p-6 rounded-2xl shadow-sm"> <h2 className="text-sm font-black text-ebony uppercase tracking-widest flex items-center mb-4">
<h2 className="text-xl font-black text-ebony uppercase tracking-widest flex items-center mb-6"> <CalendarRange className="mr-2 text-slate-teal" size={20} /> Skapa Ny Period
<CalendarRange className="mr-3 text-slate-teal" size={24} />
Skapa Ny Period
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<input type="text" id="periodName" placeholder="T.ex. Period 1" className="w-full bg-white border border-slate-teal/20 p-3 rounded-lg font-bold" disabled={isOffline} /> <input type="text" id="periodName" placeholder="T.ex. Period 1" className="w-full bg-white border border-slate-teal/10 p-3 rounded-xl text-sm font-bold focus:outline-none focus:border-slate-teal shadow-sm" disabled={isOffline} />
<input type="date" id="periodStart" className="w-full bg-white border border-slate-teal/20 p-3 rounded-lg font-bold" disabled={isOffline} /> <input type="date" id="periodStart" className="w-full bg-white border border-slate-teal/10 p-3 rounded-xl text-sm font-bold focus:outline-none focus:border-slate-teal shadow-sm" disabled={isOffline} />
<button onClick={handleCreate} disabled={isOffline} className="bg-slate-teal text-eggshell font-black uppercase px-6 py-3 rounded-lg hover:bg-ebony transition-colors disabled:opacity-50 disabled:cursor-not-allowed"> <button onClick={handleCreate} disabled={isOffline} className="bg-slate-teal text-eggshell font-black uppercase tracking-widest text-xs px-4 py-3 rounded-xl hover:bg-ebony transition-colors disabled:opacity-50 shadow-sm">
Starta Starta
</button> </button>
</div> </div>
</div> </div>
{/* List Existing Periods */}
{periods.map(period => { {periods.map(period => {
// Sort by name alphabetically
const pfYouth = period.youthList.filter(y => y.team === 'PF').sort((a, b) => a.name.localeCompare(b.name)); const pfYouth = period.youthList.filter(y => y.team === 'PF').sort((a, b) => a.name.localeCompare(b.name));
const tuYouth = period.youthList.filter(y => y.team === 'TU').sort((a, b) => a.name.localeCompare(b.name)); const tuYouth = period.youthList.filter(y => y.team === 'TU').sort((a, b) => a.name.localeCompare(b.name));
return ( return (
<div key={period.id} className="bg-eggshell border-2 border-slate-teal/20 rounded-2xl shadow-sm overflow-hidden"> <div key={period.id} className="bg-eggshell border border-slate-teal/10 rounded-3xl shadow-sm overflow-hidden">
<div className="p-6 flex justify-between items-center cursor-pointer hover:bg-slate-teal/5" onClick={() => setExpandedPeriod(expandedPeriod === period.id ? null : period.id)}> <div className="p-5 flex justify-between items-center cursor-pointer hover:bg-slate-teal/5 transition-colors" onClick={() => setExpandedPeriod(expandedPeriod === period.id ? null : period.id)}>
<div> <div>
<h3 className="text-lg font-black text-ebony uppercase">{period.name}</h3> <h3 className="text-xl font-black text-ebony uppercase tracking-wide">{period.name}</h3>
<p className="text-sm font-bold text-moss">{period.startDate} till {period.endDate} {period.youthList.length} ungdomar</p> <p className="text-xs font-bold text-moss mt-1">{period.startDate} till {period.endDate} {period.youthList.length} ungdomar</p>
</div> </div>
<button <button onClick={(e) => { e.stopPropagation(); handleDeletePeriod(period.id); }} className={`p-2.5 rounded-xl transition-colors ${isOffline ? 'text-goldenrod/40 cursor-not-allowed' : 'text-goldenrod bg-white shadow-sm hover:bg-emergency hover:text-white'}`}>
onClick={(e) => { e.stopPropagation(); handleDeletePeriod(period.id); }}
className={`p-2 transition-colors ${isOffline ? 'text-goldenrod/40 cursor-not-allowed' : 'text-goldenrod/80 hover:text-goldenrod'}`}
title={isOffline ? "Kräver internet" : "Ta bort period"}
>
<Trash2 size={20} /> <Trash2 size={20} />
</button> </button>
</div> </div>
{/* Expandable Youth Management */}
{expandedPeriod === period.id && ( {expandedPeriod === period.id && (
<div className="p-6 border-t border-slate-teal/10 bg-slate-teal/5"> <div className="p-5 border-t border-slate-teal/10 bg-slate-teal/5">
<h4 className="font-black text-ebony mb-4 flex items-center"><UserPlus size={18} className="mr-2 text-slate-teal" /> Bulk-lägg till ungdomar</h4> <h4 className="font-black text-slate-teal mb-4 flex items-center uppercase tracking-widest text-xs">
<div className="flex flex-col md:flex-row gap-4 mb-6"> <UserPlus size={16} className="mr-2" /> Bulk-lägg till ungdomar
</h4>
<div className="flex flex-col md:flex-row gap-4 mb-8">
{/* FIX: Much larger textarea for easier importing */}
<textarea <textarea
value={bulkText} value={bulkText}
onChange={(e) => setBulkText(e.target.value)} onChange={(e) => setBulkText(e.target.value)}
placeholder="Klistra in namn, ett per rad.&#10;T.ex. 'Anna' eller 'Anna, P'" placeholder="Klistra in namn, ett per rad.&#10;T.ex. 'Anna' eller 'Anna, P'"
className="w-full h-24 bg-white border border-slate-teal/20 p-3 rounded-lg font-bold resize-none" className="w-full h-32 md:h-40 bg-white border border-slate-teal/10 p-4 rounded-2xl text-sm font-bold resize-none focus:outline-none focus:border-slate-teal shadow-sm"
disabled={isOffline} disabled={isOffline}
/> />
<div className="flex flex-col gap-2 shrink-0"> <div className="flex flex-col gap-3 shrink-0">
<select value={bulkTeam} onChange={(e) => setBulkTeam(e.target.value as 'PF' | 'TU')} className="bg-white border border-slate-teal/20 p-3 rounded-lg font-bold" disabled={isOffline}> <select value={bulkTeam} onChange={(e) => setBulkTeam(e.target.value as 'PF' | 'TU')} className="bg-white border border-slate-teal/10 p-3 rounded-xl text-sm font-bold focus:outline-none shadow-sm" disabled={isOffline}>
<option value="PF">Standard: Pilgrimsfalk (P)</option> <option value="PF">Standard: Pilgrimsfalk (P)</option>
<option value="TU">Standard: Tumlare (T)</option> <option value="TU">Standard: Tumlare (T)</option>
</select> </select>
<button onClick={handleBulkAdd} disabled={isOffline} className="bg-slate-teal text-eggshell font-black uppercase px-6 py-3 rounded-lg hover:bg-ebony transition-colors h-full disabled:opacity-50 disabled:cursor-not-allowed"> <button onClick={handleBulkAdd} disabled={isOffline} className="bg-seafoam text-eggshell font-black uppercase tracking-widest text-xs px-6 py-3 rounded-xl hover:bg-slate-teal transition-colors h-full disabled:opacity-50 shadow-sm">
Importera Importera
</button> </button>
</div> </div>
</div> </div>
{pfYouth.length > 0 && <h4 className="text-xs font-black text-moss uppercase tracking-widest mb-3">Pilgrimsfalkarna</h4>} {pfYouth.length > 0 && <h4 className="text-xs font-black text-moss uppercase tracking-widest mb-3 flex items-center"><span className="w-2.5 h-2.5 rounded-full bg-gold mr-2 shadow-sm"></span> Pilgrimsfalkarna</h4>}
{renderYouthGroup(pfYouth, period.id)} {renderYouthGroup(pfYouth, period.id)}
{tuYouth.length > 0 && <h4 className="text-xs font-black text-moss uppercase tracking-widest mb-3">Tumlarna</h4>} {tuYouth.length > 0 && <h4 className="text-xs font-black text-moss uppercase tracking-widest mb-3 flex items-center"><span className="w-2.5 h-2.5 rounded-full bg-seafoam mr-2 shadow-sm"></span> Tumlarna</h4>}
{renderYouthGroup(tuYouth, period.id)} {renderYouthGroup(tuYouth, period.id)}
</div> </div>
)} )}
+34 -67
View File
@@ -14,97 +14,66 @@ import { verifyLogin } from '../actions/admin';
export default function Admin() { export default function Admin() {
const [currentUser, setCurrentUser] = useState<AppUser | null>(null); const [currentUser, setCurrentUser] = useState<AppUser | null>(null);
const [isCheckingSession, setIsCheckingSession] = useState(true); // NEW: Prevents login screen flashing const [isCheckingSession, setIsCheckingSession] = useState(true);
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [pin, setPin] = useState(''); const [pin, setPin] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false); const [loginError, setLoginError] = useState(false);
const [activeTab, setActiveTab] = useState<'setup' | 'today' | 'report'>('report'); const [activeTab, setActiveTab] = useState<'setup' | 'today' | 'report'>('report');
const adminState = useAdminState(); const adminState = useAdminState();
// NEW: Check for a saved session when the app loads
useEffect(() => { useEffect(() => {
const savedSession = localStorage.getItem('kullaberg_admin_session'); const savedSession = localStorage.getItem('kullaberg_admin_session');
if (savedSession) { if (savedSession) {
const user = JSON.parse(savedSession) as AppUser; const user = JSON.parse(savedSession) as AppUser;
setCurrentUser(user); setCurrentUser(user);
// Set their correct tab based on role
if (user.role === 'Viewer') setActiveTab('report'); if (user.role === 'Viewer') setActiveTab('report');
else if (user.role === 'Staff') setActiveTab('today'); else if (user.role === 'Staff') setActiveTab('today');
else setActiveTab('today'); // Admins go to today by default if logged in via session else setActiveTab('today');
} }
setIsCheckingSession(false); setIsCheckingSession(false);
}, []); }, []);
const handleLogin = async (e: React.SyntheticEvent<HTMLFormElement>) => { const handleLogin = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true); setLoginError(false);
setLoginError(false);
const result = await verifyLogin(username, pin); const result = await verifyLogin(username, pin);
if (result.success && result.user) { if (result.success && result.user) {
const user = result.user as AppUser; const user = result.user as AppUser;
setCurrentUser(user); setCurrentUser(user);
// NEW: Save the session to the browser
localStorage.setItem('kullaberg_admin_session', JSON.stringify(user)); localStorage.setItem('kullaberg_admin_session', JSON.stringify(user));
if (user.role === 'Viewer') setActiveTab('report'); if (user.role === 'Viewer') setActiveTab('report');
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 {
setPin(''); setPin(''); setLoginError(true);
setLoginError(true);
} }
setIsLoading(false); setIsLoading(false);
}; };
const handleLogout = () => { const handleLogout = () => {
setCurrentUser(null); setCurrentUser(null); setUsername(''); setPin('');
setUsername('');
setPin('');
localStorage.removeItem('kullaberg_admin_session'); localStorage.removeItem('kullaberg_admin_session');
}; };
if (isCheckingSession) { if (isCheckingSession) return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;
return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;
}
if (!currentUser) { if (!currentUser) {
return ( return (
<div className="flex flex-col items-center justify-center py-20 animate-fade-in px-4"> <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-8 rounded-2xl shadow-lg w-full max-w-sm text-center"> <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"> <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" /> <Lock size={32} className="text-slate-teal" />
</div> </div>
<h2 className="text-2xl font-black text-ebony uppercase mb-6">Admin Login</h2> <h2 className="text-2xl font-black text-ebony uppercase mb-6 tracking-widest">Admin Login</h2>
<form onSubmit={handleLogin} className="space-y-4 text-left">
{/* New Text Input for Username */}
<input
type="text"
value={username}
onChange={(e) => { setUsername(e.target.value); setLoginError(false); }}
placeholder="Användarnamn"
className="w-full bg-eggshell/50 border-2 border-slate-teal/30 text-ebony font-bold p-3 rounded-xl focus:outline-none focus:border-slate-teal"
/>
<input
type="password"
value={pin}
onChange={(e) => { setPin(e.target.value); setLoginError(false); }}
placeholder="•••••"
className={`w-full bg-eggshell/50 border-2 text-center text-2xl tracking-[0.5em] text-ebony font-mono p-3 rounded-xl focus:outline-none ${loginError ? 'border-emergency/50 bg-emergency/10' : 'border-slate-teal/30 focus:border-slate-teal'}`}
/>
<form onSubmit={handleLogin} className="space-y-3 text-left">
<input type="text" value={username} onChange={(e) => { setUsername(e.target.value); setLoginError(false); }} placeholder="Användarnamn" className="w-full bg-white border border-slate-teal/20 text-ebony font-bold p-3 rounded-xl focus:outline-none focus:border-slate-teal" />
<input type="password" value={pin} onChange={(e) => { setPin(e.target.value); setLoginError(false); }} placeholder="•••••" className={`w-full bg-white border text-center text-2xl tracking-[0.5em] 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'}`} />
{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>}
<button type="submit" disabled={isLoading || !pin || !username} 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 || !pin || !username} className="w-full bg-slate-teal text-eggshell font-black uppercase py-3 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>
</form> </form>
@@ -113,47 +82,47 @@ export default function Admin() {
); );
} }
// Role-based tabs logic that was missing!
const availableTabs = []; const availableTabs = [];
if (currentUser.role === 'Admin') { if (currentUser.role === 'Admin') availableTabs.push({ id: 'setup', icon: CalendarRange, label: 'Perioder' });
availableTabs.push({ id: 'setup', icon: CalendarRange, label: 'Perioder' }); if (currentUser.role === 'Admin' || currentUser.role === 'Staff') availableTabs.push({ id: 'today', icon: ClipboardCheck, label: 'Närvaro' });
}
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') {
availableTabs.push({ id: 'today', icon: ClipboardCheck, label: 'Närvaro' });
}
availableTabs.push({ id: 'report', icon: UsersIcon, label: 'Rapport' }); availableTabs.push({ id: 'report', icon: UsersIcon, label: 'Rapport' });
return ( return (
<div className="space-y-8 animate-fade-in w-full"> <div className="space-y-6 animate-fade-in w-full">
<div className="flex justify-between items-end border-b-2 border-slate-teal/20 pb-4"> <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 flex-col">
<h1 className="text-3xl font-black text-slate-teal uppercase flex items-center"> <div className="flex items-center gap-3">
<Unlock className="mr-3 text-seafoam" size={28} /> Admin <Unlock className="text-seafoam" size={24} />
</h1> <h1 className="text-2xl font-black text-slate-teal uppercase tracking-widest">Admin</h1>
{/* Display Offline Badge if no internet! */} </div>
{adminState.isOffline && ( {adminState.isOffline && (
<div className="flex items-center mt-2 text-xs font-bold bg-goldenrod/20 text-goldenrod px-3 py-1 rounded-full w-fit"> <div className="flex items-center mt-2 text-[10px] font-bold bg-goldenrod/20 text-goldenrod px-2 py-1 rounded w-fit uppercase tracking-widest">
<WifiOff size={14} className="mr-2" /> Offline Läge (Synkar när nätverk finns) <WifiOff size={12} className="mr-1.5" /> Offline Läge
</div> </div>
)} )}
</div> </div>
<div className="text-right"> <div className="flex items-center gap-3 text-sm md:text-right bg-eggshell md:bg-transparent p-2 md:p-0 rounded-lg">
<p className="text-xs font-bold text-moss mb-1">Inloggad: {currentUser.name}</p> <span className="font-bold text-ebony">Inloggad: {currentUser.name}</span>
<button onClick={handleLogout} className="text-sm font-bold text-ebony/60 hover:text-goldenrod">Logga ut</button> <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>
</div> </div>
{adminState.isLoadingData && adminState.periods.length === 0 ? ( {adminState.isLoadingData && adminState.periods.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-slate-teal"> <div className="flex flex-col items-center justify-center py-20 text-slate-teal">
<Loader2 size={48} className="animate-spin mb-4" /> <Loader2 size={40} className="animate-spin mb-4" />
<p className="font-bold animate-pulse">Hämtar data...</p> <p className="font-bold text-sm animate-pulse">Hämtar data...</p>
</div> </div>
) : ( ) : (
<> <>
{availableTabs.length > 1 && ( {availableTabs.length > 1 && (
<div className="border-b border-slate-teal/20 flex gap-2 overflow-x-auto scrollbar-hide"> <div className="flex gap-2 overflow-x-auto scrollbar-hide">
{availableTabs.map(tab => ( {availableTabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id as any)} className={`flex items-center whitespace-nowrap px-4 py-2.5 rounded-t-lg font-bold text-sm transition-colors border-b-2 ${activeTab === tab.id ? 'bg-slate-teal text-eggshell border-slate-teal' : 'bg-eggshell text-slate-teal border-transparent hover:bg-slate-teal/5'}`}> <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} <tab.icon size={16} className="mr-2 hidden md:block shrink-0" /> {tab.label}
</button> </button>
))} ))}
@@ -161,7 +130,6 @@ export default function Admin() {
)} )}
{activeTab === 'setup' && currentUser.role === 'Admin' && <SetupTab {...adminState} />} {activeTab === 'setup' && currentUser.role === 'Admin' && <SetupTab {...adminState} />}
{activeTab === 'today' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && ( {activeTab === 'today' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && (
<AttendanceTab <AttendanceTab
periods={adminState.periods} periods={adminState.periods}
@@ -174,7 +142,6 @@ export default function Admin() {
setActivePeriodId={adminState.setActivePeriodId} setActivePeriodId={adminState.setActivePeriodId}
/> />
)} )}
{activeTab === 'report' && ( {activeTab === 'report' && (
<ReportTab <ReportTab
periods={adminState.periods} periods={adminState.periods}
+22 -37
View File
@@ -12,12 +12,12 @@ export default function Documents() {
const [cachedFiles, setCachedFiles] = useState<Set<string>>(new Set()); const [cachedFiles, setCachedFiles] = useState<Set<string>>(new Set());
const docs = [ const docs = [
{ title: 'Turistkarta', icon: <Map size={24} />, file: '/files/Turistkarta - Kullaberg.pdf', size: '2.51 MB' }, { title: 'Turistkarta', icon: <Map size={20} />, file: '/files/Turistkarta - Kullaberg.pdf', size: '2.51 MB' },
{ title: 'Orienteringskarta', icon: <MapPlus size={24} />, file: '/files/Orienteringskarta - Kullaberg.pdf', size: '3.74 MB' }, { title: 'Orienteringskarta', icon: <MapPlus size={20} />, file: '/files/Orienteringskarta - Kullaberg.pdf', size: '3.74 MB' },
{ title: 'Badplatser att besöka', icon: <WavesLadder size={24} />, file: '/files/Badplatser på Kullaberg.pdf', size: '1.88 MB' }, { title: 'Badplatser att besöka', icon: <WavesLadder size={20} />, file: '/files/Badplatser på Kullaberg.pdf', size: '1.88 MB' },
{ title: 'Vandringsrutter', icon: <MapPinned size={24} />, file: '/files/Vandringsrutter.pdf', size: '4.42 MB' }, { title: 'Vandringsrutter', icon: <MapPinned size={20} />, file: '/files/Vandringsrutter.pdf', size: '4.42 MB' },
{ title: 'Underlag för guidediplomering', icon: <BookCopy size={24} />, file: '/files/Underlag för guidediplomering.pdf', size: '3.18 MB' }, { title: 'Underlag för guidediplomering', icon: <BookCopy size={20} />, file: '/files/Underlag för guidediplomering.pdf', size: '3.18 MB' },
{ title: 'Destinationskunskap Kullahalvön', icon: <BookCopy size={24} />, file: '/files/Destinationskunskap.pdf', size: '1.97 MB' } { title: 'Destinationskunskap Kullahalvön', icon: <BookCopy size={20} />, file: '/files/Destinationskunskap.pdf', size: '1.97 MB' }
]; ];
const links = [ const links = [
@@ -67,23 +67,13 @@ export default function Documents() {
const handleSyncAll = async () => { const handleSyncAll = async () => {
setSyncStatus('syncing'); setSyncStatus('syncing');
for (const doc of docs) { for (const doc of docs) {
try { try { await fetch(doc.file); } catch (error) { console.error("Kunde inte ladda ner:", doc.file); }
await fetch(doc.file);
} catch (error) {
console.error("Kunde inte ladda ner:", doc.file);
}
} }
await checkCacheStatus(); await checkCacheStatus();
setSyncStatus('done'); setSyncStatus('done');
}; };
if (!isHydrated) { if (!isHydrated) return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;
return (
<div className="flex justify-center py-20">
<Loader2 className="animate-spin text-slate-teal" size={40} />
</div>
);
}
return ( return (
<div className="space-y-8 animate-fade-in w-full"> <div className="space-y-8 animate-fade-in w-full">
@@ -98,7 +88,7 @@ export default function Documents() {
<div className="flex flex-col gap-2 items-start md:items-end"> <div className="flex flex-col gap-2 items-start md:items-end">
{isOffline && ( {isOffline && (
<div className="bg-goldenrod/10 text-goldenrod border border-goldenrod/20 px-3 py-1.5 rounded-full flex items-center text-xs font-black uppercase tracking-tighter"> <div className="bg-goldenrod/10 text-goldenrod border border-goldenrod/20 px-3 py-1.5 rounded-full flex items-center text-xs font-black uppercase tracking-widest">
<WifiOff size={14} className="mr-2" /> Offline-läge <WifiOff size={14} className="mr-2" /> Offline-läge
</div> </div>
)} )}
@@ -124,40 +114,35 @@ export default function Documents() {
<section> <section>
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">Nedladdningar</h2> <h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">Nedladdningar</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{docs.map((doc, idx) => { {docs.map((doc, idx) => {
const isCached = cachedFiles.has(doc.file); const isCached = cachedFiles.has(doc.file);
return ( return (
<a key={idx} href={doc.file} target="_blank" rel="noopener noreferrer" <a key={idx} href={doc.file} target="_blank" rel="noopener noreferrer"
className={`group flex items-center justify-between p-5 rounded-2xl border transition-all duration-300 ${isOffline && !isCached ? 'opacity-50 grayscale cursor-not-allowed pointer-events-none bg-white/20 border-transparent' : 'bg-white/60 border-white hover:-translate-y-1 hover:shadow-xl hover:shadow-moss/10 backdrop-blur-sm'}`} className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline && !isCached ? 'opacity-50 grayscale cursor-not-allowed pointer-events-none border-transparent bg-eggshell/50' : 'bg-white/50 hover:bg-white border-slate-teal/10'}`}
> >
<div className="flex items-center text-slate-teal"> <div className="flex items-center text-slate-teal">
{/* FIX: Keep the original icon, but change the background to green when cached */} <div className={`p-2.5 rounded-xl mr-3 text-eggshell shrink-0 transition-colors ${isCached ? 'bg-moss' : 'bg-slate-teal'}`}>
<div className={`p-3 rounded-xl mr-4 text-eggshell shadow-inner shrink-0 transition-colors ${isCached ? 'bg-moss' : 'bg-linear-to-br from-seafoam to-slate-teal'}`}>
{doc.icon} {doc.icon}
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-bold text-sm leading-tight">{doc.title}</span> <span className="font-bold text-sm leading-tight text-ebony">{doc.title}</span>
<div className="flex flex-wrap items-center gap-2 mt-1.5"> <div className="flex flex-wrap items-center gap-2 mt-1">
<span className="text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded-md uppercase tracking-wider border border-moss/10"> <span className="text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded uppercase tracking-wider">
{doc.size} {doc.size}
</span> </span>
{isCached && ( {isCached && (
<span className="flex items-center gap-1 text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded-md uppercase tracking-wider"> <span className="flex items-center gap-1 text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded uppercase tracking-wider">
<Archive size={12} /> Nedladdad <Archive size={10} /> Nedladdad
</span> </span>
)} )}
</div> </div>
</div> </div>
</div> </div>
{/* FIX: Swap the Download icon for a Check icon when it is already saved on the phone */}
{isCached ? ( {isCached ? (
<CheckCircle size={20} className="text-moss shrink-0 ml-2" /> <CheckCircle size={18} className="text-moss shrink-0 ml-2" />
) : ( ) : (
<Download size={20} className="text-slate-teal/50 group-hover:text-slate-teal transition-colors shrink-0 ml-2" /> <Download size={18} className="text-slate-teal/50 group-hover:text-slate-teal transition-colors shrink-0 ml-2" />
)} )}
</a> </a>
); );
@@ -169,13 +154,13 @@ export default function Documents() {
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4"> <h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">
{isOffline ? "Användbara Länkar (Kräver anslutning)" : "Användbara Länkar"} {isOffline ? "Användbara Länkar (Kräver anslutning)" : "Användbara Länkar"}
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{links.map((link, idx) => ( {links.map((link, idx) => (
<a key={idx} href={isOffline ? '#' : link.url} target={isOffline ? '_self' : '_blank'} rel="noopener noreferrer" <a key={idx} href={isOffline ? '#' : link.url} target={isOffline ? '_self' : '_blank'} rel="noopener noreferrer"
className={`group flex items-center justify-between p-5 rounded-2xl border transition-all duration-300 ${isOffline ? 'opacity-40 grayscale cursor-not-allowed pointer-events-none bg-white/20 border-transparent' : 'bg-white/60 border-white hover:-translate-y-1 hover:shadow-xl hover:shadow-moss/10 backdrop-blur-sm'}`} className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline ? 'opacity-40 grayscale cursor-not-allowed pointer-events-none bg-eggshell/50 border-transparent' : 'bg-white/50 hover:bg-white border-slate-teal/10'}`}
> >
<span className="font-bold text-slate-teal text-sm leading-tight pr-4">{link.title}</span> <span className="font-bold text-slate-teal text-sm leading-tight pr-4">{link.title}</span>
<div className={`rounded-full p-2 transition-colors shrink-0 ${isOffline ? 'bg-ebony/5' : 'bg-eggshell group-hover:bg-seafoam'}`}> <div className={`rounded-full p-2 transition-colors shrink-0 ${isOffline ? 'bg-transparent' : 'bg-white group-hover:bg-seafoam'}`}>
<ExternalLink size={16} className={isOffline ? 'text-ebony/40' : 'text-slate-teal group-hover:text-eggshell'} /> <ExternalLink size={16} className={isOffline ? 'text-ebony/40' : 'text-slate-teal group-hover:text-eggshell'} />
</div> </div>
</a> </a>
+26 -34
View File
@@ -16,67 +16,59 @@ export default function Emergency() {
</div> </div>
{/* 112 Card */} {/* 112 Card */}
<div className="bg-emergency/20 border-l-4 border-emergency p-6 rounded-r-xl shadow-sm"> <div className="bg-emergency/10 border-l-4 border-emergency p-5 rounded-r-2xl shadow-sm">
<h2 className="text-2xl font-black text-ebony flex items-center mb-2"> <h2 className="text-xl font-black text-ebony flex items-center mb-2">
<Phone className="mr-3 text-emergency" size={24} /> <Phone className="mr-2 text-emergency" size={20} /> Ring 112
Ring 112
</h2> </h2>
<p className="text-ebony/80 font-medium mb-4"> <p className="text-ebony/80 font-medium mb-4 text-sm">
Vid olycka, brand eller livshotande tillstånd. Berätta vem du är och vad som har hänt. Vid olycka, brand eller livshotande tillstånd. Berätta vem du är och vad som har hänt.
</p> </p>
<div className="bg-eggshell/50 p-4 rounded-lg border border-emergency/30"> <div className="bg-white/60 p-4 rounded-xl border border-emergency/20">
<h3 className="font-bold text-ebony flex items-center mb-1 text-sm uppercase tracking-wider"> <h3 className="font-bold text-emergency flex items-center mb-1 text-xs uppercase tracking-wider">
<MapPin className="mr-2 text-emergency" size={16} /> <MapPin className="mr-2" size={14} /> Uppge din position
Uppge din position
</h3> </h3>
<p className="text-ebony font-medium text-sm"> <p className="text-ebony font-medium text-sm">
Använd appen <strong>112</strong> eller GPS i telefonen. Säg att du befinner dig i Kullabergs Naturreservat. Var specifik (t.ex. "Nära fyren" eller "Vid Josefinelust"). Använd appen <strong>112</strong> eller GPS. Säg att du befinner dig i Kullabergs Naturreservat. Var specifik (t.ex. "Nära fyren" eller "Vid Josefinelust").
</p> </p>
</div> </div>
</div> </div>
{/* First Aid / HLR Card */} {/* First Aid / HLR Card */}
<div className="bg-eggshell border-2 border-slate-teal/20 p-6 rounded-xl shadow-sm"> <div className="bg-eggshell border-2 border-slate-teal/10 p-5 rounded-2xl shadow-sm">
<h2 className="text-xl font-black text-slate-teal flex items-center mb-4 uppercase tracking-wide"> <h2 className="text-lg font-black text-slate-teal flex items-center mb-4 uppercase tracking-wide">
<HeartPulse className="mr-3 text-seafoam" size={24} /> <HeartPulse className="mr-2 text-seafoam" size={20} /> Första Hjälpen & Hjärtstartare
Första Hjälpen & Hjärtstartare
</h2> </h2>
<ul className="space-y-3 text-ebony font-medium"> <ul className="space-y-3 text-ebony font-medium text-sm">
<li className="flex items-start"> <li className="flex items-start">
<span className="bg-seafoam text-eggshell font-bold px-2 py-0.5 rounded mr-3 text-sm">1</span> <span className="bg-seafoam text-eggshell font-bold px-2 py-0.5 rounded mr-3 text-xs shrink-0">1</span>
Säkra platsen se till att varken du eller personen utsätts för mer fara. <span>Säkra platsen se till att varken du eller personen utsätts för mer fara.</span>
</li> </li>
<li className="flex items-start"> <li className="flex items-start">
<span className="bg-seafoam text-eggshell font-bold px-2 py-0.5 rounded mr-3 text-sm shrink-0">2</span> <span className="bg-seafoam text-eggshell font-bold px-2 py-0.5 rounded mr-3 text-xs shrink-0">2</span>
<span>Finns hjärtstartare? Ja, närmaste hjärtstartare finns inne <strong className="text-ebony font-black">Naturum Kullaberg</strong> (vid fyren) under deras öppettider.</span>
{/* Mobile View: Standard wrapping text (hides on medium screens and up) */}
<p className="text-ebony font-medium">
Finns hjärtstartare? Ja, närmaste hjärtstartare finns inne <strong className="text-ebony font-black">Naturum Kullaberg</strong> (vid fyren) under deras öppettider.
</p>
</li> </li>
</ul> </ul>
</div> </div>
{/* Internal Contact */} {/* Internal Contact */}
<div className="bg-eggshell border-2 border-slate-teal/20 p-6 rounded-xl shadow-sm"> <div className="bg-eggshell border-2 border-slate-teal/10 p-5 rounded-2xl shadow-sm">
<h2 className="text-xl font-black text-slate-teal mb-2 uppercase tracking-wide"> <h2 className="text-lg font-black text-slate-teal mb-2 uppercase tracking-wide">
Intern Rapportering Intern Rapportering
</h2> </h2>
<p className="text-ebony/80 font-medium mb-4 text-sm"> <p className="text-ebony/80 font-medium mb-4 text-sm">
När situationen är under kontroll, meddela alltid arbetsledaren om vad som inträffat. När situationen är under kontroll, meddela alltid arbetsledaren om vad som inträffat.
</p> </p>
<div className="space-y-4"> <div className="space-y-3">
<div className="flex flex-col md:flex-row md:justify-between md:items-center pb-4 border-b border-moss/20 gap-3"> <div className="flex flex-col md:flex-row md:justify-between md:items-center pb-3 border-b border-slate-teal/10 gap-2">
<span className="text-base font-bold text-ebony">William Söderberg</span> <span className="text-sm font-bold text-ebony">William Söderberg</span>
<div className='flex gap-2 flex-wrap md:justify-end'> <div className='flex gap-2 flex-wrap md:justify-end text-sm'>
<SafePhoneLink parts={['072', '247', '02', '91']} display="072-247 02 91" /> <SafePhoneLink parts={['072', '247', '02', '91']} display="072-247 02 91" />
<SafePhoneLink parts={['078', '389', '355', '81', '0604']} display="078-389 35 58 10 604" /> <SafePhoneLink parts={['078', '389', '355', '81', '0604']} display="078-389 35 58 10 604" />
</div> </div>
</div> </div>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-2">
<span className="text-base font-bold text-ebony">Oliver Nilsson</span> <span className="text-sm font-bold text-ebony">Oliver Nilsson</span>
<div className='flex gap-2 flex-wrap md:justify-end'> <div className='flex gap-2 flex-wrap md:justify-end text-sm'>
<SafePhoneLink parts={['072', '717', '74', '40']} display="072-717 74 40" /> <SafePhoneLink parts={['072', '717', '74', '40']} display="072-717 74 40" />
<SafePhoneLink parts={['078', '389', '355', '81', '0605']} display="078-389 35 58 10 605" /> <SafePhoneLink parts={['078', '389', '355', '81', '0605']} display="078-389 35 58 10 605" />
</div> </div>
@@ -85,4 +77,4 @@ export default function Emergency() {
</div> </div>
</div> </div>
); );
}; }
+10 -10
View File
@@ -14,8 +14,8 @@ export default function FAQ() {
}; };
return ( return (
<div className="w-full animate-fade-in"> <div className="w-full animate-fade-in space-y-6">
<div className="mb-8"> <div>
<h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center"> <h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center">
<MessageCircleQuestionMark className="mr-3 text-seafoam" size={32} /> <MessageCircleQuestionMark className="mr-3 text-seafoam" size={32} />
Vanliga Frågor (FAQ) Vanliga Frågor (FAQ)
@@ -23,29 +23,29 @@ export default function FAQ() {
<p className="text-moss font-medium mt-2 ml-11">Använd den här guiden för att snabbt svara turisternas vanligaste frågor.</p> <p className="text-moss font-medium mt-2 ml-11">Använd den här guiden för att snabbt svara turisternas vanligaste frågor.</p>
</div> </div>
<div className="columns-1 lg:columns-2 gap-6 space-y-6"> <div className="columns-1 lg:columns-2 gap-4 space-y-4">
{faqData.map((section, sIndex) => ( {faqData.map((section, sIndex) => (
<div key={sIndex} className="break-inside-avoid bg-white/40 backdrop-blur-md border border-white p-6 rounded-3xl shadow-lg shadow-ebony/5"> <div key={sIndex} className="break-inside-avoid bg-eggshell border-2 border-slate-teal/10 p-5 rounded-2xl shadow-sm">
<h2 className="text-lg font-black text-ebony/70 uppercase tracking-widest mb-4"> <h2 className="text-sm font-black text-ebony/60 uppercase tracking-widest mb-4">
{section.category} {section.category}
</h2> </h2>
<div className="space-y-3"> <div className="space-y-2">
{section.questions.map((item, qIndex) => { {section.questions.map((item, qIndex) => {
const id = `${sIndex}-${qIndex}`; const id = `${sIndex}-${qIndex}`;
const isOpen = openIndex === id; const isOpen = openIndex === id;
return ( return (
<div key={id} className={`rounded-2xl overflow-hidden transition-all duration-300 ${isOpen ? 'bg-white shadow-md shadow-seafoam/10' : 'bg-eggshell/50 hover:bg-white'}`}> <div key={id} className={`rounded-xl overflow-hidden transition-colors ${isOpen ? 'bg-white shadow-sm' : 'bg-transparent hover:bg-white/50'}`}>
<button <button
className="w-full text-left p-4 flex justify-between items-center focus:outline-none" className="w-full text-left p-3 flex justify-between items-center focus:outline-none"
onClick={() => toggleQuestion(id)} onClick={() => toggleQuestion(id)}
> >
<span className="font-bold text-slate-teal text-sm pr-4 leading-snug">{item.q}</span> <span className="font-bold text-slate-teal text-sm pr-4 leading-snug">{item.q}</span>
<div className={`p-1 rounded-full transition-colors ${isOpen ? 'bg-seafoam text-eggshell' : 'bg-transparent text-seafoam'}`}> <div className={`p-1 rounded-full transition-colors ${isOpen ? 'bg-seafoam text-eggshell' : 'text-seafoam'}`}>
{isOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />} {isOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div> </div>
</button> </button>
<div className={`transition-all duration-300 ease-in-out ${isOpen ? 'max-h-125 opacity-100' : 'max-h-0 opacity-0 overflow-hidden'}`}> <div className={`transition-all duration-300 ease-in-out ${isOpen ? 'max-h-125 opacity-100' : 'max-h-0 opacity-0 overflow-hidden'}`}>
<div className="p-2 pb-4 text-ebony font-medium text-sm leading-relaxed border-t border-ebony/20 mx-4"> <div className="p-2 pb-3 text-ebony font-medium text-sm leading-relaxed border-t border-ebony/20 mx-4">
{item.a} {item.a}
</div> </div>
</div> </div>
+1 -1
View File
@@ -57,7 +57,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</header> </header>
{/* Main Content Area */} {/* Main Content Area */}
<main className="max-w-5xl mx-auto p-4 py-6 md:py-8"> <main className="max-w-5xl mx-auto p-3 py-6 md:py-8">
{children} {children}
</main> </main>
</body> </body>
+26 -28
View File
@@ -6,21 +6,19 @@ import { SafePhoneLink } from './components/PhoneLinks';
export default function Home() { export default function Home() {
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="space-y-6 animate-fade-in w-full mx-auto">
{/* Welcome Banner */} {/* Welcome Banner */}
<div className="bg-slate-teal rounded-2xl p-6 md:p-8 shadow-lg shadow-slate-teal/10 text-eggshell"> <div className="bg-slate-teal rounded-2xl p-6 shadow-sm text-eggshell">
<h1 className="text-3xl md:text-4xl font-black tracking-tight mb-2 drop-shadow-sm">Välkommen!</h1> <h1 className="text-3xl font-black tracking-tight mb-1">Välkommen!</h1>
<p className="text-eggshell/90 font-bold text-lg">Naturvärdarna Kullaberg Sommar {new Date().getFullYear()}</p> <p className="text-eggshell/90 font-bold">Naturvärdarna Kullaberg Sommar {new Date().getFullYear()}</p>
</div> </div>
{/* Daily Notice Card */} {/* Daily Notice Card */}
<div className="bg-gold rounded-2xl p-5 flex items-start shadow-md shadow-goldenrod/20 text-ebony"> <div className="bg-goldenrod/30 border border-goldenrod/20 rounded-2xl p-5 flex items-start shadow-sm text-ebony">
<div className="bg-eggshell/40 p-2 rounded-xl mr-4 shrink-0"> <AlertTriangle className="text-goldenrod mr-3 shrink-0 mt-0.5" size={24} />
<AlertTriangle className="text-ebony" size={24} />
</div>
<div> <div>
<h3 className="font-black uppercase tracking-widest text-sm mb-1">Dagens Påminnelse</h3> <h3 className="font-black uppercase tracking-widest text-sm mb-1 text-goldenrod">Dagens Påminnelse</h3>
<p className="text-sm font-medium leading-relaxed"> <p className="text-sm font-medium leading-relaxed">
Glöm inte minst 1 liter vatten, solkräm och myggmedel. Det förväntas bli mycket varmt idag! Glöm inte minst 1 liter vatten, solkräm och myggmedel. Det förväntas bli mycket varmt idag!
</p> </p>
@@ -29,40 +27,40 @@ export default function Home() {
{/* Quick Action Cards */} {/* Quick Action Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link href="/schedule" className="group bg-eggshell/50 border-2 border-moss/30 rounded-2xl p-6 hover:-translate-y-1 hover:bg-moss/10 hover:border-moss/60 hover:shadow-lg transition-all duration-300 flex flex-col items-start"> <Link href="/schedule" className="group bg-eggshell border-2 border-slate-teal/10 rounded-2xl p-5 hover:bg-slate-teal/5 transition-colors flex flex-col items-start shadow-sm">
<div className="bg-seafoam/20 w-12 h-12 rounded-xl flex items-center justify-center mb-4 text-slate-teal group-hover:bg-seafoam group-hover:text-eggshell transition-colors"> <div className="bg-seafoam/20 w-10 h-10 rounded-xl flex items-center justify-center mb-3 text-slate-teal group-hover:bg-seafoam group-hover:text-eggshell transition-colors">
<ArrowRight size={24} className="group-hover:rotate-45 transition-transform" /> <ArrowRight size={20} className="group-hover:rotate-45 transition-transform" />
</div> </div>
<h3 className="font-black text-ebony text-xl mb-2 uppercase tracking-wide">Ditt Schema</h3> <h3 className="font-black text-ebony text-lg mb-1 uppercase tracking-wide">Ditt Schema</h3>
<p className="text-sm text-ebony/70 font-medium">Kolla dina arbetspass och dagliga rundor snabbt.</p> <p className="text-sm text-ebony/70 font-medium">Kolla dina arbetspass och dagliga rundor snabbt.</p>
</Link> </Link>
<Link href="/faq" className="group bg-eggshell/50 border-2 border-moss/30 rounded-2xl p-6 hover:-translate-y-1 hover:bg-moss/10 hover:border-moss/60 hover:shadow-lg transition-all duration-300 flex flex-col items-start"> <Link href="/faq" className="group bg-eggshell border-2 border-slate-teal/10 rounded-2xl p-5 hover:bg-slate-teal/5 transition-colors flex flex-col items-start shadow-sm">
<div className="bg-seafoam/20 w-12 h-12 rounded-xl flex items-center justify-center mb-4 text-slate-teal group-hover:bg-seafoam group-hover:text-eggshell transition-colors"> <div className="bg-seafoam/20 w-10 h-10 rounded-xl flex items-center justify-center mb-3 text-slate-teal group-hover:bg-seafoam group-hover:text-eggshell transition-colors">
<ArrowRight size={24} className="group-hover:rotate-45 transition-transform" /> <ArrowRight size={20} className="group-hover:rotate-45 transition-transform" />
</div> </div>
<h3 className="font-black text-ebony text-xl mb-2 uppercase tracking-wide">Vanliga Frågor</h3> <h3 className="font-black text-ebony text-lg mb-1 uppercase tracking-wide">Vanliga Frågor</h3>
<p className="text-sm text-ebony/70 font-medium">Snabba svar turisternas vanligaste funderingar.</p> <p className="text-sm text-ebony/70 font-medium">Snabba svar turisternas vanligaste funderingar.</p>
</Link> </Link>
</div> </div>
{/* Contact Card */} {/* Contact Card */}
<div className="bg-eggshell/50 border-2 border-moss/30 rounded-2xl overflow-hidden shadow-sm"> <div className="bg-eggshell border-2 border-slate-teal/10 rounded-2xl overflow-hidden shadow-sm">
<div className="bg-moss/20 px-6 py-4 flex items-center border-b-2 border-moss/30"> <div className="bg-slate-teal/5 px-5 py-4 flex items-center border-b border-slate-teal/10">
<PhoneCall size={20} className="text-ebony mr-3" /> <PhoneCall size={18} className="text-slate-teal mr-3" />
<h3 className="font-black text-ebony uppercase tracking-widest text-sm">Snabbkontakt</h3> <h3 className="font-black text-slate-teal uppercase tracking-widest text-sm">Snabbkontakt</h3>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-5 space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-center pb-4 border-b border-moss/20 gap-3"> <div className="flex flex-col md:flex-row md:justify-between md:items-center pb-4 border-b border-slate-teal/10 gap-2">
<span className="text-base font-bold text-ebony">William Söderberg</span> <span className="text-sm font-bold text-ebony">William Söderberg</span>
<div className='flex gap-2 flex-wrap md:justify-end'> <div className='flex gap-2 flex-wrap md:justify-end text-sm'>
<SafePhoneLink parts={['072', '247', '02', '91']} display="072-247 02 91" /> <SafePhoneLink parts={['072', '247', '02', '91']} display="072-247 02 91" />
<SafePhoneLink parts={['078', '389', '355', '81', '0604']} display="078-389 35 58 10 604" /> <SafePhoneLink parts={['078', '389', '355', '81', '0604']} display="078-389 35 58 10 604" />
</div> </div>
</div> </div>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-2">
<span className="text-base font-bold text-ebony">Oliver Nilsson</span> <span className="text-sm font-bold text-ebony">Oliver Nilsson</span>
<div className='flex gap-2 flex-wrap md:justify-end'> <div className='flex gap-2 flex-wrap md:justify-end text-sm'>
<SafePhoneLink parts={['072', '717', '74', '40']} display="072-717 74 40" /> <SafePhoneLink parts={['072', '717', '74', '40']} display="072-717 74 40" />
<SafePhoneLink parts={['078', '389', '355', '81', '0605']} display="078-389 35 58 10 605" /> <SafePhoneLink parts={['078', '389', '355', '81', '0605']} display="078-389 35 58 10 605" />
</div> </div>
+19 -74
View File
@@ -7,62 +7,29 @@ import scheduleData from '../data/schedule.json';
import { Clock, CalendarRange } from 'lucide-react'; import { Clock, CalendarRange } from 'lucide-react';
// --- Helper Functions --- // --- Helper Functions ---
// Bulletproof parser: Finds the times regardless of spaces or dashes!
const parseTimeBlock = (timeStr: string) => { const parseTimeBlock = (timeStr: string) => {
if (!timeStr || timeStr === 'Ledig') return null; if (!timeStr || timeStr === 'Ledig') return null;
// Scans the string and extracts any "HH:MM" patterns
const matches = timeStr.match(/(\d{1,2}):(\d{2})/g); const matches = timeStr.match(/(\d{1,2}):(\d{2})/g);
if (!matches || matches.length < 2) return null; if (!matches || matches.length < 2) return null;
const parse = (t: string) => { const [h, m] = t.split(':').map(Number); return h + (m / 60); };
const parse = (t: string) => {
const [h, m] = t.split(':').map(Number);
return h + (m / 60);
};
return { start: parse(matches[0]), end: parse(matches[1]) }; return { start: parse(matches[0]), end: parse(matches[1]) };
}; };
// Checks if the two time blocks actually collide
const checkOverlap = (time1?: string, time2?: string) => { const checkOverlap = (time1?: string, time2?: string) => {
const t1 = time1 ? parseTimeBlock(time1) : null; const t1 = time1 ? parseTimeBlock(time1) : null;
const t2 = time2 ? parseTimeBlock(time2) : null; const t2 = time2 ? parseTimeBlock(time2) : null;
if (!t1 || !t2) return false; if (!t1 || !t2) return false;
return t1.start < t2.end && t1.end > t2.start; return t1.start < t2.end && t1.end > t2.start;
}; };
// --- Types & Components --- // --- Types & Components ---
interface ShiftData { time: string; title?: string; notes?: string; }
interface ShiftBlockProps { team: 'PF' | 'TU'; data: ShiftData; pos: { top: number; height: number }; isOverlapping: boolean; }
interface ShiftData {
time: string;
title?: string;
notes?: string;
}
interface ShiftBlockProps {
team: 'PF' | 'TU';
data: ShiftData;
pos: { top: number; height: number };
isOverlapping: boolean;
}
// Reusable Component for the Event Cards
const ShiftBlock: React.FC<ShiftBlockProps> = ({ team, data, pos, isOverlapping }) => { const ShiftBlock: React.FC<ShiftBlockProps> = ({ team, data, pos, isOverlapping }) => {
const isPF = team === 'PF'; const isPF = team === 'PF';
const widthClasses = isOverlapping ? isPF ? "left-1 right-1/2 mr-0.5" : "left-1/2 right-1 ml-0.5" : "left-1 right-1";
// Calculate layout based on overlap const bgClasses = isPF ? "bg-gradient-to-br from-gold to-goldenrod border-goldenrod" : "bg-gradient-to-br from-seafoam to-slate-teal border-slate-teal";
const widthClasses = isOverlapping
? isPF ? "left-1 right-1/2 mr-0.5" : "left-1/2 right-1 ml-0.5"
: "left-1 right-1";
// Team-specific styling
const bgClasses = isPF
? "bg-gradient-to-br from-gold to-goldenrod border-goldenrod"
: "bg-gradient-to-br from-seafoam to-slate-teal border-slate-teal";
const textMain = isPF ? "text-ebony" : "text-eggshell"; const textMain = isPF ? "text-ebony" : "text-eggshell";
const textMuted = isPF ? "text-ebony/90" : "text-eggshell/90"; const textMuted = isPF ? "text-ebony/90" : "text-eggshell/90";
const notesBg = isPF ? "bg-eggshell/30 border-seafoam/5" : "bg-gold/20 border-eggshell/10"; const notesBg = isPF ? "bg-eggshell/30 border-seafoam/5" : "bg-gold/20 border-eggshell/10";
@@ -70,54 +37,40 @@ const ShiftBlock: React.FC<ShiftBlockProps> = ({ team, data, pos, isOverlapping
return ( return (
<div <div
// Removed scale on hover! Added brightness-105 for a safe, non-clipping hover effect. className={`absolute ${widthClasses} ${bgClasses} rounded-lg p-1.5 flex flex-col overflow-hidden shadow-sm border transition-colors hover:brightness-105`}
className={`absolute ${widthClasses} ${bgClasses} rounded-lg p-1.5 flex flex-col overflow-hidden shadow-sm border cursor-pointer transition-all hover:brightness-105 hover:shadow-md`}
style={{ top: `${pos.top}px`, height: `${pos.height}px` }} style={{ top: `${pos.top}px`, height: `${pos.height}px` }}
> >
<p className={`text-[10px] font-black ${textMain} uppercase tracking-wide leading-tight truncate`}> <p className={`text-[10px] font-black ${textMain} uppercase tracking-wide leading-tight truncate`}>{data.title || fallbackTitle}</p>
{data.title || fallbackTitle} <p className={`text-[9px] font-bold ${textMuted} flex items-center mt-0.5 whitespace-nowrap`}><Clock size={8} className="mr-0.5 shrink-0" /><span className="truncate">{data.time}</span></p>
</p>
<p className={`text-[9px] font-bold ${textMuted} flex items-center mt-0.5 whitespace-nowrap`}>
<Clock size={8} className="mr-0.5 shrink-0" />
<span className="truncate">{data.time}</span>
</p>
{data.notes && pos.height > 50 && ( {data.notes && pos.height > 50 && (
<div className={`mt-1 ${notesBg} p-1 rounded-md border`}> <div className={`mt-1 ${notesBg} p-1 rounded-md border`}>
<p className={`text-[9px] font-medium ${textMain} leading-tight truncate pb-1`}> <p className={`text-[9px] font-medium ${textMain} leading-tight truncate pb-1`}>{data.notes}</p>
{data.notes}
</p>
</div> </div>
)} )}
</div> </div>
); );
}; };
// --- Main Schedule View --- // --- Main Schedule View ---
export default function Schedule() { export default function Schedule() {
const startHour = 7; const startHour = 7;
const endHour = 17; const endHour = 17;
const hours = Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i); const hours = Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i);
const PIXELS_PER_HOUR = 60; const PIXELS_PER_HOUR = 60;
const GRID_PADDING_TOP = 24; const GRID_PADDING_TOP = 24;
const TOTAL_GRID_HEIGHT = (hours.length * PIXELS_PER_HOUR) + GRID_PADDING_TOP; const TOTAL_GRID_HEIGHT = (hours.length * PIXELS_PER_HOUR) + GRID_PADDING_TOP;
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const calculatePosition = (timeStr: string) => { const calculatePosition = (timeStr: string) => {
const t = parseTimeBlock(timeStr); const t = parseTimeBlock(timeStr);
if (!t) return null; if (!t) return null;
const topOffset = (t.start - startHour) * PIXELS_PER_HOUR; const topOffset = (t.start - startHour) * PIXELS_PER_HOUR;
const duration = (t.end - t.start) * PIXELS_PER_HOUR; const duration = (t.end - t.start) * PIXELS_PER_HOUR;
return { top: topOffset + GRID_PADDING_TOP + 1, height: duration - 2 }; return { top: topOffset + GRID_PADDING_TOP + 1, height: duration - 2 };
}; };
return ( return (
<div className="space-y-4 animate-fade-in w-full"> <div className="space-y-6 animate-fade-in w-full">
<div> <div>
<h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center"> <h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center">
<CalendarRange className="mr-3 text-seafoam" size={32} /> <CalendarRange className="mr-3 text-seafoam" size={32} />
@@ -126,17 +79,16 @@ export default function Schedule() {
<p className="text-moss font-medium mt-2 ml-11">Här hittar du när vi bemannar Kullaberg.</p> <p className="text-moss font-medium mt-2 ml-11">Här hittar du när vi bemannar Kullaberg.</p>
</div> </div>
<div className="bg-eggshell border-2 border-slate-teal/20 rounded-2xl shadow-sm overflow-hidden flex flex-col max-h-[65vh] h-max min-h-125"> <div className="bg-eggshell border-2 border-slate-teal/10 rounded-2xl shadow-sm overflow-hidden flex flex-col max-h-[60vh] h-max min-h-125">
<div className="flex-1 overflow-auto scrollbar-hide" ref={containerRef}> <div className="flex-1 overflow-auto scrollbar-hide" ref={containerRef}>
<div className="flex min-w-200 w-full"> <div className="flex min-w-200 w-full">
{/* Sticky Time Column */} {/* Sticky Time Column */}
<div className="sticky left-0 z-20 w-18 flex-none bg-eggshell border-r-2 border-slate-teal/10 shadow-[2px_0_5px_rgba(0,0,0,0.02)]"> <div className="sticky left-0 z-20 w-18 flex-none bg-eggshell border-r border-slate-teal/10 shadow-[2px_0_5px_rgba(0,0,0,0.02)]">
<div className="sticky top-0 z-30 h-12 border-b-2 border-slate-teal/10 bg-eggshell"></div> <div className="sticky top-0 z-30 h-10 border-b border-slate-teal/10 bg-eggshell"></div>
<div className="relative w-full" style={{ height: `${TOTAL_GRID_HEIGHT}px` }}> <div className="relative w-full" style={{ height: `${TOTAL_GRID_HEIGHT}px` }}>
{hours.map((hour, i) => ( {hours.map((hour, i) => (
<div key={hour} className="absolute w-full flex justify-end pr-3" style={{ top: `${i * PIXELS_PER_HOUR + GRID_PADDING_TOP}px` }}> <div key={hour} className="absolute w-full flex justify-end pr-2" style={{ top: `${i * PIXELS_PER_HOUR + GRID_PADDING_TOP}px` }}>
<span className="text-[11px] font-bold text-slate-teal -mt-2 bg-eggshell px-1"> <span className="text-[10px] font-bold text-slate-teal -mt-2 bg-eggshell px-1">
{hour.toString().padStart(2, '0')}:00 {hour.toString().padStart(2, '0')}:00
</span> </span>
</div> </div>
@@ -147,9 +99,9 @@ export default function Schedule() {
{/* Grid Area */} {/* Grid Area */}
<div className="flex-auto relative"> <div className="flex-auto relative">
{/* Sticky Day Headers */} {/* Sticky Day Headers */}
<div className="sticky top-0 z-10 flex h-12 border-b-2 border-slate-teal/10 bg-eggshell"> <div className="sticky top-0 z-10 flex h-10 border-b border-slate-teal/10 bg-eggshell">
{scheduleData.map((d) => ( {scheduleData.map((d) => (
<div key={d.day} className="flex-1 flex items-center justify-center border-l-2 border-slate-teal/5 first:border-l-0 min-w-25"> <div key={d.day} className="flex-1 flex items-center justify-center border-l border-slate-teal/5 first:border-l-0 min-w-25">
<span className="text-xs font-black text-ebony uppercase tracking-widest">{d.day.substring(0, 3)}</span> <span className="text-xs font-black text-ebony uppercase tracking-widest">{d.day.substring(0, 3)}</span>
</div> </div>
))} ))}
@@ -159,30 +111,23 @@ export default function Schedule() {
<div className="relative w-full" style={{ height: `${TOTAL_GRID_HEIGHT}px` }}> <div className="relative w-full" style={{ height: `${TOTAL_GRID_HEIGHT}px` }}>
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
{hours.map((hour, i) => ( {hours.map((hour, i) => (
<div key={hour} className="absolute w-full border-t border-slate-teal/10" style={{ top: `${i * PIXELS_PER_HOUR + GRID_PADDING_TOP}px`, height: `${PIXELS_PER_HOUR}px` }} /> <div key={hour} className="absolute w-full border-t border-slate-teal/5" style={{ top: `${i * PIXELS_PER_HOUR + GRID_PADDING_TOP}px`, height: `${PIXELS_PER_HOUR}px` }} />
))} ))}
</div> </div>
{/* Vertical Columns & Blocks */}
<div className="absolute inset-0 flex z-0"> <div className="absolute inset-0 flex z-0">
{scheduleData.map((d, colIndex) => { {scheduleData.map((d, colIndex) => {
// Type casting here assumes scheduleData is typed loosely.
const pfData = d.pilgrimsfalkarna as ShiftData | undefined; const pfData = d.pilgrimsfalkarna as ShiftData | undefined;
const tuData = d.tumlarna as ShiftData | undefined; const tuData = d.tumlarna as ShiftData | undefined;
const hasPF = !!(pfData && pfData.time && pfData.time !== 'Ledig'); const hasPF = !!(pfData && pfData.time && pfData.time !== 'Ledig');
const hasTU = !!(tuData && tuData.time && tuData.time !== 'Ledig'); const hasTU = !!(tuData && tuData.time && tuData.time !== 'Ledig');
const isOverlapping = hasPF && hasTU && checkOverlap(pfData.time, tuData.time); const isOverlapping = hasPF && hasTU && checkOverlap(pfData.time, tuData.time);
return ( return (
<div key={colIndex} className="flex-1 border-l border-slate-teal/10 relative min-w-25"> <div key={colIndex} className="flex-1 border-l border-slate-teal/5 relative min-w-25">
{/* Pilgrimsfalkarna Component */}
{hasPF && (() => { {hasPF && (() => {
const pos = calculatePosition(pfData.time); const pos = calculatePosition(pfData.time);
return pos ? <ShiftBlock team="PF" data={pfData} pos={pos} isOverlapping={isOverlapping} /> : null; return pos ? <ShiftBlock team="PF" data={pfData} pos={pos} isOverlapping={isOverlapping} /> : null;
})()} })()}
{/* Tumlarna Component */}
{hasTU && (() => { {hasTU && (() => {
const pos = calculatePosition(tuData.time); const pos = calculatePosition(tuData.time);
return pos ? <ShiftBlock team="TU" data={tuData} pos={pos} isOverlapping={isOverlapping} /> : null; return pos ? <ShiftBlock team="TU" data={tuData} pos={pos} isOverlapping={isOverlapping} /> : null;