worked on Dateien page

This commit is contained in:
Ida Dittrich 2025-08-08 13:00:01 +02:00
parent fa1cb5be2e
commit 7cbdb0aea5
41 changed files with 1679 additions and 2258 deletions

BIN
src/assets/styles/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Popup, EditForm } from '../Popup';
import styles from './ConnectionEditModal.module.css';
import { ConnectionEditModalProps } from './interfaces';
import { ConnectionEditModalProps } from './connectionsInterfaces';
import { useLanguage } from '../../contexts/LanguageContext';
export function ConnectionEditModal({

View file

@ -1,6 +1,6 @@
import React from 'react';
import styles from './ConnectionsErrorDisplay.module.css';
import { ConnectionsErrorDisplayProps } from './interfaces';
import { ConnectionsErrorDisplayProps } from './connectionsInterfaces';
import { useLanguage } from '../../contexts/LanguageContext';
export function ConnectionsErrorDisplay({

View file

@ -1,7 +1,7 @@
import React from 'react';
import { FormGenerator } from '../FormGenerator';
import styles from './ConnectionsTable.module.css';
import { ConnectionsTableProps } from './interfaces';
import { ConnectionsTableProps } from './connectionsInterfaces';
import { useLanguage } from '../../contexts/LanguageContext';
export function ConnectionsTable({

View file

@ -13,7 +13,7 @@ import {
CreateConnectionData,
ConnectionsLogicReturn,
TableAction
} from './interfaces';
} from './connectionsInterfaces';
export function useConnectionsLogic(): ConnectionsLogicReturn {
const { t } = useLanguage();

View file

@ -4,7 +4,7 @@ export { ConnectionEditModal } from './ConnectionEditModal';
export { ConnectionsErrorDisplay } from './ConnectionsErrorDisplay';
// Export logic hook
export { useConnectionsLogic } from './logic';
export { useConnectionsLogic } from './connectionsLogic';
// Export all interfaces and types
export * from './interfaces';
export * from './connectionsInterfaces';

View file

@ -1,144 +0,0 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at' | 'source';
type SortDirection = 'asc' | 'desc';
interface DateienAllProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienAll: React.FC<DateienAllProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const { t } = useLanguage();
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort all files
const sortedFiles = [...files].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
case 'source':
result = a.source?.localeCompare(b.source ?? '') ?? 0;
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>{t('files.no_files', 'No files found.')}</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>{t('files.header.name', 'Name')}</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>{t('files.header.type', 'Type')}</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>{t('files.header.size', 'Size')}</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>{t('files.header.date', 'Date')}</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List */}
<motion.ul
className={styles.filesList}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{
opacity: 0,
scale: 0.95,
x: -50,
transition: { duration: 0.2, ease: "easeIn" }
}}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienAll;

View file

@ -1,139 +0,0 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
interface DateienCreatedProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienCreated: React.FC<DateienCreatedProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const { t } = useLanguage();
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Filter files for created (agent_created)
const createdFiles = files.filter(file => file.source === 'agent_created');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort files
const sortedFiles = [...createdFiles].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>{t('files.no_ai_files', 'No AI-created files found.')}</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>{t('files.header.name', 'Name')}</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>{t('files.header.type', 'Type')}</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>{t('files.header.size', 'Size')}</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>{t('files.header.date', 'Date')}</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List with AI-created indicator */}
<motion.ul
className={`${styles.filesList} ${styles.aiCreated}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienCreated;

View file

@ -1,349 +0,0 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.modal {
background: var(--color-bg);
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 90vw;
max-height: 90vh;
width: 900px;
height: 700px;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: var(--font-family);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--color-gray-disabled);
background-color: var(--color-surface);
flex-shrink: 0;
}
.header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--color-text);
font-family: var(--font-family);
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background-color: transparent;
color: var(--color-gray);
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
}
.closeButton:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabNavigation {
display: flex;
border-bottom: 1px solid var(--color-gray-disabled);
background-color: var(--color-surface);
padding: 0 20px;
}
.tabButton {
padding: 12px 16px;
border: none;
background: none;
color: var(--color-gray);
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
white-space: nowrap;
font-family: var(--font-family);
}
.tabButton:hover {
color: var(--color-text);
}
.tabButton.active {
color: var(--color-secondary);
border-bottom-color: var(--color-secondary);
}
.actionBar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-gray-disabled);
background-color: var(--color-bg);
}
.selectionControls {
display: flex;
align-items: center;
gap: 16px;
}
.selectAllButton {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--color-gray-disabled);
border-radius: 6px;
background-color: var(--color-bg);
color: var(--color-text);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
font-family: var(--font-family);
}
.selectAllButton:hover {
background-color: var(--color-surface);
border-color: var(--color-gray);
}
.selectionCount {
font-size: 14px;
color: var(--color-gray);
font-weight: 500;
font-family: var(--font-family);
}
.uploadButton {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: none;
border-radius: 8px;
background-color: var(--color-secondary);
color: var(--color-bg);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
font-family: var(--font-family);
}
.uploadButton:hover {
background-color: var(--color-secondary-hover);
}
.fileListContainer {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.loading,
.error,
.noFiles {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-gray);
font-size: 16px;
text-align: center;
font-family: var(--font-family);
}
.error {
color: var(--color-red);
}
.selectableFileList {
display: flex;
flex-direction: column;
gap: 8px;
}
.selectableFileItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
background-color: var(--color-bg);
cursor: pointer;
transition: all 0.2s ease;
}
.selectableFileItem:hover {
background-color: var(--color-surface);
border-color: var(--color-primary);
}
.selectableFileItem.selected {
background-color: var(--color-primary-disabled);
border-color: var(--color-primary);
}
.fileCheckbox {
display: flex;
align-items: center;
font-size: 18px;
color: var(--color-text);
}
.checkedIcon {
color: var(--color-secondary);
}
.uncheckedIcon {
color: var(--color-text);
}
.fileIcon {
display: flex;
align-items: center;
font-size: 16px;
color: var(--color-text);
}
.fileInfo {
flex: 1;
min-width: 0;
}
.fileName {
font-weight: 500;
color: var(--color-text);
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fileDetails {
font-size: 12px;
color: var(--color-text);
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid var(--color-gray-disabled);
background-color: var(--color-bg);
flex-shrink: 0;
}
.cancelButton {
padding: 10px 16px;
border: 1px solid var(--color-red);
border-radius: 8px;
background-color: var(--color-bg);
color: var(--color-text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.cancelButton:hover {
background-color: var(--color-red);
border-color: var(--color-red);
}
.confirmButton {
padding: 10px 16px;
border: none;
border-radius: 8px;
background-color: var(--color-secondary);
color: var(--color-bg);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.confirmButton:hover:not(:disabled) {
background-color: var(--color-secondary-hover);
}
.confirmButton:disabled {
background-color: var(--color-secondary-disabled);
cursor: not-allowed;
}
/* Responsive design */
@media (max-width: 768px) {
.modal {
width: 95vw;
height: 85vh;
}
.header h2 {
font-size: 18px;
}
.tabNavigation {
padding: 0 12px;
overflow-x: auto;
}
.tabButton {
padding: 10px 12px;
font-size: 13px;
}
.actionBar {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.selectionControls {
justify-content: space-between;
}
.fileListContainer {
padding: 12px;
}
.selectableFileItem {
padding: 10px;
}
.footer {
padding: 16px;
}
}

View file

@ -1,312 +0,0 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { IoClose, IoCheckbox, IoSquareOutline, IoCloudUploadOutline } from 'react-icons/io5';
import { FaFile } from 'react-icons/fa';
import { useUserFiles, UserFile, FileInfo } from '../../../hooks/useFiles';
import DateienUploadTool from './DateienUploadTool';
import DateienAll from '../DateienAll';
import DateienUploads from '../DateienUploads';
import DateienCreated from '../DateienCreated';
import DateienShared from '../DateienShared';
import { useLanguage } from '../../../contexts/LanguageContext';
import styles from './DateienSelector.module.css';
type FileListType = 'all' | 'uploads' | 'created' | 'shared';
interface DateienSelectorProps {
isOpen: boolean;
onClose: () => void;
onFilesSelected: (files: FileInfo[]) => void;
allowMultiple?: boolean;
}
const DateienSelector: React.FC<DateienSelectorProps> = ({
isOpen,
onClose,
onFilesSelected,
allowMultiple = true
}) => {
const { t } = useLanguage();
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
const [activeTab, setActiveTab] = useState<FileListType>('all');
const [showUploadTool, setShowUploadTool] = useState(false);
const { files, loading, error, refetch } = useUserFiles();
// Filter files based on source
const getFilteredFiles = (files: UserFile[], type: FileListType): UserFile[] => {
switch (type) {
case 'uploads':
return files.filter(file => file.source === 'user_uploaded');
case 'created':
return files.filter(file => file.source === 'agent_created');
case 'shared':
return files.filter(file => file.source === 'shared_with_me');
case 'all':
default:
return files;
}
};
const filteredFiles = getFilteredFiles(files, activeTab);
// Reset selection when tab changes
useEffect(() => {
setSelectedFiles(new Set());
}, [activeTab]);
// Reset state when modal closes
useEffect(() => {
if (!isOpen) {
setSelectedFiles(new Set());
setShowUploadTool(false);
}
}, [isOpen]);
const handleFileSelect = (fileId: number) => {
if (allowMultiple) {
setSelectedFiles(prev => {
const newSet = new Set(prev);
if (newSet.has(fileId)) {
newSet.delete(fileId);
} else {
newSet.add(fileId);
}
return newSet;
});
} else {
setSelectedFiles(new Set([fileId]));
}
};
const handleSelectAll = () => {
if (selectedFiles.size === filteredFiles.length) {
setSelectedFiles(new Set());
} else {
setSelectedFiles(new Set(filteredFiles.map(file => file.id)));
}
};
const handleConfirmSelection = () => {
const selectedFileObjects: FileInfo[] = files
.filter(file => selectedFiles.has(file.id))
.map(file => ({
id: file.id,
name: file.file_name,
mimeType: deriveMimeTypeFromAction(file.action),
size: file.size,
creationDate: file.created_at,
source: file.source
}));
onFilesSelected(selectedFileObjects);
onClose();
};
// Helper function to derive MIME type from action (reverse mapping)
const deriveMimeTypeFromAction = (action: string): string => {
switch (action.toLowerCase()) {
case 'bild':
return 'image/jpeg'; // Default image type
case 'pdf':
return 'application/pdf';
case 'dokument':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
case 'tabelle':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case 'text':
return 'text/plain';
case 'video':
return 'video/mp4'; // Default video type
case 'audio':
return 'audio/mpeg'; // Default audio type
default:
return 'application/octet-stream'; // Default binary type
}
};
const handleFileUpload = (file: File) => {
// Refresh file list after upload
refetch();
setShowUploadTool(false);
};
const renderFileListComponent = () => {
const commonProps = {
files: filteredFiles,
onFileDeleted: refetch
};
switch (activeTab) {
case 'uploads':
return <DateienUploads {...commonProps} />;
case 'created':
return <DateienCreated {...commonProps} />;
case 'shared':
return <DateienShared {...commonProps} />;
case 'all':
default:
return <DateienAll {...commonProps} />;
}
};
const getTabLabel = (type: FileListType) => {
const counts = {
all: files.length,
uploads: files.filter(f => f.source === 'user_uploaded').length,
created: files.filter(f => f.source === 'agent_created').length,
shared: files.filter(f => f.source === 'shared_with_me').length
};
const labels = {
all: t('files.selector.tab.all', 'All files'),
uploads: t('files.selector.tab.uploads', 'Uploaded'),
created: t('files.selector.tab.created', 'AI-created'),
shared: t('files.selector.tab.shared', 'Shared')
};
return `${labels[type]} (${counts[type]})`;
};
if (!isOpen) return null;
if (showUploadTool) {
return (
<DateienUploadTool
isOpen={true}
onClose={() => setShowUploadTool(false)}
onFileUpload={handleFileUpload}
/>
);
}
return (
<div className={styles.overlay}>
<motion.div
className={styles.modal}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3 }}
>
<div className={styles.header}>
<h2>{t('files.selector.title', 'Select files')}</h2>
<button className={styles.closeButton} onClick={onClose}>
<IoClose />
</button>
</div>
<div className={styles.content}>
{/* Tab Navigation */}
<div className={styles.tabNavigation}>
{(['all', 'uploads', 'created', 'shared'] as FileListType[]).map(tab => (
<button
key={tab}
className={`${styles.tabButton} ${activeTab === tab ? styles.active : ''}`}
onClick={() => setActiveTab(tab)}
>
{getTabLabel(tab)}
</button>
))}
</div>
{/* Action Bar */}
<div className={styles.actionBar}>
<div className={styles.selectionControls}>
{allowMultiple && filteredFiles.length > 0 && (
<button
className={styles.selectAllButton}
onClick={handleSelectAll}
>
{selectedFiles.size === filteredFiles.length ? (
<>
<IoCheckbox /> {t('files.selector.deselect_all', 'Deselect all')}
</>
) : (
<>
<IoSquareOutline /> {t('files.selector.select_all', 'Select all')}
</>
)}
</button>
)}
{selectedFiles.size > 0 && (
<span className={styles.selectionCount}>
{selectedFiles.size} {selectedFiles.size === 1 ? t('files.selector.file_selected', 'File') : t('files.selector.files_selected', 'Files')} {t('files.selector.selected_suffix', 'selected')}
</span>
)}
</div>
<button
className={styles.uploadButton}
onClick={() => setShowUploadTool(true)}
>
<IoCloudUploadOutline />
{t('files.selector.upload_new', 'Upload new file')}
</button>
</div>
{/* File List */}
<div className={styles.fileListContainer}>
{loading ? (
<div className={styles.loading}>{t('files.selector.loading', 'Loading files...')}</div>
) : error ? (
<div className={styles.error}>{t('files.selector.error_loading', 'Error loading files:')} {error}</div>
) : filteredFiles.length === 0 ? (
<div className={styles.noFiles}>{t('files.no_files', 'No files found.')}</div>
) : (
<div className={styles.selectableFileList}>
{filteredFiles.map(file => (
<div
key={file.id}
className={`${styles.selectableFileItem} ${
selectedFiles.has(file.id) ? styles.selected : ''
}`}
onClick={() => handleFileSelect(file.id)}
>
<div className={styles.fileCheckbox}>
{selectedFiles.has(file.id) ? (
<IoCheckbox className={styles.checkedIcon} />
) : (
<IoSquareOutline className={styles.uncheckedIcon} />
)}
</div>
<div className={styles.fileIcon}>
<FaFile />
</div>
<div className={styles.fileInfo}>
<div className={styles.fileName}>{file.file_name}</div>
<div className={styles.fileDetails}>
{file.action} {file.size ? `${Math.round(file.size / 1024)} KB` : t('files.unknown_size', 'Unknown Size')}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Footer */}
<div className={styles.footer}>
<button
className={styles.cancelButton}
onClick={onClose}
>
{t('common.cancel', 'Cancel')}
</button>
<button
className={styles.confirmButton}
onClick={handleConfirmSelection}
disabled={selectedFiles.size === 0}
>
{selectedFiles.size === 0
? t('files.selector.title', 'Select files')
: `${selectedFiles.size} ${selectedFiles.size === 1 ? t('files.selector.file_selected', 'File') : t('files.selector.files_selected', 'Files')} ${t('common.save', 'Save')}`
}
</button>
</div>
</motion.div>
</div>
);
};
export default DateienSelector;

View file

@ -1,163 +0,0 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.h2 {
font-size: 24px;
font-weight: 600;
font-family: var(--font-family);
color: var(--color-text);
}
.modal {
background: var(--color-bg);
padding: 35px 40px 30px 40px;
border-radius: 30px;
width: 90%;
max-width: 500px;
position: relative;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-family: var(--font-family);
}
.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.modalHeader h2 {
margin: 0;
font-size: 1.5rem;
color: var(--color-text);
font-family: var(--font-family);
}
.closeButton {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--color-gray);
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.closeButton:hover {
color: var(--color-text);
}
.closeButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.uploadStatus {
padding: 1rem;
border-radius: 15px;
margin-bottom: 1rem;
text-align: center;
font-weight: 500;
font-family: var(--font-family);
}
.uploadStatus.success {
background-color: var(--color-secondary-disabled);
color: var(--color-secondary);
border: 1px solid var(--color-secondary);
}
.uploadStatus.error {
background-color: var(--color-red-disabled);
color: var(--color-red);
border: 1px solid var(--color-red);
}
.dropzone {
border: 2px dashed var(--color-gray-disabled);
border-radius: 15px;
padding: 2rem;
text-align: center;
cursor: pointer;
margin: 1rem 0;
transition: all 0.3s ease;
background-color: var(--color-bg);
}
.dropzone.active {
border-color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
}
.dropzone.uploading {
border-color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
cursor: wait;
}
.uploadIcon {
font-size: 3rem;
color: var(--color-gray);
margin-bottom: 1rem;
}
.dropzoneText {
color: var(--color-gray);
font-family: var(--font-family);
}
.dropzoneText p {
margin: 0.5rem 0;
}
.browseButton {
background-color: var(--color-secondary);
color: var(--color-bg);
border: none;
padding: 0.5rem 1rem;
border-radius: 15px;
cursor: pointer;
font-family: var(--font-family);
}
.browseButton:hover {
background-color: var(--color-secondary-hover);
}
.browseButton:disabled {
background-color: var(--color-gray-disabled);
color: var(--color-gray);
cursor: not-allowed;
}
.uploadButton {
background-color: var(--color-secondary);
color: var(--color-bg);
border: none;
padding: 0.5rem 1rem;
border-radius: 15px;
cursor: pointer;
font-family: var(--font-family);
}
.uploadButton:hover {
background-color: var(--color-secondary-hover);
}
.uploadButton:disabled {
background-color: var(--color-gray-disabled);
color: var(--color-gray);
cursor: not-allowed;
}

View file

@ -1,127 +0,0 @@
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import styles from './DateienUploadTool.module.css';
import { IoCloudUploadOutline } from "react-icons/io5";
import { IoClose } from "react-icons/io5";
import { useFileOperations } from '../../../hooks/useFiles';
import { useLanguage } from '../../../contexts/LanguageContext';
interface DateienUploadToolProps {
isOpen: boolean;
onClose: () => void;
onFileUpload: (file: File) => void;
}
function DateienUploadTool({ isOpen, onClose, onFileUpload }: DateienUploadToolProps) {
const { t } = useLanguage();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadStatus, setUploadStatus] = useState<{ success: boolean; message: string } | null>(null);
const { handleFileUpload, uploadError } = useFileOperations();
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
setSelectedFile(acceptedFiles[0]);
// Clear previous upload status when selecting a new file
setUploadStatus(null);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false
});
const handleUpload = async () => {
if (selectedFile) {
setIsUploading(true);
setUploadStatus(null);
try {
const result = await handleFileUpload(selectedFile);
if (result.success) {
setUploadStatus({
success: true,
message: t('files.upload.success', 'File uploaded successfully!')
});
onFileUpload(selectedFile);
setSelectedFile(null);
// Close modal after brief success message
setTimeout(() => {
onClose();
}, 1500);
} else {
setUploadStatus({
success: false,
message: uploadError || t('files.upload.error', 'An error occurred while uploading.')
});
}
} catch (error) {
setUploadStatus({
success: false,
message: t('files.upload.unexpected_error', 'An unexpected error occurred while uploading.')
});
} finally {
setIsUploading(false);
}
}
};
if (!isOpen) return null;
return (
<div className={styles.overlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2>{t('files.upload.title', 'Upload file')}</h2>
<button className={styles.closeButton} onClick={onClose} disabled={isUploading}>
<IoClose />
</button>
</div>
{uploadStatus && (
<div className={`${styles.uploadStatus} ${uploadStatus.success ? styles.success : styles.error}`}>
{uploadStatus.message}
</div>
)}
<div
{...getRootProps()}
className={`${styles.dropzone} ${isDragActive ? styles.active : ''} ${isUploading ? styles.uploading : ''}`}
>
<input {...getInputProps()} disabled={isUploading} />
<IoCloudUploadOutline className={styles.uploadIcon} />
{isDragActive ? (
<p>{t('files.upload.drop_here', 'Drop file here...')}</p>
) : isUploading ? (
<p>{t('files.upload.uploading', 'Uploading...')}</p>
) : (
<div className={styles.dropzoneText}>
<p>{t('files.upload.drag_files', 'Drag files here')}</p>
<p>{t('files.upload.or', 'or')}</p>
<button className={styles.browseButton} disabled={isUploading}>
{t('files.upload.browse', 'Browse')}
</button>
</div>
)}
</div>
{selectedFile && !isUploading && !uploadStatus?.success && (
<div className={styles.selectedFile}>
<p>{t('files.upload.selected_file', 'Selected file:')} {selectedFile.name}</p>
<button
className={styles.uploadButton}
onClick={handleUpload}
disabled={isUploading}
>
{isUploading ? t('files.upload.uploading_button', 'Uploading...') : t('files.upload.upload_button', 'Upload')}
</button>
</div>
)}
</div>
</div>
);
}
export default DateienUploadTool;

View file

@ -1,175 +0,0 @@
.fileItem {
display: flex;
align-items: center;
height: 60px;
padding: 0px 16px;
justify-content: space-between;
color: var(--color-text);
transition: background-color 0.2s ease;
font-family: var(--font-family);
}
.fileItem:hover {
background-color: var(--color-surface);
}
/* Column layout matching the header structure */
.fileName {
display: flex;
align-items: center;
overflow: hidden;
font-weight: 500;
color: var(--color-text);
padding-left: 14px; /* Align with table header */
font-family: var(--font-family);
}
.fileName span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 12px;
}
.fileType {
font-size: 14px;
color: var(--color-gray);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
flex-direction: column;
gap: 2px;
font-family: var(--font-family);
}
.fileSource {
font-size: 12px;
color: var(--color-gray-hover);
font-weight: 400;
opacity: 0.8;
font-family: var(--font-family);
}
.fileSize {
font-size: 14px;
color: var(--color-gray);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-family: var(--font-family);
}
.fileDateWithActions {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.fileDate {
font-size: 14px;
color: var(--color-gray);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 8px;
font-family: var(--font-family);
}
.icon {
font-size: 18px;
color: var(--color-gray);
flex-shrink: 0;
}
.actionButtons {
display: flex;
gap: 4px;
justify-content: flex-end;
margin-left: auto;
}
.downloadButton,
.deleteButton,
.previewButton {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px;
border: none;
border-radius: 4px;
background-color: transparent;
color: var(--color-gray);
cursor: pointer;
transition: all 0.2s ease;
min-width: 32px;
font-family: var(--font-family);
}
.downloadButton:hover:not(:disabled),
.deleteButton:hover:not(:disabled),
.previewButton:hover:not(:disabled) {
background-color: var(--color-surface);
color: var(--color-text);
}
.deleteButton:hover:not(:disabled) {
color: var(--color-red);
}
.previewButton:hover:not(:disabled) {
color: var(--color-secondary);
}
.deleteButton.confirm {
background-color: var(--color-red-disabled);
color: var(--color-red);
}
.deleteButton.confirm:hover:not(:disabled) {
background-color: var(--color-red-hover);
}
.downloadButton:disabled,
.deleteButton:disabled,
.previewButton:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.actionIcon {
font-size: 16px;
flex-shrink: 0;
}
.downloadButton.downloading,
.deleteButton.deleting {
background-color: var(--color-surface);
}
.actionText {
font-size: 12px;
color: var(--color-gray);
animation: pulse 1.5s infinite;
white-space: nowrap;
font-family: var(--font-family);
}
.deleteButton.confirm .actionText {
color: var(--color-red);
animation: none;
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}

View file

@ -1,199 +0,0 @@
import { FaFile, FaDownload, FaTrash } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md";
import styles from "./DateienItem.module.css";
import { useState } from "react";
import { useFileOperations } from "../../hooks/useFiles";
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatAreaFilePreview";
import { Document } from "../Dashboard/DashboardChat/dashboardChatAreaTypes";
import { useLanguage } from "../../contexts/LanguageContext";
type DateienItemProps = {
file: {
id: number;
file_name: string;
action: string;
created_at: string;
size?: number;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
};
onDelete?: () => void;
onOptimisticDelete?: () => void; // New prop for immediate UI update
};
const DateienItem = ({ file, onDelete, onOptimisticDelete }: DateienItemProps) => {
const { t } = useLanguage();
const { downloadingFiles, deletingFiles, handleFileDownload, handleFileDelete } = useFileOperations();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const isDownloading = downloadingFiles.has(file.id);
const isDeleting = deletingFiles.has(file.id);
/**
* Formats a file size in bytes to a human-readable string (KB, MB, etc.)
*/
const formatFileSize = (bytes?: number): string => {
if (bytes === undefined || bytes === null) return t('files.unknown_size', 'Unknown Size');
if (bytes === 0) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
if (i === 0) return `${bytes} ${sizes[i]}`;
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
};
/**
* Formats the file source for display
*/
const formatFileSource = (source?: string): string => {
switch (source) {
case 'user_uploaded':
return t('files.source.uploaded', 'Uploaded');
case 'agent_created':
return t('files.source.ai_created', 'AI-created');
case 'shared_with_me':
return t('files.source.shared', 'Shared');
default:
return t('files.source.unknown', 'Unknown');
}
};
// Format the date properly
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
// Check if date is valid
if (isNaN(date.getTime())) {
return t('files.unknown_date', 'Unknown Date');
}
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
} catch (e) {
console.error('Error formatting date:', e);
return t('files.unknown_date', 'Unknown Date');
}
};
const handleDeleteClick = async () => {
if (showDeleteConfirm) {
const success = await handleFileDelete(file.id, onOptimisticDelete);
if (!success) {
// If deletion failed, refresh the file list to restore the file
if (onDelete) {
onDelete();
}
}
setShowDeleteConfirm(false);
} else {
setShowDeleteConfirm(true);
}
};
const handleCancelDelete = () => {
setShowDeleteConfirm(false);
};
const handlePreview = () => {
// Split filename to get name and extension
const nameParts = file.file_name.split('.');
const extension = nameParts.length > 1 ? nameParts.pop() : undefined;
const fileName = nameParts.join('.');
// Create a Document object compatible with FilePreviewPopup
const document: Document = {
id: String(file.id),
fileId: file.id,
name: fileName,
ext: extension,
size: file.size
};
setPreviewDocument(document);
setIsPreviewOpen(true);
};
const handleClosePreview = () => {
setIsPreviewOpen(false);
setPreviewDocument(null);
};
return (
<>
<li>
{/* 1st column: Name with icon */}
<div className={styles.fileName}>
<FaFile className={styles.icon} />
<span>{file.file_name}</span>
</div>
{/* 2nd column: Type with source */}
<div className={styles.fileType}>
<div>{file.action}</div>
<div className={styles.fileSource}>{formatFileSource(file.source)}</div>
</div>
{/* 3rd column: Size */}
<div className={styles.fileSize}>
{formatFileSize(file.size)}
</div>
{/* 4th column: Date and action buttons */}
<div className={styles.fileDateWithActions}>
<span className={styles.fileDate}>
{formatDate(file.created_at)}
</span>
<div className={styles.actionButtons}>
<button
className={styles.previewButton}
onClick={handlePreview}
disabled={isDownloading || isDeleting}
title={t('files.preview_tooltip', 'Preview file')}
>
<MdOutlineRemoveRedEye className={styles.actionIcon} />
</button>
<button
className={`${styles.downloadButton} ${isDownloading ? styles.downloading : ''}`}
onClick={() => handleFileDownload(file.id, file.file_name)}
disabled={isDownloading || isDeleting}
title={t('files.download_tooltip', 'Download file')}
>
<FaDownload className={styles.actionIcon} />
{isDownloading && <span className={styles.actionText}>{t('files.downloading', 'Downloading...')}</span>}
</button>
<button
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
onClick={handleDeleteClick}
disabled={isDownloading || isDeleting}
title={showDeleteConfirm ? t('files.delete_confirm_tooltip', 'Click again to confirm deletion') : t('files.delete_tooltip', 'Delete file')}
onBlur={handleCancelDelete}
>
<FaTrash className={styles.actionIcon} />
{isDeleting && <span className={styles.actionText}>{t('files.deleting', 'Deleting...')}</span>}
{showDeleteConfirm && <span className={styles.actionText}>{t('files.delete_confirm', 'Click to confirm...')}</span>}
</button>
</div>
</div>
</li>
{/* File Preview Popup */}
{previewDocument && (
<FilePreviewPopup
document={previewDocument}
isOpen={isPreviewOpen}
onClose={handleClosePreview}
/>
)}
</>
);
};
export default DateienItem;

View file

@ -1,102 +0,0 @@
/* No files message */
.noFilesMessage {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 20px;
color: var(--color-gray);
font-style: italic;
font-family: var(--font-family);
}
/* Files table container */
.filesTable {
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
}
/* Table header with exact grid positioning */
.tableHeader {
display: grid;
grid-template-columns: 45% 15% 15% 25%;
align-items: center;
height: 40px;
padding: 0px 16px;
position: sticky;
top: 0;
z-index: 10;
background-color: var(--color-bg);
border-bottom: 1px solid var(--color-gray-disabled);
margin-bottom: 10px;
flex-shrink: 0;
}
/* Header cells with exact positioning */
.headerCell {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
color: var(--color-text);
cursor: pointer;
white-space: nowrap;
padding-left: 0;
transition: color 0.2s ease;
font-family: var(--font-family);
}
.headerCell:hover {
color: var(--color-primary);
}
/* Adjust first column for icon space */
.headerCell:nth-child(1) {
padding-left: 30px;
}
/* Simple sort icon styling */
.sortIcon {
margin-left: 6px;
font-size: 14px;
color: var(--color-gray);
transition: color 0.2s ease;
}
.headerCell:hover .sortIcon {
color: var(--color-primary);
}
/* File list styling */
.filesList {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* Override the flex layout in DateienItem to force matching the header */
.filesList li {
display: grid !important;
grid-template-columns: 45% 15% 15% 25%;
border-bottom: 1px solid var(--color-gray-disabled);
height: 60px;
padding: 0 16px;
align-items: center;
transition: background-color 0.2s ease;
position: relative;
}
.filesList li:hover {
background-color: var(--color-surface);
}

View file

@ -1,139 +0,0 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
interface DateienSharedProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienShared: React.FC<DateienSharedProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const { t } = useLanguage();
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Filter files for shared (shared_with_me)
const sharedFiles = files.filter(file => file.source === 'shared_with_me');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort files
const sortedFiles = [...sharedFiles].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>{t('files.no_shared_files', 'No shared files found.')}</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>{t('files.header.name', 'Name')}</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>{t('files.header.type', 'Type')}</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>{t('files.header.size', 'Size')}</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>{t('files.header.date', 'Date')}</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List with shared indicator */}
<motion.ul
className={`${styles.filesList} ${styles.shared}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienShared;

View file

@ -0,0 +1,249 @@
.dateienTable {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.dateienFormGenerator {
flex: 1;
height: 100%;
}
/* Error state styling */
.errorState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
text-align: center;
color: var(--color-error, #dc3545);
background-color: var(--color-error-bg, #f8d7da);
border: 1px solid var(--color-error-border, #f5c6cb);
border-radius: 8px;
margin: 1rem;
}
.retryButton {
padding: 0.5rem 1rem;
background-color: var(--color-primary, #007bff);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
transition: background-color 0.2s ease;
}
.retryButton:hover {
background-color: var(--color-primary, #0056b3);
}
/* Table cell styling */
.fileName {
font-weight: 500;
color: var(--color-text) !important;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fileTypeBadge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
text-transform: capitalize;
text-align: center;
min-width: 60px;
}
.type-bild,
.type-image {
background-color: #e3f2fd;
color: #1565c0;
border: 1px solid #bbdefb;
}
.type-pdf {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.type-dokument,
.type-document {
background-color: #e8f5e8;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.type-tabelle,
.type-spreadsheet {
background-color: #fff3e0;
color: #ef6c00;
border: 1px solid #ffe0b2;
}
.type-text {
background-color: #f3e5f5;
color: #7b1fa2;
border: 1px solid #e1bee7;
}
.type-video {
background-color: #fce4ec;
color: #ad1457;
border: 1px solid #f8bbd9;
}
.type-audio {
background-color: #e0f2f1;
color: #00695c;
border: 1px solid #b2dfdb;
}
.type-datei,
.type-file {
background-color: #f5f5f5;
color: #495057;
border: 1px solid #dee2e6;
}
.fileSize {
font-weight: 500;
color: var(--color-text-secondary, #666);
background-color: var(--color-bg-secondary, #f8f9fa);
padding: 0.25rem 0.5rem;
border-radius: 8px;
font-size: 0.9em;
text-align: center;
display: inline-block;
min-width: 60px;
}
.sourceBadge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
text-align: center;
min-width: 80px;
}
.source-user-uploaded {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.source-agent-created {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.source-shared-with-me {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
/* Responsive design */
@media (max-width: 768px) {
.fileName {
font-size: 0.9em;
}
.fileTypeBadge,
.sourceBadge {
font-size: 0.8em;
padding: 0.2rem 0.4rem;
}
.fileSize {
font-size: 0.8em;
padding: 0.2rem 0.4rem;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.fileName {
color: #f8f9fa;
}
.fileSize {
background-color: #374151;
color: #d1d5db;
}
.type-bild,
.type-image {
background-color: #1e3a8a;
color: #bfdbfe;
}
.type-pdf {
background-color: #7f1d1d;
color: #fecaca;
}
.type-dokument,
.type-document {
background-color: #14532d;
color: #bbf7d0;
}
.type-tabelle,
.type-spreadsheet {
background-color: #9a3412;
color: #fed7aa;
}
.type-text {
background-color: #581c87;
color: #e9d5ff;
}
.type-video {
background-color: #831843;
color: #fbcfe8;
}
.type-audio {
background-color: #0f766e;
color: #99f6e4;
}
.type-datei,
.type-file {
background-color: #374151;
color: #d1d5db;
}
.source-user-uploaded {
background-color: #14532d;
color: #bbf7d0;
}
.source-agent-created {
background-color: #0c4a6e;
color: #bae6fd;
}
.source-shared-with-me {
background-color: #92400e;
color: #fde68a;
}
.errorState {
background-color: #2d1b1e;
border-color: #5c2b33;
color: #f5c6cb;
}
}

View file

@ -0,0 +1,80 @@
import { FormGenerator } from '../FormGenerator';
import { useLanguage } from '../../contexts/LanguageContext';
import { Popup, EditForm } from '../Popup';
import styles from './DateienTable.module.css';
import { useDateienLogic } from './dateienLogic.tsx';
import type { DateienTableProps } from './dateienInterfaces';
export function DateienTable({ className = '' }: DateienTableProps) {
const { t } = useLanguage();
// Use the custom hook for all business logic
const {
files,
loading,
error,
columns,
actions,
editModalOpen,
editingFile,
editFileFields,
handleSaveFile,
handleCancelEdit
} = useDateienLogic();
// Show error state
if (error) {
return (
<div className={`${styles.dateienTable} ${className}`}>
<div className={styles.errorState}>
<p>{t('files.error.loading')} {error}</p>
<button onClick={() => window.location.reload()} className={styles.retryButton}>
{t('files.button.retry')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.dateienTable} ${className}`}>
<FormGenerator
data={files}
columns={columns}
title={t('files.table.title')}
loading={loading}
searchable={true}
filterable={true}
sortable={true}
resizable={true}
pagination={true}
pageSize={10}
selectable={false}
onRowClick={undefined}
actions={actions}
className={styles.dateienFormGenerator}
/>
{/* Edit File Modal */}
<Popup
isOpen={editModalOpen}
title={t('files.edit.title', 'Edit File')}
onClose={handleCancelEdit}
size="small"
>
{editingFile && (
<EditForm
data={editingFile}
fields={editFileFields}
onSave={handleSaveFile}
onCancel={handleCancelEdit}
saveButtonText={t('common.save', 'Save')}
cancelButtonText={t('common.cancel', 'Cancel')}
/>
)}
</Popup>
</div>
);
}
export default DateienTable;

View file

@ -1,139 +0,0 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
interface DateienUploadsProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienUploads: React.FC<DateienUploadsProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const { t } = useLanguage();
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Filter files for uploads (user_uploaded)
const uploadedFiles = files.filter(file => file.source === 'user_uploaded');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort files
const sortedFiles = [...uploadedFiles].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>{t('files.no_uploaded_files', 'No uploaded files found.')}</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>{t('files.header.name', 'Name')}</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>{t('files.header.type', 'Type')}</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>{t('files.header.size', 'Size')}</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>{t('files.header.date', 'Date')}</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List */}
<motion.ul
className={styles.filesList}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienUploads;

View file

@ -0,0 +1,90 @@
import { ColumnConfig } from '../FormGenerator';
import React from 'react';
import { EditFieldConfig } from '../Popup/EditForm';
// Re-export file-related interfaces from hooks
export type { UserFile, FileInfo } from '../../hooks/useFiles';
// Import for local use
import type { UserFile } from '../../hooks/useFiles';
// Component Props Interfaces
export interface DateienTableProps {
className?: string;
}
// Table Action Interface
export interface TableAction {
label: string;
onClick: (file: UserFile) => Promise<void> | void;
icon: React.ReactNode | ((file: UserFile) => React.ReactNode);
}
// File Operation Handler Types
export interface FileHandlers {
handleFileDownload: (fileId: string, fileName: string) => Promise<boolean>;
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
handleFileUpload: (file: globalThis.File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
handleFileUpdate: (fileId: string, updateData: Partial<{ filename: string }>) => Promise<{ success: boolean; fileData?: any; error?: string }>;
}
// Hook Return Types for File Operations
export interface FileOperationsReturn extends FileHandlers {
downloadingFiles: Set<string>;
deletingFiles: Set<string>;
uploadingFile: boolean;
downloadError: string | null;
deleteError: string | null;
uploadError: string | null;
isLoading: boolean;
}
// Hook Return Types for User Files
export interface UserFilesReturn {
files: UserFile[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
removeFileOptimistically: (fileId: string) => void;
addFileOptimistically: (newFile: UserFile) => void;
}
// File Table Configuration
export interface FileTableConfig {
columns: ColumnConfig[];
actions: TableAction[];
pageSize: number;
searchable: boolean;
filterable: boolean;
sortable: boolean;
resizable: boolean;
pagination: boolean;
}
// File Size Formatter Function Type
export type FileSizeFormatter = (sizeInBytes?: number) => string;
// Date Formatter Function Type
export type DateFormatter = (value?: string) => string;
// Hook Return Type for Dateien Logic
export interface DateienLogicReturn {
files: UserFile[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
columns: ColumnConfig[];
actions: TableAction[];
downloadingFiles: Set<string>;
deletingFiles: Set<string>;
downloadError: string | null;
deleteError: string | null;
editModalOpen: boolean;
editingFile: UserFile | null;
editFileFields: EditFieldConfig[];
handleEditFile: (file: UserFile) => void;
handleSaveFile: (updatedFile: UserFile) => Promise<void>;
handleCancelEdit: () => void;
}

View file

@ -0,0 +1,274 @@
import { useMemo, useState } from 'react';
import { IoIosTrash, IoIosDownload } from 'react-icons/io';
import { MdModeEdit } from 'react-icons/md';
import { ColumnConfig } from '../FormGenerator';
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
import { useLanguage } from '../../contexts/LanguageContext';
import { EditFieldConfig } from '../Popup/EditForm';
import type {
TableAction,
FileSizeFormatter,
DateFormatter,
UserFile,
DateienLogicReturn
} from './dateienInterfaces';
export function useDateienLogic(): DateienLogicReturn {
const { files, loading, error, refetch } = useUserFiles();
const {
handleFileDownload,
handleFileDelete,
handleFileUpdate,
downloadingFiles,
deletingFiles,
downloadError,
deleteError
} = useFileOperations();
const { t } = useLanguage();
// Edit modal state
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
// Configure edit fields for filename editing
const editFileFields: EditFieldConfig[] = useMemo(() => [
{
key: 'file_name',
label: t('files.field.filename', 'Filename'),
type: 'string',
editable: true,
required: true,
validator: (value: string) => {
if (!value || value.trim() === '') {
return 'Filename cannot be empty';
}
if (value.includes('/') || value.includes('\\')) {
return 'Filename cannot contain / or \\ characters';
}
return null;
}
}
], [t]);
// Handle edit file
const handleEditFile = (file: UserFile) => {
setEditingFile(file);
setEditModalOpen(true);
};
// Handle save file
const handleSaveFile = async (updatedFile: UserFile) => {
if (!editingFile) return;
try {
// Call API to update filename
const result = await handleFileUpdate(editingFile.id, {
filename: updatedFile.file_name
});
if (result.success) {
// Close modal
setEditModalOpen(false);
setEditingFile(null);
// Refresh file list
await refetch();
} else {
console.error('Failed to update file:', result.error);
// TODO: Show error message to user
}
} catch (error) {
console.error('Failed to update file:', error);
// TODO: Show error message to user
}
};
// Handle cancel edit
const handleCancelEdit = () => {
setEditModalOpen(false);
setEditingFile(null);
};
// Helper function to format file size
const formatFileSize: FileSizeFormatter = (sizeInBytes) => {
if (!sizeInBytes || sizeInBytes === 0) return '-';
const units = ['Bytes', 'KB', 'MB', 'GB'];
let size = sizeInBytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
};
// Helper function to format date
const formatDate: DateFormatter = (value) => {
if (!value) return '-';
try {
const date = new Date(value);
const pad = (n: number) => n.toString().padStart(2, '0');
const yyyy = date.getFullYear();
const mm = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
const hh = pad(date.getHours());
const mi = pad(date.getMinutes());
const ss = pad(date.getSeconds());
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
} catch {
return value;
}
};
// Configure columns for the files table
const columns: ColumnConfig[] = useMemo(() => [
{
key: 'file_name',
label: 'Filename',
type: 'string',
width: 300,
minWidth: 200,
maxWidth: 400,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string) => (
<span
style={{
color: 'var(--color-text)',
fontWeight: 500,
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
paddingLeft: '8px'
}}
title={value}
>
{value}
</span>
)
},
{
key: 'mime_type',
label: 'mime type',
type: 'string',
width: 200,
minWidth: 150,
maxWidth: 300,
sortable: true,
filterable: true,
searchable: true,
},
{
key: 'size',
label: 'filesize',
type: 'number',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: false,
formatter: (value: number | string | undefined) => (
<span style={{ fontWeight: 500, color: 'var(--color-text)' }}>
{formatFileSize(typeof value === 'string' ? parseInt(value, 10) : value)}
</span>
)
},
{
key: 'created_at',
label: 'creation date',
type: 'date',
width: 200,
minWidth: 180,
maxWidth: 240,
sortable: true,
filterable: true,
formatter: (value: string | undefined) => formatDate(value)
},
], []);
// Handle file download
const handleDownload = async (file: UserFile) => {
const success = await handleFileDownload(file.id, file.file_name);
if (!success && downloadError) {
console.error('Download failed:', downloadError);
}
};
// Handle file deletion
const handleDelete = async (file: UserFile) => {
if (window.confirm(t('files.delete.confirm').replace('{name}', file.file_name))) {
const success = await handleFileDelete(file.id, () => {
// Optimistic update - this will be called immediately
refetch();
});
if (!success && deleteError) {
console.error('Delete failed:', deleteError);
// Refetch to restore the file in case of failure
refetch();
}
}
};
// Configure action buttons
const actions: TableAction[] = useMemo(() => [
{
label: t('files.action.edit', 'Edit'),
icon: <MdModeEdit />,
onClick: (row: UserFile) => {
handleEditFile(row);
}
},
{
label: t('files.action.download'),
icon: (row: UserFile) => {
const isDownloadingThis = downloadingFiles.has(row.id);
if (isDownloadingThis) return '⏳';
return <IoIosDownload />;
},
onClick: (row: UserFile) => {
if (!downloadingFiles.has(row.id)) {
handleDownload(row);
}
}
},
{
label: t('files.action.delete'),
icon: (row: UserFile) => {
const isDeletingThis = deletingFiles.has(row.id);
if (isDeletingThis) return '⏳';
return <IoIosTrash />;
},
onClick: (row: UserFile) => {
if (!deletingFiles.has(row.id)) {
handleDelete(row);
}
}
}
], [t, downloadingFiles, deletingFiles, handleDownload, handleDelete]);
return {
files,
loading,
error,
refetch,
columns,
actions,
downloadingFiles,
deletingFiles,
downloadError,
deleteError,
editModalOpen,
editingFile,
editFileFields,
handleEditFile,
handleSaveFile,
handleCancelEdit
};
}

View file

@ -0,0 +1,3 @@
export { default as DateienTable } from './DateienTable';
export { useDateienLogic } from './dateienLogic.tsx';
export * from './dateienInterfaces';

View file

@ -328,35 +328,45 @@
transform: scale(1.2);
}
/* Actions Column */
/* Actions Column - Resizable like other columns */
.actionsColumn {
white-space: nowrap;
text-align: center;
padding: 12px 8px !important;
text-align: left;
padding: 8px !important;
font-weight: 400;
box-sizing: border-box;
}
/* Actions Column header */
thead .actionsColumn {
text-align: center;
padding: 8px !important;
}
/* Actions Column border only on body cells, not header */
tbody .actionsColumn {
border-top: 1px solid var(--color-primary);
text-align: center;
}
.actionButtons {
display: flex;
gap: 4px;
gap: 2px;
justify-content: center;
align-items: center;
width: fit-content;
width: 100%;
margin: 0 auto;
}
.actionButton {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
padding: 6px;
border: none;
border-radius: 50%;
font-size: 12px;
@ -365,16 +375,14 @@ tbody .actionsColumn {
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
min-width: 32px;
min-height: 32px;
min-width: 28px;
min-height: 28px;
background: var(--color-secondary);
color: var(--color-bg);
}
.actionButton:hover {
background: var(--color-secondary-hover);
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.actionIcon {
@ -386,43 +394,6 @@ tbody .actionsColumn {
justify-content: center;
}
/* Custom Tooltip */
.tooltip {
position: absolute;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
background: var(--color-text);
color: var(--color-bg);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
z-index: 1000;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Tooltip arrow */
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: var(--color-text);
}
/* Show tooltip on button hover */
.actionButton:hover .tooltip {
opacity: 1;
visibility: visible;
}
/* Pagination */
.pagination {
display: flex;
@ -575,4 +546,30 @@ tbody .actionsColumn {
.tableContainer::-webkit-scrollbar-thumb:hover {
background: var(--color-secondary);
}
/* Loading State */
.loadingState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
color: var(--color-text-secondary, #666);
}
.loadingSpinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-bg-secondary, #e9ecef);
border-top: 3px solid var(--color-primary, #007bff);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View file

@ -1,4 +1,5 @@
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './FormGenerator.module.css';
// Types for the FormGenerator
@ -55,6 +56,7 @@ export function FormGenerator<T extends Record<string, any>>({
actions = [],
className = ''
}: FormGeneratorProps<T>) {
const { t } = useLanguage();
// Auto-detect columns if not provided
const detectedColumns = useMemo((): ColumnConfig[] => {
if (providedColumns) return providedColumns;
@ -108,12 +110,18 @@ export function FormGenerator<T extends Record<string, any>>({
// Initialize column widths
useEffect(() => {
const initialWidths: Record<string, number> = {};
// Add actions column if present
if (actions.length > 0) {
initialWidths['actions'] = 120; // Default width for actions column
}
detectedColumns.forEach(col => {
// Set a default width if none specified to ensure all columns have explicit widths
initialWidths[col.key] = col.width || 150;
});
setColumnWidths(initialWidths);
}, [detectedColumns]);
}, [detectedColumns, actions]);
// Filter and search data
const filteredData = useMemo(() => {
@ -143,15 +151,6 @@ export function FormGenerator<T extends Record<string, any>>({
} else if (column?.type === 'number') {
return Number(value) === Number(filterValue);
} else if (column?.type === 'date') {
// Convert DD.MM.YYYY to comparable format
const parseDate = (dateStr: string) => {
if (dateStr.includes('.')) {
const [day, month, year] = dateStr.split('.');
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
return new Date(dateStr);
};
// Convert row value to DD.MM.YYYY format for comparison
const rowDate = new Date(value);
const rowFormatted = `${rowDate.getDate().toString().padStart(2, '0')}.${(rowDate.getMonth() + 1).toString().padStart(2, '0')}.${rowDate.getFullYear()}`;
@ -281,7 +280,7 @@ export function FormGenerator<T extends Record<string, any>>({
const tableContainer = tableRef.current?.parentElement;
if (tableContainer) {
const containerWidth = tableContainer.clientWidth;
const actionsColumnWidth = actions.length > 0 ? 120 : 0;
const actionsColumnWidth = 0; // Actions column is now resizable like other columns
const selectColumnWidth = selectable ? 40 : 0;
const fixedWidth = actionsColumnWidth + selectColumnWidth;
@ -334,6 +333,8 @@ export function FormGenerator<T extends Record<string, any>>({
return (
<div className={`${styles.formGenerator} ${className}`}>
{(searchable || filterable) && (
<div className={styles.controls}>
{searchable && (
@ -348,7 +349,7 @@ export function FormGenerator<T extends Record<string, any>>({
onBlur={() => setSearchFocused(false)}
className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`}
/>
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>Search...</label>
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>{t('formgen.search.placeholder')}</label>
</div>
</div>
)}
@ -365,15 +366,15 @@ export function FormGenerator<T extends Record<string, any>>({
className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`}
>
<option value="" disabled hidden>{column.label}</option>
<option value="true">Yes</option>
<option value="false">No</option>
<option value="true">{t('formgen.filter.yes')}</option>
<option value="false">{t('formgen.filter.no')}</option>
</select>
{filters[column.key] && (
<button
type="button"
onClick={() => handleFilter(column.key, '')}
className={styles.clearFilterButton}
title="Clear filter"
title={t('formgen.filter.clear')}
>
</button>
@ -397,7 +398,7 @@ export function FormGenerator<T extends Record<string, any>>({
type="button"
onClick={() => handleFilter(column.key, '')}
className={styles.clearFilterButton}
title="Clear filter"
title={t('formgen.filter.clear')}
>
</button>
@ -490,7 +491,7 @@ export function FormGenerator<T extends Record<string, any>>({
className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`}
/>
<label className={filterFocused[column.key] || filters[column.key] ? styles.focusedLabel : styles.label}>
Filter {column.label}
{t('formgen.filter.placeholder').replace('{column}', column.label)}
</label>
</div>
)}
@ -503,10 +504,33 @@ export function FormGenerator<T extends Record<string, any>>({
{/* Table */}
<div className={styles.tableContainer}>
{(
{loading ? (
<div className={styles.loadingState}>
<div className={styles.loadingSpinner}></div>
<p>{t('common.loading', 'Loading...')}</p>
</div>
) : (
<table ref={tableRef} className={styles.table}>
<thead>
<tr>
{actions.length > 0 && (
<th
className={`${styles.actionsColumn} ${styles.th} ${resizable ? styles.sortable : ''}`}
style={{
width: columnWidths['actions'] || 120,
minWidth: columnWidths['actions'] || 120,
maxWidth: columnWidths['actions'] || 120
}}
>
{resizable && (
<div
className={styles.resizeHandle}
onMouseDown={(e) => handleMouseDown(e, 'actions')}
/>
)}
</th>
)}
{selectable && (
<th className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
@ -516,12 +540,6 @@ export function FormGenerator<T extends Record<string, any>>({
/>
</th>
)}
{actions.length > 0 && (
<th className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
Actions
</th>
)}
{detectedColumns.map(column => (
<th
key={column.key}
@ -560,18 +578,15 @@ export function FormGenerator<T extends Record<string, any>>({
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
onClick={() => onRowClick?.(row, index)}
>
{selectable && (
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={selectedRows.has(index)}
onChange={() => handleRowSelect(index)}
onClick={(e) => e.stopPropagation()}
/>
</td>
)}
{actions.length > 0 && (
<td className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
{actions.length > 0 && (
<td
className={styles.actionsColumn}
style={{
width: columnWidths['actions'] || 120,
minWidth: columnWidths['actions'] || 120,
maxWidth: columnWidths['actions'] || 120
}}
>
<div className={styles.actionButtons}>
{actions.map((action, actionIndex) => (
<button
@ -588,12 +603,21 @@ export function FormGenerator<T extends Record<string, any>>({
{typeof action.icon === 'function' ? action.icon(row) : action.icon}
</span>
)}
<span className={styles.tooltip}>{action.label}</span>
</button>
))}
</div>
</td>
)}
{selectable && (
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={selectedRows.has(index)}
onChange={() => handleRowSelect(index)}
onClick={(e) => e.stopPropagation()}
/>
</td>
)}
{detectedColumns.map(column => (
<td
key={column.key}
@ -622,6 +646,7 @@ export function FormGenerator<T extends Record<string, any>>({
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className={styles.paginationButton}
title={t('formgen.pagination.first')}
>
««
</button>
@ -629,18 +654,23 @@ export function FormGenerator<T extends Record<string, any>>({
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
className={styles.paginationButton}
title={t('formgen.pagination.prev')}
>
«
</button>
<span className={styles.paginationInfo}>
Page {currentPage} of {totalPages} ({filteredData.length} items)
{t('formgen.pagination.info')
.replace('{page}', currentPage.toString())
.replace('{total}', totalPages.toString())
.replace('{count}', filteredData.length.toString())}
</span>
<button
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
className={styles.paginationButton}
title={t('formgen.pagination.next')}
>
»
</button>
@ -648,6 +678,7 @@ export function FormGenerator<T extends Record<string, any>>({
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className={styles.paginationButton}
title={t('formgen.pagination.last')}
>
»»
</button>

View file

@ -2,7 +2,11 @@
.sidebarContainer {
border-radius: 0px;
background: var(--color-bg);
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
/*background-image: url('../../../assets/styles/bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);*/
width: 240px;
padding-bottom: 1px;
display: flex;

View file

@ -0,0 +1,189 @@
.workflowsTable {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.workflowsFormGenerator {
flex: 1;
height: 100%;
}
/* Error state styling */
.errorState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
text-align: center;
color: var(--color-error, #dc3545);
background-color: var(--color-error-bg, #f8d7da);
border: 1px solid var(--color-error-border, #f5c6cb);
border-radius: 8px;
margin: 1rem;
}
.retryButton {
padding: 0.5rem 1rem;
background-color: var(--color-primary, #007bff);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
transition: background-color 0.2s ease;
}
.retryButton:hover {
background-color: var(--color-primary-dark, #0056b3);
}
/* Table cell styling */
.workflowId {
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: var(--color-text-secondary, #666);
cursor: help;
}
.workflowName {
font-weight: 500;
color: var(--color-text-primary, #333);
}
.statusBadge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
text-transform: capitalize;
text-align: center;
min-width: 60px;
}
.status-running {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-completed {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.status-failed {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-stopped {
background-color: #f5f5f5;
color: #495057;
border: 1px solid #dee2e6;
}
.status-pending {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.roundNumber {
font-weight: 600;
color: var(--color-primary, #007bff);
background-color: var(--color-primary-light, #e3f2fd);
padding: 0.25rem 0.5rem;
border-radius: 8px;
font-size: 0.9em;
min-width: 24px;
text-align: center;
display: inline-block;
}
.messageCount {
font-weight: 500;
color: var(--color-text-secondary, #666);
background-color: var(--color-bg-secondary, #f8f9fa);
padding: 0.25rem 0.5rem;
border-radius: 8px;
font-size: 0.9em;
min-width: 24px;
text-align: center;
display: inline-block;
}
/* Responsive design */
@media (max-width: 768px) {
.workflowId {
font-size: 0.8em;
}
.statusBadge {
font-size: 0.8em;
padding: 0.2rem 0.4rem;
}
.roundNumber,
.messageCount {
font-size: 0.8em;
padding: 0.2rem 0.4rem;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.workflowId {
color: #adb5bd;
}
.workflowName {
color: #f8f9fa;
}
.status-running {
background-color: #155724;
color: #d4edda;
}
.status-completed {
background-color: #0c5460;
color: #d1ecf1;
}
.status-failed {
background-color: #721c24;
color: #f8d7da;
}
.status-stopped {
background-color: #495057;
color: #f5f5f5;
}
.status-pending {
background-color: #856404;
color: #fff3cd;
}
.roundNumber {
background-color: #1e3a8a;
color: #bfdbfe;
}
.messageCount {
background-color: #374151;
color: #d1d5db;
}
.errorState {
background-color: #2d1b1e;
border-color: #5c2b33;
color: #f5c6cb;
}
}

View file

@ -0,0 +1,227 @@
import { useMemo } from 'react';
import { FormGenerator, ColumnConfig } from '../FormGenerator/FormGenerator';
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './WorkflowsTable.module.css';
interface WorkflowsTableProps {
className?: string;
}
function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
const { workflows, loading, error, refetch } = useWorkflows();
const {
stopWorkflow,
deleteWorkflow,
stoppingWorkflows,
deletingWorkflows
} = useWorkflowOperations();
const { t } = useLanguage();
// Configure columns for the workflows table
const columns: ColumnConfig[] = useMemo(() => [
{
key: 'id',
label: t('workflows.column.id'),
type: 'string',
width: 180,
minWidth: 150,
maxWidth: 250,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string) => (
<span className={styles.workflowId} title={value}>
{value.length > 8 ? `${value.substring(0, 8)}...` : value}
</span>
)
},
{
key: 'name',
label: t('workflows.column.name'),
type: 'string',
width: 200,
minWidth: 150,
maxWidth: 300,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string | undefined, row: Workflow) => (
<span className={styles.workflowName}>
{value || row.title || t('workflows.unnamed')}
</span>
)
},
{
key: 'status',
label: t('workflows.column.status'),
type: 'enum',
width: 120,
minWidth: 100,
maxWidth: 150,
sortable: true,
filterable: true,
filterOptions: ['running', 'completed', 'failed', 'stopped', 'pending'],
formatter: (value: string) => (
<span className={`${styles.statusBadge} ${styles[`status-${value}`]}`}>
{t(`workflows.status.${value}`, value)}
</span>
)
},
{
key: 'currentRound',
label: t('workflows.column.round'),
type: 'number',
width: 80,
minWidth: 60,
maxWidth: 100,
sortable: true,
filterable: true,
formatter: (value: number | undefined) => (
<span className={styles.roundNumber}>
{value || 1}
</span>
)
},
{
key: 'startedAt',
label: t('workflows.column.started'),
type: 'date',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | undefined) => {
if (!value) return '-';
try {
const date = new Date(value);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return value;
}
}
},
{
key: 'lastActivity',
label: t('workflows.column.lastActivity'),
type: 'date',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | undefined) => {
if (!value) return '-';
try {
const date = new Date(value);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return value;
}
}
},
{
key: 'messages',
label: t('workflows.column.messages'),
type: 'number',
width: 100,
minWidth: 80,
maxWidth: 120,
sortable: true,
filterable: false,
formatter: (value: any[] | undefined) => (
<span className={styles.messageCount}>
{value?.length || 0}
</span>
)
}
], [t]);
// Handle workflow actions
const handleStopWorkflow = async (workflow: Workflow) => {
const success = await stopWorkflow(workflow.id);
if (success) {
refetch(); // Refresh the workflows list
}
};
const handleDeleteWorkflow = async (workflow: Workflow) => {
const workflowName = workflow.name || workflow.title || workflow.id;
if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) {
const success = await deleteWorkflow(workflow.id);
if (success) {
refetch(); // Refresh the workflows list
}
}
};
// Configure action buttons
const actions = useMemo(() => [
{
label: t('workflows.action.stop'),
icon: (row: Workflow) => {
const isStoppingThis = stoppingWorkflows.has(row.id);
if (isStoppingThis) return '⏳';
return '⏹️';
},
onClick: (row: Workflow) => {
if (row.status === 'running' && !stoppingWorkflows.has(row.id)) {
handleStopWorkflow(row);
}
}
},
{
label: t('workflows.action.delete'),
icon: (row: Workflow) => {
const isDeletingThis = deletingWorkflows.has(row.id);
if (isDeletingThis) return '⏳';
return '🗑️';
},
onClick: (row: Workflow) => {
if (!deletingWorkflows.has(row.id)) {
handleDeleteWorkflow(row);
}
}
}
], [t, stoppingWorkflows, deletingWorkflows, handleStopWorkflow, handleDeleteWorkflow]);
// Show error state
if (error) {
return (
<div className={`${styles.workflowsTable} ${className}`}>
<div className={styles.errorState}>
<p>{t('workflows.error.loading')} {error}</p>
<button onClick={refetch} className={styles.retryButton}>
{t('workflows.button.retry')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.workflowsTable} ${className}`}>
<FormGenerator
data={workflows}
columns={columns}
title={t('workflows.table.title')}
searchable={true}
filterable={true}
sortable={true}
resizable={true}
pagination={true}
pageSize={10}
loading={loading}
actions={actions}
className={styles.workflowsFormGenerator}
onRowClick={(workflow: Workflow) => {
// TODO: Navigate to workflow detail view
console.log('Clicked workflow:', workflow);
}}
/>
</div>
);
}
export default WorkflowsTable;

View file

@ -0,0 +1 @@
export { default as WorkflowsTable } from './WorkflowsTable';

View file

@ -3,21 +3,21 @@ import { useApiRequest } from './useApi';
// File interfaces
export interface FileInfo {
id: number;
name: string;
id: string;
filename: string;
mimeType: string;
size?: number;
fileSize: number;
creationDate: string;
fileHash?: string;
mandateId?: number;
userId?: number;
mandateId?: string;
workflowId?: string;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
}
export interface UserFile {
id: number;
id: string;
file_name: string;
mime_type?: string;
action: string;
created_at: string;
size?: number;
@ -32,7 +32,7 @@ export function useUserFiles() {
const fetchFiles = async () => {
try {
const data = await request({
url: '/api/files',
url: '/api/files/list',
method: 'get'
});
@ -73,10 +73,11 @@ export function useUserFiles() {
return {
id: apiFile.id,
file_name: apiFile.name,
file_name: apiFile.filename,
mime_type: apiFile.mimeType,
action: action,
created_at: apiFile.creationDate,
size: apiFile.size,
size: apiFile.fileSize,
source: apiFile.source
};
});
@ -88,7 +89,7 @@ export function useUserFiles() {
};
// Optimistically remove a file from the local state
const removeFileOptimistically = (fileId: number) => {
const removeFileOptimistically = (fileId: string) => {
setFiles(prevFiles => prevFiles.filter(file => file.id !== fileId));
};
@ -113,21 +114,21 @@ export function useUserFiles() {
// File operations hook
export function useFileOperations() {
const [downloadingFiles, setDownloadingFiles] = useState<Set<number>>(new Set());
const [deletingFiles, setDeletingFiles] = useState<Set<number>>(new Set());
const [downloadingFiles, setDownloadingFiles] = useState<Set<string>>(new Set());
const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set());
const [uploadingFile, setUploadingFile] = useState(false);
const { request, error: apiError, isLoading } = useApiRequest();
const { request, isLoading } = useApiRequest();
const [downloadError, setDownloadError] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [uploadError, setUploadError] = useState<string | null>(null);
const handleFileDownload = async (fileId: number, fileName: string) => {
const handleFileDownload = async (fileId: string, fileName: string) => {
setDownloadError(null);
setDownloadingFiles(prev => new Set(prev).add(fileId));
try {
const blob = await request({
url: `/api/files/${fileId}`,
url: `/api/files/${fileId}/download`,
method: 'get',
// Override axios config for blob response
additionalConfig: { responseType: 'blob' }
@ -156,7 +157,7 @@ export function useFileOperations() {
}
};
const handleFileDelete = async (fileId: number, onOptimisticDelete?: () => void) => {
const handleFileDelete = async (fileId: string, onOptimisticDelete?: () => void) => {
setDeleteError(null);
setDeletingFiles(prev => new Set(prev).add(fileId));
@ -220,6 +221,23 @@ export function useFileOperations() {
}
};
const handleFileUpdate = async (fileId: string, updateData: Partial<{ filename: string }>) => {
setUploadError(null); // Reuse upload error state for update operations
try {
const updatedFile = await request({
url: `/api/files/${fileId}`,
method: 'put',
data: updateData
});
return { success: true, fileData: updatedFile };
} catch (error: any) {
setUploadError(error.message);
return { success: false, error: error.message };
}
};
return {
downloadingFiles,
deletingFiles,
@ -230,6 +248,7 @@ export function useFileOperations() {
handleFileDownload,
handleFileDelete,
handleFileUpload,
handleFileUpdate,
isLoading
};
}

View file

@ -1,18 +1,23 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect } from 'react';
import { useApiRequest } from './useApi';
// Workflow interfaces
// Workflow interfaces (matching backend ChatWorkflow model)
export interface Workflow {
id: string;
name?: string;
title?: string;
mandateId: string;
status: string;
startedAt?: string;
lastActivity?: string;
currentRound?: number;
dataStats?: Record<string, any>;
userId?: number;
messageIds?: string[];
name?: string;
title?: string; // Keep for backward compatibility
currentRound: number;
lastActivity: string;
startedAt: string;
logs?: any[];
messages?: any[];
stats?: Record<string, any>;
tasks?: any[];
dataStats?: Record<string, any>; // Keep for backward compatibility
userId?: number; // Keep for backward compatibility
messageIds?: string[]; // Keep for backward compatibility
}
export interface WorkflowMessage {
@ -43,7 +48,7 @@ export function useWorkflows() {
const fetchWorkflows = async () => {
try {
const data = await request({
url: '/api/workflows',
url: '/api/workflows/',
method: 'get'
});
@ -65,7 +70,7 @@ export function useWorkflowOperations() {
const [startingWorkflow, setStartingWorkflow] = useState(false);
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
const { request, error: apiError, isLoading } = useApiRequest();
const { request, isLoading } = useApiRequest();
const [startError, setStartError] = useState<string | null>(null);
const [stopError, setStopError] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
@ -274,10 +279,10 @@ export function useFilePreview() {
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
}
const response = await request({
url: `/api/workflows/files/${numericFileId}/preview`,
method: 'get'
});
const response = await request({
url: `/api/files/${numericFileId}/preview`,
method: 'get'
});
// Handle response as object with metadata and preview content
if (typeof response === 'object' && response !== null) {
@ -315,7 +320,7 @@ export function useFilePreview() {
try {
// Try to fetch the raw file content using download endpoint
const rawResponse = await request({
url: `/api/workflows/files/${numericFileId}/download`,
url: `/api/files/${numericFileId}/download`,
method: 'get',
additionalConfig: { responseType: 'text' }
});
@ -389,7 +394,7 @@ export function useFileDownload() {
// Use the same approach as useFiles.ts - use request with blob response type
const blob = await request({
url: `/api/workflows/files/${numericFileId}/download`,
url: `/api/files/${numericFileId}/download`,
method: 'get',
// Override axios config for blob response
additionalConfig: { responseType: 'blob' }

View file

@ -253,6 +253,10 @@ export default {
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
// Files Page
'files.title': 'Dateien',
'files.table.title': 'Dateien',
'files.error.loading': 'Fehler beim Laden der Dateien:',
'files.button.retry': 'Wiederholen',
'files.page.tab.all': 'Alle Dateien',
'files.page.tab.uploads': 'Meine Uploads',
'files.page.tab.created': 'Erstellte Dateien',
@ -260,4 +264,78 @@ export default {
'files.page.add_file': 'Datei hinzufügen',
'files.page.loading': 'Dateien werden geladen...',
'files.page.error': 'Fehler:',
// File Table Columns
'files.column.name': 'Name',
'files.column.type': 'Typ',
'files.column.size': 'Größe',
'files.column.created': 'Erstellt',
'files.column.source': 'Quelle',
// File Types
'files.type.image': 'Bild',
'files.type.pdf': 'PDF',
'files.type.document': 'Dokument',
'files.type.spreadsheet': 'Tabelle',
'files.type.text': 'Text',
'files.type.video': 'Video',
'files.type.audio': 'Audio',
'files.type.file': 'Datei',
// File Sources
'files.source.uploaded': 'Hochgeladen',
'files.source.created': 'KI Erstellt',
'files.source.shared': 'Geteilt',
// File Actions
'files.action.download': 'Herunterladen',
'files.action.delete': 'Löschen',
'files.delete.confirm': 'Sind Sie sicher, dass Sie die Datei "{name}" löschen möchten?',
// Workflows Page
'workflows.title': 'Workflows',
'workflows.table.title': 'Workflows',
'workflows.error.loading': 'Fehler beim Laden der Workflows:',
'workflows.button.retry': 'Wiederholen',
'workflows.table.empty': 'Keine Workflows gefunden',
// Workflow Table Columns
'workflows.column.id': 'ID',
'workflows.column.name': 'Name',
'workflows.column.status': 'Status',
'workflows.column.round': 'Runde',
'workflows.column.started': 'Gestartet',
'workflows.column.lastActivity': 'Letzte Aktivität',
'workflows.column.messages': 'Nachrichten',
// Workflow Status
'workflows.status.running': 'Läuft',
'workflows.status.completed': 'Abgeschlossen',
'workflows.status.failed': 'Fehlgeschlagen',
'workflows.status.stopped': 'Gestoppt',
'workflows.status.pending': 'Wartend',
// Workflow Actions
'workflows.action.stop': 'Stoppen',
'workflows.action.delete': 'Löschen',
'workflows.action.stop.tooltip': 'Workflow stoppen',
'workflows.action.delete.tooltip': 'Workflow löschen',
// Workflow Messages
'workflows.unnamed': 'Unbenannter Workflow',
'workflows.delete.confirm': 'Sind Sie sicher, dass Sie den Workflow "{name}" löschen möchten?',
'workflows.loading': 'Workflows werden geladen...',
// FormGenerator
'formgen.search.placeholder': 'Suchen...',
'formgen.filter.yes': 'Ja',
'formgen.filter.no': 'Nein',
'formgen.filter.clear': 'Filter löschen',
'formgen.filter.placeholder': '{column} filtern',
'formgen.actions.column': 'Aktionen',
'formgen.pagination.info': 'Seite {page} von {total} ({count} Einträge)',
'formgen.pagination.first': 'Erste Seite',
'formgen.pagination.prev': 'Vorherige Seite',
'formgen.pagination.next': 'Nächste Seite',
'formgen.pagination.last': 'Letzte Seite',
};

View file

@ -254,6 +254,10 @@ export default {
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
// Files Page
'files.title': 'Files',
'files.table.title': 'Files',
'files.error.loading': 'Error loading files:',
'files.button.retry': 'Retry',
'files.page.tab.all': 'All Files',
'files.page.tab.uploads': 'My Uploads',
'files.page.tab.created': 'Created Files',
@ -261,4 +265,78 @@ export default {
'files.page.add_file': 'Add File',
'files.page.loading': 'Loading files...',
'files.page.error': 'Error:',
// File Table Columns
'files.column.name': 'Name',
'files.column.type': 'Type',
'files.column.size': 'Size',
'files.column.created': 'Created',
'files.column.source': 'Source',
// File Types
'files.type.image': 'Image',
'files.type.pdf': 'PDF',
'files.type.document': 'Document',
'files.type.spreadsheet': 'Spreadsheet',
'files.type.text': 'Text',
'files.type.video': 'Video',
'files.type.audio': 'Audio',
'files.type.file': 'File',
// File Sources
'files.source.uploaded': 'Uploaded',
'files.source.created': 'AI Created',
'files.source.shared': 'Shared',
// File Actions
'files.action.download': 'Download',
'files.action.delete': 'Delete',
'files.delete.confirm': 'Are you sure you want to delete the file "{name}"?',
// Workflows Page
'workflows.title': 'Workflows',
'workflows.table.title': 'Workflows',
'workflows.error.loading': 'Error loading workflows:',
'workflows.button.retry': 'Retry',
'workflows.table.empty': 'No workflows found',
// Workflow Table Columns
'workflows.column.id': 'ID',
'workflows.column.name': 'Name',
'workflows.column.status': 'Status',
'workflows.column.round': 'Round',
'workflows.column.started': 'Started',
'workflows.column.lastActivity': 'Last Activity',
'workflows.column.messages': 'Messages',
// Workflow Status
'workflows.status.running': 'Running',
'workflows.status.completed': 'Completed',
'workflows.status.failed': 'Failed',
'workflows.status.stopped': 'Stopped',
'workflows.status.pending': 'Pending',
// Workflow Actions
'workflows.action.stop': 'Stop',
'workflows.action.delete': 'Delete',
'workflows.action.stop.tooltip': 'Stop workflow',
'workflows.action.delete.tooltip': 'Delete workflow',
// Workflow Messages
'workflows.unnamed': 'Unnamed Workflow',
'workflows.delete.confirm': 'Are you sure you want to delete workflow "{name}"?',
'workflows.loading': 'Loading workflows...',
// FormGenerator
'formgen.search.placeholder': 'Search...',
'formgen.filter.yes': 'Yes',
'formgen.filter.no': 'No',
'formgen.filter.clear': 'Clear filter',
'formgen.filter.placeholder': 'Filter {column}',
'formgen.actions.column': 'Actions',
'formgen.pagination.info': 'Page {page} of {total} ({count} items)',
'formgen.pagination.first': 'First page',
'formgen.pagination.prev': 'Previous page',
'formgen.pagination.next': 'Next page',
'formgen.pagination.last': 'Last page',
};

View file

@ -253,6 +253,10 @@ export default {
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
// Files Page
'files.title': 'Fichiers',
'files.table.title': 'Fichiers',
'files.error.loading': 'Erreur lors du chargement des fichiers:',
'files.button.retry': 'Réessayer',
'files.page.tab.all': 'Tous les fichiers',
'files.page.tab.uploads': 'Mes téléchargements',
'files.page.tab.created': 'Fichiers créés',
@ -260,4 +264,78 @@ export default {
'files.page.add_file': 'Ajouter un fichier',
'files.page.loading': 'Chargement des fichiers...',
'files.page.error': 'Erreur:',
// File Table Columns
'files.column.name': 'Nom',
'files.column.type': 'Type',
'files.column.size': 'Taille',
'files.column.created': 'Créé',
'files.column.source': 'Source',
// File Types
'files.type.image': 'Image',
'files.type.pdf': 'PDF',
'files.type.document': 'Document',
'files.type.spreadsheet': 'Feuille de calcul',
'files.type.text': 'Texte',
'files.type.video': 'Vidéo',
'files.type.audio': 'Audio',
'files.type.file': 'Fichier',
// File Sources
'files.source.uploaded': 'Téléchargé',
'files.source.created': 'Créé par IA',
'files.source.shared': 'Partagé',
// File Actions
'files.action.download': 'Télécharger',
'files.action.delete': 'Supprimer',
'files.delete.confirm': 'Êtes-vous sûr de vouloir supprimer le fichier "{name}"?',
// Workflows Page
'workflows.title': 'Workflows',
'workflows.table.title': 'Workflows',
'workflows.error.loading': 'Erreur lors du chargement des workflows:',
'workflows.button.retry': 'Réessayer',
'workflows.table.empty': 'Aucun workflow trouvé',
// Workflow Table Columns
'workflows.column.id': 'ID',
'workflows.column.name': 'Nom',
'workflows.column.status': 'Statut',
'workflows.column.round': 'Tour',
'workflows.column.started': 'Démarré',
'workflows.column.lastActivity': 'Dernière activité',
'workflows.column.messages': 'Messages',
// Workflow Status
'workflows.status.running': 'En cours',
'workflows.status.completed': 'Terminé',
'workflows.status.failed': 'Échoué',
'workflows.status.stopped': 'Arrêté',
'workflows.status.pending': 'En attente',
// Workflow Actions
'workflows.action.stop': 'Arrêter',
'workflows.action.delete': 'Supprimer',
'workflows.action.stop.tooltip': 'Arrêter le workflow',
'workflows.action.delete.tooltip': 'Supprimer le workflow',
// Workflow Messages
'workflows.unnamed': 'Workflow sans nom',
'workflows.delete.confirm': 'Êtes-vous sûr de vouloir supprimer le workflow "{name}"?',
'workflows.loading': 'Chargement des workflows...',
// FormGenerator
'formgen.search.placeholder': 'Rechercher...',
'formgen.filter.yes': 'Oui',
'formgen.filter.no': 'Non',
'formgen.filter.clear': 'Effacer le filtre',
'formgen.filter.placeholder': 'Filtrer {column}',
'formgen.actions.column': 'Actions',
'formgen.pagination.info': 'Page {page} sur {total} ({count} éléments)',
'formgen.pagination.first': 'Première page',
'formgen.pagination.prev': 'Page précédente',
'formgen.pagination.next': 'Page suivante',
'formgen.pagination.last': 'Dernière page',
};

View file

@ -1,156 +1,104 @@
import styles from './HomeStyles/Dateien.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
import { IoAddCircleOutline } from "react-icons/io5";
import DateienUpload from '../../components/Dateien/DateienHinzufügen/DateienUploadTool';
import DateienAll from '../../components/Dateien/DateienAll';
import DateienUploads from '../../components/Dateien/DateienUploads';
import DateienCreated from '../../components/Dateien/DateienCreated';
import DateienShared from '../../components/Dateien/DateienShared';
import { useState } from 'react';
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
import { motion, AnimatePresence } from "framer-motion";
import { useLanguage } from '../../contexts/LanguageContext';
// Tab types
type TabType = 'alle' | 'uploads' | 'erstellt' | 'geteilt';
import { useRef, useState } from 'react';
import { IoMdCloudUpload } from 'react-icons/io';
import { useLanguage } from '../../contexts/LanguageContext';
import sharedStyles from './HomeStyles/pages.module.css'
import styles from './HomeStyles/Dateien.module.css'
import { DateienTable } from '../../components/Dateien'
import { useFileOperations } from '../../hooks/useFiles';
function Dateien() {
const { t } = useLanguage();
const { files, loading, error, refetch, removeFileOptimistically } = useUserFiles();
const [isUploadOpen, setIsUploadOpen] = useState(false);
const { uploadError, downloadError, deleteError } = useFileOperations();
const [activeTab, setActiveTab] = useState<TabType>('alle');
const { handleFileUpload, uploadingFile } = useFileOperations();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [tableRefreshKey, setTableRefreshKey] = useState(0);
const [isDragOver, setIsDragOver] = useState(false);
// Tab configuration with translations
const tabs = [
{ key: 'alle' as TabType, label: t('files.page.tab.all', 'All Files') },
{ key: 'uploads' as TabType, label: t('files.page.tab.uploads', 'My Uploads') },
{ key: 'erstellt' as TabType, label: t('files.page.tab.created', 'Created Files') },
{ key: 'geteilt' as TabType, label: t('files.page.tab.shared', 'Shared Files') }
];
// Single function to handle file refresh
const refreshFiles = () => {
console.log('Refreshing files list');
refetch();
const triggerFilePicker = () => {
fileInputRef.current?.click();
};
const handleFileUpload = async (file: File) => {
console.log('File upload completed:', file.name);
};
const onFilesSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (!files || files.length === 0) return;
const handleUploadClose = () => {
setIsUploadOpen(false);
// Refresh files when upload modal is closed
setTimeout(() => {
refreshFiles();
}, 300);
};
const handleFileDeleted = () => {
refreshFiles();
};
// Render the appropriate component based on active tab
const renderActiveTabContent = () => {
const commonProps = {
files,
onFileDeleted: handleFileDeleted,
onOptimisticDelete: removeFileOptimistically
};
switch (activeTab) {
case 'alle':
return <DateienAll {...commonProps} />;
case 'uploads':
return <DateienUploads {...commonProps} />;
case 'erstellt':
return <DateienCreated {...commonProps} />;
case 'geteilt':
return <DateienShared {...commonProps} />;
default:
return <DateienAll {...commonProps} />;
// Upload files sequentially
for (const file of Array.from(files)) {
await handleFileUpload(file);
}
// Force remount DateienTable to refetch
setTableRefreshKey(prev => prev + 1);
// Reset input value to allow re-selecting the same file(s)
event.target.value = '';
};
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
const files = event.dataTransfer.files;
if (files.length === 0) return;
// Upload files sequentially
for (const file of Array.from(files)) {
await handleFileUpload(file);
}
// Force remount DateienTable to refetch
setTableRefreshKey(prev => prev + 1);
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(true);
};
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
};
return (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<h1 className={sharedStyles.pageTitle}>Dateien</h1>
<div className={sharedStyles.horizontalDivider}></div>
{/* Combined Header with Tabs and Add Button */}
<motion.div
className={`${sharedStyles.pageHeader} ${styles.combinedHeader}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className={styles.tabButtonDiv}>
{tabs.map((tab) => (
<div key={tab.key} className={styles.tabButtonWrapper}>
<motion.button
className={`${styles.tabButton} ${
activeTab === tab.key ? styles.tabButtonActive : styles.tabButtonInactive
}`}
onClick={() => setActiveTab(tab.key)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{tab.label}
</motion.button>
<AnimatePresence>
{activeTab === tab.key && (
<motion.div
className={styles.tabUnderline}
initial={{ opacity: 0, width: "0%" }}
animate={{ opacity: 1, width: "100%" }}
exit={{ opacity: 0, width: "0%" }}
transition={{ duration: 0.3, ease: "easeOut" }}
/>
)}
</AnimatePresence>
</div>
))}
<div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>{t('files.title')}</h1>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
<div
className={`${styles.dropZone} ${isDragOver ? styles.dropZoneActive : ''}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={triggerFilePicker}
>
<span className={styles.dropZoneText}>
{isDragOver ? 'Drop files here' : 'Drop files here or click to browse'}
</span>
</div>
<button
className={sharedStyles.primaryButton}
onClick={triggerFilePicker}
disabled={uploadingFile}
aria-label="Upload files"
>
<span className={sharedStyles.buttonIcon}><IoMdCloudUpload /></span>
{uploadingFile ? 'Uploading...' : 'Upload Files'}
</button>
<input
ref={fileInputRef}
type="file"
multiple
style={{ display: 'none' }}
onChange={onFilesSelected}
/>
</div>
<button
className={`${sharedStyles.primaryButton} ${styles.datei_hinzufügen_button}`}
onClick={() => setIsUploadOpen(true)}
>
<IoAddCircleOutline className={sharedStyles.buttonIcon}/>
{t('files.page.add_file', 'Add File')}
</button>
</motion.div>
</div>
<div className={sharedStyles.horizontalDivider}></div>
<div className={`${sharedStyles.contentArea} ${styles.contentArea}`}>
<DateienUpload
isOpen={isUploadOpen}
onClose={handleUploadClose}
onFileUpload={handleFileUpload}
/>
{(uploadError || downloadError || deleteError) && (
<p className={styles.error}>
{uploadError || downloadError || deleteError}
</p>
)}
{loading && <p>{t('files.page.loading', 'Loading files...')}</p>}
{error && <p>{t('files.page.error', 'Error:')} {error}</p>}
{!loading && !error && (
<motion.div
key={activeTab} // Force re-render when tab changes
initial={{ opacity: 0}}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{renderActiveTabContent()}
</motion.div>
)}
<div className={sharedStyles.contentArea}>
<DateienTable key={tableRefreshKey} className={styles.dateienTableContainer} />
</div>
</div>
</div>

View file

@ -77,3 +77,49 @@
/* Any Dateien-specific content area customizations go here */
/* Base styling now comes from sharedStyles.contentArea */
}
/* Dateien table container */
.dateienTableContainer {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
min-height: 400px; /* Ensure minimum height for table functionality */
}
/* Drop zone styles */
.dropZone {
border: 2px dashed var(--color-primary);
border-radius: 30px;
padding: 15px 20px;
background: var(--color-bg);
cursor: pointer;
transition: all 0.2s ease;
min-width: 200px;
text-align: center;
}
.dropZone:hover {
border-color: var(--color-secondary);
background: var(--color-gray-disabled);
}
.dropZoneActive {
border-color: var(--color-secondary);
background: var(--color-gray-disabled);
border-style: solid;
}
.dropZoneText {
color: var(--color-text);
font-size: 14px;
font-family: var(--font-family);
font-weight: 400;
opacity: 0.8;
}
.dropZoneActive .dropZoneText {
opacity: 1;
font-weight: 500;
}

View file

@ -1,6 +1,6 @@
.homeContainer {
position: relative;
background-color: var(--color-surface);
background-color: var(--color-bg);
min-height: 100vh;
max-height: 100vh;
width: 100vw;
@ -14,7 +14,6 @@
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background-image: radial-gradient(circle, var(--color-gray-disabled) 1px, transparent 1px);
background-size: 8px 8px;
opacity: 0.4;
z-index: -1;

View file

@ -6,7 +6,17 @@
/* Remove old .workflowsContainer - now using sharedStyles.pageContainer + sharedStyles.pageCard */
/* Workflow-specific styles go here */
/* Workflow table container */
.workflowsTableContainer {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
min-height: 400px; /* Ensure minimum height for table functionality */
}
/* Future workflow-specific styles go here */
.workflowItem {
/* Future workflow item styling */
}

View file

@ -1,16 +1,19 @@
import styles from './HomeStyles/Workflows.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
import { WorkflowsTable } from '../../components/Workflows'
import { useLanguage } from '../../contexts/LanguageContext'
function Workflows () {
const { t } = useLanguage()
return (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<h1 className={sharedStyles.pageTitle}>Workflows</h1>
<h1 className={sharedStyles.pageTitle}>{t('workflows.title')}</h1>
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
{/* Workflow content will go here */}
<p>Workflow management coming soon...</p>
<WorkflowsTable className={styles.workflowsTableContainer} />
</div>
</div>
</div>