import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import { deleteWorkflowApi, deleteWorkflowsApi, updateWorkflowApi, fetchWorkflows as fetchWorkflowsApi, fetchWorkflow as fetchWorkflowByIdApi, fetchAttributes as fetchAttributesApi, startWorkflowApi, stopWorkflowApi, deleteMessageApi, deleteFileFromMessageApi, type Workflow, type AttributeDefinition, type StartWorkflowRequest } from '../api/workflowApi'; import { useWorkflowSelection } from '../contexts/WorkflowSelectionContext'; import { usePermissions, type UserPermissions } from './usePermissions'; // Workflow interface matching backend export interface UserWorkflow { id: string; mandateId: string; status: string; name?: string; workflowMode?: string; [key: string]: any; // Allow additional properties } // Re-export AttributeDefinition from workflowApi export type { AttributeDefinition } from '../api/workflowApi'; // Attribute option interface (from backend) export interface AttributeOption { value: string | number; label: string | { [key: string]: string }; // Can be string or object with language keys } // Pagination parameters export interface PaginationParams { page?: number; pageSize?: number; sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; } /** Get apiBaseUrl from instanceId and featureCode for feature-scoped workflow APIs */ export function getWorkflowApiBaseUrl(instanceId: string | undefined, featureCode: string | undefined): string | undefined { if (!instanceId || !featureCode) return undefined; if (featureCode === 'automation') return `/api/automations/${instanceId}`; return undefined; } // Workflows list hook - pass instanceId and featureCode when in feature context for feature-scoped API export function useUserWorkflows(options?: { instanceId?: string; featureCode?: string }) { const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode); const [workflows, setWorkflows] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { const attrs = await fetchAttributesApi(request, 'ChatWorkflow'); setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, [request]); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'ChatWorkflow'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchWorkflowsData = useCallback(async (params?: PaginationParams) => { try { const requestParams: any = {}; // Build pagination object if provided if (params) { const paginationObj: any = {}; if (params.page !== undefined) paginationObj.page = params.page; if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } } let data: any; if (!apiBaseUrl) { console.error('useUserWorkflows: apiBaseUrl is required (missing instanceId/featureCode)'); return; } const url = `${apiBaseUrl}/workflows`; if (Object.keys(requestParams).length > 0) { data = await request({ url, method: 'get', params: requestParams }); } else { data = await fetchWorkflowsApi(request, undefined, apiBaseUrl); } // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; // Map API response to our frontend model const mappedWorkflows = items.map((apiWorkflow: any): UserWorkflow => { return { id: apiWorkflow.id, mandateId: apiWorkflow.mandateId || '', status: apiWorkflow.status || 'unknown', name: apiWorkflow.name, workflowMode: apiWorkflow.workflowMode, ...apiWorkflow // Include any additional properties }; }); setWorkflows(mappedWorkflows); if (data.pagination) { setPagination(data.pagination); } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; const mappedWorkflows = items.map((apiWorkflow: any): UserWorkflow => { return { id: apiWorkflow.id, mandateId: apiWorkflow.mandateId || '', status: apiWorkflow.status || 'unknown', name: apiWorkflow.name, workflowMode: apiWorkflow.workflowMode, ...apiWorkflow }; }); setWorkflows(mappedWorkflows); setPagination(null); } } catch (error: any) { // Error is already handled by useApiRequest setWorkflows([]); setPagination(null); } }, [request, apiBaseUrl]); // Optimistically remove a workflow from the local state const removeOptimistically = (workflowId: string) => { setWorkflows(prevWorkflows => prevWorkflows.filter(workflow => workflow.id !== workflowId)); }; // Optimistically update a workflow in the local state const updateOptimistically = (workflowId: string, updateData: Partial) => { setWorkflows(prevWorkflows => prevWorkflows.map(workflow => workflow.id === workflowId ? { ...workflow, ...updateData } : workflow ) ); }; // Fetch a single workflow by ID const fetchWorkflowById = useCallback(async (workflowId: string): Promise => { try { const workflow = await fetchWorkflowByIdApi(request, workflowId, apiBaseUrl); return workflow as UserWorkflow | null; } catch (error: any) { console.error('Error fetching workflow by ID:', error); return null; } }, [request, apiBaseUrl]); // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; // For options that need to be fetched (e.g., "user.role") }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields based on readonly/editable flags if (attr.readonly === true || attr.editable === false) { return false; // Don't show readonly fields in edit form } // Also filter out common non-editable fields const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; // Map backend types to form field types if (attr.type === 'checkbox') { fieldType = 'boolean'; } else if (attr.type === 'email') { fieldType = 'email'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'select') { fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'text') { fieldType = (attr as any).multiline === true ? 'textarea' : 'string'; } // Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union // If needed, they should be handled via type casting: (attr as any).type === 'boolean' // Define validators and required fields let required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; if (attr.name === 'name') { required = true; validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return 'Workflow name cannot be empty'; } if (typeof value === 'string' && value.length > 100) { return 'Workflow name cannot exceed 100 characters'; } return null; }; } else if (fieldType === 'textarea') { minRows = 4; maxRows = 8; } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: attr.editable !== false && attr.readonly !== true, required, validator, minRows, maxRows, options, optionsReference }; }); return editableFields; }, [attributes]); // Ensure attributes are loaded - can be called by EditActionButton const ensureAttributesLoaded = useCallback(async () => { // If attributes are already loaded, return them if (attributes && attributes.length > 0) { return attributes; } // Otherwise, fetch them and return the result const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch attributes and permissions on mount // Note: Do NOT fetch workflows here - let the table component control pagination useEffect(() => { fetchAttributes(); fetchPermissions(); }, [fetchAttributes, fetchPermissions]); // Listen for workflow creation events to refetch workflows list useEffect(() => { const handleWorkflowCreated = (_event: CustomEvent<{ workflow: UserWorkflow }>) => { // Refetch to ensure we have the latest data fetchWorkflowsData(); }; window.addEventListener('workflowCreated', handleWorkflowCreated as EventListener); return () => { window.removeEventListener('workflowCreated', handleWorkflowCreated as EventListener); }; }, [fetchWorkflowsData]); return { data: workflows, loading, error, refetch: fetchWorkflowsData, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchWorkflowById, generateEditFieldsFromAttributes, ensureAttributesLoaded }; } // Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) { const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode); const [startingWorkflow, setStartingWorkflow] = useState(false); const [stoppingWorkflows, setStoppingWorkflows] = useState>(new Set()); const [deletingWorkflows, setDeletingWorkflows] = useState>(new Set()); const [editingWorkflows, setEditingWorkflows] = useState>(new Set()); const [deletingMessages, setDeletingMessages] = useState>(new Set()); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [startError, setStartError] = useState(null); const [stopError, setStopError] = useState(null); const [deleteError, setDeleteError] = useState(null); const [updateError, setUpdateError] = useState(null); const [deleteMessageError, setDeleteMessageError] = useState(null); const [deleteFileError, setDeleteFileError] = useState(null); // Workflow selection context - to clear selection if deleted workflow is selected const { selectedWorkflowId, clearWorkflow } = useWorkflowSelection(); const { request } = useApiRequest(); // Generic delete operation handler const handleDeleteOperation = async ( operationKey: string, setLoadingSet: React.Dispatch>>, setErrorState: React.Dispatch>, operation: () => Promise, errorMessages: { default: string; notFound: string; forbidden: string } ): Promise<{ success: boolean; error?: string }> => { setErrorState(null); setLoadingSet(prev => new Set(prev).add(operationKey)); try { await operation(); return { success: true }; } catch (error: any) { let errorMessage = error.message || errorMessages.default; if (error.response?.status === 404) { errorMessage = errorMessages.notFound; return { success: true }; } else if (error.response?.status === 403) { errorMessage = errorMessages.forbidden; } setErrorState(errorMessage); return { success: false, error: errorMessage }; } finally { setLoadingSet(prev => { const newSet = new Set(prev); newSet.delete(operationKey); return newSet; }); } }; const startWorkflow = async ( instanceId: string, workflowData: StartWorkflowRequest, options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' } ) => { setStartError(null); setStartingWorkflow(true); try { const response = await startWorkflowApi(request, instanceId, workflowData, options); return { success: true, data: response }; } catch (error: any) { const errorMessage = error.message || 'Failed to start workflow'; setStartError(errorMessage); return { success: false, error: errorMessage }; } finally { setStartingWorkflow(false); } }; const stopWorkflow = async (instanceId: string, workflowId: string) => { setStopError(null); setStoppingWorkflows(prev => new Set(prev).add(workflowId)); try { await stopWorkflowApi(request, instanceId, workflowId); return { success: true }; } catch (error: any) { const errorMessage = error.message || 'Failed to stop workflow'; setStopError(errorMessage); return { success: false, error: errorMessage }; } finally { setStoppingWorkflows(prev => { const newSet = new Set(prev); newSet.delete(workflowId); return newSet; }); } }; const handleWorkflowDelete = async (workflowId: string) => { const result = await handleDeleteOperation( workflowId, setDeletingWorkflows, setDeleteError, () => deleteWorkflowApi(request, workflowId, apiBaseUrl), { default: 'Failed to delete workflow', notFound: 'Workflow not found or has already been deleted.', forbidden: 'No permission to delete this workflow.' } ); if (result.success) { // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); // Dispatch event to notify other components (e.g., dashboard dropdown) window.dispatchEvent(new CustomEvent('workflowDeleted', { detail: { workflowIds: [workflowId] } })); // Clear workflow selection if the deleted workflow was selected if (selectedWorkflowId === workflowId) { clearWorkflow(); } } return result.success; }; const handleWorkflowDeleteMultiple = async (workflowIds: string[]) => { setDeleteError(null); setDeletingWorkflows(prev => { const newSet = new Set(prev); workflowIds.forEach(id => newSet.add(id)); return newSet; }); try { // Delete workflows one by one since there's no bulk delete endpoint await deleteWorkflowsApi(request, workflowIds, apiBaseUrl); // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); // Dispatch event to notify other components (e.g., dashboard dropdown) window.dispatchEvent(new CustomEvent('workflowDeleted', { detail: { workflowIds } })); // Clear workflow selection if the selected workflow was deleted if (selectedWorkflowId && workflowIds.includes(selectedWorkflowId)) { clearWorkflow(); } return true; } catch (error: any) { console.error(`❌ Bulk delete failed:`, error); setDeleteError(error.message || 'Bulk delete failed'); return false; } finally { setDeletingWorkflows(prev => { const newSet = new Set(prev); workflowIds.forEach(id => newSet.delete(id)); return newSet; }); } }; const deleteMessage = async (workflowId: string, messageId: string) => { const operationKey = `${workflowId}:${messageId}`; return handleDeleteOperation( operationKey, setDeletingMessages, setDeleteMessageError, () => deleteMessageApi(request, workflowId, messageId, apiBaseUrl), { default: 'Failed to delete message', notFound: 'Message not found or has already been deleted.', forbidden: 'No permission to delete this message.' } ); }; const deleteFileFromMessage = async ( workflowId: string, messageId: string, fileId: string ) => { const operationKey = `${workflowId}:${messageId}:${fileId}`; return handleDeleteOperation( operationKey, setDeletingFiles, setDeleteFileError, () => deleteFileFromMessageApi(request, workflowId, messageId, fileId, apiBaseUrl), { default: 'Failed to delete file', notFound: 'File not found or has already been deleted.', forbidden: 'No permission to delete this file.' } ); }; const handleWorkflowUpdate = async (workflowId: string, updateData: Partial<{ name: string; description?: string; tags?: string[] }>, _originalWorkflowData?: any) => { setUpdateError(null); setEditingWorkflows(prev => new Set(prev).add(workflowId)); try { const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData, apiBaseUrl); return { success: true, workflowData: updatedWorkflow }; } catch (error: any) { console.error(`Update failed for workflow ID ${workflowId}:`, error); const errorMessage = error.response?.data?.message || error.message || 'Failed to update workflow'; const statusCode = error.response?.status; setUpdateError(errorMessage); // Return detailed error information for proper handling return { success: false, error: errorMessage, statusCode, isPermissionError: statusCode === 403, isValidationError: statusCode === 400 }; } finally { setEditingWorkflows(prev => { const newSet = new Set(prev); newSet.delete(workflowId); return newSet; }); } }; // Generic inline update handler for FormGeneratorTable // Must merge changes with existing row data because backend requires full object const handleInlineUpdate = async (workflowId: string, changes: Partial, existingRow?: any) => { if (!existingRow) { throw new Error(`Existing row data required for inline update`); } // Merge changes with existing row data const mergedData = { name: existingRow.name, ...changes }; const result = await handleWorkflowUpdate(workflowId, mergedData); if (!result.success) { throw new Error(result.error || 'Failed to update'); } return result; }; return { // Loading states startingWorkflow, stoppingWorkflows, deletingWorkflows, editingWorkflows, deletingMessages, deletingFiles, // Error states startError, stopError, deleteError, updateError, deleteMessageError, deleteFileError, // Operations startWorkflow, stopWorkflow, handleWorkflowDelete, handleWorkflowDeleteMultiple, handleWorkflowUpdate, handleInlineUpdate, deleteMessage, deleteFileFromMessage }; }