feat: Added viewer
This commit is contained in:
182
viewer/components/own/schedule-viewer.tsx
Normal file
182
viewer/components/own/schedule-viewer.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { format, startOfWeek, addDays, parseISO } from 'date-fns';
|
||||
import { cs } from 'date-fns/locale';
|
||||
import { LocalData, SubstitutionData, ChangeEntry, Hour } from '@/lib/types';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
interface ScheduleViewerProps {
|
||||
localData: LocalData;
|
||||
substitutionData: SubstitutionData | null;
|
||||
}
|
||||
|
||||
function getChangesForClass(changes: Record<string, (ChangeEntry | null)[]> | undefined, className: string): (ChangeEntry | null)[] {
|
||||
if (!changes) return [];
|
||||
|
||||
if (changes[className]) return changes[className];
|
||||
|
||||
const key = Object.keys(changes).find(k => k.toLowerCase() === className.toLowerCase());
|
||||
if (key) return changes[key];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export default function ScheduleViewer({ localData, substitutionData }: ScheduleViewerProps) {
|
||||
const referenceDate = useMemo(() => {
|
||||
if (substitutionData?.schedule) {
|
||||
const dates = Object.keys(substitutionData.schedule).sort();
|
||||
if (dates.length > 0) {
|
||||
return parseISO(dates[0]);
|
||||
}
|
||||
}
|
||||
return new Date();
|
||||
}, [substitutionData]);
|
||||
|
||||
const mondayDate = useMemo(() => {
|
||||
return startOfWeek(referenceDate, { weekStartsOn: 1 });
|
||||
}, [referenceDate]);
|
||||
|
||||
const weekDays = useMemo(() => {
|
||||
return Array.from({ length: 5 }, (_, i) => addDays(mondayDate, i));
|
||||
}, [mondayDate]);
|
||||
|
||||
const maxHours = useMemo(() => {
|
||||
let max = 0;
|
||||
localData.timetable.forEach(day => {
|
||||
if (day.length > max) max = day.length;
|
||||
});
|
||||
return max;
|
||||
}, [localData]);
|
||||
|
||||
const currentWeekMaxHours = useMemo(() => {
|
||||
let max = maxHours;
|
||||
if (substitutionData?.schedule) {
|
||||
weekDays.forEach(date => {
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const dayData = substitutionData.schedule[dateStr];
|
||||
if (dayData && dayData.changes) {
|
||||
const changes = getChangesForClass(dayData.changes, localData.class);
|
||||
if (changes.length > max) max = changes.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
return max;
|
||||
}, [maxHours, substitutionData, weekDays, localData.class]);
|
||||
|
||||
const hours = Array.from({ length: currentWeekMaxHours }, (_, i) => i + 1);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Zobrazen týden od {format(mondayDate, 'd. M.', { locale: cs })} do {format(weekDays[4], 'd. M. yyyy', { locale: cs })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden border p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-3 font-semibold border-b border-r min-w-[100px] text-center">
|
||||
Den
|
||||
</th>
|
||||
{hours.map(h => (
|
||||
<th key={h} className="p-3 font-semibold border-b border-r min-w-[120px] text-center">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{weekDays.map((date, dayIndex) => {
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const dayName = format(date, 'EEEE', { locale: cs });
|
||||
|
||||
const staticDay = localData.timetable[dayIndex] || [];
|
||||
|
||||
const dynamicDay = substitutionData?.schedule?.[dateStr];
|
||||
const changes = getChangesForClass(dynamicDay?.changes, localData.class);
|
||||
|
||||
const has3 = hours.some((_, hourIndex) => staticDay[hourIndex].length == 3);
|
||||
|
||||
return (
|
||||
<tr key={dateStr} className="border-b last:border-0 group hover:bg-muted/5" style={{height: has3 ? "120px" : "80px"}}>
|
||||
<td className="p-3 font-medium border-r text-center">
|
||||
<div className="capitalize">{dayName}</div>
|
||||
<div className="text-xs text-muted-foreground">{format(date, 'd. M.')}</div>
|
||||
</td>
|
||||
{hours.map((_, hourIndex) => {
|
||||
const change = changes[hourIndex];
|
||||
const staticLessons = staticDay[hourIndex] || [];
|
||||
|
||||
return (
|
||||
<td key={hourIndex} className="border-r min-w-[120px] h-full align-top relative p-0">
|
||||
<CellContent
|
||||
staticLessons={staticLessons}
|
||||
change={change}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CellContent({ staticLessons, change }: { staticLessons: Hour[], change: ChangeEntry | null | undefined }) {
|
||||
if (change) {
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full p-2 text-xs flex items-center justify-center text-center font-medium shadow-sm"
|
||||
style={{
|
||||
backgroundColor: change.backgroundColor || '#f0f0f0',
|
||||
color: change.foregroundColor ? "#" + change.foregroundColor.substring(3, 6) : '#000',
|
||||
}}
|
||||
>
|
||||
{change.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!staticLessons || staticLessons.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (staticLessons.length > 1) {
|
||||
return (
|
||||
<div className="flex flex-col min-h-[80px] h-full">
|
||||
{staticLessons.map((lesson, idx) => (
|
||||
<div key={idx} className="flex-1 flex flex-col justify-between p-1 text-[10px] border-b min-h-[40px]">
|
||||
<div className='flex justify-between'>
|
||||
<div className="font-bold truncate">{lesson.subject}</div>
|
||||
<span className="truncate opacity-70">{lesson.teacher.code}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="truncate max-w-[40px]">{lesson.room}</span>
|
||||
<span className="truncate opacity-70">{lesson.group}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const lesson = staticLessons[0];
|
||||
return (
|
||||
<div className="w-full min-h-[80px] h-full p-2 border-b flex flex-col justify-between text-xs hover:shadow-md transition-shadow">
|
||||
<div className="font-bold text-lg text-primary">{lesson.subject}</div>
|
||||
<div className="flex justify-between items-end mt-1">
|
||||
<div className="font-mono font-medium">{lesson.room}</div>
|
||||
<div className="text-[10px] opacity-80" title={lesson.teacher.name}>{lesson.teacher.code}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user