diff --git a/src/assets/styles/bg.jpg b/src/assets/styles/bg.jpg new file mode 100644 index 0000000..cea4869 Binary files /dev/null and b/src/assets/styles/bg.jpg differ diff --git a/src/components/Connections/ConnectionEditModal.tsx b/src/components/Connections/ConnectionEditModal.tsx index 8c1b529..789ac39 100644 --- a/src/components/Connections/ConnectionEditModal.tsx +++ b/src/components/Connections/ConnectionEditModal.tsx @@ -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({ diff --git a/src/components/Connections/ConnectionsErrorDisplay.tsx b/src/components/Connections/ConnectionsErrorDisplay.tsx index 16ebed0..813b51a 100644 --- a/src/components/Connections/ConnectionsErrorDisplay.tsx +++ b/src/components/Connections/ConnectionsErrorDisplay.tsx @@ -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({ diff --git a/src/components/Connections/ConnectionsTable.tsx b/src/components/Connections/ConnectionsTable.tsx index 599967d..5ea1543 100644 --- a/src/components/Connections/ConnectionsTable.tsx +++ b/src/components/Connections/ConnectionsTable.tsx @@ -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({ diff --git a/src/components/Connections/interfaces.ts b/src/components/Connections/connectionsInterfaces.ts similarity index 100% rename from src/components/Connections/interfaces.ts rename to src/components/Connections/connectionsInterfaces.ts diff --git a/src/components/Connections/logic.tsx b/src/components/Connections/connectionsLogic.tsx similarity index 99% rename from src/components/Connections/logic.tsx rename to src/components/Connections/connectionsLogic.tsx index 5118ec7..8268ae6 100644 --- a/src/components/Connections/logic.tsx +++ b/src/components/Connections/connectionsLogic.tsx @@ -13,7 +13,7 @@ import { CreateConnectionData, ConnectionsLogicReturn, TableAction -} from './interfaces'; +} from './connectionsInterfaces'; export function useConnectionsLogic(): ConnectionsLogicReturn { const { t } = useLanguage(); diff --git a/src/components/Connections/index.ts b/src/components/Connections/index.ts index 272a124..50ef52a 100644 --- a/src/components/Connections/index.ts +++ b/src/components/Connections/index.ts @@ -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'; \ No newline at end of file +export * from './connectionsInterfaces'; \ No newline at end of file diff --git a/src/components/Dateien/DateienAll.tsx b/src/components/Dateien/DateienAll.tsx deleted file mode 100644 index 223c5fc..0000000 --- a/src/components/Dateien/DateienAll.tsx +++ /dev/null @@ -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 = ({ files, onFileDeleted, onOptimisticDelete }) => { - const { t } = useLanguage(); - const [sortField, setSortField] = useState('created_at'); - const [sortDirection, setSortDirection] = useState('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 ; - return sortDirection === 'asc' ? - : - ; - }; - - if (sortedFiles.length === 0) { - return ( - -

{t('files.no_files', 'No files found.')}

-
- ); - } - - return ( - - {/* Table Headers */} -
-
handleSort('file_name')}> - {t('files.header.name', 'Name')} - {renderSortIcon('file_name')} -
-
handleSort('action')}> - {t('files.header.type', 'Type')} - {renderSortIcon('action')} -
-
handleSort('size')}> - {t('files.header.size', 'Size')} - {renderSortIcon('size')} -
-
handleSort('created_at')}> - {t('files.header.date', 'Date')} - {renderSortIcon('created_at')} -
-
- - {/* Files List */} - - - {sortedFiles.map((file: UserFile) => ( - - onOptimisticDelete(file.id) : undefined} - /> - - ))} - - -
- ); -}; - -export default DateienAll; diff --git a/src/components/Dateien/DateienCreated.tsx b/src/components/Dateien/DateienCreated.tsx deleted file mode 100644 index 4ba2c4c..0000000 --- a/src/components/Dateien/DateienCreated.tsx +++ /dev/null @@ -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 = ({ files, onFileDeleted, onOptimisticDelete }) => { - const { t } = useLanguage(); - const [sortField, setSortField] = useState('created_at'); - const [sortDirection, setSortDirection] = useState('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 ; - return sortDirection === 'asc' ? - : - ; - }; - - if (sortedFiles.length === 0) { - return ( - -

{t('files.no_ai_files', 'No AI-created files found.')}

-
- ); - } - - return ( - - {/* Table Headers */} -
-
handleSort('file_name')}> - {t('files.header.name', 'Name')} - {renderSortIcon('file_name')} -
-
handleSort('action')}> - {t('files.header.type', 'Type')} - {renderSortIcon('action')} -
-
handleSort('size')}> - {t('files.header.size', 'Size')} - {renderSortIcon('size')} -
-
handleSort('created_at')}> - {t('files.header.date', 'Date')} - {renderSortIcon('created_at')} -
-
- - {/* Files List with AI-created indicator */} - - - {sortedFiles.map((file: UserFile) => ( - - onOptimisticDelete(file.id) : undefined} - /> - - ))} - - -
- ); -}; - -export default DateienCreated; diff --git a/src/components/Dateien/DateienHinzufügen/DateienSelector.module.css b/src/components/Dateien/DateienHinzufügen/DateienSelector.module.css deleted file mode 100644 index 42c7497..0000000 --- a/src/components/Dateien/DateienHinzufügen/DateienSelector.module.css +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/components/Dateien/DateienHinzufügen/DateienSelector.tsx b/src/components/Dateien/DateienHinzufügen/DateienSelector.tsx deleted file mode 100644 index 6e85e50..0000000 --- a/src/components/Dateien/DateienHinzufügen/DateienSelector.tsx +++ /dev/null @@ -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 = ({ - isOpen, - onClose, - onFilesSelected, - allowMultiple = true -}) => { - const { t } = useLanguage(); - const [selectedFiles, setSelectedFiles] = useState>(new Set()); - const [activeTab, setActiveTab] = useState('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 ; - case 'created': - return ; - case 'shared': - return ; - case 'all': - default: - return ; - } - }; - - 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 ( - setShowUploadTool(false)} - onFileUpload={handleFileUpload} - /> - ); - } - - return ( -
- -
-

{t('files.selector.title', 'Select files')}

- -
- -
- {/* Tab Navigation */} -
- {(['all', 'uploads', 'created', 'shared'] as FileListType[]).map(tab => ( - - ))} -
- - {/* Action Bar */} -
-
- {allowMultiple && filteredFiles.length > 0 && ( - - )} - {selectedFiles.size > 0 && ( - - {selectedFiles.size} {selectedFiles.size === 1 ? t('files.selector.file_selected', 'File') : t('files.selector.files_selected', 'Files')} {t('files.selector.selected_suffix', 'selected')} - - )} -
- - -
- - {/* File List */} -
- {loading ? ( -
{t('files.selector.loading', 'Loading files...')}
- ) : error ? ( -
{t('files.selector.error_loading', 'Error loading files:')} {error}
- ) : filteredFiles.length === 0 ? ( -
{t('files.no_files', 'No files found.')}
- ) : ( -
- {filteredFiles.map(file => ( -
handleFileSelect(file.id)} - > -
- {selectedFiles.has(file.id) ? ( - - ) : ( - - )} -
-
- -
-
-
{file.file_name}
-
- {file.action} • {file.size ? `${Math.round(file.size / 1024)} KB` : t('files.unknown_size', 'Unknown Size')} -
-
-
- ))} -
- )} -
-
- - {/* Footer */} -
- - -
-
-
- ); -}; - -export default DateienSelector; \ No newline at end of file diff --git a/src/components/Dateien/DateienHinzufügen/DateienUploadTool.module.css b/src/components/Dateien/DateienHinzufügen/DateienUploadTool.module.css deleted file mode 100644 index 540573e..0000000 --- a/src/components/Dateien/DateienHinzufügen/DateienUploadTool.module.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/components/Dateien/DateienHinzufügen/DateienUploadTool.tsx b/src/components/Dateien/DateienHinzufügen/DateienUploadTool.tsx deleted file mode 100644 index 934d749..0000000 --- a/src/components/Dateien/DateienHinzufügen/DateienUploadTool.tsx +++ /dev/null @@ -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(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 ( -
-
-
-

{t('files.upload.title', 'Upload file')}

- -
- - {uploadStatus && ( -
- {uploadStatus.message} -
- )} - -
- - - {isDragActive ? ( -

{t('files.upload.drop_here', 'Drop file here...')}

- ) : isUploading ? ( -

{t('files.upload.uploading', 'Uploading...')}

- ) : ( -
-

{t('files.upload.drag_files', 'Drag files here')}

-

{t('files.upload.or', 'or')}

- -
- )} -
- - {selectedFile && !isUploading && !uploadStatus?.success && ( -
-

{t('files.upload.selected_file', 'Selected file:')} {selectedFile.name}

- -
- )} -
-
- ); -} - -export default DateienUploadTool; \ No newline at end of file diff --git a/src/components/Dateien/DateienItem.module.css b/src/components/Dateien/DateienItem.module.css deleted file mode 100644 index 607ed9e..0000000 --- a/src/components/Dateien/DateienItem.module.css +++ /dev/null @@ -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; - } -} diff --git a/src/components/Dateien/DateienItem.tsx b/src/components/Dateien/DateienItem.tsx deleted file mode 100644 index 36a9e25..0000000 --- a/src/components/Dateien/DateienItem.tsx +++ /dev/null @@ -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(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 ( - <> -
  • - {/* 1st column: Name with icon */} -
    - - {file.file_name} -
    - - {/* 2nd column: Type with source */} -
    -
    {file.action}
    -
    {formatFileSource(file.source)}
    -
    - - {/* 3rd column: Size */} -
    - {formatFileSize(file.size)} -
    - - {/* 4th column: Date and action buttons */} -
    - - {formatDate(file.created_at)} - - -
    - - - -
    -
    -
  • - - {/* File Preview Popup */} - {previewDocument && ( - - )} - - ); -}; - -export default DateienItem; - diff --git a/src/components/Dateien/DateienLists.module.css b/src/components/Dateien/DateienLists.module.css deleted file mode 100644 index b1cd95e..0000000 --- a/src/components/Dateien/DateienLists.module.css +++ /dev/null @@ -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); -} - - diff --git a/src/components/Dateien/DateienShared.tsx b/src/components/Dateien/DateienShared.tsx deleted file mode 100644 index b032660..0000000 --- a/src/components/Dateien/DateienShared.tsx +++ /dev/null @@ -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 = ({ files, onFileDeleted, onOptimisticDelete }) => { - const { t } = useLanguage(); - const [sortField, setSortField] = useState('created_at'); - const [sortDirection, setSortDirection] = useState('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 ; - return sortDirection === 'asc' ? - : - ; - }; - - if (sortedFiles.length === 0) { - return ( - -

    {t('files.no_shared_files', 'No shared files found.')}

    -
    - ); - } - - return ( - - {/* Table Headers */} -
    -
    handleSort('file_name')}> - {t('files.header.name', 'Name')} - {renderSortIcon('file_name')} -
    -
    handleSort('action')}> - {t('files.header.type', 'Type')} - {renderSortIcon('action')} -
    -
    handleSort('size')}> - {t('files.header.size', 'Size')} - {renderSortIcon('size')} -
    -
    handleSort('created_at')}> - {t('files.header.date', 'Date')} - {renderSortIcon('created_at')} -
    -
    - - {/* Files List with shared indicator */} - - - {sortedFiles.map((file: UserFile) => ( - - onOptimisticDelete(file.id) : undefined} - /> - - ))} - - -
    - ); -}; - -export default DateienShared; diff --git a/src/components/Dateien/DateienTable.module.css b/src/components/Dateien/DateienTable.module.css new file mode 100644 index 0000000..2f31632 --- /dev/null +++ b/src/components/Dateien/DateienTable.module.css @@ -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; + } +} diff --git a/src/components/Dateien/DateienTable.tsx b/src/components/Dateien/DateienTable.tsx new file mode 100644 index 0000000..8f58406 --- /dev/null +++ b/src/components/Dateien/DateienTable.tsx @@ -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 ( +
    +
    +

    {t('files.error.loading')} {error}

    + +
    +
    + ); + } + + return ( +
    + + + {/* Edit File Modal */} + + {editingFile && ( + + )} + +
    + ); +} + +export default DateienTable; \ No newline at end of file diff --git a/src/components/Dateien/DateienUploadModal.module.css b/src/components/Dateien/DateienUploadModal.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Dateien/DateienUploadModal.tsx b/src/components/Dateien/DateienUploadModal.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Dateien/DateienUploads.tsx b/src/components/Dateien/DateienUploads.tsx deleted file mode 100644 index 0da4cab..0000000 --- a/src/components/Dateien/DateienUploads.tsx +++ /dev/null @@ -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 = ({ files, onFileDeleted, onOptimisticDelete }) => { - const { t } = useLanguage(); - const [sortField, setSortField] = useState('created_at'); - const [sortDirection, setSortDirection] = useState('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 ; - return sortDirection === 'asc' ? - : - ; - }; - - if (sortedFiles.length === 0) { - return ( - -

    {t('files.no_uploaded_files', 'No uploaded files found.')}

    -
    - ); - } - - return ( - - {/* Table Headers */} -
    -
    handleSort('file_name')}> - {t('files.header.name', 'Name')} - {renderSortIcon('file_name')} -
    -
    handleSort('action')}> - {t('files.header.type', 'Type')} - {renderSortIcon('action')} -
    -
    handleSort('size')}> - {t('files.header.size', 'Size')} - {renderSortIcon('size')} -
    -
    handleSort('created_at')}> - {t('files.header.date', 'Date')} - {renderSortIcon('created_at')} -
    -
    - - {/* Files List */} - - - {sortedFiles.map((file: UserFile) => ( - - onOptimisticDelete(file.id) : undefined} - /> - - ))} - - -
    - ); -}; - -export default DateienUploads; diff --git a/src/components/Dateien/dateienInterfaces.ts b/src/components/Dateien/dateienInterfaces.ts new file mode 100644 index 0000000..ad596bc --- /dev/null +++ b/src/components/Dateien/dateienInterfaces.ts @@ -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; + icon: React.ReactNode | ((file: UserFile) => React.ReactNode); +} + +// File Operation Handler Types +export interface FileHandlers { + handleFileDownload: (fileId: string, fileName: string) => Promise; + handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise; + 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; + deletingFiles: Set; + 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; + 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; + columns: ColumnConfig[]; + actions: TableAction[]; + downloadingFiles: Set; + deletingFiles: Set; + downloadError: string | null; + deleteError: string | null; + editModalOpen: boolean; + editingFile: UserFile | null; + editFileFields: EditFieldConfig[]; + handleEditFile: (file: UserFile) => void; + handleSaveFile: (updatedFile: UserFile) => Promise; + handleCancelEdit: () => void; +} + + diff --git a/src/components/Dateien/dateienLogic.tsx b/src/components/Dateien/dateienLogic.tsx new file mode 100644 index 0000000..01b9948 --- /dev/null +++ b/src/components/Dateien/dateienLogic.tsx @@ -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(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) => ( + + {value} + + ) + }, + { + 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) => ( + + {formatFileSize(typeof value === 'string' ? parseInt(value, 10) : value)} + + ) + }, + { + 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: , + onClick: (row: UserFile) => { + handleEditFile(row); + } + }, + { + label: t('files.action.download'), + icon: (row: UserFile) => { + const isDownloadingThis = downloadingFiles.has(row.id); + if (isDownloadingThis) return '⏳'; + return ; + }, + 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 ; + }, + 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 + }; +} diff --git a/src/components/Dateien/index.ts b/src/components/Dateien/index.ts new file mode 100644 index 0000000..7b0be53 --- /dev/null +++ b/src/components/Dateien/index.ts @@ -0,0 +1,3 @@ +export { default as DateienTable } from './DateienTable'; +export { useDateienLogic } from './dateienLogic.tsx'; +export * from './dateienInterfaces'; \ No newline at end of file diff --git a/src/components/FormGenerator/FormGenerator.module.css b/src/components/FormGenerator/FormGenerator.module.css index ccb87ac..2d462bb 100644 --- a/src/components/FormGenerator/FormGenerator.module.css +++ b/src/components/FormGenerator/FormGenerator.module.css @@ -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); } } \ No newline at end of file diff --git a/src/components/FormGenerator/FormGenerator.tsx b/src/components/FormGenerator/FormGenerator.tsx index df77ab0..abc26d5 100644 --- a/src/components/FormGenerator/FormGenerator.tsx +++ b/src/components/FormGenerator/FormGenerator.tsx @@ -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>({ actions = [], className = '' }: FormGeneratorProps) { + const { t } = useLanguage(); // Auto-detect columns if not provided const detectedColumns = useMemo((): ColumnConfig[] => { if (providedColumns) return providedColumns; @@ -108,12 +110,18 @@ export function FormGenerator>({ // Initialize column widths useEffect(() => { const initialWidths: Record = {}; + + // 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>({ } 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>({ 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>({ return (
    + + {(searchable || filterable) && (
    {searchable && ( @@ -348,7 +349,7 @@ export function FormGenerator>({ onBlur={() => setSearchFocused(false)} className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`} /> - +
    )} @@ -365,15 +366,15 @@ export function FormGenerator>({ className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`} > - - + + {filters[column.key] && ( @@ -397,7 +398,7 @@ export function FormGenerator>({ type="button" onClick={() => handleFilter(column.key, '')} className={styles.clearFilterButton} - title="Clear filter" + title={t('formgen.filter.clear')} > ✕ @@ -490,7 +491,7 @@ export function FormGenerator>({ className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`} /> )} @@ -503,10 +504,33 @@ export function FormGenerator>({ {/* Table */}
    - {( + {loading ? ( +
    +
    +

    {t('common.loading', 'Loading...')}

    +
    + ) : ( + {actions.length > 0 && ( + + )} {selectable && ( )} - {actions.length > 0 && ( - - - )} {detectedColumns.map(column => ( - )} - {actions.length > 0 && ( - )} + {selectable && ( + + )} {detectedColumns.map(column => (
    + + {resizable && ( +
    handleMouseDown(e, 'actions')} + /> + )} +
    >({ /> - Actions - >({ className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`} onClick={() => onRowClick?.(row, index)} > - {selectable && ( - - handleRowSelect(index)} - onClick={(e) => e.stopPropagation()} - /> - + {actions.length > 0 && ( +
    {actions.map((action, actionIndex) => ( ))}
    + handleRowSelect(index)} + onClick={(e) => e.stopPropagation()} + /> + >({ onClick={() => setCurrentPage(1)} disabled={currentPage === 1} className={styles.paginationButton} + title={t('formgen.pagination.first')} > «« @@ -629,18 +654,23 @@ export function FormGenerator>({ onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1} className={styles.paginationButton} + title={t('formgen.pagination.prev')} > « - Page {currentPage} of {totalPages} ({filteredData.length} items) + {t('formgen.pagination.info') + .replace('{page}', currentPage.toString()) + .replace('{total}', totalPages.toString()) + .replace('{count}', filteredData.length.toString())} @@ -648,6 +678,7 @@ export function FormGenerator>({ onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages} className={styles.paginationButton} + title={t('formgen.pagination.last')} > »» diff --git a/src/components/Sidebar/SidebarStyles/Sidebar.module.css b/src/components/Sidebar/SidebarStyles/Sidebar.module.css index 30928ba..eb9351e 100644 --- a/src/components/Sidebar/SidebarStyles/Sidebar.module.css +++ b/src/components/Sidebar/SidebarStyles/Sidebar.module.css @@ -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; diff --git a/src/components/Workflows/WorkflowsTable.module.css b/src/components/Workflows/WorkflowsTable.module.css new file mode 100644 index 0000000..6fbeb00 --- /dev/null +++ b/src/components/Workflows/WorkflowsTable.module.css @@ -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; + } +} diff --git a/src/components/Workflows/WorkflowsTable.tsx b/src/components/Workflows/WorkflowsTable.tsx new file mode 100644 index 0000000..c3f7d0f --- /dev/null +++ b/src/components/Workflows/WorkflowsTable.tsx @@ -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) => ( + + {value.length > 8 ? `${value.substring(0, 8)}...` : value} + + ) + }, + { + 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) => ( + + {value || row.title || t('workflows.unnamed')} + + ) + }, + { + 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) => ( + + {t(`workflows.status.${value}`, value)} + + ) + }, + { + key: 'currentRound', + label: t('workflows.column.round'), + type: 'number', + width: 80, + minWidth: 60, + maxWidth: 100, + sortable: true, + filterable: true, + formatter: (value: number | undefined) => ( + + {value || 1} + + ) + }, + { + 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) => ( + + {value?.length || 0} + + ) + } + ], [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 ( +
    +
    +

    {t('workflows.error.loading')} {error}

    + +
    +
    + ); + } + + return ( +
    + { + // TODO: Navigate to workflow detail view + console.log('Clicked workflow:', workflow); + }} + /> +
    + ); +} + +export default WorkflowsTable; diff --git a/src/components/Workflows/index.ts b/src/components/Workflows/index.ts new file mode 100644 index 0000000..e7b0989 --- /dev/null +++ b/src/components/Workflows/index.ts @@ -0,0 +1 @@ +export { default as WorkflowsTable } from './WorkflowsTable'; \ No newline at end of file diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts index 1171672..f970053 100644 --- a/src/hooks/useFiles.ts +++ b/src/hooks/useFiles.ts @@ -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>(new Set()); - const [deletingFiles, setDeletingFiles] = useState>(new Set()); + const [downloadingFiles, setDownloadingFiles] = useState>(new Set()); + const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [uploadingFile, setUploadingFile] = useState(false); - const { request, error: apiError, isLoading } = useApiRequest(); + const { request, isLoading } = useApiRequest(); const [downloadError, setDownloadError] = useState(null); const [deleteError, setDeleteError] = useState(null); const [uploadError, setUploadError] = useState(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 }; } \ No newline at end of file diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index ae63283..c4f609c 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -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; - userId?: number; - messageIds?: string[]; + name?: string; + title?: string; // Keep for backward compatibility + currentRound: number; + lastActivity: string; + startedAt: string; + logs?: any[]; + messages?: any[]; + stats?: Record; + tasks?: any[]; + dataStats?: Record; // 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>(new Set()); const [deletingWorkflows, setDeletingWorkflows] = useState>(new Set()); - const { request, error: apiError, isLoading } = useApiRequest(); + const { request, isLoading } = useApiRequest(); const [startError, setStartError] = useState(null); const [stopError, setStopError] = useState(null); const [deleteError, setDeleteError] = useState(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' } diff --git a/src/locales/de.ts b/src/locales/de.ts index b934c93..6daf96d 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -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', }; \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index 83ab5c6..32f7a50 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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', }; \ No newline at end of file diff --git a/src/locales/fr.ts b/src/locales/fr.ts index a5b077f..d7806e7 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -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', }; \ No newline at end of file diff --git a/src/pages/Home/Dateien.tsx b/src/pages/Home/Dateien.tsx index b41dbe6..78eb642 100644 --- a/src/pages/Home/Dateien.tsx +++ b/src/pages/Home/Dateien.tsx @@ -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('alle'); + const { handleFileUpload, uploadingFile } = useFileOperations(); + const fileInputRef = useRef(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) => { + 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 ; - case 'uploads': - return ; - case 'erstellt': - return ; - case 'geteilt': - return ; - default: - return ; + // 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) => { + 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) => { + event.preventDefault(); + setIsDragOver(true); + }; + + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragOver(false); }; return (
    -

    Dateien

    -
    - - {/* Combined Header with Tabs and Add Button */} - -
    - {tabs.map((tab) => ( -
    - setActiveTab(tab.key)} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - {tab.label} - - - {activeTab === tab.key && ( - - )} - -
    - ))} +
    +

    {t('files.title')}

    +
    +
    + + {isDragOver ? 'Drop files here' : 'Drop files here or click to browse'} + +
    + +
    - - - +
    - -
    - - - {(uploadError || downloadError || deleteError) && ( -

    - {uploadError || downloadError || deleteError} -

    - )} - {loading &&

    {t('files.page.loading', 'Loading files...')}

    } - {error &&

    {t('files.page.error', 'Error:')} {error}

    } - - {!loading && !error && ( - - {renderActiveTabContent()} - - )} +
    +
    diff --git a/src/pages/Home/HomeStyles/Dateien.module.css b/src/pages/Home/HomeStyles/Dateien.module.css index 03544dd..75339f9 100644 --- a/src/pages/Home/HomeStyles/Dateien.module.css +++ b/src/pages/Home/HomeStyles/Dateien.module.css @@ -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; +} \ No newline at end of file diff --git a/src/pages/Home/HomeStyles/Home.module.css b/src/pages/Home/HomeStyles/Home.module.css index ddc6de4..e2a3784 100644 --- a/src/pages/Home/HomeStyles/Home.module.css +++ b/src/pages/Home/HomeStyles/Home.module.css @@ -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; diff --git a/src/pages/Home/HomeStyles/Workflows.module.css b/src/pages/Home/HomeStyles/Workflows.module.css index adcb8ff..79c8cc7 100644 --- a/src/pages/Home/HomeStyles/Workflows.module.css +++ b/src/pages/Home/HomeStyles/Workflows.module.css @@ -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 */ } diff --git a/src/pages/Home/Workflows.tsx b/src/pages/Home/Workflows.tsx index 85adc7c..d508cda 100644 --- a/src/pages/Home/Workflows.tsx +++ b/src/pages/Home/Workflows.tsx @@ -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 (
    -

    Workflows

    +

    {t('workflows.title')}

    - {/* Workflow content will go here */} -

    Workflow management coming soon...

    +