From 8fcad7de45cb1f1d7b105f5cdfa4559b0670c0db Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 31 Mar 2026 21:53:14 +0200 Subject: [PATCH] add hard delete for mandates (SysAdmin only) Made-with: Cursor --- src/api/mandateApi.ts | 21 +++++++++++++- src/hooks/useMandates.ts | 15 ++++++++++ src/pages/admin/AdminMandatesPage.tsx | 40 +++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/api/mandateApi.ts b/src/api/mandateApi.ts index b29d138..9f4076d 100644 --- a/src/api/mandateApi.ts +++ b/src/api/mandateApi.ts @@ -122,7 +122,7 @@ export async function createMandate( } /** - * Delete a mandate + * Soft-delete a mandate (sets enabled=false, 30-day retention) * Endpoint: DELETE /api/mandates/{mandateId} */ export async function deleteMandate( @@ -134,3 +134,22 @@ export async function deleteMandate( method: 'delete' }); } + +/** + * Hard-delete a mandate with full cascade (irreversible) + * Endpoint: DELETE /api/mandates/{mandateId}?force=true + */ +export async function hardDeleteMandate( + request: ApiRequestFunction, + mandateId: string, + confirmName: string +): Promise { + await request({ + url: `/api/mandates/${mandateId}`, + method: 'delete', + params: { force: true }, + additionalConfig: { + headers: { 'X-Confirm-Name': confirmName } + } + }); +} diff --git a/src/hooks/useMandates.ts b/src/hooks/useMandates.ts index a276200..f57c299 100644 --- a/src/hooks/useMandates.ts +++ b/src/hooks/useMandates.ts @@ -15,6 +15,7 @@ import { createMandate as createMandateApi, updateMandate as updateMandateApi, deleteMandate as deleteMandateApi, + hardDeleteMandate as hardDeleteMandateApi, type Mandate, type MandateUpdateData, type PaginationParams @@ -203,6 +204,19 @@ export function useAdminMandates() { } }, [request, fetchMandates]); + // Hard-delete mandate (irreversible) + const handleHardDelete = useCallback(async (mandateId: string, confirmName: string): Promise => { + try { + removeOptimistically(mandateId); + await hardDeleteMandateApi(request, mandateId, confirmName); + return true; + } catch (error: any) { + console.error('Error hard-deleting mandate:', error); + await fetchMandates(); + return false; + } + }, [request, fetchMandates]); + // Inline update const handleInlineUpdate = useCallback(async ( mandateId: string, @@ -231,6 +245,7 @@ export function useAdminMandates() { handleCreate, handleUpdate, handleDelete, + handleHardDelete, handleInlineUpdate, updateOptimistically, }; diff --git a/src/pages/admin/AdminMandatesPage.tsx b/src/pages/admin/AdminMandatesPage.tsx index cac150a..0ba5b04 100644 --- a/src/pages/admin/AdminMandatesPage.tsx +++ b/src/pages/admin/AdminMandatesPage.tsx @@ -17,7 +17,7 @@ import { useToast } from '../../contexts/ToastContext'; import { usePrompt } from '../../hooks/usePrompt'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; -import { FaPlus, FaSync, FaBuilding, FaUsers, FaLock } from 'react-icons/fa'; +import { FaPlus, FaSync, FaBuilding, FaUsers, FaLock, FaSkullCrossbones } from 'react-icons/fa'; import styles from './Admin.module.css'; export const AdminMandatesPage: React.FC = () => { @@ -37,6 +37,7 @@ export const AdminMandatesPage: React.FC = () => { handleCreate, handleUpdate, handleDelete, + handleHardDelete, handleInlineUpdate, updateOptimistically, } = useAdminMandates(); @@ -118,17 +119,37 @@ export const AdminMandatesPage: React.FC = () => { return; } const entered = await prompt( - `Um den Mandanten "${mandate.name}" unwiderruflich zu löschen, geben Sie den Namen ein:`, - { title: 'Mandant löschen', confirmLabel: 'Löschen', variant: 'danger', placeholder: mandate.name }, + `Um den Mandanten "${mandate.name}" zu deaktivieren (Soft-Delete), geben Sie den Namen ein:`, + { title: 'Mandant deaktivieren', confirmLabel: 'Deaktivieren', variant: 'danger', placeholder: mandate.name }, ); if (entered === null) return; if (entered !== mandate.name) { - showWarning('Löschung abgebrochen', 'Der eingegebene Name stimmt nicht überein.'); + showWarning('Abgebrochen', 'Der eingegebene Name stimmt nicht überein.'); return; } await handleDelete(mandate.id); }; + const handleHardDeleteMandate = async (mandate: Mandate) => { + if (mandate.isSystem) { + showWarning('Nicht erlaubt', 'System-Mandanten können nicht gelöscht werden.'); + return; + } + const entered = await prompt( + `ACHTUNG: Dies löscht den Mandanten "${mandate.name}" unwiderruflich inkl. aller Subscriptions, Features, Benutzer-Zuweisungen und Daten. Geben Sie den exakten Namen ein:`, + { title: 'Hard Delete (irreversibel)', confirmLabel: 'Endgültig löschen', variant: 'danger', placeholder: mandate.name }, + ); + if (entered === null) return; + if (entered !== mandate.name) { + showWarning('Abgebrochen', 'Der eingegebene Name stimmt nicht überein.'); + return; + } + const ok = await handleHardDelete(mandate.id, entered); + if (ok) { + showSuccess('Gelöscht', `Mandant "${mandate.name}" wurde endgültig gelöscht.`); + } + }; + if (error) { return (
@@ -218,12 +239,21 @@ export const AdminMandatesPage: React.FC = () => { }] : []), ...(canDelete ? [{ type: 'delete' as const, - title: 'Löschen', + title: 'Deaktivieren (Soft-Delete)', disabled: (row: Mandate) => row.isSystem ? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' } : false }] : []), ]} + customActions={canDelete ? [{ + id: 'hard-delete', + icon: , + onClick: handleHardDeleteMandate, + title: 'Hard Delete (irreversibel)', + disabled: (row: Mandate) => row.isSystem + ? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' } + : false, + }] : []} onDelete={handleDeleteMandate} hookData={{ refetch,