feat: enhance API response handling and add new components for module management
This commit is contained in:
@@ -1,17 +1,12 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Pencil, Trash2, Lock, Unlock } from 'lucide-react';
|
||||
|
||||
import { createModuleBuilderApi } from '@kit/module-builder/api';
|
||||
import { ModuleForm } from '@kit/module-builder/components';
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
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 { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
import { RecordDetailClient } from './record-detail-client';
|
||||
|
||||
interface RecordDetailPageProps {
|
||||
params: Promise<{ account: string; moduleId: string; recordId: string }>;
|
||||
}
|
||||
@@ -23,6 +18,14 @@ export default async function RecordDetailPage({
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createModuleBuilderApi(client);
|
||||
|
||||
const { data: accountData } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
if (!accountData) return <AccountNotFound />;
|
||||
|
||||
const [moduleWithFields, record] = await Promise.all([
|
||||
api.modules.getModuleWithFields(moduleId),
|
||||
api.records.getRecord(recordId),
|
||||
@@ -49,68 +52,23 @@ export default async function RecordDetailPage({
|
||||
}
|
||||
).fields;
|
||||
|
||||
const data = (record.data ?? {}) as Record<string, unknown>;
|
||||
const isLocked = record.status === 'locked';
|
||||
|
||||
return (
|
||||
<CmsPageShell
|
||||
account={account}
|
||||
title={`${String(moduleWithFields.display_name)} — Datensatz`}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
isLocked
|
||||
? 'destructive'
|
||||
: record.status === 'active'
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
>
|
||||
{String(record.status)}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Erstellt: {formatDate(record.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{isLocked ? (
|
||||
<Button variant="outline" size="sm">
|
||||
<Unlock className="mr-2 h-4 w-4" />
|
||||
Entsperren
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" size="sm">
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
Sperren
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Löschen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Pencil className="h-4 w-4" />
|
||||
Datensatz bearbeiten
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ModuleForm
|
||||
fields={fields as Parameters<typeof ModuleForm>[0]['fields']}
|
||||
initialData={data}
|
||||
onSubmit={async () => {}}
|
||||
isLoading={false}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<RecordDetailClient
|
||||
fields={fields as Parameters<typeof ModuleForm>[0]['fields']}
|
||||
record={{
|
||||
id: record.id as string,
|
||||
data: (record.data ?? {}) as Record<string, unknown>,
|
||||
status: record.status as string,
|
||||
created_at: record.created_at as string,
|
||||
}}
|
||||
moduleId={moduleId}
|
||||
accountId={accountData.id}
|
||||
accountSlug={account}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Pencil, Trash2, Lock, Unlock } from 'lucide-react';
|
||||
|
||||
import {
|
||||
updateRecord,
|
||||
deleteRecord,
|
||||
lockRecord,
|
||||
} from '@kit/module-builder/actions/record-actions';
|
||||
import { ModuleForm } from '@kit/module-builder/components';
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||
|
||||
type FieldDef = Parameters<typeof ModuleForm>[0]['fields'][number];
|
||||
|
||||
interface RecordDetailClientProps {
|
||||
fields: FieldDef[];
|
||||
record: {
|
||||
id: string;
|
||||
data: Record<string, unknown>;
|
||||
status: string;
|
||||
created_at: string;
|
||||
};
|
||||
moduleId: string;
|
||||
accountId: string;
|
||||
accountSlug: string;
|
||||
}
|
||||
|
||||
export function RecordDetailClient({
|
||||
fields,
|
||||
record,
|
||||
moduleId,
|
||||
accountId,
|
||||
accountSlug,
|
||||
}: RecordDetailClientProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const isLocked = record.status === 'locked';
|
||||
|
||||
const lockSuccessMessage = isLocked
|
||||
? 'Datensatz entsperrt'
|
||||
: 'Datensatz gesperrt';
|
||||
|
||||
const { execute: execUpdate, isPending: isUpdating } = useActionWithToast(
|
||||
updateRecord,
|
||||
{
|
||||
successMessage: 'Datensatz aktualisiert',
|
||||
errorMessage: 'Fehler beim Aktualisieren',
|
||||
onSuccess: () => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { execute: execDelete, isPending: isDeleting } = useActionWithToast(
|
||||
deleteRecord,
|
||||
{
|
||||
successMessage: 'Datensatz gelöscht',
|
||||
errorMessage: 'Fehler beim Löschen',
|
||||
onSuccess: () => {
|
||||
startTransition(() => {
|
||||
router.push(`/home/${accountSlug}/modules/${moduleId}`);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { execute: execLock, isPending: isLocking } = useActionWithToast(
|
||||
lockRecord,
|
||||
{
|
||||
successMessage: lockSuccessMessage,
|
||||
errorMessage: 'Fehler beim Sperren',
|
||||
onSuccess: () => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const isBusy = isUpdating || isDeleting || isLocking || isPending;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
isLocked
|
||||
? 'destructive'
|
||||
: record.status === 'active'
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
>
|
||||
{record.status}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Erstellt: {formatDate(record.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isBusy}
|
||||
onClick={() =>
|
||||
execLock({
|
||||
recordId: record.id,
|
||||
lock: !isLocked,
|
||||
accountId,
|
||||
})
|
||||
}
|
||||
>
|
||||
{isLocked ? (
|
||||
<>
|
||||
<Unlock className="mr-2 h-4 w-4" />
|
||||
Entsperren
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
Sperren
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" disabled={isBusy}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Löschen
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Datensatz löschen?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Dieser Datensatz wird unwiderruflich gelöscht. Diese Aktion
|
||||
kann nicht rückgängig gemacht werden.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
execDelete({
|
||||
recordId: record.id,
|
||||
hard: false,
|
||||
accountId,
|
||||
})
|
||||
}
|
||||
>
|
||||
Löschen
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Pencil className="h-4 w-4" />
|
||||
Datensatz bearbeiten
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ModuleForm
|
||||
fields={fields}
|
||||
initialData={record.data}
|
||||
onSubmit={async (data) => {
|
||||
execUpdate({
|
||||
recordId: record.id,
|
||||
data,
|
||||
accountId,
|
||||
});
|
||||
}}
|
||||
isLoading={isBusy}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
interface FilterValue {
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function encodeFilters(filters: FilterValue[]): string {
|
||||
return filters
|
||||
.map((f) => `${f.field}:${f.operator}:${encodeURIComponent(f.value)}`)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
export function decodeFilters(raw: string | undefined | null): FilterValue[] {
|
||||
if (!raw) return [];
|
||||
return raw.split(',').map((segment) => {
|
||||
const [field, operator, ...rest] = segment.split(':');
|
||||
return {
|
||||
field: field ?? '',
|
||||
operator: operator ?? 'eq',
|
||||
value: decodeURIComponent(rest.join(':')),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||
|
||||
import { ModuleSearch } from '@kit/module-builder/components';
|
||||
|
||||
import { encodeFilters, decodeFilters } from './_lib/filter-params';
|
||||
|
||||
interface FieldOption {
|
||||
name: string;
|
||||
display_name: string;
|
||||
show_in_filter: boolean;
|
||||
show_in_search: boolean;
|
||||
}
|
||||
|
||||
interface ModuleSearchBarProps {
|
||||
fields: FieldOption[];
|
||||
}
|
||||
|
||||
export function ModuleSearchBar({ fields }: ModuleSearchBarProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const currentSearch = searchParams.get('q') ?? '';
|
||||
const currentFilters = decodeFilters(searchParams.get('f') ?? '');
|
||||
|
||||
const updateParams = useCallback(
|
||||
(updates: Record<string, string | null>) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
// Reset to page 1 when search/filters change
|
||||
params.delete('page');
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value === null || value === '') {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, value);
|
||||
}
|
||||
}
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
},
|
||||
[router, pathname, searchParams],
|
||||
);
|
||||
|
||||
return (
|
||||
<ModuleSearch
|
||||
fields={fields}
|
||||
initialSearch={currentSearch}
|
||||
initialFilters={currentFilters}
|
||||
onSearch={(search) => updateParams({ q: search || null })}
|
||||
onFilter={(filters) =>
|
||||
updateParams({ f: filters.length > 0 ? encodeFilters(filters) : null })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { createRecord } from '@kit/module-builder/actions/record-actions';
|
||||
import { ModuleForm } from '@kit/module-builder/components';
|
||||
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||
|
||||
type FieldDef = Parameters<typeof ModuleForm>[0]['fields'][number];
|
||||
|
||||
interface CreateRecordFormProps {
|
||||
fields: FieldDef[];
|
||||
moduleId: string;
|
||||
accountId: string;
|
||||
accountSlug: string;
|
||||
}
|
||||
|
||||
export function CreateRecordForm({
|
||||
fields,
|
||||
moduleId,
|
||||
accountId,
|
||||
accountSlug,
|
||||
}: CreateRecordFormProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { execute, isPending: isExecuting } = useActionWithToast(createRecord, {
|
||||
successMessage: 'Datensatz erstellt',
|
||||
errorMessage: 'Fehler beim Erstellen',
|
||||
onSuccess: () => {
|
||||
startTransition(() => {
|
||||
router.push(`/home/${accountSlug}/modules/${moduleId}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ModuleForm
|
||||
fields={fields}
|
||||
onSubmit={async (data) => {
|
||||
execute({ moduleId, accountId, data });
|
||||
}}
|
||||
isLoading={isExecuting || isPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import { createModuleBuilderApi } from '@kit/module-builder/api';
|
||||
import { ModuleForm } from '@kit/module-builder/components';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
import { CreateRecordForm } from './create-record-form';
|
||||
|
||||
interface NewRecordPageProps {
|
||||
params: Promise<{ account: string; moduleId: string }>;
|
||||
}
|
||||
@@ -13,6 +16,14 @@ export default async function NewRecordPage({ params }: NewRecordPageProps) {
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createModuleBuilderApi(client);
|
||||
|
||||
const { data: accountData } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
if (!accountData) return <AccountNotFound />;
|
||||
|
||||
const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
|
||||
if (!moduleWithFields) return <div>Modul nicht gefunden</div>;
|
||||
|
||||
@@ -41,10 +52,11 @@ export default async function NewRecordPage({ params }: NewRecordPageProps) {
|
||||
title={`${String(moduleWithFields.display_name)} — Neuer Datensatz`}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<ModuleForm
|
||||
<CreateRecordForm
|
||||
fields={fields as Parameters<typeof ModuleForm>[0]['fields']}
|
||||
onSubmit={async () => {}}
|
||||
isLoading={false}
|
||||
moduleId={moduleId}
|
||||
accountId={accountData.id}
|
||||
accountSlug={account}
|
||||
/>
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { createModuleBuilderApi } from '@kit/module-builder/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
|
||||
import { decodeFilters } from './_lib/filter-params';
|
||||
import { ModuleSearchBar } from './module-search-bar';
|
||||
|
||||
interface ModuleDetailPageProps {
|
||||
params: Promise<{ account: string; moduleId: string }>;
|
||||
@@ -25,6 +33,8 @@ export default async function ModuleDetailPage({
|
||||
const pageSize =
|
||||
Number(search.pageSize) || moduleWithFields.default_page_size || 25;
|
||||
|
||||
const filters = decodeFilters(search.f as string | undefined);
|
||||
|
||||
const result = await api.query.query({
|
||||
moduleId,
|
||||
page,
|
||||
@@ -38,9 +48,20 @@ export default async function ModuleDetailPage({
|
||||
(moduleWithFields.default_sort_direction as 'asc' | 'desc') ??
|
||||
'asc',
|
||||
search: (search.q as string) ?? undefined,
|
||||
filters: [],
|
||||
filters,
|
||||
});
|
||||
|
||||
const fields = (
|
||||
moduleWithFields as unknown as {
|
||||
fields: Array<{
|
||||
name: string;
|
||||
display_name: string;
|
||||
show_in_filter: boolean;
|
||||
show_in_search: boolean;
|
||||
}>;
|
||||
}
|
||||
).fields;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -54,14 +75,21 @@ export default async function ModuleDetailPage({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href={`/home/${account}/modules/${moduleId}/new`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Neuer Datensatz
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ModuleSearchBar fields={fields} />
|
||||
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{result.pagination.total} Datensätze — Seite {result.pagination.page}{' '}
|
||||
von {result.pagination.totalPages}
|
||||
</div>
|
||||
|
||||
{/* Phase 3 will replace this with module-table component */}
|
||||
<div className="rounded-lg border">
|
||||
<pre className="max-h-96 overflow-auto p-4 text-xs">
|
||||
{JSON.stringify(result.data, null, 2)}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trash2 } from 'lucide-react';
|
||||
|
||||
import { deleteModule } from '@kit/module-builder/actions/module-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 DeleteModuleButtonProps {
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
accountSlug: string;
|
||||
}
|
||||
|
||||
export function DeleteModuleButton({
|
||||
moduleId,
|
||||
moduleName,
|
||||
accountSlug,
|
||||
}: DeleteModuleButtonProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { execute, isPending: isDeleting } = useActionWithToast(deleteModule, {
|
||||
successMessage: `Modul "${moduleName}" wurde archiviert`,
|
||||
errorMessage: 'Fehler beim Löschen',
|
||||
onSuccess: () => {
|
||||
startTransition(() => {
|
||||
router.push(`/home/${accountSlug}/modules`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" disabled={isDeleting || isPending}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Modul archivieren
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Modul archivieren?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Das Modul "{moduleName}" wird archiviert und ist nicht
|
||||
mehr sichtbar. Diese Aktion kann durch einen Administrator
|
||||
rückgängig gemacht werden.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => execute({ moduleId })}>
|
||||
Archivieren
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { Label } from '@kit/ui/label';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
import { DeleteModuleButton } from './delete-module-button';
|
||||
|
||||
interface ModuleSettingsPageProps {
|
||||
params: Promise<{ account: string; moduleId: string }>;
|
||||
}
|
||||
@@ -178,6 +180,25 @@ export default async function ModuleSettingsPage({
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="border-destructive/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive flex items-center gap-2">
|
||||
Gefahrenbereich
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Das Modul wird archiviert und ist nicht mehr sichtbar.
|
||||
</p>
|
||||
<DeleteModuleButton
|
||||
moduleId={moduleId}
|
||||
moduleName={String(mod.display_name)}
|
||||
accountSlug={account}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user