135 lines
8.1 KiB
TypeScript
135 lines
8.1 KiB
TypeScript
// app/admin/YouthReportCard.tsx
|
|
|
|
"use client";
|
|
|
|
import { AlertTriangle, ChevronDown, ChevronUp, Clock } from 'lucide-react';
|
|
import Image from 'next/image';
|
|
import React from 'react';
|
|
import falconIcon from '../assets/falcon.svg';
|
|
import porpoiseIcon from '../assets/porpoise.svg';
|
|
import { formatTimeHHMM, isWeekend, type Youth } from './adminTypes';
|
|
import { getTeamShiftInfo } from './AttendanceTab';
|
|
|
|
interface YouthReportCardProps {
|
|
youth: Youth;
|
|
totalHours: number;
|
|
potHourLimit: number;
|
|
isExpanded: boolean;
|
|
onToggle: () => void;
|
|
timeline: any[];
|
|
scheduleData: any[];
|
|
}
|
|
|
|
const formatHours = (h: number) => Number(h.toFixed(1)).toString();
|
|
|
|
export const YouthReportCard: React.FC<YouthReportCardProps> = ({
|
|
youth,
|
|
totalHours,
|
|
potHourLimit,
|
|
isExpanded,
|
|
onToggle,
|
|
timeline,
|
|
scheduleData
|
|
}) => {
|
|
const warningStatus = totalHours > potHourLimit ? 'red' : (totalHours >= potHourLimit - 10 ? 'yellow' : 'none');
|
|
|
|
return (
|
|
<div 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
|
|
onClick={onToggle}
|
|
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"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className={`flex items-center justify-center w-12 h-12 rounded-2xl ${youth.team === 'PF' ? 'bg-gold' : 'bg-seafoam'} shrink-0 shadow-inner`}>
|
|
<Image src={youth.team === 'PF' ? falconIcon : porpoiseIcon} alt={youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare'} width={24} height={24} />
|
|
</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>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-4">
|
|
{warningStatus === 'red' && <AlertTriangle className="text-goldenrod shrink-0" size={28} />}
|
|
<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')}`}>
|
|
{formatHours(totalHours)}<span className="text-sm font-bold text-ebony/40"> / {potHourLimit}h</span>
|
|
</div>
|
|
<p className="text-[10px] font-black uppercase tracking-widest mt-0.5 text-ebony/50">Viktade timmar</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="bg-slate-teal/5 border-t border-slate-teal/10 p-4 md:p-6">
|
|
<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
|
|
</h4>
|
|
{timeline.length === 0 ? (
|
|
<p className="text-sm font-bold text-slate-teal/60 italic">Ingen närvaro loggad.</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{timeline.map((entry, idx) => {
|
|
const isWknd = isWeekend(entry.date);
|
|
const dateObj = new Date(entry.date + 'T12:00:00');
|
|
const dayNameStrFull = dateObj.toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase();
|
|
|
|
const dayNameShort = dateObj.toLocaleDateString('sv-SE', { weekday: 'short' });
|
|
const capitalizedShortDay = dayNameShort.charAt(0).toUpperCase() + dayNameShort.slice(1);
|
|
|
|
const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayNameStrFull);
|
|
|
|
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 = entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag';
|
|
|
|
return (
|
|
<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 hover:shadow-sm transition-shadow">
|
|
<div className="flex items-center flex-wrap gap-2">
|
|
<span className="font-black text-ebony min-w-24">
|
|
{entry.date} | {capitalizedShortDay}
|
|
</span>
|
|
{!isWknd && (
|
|
<span className="text-[10px] font-black uppercase tracking-widest text-slate-teal bg-slate-teal/10 px-2 py-1 rounded-lg">
|
|
{shiftLabel}
|
|
</span>
|
|
)}
|
|
{isWknd && <span className="text-[10px] font-black uppercase tracking-widest text-goldenrod bg-goldenrod/10 px-2 py-1 rounded-lg">Helg</span>}
|
|
{isExtra && (
|
|
<span className="text-[10px] font-black uppercase tracking-widest bg-slate-teal text-eggshell px-2 py-1 rounded-lg shadow-sm">
|
|
Extra
|
|
</span>
|
|
)}
|
|
</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>}
|
|
|
|
<span className="font-black text-slate-teal text-base">
|
|
{formatTimeHHMM(entry.hoursWorked)} <span className="text-xs opacity-70">(+{entry.weightedHours.toFixed(1)}h)</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}; |