Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/courses/calendar/page.tsx
T. Zehetbauer c6b2824da8
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m53s
Workflow / ⚫️ Test (push) Has been skipped
feat: add update and delete functionality for courses, events, and species; enhance attendance tracking and category creation
2026-04-01 16:03:50 +02:00

287 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from 'next/link';
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
import { createCourseManagementApi } from '@kit/course-management/api';
import { formatDate } from '@kit/shared/dates';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
interface PageProps {
params: Promise<{ account: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
const MONTH_NAMES = [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'Juli',
'August',
'September',
'Oktober',
'November',
'Dezember',
];
function getDaysInMonth(year: number, month: number): number {
return new Date(year, month + 1, 0).getDate();
}
function getFirstWeekday(year: number, month: number): number {
const day = new Date(year, month, 1).getDay();
return day === 0 ? 6 : day - 1;
}
export default async function CourseCalendarPage({
params,
searchParams,
}: PageProps) {
const { account } = await params;
const search = await searchParams;
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
.select('id')
.eq('slug', account)
.single();
if (!acct) return <AccountNotFound />;
const api = createCourseManagementApi(client);
const courses = await api.listCourses(acct.id, { page: 1, pageSize: 100 });
const now = new Date();
const monthParam = search.month as string | undefined;
let year: number;
let month: number;
if (monthParam && /^\d{4}-\d{2}$/.test(monthParam)) {
const [y, m] = monthParam.split('-').map(Number);
year = y!;
month = m! - 1;
} else {
year = now.getFullYear();
month = now.getMonth();
}
const daysInMonth = getDaysInMonth(year, month);
const firstWeekday = getFirstWeekday(year, month);
// Build set of days that have running courses
const courseDates = new Set<number>();
for (const course of courses.data) {
const courseItem = course as Record<string, unknown>;
if (courseItem.status === 'cancelled') continue;
const startDate = courseItem.start_date
? new Date(String(courseItem.start_date))
: null;
const endDate = courseItem.end_date
? new Date(String(courseItem.end_date))
: null;
if (!startDate) continue;
const courseStart = startDate;
const courseEnd = endDate ?? startDate;
for (let d = 1; d <= daysInMonth; d++) {
const dayDate = new Date(year, month, d);
if (dayDate >= courseStart && dayDate <= courseEnd) {
courseDates.add(d);
}
}
}
// Build calendar grid
const cells: Array<{
day: number | null;
hasCourse: boolean;
isToday: boolean;
}> = [];
for (let i = 0; i < firstWeekday; i++) {
cells.push({ day: null, hasCourse: false, isToday: false });
}
for (let d = 1; d <= daysInMonth; d++) {
cells.push({
day: d,
hasCourse: courseDates.has(d),
isToday:
d === now.getDate() &&
month === now.getMonth() &&
year === now.getFullYear(),
});
}
while (cells.length % 7 !== 0) {
cells.push({ day: null, hasCourse: false, isToday: false });
}
const activeCourses = courses.data.filter(
(courseItem: Record<string, unknown>) =>
courseItem.status === 'open' || courseItem.status === 'running',
);
return (
<CmsPageShell account={account} title="Kurskalender">
<div className="flex w-full flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href={`/home/${account}/courses`}>
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<p className="text-muted-foreground">Kurstermine im Überblick</p>
</div>
</div>
{/* Month Calendar */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Link
href={`/home/${account}/courses/calendar?month=${
month === 0
? `${year - 1}-12`
: `${year}-${String(month).padStart(2, '0')}`
}`}
>
<Button variant="ghost" size="icon">
<ChevronLeft className="h-4 w-4" />
</Button>
</Link>
<CardTitle>
{MONTH_NAMES[month]} {year}
</CardTitle>
<Link
href={`/home/${account}/courses/calendar?month=${
month === 11
? `${year + 1}-01`
: `${year}-${String(month + 2).padStart(2, '0')}`
}`}
>
<Button variant="ghost" size="icon">
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
{/* Weekday Header */}
<div className="mb-1 grid grid-cols-7 gap-1">
{WEEKDAYS.map((day) => (
<div
key={day}
className="text-muted-foreground py-2 text-center text-xs font-medium"
>
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-1">
{cells.map((cell, idx) => (
<div
key={idx}
className={`relative flex h-12 items-center justify-center rounded-md text-sm transition-colors ${
cell.day === null
? 'bg-transparent'
: cell.hasCourse
? 'bg-emerald-500/15 font-semibold text-emerald-700 dark:text-emerald-400'
: 'bg-muted/30 hover:bg-muted/50'
} ${cell.isToday ? 'ring-primary ring-2 ring-offset-1' : ''}`}
>
{cell.day !== null && (
<>
<span>{cell.day}</span>
{cell.hasCourse && (
<span className="absolute bottom-1 left-1/2 h-1.5 w-1.5 -translate-x-1/2 rounded-full bg-emerald-500" />
)}
</>
)}
</div>
))}
</div>
{/* Legend */}
<div className="text-muted-foreground mt-4 flex items-center gap-4 text-xs">
<div className="flex items-center gap-1.5">
<span className="inline-block h-3 w-3 rounded-sm bg-emerald-500/15" />
Kurstag
</div>
<div className="flex items-center gap-1.5">
<span className="bg-muted/30 inline-block h-3 w-3 rounded-sm" />
Frei
</div>
<div className="flex items-center gap-1.5">
<span className="ring-primary inline-block h-3 w-3 rounded-sm ring-2" />
Heute
</div>
</div>
</CardContent>
</Card>
{/* Active Courses this Month */}
<Card>
<CardHeader>
<CardTitle>Aktive Kurse ({activeCourses.length})</CardTitle>
</CardHeader>
<CardContent>
{activeCourses.length === 0 ? (
<p className="text-muted-foreground text-sm">
Keine aktiven Kurse in diesem Monat.
</p>
) : (
<div className="space-y-3">
{activeCourses.map((course: Record<string, unknown>) => (
<div
key={String(course.id)}
className="flex items-center justify-between rounded-md border p-3"
>
<div>
<Link
href={`/home/${account}/courses/${String(course.id)}`}
className="font-medium hover:underline"
>
{String(course.name)}
</Link>
<p className="text-muted-foreground text-xs">
{formatDate(course.start_date as string)} {' '}
{formatDate(course.end_date as string)}
</p>
</div>
<Badge
variant={
String(course.status) === 'running' ? 'info' : 'default'
}
>
{String(course.status) === 'running'
? 'Laufend'
: 'Offen'}
</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</CmsPageShell>
);
}