frontend_nyla/src/pages/views/trustee/TrusteePositionDocumentsView.tsx
ValueOn AG 6a406d885d fixes
2026-01-24 18:01:35 +01:00

234 lines
7.1 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 für konsistentes UI.
*/
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,
deletingItems,
creatingItem,
} = useTrusteePositionDocumentOperations();
// Modal state
const [isCreateMode, setIsCreateMode] = useState(false);
// Initial fetch
useEffect(() => {
if (instanceId) {
refetch();
}
}, [instanceId]);
// Generate columns from attributes
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
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 || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
}, [attributes]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle create click
const handleCreateClick = () => {
setIsCreateMode(true);
};
// Handle form submit
const handleFormSubmit = async (data: Partial<TrusteePositionDocument>) => {
const result = await handleCreate(data);
if (result.success) {
setIsCreateMode(false);
refetch();
}
};
// Handle delete
const handleDeleteLink = async (link: TrusteePositionDocument) => {
if (window.confirm('Verknüpfung wirklich entfernen?')) {
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', 'instanceId', '_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={[
...(canDelete ? [{
type: 'delete' as const,
title: 'Verknüpfung entfernen',
loading: (row: TrusteePositionDocument) => deletingItems.has(row.id),
}] : []),
]}
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>
)}
</div>
);
};
export default TrusteePositionDocumentsView;