frontend_nyla/src/pages/views/trustee/TrusteeDocumentsView.tsx
2026-04-14 00:15:51 +02:00

297 lines
9.3 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, 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';
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]);
// 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(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} onClick={handleCloseModal}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<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;