Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/courses/calendar/page.tsx
Zaid Marzguioui ebd0fd4638
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m26s
Workflow / ⚫️ Test (push) Has been skipped
feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Major changes:
- Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI
- Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions
- Sitzungsprotokolle module: meeting protocols, agenda items, task tracking
- Verbandsverwaltung module: federation management, member clubs, contacts, fees
- Per-account module activation via Modules page toggle
- Site Builder: live CMS data in Puck blocks (courses, events, membership registration)
- Public registration APIs: course signup, event registration, membership application
- Document generation: PDF member cards, Excel reports, HTML labels
- Landing page: real Com.BISS content (no filler text)
- UX audit fixes: AccountNotFound component, shared status badges, confirm dialog,
  pagination, duplicate heading removal, emoji→badge replacement, a11y fixes
- QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
2026-03-31 16:35:46 +02:00

247 lines
8.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 { 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 { createCourseManagementApi } from '@kit/course-management/api';
import { CmsPageShell } from '~/components/cms-page-shell';
import { AccountNotFound } from '~/components/account-not-found';
interface PageProps {
params: Promise<{ account: string }>;
}
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 }: PageProps) {
const { account } = await params;
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 year = now.getFullYear();
const 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 c = course as Record<string, unknown>;
if (c.status === 'cancelled') continue;
const startDate = c.start_date ? new Date(String(c.start_date)) : null;
const endDate = c.end_date ? new Date(String(c.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(
(c: Record<string, unknown>) =>
c.status === 'open' || c.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">
<Button variant="ghost" size="icon" disabled>
<ChevronLeft className="h-4 w-4" />
</Button>
<CardTitle>
{MONTH_NAMES[month]} {year}
</CardTitle>
<Button variant="ghost" size="icon" disabled>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* Weekday Header */}
<div className="grid grid-cols-7 gap-1 mb-1">
{WEEKDAYS.map((day) => (
<div
key={day}
className="text-center text-xs font-medium text-muted-foreground py-2"
>
{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 text-emerald-700 dark:text-emerald-400 font-semibold'
: 'bg-muted/30 hover:bg-muted/50'
} ${cell.isToday ? 'ring-2 ring-primary ring-offset-1' : ''}`}
>
{cell.day !== null && (
<>
<span>{cell.day}</span>
{cell.hasCourse && (
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 h-1.5 w-1.5 rounded-full bg-emerald-500" />
)}
</>
)}
</div>
))}
</div>
{/* Legend */}
<div className="mt-4 flex items-center gap-4 text-xs text-muted-foreground">
<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="inline-block h-3 w-3 rounded-sm bg-muted/30" />
Frei
</div>
<div className="flex items-center gap-1.5">
<span className="inline-block h-3 w-3 rounded-sm ring-2 ring-primary" />
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-sm text-muted-foreground">
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-xs text-muted-foreground">
{course.start_date
? new Date(String(course.start_date)).toLocaleDateString('de-DE')
: '—'}{' '}
{' '}
{course.end_date
? new Date(String(course.end_date)).toLocaleDateString('de-DE')
: '—'}
</p>
</div>
<Badge variant={String(course.status) === 'running' ? 'info' : 'default'}>
{String(course.status) === 'running' ? 'Laufend' : 'Offen'}
</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</CmsPageShell>
);
}