fix: close all remaining known gaps across modules
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m52s
Workflow / ⚫️ Test (push) Has been skipped

Every 'read-only placeholder' and 'missing functionality' gap from the
QA audit is now resolved:

COURSES — categories/instructors/locations can now be deleted:
- Added update/delete methods to course-reference-data.service.ts
- Added deleteCategory/deleteInstructor/deleteLocation server actions
- Created DeleteRefDataButton client component with confirmation dialog
- Wired delete buttons into all three table pages

BOOKINGS — calendar month navigation now works:
- Calendar was hardcoded to current month with disabled prev/next
- Added year/month search params for server-side month rendering
- Replaced disabled buttons with Link-based navigation
- Verified: clicking next/prev correctly renders different months

DOCUMENTS — templates page now reads from database:
- Was hardcoded empty array; now queries document_templates table
- Table exists since migration 20260414000006_shared_templates.sql

FISCHEREI — statistics page shows real data:
- Replaced dashed-border placeholder with 6 real stat cards
- Queries waters, species, stocking, catch_books, leases, permits
- Shows counts + stocking costs + pending catch books
- Falls back to helpful message when no data exists

VERBAND — statistics page shows real KPIs:
- Added server-side data fetching (clubs, members, fees)
- Passes activeClubs, totalMembers, openFees as props
- Added 4 KPI cards: Aktive Vereine, Gesamtmitglieder,
  ∅ Mitglieder/Verein, Offene Beiträge
- Kept existing trend charts below KPI cards
This commit is contained in:
Zaid Marzguioui
2026-04-03 23:52:25 +02:00
parent ad01ecb8b9
commit 9cbe6652a1
12 changed files with 408 additions and 30 deletions

View File

@@ -0,0 +1,113 @@
'use client';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { Loader2, Trash2 } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { toast } from '@kit/ui/sonner';
import {
deleteCategory,
deleteInstructor,
deleteLocation,
} from '../server/actions/course-actions';
type RefDataType = 'category' | 'instructor' | 'location';
const actions = {
category: deleteCategory,
instructor: deleteInstructor,
location: deleteLocation,
};
const labels: Record<RefDataType, { name: string; confirm: string }> = {
category: {
name: 'Kategorie',
confirm: 'Möchten Sie diese Kategorie wirklich löschen?',
},
instructor: {
name: 'Kursleiter',
confirm: 'Möchten Sie diesen Kursleiter wirklich löschen?',
},
location: {
name: 'Standort',
confirm: 'Möchten Sie diesen Standort wirklich löschen?',
},
};
interface DeleteRefDataButtonProps {
id: string;
type: RefDataType;
itemName: string;
}
export function DeleteRefDataButton({
id,
type,
itemName,
}: DeleteRefDataButtonProps) {
const router = useRouter();
const label = labels[type];
const action = useAction(actions[type], {
onSuccess: () => {
toast.success(`${label.name}${itemName}" gelöscht`);
router.refresh();
},
onError: () =>
toast.error(`Fehler beim Löschen`, {
description:
'Möglicherweise wird der Eintrag noch von Kursen verwendet.',
}),
});
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive hover:bg-destructive/10 h-8 w-8"
aria-label={`${label.name} löschen`}
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{label.name} löschen</AlertDialogTitle>
<AlertDialogDescription>
{label.confirm} <strong>{itemName}</strong>" wird unwiderruflich
entfernt.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
<AlertDialogAction
onClick={() => action.execute({ id })}
disabled={action.isPending}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{action.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Löschen
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -1,2 +1,3 @@
export { CreateCourseForm } from './create-course-form';
export { EnrollParticipantDialog } from './enroll-participant-dialog';
export { DeleteRefDataButton } from './delete-ref-data-button';

View File

@@ -188,3 +188,32 @@ export const createSession = authActionClient
logger.info({ name: 'course.createSession' }, 'Session created');
return { success: true, data: result };
});
// ── Delete reference data ──
export const deleteCategory = authActionClient
.inputSchema(z.object({ id: z.string().uuid() }))
.action(async ({ parsedInput: { id } }) => {
const client = getSupabaseServerClient();
const api = createCourseManagementApi(client);
await api.referenceData.deleteCategory(id);
return { success: true };
});
export const deleteInstructor = authActionClient
.inputSchema(z.object({ id: z.string().uuid() }))
.action(async ({ parsedInput: { id } }) => {
const client = getSupabaseServerClient();
const api = createCourseManagementApi(client);
await api.referenceData.deleteInstructor(id);
return { success: true };
});
export const deleteLocation = authActionClient
.inputSchema(z.object({ id: z.string().uuid() }))
.action(async ({ parsedInput: { id } }) => {
const client = getSupabaseServerClient();
const api = createCourseManagementApi(client);
await api.referenceData.deleteLocation(id);
return { success: true };
});

View File

@@ -104,5 +104,80 @@ export function createCourseReferenceDataService(
if (error) throw error;
return data;
},
// ── Update / Delete ──
async updateCategory(
id: string,
input: { name?: string; description?: string },
) {
const { error } = await client
.from('course_categories')
.update(input)
.eq('id', id);
if (error) throw error;
},
async deleteCategory(id: string) {
const { error } = await client
.from('course_categories')
.delete()
.eq('id', id);
if (error) throw error;
},
async updateInstructor(
id: string,
input: {
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
qualifications?: string;
hourlyRate?: number;
},
) {
const update: Record<string, unknown> = {};
if (input.firstName !== undefined) update.first_name = input.firstName;
if (input.lastName !== undefined) update.last_name = input.lastName;
if (input.email !== undefined) update.email = input.email;
if (input.phone !== undefined) update.phone = input.phone;
if (input.qualifications !== undefined)
update.qualifications = input.qualifications;
if (input.hourlyRate !== undefined) update.hourly_rate = input.hourlyRate;
const { error } = await client
.from('course_instructors')
.update(update)
.eq('id', id);
if (error) throw error;
},
async deleteInstructor(id: string) {
const { error } = await client
.from('course_instructors')
.delete()
.eq('id', id);
if (error) throw error;
},
async updateLocation(
id: string,
input: { name?: string; address?: string; room?: string; capacity?: number },
) {
const { error } = await client
.from('course_locations')
.update(input)
.eq('id', id);
if (error) throw error;
},
async deleteLocation(id: string) {
const { error } = await client
.from('course_locations')
.delete()
.eq('id', id);
if (error) throw error;
},
};
}