feat: add update and delete functionality for courses, events, and species; enhance attendance tracking and category creation
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m53s
Workflow / ⚫️ Test (push) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-01 16:03:50 +02:00
parent 7b078f298b
commit c6b2824da8
48 changed files with 2036 additions and 390 deletions

View File

@@ -0,0 +1,115 @@
'use client';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Save } from 'lucide-react';
import { markAttendance } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button';
import { toast } from '@kit/ui/sonner';
interface Participant {
id: string;
firstName: string;
lastName: string;
}
interface AttendanceGridProps {
sessionId: string;
participants: Participant[];
initialAttendance: Map<string, boolean>;
}
export function AttendanceGrid({
sessionId,
participants,
initialAttendance,
}: AttendanceGridProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [attendance, setAttendance] = useState<Map<string, boolean>>(
() => new Map(initialAttendance),
);
const [isSaving, setIsSaving] = useState(false);
const toggle = (participantId: string) => {
setAttendance((prev) => {
const next = new Map(prev);
next.set(participantId, !prev.get(participantId));
return next;
});
};
const handleSave = async () => {
setIsSaving(true);
try {
const promises = participants.map((p) =>
markAttendance({
sessionId,
participantId: p.id,
present: attendance.get(p.id) ?? false,
}),
);
await Promise.all(promises);
toast.success('Anwesenheit gespeichert');
startTransition(() => router.refresh());
} catch {
toast.error('Fehler beim Speichern der Anwesenheit');
} finally {
setIsSaving(false);
}
};
return (
<div className="space-y-4">
{participants.length === 0 ? (
<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="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) => (
<tr
key={p.id}
className="hover:bg-muted/30 cursor-pointer border-b"
onClick={() => toggle(p.id)}
>
<td className="p-3 font-medium">
{p.lastName}, {p.firstName}
</td>
<td className="p-3 text-center">
<input
type="checkbox"
checked={attendance.get(p.id) ?? false}
onChange={() => toggle(p.id)}
className="h-4 w-4 rounded border-gray-300"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{participants.length > 0 && (
<div className="flex justify-end">
<Button onClick={handleSave} disabled={isSaving || isPending}>
<Save className="mr-2 h-4 w-4" />
{isSaving ? 'Wird gespeichert...' : 'Anwesenheit speichern'}
</Button>
</div>
)}
</div>
);
}

View File

@@ -10,6 +10,8 @@ import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
import { AttendanceGrid } from './attendance-grid';
interface PageProps {
params: Promise<{ account: string; courseId: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
@@ -49,6 +51,12 @@ export default async function AttendancePage({
]),
);
const participantList = participants.map((p: Record<string, unknown>) => ({
id: String(p.id),
firstName: String(p.first_name ?? ''),
lastName: String(p.last_name ?? ''),
}));
return (
<CmsPageShell account={account} title="Anwesenheit">
<div className="flex w-full flex-col gap-6">
@@ -59,7 +67,6 @@ export default async function AttendancePage({
</p>
</div>
{/* Session Selector */}
{sessions.length === 0 ? (
<EmptyState
icon={<Calendar className="h-8 w-8" />}
@@ -96,7 +103,6 @@ export default async function AttendancePage({
</CardContent>
</Card>
{/* Attendance Grid */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -105,48 +111,16 @@ export default async function AttendancePage({
</CardTitle>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
<p className="text-muted-foreground py-6 text-center text-sm">
Keine Teilnehmer in diesem Kurs
</p>
{selectedSessionId ? (
<AttendanceGrid
sessionId={selectedSessionId}
participants={participantList}
initialAttendance={attendanceMap}
/>
) : (
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<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="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 text-center">
<input
type="checkbox"
defaultChecked={
attendanceMap.get(String(p.id)) ?? false
}
className="h-4 w-4 rounded border-gray-300"
aria-label={`Anwesenheit ${String(p.last_name)}`}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-muted-foreground py-6 text-center text-sm">
Bitte wählen Sie einen Termin aus
</p>
)}
</CardContent>
</Card>

View File

@@ -0,0 +1,61 @@
'use client';
import { useRouter } from 'next/navigation';
import { Trash2 } from 'lucide-react';
import { deleteCourse } from '@kit/course-management/actions/course-actions';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
interface Props {
courseId: string;
accountSlug: string;
}
export function DeleteCourseButton({ courseId, accountSlug }: Props) {
const router = useRouter();
const { execute, isPending } = useActionWithToast(deleteCourse, {
successMessage: 'Kurs wurde abgesagt',
errorMessage: 'Fehler beim Absagen',
onSuccess: () => router.push(`/home/${accountSlug}/courses`),
});
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" disabled={isPending}>
<Trash2 className="mr-2 h-4 w-4" />
Kurs absagen
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Kurs absagen?</AlertDialogTitle>
<AlertDialogDescription>
Der Kurs wird auf den Status &quot;Abgesagt&quot; gesetzt. Diese
Aktion kann rückgängig gemacht werden.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
<AlertDialogAction onClick={() => execute({ courseId })}>
Absagen
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -0,0 +1,53 @@
import { createCourseManagementApi } from '@kit/course-management/api';
import { CreateCourseForm } from '@kit/course-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
interface PageProps {
params: Promise<{ account: string; courseId: string }>;
}
export default async function EditCoursePage({ params }: PageProps) {
const { account, courseId } = 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 course = await api.getCourse(courseId);
if (!course) return <AccountNotFound />;
const c = course as Record<string, unknown>;
return (
<CmsPageShell account={account} title={`${String(c.name)} — Bearbeiten`}>
<CreateCourseForm
accountId={acct.id}
account={account}
courseId={courseId}
initialData={{
courseNumber: String(c.course_number ?? ''),
name: String(c.name ?? ''),
description: String(c.description ?? ''),
startDate: String(c.start_date ?? ''),
endDate: String(c.end_date ?? ''),
fee: Number(c.fee ?? 0),
reducedFee: Number(c.reduced_fee ?? 0),
capacity: Number(c.capacity ?? 20),
minParticipants: Number(c.min_participants ?? 5),
status: String(c.status ?? 'planned'),
registrationDeadline: String(c.registration_deadline ?? ''),
notes: String(c.notes ?? ''),
}}
/>
</CmsPageShell>
);
}

View File

@@ -7,6 +7,7 @@ import {
Euro,
User,
Clock,
Pencil,
} from 'lucide-react';
import { createCourseManagementApi } from '@kit/course-management/api';
@@ -19,6 +20,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { DeleteCourseButton } from './delete-course-button';
interface PageProps {
params: Promise<{ account: string; courseId: string }>;
}
@@ -60,6 +63,17 @@ export default async function CourseDetailPage({ params }: PageProps) {
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" />
Bearbeiten
</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>

View File

@@ -14,6 +14,7 @@ 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'];
@@ -42,8 +43,12 @@ function getFirstWeekday(year: number, month: number): number {
return day === 0 ? 6 : day - 1;
}
export default async function CourseCalendarPage({ params }: PageProps) {
export default async function CourseCalendarPage({
params,
searchParams,
}: PageProps) {
const { account } = await params;
const search = await searchParams;
const client = getSupabaseServerClient();
const { data: acct } = await client
@@ -58,8 +63,17 @@ export default async function CourseCalendarPage({ params }: PageProps) {
const courses = await api.listCourses(acct.id, { page: 1, pageSize: 100 });
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
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);
@@ -139,15 +153,31 @@ export default async function CourseCalendarPage({ params }: PageProps) {
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Button variant="ghost" size="icon" disabled>
<ChevronLeft className="h-4 w-4" />
</Button>
<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>
<Button variant="ghost" size="icon" disabled>
<ChevronRight className="h-4 w-4" />
</Button>
<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>

View File

@@ -0,0 +1,104 @@
'use client';
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react';
import { createCategory } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
interface CreateCategoryDialogProps {
accountId: string;
}
export function CreateCategoryDialog({ accountId }: CreateCategoryDialogProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const { execute, isPending } = useActionWithToast(createCategory, {
successMessage: 'Kategorie erstellt',
errorMessage: 'Fehler beim Erstellen der Kategorie',
onSuccess: () => {
setOpen(false);
setName('');
setDescription('');
router.refresh();
},
});
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
execute({
accountId,
name: name.trim(),
description: description.trim() || undefined,
});
},
[execute, accountId, name, description],
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button size="sm" />}>
<Plus className="mr-1 h-4 w-4" />
Neue Kategorie
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Neue Kategorie</DialogTitle>
<DialogDescription>
Erstellen Sie eine neue Kurskategorie.
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="space-y-2">
<Label htmlFor="cat-name">Name</Label>
<Input
id="cat-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="z. B. Sprachkurse"
required
minLength={1}
maxLength={128}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cat-description">Beschreibung (optional)</Label>
<Input
id="cat-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Kurze Beschreibung"
/>
</div>
</div>
<DialogFooter className="mt-4">
<Button type="submit" disabled={isPending || !name.trim()}>
{isPending ? 'Wird erstellt...' : 'Erstellen'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,14 +1,15 @@
import { FolderTree, Plus } from 'lucide-react';
import { FolderTree } 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 { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
import { CreateCategoryDialog } from './create-category-dialog';
interface PageProps {
params: Promise<{ account: string }>;
}
@@ -33,10 +34,7 @@ export default async function CategoriesPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<p className="text-muted-foreground">Kurskategorien verwalten</p>
<Button data-test="categories-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neue Kategorie
</Button>
<CreateCategoryDialog accountId={acct.id} />
</div>
{categories.length === 0 ? (

View File

@@ -0,0 +1,182 @@
'use client';
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react';
import { createInstructor } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Textarea } from '@kit/ui/textarea';
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
interface CreateInstructorDialogProps {
accountId: string;
}
export function CreateInstructorDialog({
accountId,
}: CreateInstructorDialogProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [qualifications, setQualifications] = useState('');
const [hourlyRate, setHourlyRate] = useState('');
const { execute, isPending } = useActionWithToast(createInstructor, {
successMessage: 'Dozent erstellt',
errorMessage: 'Fehler beim Erstellen des Dozenten',
onSuccess: () => {
setOpen(false);
setFirstName('');
setLastName('');
setEmail('');
setPhone('');
setQualifications('');
setHourlyRate('');
router.refresh();
},
});
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!firstName.trim() || !lastName.trim()) return;
execute({
accountId,
firstName: firstName.trim(),
lastName: lastName.trim(),
email: email.trim() || undefined,
phone: phone.trim() || undefined,
qualifications: qualifications.trim() || undefined,
hourlyRate: hourlyRate ? Number(hourlyRate) : undefined,
});
},
[
execute,
accountId,
firstName,
lastName,
email,
phone,
qualifications,
hourlyRate,
],
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button size="sm" />}>
<Plus className="mr-1 h-4 w-4" />
Neuer Dozent
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Neuer Dozent</DialogTitle>
<DialogDescription>
Einen neuen Dozenten zum Dozentenpool hinzufuegen.
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="inst-first-name">Vorname</Label>
<Input
id="inst-first-name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="Vorname"
required
minLength={1}
maxLength={128}
/>
</div>
<div className="space-y-2">
<Label htmlFor="inst-last-name">Nachname</Label>
<Input
id="inst-last-name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Nachname"
required
minLength={1}
maxLength={128}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="inst-email">E-Mail (optional)</Label>
<Input
id="inst-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="dozent@beispiel.de"
/>
</div>
<div className="space-y-2">
<Label htmlFor="inst-phone">Telefon (optional)</Label>
<Input
id="inst-phone"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+49 123 456789"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="inst-qualifications">
Qualifikationen (optional)
</Label>
<Textarea
id="inst-qualifications"
value={qualifications}
onChange={(e) => setQualifications(e.target.value)}
placeholder="z. B. Zertifizierter Trainer, Erste-Hilfe-Ausbilder"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="inst-hourly-rate">Stundensatz (optional)</Label>
<Input
id="inst-hourly-rate"
type="number"
min={0}
step={0.01}
value={hourlyRate}
onChange={(e) => setHourlyRate(e.target.value)}
placeholder="0.00"
/>
</div>
</div>
<DialogFooter className="mt-4">
<Button
type="submit"
disabled={isPending || !firstName.trim() || !lastName.trim()}
>
{isPending ? 'Wird erstellt...' : 'Erstellen'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,14 +1,15 @@
import { GraduationCap, Plus } from 'lucide-react';
import { GraduationCap } 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 { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
import { CreateInstructorDialog } from './create-instructor-dialog';
interface PageProps {
params: Promise<{ account: string }>;
}
@@ -33,10 +34,7 @@ export default async function InstructorsPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<p className="text-muted-foreground">Dozentenpool verwalten</p>
<Button data-test="instructors-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Dozent
</Button>
<CreateInstructorDialog accountId={acct.id} />
</div>
{instructors.length === 0 ? (

View File

@@ -0,0 +1,132 @@
'use client';
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react';
import { createLocation } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
interface CreateLocationDialogProps {
accountId: string;
}
export function CreateLocationDialog({ accountId }: CreateLocationDialogProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const [room, setRoom] = useState('');
const [capacity, setCapacity] = useState('');
const { execute, isPending } = useActionWithToast(createLocation, {
successMessage: 'Ort erstellt',
errorMessage: 'Fehler beim Erstellen des Ortes',
onSuccess: () => {
setOpen(false);
setName('');
setAddress('');
setRoom('');
setCapacity('');
router.refresh();
},
});
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
execute({
accountId,
name: name.trim(),
address: address.trim() || undefined,
room: room.trim() || undefined,
capacity: capacity ? Number(capacity) : undefined,
});
},
[execute, accountId, name, address, room, capacity],
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button size="sm" />}>
<Plus className="mr-1 h-4 w-4" />
Neuer Ort
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Neuer Ort</DialogTitle>
<DialogDescription>
Einen neuen Kurs- oder Veranstaltungsort hinzufuegen.
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="space-y-2">
<Label htmlFor="loc-name">Name</Label>
<Input
id="loc-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="z. B. Vereinsheim"
required
minLength={1}
maxLength={128}
/>
</div>
<div className="space-y-2">
<Label htmlFor="loc-address">Adresse (optional)</Label>
<Input
id="loc-address"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="Musterstr. 1, 12345 Musterstadt"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="loc-room">Raum (optional)</Label>
<Input
id="loc-room"
value={room}
onChange={(e) => setRoom(e.target.value)}
placeholder="z. B. Raum 101"
/>
</div>
<div className="space-y-2">
<Label htmlFor="loc-capacity">Kapazitaet (optional)</Label>
<Input
id="loc-capacity"
type="number"
min={1}
value={capacity}
onChange={(e) => setCapacity(e.target.value)}
placeholder="z. B. 30"
/>
</div>
</div>
</div>
<DialogFooter className="mt-4">
<Button type="submit" disabled={isPending || !name.trim()}>
{isPending ? 'Wird erstellt...' : 'Erstellen'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,14 +1,15 @@
import { MapPin, Plus } from 'lucide-react';
import { MapPin } 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 { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
import { CreateLocationDialog } from './create-location-dialog';
interface PageProps {
params: Promise<{ account: string }>;
}
@@ -35,10 +36,7 @@ export default async function LocationsPage({ params }: PageProps) {
<p className="text-muted-foreground">
Kurs- und Veranstaltungsorte verwalten
</p>
<Button data-test="locations-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neuer Ort
</Button>
<CreateLocationDialog accountId={acct.id} />
</div>
{locations.length === 0 ? (