diff --git a/app/admin/AttendanceTab.tsx b/app/admin/AttendanceTab.tsx index afc859a..05ec251 100644 --- a/app/admin/AttendanceTab.tsx +++ b/app/admin/AttendanceTab.tsx @@ -2,13 +2,11 @@ "use client"; -import { CheckCircle, ChevronLeft, ChevronRight, ClipboardCheck, Edit3, FileText, Undo, Zap } from 'lucide-react'; -import Image from 'next/image'; +import { CalendarRange, CheckCircle, ChevronLeft, ChevronRight } from 'lucide-react'; import React, { useEffect, useState } from 'react'; -import falconIcon from '../assets/falcon.svg'; -import porpoiseIcon from '../assets/porpoise.svg'; import scheduleData from '../data/schedule.json'; -import { type AttendanceDataMap, calculateShiftDuration, formatTimeHHMM, getAttendanceKey, isWeekend, parseTimeInput, type Period, toIsoDate } from './adminTypes'; +import { type AttendanceDataMap, getAttendanceKey, type Period, toIsoDate } from './adminTypes'; +import { TeamAttendanceCard } from './TeamAttendanceCard'; interface Props { periods: Period[]; attendance: AttendanceDataMap; @@ -80,13 +78,6 @@ export const AttendanceTab: React.FC = ({ periods, attendance, setManualA const todayIso = toIsoDate(new Date()); const currentDayStats = getDailyCompletionStats(currentDate, activePeriod, attendance); - const markStandardAttendance = (youthId: string, team: 'PF' | 'TU', shiftId: 'MORNING' | 'AFTERNOON') => { - const { time: shiftTime } = getTeamShiftInfo(daySchedule, team); - const rawHours = calculateShiftDuration(shiftTime); - const actualHours = isWeekend(currentDate) ? Math.max(0, rawHours - 0.5) : rawHours; - if (actualHours > 0) setManualAttendance(currentDate, youthId, shiftId, 'Present', actualHours); - }; - const teamsToRender = ['PF', 'TU'].sort((a, b) => { const timeA = getTeamShiftInfo(daySchedule, a as 'PF' | 'TU').time; const timeB = getTeamShiftInfo(daySchedule, b as 'PF' | 'TU').time; @@ -97,9 +88,11 @@ export const AttendanceTab: React.FC = ({ periods, attendance, setManualA return (
-
+
-

Periodöversikt

+

+ Periodöversikt +

@@ -123,16 +116,15 @@ export const AttendanceTab: React.FC = ({ periods, attendance, setManualA
-
- +
+

{dayNameStr}

{currentDate}

- +
- {/* FIX: Status banner is fully restored and green when done! */} {currentDayStats.hasWork && (
{currentDayStats.isComplete ? <> All närvaro rapporterad : `Ifyllt: ${currentDayStats.completed} av ${currentDayStats.expected} pers`} @@ -149,122 +141,20 @@ export const AttendanceTab: React.FC = ({ periods, attendance, setManualA const { time: standardTime, shiftId } = getTeamShiftInfo(daySchedule, team); if (standardTime === 'Ledig') return null; - const rawDuration = calculateShiftDuration(standardTime); - const isWknd = isWeekend(currentDate); - const actualDuration = isWknd ? Math.max(0, rawDuration - 0.5) : rawDuration; - const weight = isWknd ? 1.5 : 1.0; - const weightedDuration = actualDuration * weight; - - 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 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 handleQuickLogAll = () => { - const recordsToUpdate: any[] = []; - displayYouth.forEach(y => { - const entry = attendance[getAttendanceKey(currentDate, y.id, shiftId)]; - if (!entry || entry.status === 'Pending') { - recordsToUpdate.push({ date: currentDate, youthId: y.id, shiftId, status: 'Present', hoursWorked: actualDuration, weightedHours: weightedDuration, note: '' }); - } - }); - if (recordsToUpdate.length > 0) bulkSetManualAttendance(recordsToUpdate); - }; - return ( -
-
-
-
- {team -
-

{team === 'PF' ? 'Pilgrimsfalkarna' : 'Tumlarna'}

-
-
-
- {standardTime} ({formatTimeHHMM(rawDuration)}) -
- -
-
- -
- {displayYouth.map(youth => { - const entry = attendance[getAttendanceKey(currentDate, youth.id, shiftId)]; - const isExtra = youth.team !== team; - const isPending = entry?.status === 'Pending'; - const isCompleted = entry && !isPending; - - return ( -
-
- {isCompleted ? :
} -
- {youth.team -
- - {youth.name} {isExtra && Extra pass} - -
- -
- {isPending && Väntar...} - {isCompleted && ( - - {entry.status === 'Absent' ? entry.note : `${formatTimeHHMM(entry.hoursWorked)} (+${entry.weightedHours.toFixed(1)}h)`} - - )} - - {(!entry || isPending) && ( - <> - - - - - )} - - {entry && ( - - )} -
-
- ); - })} - - {availableExtras.length > 0 && ( -
- -
- )} -
-
+ ); }) )} diff --git a/app/admin/ReportTab.tsx b/app/admin/ReportTab.tsx index 45a1f97..c446c6f 100644 --- a/app/admin/ReportTab.tsx +++ b/app/admin/ReportTab.tsx @@ -2,21 +2,16 @@ "use client"; -import { AlertTriangle, ChevronDown, ChevronUp, Clock, Download, Users } from 'lucide-react'; -import Image from 'next/image'; +import { Download, Users } from 'lucide-react'; import React, { useState } from 'react'; -import scheduleData from '../data/schedule.json'; -import falconIcon from '../assets/falcon.svg'; -import porpoiseIcon from '../assets/porpoise.svg'; import { type AttendanceDataMap, formatTimeHHMM, isWeekend, type Period, type Role } from './adminTypes'; -import { getTeamShiftInfo } from './AttendanceTab'; +import { YouthReportCard } from './YouthReportCard'; interface Props { periods: Period[]; attendance: AttendanceDataMap; activePeriodId: string; setActivePeriodId: (id: string) => void; currentUserRole: Role; } const POT_HOUR_LIMIT = 90; -const formatHours = (h: number) => Number(h.toFixed(1)).toString(); export const ReportTab: React.FC = ({ periods, attendance, activePeriodId, setActivePeriodId, currentUserRole }) => { const activePeriod = periods.find(p => p.id === activePeriodId); @@ -81,7 +76,7 @@ export const ReportTab: React.FC = ({ periods, attendance, activePeriodId return (
-
+
@@ -91,10 +86,10 @@ export const ReportTab: React.FC = ({ periods, attendance, activePeriodId
-
+
-

- Timrapport +

+ Timrapport

{currentUserRole !== 'Viewer' && ( @@ -105,100 +100,17 @@ export const ReportTab: React.FC = ({ periods, attendance, activePeriodId

- {sortedActiveYouth.map(youth => { - const total = getPeriodHoursTotal(youth.id); - const warningStatus = total > POT_HOUR_LIMIT ? 'red' : (total >= POT_HOUR_LIMIT - 10 ? 'yellow' : 'none'); - const isExpanded = expandedYouthId === youth.id; - const timeline = getTimelineForYouth(youth.id); - - return ( -
- -
setExpandedYouthId(isExpanded ? null : youth.id)} - 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" - > -
-
- {youth.team -
-
- {youth.name} -
- {isExpanded ? : } - {isExpanded ? 'Dölj detaljer' : 'Klicka för detaljer'} -
-
-
- -
- {warningStatus === 'red' && } -
-
- {formatHours(total)} / {POT_HOUR_LIMIT}h -
-

Viktade timmar

-
-
-
- - {isExpanded && ( -
-

- Arbetspass -

- {timeline.length === 0 ? ( -

Ingen närvaro loggad.

- ) : ( -
- {timeline.map((entry, idx) => { - const isWknd = isWeekend(entry.date); - const dayNameStr = new Date(entry.date + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase(); - const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStr); - - // FIX: Bulletproof isExtra logic that catches weekend-workers too! - let isExtra = false; - if (daySchedule) { - const expectedShift = getTeamShiftInfo(daySchedule, youth.team); - if (expectedShift.time.toLowerCase() === 'ledig') { - isExtra = true; - } else if (!isWknd) { - isExtra = entry.shiftId !== expectedShift.shiftId; - } - } else { - isExtra = true; - } - - const shiftLabel = isWknd ? 'Heldag' : (entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag'); - - return ( -
-
- {entry.date} - {shiftLabel} - - {isExtra && Extra} - {isWknd && Helg} -
-
- {entry.status === 'Pending' ? Väntar... : - entry.status === 'Absent' ? {entry.note || 'Frånvarande'} : - {entry.status === 'Late' ? 'Manuell' : 'Närvarande'}} - - - {formatTimeHHMM(entry.hoursWorked)} (+{entry.weightedHours.toFixed(1)}h) - -
-
- ); - })} -
- )} -
- )} -
- ); - })} + {sortedActiveYouth.map(youth => ( + setExpandedYouthId(expandedYouthId === youth.id ? null : youth.id)} + timeline={getTimelineForYouth(youth.id)} + /> + ))}
diff --git a/app/admin/SetupTab.tsx b/app/admin/SetupTab.tsx index 2df283e..2096b75 100644 --- a/app/admin/SetupTab.tsx +++ b/app/admin/SetupTab.tsx @@ -2,7 +2,7 @@ "use client"; -import { CalendarRange, Trash2, UserPlus } from 'lucide-react'; +import { CalendarPlus, Trash2, UserPlus } from 'lucide-react'; import Image from 'next/image'; import React, { useState } from 'react'; import falconIcon from '../assets/falcon.svg'; @@ -78,14 +78,39 @@ export const SetupTab: React.FC = ({ periods, createPeriod, deletePeriod, return (
-
+

- Skapa Ny Period + Skapa Ny Period

-
- - -
@@ -96,8 +121,8 @@ export const SetupTab: React.FC = ({ periods, createPeriod, deletePeriod, const tuYouth = period.youthList.filter(y => y.team === 'TU').sort((a, b) => a.name.localeCompare(b.name)); return ( -
-
setExpandedPeriod(expandedPeriod === period.id ? null : period.id)}> +
+
setExpandedPeriod(expandedPeriod === period.id ? null : period.id)}>

{period.name}

{period.startDate} till {period.endDate} • {period.youthList.length} ungdomar

@@ -108,12 +133,11 @@ export const SetupTab: React.FC = ({ periods, createPeriod, deletePeriod,
{expandedPeriod === period.id && ( -
+

Bulk-lägg till ungdomar

- {/* FIX: Much larger textarea for easier importing */}