120 lines
6.5 KiB
TypeScript
120 lines
6.5 KiB
TypeScript
// app/admin/ReportTab.tsx
|
|
|
|
"use client";
|
|
|
|
import { Download, Users } from 'lucide-react';
|
|
import React, { useState } from 'react';
|
|
import { type AttendanceDataMap, formatTimeHHMM, isWeekend, type Period, type Role } from './adminTypes';
|
|
import { YouthReportCard } from './YouthReportCard';
|
|
|
|
interface Props {
|
|
periods: Period[]; attendance: AttendanceDataMap; activePeriodId: string; setActivePeriodId: (id: string) => void; currentUserRole: Role;
|
|
scheduleData: any[];
|
|
}
|
|
|
|
const POT_HOUR_LIMIT = 90;
|
|
|
|
export const ReportTab: React.FC<Props> = ({ periods, attendance, activePeriodId, setActivePeriodId, currentUserRole, scheduleData }) => {
|
|
const activePeriod = periods.find(p => p.id === activePeriodId);
|
|
const [expandedYouthId, setExpandedYouthId] = useState<string | null>(null);
|
|
|
|
if (!activePeriod) return <p className="text-center font-bold text-slate-teal mt-10">Ingen period tillgänglig.</p>;
|
|
|
|
const getPeriodHoursTotal = (youthId: string): number => {
|
|
let total = 0;
|
|
for (const key in attendance) {
|
|
const entry = attendance[key];
|
|
if (entry.youthId === youthId && entry.date >= activePeriod.startDate && entry.date <= activePeriod.endDate) total += entry.weightedHours;
|
|
}
|
|
return total;
|
|
};
|
|
|
|
const getTimelineForYouth = (youthId: string) => {
|
|
return Object.values(attendance).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; });
|
|
};
|
|
|
|
const exportToCSV = () => {
|
|
if (!activePeriod) return;
|
|
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);
|
|
|
|
entries.sort((a, b) => {
|
|
if (a.date !== b.date) return a.date.localeCompare(b.date);
|
|
if (a.shiftId !== b.shiftId) return a.shiftId.localeCompare(b.shiftId);
|
|
const nameA = activePeriod.youthList.find(y => y.id === a.youthId)?.name || '';
|
|
const nameB = activePeriod.youthList.find(y => y.id === b.youthId)?.name || '';
|
|
return nameA.localeCompare(nameB);
|
|
});
|
|
|
|
entries.forEach(entry => {
|
|
const youth = activePeriod.youthList.find(y => y.id === entry.youthId);
|
|
if (!youth) return;
|
|
const shift = isWeekend(entry.date) ? 'Hela dagen' : (entry.shiftId === 'MORNING' ? 'Morgon' : 'Eftermiddag');
|
|
const teamName = youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare';
|
|
const worked = formatTimeHHMM(entry.hoursWorked);
|
|
const weighted = entry.weightedHours.toFixed(2).replace('.', ',');
|
|
const note = entry.note || '';
|
|
csvContent += `${entry.date};${shift};${youth.name};${teamName};${entry.status};${worked};${weighted};${note}\n`;
|
|
});
|
|
|
|
csvContent += "\nSummering (Timpott)\nNamn;Lag;Total Viktad Pott\n";
|
|
const sortedYouth = [...activePeriod.youthList].sort((a, b) => a.name.localeCompare(b.name));
|
|
sortedYouth.forEach(youth => {
|
|
const total = getPeriodHoursTotal(youth.id).toFixed(2).replace('.', ',');
|
|
const teamName = youth.team === 'PF' ? 'Pilgrimsfalk' : 'Tumlare';
|
|
csvContent += `${youth.name};${teamName};${total}\n`;
|
|
});
|
|
|
|
const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", url); link.setAttribute("download", `Narvaro_${activePeriod.name.replace(/ /g, '_')}.csv`);
|
|
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
|
};
|
|
|
|
const sortedActiveYouth = [...activePeriod.youthList].sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fade-in">
|
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4 rounded-3xl shadow-sm transition-all duration-300 bg-white/40 backdrop-blur-md border border-white/40 p-3 sm:p-5 hover:shadow-md">
|
|
<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">
|
|
{periods.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
</select>
|
|
<div className="text-center md:text-right">
|
|
<h2 className="text-base font-black text-ebony uppercase tracking-widest">{activePeriod.name}</h2>
|
|
<p className="text-xs font-bold text-moss">{activePeriod.startDate} — {activePeriod.endDate}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex flex-col md:flex-row justify-between md:items-center mb-6 gap-3">
|
|
<h2 className="font-black text-ebony uppercase tracking-widest flex items-center text-2xl justify-center sm:justify-normal">
|
|
<Users className="mr-2 text-slate-teal" size={24} /> Timrapport
|
|
</h2>
|
|
|
|
{currentUserRole !== 'Viewer' && (
|
|
<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">
|
|
<Download size={16} /> Exportera till Excel
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{sortedActiveYouth.map(youth => (
|
|
<YouthReportCard
|
|
key={youth.id}
|
|
youth={youth}
|
|
totalHours={getPeriodHoursTotal(youth.id)}
|
|
potHourLimit={POT_HOUR_LIMIT}
|
|
isExpanded={expandedYouthId === youth.id}
|
|
onToggle={() => setExpandedYouthId(expandedYouthId === youth.id ? null : youth.id)}
|
|
timeline={getTimelineForYouth(youth.id)}
|
|
scheduleData={scheduleData}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |