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>