ui-nyla/src/pages/views/trustee/TrusteePositionDocumentsView.tsx
2026-01-29 10:14:33 +01:00

312 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 {
items: links,
attributes,
permissions,
pagination,
loading,
error,
refetch,
removeOptimistically,
} = useTrusteePositionDocuments();
// Operations hook
const {
handleDelete,
handleCreate,
handleUpdate,
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 => !excludedFields.includes(attr.name))
.map(attr => {
// 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
const handleEditSubmit = async (data: Partial<TrusteePositionDocument>) => {
if (!editingLink) return;
const result = await handleUpdate(editingLink.id, data);
if (result.success) {
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 => !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}>
<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}>
{loading && (!links || links.length === 0) ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Verknüpfungen...</span>
</div>
) : !links || links.length === 0 ? (
<div className={styles.emptyState}>
<FaLink className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Verknüpfungen vorhanden</h3>
<p className={styles.emptyDescription}>
Verknüpfen Sie Belege mit Buchungspositionen.
</p>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ Neue Verknüpfung
</button>
)}
</div>
) : (
<FormGeneratorTable
data={links}
columns={columns}
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;