diff --git a/.env b/.env index 9ffb34e..cccab81 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -VITE_API_BASE_URL="https://gateway-int.poweron-center.net/" +VITE_API_BASE_URL="http://localhost:8000" VITE_MICROSOFT_CLIENT_ID="24cd6c8a-b592-4905-a5ba-d5fa9f911154" VITE_MICROSOFT_TENANT_ID="6a51aaeb-2467-4186-9504-2a05aedc591f" VITE_ENTRA_CLIENT_SECRET="2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD" diff --git a/src/components/Connections/connectionsLogic.tsx b/src/components/Connections/connectionsLogic.tsx index e9dbd95..66d6288 100644 --- a/src/components/Connections/connectionsLogic.tsx +++ b/src/components/Connections/connectionsLogic.tsx @@ -90,10 +90,11 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { label: t('connections.field.connected_at', 'Connected At'), type: 'readonly', editable: false, - formatter: (value: string) => { + formatter: (value: number) => { if (!value) return t('connections.not_available', 'N/A'); try { - return new Date(value).toLocaleString(); + // Convert from seconds to milliseconds for Date constructor + return new Date(value * 1000).toLocaleString(); } catch { return t('connections.invalid_date', 'Invalid Date'); } @@ -104,10 +105,11 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { label: t('connections.field.last_checked', 'Last Checked'), type: 'readonly', editable: false, - formatter: (value: string) => { + formatter: (value: number) => { if (!value) return t('connections.not_available', 'N/A'); try { - return new Date(value).toLocaleString(); + // Convert from seconds to milliseconds for Date constructor + return new Date(value * 1000).toLocaleString(); } catch { return t('connections.invalid_date', 'Invalid Date'); } @@ -199,24 +201,6 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { }, []); // Handler functions - const handleCreateConnection = async (type: 'msft' | 'google') => { - console.log('Creating connection for type:', type); - try { - const connectionData: CreateConnectionData = { - type: type, - status: 'pending', - connectedAt: new Date().toISOString(), - lastChecked: new Date().toISOString() - }; - - console.log('Sending connection data to backend:', connectionData); - const newConnection = await createConnection(connectionData); - console.log('Connection created successfully:', newConnection); - await fetchConnections(); - } catch (error) { - console.error('Error creating connection:', error); - } - }; const handleConnect = async (connection: Connection) => { console.log('Connecting to service:', connection); @@ -228,6 +212,31 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { } }; + const handleCreateConnection = async (type: 'msft' | 'google') => { + console.log('Creating connection for type:', type); + try { + // Get current UTC timestamp in seconds (float) to match backend + const currentTimestamp = Math.floor(Date.now() / 1000); + + const connectionData: CreateConnectionData = { + type: type, + status: 'pending', + connectedAt: currentTimestamp, + lastChecked: currentTimestamp + }; + + console.log('Sending connection data to backend:', connectionData); + const newConnection = await createConnection(connectionData); + console.log('Connection created successfully:', newConnection); + handleConnect(newConnection); + await fetchConnections(); + } catch (error) { + console.error('Error creating connection:', error); + } + }; + + + const handleDisconnect = async (connection: Connection) => { console.log('Disconnecting from service:', connection); try { diff --git a/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts b/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts index 3a3c228..cdcb8dc 100644 --- a/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts +++ b/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts @@ -50,7 +50,7 @@ export interface WorkflowMessage { role: 'user' | 'assistant' | 'system'; status: string; sequenceNr: number; - publishedAt: string; + publishedAt: number; // UTC timestamp in seconds (float from backend) timestamp?: string; // For backward compatibility fileIds?: string[]; // For backward compatibility stats?: WorkflowMessageStats; @@ -115,8 +115,8 @@ export interface Workflow { status: string; name?: string; currentRound: number; - lastActivity: string; - startedAt: string; + lastActivity: number; // UTC timestamp in seconds (float from backend) + startedAt: number; // UTC timestamp in seconds (float from backend) logs?: WorkflowLog[]; messages?: WorkflowMessage[]; stats?: WorkflowStats; diff --git a/src/components/Dashboard/DashboardChat/useWorkflowManager.ts b/src/components/Dashboard/DashboardChat/useWorkflowManager.ts index c60ea3b..fb9cc45 100644 --- a/src/components/Dashboard/DashboardChat/useWorkflowManager.ts +++ b/src/components/Dashboard/DashboardChat/useWorkflowManager.ts @@ -23,7 +23,8 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow // Helper to create optimistic user message const createOptimisticMessage = useCallback((prompt: string, fileIds: string[] = []) => { - const timestamp = new Date().toISOString(); + // Use UTC timestamp in seconds (float) to match backend expectation + const timestamp = Math.floor(Date.now() / 1000); return { id: `temp-${Date.now()}`, workflowId: currentWorkflowId || 'pending', diff --git a/src/components/Dateien/dateienInterfaces.ts b/src/components/Dateien/dateienInterfaces.ts index ad596bc..f02df0d 100644 --- a/src/components/Dateien/dateienInterfaces.ts +++ b/src/components/Dateien/dateienInterfaces.ts @@ -25,7 +25,7 @@ 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 }>; + handleFileUpdate: (fileId: string, updateData: Partial<{ fileName: string }>) => Promise<{ success: boolean; fileData?: any; error?: string }>; } // Hook Return Types for File Operations diff --git a/src/components/Dateien/dateienLogic.tsx b/src/components/Dateien/dateienLogic.tsx index f86c171..c27d358 100644 --- a/src/components/Dateien/dateienLogic.tsx +++ b/src/components/Dateien/dateienLogic.tsx @@ -64,7 +64,7 @@ export function useDateienLogic(): DateienLogicReturn { try { // Call API to update filename const result = await handleFileUpdate(editingFile.id, { - filename: updatedFile.file_name + fileName: updatedFile.file_name }); if (result.success) { @@ -108,9 +108,12 @@ export function useDateienLogic(): DateienLogicReturn { // Helper function to format date const formatDate: DateFormatter = (value) => { - if (!value) return '-'; + if (!value || value === 'null' || value === 'undefined') return '-'; try { const date = new Date(value); + if (isNaN(date.getTime())) { + return '-'; + } const pad = (n: number) => n.toString().padStart(2, '0'); const yyyy = date.getFullYear(); const mm = pad(date.getMonth() + 1); @@ -120,10 +123,76 @@ export function useDateienLogic(): DateienLogicReturn { const ss = pad(date.getSeconds()); return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`; } catch { - return value; + return '-'; } }; + // Helper function to format MIME type to user-friendly display + const formatMimeType = (mimeType?: string): string => { + if (!mimeType) return '-'; + + // Excel files + if (mimeType.includes('spreadsheet') || + mimeType.includes('excel') || + mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || + mimeType === 'application/vnd.ms-excel') { + return 'Excel Table'; + } + + // Word documents + if (mimeType.includes('word') || + mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + mimeType === 'application/msword') { + return 'Word Document'; + } + + // PowerPoint presentations + if (mimeType.includes('presentation') || + mimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || + mimeType === 'application/vnd.ms-powerpoint') { + return 'PowerPoint Presentation'; + } + + // PDF files + if (mimeType === 'application/pdf') { + return 'PDF Document'; + } + + // Images + if (mimeType.startsWith('image/')) { + const subType = mimeType.split('/')[1]?.toUpperCase(); + return `${subType} Image`; + } + + // Text files + if (mimeType.startsWith('text/')) { + if (mimeType === 'text/plain') return 'Text File'; + if (mimeType === 'text/csv') return 'CSV File'; + if (mimeType === 'text/html') return 'HTML File'; + return 'Text Document'; + } + + // Video files + if (mimeType.startsWith('video/')) { + const subType = mimeType.split('/')[1]?.toUpperCase(); + return `${subType} Video`; + } + + // Audio files + if (mimeType.startsWith('audio/')) { + const subType = mimeType.split('/')[1]?.toUpperCase(); + return `${subType} Audio`; + } + + // Archive files + if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('archive')) { + return 'Archive File'; + } + + // Fallback: return the MIME type as-is but shortened + return mimeType.length > 30 ? mimeType.substring(0, 30) + '...' : mimeType; + }; + // Configure columns for the files table const columns: ColumnConfig[] = useMemo(() => [ { @@ -162,6 +231,21 @@ export function useDateienLogic(): DateienLogicReturn { sortable: true, filterable: true, searchable: true, + formatter: (value: string | undefined) => ( + + {formatMimeType(value)} + + ) }, { key: 'size', diff --git a/src/components/FormGenerator/FormGenerator.tsx b/src/components/FormGenerator/FormGenerator.tsx index 802673a..a0b46d3 100644 --- a/src/components/FormGenerator/FormGenerator.tsx +++ b/src/components/FormGenerator/FormGenerator.tsx @@ -32,9 +32,10 @@ export interface FormGeneratorProps { onRowClick?: (row: T, index: number) => void; onRowSelect?: (selectedRows: T[]) => void; selectable?: boolean; + isRowSelectable?: (row: T) => boolean; loading?: boolean; actions?: { - label: string; + label: string | ((row: T) => string); onClick: (row: T) => void; icon?: string | React.ReactNode | ((row: T) => React.ReactNode); }[]; @@ -57,6 +58,7 @@ export function FormGenerator>({ onRowClick, onRowSelect, selectable = true, // Default to true for selection functionality + isRowSelectable, loading = false, actions = [], onDelete, @@ -232,6 +234,9 @@ export function FormGenerator>({ const handleRowSelect = (index: number) => { if (!selectable) return; + const row = paginatedData[index]; + if (isRowSelectable && !isRowSelectable(row)) return; + const newSelected = new Set(selectedRows); if (newSelected.has(index)) { newSelected.delete(index); @@ -250,13 +255,20 @@ export function FormGenerator>({ const handleSelectAll = () => { if (!selectable) return; - if (selectedRows.size === paginatedData.length) { + // Get only selectable rows + const selectableIndices = paginatedData + .map((row, index) => ({ row, index })) + .filter(({ row }) => !isRowSelectable || isRowSelectable(row)) + .map(({ index }) => index); + + if (selectedRows.size === selectableIndices.length) { setSelectedRows(new Set()); onRowSelect?.([]); } else { - const allIndices = new Set(paginatedData.map((_, index) => index)); - setSelectedRows(allIndices); - onRowSelect?.(paginatedData); + const allSelectableIndices = new Set(selectableIndices); + setSelectedRows(allSelectableIndices); + const selectableData = selectableIndices.map(i => paginatedData[i]); + onRowSelect?.(selectableData); } }; @@ -585,9 +597,15 @@ export function FormGenerator>({ 0} + checked={(() => { + const selectableIndices = paginatedData + .map((row, index) => ({ row, index })) + .filter(({ row }) => !isRowSelectable || isRowSelectable(row)) + .map(({ index }) => index); + return selectedRows.size === selectableIndices.length && selectableIndices.length > 0; + })()} onChange={handleSelectAll} - title="Select all items" + title={t('formgen.select.all', 'Select all items')} /> )} @@ -644,7 +662,16 @@ export function FormGenerator>({ checked={selectedRows.has(index)} onChange={() => handleRowSelect(index)} onClick={(e) => e.stopPropagation()} - title="Select this item" + disabled={isRowSelectable && !isRowSelectable(row)} + title={ + isRowSelectable && !isRowSelectable(row) + ? t('formgen.select.disabled', 'This item cannot be selected') + : t('formgen.select.item', 'Select this item') + } + style={{ + opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1, + cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer' + }} /> )} @@ -654,23 +681,26 @@ export function FormGenerator>({ style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }} >
- {actions.map((action, actionIndex) => ( - - ))} + {actions.map((action, actionIndex) => { + const actionLabel = typeof action.label === 'function' ? action.label(row) : action.label; + return ( + + ); + })}
)} diff --git a/src/components/Prompts/PromptsTable.tsx b/src/components/Prompts/PromptsTable.tsx index f196baf..70d4395 100644 --- a/src/components/Prompts/PromptsTable.tsx +++ b/src/components/Prompts/PromptsTable.tsx @@ -4,10 +4,31 @@ import { FormGenerator } from '../FormGenerator/FormGenerator'; import { Popup } from '../Popup/Popup'; import { EditForm } from '../Popup/EditForm'; import { usePromptsLogic } from './promptsLogic'; -import { PromptsTableProps } from './promptsTypes'; +import { PromptsTableProps, Prompt } from './promptsTypes'; import { useLanguage } from '../../contexts/LanguageContext'; import styles from './PromptsTable.module.css'; +// Helper function to determine if a prompt can be selected/deleted +const isPromptSelectable = (prompt: Prompt): boolean => { + // Primary check: If backend explicitly sets _hideDelete, respect that + if (prompt._hideDelete === true) { + return false; + } + + // Hardcoded list of the six default system prompt IDs that cannot be selected/deleted + const systemPromptIds = [ + "097d34f0-9fcc-4233-bf1e-e64e13818464", + "17546dc6-b792-40e1-9aa1-0fcc4860d0c1", + "17c42519-2bf6-49b4-83f9-3cde7498310c", + "2bb85d1e-4e02-4de8-98ae-0e815267d864", + "93343a5b-49f0-4dbf-9513-2ab5f6938fd8", + "cfb51260-486f-4b42-96fe-ef03f406dcf1" + ]; + + // Check if this prompt is one of the system default prompts + return !systemPromptIds.includes(prompt.id); +}; + function PromptsTable({ className = '' }: PromptsTableProps) { const { t } = useLanguage(); const { @@ -50,6 +71,7 @@ function PromptsTable({ className = '' }: PromptsTableProps) { pagination={true} pageSize={10} selectable={true} + isRowSelectable={isPromptSelectable} loading={loading} actions={actions} onDelete={handleDeleteSingle} diff --git a/src/components/Prompts/promptsLogic.tsx b/src/components/Prompts/promptsLogic.tsx index 2995c9c..a7c0229 100644 --- a/src/components/Prompts/promptsLogic.tsx +++ b/src/components/Prompts/promptsLogic.tsx @@ -6,6 +6,27 @@ import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts' import { useLanguage } from '../../contexts/LanguageContext'; import type { EditFieldConfig } from '../Popup/EditForm'; +// Helper function to determine if a prompt can be deleted +const isPromptDeletable = (prompt: Prompt): boolean => { + // Primary check: If backend explicitly sets _hideDelete, respect that + if (prompt._hideDelete === true) { + return false; + } + + // Hardcoded list of the six default system prompt IDs that cannot be deleted + const systemPromptIds = [ + "097d34f0-9fcc-4233-bf1e-e64e13818464", + "17546dc6-b792-40e1-9aa1-0fcc4860d0c1", + "17c42519-2bf6-49b4-83f9-3cde7498310c", + "2bb85d1e-4e02-4de8-98ae-0e815267d864", + "93343a5b-49f0-4dbf-9513-2ab5f6938fd8", + "cfb51260-486f-4b42-96fe-ef03f406dcf1" + ]; + + // Check if this prompt is one of the system default prompts + return !systemPromptIds.includes(prompt.id); +}; + import type { PromptsLogicReturn, PromptActionConfig, @@ -15,7 +36,6 @@ import type { export function usePromptsLogic(): PromptsLogicReturn { const { prompts, loading, error, refetch } = usePrompts(); const { t } = useLanguage(); - const { handlePromptDelete, handlePromptUpdate, @@ -258,12 +278,26 @@ export function usePromptsLogic(): PromptsLogicReturn { } }, { - label: t('prompts.action.delete', 'Delete'), - icon: (_row: Prompt) => { - return ; + label: (row: Prompt) => { + const isDeletable = isPromptDeletable(row); + return isDeletable + ? t('prompts.action.delete', 'Delete') + : t('prompts.action.delete.disabled', 'No permission to delete prompt'); + }, + icon: (row: Prompt) => { + const isDeletable = isPromptDeletable(row); + return ( + + ); }, onClick: (row: Prompt) => { - if (!deletingPrompts.has(row.id)) { + const isDeletable = isPromptDeletable(row); + if (isDeletable && !deletingPrompts.has(row.id)) { handleDeletePrompt(row); } } diff --git a/src/components/Prompts/promptsTypes.ts b/src/components/Prompts/promptsTypes.ts index 14e9b8a..fbc1495 100644 --- a/src/components/Prompts/promptsTypes.ts +++ b/src/components/Prompts/promptsTypes.ts @@ -6,6 +6,8 @@ export interface Prompt { mandateId: string; content: string; name: string; + _createdBy?: string; // Optional field to track who created the prompt + _hideDelete?: boolean; // Backend access control flag } // Props for the PromptsTable component @@ -15,7 +17,7 @@ export interface PromptsTableProps { // Action configuration for prompt actions export interface PromptActionConfig { - label: string; + label: string | ((row: Prompt) => string); icon: (row: Prompt) => React.ReactElement; onClick: (row: Prompt) => void; } diff --git a/src/components/Workflows/workflowsLogic.tsx b/src/components/Workflows/workflowsLogic.tsx index 4bc079a..e798fd4 100644 --- a/src/components/Workflows/workflowsLogic.tsx +++ b/src/components/Workflows/workflowsLogic.tsx @@ -182,22 +182,11 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn { maxWidth: 180, sortable: true, filterable: true, - formatter: (value: string | number | undefined) => { + formatter: (value: number | undefined) => { if (!value) return '-'; try { - let date: Date; - - // Handle Unix timestamp (as string or number) - if (typeof value === 'string' && /^\d+$/.test(value)) { - // Unix timestamp as string - date = new Date(parseInt(value) * 1000); - } else if (typeof value === 'number') { - // Unix timestamp as number - date = new Date(value * 1000); - } else { - // ISO string or other date format - date = new Date(value); - } + // Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor + const date = new Date(value * 1000); // Check if date is valid if (isNaN(date.getTime())) { @@ -220,22 +209,11 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn { maxWidth: 180, sortable: true, filterable: true, - formatter: (value: string | number | undefined) => { + formatter: (value: number | undefined) => { if (!value) return '-'; try { - let date: Date; - - // Handle Unix timestamp (as string or number) - if (typeof value === 'string' && /^\d+$/.test(value)) { - // Unix timestamp as string - date = new Date(parseInt(value) * 1000); - } else if (typeof value === 'number') { - // Unix timestamp as number - date = new Date(value * 1000); - } else { - // ISO string or other date format - date = new Date(value); - } + // Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor + const date = new Date(value * 1000); // Check if date is valid if (isNaN(date.getTime())) { diff --git a/src/hooks/useConnections.ts b/src/hooks/useConnections.ts index 600b732..4e9707d 100644 --- a/src/hooks/useConnections.ts +++ b/src/hooks/useConnections.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useApiRequest } from './useApi'; -// Connection interfaces based on backend UserConnection model +// Connection interfaces - exactly matching backend UserConnection model export interface Connection { id: string; userId: string; @@ -10,9 +10,9 @@ export interface Connection { externalUsername: string; externalEmail?: string; status: 'active' | 'expired' | 'revoked' | 'pending'; - connectedAt: string; - lastChecked: string; - expiresAt?: string; + connectedAt: number; // Backend uses float for UTC timestamp in seconds + lastChecked: number; // Backend uses float for UTC timestamp in seconds + expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds } export interface CreateConnectionData { @@ -24,9 +24,9 @@ export interface CreateConnectionData { externalUsername?: string; externalEmail?: string; status?: 'active' | 'expired' | 'revoked' | 'pending'; - connectedAt?: string; - lastChecked?: string; - expiresAt?: string; + connectedAt?: number; // Backend uses float for UTC timestamp in seconds + lastChecked?: number; // Backend uses float for UTC timestamp in seconds + expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds } export interface ConnectResponse { diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts index f970053..4d3969a 100644 --- a/src/hooks/useFiles.ts +++ b/src/hooks/useFiles.ts @@ -1,17 +1,15 @@ import { useState, useEffect } from 'react'; import { useApiRequest } from './useApi'; -// File interfaces +// File interfaces - exactly matching backend FileItem model export interface FileInfo { id: string; - filename: string; + mandateId: string; // Required in backend + fileName: string; // Required in backend mimeType: string; + fileHash: string; fileSize: number; - creationDate: string; - fileHash?: string; - mandateId?: string; - workflowId?: string; - source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me' + creationDate: number; // Backend uses float for UTC timestamp in seconds } export interface UserFile { @@ -19,7 +17,7 @@ export interface UserFile { file_name: string; mime_type?: string; action: string; - created_at: string; + created_at: number |string; size?: number; source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me' } @@ -31,13 +29,71 @@ export function useUserFiles() { const fetchFiles = async () => { try { + console.log('🔍 Fetching files from API...'); const data = await request({ url: '/api/files/list', method: 'get' }); - // Map API response to our frontend model - const mappedFiles = data.map((apiFile: FileInfo): UserFile => { + console.log('đŸ“„ Raw API response:', data); + + // Ensure data is an array, handle null/undefined responses + const fileList = Array.isArray(data) ? data : []; + console.log(`📋 Processing ${fileList.length} files from API`); + + // Filter out invalid files and map API response to our frontend model + const validFiles = fileList.filter((apiFile: any): boolean => { + // Skip files with invalid data that would cause backend validation errors + // Backend FileItem requires: id, mandateId, fileName, mimeType, fileHash, fileSize, creationDate + const isValid = !!( + apiFile && + typeof apiFile === 'object' && + apiFile.id && + typeof apiFile.id === 'string' && + apiFile.mandateId && + typeof apiFile.mandateId === 'string' && + apiFile.fileName && + typeof apiFile.fileName === 'string' && + apiFile.fileName.trim() !== '' && + apiFile.mimeType && + typeof apiFile.mimeType === 'string' && + apiFile.fileHash && + typeof apiFile.fileHash === 'string' && + typeof apiFile.fileSize === 'number' && + apiFile.fileSize >= 0 && + typeof apiFile.creationDate === 'number' && + apiFile.creationDate > 0 + ); + + if (!isValid) { + console.warn('❌ Filtering out invalid file record:', { + id: apiFile?.id, + mandateId: apiFile?.mandateId, + fileName: apiFile?.fileName, + mimeType: apiFile?.mimeType, + fileHash: apiFile?.fileHash, + fileSize: apiFile?.fileSize, + creationDate: apiFile?.creationDate, + creationDateType: typeof apiFile?.creationDate, + fullObject: apiFile + }); + } else { + console.log('✅ Valid file:', { + id: apiFile.id, + fileName: apiFile.fileName, + creationDate: apiFile.creationDate + }); + } + + return isValid; + }); + + console.log(`✹ Filtered to ${validFiles.length} valid files`); + if (validFiles.length !== fileList.length) { + console.warn(`⚠ Filtered out ${fileList.length - validFiles.length} invalid files`); + } + + const mappedFiles = validFiles.map((apiFile: any): UserFile => { // Derive a simplified action from the MIME type let action = 'Document'; if (apiFile.mimeType) { @@ -71,20 +127,28 @@ export function useUserFiles() { } } + // Convert creationDate from backend float (seconds since epoch) to ISO string + // Backend always sends creationDate as float seconds since epoch (UTC) + const createdAt = new Date(apiFile.creationDate * 1000).toISOString(); + return { id: apiFile.id, - file_name: apiFile.filename, - mime_type: apiFile.mimeType, + file_name: apiFile.fileName, // Required field from backend + mime_type: apiFile.mimeType, // Required field from backend action: action, - created_at: apiFile.creationDate, - size: apiFile.fileSize, - source: apiFile.source + created_at: createdAt, + size: apiFile.fileSize, // Required field from backend + source: 'user_uploaded' // Default source since workflowId is not part of FileItem model }; }); + console.log(`🎉 Successfully processed ${mappedFiles.length} files for display`); setFiles(mappedFiles); } catch (error) { // Error is already handled by useApiRequest + console.error('❌ Error fetching files:', error); + // Set empty array on error to prevent UI issues + setFiles([]); } }; @@ -127,15 +191,25 @@ export function useFileOperations() { setDownloadingFiles(prev => new Set(prev).add(fileId)); try { + console.log(`đŸ“„ Starting download for file: ${fileName} (ID: ${fileId})`); + + // Try to get the file download const blob = await request({ url: `/api/files/${fileId}/download`, method: 'get', // Override axios config for blob response - additionalConfig: { responseType: 'blob' } + additionalConfig: { + responseType: 'blob', + // Better error handling for blob responses + validateStatus: function (status: number) { + return status >= 200 && status < 300; // default + } + } }); + console.log(`✅ Download successful for: ${fileName}`, { size: blob.size, type: blob.type }); // Create a download link and trigger the download - const url = window.URL.createObjectURL(new Blob([blob])); + const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.setAttribute('download', fileName); @@ -146,7 +220,16 @@ export function useFileOperations() { return true; } catch (error: any) { - setDownloadError(error.message); + console.error(`❌ Download failed for ${fileName}:`, error); + let errorMessage = error.message; + + if (error.response?.status === 404) { + errorMessage = `File "${fileName}" not found or has been deleted.`; + } else if (error.response?.status === 403) { + errorMessage = `No permission to download "${fileName}".`; + } + + setDownloadError(errorMessage); return false; } finally { setDownloadingFiles(prev => { @@ -167,16 +250,31 @@ export function useFileOperations() { } try { + console.log(`đŸ—‘ïž Starting delete for file ID: ${fileId}`); + await request({ url: `/api/files/${fileId}`, method: 'delete' }); + console.log(`✅ Delete successful for file ID: ${fileId}`); + // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { - setDeleteError(error.message); + console.error(`❌ Delete failed for file ID ${fileId}:`, error); + let errorMessage = error.message; + + if (error.response?.status === 404) { + errorMessage = `File not found or has already been deleted.`; + // If file doesn't exist, consider it successfully "deleted" + return true; + } else if (error.response?.status === 403) { + errorMessage = `No permission to delete this file.`; + } + + setDeleteError(errorMessage); // If deletion failed and we optimistically removed it, we should refetch to restore the file return false; } finally { @@ -188,11 +286,35 @@ export function useFileOperations() { } }; + /** + * File upload function - backend bug has been fixed! + * + * BACKEND FIXES APPLIED: + * - Fixed file.fileName → file.filename in routeDataFiles.py + * - Removed workflowId from FileItem creation in interfaceComponentObjects.py + * - Upload should now work correctly + */ const handleFileUpload = async (file: globalThis.File, workflowId?: string) => { setUploadError(null); setUploadingFile(true); try { + console.log('đŸ“€ Starting file upload...', { + fileName: file.name, + fileSize: file.size, + fileType: file.type, + workflowId: workflowId + }); + + // Validate file before upload + if (!file || !file.name || file.name.trim() === '') { + throw new Error('Invalid file: File must have a valid name'); + } + + if (file.size === 0) { + throw new Error('Invalid file: File cannot be empty'); + } + const formData = new FormData(); formData.append('file', file); @@ -200,6 +322,25 @@ export function useFileOperations() { formData.append('workflowId', workflowId); } + // FormData is now correctly configured for backend + + console.log('📋 FormData prepared:', { + hasFile: formData.has('file'), + hasWorkflowId: formData.has('workflowId'), + workflowId: workflowId + }); + + // Log the actual file object being sent + console.log('📁 File object details:', { + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified, + constructor: file.constructor.name + }); + + console.log('🚀 Sending upload request...'); + const fileData = await request({ url: '/api/files/upload', method: 'post', @@ -212,29 +353,66 @@ export function useFileOperations() { } }); + console.log('✅ Upload successful:', fileData); return { success: true, fileData }; } catch (error: any) { - setUploadError(error.message); - return { success: false, error: error.message }; + console.error('❌ Upload failed:', { + error: error, + message: error.message, + response: error.response, + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }); + + // Provide detailed error analysis for backend team + let errorMessage = error.message; + + if (error.response?.status === 500) { + if (errorMessage.includes('validation')) { + errorMessage = 'File validation failed. Please ensure the file is valid and try again.'; + } else { + errorMessage = 'Server error during upload. Please try again later.'; + } + } + + console.error('❌ Upload error details:', error); + + setUploadError(errorMessage); + return { success: false, error: errorMessage }; } finally { setUploadingFile(false); } }; - const handleFileUpdate = async (fileId: string, updateData: Partial<{ filename: string }>) => { + const handleFileUpdate = async (fileId: string, updateData: Partial<{ fileName: string }>) => { setUploadError(null); // Reuse upload error state for update operations try { + console.log(`✏ Starting update for file ID: ${fileId}`, updateData); + const updatedFile = await request({ url: `/api/files/${fileId}`, method: 'put', data: updateData }); + console.log(`✅ Update successful for file ID: ${fileId}`); return { success: true, fileData: updatedFile }; } catch (error: any) { - setUploadError(error.message); - return { success: false, error: error.message }; + console.error(`❌ Update failed for file ID ${fileId}:`, error); + let errorMessage = error.message; + + if (error.response?.status === 404) { + errorMessage = `File not found or has been deleted.`; + } else if (error.response?.status === 403) { + errorMessage = `No permission to update this file.`; + } else if (error.response?.status === 400) { + errorMessage = `Invalid file update data.`; + } + + setUploadError(errorMessage); + return { success: false, error: errorMessage }; } }; diff --git a/src/hooks/usePrompts.ts b/src/hooks/usePrompts.ts index ee18dbb..9b0a2a1 100644 --- a/src/hooks/usePrompts.ts +++ b/src/hooks/usePrompts.ts @@ -7,6 +7,8 @@ export interface Prompt { mandateId: string; content: string; name: string; + _createdBy?: string; // Optional field to track who created the prompt + _hideDelete?: boolean; // Backend access control flag } // Prompts list hook diff --git a/src/locales/de.ts b/src/locales/de.ts index 76407e8..3052317 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -391,6 +391,9 @@ export default { 'formgen.pagination.prev': 'Vorherige Seite', 'formgen.pagination.next': 'NĂ€chste Seite', 'formgen.pagination.last': 'Letzte Seite', + 'formgen.select.all': 'Alle Elemente auswĂ€hlen', + 'formgen.select.item': 'Dieses Element auswĂ€hlen', + 'formgen.select.disabled': 'Dieses Element kann nicht ausgewĂ€hlt werden', 'formgen.delete.multiple': 'Löschen ({count})', 'formgen.delete.single': 'Löschen', 'formgen.delete.confirm': 'Sind Sie sicher, dass Sie die {count} ausgewĂ€hlten Elemente löschen möchten?', @@ -407,6 +410,7 @@ export default { 'prompts.action.edit': 'Bearbeiten', 'prompts.action.copy': 'Kopieren', 'prompts.action.delete': 'Löschen', + 'prompts.action.delete.disabled': 'Keine Berechtigung zum Löschen des Prompts', 'prompts.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?', 'prompts.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Prompts löschen möchten?', 'prompts.field.name': 'Prompt-Name', diff --git a/src/locales/en.ts b/src/locales/en.ts index a320ff0..5c0eeb2 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -394,6 +394,9 @@ export default { 'formgen.pagination.prev': 'Previous page', 'formgen.pagination.next': 'Next page', 'formgen.pagination.last': 'Last page', + 'formgen.select.all': 'Select all items', + 'formgen.select.item': 'Select this item', + 'formgen.select.disabled': 'This item cannot be selected', 'formgen.delete.multiple': 'Delete ({count})', 'formgen.delete.confirm_multiple': 'Are you sure you want to delete the {count} selected items?', @@ -407,6 +410,7 @@ export default { 'prompts.action.edit': 'Edit', 'prompts.action.copy': 'Copy', 'prompts.action.delete': 'Delete', + 'prompts.action.delete.disabled': 'No permission to delete prompt', 'prompts.delete.confirm': 'Are you sure you want to delete "{name}"?', 'prompts.delete.confirmMultiple': 'Are you sure you want to delete {count} prompts?', 'prompts.field.name': 'Prompt Name', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 1fe1bc2..1d991cc 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -394,6 +394,9 @@ export default { 'formgen.pagination.prev': 'Page prĂ©cĂ©dente', 'formgen.pagination.next': 'Page suivante', 'formgen.pagination.last': 'DerniĂšre page', + 'formgen.select.all': 'SĂ©lectionner tous les Ă©lĂ©ments', + 'formgen.select.item': 'SĂ©lectionner cet Ă©lĂ©ment', + 'formgen.select.disabled': 'Cet Ă©lĂ©ment ne peut pas ĂȘtre sĂ©lectionnĂ©', 'formgen.delete.multiple': 'Supprimer ({count})', 'formgen.delete.confirm_multiple': 'Êtes-vous sĂ»r de vouloir supprimer les {count} Ă©lĂ©ments sĂ©lectionnĂ©s ?', @@ -407,6 +410,7 @@ export default { 'prompts.action.edit': 'Modifier', 'prompts.action.copy': 'Copier', 'prompts.action.delete': 'Supprimer', + 'prompts.action.delete.disabled': 'Aucune permission de supprimer l\'invite', 'prompts.delete.confirm': 'Êtes-vous sĂ»r de vouloir supprimer "{name}" ?', 'prompts.delete.confirmMultiple': 'Êtes-vous sĂ»r de vouloir supprimer {count} prompts ?', 'prompts.field.name': 'Nom du prompt',