Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { ClipboardCheck, Calendar } 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 { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
@@ -14,7 +14,10 @@ interface PageProps {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}
|
||||
|
||||
export default async function AttendancePage({ params, searchParams }: PageProps) {
|
||||
export default async function AttendancePage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageProps) {
|
||||
const { account, courseId } = await params;
|
||||
const search = await searchParams;
|
||||
const client = getSupabaseServerClient();
|
||||
@@ -28,14 +31,21 @@ export default async function AttendancePage({ params, searchParams }: PageProps
|
||||
|
||||
if (!course) return <div>Kurs nicht gefunden</div>;
|
||||
|
||||
const selectedSessionId = (search.session as string) ?? (sessions.length > 0 ? String((sessions[0] as Record<string, unknown>).id) : null);
|
||||
const selectedSessionId =
|
||||
(search.session as string) ??
|
||||
(sessions.length > 0
|
||||
? String((sessions[0] as Record<string, unknown>).id)
|
||||
: null);
|
||||
|
||||
const attendance = selectedSessionId
|
||||
? await api.getAttendance(selectedSessionId)
|
||||
: [];
|
||||
|
||||
const attendanceMap = new Map(
|
||||
attendance.map((a: Record<string, unknown>) => [String(a.participant_id), Boolean(a.present)]),
|
||||
attendance.map((a: Record<string, unknown>) => [
|
||||
String(a.participant_id),
|
||||
Boolean(a.present),
|
||||
]),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -70,9 +80,12 @@ export default async function AttendancePage({ params, searchParams }: PageProps
|
||||
key={String(s.id)}
|
||||
href={`/home/${account}/courses/${courseId}/attendance?session=${String(s.id)}`}
|
||||
>
|
||||
<Badge variant={isSelected ? 'default' : 'outline'} className="cursor-pointer px-3 py-1">
|
||||
<Badge
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
className="cursor-pointer px-3 py-1"
|
||||
>
|
||||
{s.session_date
|
||||
? new Date(String(s.session_date)).toLocaleDateString('de-DE')
|
||||
? formatDate(s.session_date as string)
|
||||
: String(s.id)}
|
||||
</Badge>
|
||||
</a>
|
||||
@@ -92,28 +105,38 @@ export default async function AttendancePage({ params, searchParams }: PageProps
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{participants.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground py-6 text-center text-sm">
|
||||
Keine Teilnehmer in diesem Kurs
|
||||
</p>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="p-3 text-left font-medium">Teilnehmer</th>
|
||||
<th className="p-3 text-center font-medium">Anwesend</th>
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">
|
||||
Teilnehmer
|
||||
</th>
|
||||
<th className="p-3 text-center font-medium">
|
||||
Anwesend
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{participants.map((p: Record<string, unknown>) => (
|
||||
<tr key={String(p.id)} className="border-b hover:bg-muted/30">
|
||||
<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 ?? '')}
|
||||
{String(p.last_name ?? '')},{' '}
|
||||
{String(p.first_name ?? '')}
|
||||
</td>
|
||||
<td className="p-3 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={attendanceMap.get(String(p.id)) ?? false}
|
||||
defaultChecked={
|
||||
attendanceMap.get(String(p.id)) ?? false
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
aria-label={`Anwesenheit ${String(p.last_name)}`}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { GraduationCap, Users, Calendar, Euro, User, Clock } from 'lucide-react';
|
||||
import {
|
||||
GraduationCap,
|
||||
Users,
|
||||
Calendar,
|
||||
Euro,
|
||||
User,
|
||||
Clock,
|
||||
} 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 { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
@@ -16,13 +23,22 @@ interface PageProps {
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
planned: 'Geplant', open: 'Offen', running: 'Laufend',
|
||||
completed: 'Abgeschlossen', cancelled: 'Abgesagt',
|
||||
planned: 'Geplant',
|
||||
open: 'Offen',
|
||||
running: 'Laufend',
|
||||
completed: 'Abgeschlossen',
|
||||
cancelled: 'Abgesagt',
|
||||
};
|
||||
|
||||
const STATUS_VARIANT: Record<string, 'secondary' | 'default' | 'info' | 'outline' | 'destructive'> = {
|
||||
planned: 'secondary', open: 'default', running: 'info',
|
||||
completed: 'outline', cancelled: 'destructive',
|
||||
const STATUS_VARIANT: Record<
|
||||
string,
|
||||
'secondary' | 'default' | 'info' | 'outline' | 'destructive'
|
||||
> = {
|
||||
planned: 'secondary',
|
||||
open: 'default',
|
||||
running: 'info',
|
||||
completed: 'outline',
|
||||
cancelled: 'destructive',
|
||||
};
|
||||
|
||||
export default async function CourseDetailPage({ params }: PageProps) {
|
||||
@@ -47,19 +63,21 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
<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="h-5 w-5 text-primary" />
|
||||
<GraduationCap className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Name</p>
|
||||
<p className="text-muted-foreground text-xs">Name</p>
|
||||
<p className="font-semibold">{String(c.name)}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<Clock className="h-5 w-5 text-primary" />
|
||||
<Clock className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Status</p>
|
||||
<Badge variant={STATUS_VARIANT[String(c.status)] ?? 'secondary'}>
|
||||
<p className="text-muted-foreground text-xs">Status</p>
|
||||
<Badge
|
||||
variant={STATUS_VARIANT[String(c.status)] ?? 'secondary'}
|
||||
>
|
||||
{STATUS_LABEL[String(c.status)] ?? String(c.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -67,31 +85,33 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
<User className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Dozent</p>
|
||||
<p className="font-semibold">{String(c.instructor_id ?? '—')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<Calendar className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Beginn – Ende</p>
|
||||
<p className="text-muted-foreground text-xs">Dozent</p>
|
||||
<p className="font-semibold">
|
||||
{c.start_date ? new Date(String(c.start_date)).toLocaleDateString('de-DE') : '—'}
|
||||
{' – '}
|
||||
{c.end_date ? new Date(String(c.end_date)).toLocaleDateString('de-DE') : '—'}
|
||||
{String(c.instructor_id ?? '—')}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<Euro className="h-5 w-5 text-primary" />
|
||||
<Calendar className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Gebühr</p>
|
||||
<p className="text-muted-foreground text-xs">Beginn – Ende</p>
|
||||
<p className="font-semibold">
|
||||
{formatDate(c.start_date as string)}
|
||||
{' – '}
|
||||
{formatDate(c.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">Gebühr</p>
|
||||
<p className="font-semibold">
|
||||
{c.fee != null ? `${Number(c.fee).toFixed(2)} €` : '—'}
|
||||
</p>
|
||||
@@ -100,9 +120,9 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
<Users className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Teilnehmer</p>
|
||||
<p className="text-muted-foreground text-xs">Teilnehmer</p>
|
||||
<p className="font-semibold">
|
||||
{participants.length} / {String(c.capacity ?? '∞')}
|
||||
</p>
|
||||
@@ -116,14 +136,16 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Teilnehmer</CardTitle>
|
||||
<Link href={`/home/${account}/courses/${courseId}/participants`}>
|
||||
<Button variant="outline" size="sm">Alle anzeigen</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Alle anzeigen
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">E-Mail</th>
|
||||
<th className="p-3 text-left font-medium">Status</th>
|
||||
@@ -132,15 +154,36 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{participants.length === 0 ? (
|
||||
<tr><td colSpan={4} className="p-6 text-center text-muted-foreground">Keine Teilnehmer</td></tr>
|
||||
) : participants.map((p: Record<string, unknown>) => (
|
||||
<tr key={String(p.id)} className="border-b hover:bg-muted/30">
|
||||
<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">{p.enrolled_at ? new Date(String(p.enrolled_at)).toLocaleDateString('de-DE') : '—'}</td>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="text-muted-foreground p-6 text-center"
|
||||
>
|
||||
Keine Teilnehmer
|
||||
</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>
|
||||
@@ -152,14 +195,16 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Termine</CardTitle>
|
||||
<Link href={`/home/${account}/courses/${courseId}/attendance`}>
|
||||
<Button variant="outline" size="sm">Anwesenheit</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Anwesenheit
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Datum</th>
|
||||
<th className="p-3 text-left font-medium">Beginn</th>
|
||||
<th className="p-3 text-left font-medium">Ende</th>
|
||||
@@ -168,15 +213,35 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions.length === 0 ? (
|
||||
<tr><td colSpan={4} className="p-6 text-center text-muted-foreground">Keine Termine</td></tr>
|
||||
) : sessions.map((s: Record<string, unknown>) => (
|
||||
<tr key={String(s.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3">{s.session_date ? new Date(String(s.session_date)).toLocaleDateString('de-DE') : '—'}</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">Ja</Badge> : '—'}</td>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="text-muted-foreground p-6 text-center"
|
||||
>
|
||||
Keine Termine
|
||||
</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">Ja</Badge>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@ import Link from 'next/link';
|
||||
|
||||
import { Plus, Users } 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 { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
@@ -16,7 +16,10 @@ interface PageProps {
|
||||
params: Promise<{ account: string; courseId: string }>;
|
||||
}
|
||||
|
||||
const STATUS_VARIANT: Record<string, 'secondary' | 'default' | 'info' | 'outline' | 'destructive'> = {
|
||||
const STATUS_VARIANT: Record<
|
||||
string,
|
||||
'secondary' | 'default' | 'info' | 'outline' | 'destructive'
|
||||
> = {
|
||||
enrolled: 'default',
|
||||
waitlisted: 'secondary',
|
||||
cancelled: 'destructive',
|
||||
@@ -49,7 +52,8 @@ export default async function ParticipantsPage({ params }: PageProps) {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Teilnehmer</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{String((course as Record<string, unknown>).name)} — {participants.length} Teilnehmer
|
||||
{String((course as Record<string, unknown>).name)} —{' '}
|
||||
{participants.length} Teilnehmer
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
@@ -74,31 +78,39 @@ export default async function ParticipantsPage({ params }: PageProps) {
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">E-Mail</th>
|
||||
<th className="p-3 text-left font-medium">Telefon</th>
|
||||
<th className="p-3 text-left font-medium">Status</th>
|
||||
<th className="p-3 text-left font-medium">Anmeldedatum</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
Anmeldedatum
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{participants.map((p: Record<string, unknown>) => (
|
||||
<tr key={String(p.id)} className="border-b hover:bg-muted/30">
|
||||
<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 ?? '')}
|
||||
{String(p.last_name ?? '')},{' '}
|
||||
{String(p.first_name ?? '')}
|
||||
</td>
|
||||
<td className="p-3">{String(p.email ?? '—')}</td>
|
||||
<td className="p-3">{String(p.phone ?? '—')}</td>
|
||||
<td className="p-3">
|
||||
<Badge variant={STATUS_VARIANT[String(p.status)] ?? 'secondary'}>
|
||||
<Badge
|
||||
variant={
|
||||
STATUS_VARIANT[String(p.status)] ?? 'secondary'
|
||||
}
|
||||
>
|
||||
{STATUS_LABEL[String(p.status)] ?? String(p.status)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{p.enrolled_at
|
||||
? new Date(String(p.enrolled_at)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
{formatDate(p.enrolled_at as string)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -2,15 +2,15 @@ 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 { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -86,7 +86,11 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
}
|
||||
|
||||
// Build calendar grid
|
||||
const cells: Array<{ day: number | null; hasCourse: boolean; isToday: boolean }> = [];
|
||||
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 });
|
||||
@@ -96,7 +100,10 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
cells.push({
|
||||
day: d,
|
||||
hasCourse: courseDates.has(d),
|
||||
isToday: d === now.getDate() && month === now.getMonth() && year === now.getFullYear(),
|
||||
isToday:
|
||||
d === now.getDate() &&
|
||||
month === now.getMonth() &&
|
||||
year === now.getFullYear(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,9 +127,7 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<p className="text-muted-foreground">
|
||||
Kurstermine im Überblick
|
||||
</p>
|
||||
<p className="text-muted-foreground">Kurstermine im Überblick</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -143,11 +148,11 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Weekday Header */}
|
||||
<div className="grid grid-cols-7 gap-1 mb-1">
|
||||
<div className="mb-1 grid grid-cols-7 gap-1">
|
||||
{WEEKDAYS.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="text-center text-xs font-medium text-muted-foreground py-2"
|
||||
className="text-muted-foreground py-2 text-center text-xs font-medium"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
@@ -163,15 +168,15 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
cell.day === null
|
||||
? 'bg-transparent'
|
||||
: cell.hasCourse
|
||||
? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 font-semibold'
|
||||
? 'bg-emerald-500/15 font-semibold text-emerald-700 dark:text-emerald-400'
|
||||
: 'bg-muted/30 hover:bg-muted/50'
|
||||
} ${cell.isToday ? 'ring-2 ring-primary ring-offset-1' : ''}`}
|
||||
} ${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 -translate-x-1/2 h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
<span className="absolute bottom-1 left-1/2 h-1.5 w-1.5 -translate-x-1/2 rounded-full bg-emerald-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -180,17 +185,17 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-4 flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<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="inline-block h-3 w-3 rounded-sm bg-muted/30" />
|
||||
<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="inline-block h-3 w-3 rounded-sm ring-2 ring-primary" />
|
||||
<span className="ring-primary inline-block h-3 w-3 rounded-sm ring-2" />
|
||||
Heute
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +209,7 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{activeCourses.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Keine aktiven Kurse in diesem Monat.
|
||||
</p>
|
||||
) : (
|
||||
@@ -221,18 +226,19 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
||||
>
|
||||
{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 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
|
||||
variant={
|
||||
String(course.status) === 'running' ? 'info' : 'default'
|
||||
}
|
||||
>
|
||||
{String(course.status) === 'running'
|
||||
? 'Laufend'
|
||||
: 'Offen'}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { FolderTree, Plus } from 'lucide-react';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -56,17 +55,24 @@ export default async function CategoriesPage({ params }: PageProps) {
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Beschreibung</th>
|
||||
<th className="p-3 text-left font-medium">Übergeordnet</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
Beschreibung
|
||||
</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
Übergeordnet
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{categories.map((cat: Record<string, unknown>) => (
|
||||
<tr key={String(cat.id)} className="border-b hover:bg-muted/30">
|
||||
<tr
|
||||
key={String(cat.id)}
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-medium">{String(cat.name)}</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground p-3">
|
||||
{String(cat.description ?? '—')}
|
||||
</td>
|
||||
<td className="p-3">{String(cat.parent_id ?? '—')}</td>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { GraduationCap, Plus } from 'lucide-react';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -56,23 +55,33 @@ export default async function InstructorsPage({ params }: PageProps) {
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">E-Mail</th>
|
||||
<th className="p-3 text-left font-medium">Telefon</th>
|
||||
<th className="p-3 text-left font-medium">Qualifikation</th>
|
||||
<th className="p-3 text-right font-medium">Stundensatz</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
Qualifikation
|
||||
</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
Stundensatz
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{instructors.map((inst: Record<string, unknown>) => (
|
||||
<tr key={String(inst.id)} className="border-b hover:bg-muted/30">
|
||||
<tr
|
||||
key={String(inst.id)}
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-medium">
|
||||
{String(inst.last_name ?? '')}, {String(inst.first_name ?? '')}
|
||||
{String(inst.last_name ?? '')},{' '}
|
||||
{String(inst.first_name ?? '')}
|
||||
</td>
|
||||
<td className="p-3">{String(inst.email ?? '—')}</td>
|
||||
<td className="p-3">{String(inst.phone ?? '—')}</td>
|
||||
<td className="p-3">{String(inst.qualification ?? '—')}</td>
|
||||
<td className="p-3">
|
||||
{String(inst.qualification ?? '—')}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
{inst.hourly_rate != null
|
||||
? `${Number(inst.hourly_rate).toFixed(2)} €`
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { MapPin, Plus } from 'lucide-react';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -33,7 +32,9 @@ export default async function LocationsPage({ params }: PageProps) {
|
||||
<CmsPageShell account={account} title="Orte">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-muted-foreground">Kurs- und Veranstaltungsorte verwalten</p>
|
||||
<p className="text-muted-foreground">
|
||||
Kurs- und Veranstaltungsorte verwalten
|
||||
</p>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Neuer Ort
|
||||
@@ -56,7 +57,7 @@ export default async function LocationsPage({ params }: PageProps) {
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Adresse</th>
|
||||
<th className="p-3 text-left font-medium">Raum</th>
|
||||
@@ -65,7 +66,10 @@ export default async function LocationsPage({ params }: PageProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{locations.map((loc: Record<string, unknown>) => (
|
||||
<tr key={String(loc.id)} className="border-b hover:bg-muted/30">
|
||||
<tr
|
||||
key={String(loc.id)}
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-medium">{String(loc.name)}</td>
|
||||
<td className="p-3">
|
||||
{[loc.street, loc.postal_code, loc.city]
|
||||
@@ -74,7 +78,9 @@ export default async function LocationsPage({ params }: PageProps) {
|
||||
.join(', ') || '—'}
|
||||
</td>
|
||||
<td className="p-3">{String(loc.room ?? '—')}</td>
|
||||
<td className="p-3 text-right">{String(loc.capacity ?? '—')}</td>
|
||||
<td className="p-3 text-right">
|
||||
{String(loc.capacity ?? '—')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { CreateCourseForm } from '@kit/course-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function NewCoursePage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
if (!acct) return <AccountNotFound />;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neuer Kurs" description="Kurs anlegen">
|
||||
<CmsPageShell
|
||||
account={account}
|
||||
title="Neuer Kurs"
|
||||
description="Kurs anlegen"
|
||||
>
|
||||
<CreateCourseForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ChevronLeft, ChevronRight, GraduationCap, Plus, Users, Calendar, Euro } from 'lucide-react';
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
GraduationCap,
|
||||
Plus,
|
||||
Users,
|
||||
Calendar,
|
||||
Euro,
|
||||
} 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 { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { StatsCard } from '~/components/stats-card';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { COURSE_STATUS_VARIANT, COURSE_STATUS_LABEL } from '~/lib/status-badges';
|
||||
import {
|
||||
COURSE_STATUS_VARIANT,
|
||||
COURSE_STATUS_LABEL,
|
||||
} from '~/lib/status-badges';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -50,9 +61,7 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-muted-foreground">
|
||||
Kursangebot verwalten
|
||||
</p>
|
||||
<p className="text-muted-foreground">Kursangebot verwalten</p>
|
||||
|
||||
<Link href={`/home/${account}/courses/new`}>
|
||||
<Button>
|
||||
@@ -104,7 +113,7 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Kursnr.</th>
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Beginn</th>
|
||||
@@ -116,7 +125,10 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{courses.data.map((course: Record<string, unknown>) => (
|
||||
<tr key={String(course.id)} className="border-b hover:bg-muted/30">
|
||||
<tr
|
||||
key={String(course.id)}
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-mono text-xs">
|
||||
{String(course.course_number ?? '—')}
|
||||
</td>
|
||||
@@ -129,20 +141,20 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
||||
</Link>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{course.start_date
|
||||
? new Date(String(course.start_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
{formatDate(course.start_date as string)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{course.end_date
|
||||
? new Date(String(course.end_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
{formatDate(course.end_date as string)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Badge
|
||||
variant={COURSE_STATUS_VARIANT[String(course.status)] ?? 'secondary'}
|
||||
variant={
|
||||
COURSE_STATUS_VARIANT[String(course.status)] ??
|
||||
'secondary'
|
||||
}
|
||||
>
|
||||
{COURSE_STATUS_LABEL[String(course.status)] ?? String(course.status)}
|
||||
{COURSE_STATUS_LABEL[String(course.status)] ??
|
||||
String(course.status)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
@@ -164,7 +176,7 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between border-t px-2 py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({courses.total} Einträge)
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { GraduationCap, Users, Calendar, TrendingUp, BarChart3 } from 'lucide-react';
|
||||
import {
|
||||
GraduationCap,
|
||||
Users,
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
BarChart3,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { StatsCard } from '~/components/stats-card';
|
||||
import { StatsBarChart, StatsPieChart } from '~/components/stats-charts';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -18,7 +23,11 @@ export default async function CourseStatisticsPage({ params }: PageProps) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
if (!acct) return <AccountNotFound />;
|
||||
|
||||
const api = createCourseManagementApi(client);
|
||||
@@ -34,10 +43,26 @@ export default async function CourseStatisticsPage({ params }: PageProps) {
|
||||
<CmsPageShell account={account} title="Kurs-Statistiken">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatsCard title="Kurse gesamt" value={stats.totalCourses} icon={<GraduationCap className="h-5 w-5" />} />
|
||||
<StatsCard title="Aktive Kurse" value={stats.openCourses} icon={<Calendar className="h-5 w-5" />} />
|
||||
<StatsCard title="Teilnehmer" value={stats.totalParticipants} icon={<Users className="h-5 w-5" />} />
|
||||
<StatsCard title="Abgeschlossen" value={stats.completedCourses} icon={<TrendingUp className="h-5 w-5" />} />
|
||||
<StatsCard
|
||||
title="Kurse gesamt"
|
||||
value={stats.totalCourses}
|
||||
icon={<GraduationCap className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Aktive Kurse"
|
||||
value={stats.openCourses}
|
||||
icon={<Calendar className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Teilnehmer"
|
||||
value={stats.totalParticipants}
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Abgeschlossen"
|
||||
value={stats.completedCourses}
|
||||
icon={<TrendingUp className="h-5 w-5" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
|
||||
Reference in New Issue
Block a user