frontend_nyla/src/pages/views/trustee/TrusteeDocumentsView.tsx
2026-04-26 18:11:52 +02:00

316 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.

/**
* TrusteeDocumentsView
*
* Dokument-Verwaltung für eine Trustee-Instanz.
* Verwendet FormGeneratorTable für konsistentes UI.
*
* NOTE: Mounted only as a tab inside `TrusteeDataTablesView` (Tab `documents`
* unter `/mandates/{m}/trustee/{i}/data-tables?tab=documents`). Es gibt keine
* eigenständige Top-Level-Route mehr (`/trustee/{i}/documents` wurde entfernt
* -- siehe `wiki/c-work/4-done/2026-04-trustee-cleanup-positions-documents.md`).
* Direkt-Import durch `TrusteeDataTablesView`; kein Re-Export über
* `views/trustee/index.ts`.
*/
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, FaDownload } from 'react-icons/fa';
import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api';
import styles from '../../admin/Admin.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { resolveColumnTypes } from '../../../utils/columnTypeResolver';
export const TrusteeDocumentsView: React.FC = () => {
const { t } = useLanguage();
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]);
const documentColumnOrder = ['sysCreatedAt'];
const columns = useMemo(() => {
const allCols = (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
displayField: attr.displayField,
}));
const byKey = new Map(allCols.map(c => [c.key, c]));
const ordered: typeof allCols = [];
for (const key of documentColumnOrder) {
const col = byKey.get(key);
if (col) { ordered.push(col); byKey.delete(key); }
}
for (const col of allCols) {
if (byKey.has(col.key)) ordered.push(col);
}
return resolveColumnTypes(ordered, attributes || []);
}, [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(t('Fehler'), t('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', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'sysModifiedBy'];
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}>{t('Fehler beim Laden der Dokumente: {detail}', { detail: String(error) })}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<p className={styles.pageSubtitle}>{t('Belege und Dokumente verwalten')}</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ {t('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}
initialSort={[{ key: 'sysCreatedAt', direction: 'desc' }]}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('Löschen'),
loading: (row: TrusteeDocument) => deletingItems.has(row.id),
}] : []),
]}
customActions={[
{
id: 'download',
icon: <FaDownload />,
onClick: handleDownload,
title: t('Herunterladen'),
loading: (row: TrusteeDocument) => downloadingId === row.id,
},
]}
onDelete={handleDeleteDoc}
hookData={{
refetch,
permissions,
pagination,
handleDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('Keine Dokumente gefunden')}
/>
</div>
{/* Create/Edit Modal */}
{(editingDocument || isCreateMode) && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{isCreateMode ? t('Neues Dokument') : t('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>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
data={editingDocument || {}}
mode={isCreateMode ? 'create' : 'edit'}
onSubmit={handleFormSubmit}
onCancel={handleCloseModal}
submitButtonText={isCreateMode ? t('Erstellen') : t('Speichern')}
cancelButtonText={t('Abbrechen')}
instanceId={instanceId}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default TrusteeDocumentsView;