368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
/**
|
||
* 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;
|