ui-nyla/src/hooks/useMandates.ts

386 lines
12 KiB
TypeScript

/**
* useMandates Hook
*
* Hook für die Verwaltung von Mandanten (Mandates) im Admin-Bereich.
* Folgt dem gleichen Pattern wie useOrgUsers.
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useApiRequest } from './useApi';
import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions';
import {
fetchMandates as fetchMandatesApi,
fetchMandateById as fetchMandateByIdApi,
createMandate as createMandateApi,
updateMandate as updateMandateApi,
deleteMandate as deleteMandateApi,
hardDeleteMandate as hardDeleteMandateApi,
type Mandate,
type MandateCreateData,
type MandateUpdateData,
type PaginationParams
} from '../api/mandateApi';
import type { AttributeDefinition as FormGenAttr } from '../components/FormGenerator/FormGeneratorForm';
import { getMandateBillingFormAttributes } from '../utils/mandateBillingFormMerge';
import { validateMandateName } from '../utils/mandateNameUtils';
import { resolveColumnTypes } from '../utils/columnTypeResolver';
import type { ColumnConfig } from '../components/FormGenerator/FormGeneratorTable';
// Re-export types
export type { Mandate, MandateCreateData, MandateUpdateData, PaginationParams };
/** List-table columns only; invoice/address fields stay in the edit form. */
const MANDATE_LIST_COLUMN_KEYS = ['name', 'label', 'enabled', 'mfaRequired', 'isSystem'] as const;
const MANDATE_LIST_COLUMN_WIDTHS: Record<string, number> = {
name: 120,
label: 200,
enabled: 88,
mfaRequired: 100,
isSystem: 110,
};
export interface AttributeDefinition {
name: string;
type: string;
label: string;
description?: string;
required?: boolean;
default?: any;
options?: Array<{ value: string | number; label: string }> | string;
sortable?: boolean;
filterable?: boolean;
searchable?: boolean;
width?: number;
minWidth?: number;
maxWidth?: number;
readonly?: boolean;
editable?: boolean;
}
/**
* Hook for managing mandates in admin panel
*/
export function useAdminMandates() {
const [mandates, setMandates] = useState<Mandate[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Mandate[]>();
const { checkPermission } = usePermissions();
// Fetch attributes from backend
const fetchAttributes = useCallback(async () => {
try {
const response = await api.get('/api/attributes/Mandate');
let attrs: AttributeDefinition[] = [];
if (response.data?.attributes && Array.isArray(response.data.attributes)) {
attrs = response.data.attributes;
} else if (Array.isArray(response.data)) {
attrs = response.data;
} else if (response.data && typeof response.data === 'object') {
const keys = Object.keys(response.data);
for (const key of keys) {
if (Array.isArray(response.data[key])) {
attrs = response.data[key];
break;
}
}
}
setAttributes(attrs);
return attrs;
} catch (error: any) {
if (error.response?.status === 429) {
console.warn('Rate limit exceeded while fetching mandate attributes.');
} else if (error.response?.status !== 401) {
console.error('Error fetching mandate attributes:', error);
}
setAttributes([]);
return [];
}
}, []);
// Fetch permissions
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'Mandate');
setPermissions(perms);
return perms;
} catch (error: any) {
console.error('Error fetching mandate permissions:', error);
const defaultPerms: UserPermissions = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
setPermissions(defaultPerms);
return defaultPerms;
}
}, [checkPermission]);
// Fetch mandates
const fetchMandates = useCallback(async (params?: PaginationParams) => {
try {
const data = await fetchMandatesApi(request, params);
if (data && typeof data === 'object' && 'items' in data) {
const items = Array.isArray(data.items) ? data.items : [];
setMandates(items);
if (data.pagination) {
setPagination(data.pagination);
}
} else {
const items = Array.isArray(data) ? data : [];
setMandates(items);
setPagination(null);
}
} catch (error: any) {
setMandates([]);
setPagination(null);
}
}, [request]);
// Optimistic updates
const removeOptimistically = (mandateId: string) => {
setMandates(prev => prev.filter(m => m.id !== mandateId));
};
const updateOptimistically = (mandateId: string, updateData: Partial<Mandate>) => {
setMandates(prev =>
prev.map(m => m.id === mandateId ? { ...m, ...updateData } : m)
);
};
// Fetch single mandate
const fetchMandateById = useCallback(async (mandateId: string): Promise<Mandate | null> => {
return await fetchMandateByIdApi(request, mandateId);
}, [request]);
// Generate columns from attributes (types merged via resolveColumnTypes)
const columns: ColumnConfig[] = useMemo(() => {
const raw = MANDATE_LIST_COLUMN_KEYS.map((key) => {
const attr = attributes.find((a) => a.name === key);
if (!attr) return null;
return {
key: attr.name,
label: attr.label || attr.name,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: MANDATE_LIST_COLUMN_WIDTHS[attr.name] ?? attr.width ?? 120,
minWidth: attr.minWidth ?? 72,
maxWidth: attr.maxWidth ?? 240,
displayField: (attr as { displayField?: string }).displayField,
};
}).filter((c): c is NonNullable<typeof c> => c != null);
return resolveColumnTypes(raw, attributes);
}, [attributes]);
// Create mandate
const handleCreate = useCallback(async (mandateData: Partial<Mandate>): Promise<Mandate | null> => {
try {
const label = typeof mandateData.label === 'string' ? mandateData.label.trim() : '';
if (!label) {
console.error('createMandate: label (Voller Name) is required');
return null;
}
if (typeof mandateData.name === 'string' && mandateData.name.length > 0) {
const slugErr = validateMandateName(mandateData.name);
if (slugErr) {
console.error(`createMandate: invalid Kurzzeichen — ${slugErr}`);
return null;
}
}
const created = await createMandateApi(request, { ...mandateData, label } as MandateCreateData);
await fetchMandates();
return created ?? null;
} catch (error: any) {
console.error('Error creating mandate:', error);
return null;
}
}, [request, fetchMandates]);
// Update mandate
const handleUpdate = useCallback(async (mandateId: string, updateData: MandateUpdateData): Promise<boolean> => {
try {
if ('label' in updateData) {
const lbl = typeof updateData.label === 'string' ? updateData.label.trim() : '';
if (!lbl) {
console.error('updateMandate: label (Voller Name) must not be empty');
return false;
}
updateData = { ...updateData, label: lbl };
}
if ('name' in updateData && typeof updateData.name === 'string') {
const slugErr = validateMandateName(updateData.name);
if (slugErr) {
console.error(`updateMandate: invalid Kurzzeichen — ${slugErr}`);
return false;
}
}
updateOptimistically(mandateId, updateData);
await updateMandateApi(request, mandateId, updateData);
return true;
} catch (error: any) {
console.error('Error updating mandate:', error);
await fetchMandates();
return false;
}
}, [request, fetchMandates]);
// Delete mandate
const handleDelete = useCallback(async (mandateId: string): Promise<boolean> => {
try {
removeOptimistically(mandateId);
await deleteMandateApi(request, mandateId);
return true;
} catch (error: any) {
console.error('Error deleting mandate:', error);
await fetchMandates();
return false;
}
}, [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,
updateData: Partial<Mandate>
): Promise<void> => {
await handleUpdate(mandateId, updateData);
}, [handleUpdate]);
// Load data on mount
useEffect(() => {
fetchAttributes();
fetchPermissions();
fetchMandates();
}, []);
return {
mandates,
attributes,
columns,
permissions,
pagination,
loading,
error,
refetch: fetchMandates,
fetchMandateById,
handleCreate,
handleUpdate,
handleDelete,
handleHardDelete,
handleInlineUpdate,
updateOptimistically,
};
}
export default useAdminMandates;
/**
* Mandate model attributes for FormGenerator (create/edit) — shared by Admin page and wizard.
*/
export function useMandateFormAttributes() {
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await api.get('/api/attributes/Mandate');
let attrs: AttributeDefinition[] = [];
const data = response.data;
if (data?.attributes && Array.isArray(data.attributes)) {
attrs = data.attributes;
} else if (Array.isArray(data)) {
attrs = data;
} else if (data && typeof data === 'object') {
for (const key of Object.keys(data)) {
if (Array.isArray((data as Record<string, unknown>)[key])) {
attrs = (data as Record<string, AttributeDefinition[]>)[key];
break;
}
}
}
setAttributes(attrs);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Attribute-Laden fehlgeschlagen';
setError(msg);
setAttributes([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const formAttributes: FormGenAttr[] = useMemo(() => {
const list = attributes
.filter(attr => attr.name !== 'id')
.map(attr => ({ ...attr, type: attr.type })) as FormGenAttr[];
const labelIdx = list.findIndex(a => a.name === 'label');
const nameIdx = list.findIndex(a => a.name === 'name');
if (labelIdx >= 0 && nameIdx >= 0 && nameIdx < labelIdx) {
const [labelAttr] = list.splice(labelIdx, 1);
list.splice(nameIdx, 0, labelAttr);
}
return list;
}, [attributes]);
const createFormAttributes: FormGenAttr[] = useMemo(
() => formAttributes.filter(attr => attr.name !== 'isSystem'),
[formAttributes]
);
const billingFormAttributes: FormGenAttr[] = useMemo(() => getMandateBillingFormAttributes(), []);
/** Mandate attributes + billing (Abrechnung) for SysAdmin create flows */
const createFormAttributesWithBilling: FormGenAttr[] = useMemo(
() => [...createFormAttributes, ...billingFormAttributes],
[createFormAttributes, billingFormAttributes]
);
/** Mandate attributes + billing for SysAdmin edit flows */
const formAttributesWithBilling: FormGenAttr[] = useMemo(
() => [...formAttributes, ...billingFormAttributes],
[formAttributes, billingFormAttributes]
);
return {
formAttributes,
createFormAttributes,
formAttributesWithBilling,
createFormAttributesWithBilling,
loading,
error,
refetch: load,
};
}