add hard delete for mandates (SysAdmin only)

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-03-31 21:53:14 +02:00
parent ca019ae28d
commit 8fcad7de45
3 changed files with 70 additions and 6 deletions

View file

@ -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<void> {
await request({
url: `/api/mandates/${mandateId}`,
method: 'delete',
params: { force: true },
additionalConfig: {
headers: { 'X-Confirm-Name': confirmName }
}
});
}

View file

@ -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<boolean> => {
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,
};

View file

@ -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 (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
@ -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: <FaSkullCrossbones style={{ color: 'var(--error-color, #e53e3e)' }} />,
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,