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

@@ -33,24 +33,7 @@ export default async function RecordDetailPage({
if (!moduleWithFields || !record) return <div>Nicht gefunden</div>;
const fields = (
moduleWithFields as unknown as {
fields: Array<{
name: string;
display_name: string;
field_type: string;
is_required: boolean;
placeholder: string | null;
help_text: string | null;
is_readonly: boolean;
select_options: Array<{ label: string; value: string }> | null;
section: string;
sort_order: number;
show_in_form: boolean;
width: string;
}>;
}
).fields;
const fields = moduleWithFields.fields;
return (
<CmsPageShell

View File

@@ -19,12 +19,7 @@ export default async function ImportPage({ params }: ImportPageProps) {
const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
if (!moduleWithFields) return <div>Modul nicht gefunden</div>;
const fields =
(
moduleWithFields as unknown as {
fields: Array<{ name: string; display_name: string }>;
}
).fields ?? [];
const fields = moduleWithFields.fields ?? [];
return (
<CmsPageShell

View File

@@ -0,0 +1,74 @@
'use client';
import { useCallback } from 'react';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { ModuleTable } from '@kit/module-builder/components';
type FieldDef = Parameters<typeof ModuleTable>[0]['fields'][number];
interface ModuleRecordsTableProps {
fields: FieldDef[];
records: Array<{
id: string;
data: Record<string, unknown>;
status: string;
created_at: string;
updated_at: string;
}>;
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
account: string;
moduleId: string;
currentSort?: { field: string; direction: 'asc' | 'desc' };
}
export function ModuleRecordsTable({
fields,
records,
pagination,
account,
moduleId,
currentSort,
}: ModuleRecordsTableProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const updateParams = useCallback(
(updates: Record<string, string | null>) => {
const params = new URLSearchParams(searchParams.toString());
for (const [key, value] of Object.entries(updates)) {
if (value === null || value === '') {
params.delete(key);
} else {
params.set(key, value);
}
}
const qs = params.toString();
router.push(qs ? `${pathname}?${qs}` : pathname);
},
[router, pathname, searchParams],
);
return (
<ModuleTable
fields={fields}
records={records}
pagination={pagination}
currentSort={currentSort}
onPageChange={(page) => updateParams({ page: String(page) })}
onSort={(field, direction) =>
updateParams({ sort: field, dir: direction, page: null })
}
onRowClick={(recordId) =>
router.push(`/home/${account}/modules/${moduleId}/${recordId}`)
}
/>
);
}

View File

@@ -27,24 +27,7 @@ export default async function NewRecordPage({ params }: NewRecordPageProps) {
const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
if (!moduleWithFields) return <div>Modul nicht gefunden</div>;
const fields = (
moduleWithFields as unknown as {
fields: Array<{
name: string;
display_name: string;
field_type: string;
is_required: boolean;
placeholder: string | null;
help_text: string | null;
is_readonly: boolean;
select_options: Array<{ label: string; value: string }> | null;
section: string;
sort_order: number;
show_in_form: boolean;
width: string;
}>;
}
).fields;
const fields = moduleWithFields.fields;
return (
<CmsPageShell

View File

@@ -7,6 +7,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button';
import { decodeFilters } from './_lib/filter-params';
import { ModuleRecordsTable } from './module-records-table';
import { ModuleSearchBar } from './module-search-bar';
interface ModuleDetailPageProps {
@@ -33,34 +34,34 @@ export default async function ModuleDetailPage({
const pageSize =
Number(search.pageSize) || moduleWithFields.default_page_size || 25;
const sortField =
(search.sort as string) ?? moduleWithFields.default_sort_field ?? undefined;
const sortDirection =
(search.dir as 'asc' | 'desc') ??
(moduleWithFields.default_sort_direction as 'asc' | 'desc') ??
'asc';
const filters = decodeFilters(search.f as string | undefined);
const result = await api.query.query({
moduleId,
page,
pageSize,
sortField:
(search.sort as string) ??
moduleWithFields.default_sort_field ??
undefined,
sortDirection:
(search.dir as 'asc' | 'desc') ??
(moduleWithFields.default_sort_direction as 'asc' | 'desc') ??
'asc',
sortField,
sortDirection,
search: (search.q as string) ?? undefined,
filters,
});
const fields = (
moduleWithFields as unknown as {
fields: Array<{
name: string;
display_name: string;
show_in_filter: boolean;
show_in_search: boolean;
}>;
}
).fields;
const allFields = moduleWithFields.fields;
const records = (result.data ?? []).map((row: Record<string, unknown>) => ({
id: String(row.id ?? ''),
data: (row.data ?? {}) as Record<string, unknown>,
status: String(row.status ?? 'active'),
created_at: String(row.created_at ?? ''),
updated_at: String(row.updated_at ?? ''),
}));
return (
<div className="flex flex-col gap-4">
@@ -83,18 +84,18 @@ export default async function ModuleDetailPage({
</Button>
</div>
<ModuleSearchBar fields={fields} />
<ModuleSearchBar fields={allFields} />
<div className="text-muted-foreground text-sm">
{result.pagination.total} Datensätze Seite {result.pagination.page}{' '}
von {result.pagination.totalPages}
</div>
<div className="rounded-lg border">
<pre className="max-h-96 overflow-auto p-4 text-xs">
{JSON.stringify(result.data, null, 2)}
</pre>
</div>
<ModuleRecordsTable
fields={allFields}
records={records}
pagination={result.pagination}
account={account}
moduleId={moduleId}
currentSort={
sortField ? { field: sortField, direction: sortDirection } : undefined
}
/>
</div>
);
}

View File

@@ -0,0 +1,146 @@
'use client';
import { useRouter } from 'next/navigation';
import { Settings2 } from 'lucide-react';
import { updateModule } from '@kit/module-builder/actions/module-actions';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
const FEATURE_TOGGLES = [
{ key: 'enableSearch', dbKey: 'enable_search', label: 'Suche' },
{ key: 'enableFilter', dbKey: 'enable_filter', label: 'Filter' },
{ key: 'enableExport', dbKey: 'enable_export', label: 'Export' },
{ key: 'enableImport', dbKey: 'enable_import', label: 'Import' },
{ key: 'enablePrint', dbKey: 'enable_print', label: 'Drucken' },
{ key: 'enableCopy', dbKey: 'enable_copy', label: 'Kopieren' },
{ key: 'enableHistory', dbKey: 'enable_history', label: 'Verlauf' },
{ key: 'enableSoftDelete', dbKey: 'enable_soft_delete', label: 'Papierkorb' },
{ key: 'enableLock', dbKey: 'enable_lock', label: 'Sperren' },
] as const;
interface ModuleSettingsFormProps {
moduleId: string;
initialData: {
name: string;
displayName: string;
description: string;
icon: string;
defaultPageSize: number;
features: Record<string, boolean>;
};
}
export function ModuleSettingsForm({
moduleId,
initialData,
}: ModuleSettingsFormProps) {
const router = useRouter();
const { execute, isPending } = useActionWithToast(updateModule, {
successMessage: 'Einstellungen gespeichert',
errorMessage: 'Fehler beim Speichern',
onSuccess: () => router.refresh(),
});
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Allgemein
</CardTitle>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
execute({
moduleId,
displayName: fd.get('displayName') as string,
description: (fd.get('description') as string) || undefined,
icon: (fd.get('icon') as string) || undefined,
defaultPageSize: Number(fd.get('defaultPageSize')) || 25,
...Object.fromEntries(
FEATURE_TOGGLES.map(({ key }) => [key, fd.get(key) === 'on']),
),
});
}}
className="space-y-4"
>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="displayName">Anzeigename</Label>
<Input
id="displayName"
name="displayName"
defaultValue={initialData.displayName}
/>
</div>
<div className="space-y-2">
<Label>Systemname</Label>
<Input
defaultValue={initialData.name}
readOnly
className="bg-muted"
/>
</div>
<div className="col-span-full space-y-2">
<Label htmlFor="description">Beschreibung</Label>
<Input
id="description"
name="description"
defaultValue={initialData.description}
/>
</div>
<div className="space-y-2">
<Label htmlFor="icon">Symbol</Label>
<Input id="icon" name="icon" defaultValue={initialData.icon} />
</div>
<div className="space-y-2">
<Label htmlFor="defaultPageSize">Seitengröße</Label>
<Input
id="defaultPageSize"
name="defaultPageSize"
type="number"
defaultValue={String(initialData.defaultPageSize)}
/>
</div>
</div>
<div className="flex flex-wrap gap-2">
{FEATURE_TOGGLES.map(({ key, dbKey, label }) => {
const isEnabled = initialData.features[dbKey] ?? false;
return (
<label
key={key}
className="flex cursor-pointer items-center gap-1.5"
>
<input
type="checkbox"
name={key}
defaultChecked={isEnabled}
className="h-4 w-4 rounded border-gray-300"
/>
<Badge variant={isEnabled ? 'default' : 'secondary'}>
{label}
</Badge>
</label>
);
})}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? 'Wird gespeichert...' : 'Einstellungen speichern'}
</Button>
</form>
</CardContent>
</Card>
);
}

View File

@@ -1,16 +1,14 @@
import { Settings2, List, Shield } from 'lucide-react';
import { List, Shield } from 'lucide-react';
import { createModuleBuilderApi } from '@kit/module-builder/api';
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 { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { CmsPageShell } from '~/components/cms-page-shell';
import { DeleteModuleButton } from './delete-module-button';
import { ModuleSettingsForm } from './module-settings-form';
interface ModuleSettingsPageProps {
params: Promise<{ account: string; moduleId: string }>;
@@ -27,8 +25,24 @@ export default async function ModuleSettingsPage({
if (!moduleWithFields) return <div>Modul nicht gefunden</div>;
const mod = moduleWithFields;
const fields =
(mod as unknown as { fields: Array<Record<string, unknown>> }).fields ?? [];
const fields = mod.fields ?? [];
const featureKeys = [
'enable_search',
'enable_filter',
'enable_export',
'enable_import',
'enable_print',
'enable_copy',
'enable_history',
'enable_soft_delete',
'enable_lock',
] as const;
const features: Record<string, boolean> = {};
for (const key of featureKeys) {
features[key] = Boolean(mod[key]);
}
return (
<CmsPageShell
@@ -36,71 +50,17 @@ export default async function ModuleSettingsPage({
title={`${String(mod.display_name)} — Einstellungen`}
>
<div className="space-y-6">
{/* General Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Allgemein
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Anzeigename</Label>
<Input defaultValue={String(mod.display_name)} />
</div>
<div className="space-y-2">
<Label>Systemname</Label>
<Input
defaultValue={String(mod.name)}
readOnly
className="bg-muted"
/>
</div>
<div className="col-span-full space-y-2">
<Label>Beschreibung</Label>
<Input defaultValue={String(mod.description ?? '')} />
</div>
<div className="space-y-2">
<Label>Symbol</Label>
<Input defaultValue={String(mod.icon ?? 'table')} />
</div>
<div className="space-y-2">
<Label>Seitengröße</Label>
<Input
type="number"
defaultValue={String(mod.default_page_size ?? 25)}
/>
</div>
</div>
<div className="flex flex-wrap gap-2">
{[
{ key: 'enable_search', label: 'Suche' },
{ key: 'enable_filter', label: 'Filter' },
{ key: 'enable_export', label: 'Export' },
{ key: 'enable_import', label: 'Import' },
{ key: 'enable_print', label: 'Drucken' },
{ key: 'enable_copy', label: 'Kopieren' },
{ key: 'enable_history', label: 'Verlauf' },
{ key: 'enable_soft_delete', label: 'Papierkorb' },
{ key: 'enable_lock', label: 'Sperren' },
].map(({ key, label }) => (
<Badge
key={key}
variant={
(mod as Record<string, unknown>)[key]
? 'default'
: 'secondary'
}
>
{(mod as Record<string, unknown>)[key] ? '✓' : '✗'} {label}
</Badge>
))}
</div>
<Button>Einstellungen speichern</Button>
</CardContent>
</Card>
<ModuleSettingsForm
moduleId={moduleId}
initialData={{
name: String(mod.name),
displayName: String(mod.display_name),
description: String(mod.description ?? ''),
icon: String(mod.icon ?? 'table'),
defaultPageSize: Number(mod.default_page_size ?? 25),
features,
}}
/>
{/* Field Definitions */}
<Card>
@@ -109,7 +69,6 @@ export default async function ModuleSettingsPage({
<List className="h-4 w-4" />
Felder ({fields.length})
</CardTitle>
<Button size="sm">+ Feld hinzufügen</Button>
</CardHeader>
<CardContent>
<div className="rounded-md border">