frontend_nyla/src/pages/views/trustee/TrusteeDocumentsView.tsx

292 lines
9 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.

/**
* TrusteeDocumentsView
*
* Dokument-Verwaltung für eine Trustee-Instanz.
* Verwendet FormGeneratorTable für konsistentes UI.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useTrusteeDocuments, useTrusteeDocumentOperations, TrusteeDocument } from '../../../hooks/useTrustee';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaFileAlt, FaDownload } from 'react-icons/fa';
import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api';
import styles from '../../admin/Admin.module.css';
export const TrusteeDocumentsView: React.FC = () => {
const { showError } = useToast();
const instanceId = useInstanceId();
// Entity hook
const {
items: documents,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchById,
updateOptimistically,
removeOptimistically,
} = useTrusteeDocuments();
// Operations hook
const {
handleDelete,
handleCreate,
handleUpdate,
deletingItems,
} = useTrusteeDocumentOperations();
// Modal state
const [editingDocument, setEditingDocument] = useState<TrusteeDocument | null>(null);
const [isCreateMode, setIsCreateMode] = useState(false);
const [downloadingId, setDownloadingId] = useState<string | null>(null);
// 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 canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (doc: TrusteeDocument) => {
const fullDoc = await fetchById(doc.id);
if (fullDoc) {
setEditingDocument(fullDoc);
setIsCreateMode(false);
}
};
// Handle create click
const handleCreateClick = () => {
setEditingDocument(null);
setIsCreateMode(true);
};
// Handle form submit
const handleFormSubmit = async (data: Partial<TrusteeDocument>) => {
if (isCreateMode) {
const result = await handleCreate(data);
if (result.success) {
setIsCreateMode(false);
refetch();
}
} else if (editingDocument) {
const result = await handleUpdate(editingDocument.id, data);
if (result.success) {
setEditingDocument(null);
refetch();
}
}
};
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteDoc = async (doc: TrusteeDocument) => {
removeOptimistically(doc.id);
const success = await handleDelete(doc.id);
if (!success) {
refetch(); // Revert on error
}
};
// Handle download
const handleDownload = async (doc: TrusteeDocument) => {
if (!instanceId) return;
setDownloadingId(doc.id);
try {
const response = await api.get(
`/api/trustee/${instanceId}/documents/${doc.id}/data`,
{ responseType: 'blob' }
);
const blob = new Blob([response.data], { type: doc.documentMimeType });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = doc.documentName || 'document';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Download error:', err);
showError('Fehler', 'Fehler beim Herunterladen des Dokuments.');
} finally {
setDownloadingId(null);
}
};
// Close modal
const handleCloseModal = () => {
setEditingDocument(null);
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]);
// Handle inline update
const handleInlineUpdate = async (itemId: string, updateData: Partial<TrusteeDocument>, row: TrusteeDocument) => {
updateOptimistically(itemId, updateData);
const result = await handleUpdate(itemId, { ...row, ...updateData });
if (!result.success) {
refetch(); // Revert on error
}
};
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Dokumente: {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 und Dokumente verwalten</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}
>
+ Neues Dokument
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
<FormGeneratorTable
data={documents}
columns={columns}
apiEndpoint={instanceId ? `/api/trustee/${instanceId}/documents` : undefined}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={true}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
loading: (row: TrusteeDocument) => deletingItems.has(row.id),
}] : []),
]}
customActions={[
{
id: 'download',
icon: <FaDownload />,
onClick: handleDownload,
title: 'Herunterladen',
loading: (row: TrusteeDocument) => downloadingId === row.id,
},
]}
onDelete={handleDeleteDoc}
hookData={{
refetch,
permissions,
pagination,
handleDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Dokumente gefunden"
/>
</div>
{/* Create/Edit Modal */}
{(editingDocument || isCreateMode) && (
<div className={styles.modalOverlay} onClick={handleCloseModal}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{isCreateMode ? 'Neues Dokument' : 'Dokument bearbeiten'}
</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={editingDocument || {}}
mode={isCreateMode ? 'create' : 'edit'}
onSubmit={handleFormSubmit}
onCancel={handleCloseModal}
submitButtonText={isCreateMode ? 'Erstellen' : 'Speichern'}
cancelButtonText="Abbrechen"
instanceId={instanceId}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default TrusteeDocumentsView;