fix: close all remaining known gaps across modules
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { CreateCourseForm } from './create-course-form';
|
||||
export { EnrollParticipantDialog } from './enroll-participant-dialog';
|
||||
export { DeleteRefDataButton } from './delete-ref-data-button';
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user