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} * Endpoint: DELETE /api/mandates/{mandateId}
*/ */
export async function deleteMandate( export async function deleteMandate(
@ -134,3 +134,22 @@ export async function deleteMandate(
method: 'delete' 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, createMandate as createMandateApi,
updateMandate as updateMandateApi, updateMandate as updateMandateApi,
deleteMandate as deleteMandateApi, deleteMandate as deleteMandateApi,
hardDeleteMandate as hardDeleteMandateApi,
type Mandate, type Mandate,
type MandateUpdateData, type MandateUpdateData,
type PaginationParams type PaginationParams
@ -203,6 +204,19 @@ export function useAdminMandates() {
} }
}, [request, fetchMandates]); }, [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 // Inline update
const handleInlineUpdate = useCallback(async ( const handleInlineUpdate = useCallback(async (
mandateId: string, mandateId: string,
@ -231,6 +245,7 @@ export function useAdminMandates() {
handleCreate, handleCreate,
handleUpdate, handleUpdate,
handleDelete, handleDelete,
handleHardDelete,
handleInlineUpdate, handleInlineUpdate,
updateOptimistically, updateOptimistically,
}; };

View file

@ -17,7 +17,7 @@ import { useToast } from '../../contexts/ToastContext';
import { usePrompt } from '../../hooks/usePrompt'; import { usePrompt } from '../../hooks/usePrompt';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; 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'; import styles from './Admin.module.css';
export const AdminMandatesPage: React.FC = () => { export const AdminMandatesPage: React.FC = () => {
@ -37,6 +37,7 @@ export const AdminMandatesPage: React.FC = () => {
handleCreate, handleCreate,
handleUpdate, handleUpdate,
handleDelete, handleDelete,
handleHardDelete,
handleInlineUpdate, handleInlineUpdate,
updateOptimistically, updateOptimistically,
} = useAdminMandates(); } = useAdminMandates();
@ -118,17 +119,37 @@ export const AdminMandatesPage: React.FC = () => {
return; return;
} }
const entered = await prompt( const entered = await prompt(
`Um den Mandanten "${mandate.name}" unwiderruflich zu löschen, geben Sie den Namen ein:`, `Um den Mandanten "${mandate.name}" zu deaktivieren (Soft-Delete), geben Sie den Namen ein:`,
{ title: 'Mandant löschen', confirmLabel: 'Löschen', variant: 'danger', placeholder: mandate.name }, { title: 'Mandant deaktivieren', confirmLabel: 'Deaktivieren', variant: 'danger', placeholder: mandate.name },
); );
if (entered === null) return; if (entered === null) return;
if (entered !== mandate.name) { if (entered !== mandate.name) {
showWarning('Löschung abgebrochen', 'Der eingegebene Name stimmt nicht überein.'); showWarning('Abgebrochen', 'Der eingegebene Name stimmt nicht überein.');
return; return;
} }
await handleDelete(mandate.id); 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) { if (error) {
return ( return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}> <div className={`${styles.adminPage} ${styles.adminPageFill}`}>
@ -218,12 +239,21 @@ export const AdminMandatesPage: React.FC = () => {
}] : []), }] : []),
...(canDelete ? [{ ...(canDelete ? [{
type: 'delete' as const, type: 'delete' as const,
title: 'Löschen', title: 'Deaktivieren (Soft-Delete)',
disabled: (row: Mandate) => row.isSystem disabled: (row: Mandate) => row.isSystem
? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' } ? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' }
: false : 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} onDelete={handleDeleteMandate}
hookData={{ hookData={{
refetch, refetch,