frontend_nyla/src/pages/basedata/FilesPage.tsx
2026-02-09 23:45:05 +01:00

368 lines
11 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.

/**
* FilesPage
*
* Page for file management using FormGeneratorTable.
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
*/
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaFolder, FaUpload, FaDownload, FaEye } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import styles from '../admin/Admin.module.css';
interface UserFile {
id: string;
fileName: string;
mimeType?: string;
fileSize?: number;
[key: string]: any;
}
export const FilesPage: React.FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const { showSuccess, showError } = useToast();
// Data hook
const {
data: files,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchFileById,
updateFileOptimistically,
} = useUserFiles();
// Operations hook
const {
handleFileDownload,
handleFileDelete,
handleFileDeleteMultiple,
handleFileUpload,
handleFileUpdate,
handleFilePreview,
handleInlineUpdate,
deletingFiles,
downloadingFiles,
uploadingFile,
previewingFiles,
} = useFileOperations();
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
// Initial fetch
useEffect(() => {
refetch();
}, []);
// Generate columns from attributes - hide internal fields
const columns = useMemo(() => {
const hiddenColumns = ['id', 'mandateId', 'featureInstanceId', 'fileHash'];
const cols = (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.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,
}));
// Add _createdBy column with FK resolution to show username
cols.push({
key: '_createdBy',
label: 'Created By',
type: 'text' as any,
sortable: true,
filterable: false,
searchable: false,
width: 150,
minWidth: 100,
maxWidth: 250,
fkSource: '/api/users/',
fkDisplayField: 'username',
});
return cols;
}, [attributes]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (file: UserFile) => {
const fullFile = await fetchFileById(file.id);
if (fullFile) {
setEditingFile(fullFile as UserFile);
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<UserFile>) => {
if (!editingFile) return;
const result = await handleFileUpdate(editingFile.id, {
fileName: data.fileName || editingFile.fileName
}, editingFile);
if (result.success) {
setEditingFile(null);
refetch();
}
};
// Handle delete single file (confirmation handled by DeleteActionButton)
const handleDelete = async (file: UserFile) => {
const success = await handleFileDelete(file.id);
if (success) {
refetch();
}
};
// Handle delete multiple files (confirmation handled by FormGenerator)
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
const ids = filesToDelete.map(f => f.id);
const success = await handleFileDeleteMultiple(ids);
if (success) {
refetch();
}
};
// Handle download
const handleDownload = async (file: UserFile) => {
await handleFileDownload(file.id, file.fileName);
};
// Handle preview
const handlePreview = async (file: UserFile) => {
const result = await handleFilePreview(file.id, file.fileName, file.mimeType);
if (result.success && result.previewUrl) {
window.open(result.previewUrl, '_blank');
}
};
// Handle upload click
const handleUploadClick = () => {
fileInputRef.current?.click();
};
// Handle file selection
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files;
if (selectedFiles && selectedFiles.length > 0) {
let successCount = 0;
let errorCount = 0;
for (const file of Array.from(selectedFiles)) {
const result = await handleFileUpload(file);
if (result?.success) {
successCount++;
} else {
errorCount++;
}
}
// Reset input first
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
// Refresh table to show new files
await refetch();
// Show feedback
if (successCount > 0) {
showSuccess(
'Upload erfolgreich',
`${successCount} Datei(en) hochgeladen${errorCount > 0 ? `, ${errorCount} fehlgeschlagen` : ''}`
);
} else if (errorCount > 0) {
showError('Upload fehlgeschlagen', `${errorCount} Datei(en) konnten nicht hochgeladen werden`);
}
}
};
// Form attributes for edit modal
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'mandateId', 'fileHash', '_createdBy', '_createdAt', '_modifiedAt', 'creationDate', 'source'];
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 Dateien: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
multiple
style={{ display: 'none' }}
onChange={handleFileSelect}
/>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Dateien</h1>
<p className={styles.pageSubtitle}>Dateiverwaltung</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={handleUploadClick}
disabled={uploadingFile}
>
<FaUpload /> {uploadingFile ? 'Uploading...' : 'Datei hochladen'}
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
{loading && (!files || files.length === 0) ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Dateien...</span>
</div>
) : !files || files.length === 0 ? (
<div className={styles.emptyState}>
<FaFolder className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Dateien vorhanden</h3>
<p className={styles.emptyDescription}>
Laden Sie eine Datei hoch, um loszulegen.
</p>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleUploadClick}
disabled={uploadingFile}
>
<FaUpload /> Erste Datei hochladen
</button>
)}
</div>
) : (
<FormGeneratorTable
data={files}
columns={columns}
apiEndpoint="/api/files/list"
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: UserFile) => deletingFiles.has(row.id),
}] : []),
]}
customActions={[
{
id: 'download',
icon: <FaDownload />,
onClick: handleDownload,
title: 'Herunterladen',
loading: (row: UserFile) => downloadingFiles.has(row.id),
},
{
id: 'preview',
icon: <FaEye />,
onClick: handlePreview,
title: 'Vorschau',
loading: (row: UserFile) => previewingFiles.has(row.id),
},
]}
onDelete={handleDelete}
onDeleteMultiple={handleDeleteMultiple}
hookData={{
refetch,
permissions,
pagination,
handleDelete: handleFileDelete,
handleInlineUpdate,
updateOptimistically: updateFileOptimistically,
}}
emptyMessage="Keine Dateien gefunden"
/>
)}
</div>
{/* Edit Modal */}
{editingFile && (
<div className={styles.modalOverlay} onClick={() => setEditingFile(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Datei bearbeiten</h2>
<button
className={styles.modalClose}
onClick={() => setEditingFile(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={editingFile}
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingFile(null)}
submitButtonText="Speichern"
cancelButtonText="Abbrechen"
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default FilesPage;