Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx
T. Zehetbauer c6d564836f
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 17m4s
Workflow / ⚫️ Test (push) Has been skipped
fix: add missing newlines at the end of JSON files; clean up formatting in page components
2026-04-02 11:02:58 +02:00

296 lines
11 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 {
GraduationCap,
Users,
Calendar,
Euro,
User,
Clock,
Pencil,
} from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api';
import { formatDate, formatCurrencyAmount } 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';
import {
COURSE_STATUS_VARIANT,
COURSE_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
import { CreateSessionDialog } from './create-session-dialog';
import { DeleteCourseButton } from './delete-course-button';
interface PageProps {
params: Promise<{ account: string; courseId: string }>;
}
export default async function CourseDetailPage({ params }: PageProps) {
const { account, courseId } = await params;
const client = getSupabaseServerClient();
const api = createCourseManagementApi(client);
const t = await getTranslations('courses');
const [course, participants, sessions] = await Promise.all([
api.getCourse(courseId),
api.getParticipants(courseId),
api.getSessions(courseId),
]);
if (!course) return <AccountNotFound />;
const courseData = course as Record<string, unknown>;
return (
<CmsPageShell account={account} title={String(courseData.name)}>
<div className="flex w-full flex-col gap-6">
{/* Action Buttons */}
<div className="flex justify-end gap-2">
<Button asChild variant="outline" size="sm">
<Link href={`/home/${account}/courses/${courseId}/edit`}>
<Pencil className="mr-2 h-4 w-4" />
{t('detail.edit')}
</Link>
</Button>
<DeleteCourseButton courseId={courseId} accountSlug={account} />
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardContent className="flex items-center gap-3 p-4">
<GraduationCap className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">
{t('detail.name')}
</p>
<p className="font-semibold">{String(courseData.name)}</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<Clock className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">
{t('common.status')}
</p>
<Badge
variant={
COURSE_STATUS_VARIANT[String(courseData.status)] ??
'secondary'
}
>
{t(
COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ??
String(courseData.status),
)}
</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<User className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">
{t('detail.instructor')}
</p>
<p className="font-semibold">
{String(courseData.instructor_id ?? '—')}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<Calendar className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">
{t('detail.dateRange')}
</p>
<p className="font-semibold">
{formatDate(courseData.start_date as string)}
{' '}
{formatDate(courseData.end_date as string)}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<Euro className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">{t('list.fee')}</p>
<p className="font-semibold">
{courseData.fee != null
? formatCurrencyAmount(courseData.fee as number)
: '—'}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<Users className="text-primary h-5 w-5" />
<div>
<p className="text-muted-foreground text-xs">
{t('detail.participants')}
</p>
<p className="font-semibold">
{participants.length} / {String(courseData.capacity ?? '∞')}
</p>
</div>
</CardContent>
</Card>
</div>
{/* Participants Section */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>{t('detail.participants')}</CardTitle>
<Button variant="outline" size="sm" asChild>
<Link href={`/home/${account}/courses/${courseId}/participants`}>
{t('detail.viewAll')}
</Link>
</Button>
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th scope="col" className="p-3 text-left font-medium">
{t('detail.name')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('detail.email')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.status')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('detail.date')}
</th>
</tr>
</thead>
<tbody>
{participants.length === 0 ? (
<tr>
<td
colSpan={4}
className="text-muted-foreground p-6 text-center"
>
{t('detail.noParticipants')}
</td>
</tr>
) : (
participants.map((p: Record<string, unknown>) => (
<tr
key={String(p.id)}
className="hover:bg-muted/30 border-b"
>
<td className="p-3 font-medium">
{String(p.last_name ?? '')},{' '}
{String(p.first_name ?? '')}
</td>
<td className="p-3">{String(p.email ?? '—')}</td>
<td className="p-3">
<Badge variant="outline">
{String(p.status ?? '—')}
</Badge>
</td>
<td className="p-3">
{formatDate(p.enrolled_at as string)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Sessions Section */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>{t('detail.sessions')}</CardTitle>
<div className="flex gap-2">
<CreateSessionDialog courseId={courseId} />
<Button variant="outline" size="sm" asChild>
<Link href={`/home/${account}/courses/${courseId}/attendance`}>
{t('detail.attendance')}
</Link>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th scope="col" className="p-3 text-left font-medium">
{t('detail.date')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('list.startDate')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('list.endDate')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('detail.cancelled')}
</th>
</tr>
</thead>
<tbody>
{sessions.length === 0 ? (
<tr>
<td
colSpan={4}
className="text-muted-foreground p-6 text-center"
>
{t('detail.noSessions')}
</td>
</tr>
) : (
sessions.map((s: Record<string, unknown>) => (
<tr
key={String(s.id)}
className="hover:bg-muted/30 border-b"
>
<td className="p-3">
{formatDate(s.session_date as string)}
</td>
<td className="p-3">{String(s.start_time ?? '—')}</td>
<td className="p-3">{String(s.end_time ?? '—')}</td>
<td className="p-3">
{s.cancelled ? (
<Badge variant="destructive">
{t('common.yes')}
</Badge>
) : (
'—'
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
</CmsPageShell>
);
}