285 lines
9.5 KiB
TypeScript
285 lines
9.5 KiB
TypeScript
/**
|
||
* TrusteePositionDocumentsView
|
||
*
|
||
* Verknüpfungs-Verwaltung zwischen Positionen und Dokumenten.
|
||
* Verwendet FormGeneratorTable mit Spalten aus den Pydantic-Attributen.
|
||
*/
|
||
|
||
import React, { useState, useMemo, useEffect } from 'react';
|
||
import { useTrusteePositionDocuments, useTrusteePositionDocumentOperations, TrusteePositionDocument } from '../../../hooks/useTrustee';
|
||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
|
||
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
|
||
import { FaSync, FaLink } from 'react-icons/fa';
|
||
import styles from '../../admin/Admin.module.css';
|
||
|
||
export const TrusteePositionDocumentsView: React.FC = () => {
|
||
const instanceId = useInstanceId();
|
||
|
||
// Entity hook
|
||
const {
|
||
positionDocuments: links,
|
||
attributes,
|
||
permissions,
|
||
pagination,
|
||
loading,
|
||
error,
|
||
refetch,
|
||
removeOptimistically,
|
||
} = useTrusteePositionDocuments();
|
||
|
||
// Operations hook
|
||
const {
|
||
handlePositionDocumentDelete: handleDelete,
|
||
handlePositionDocumentCreate: handleCreate,
|
||
deletingPositionDocuments: deletingItems,
|
||
} = useTrusteePositionDocumentOperations();
|
||
|
||
// Modal state
|
||
const [isCreateMode, setIsCreateMode] = useState(false);
|
||
const [editingLink, setEditingLink] = useState<TrusteePositionDocument | null>(null);
|
||
|
||
// Initial fetch
|
||
useEffect(() => {
|
||
if (instanceId) {
|
||
refetch();
|
||
}
|
||
}, [instanceId]);
|
||
|
||
// Generate columns from attributes (like TrusteePositionsView)
|
||
// Map frontend_options to fkSource for FK resolution
|
||
const columns = useMemo(() => {
|
||
if (!attributes || attributes.length === 0) return [];
|
||
|
||
// Exclude system fields from table columns
|
||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', '_createdBy', '_createdAt', '_modifiedAt', '_modifiedBy'];
|
||
|
||
return attributes
|
||
.filter((attr: any) => !excludedFields.includes(attr.name))
|
||
.map((attr: any) => {
|
||
// Replace {instanceId} placeholder in options URL
|
||
let fkSource = attr.options;
|
||
if (typeof fkSource === 'string' && instanceId) {
|
||
fkSource = fkSource.replace('{instanceId}', instanceId);
|
||
}
|
||
|
||
return {
|
||
key: attr.name,
|
||
label: attr.label || attr.name,
|
||
type: attr.type as any,
|
||
sortable: attr.sortable !== false,
|
||
filterable: attr.filterable !== false,
|
||
searchable: attr.searchable !== false,
|
||
width: attr.width || 200,
|
||
minWidth: attr.minWidth || 100,
|
||
maxWidth: attr.maxWidth || 400,
|
||
// Use frontend_options as fkSource for FK resolution
|
||
fkSource: typeof fkSource === 'string' ? fkSource : undefined,
|
||
fkDisplayField: 'label',
|
||
};
|
||
});
|
||
}, [attributes, instanceId]);
|
||
|
||
// Check permissions (general level)
|
||
// Row-level permissions are handled automatically by FormGeneratorTable
|
||
const canCreate = permissions?.create !== 'n';
|
||
const canUpdate = permissions?.update !== 'n';
|
||
const canDelete = permissions?.delete !== 'n';
|
||
|
||
// Handle create click
|
||
const handleCreateClick = () => {
|
||
setIsCreateMode(true);
|
||
};
|
||
|
||
// Handle edit click
|
||
const handleEditClick = (link: TrusteePositionDocument) => {
|
||
setEditingLink(link);
|
||
};
|
||
|
||
// Handle create form submit
|
||
const handleFormSubmit = async (data: Partial<TrusteePositionDocument>) => {
|
||
const result = await handleCreate(data);
|
||
if (result.success) {
|
||
setIsCreateMode(false);
|
||
refetch();
|
||
}
|
||
};
|
||
|
||
// Handle edit form submit (position-document links are typically deleted + re-created, not updated)
|
||
const handleEditSubmit = async (_data: Partial<TrusteePositionDocument>) => {
|
||
setEditingLink(null);
|
||
refetch();
|
||
};
|
||
|
||
// Handle delete (confirmation handled by DeleteActionButton)
|
||
const handleDeleteLink = async (link: TrusteePositionDocument) => {
|
||
removeOptimistically(link.id);
|
||
const success = await handleDelete(link.id);
|
||
if (!success) {
|
||
refetch(); // Revert on error
|
||
}
|
||
};
|
||
|
||
// Close modal
|
||
const handleCloseModal = () => {
|
||
setIsCreateMode(false);
|
||
};
|
||
|
||
// Form attributes (exclude system fields)
|
||
const formAttributes = useMemo(() => {
|
||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', '_createdBy', '_createdAt', '_modifiedAt', '_modifiedBy'];
|
||
return (attributes || []).filter((attr: any) => !excludedFields.includes(attr.name));
|
||
}, [attributes]);
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler beim Laden der Verknüpfungen: {error}</p>
|
||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||
<FaSync /> Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<p className={styles.pageSubtitle}>Belege mit Buchungspositionen verknüpfen</p>
|
||
</div>
|
||
<div className={styles.headerActions}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => refetch()}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||
</button>
|
||
{canCreate && (
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={handleCreateClick}
|
||
>
|
||
+ Neue Verknüpfung
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles.tableContainer}>
|
||
<FormGeneratorTable
|
||
data={links}
|
||
columns={columns}
|
||
apiEndpoint={instanceId ? `/api/trustee/${instanceId}/position-documents` : undefined}
|
||
loading={loading}
|
||
pagination={true}
|
||
pageSize={25}
|
||
searchable={true}
|
||
filterable={true}
|
||
sortable={true}
|
||
selectable={false}
|
||
actionButtons={[
|
||
...(canUpdate ? [{
|
||
type: 'edit' as const,
|
||
title: 'Verknüpfung bearbeiten',
|
||
onAction: handleEditClick,
|
||
// Row-level permissions handled automatically by FormGeneratorTable
|
||
}] : []),
|
||
...(canDelete ? [{
|
||
type: 'delete' as const,
|
||
title: 'Verknüpfung entfernen',
|
||
loading: (row: TrusteePositionDocument) => deletingItems.has(row.id),
|
||
// Row-level permissions handled automatically by FormGeneratorTable
|
||
}] : []),
|
||
]}
|
||
onDelete={handleDeleteLink}
|
||
hookData={{
|
||
refetch,
|
||
permissions,
|
||
pagination,
|
||
handleDelete,
|
||
}}
|
||
emptyMessage="Keine Verknüpfungen gefunden"
|
||
/>
|
||
</div>
|
||
|
||
{/* Create Modal */}
|
||
{isCreateMode && (
|
||
<div className={styles.modalOverlay} onClick={handleCloseModal}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>Neue Verknüpfung erstellen</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={handleCloseModal}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
{formAttributes.length === 0 ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Formular...</span>
|
||
</div>
|
||
) : (
|
||
<FormGeneratorForm
|
||
attributes={formAttributes}
|
||
data={{}}
|
||
mode="create"
|
||
onSubmit={handleFormSubmit}
|
||
onCancel={handleCloseModal}
|
||
submitButtonText="Verknüpfung erstellen"
|
||
cancelButtonText="Abbrechen"
|
||
instanceId={instanceId}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Modal */}
|
||
{editingLink && (
|
||
<div className={styles.modalOverlay} onClick={() => setEditingLink(null)}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>Verknüpfung bearbeiten</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={() => setEditingLink(null)}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
{formAttributes.length === 0 ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Formular...</span>
|
||
</div>
|
||
) : (
|
||
<FormGeneratorForm
|
||
attributes={formAttributes}
|
||
data={editingLink}
|
||
mode="edit"
|
||
onSubmit={handleEditSubmit}
|
||
onCancel={() => setEditingLink(null)}
|
||
submitButtonText="Speichern"
|
||
cancelButtonText="Abbrechen"
|
||
instanceId={instanceId}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default TrusteePositionDocumentsView;
|