frontend_nyla/src/hooks/useAutomations.ts

618 lines
22 KiB
TypeScript

import { useState, useCallback } from 'react';
import { useApiRequest } from './useApi';
import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions';
import {
fetchAutomations as fetchAutomationsApi,
fetchAutomation as fetchAutomationApi,
createAutomationApi,
updateAutomationApi,
deleteAutomationApi,
executeAutomationApi,
fetchAutomationTemplates as fetchTemplatesApi,
fetchAutomationTemplateById,
createAutomationTemplateApi,
updateAutomationTemplateApi,
deleteAutomationTemplateApi,
fetchAutomationTemplateAttributes,
fetchWorkflowActions as fetchWorkflowActionsApi,
type Automation,
type AutomationTemplate,
type TextMultilingual,
type WorkflowAction,
type CreateAutomationRequest,
type UpdateAutomationRequest
} from '../api/automationApi';
// Re-export types
export type {
Automation,
AutomationTemplate,
TextMultilingual,
WorkflowAction,
CreateAutomationRequest,
UpdateAutomationRequest
};
// Attribute definition interface
export interface AttributeDefinition {
name: string;
type: 'text' | 'email' | 'date' | 'checkbox' | 'select' | 'multiselect' | 'number' | 'textarea';
label: string;
description?: string;
required?: boolean;
default?: any;
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
validation?: any;
readonly?: boolean;
editable?: boolean;
visible?: boolean;
order?: number;
sortable?: boolean;
filterable?: boolean;
searchable?: boolean;
width?: number;
minWidth?: number;
maxWidth?: number;
filterOptions?: string[];
}
// Automations list hook
export function useAutomations() {
const [automations, setAutomations] = useState<Automation[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Automation[]>();
const { checkPermission } = usePermissions();
// Fetch attributes from backend - no fallback, errors should be visible
const fetchAttributes = useCallback(async () => {
try {
const response = await api.get('/api/automations/attributes');
let attrs: AttributeDefinition[] = [];
// Backend returns: { attributes: { model: "...", attributes: [...] } }
// So we need to access response.data.attributes.attributes
if (response.data?.attributes?.attributes && Array.isArray(response.data.attributes.attributes)) {
attrs = response.data.attributes.attributes;
} else if (response.data?.attributes && Array.isArray(response.data.attributes)) {
// Fallback: if attributes is directly an array
attrs = response.data.attributes;
} else if (Array.isArray(response.data)) {
attrs = response.data;
}
if (attrs.length === 0) {
console.warn('No attributes returned from backend for AutomationDefinition');
}
setAttributes(attrs);
return attrs;
} catch (error: any) {
console.error('Error fetching automation attributes:', error);
setAttributes([]);
return [];
}
}, []);
// Fetch permissions from backend
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'data.automation.AutomationDefinition');
setPermissions(perms);
return perms;
} catch (error: any) {
console.error('Error fetching automation permissions:', error);
const defaultPerms: UserPermissions = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
setPermissions(defaultPerms);
return defaultPerms;
}
}, [checkPermission]);
const fetchAutomations = useCallback(async () => {
try {
const data = await fetchAutomationsApi(request);
// Handle paginated response
if (data && typeof data === 'object' && 'items' in data) {
const items = Array.isArray((data as any).items) ? (data as any).items : [];
setAutomations(items);
if ((data as any).pagination) {
setPagination((data as any).pagination);
}
} else {
const items = Array.isArray(data) ? data : [];
setAutomations(items);
setPagination(null);
}
} catch (error: any) {
setAutomations([]);
setPagination(null);
}
}, [request]);
// Optimistically remove an automation from the local state
const removeOptimistically = (automationId: string) => {
setAutomations(prev => prev.filter(a => a.id !== automationId));
};
// Optimistically update an automation in the local state
const updateOptimistically = (automationId: string, updateData: Partial<Automation>) => {
setAutomations(prev =>
prev.map(a => a.id === automationId ? { ...a, ...updateData } : a)
);
};
// Fetch a single automation by ID
const fetchAutomationById = useCallback(async (automationId: string): Promise<Automation | null> => {
try {
return await fetchAutomationApi(request, automationId);
} catch (error) {
console.error('Error fetching automation by ID:', error);
return null;
}
}, [request]);
// 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;
minRows?: number;
maxRows?: number;
options?: Array<{ value: string | number; label: string }>;
}> => {
if (!attributes || attributes.length === 0) {
return [];
}
// Fields to show in edit form
const editableFields = ['label', 'schedule', 'template', 'placeholders', 'active'];
return attributes
.filter(attr => editableFields.includes(attr.name) && attr.editable !== false)
.map(attr => {
let fieldType: 'string' | 'boolean' | 'textarea' | 'enum' | 'readonly' = 'string';
if (attr.type === 'checkbox') {
fieldType = 'boolean';
} else if (attr.type === 'textarea' || attr.name === 'template' || attr.name === 'placeholders') {
fieldType = 'textarea';
} else if (attr.type === 'select' && attr.options) {
fieldType = 'enum';
}
const field: any = {
key: attr.name,
label: attr.label || attr.name,
type: fieldType,
editable: attr.editable !== false,
required: attr.required || false,
};
if (fieldType === 'textarea') {
field.minRows = 3;
field.maxRows = 15;
}
if (fieldType === 'enum' && attr.options) {
field.options = Array.isArray(attr.options)
? attr.options.map(opt => ({
value: typeof opt === 'object' ? opt.value : opt,
label: typeof opt === 'object'
? (typeof opt.label === 'object' ? opt.label['en'] || opt.label['de'] : opt.label)
: opt
}))
: [];
}
return field;
});
}, [attributes]);
// Generate create fields from attributes
const generateCreateFieldsFromAttributes = useCallback(() => {
return generateEditFieldsFromAttributes();
}, [generateEditFieldsFromAttributes]);
// Ensure attributes are loaded
const ensureAttributesLoaded = useCallback(async () => {
if (attributes.length === 0) {
await fetchAttributes();
}
}, [attributes.length, fetchAttributes]);
// Initial data fetch
const refetch = useCallback(async () => {
await Promise.all([
fetchAutomations(),
fetchAttributes(),
fetchPermissions()
]);
}, [fetchAutomations, fetchAttributes, fetchPermissions]);
return {
automations,
data: automations, // Alias for FormGenerator compatibility
loading,
error,
refetch,
removeOptimistically,
updateOptimistically,
attributes,
permissions,
pagination,
fetchAutomationById,
generateEditFieldsFromAttributes,
generateCreateFieldsFromAttributes,
ensureAttributesLoaded
};
}
// Automation operations hook
export function useAutomationOperations() {
const { request } = useApiRequest();
const [deletingAutomations, setDeletingAutomations] = useState<Set<string>>(new Set());
const [creatingAutomation, setCreatingAutomation] = useState(false);
const [executingAutomations, setExecutingAutomations] = useState<Set<string>>(new Set());
const [deleteError, setDeleteError] = useState<string | null>(null);
const [createError, setCreateError] = useState<string | null>(null);
const [updateError, setUpdateError] = useState<string | null>(null);
// Create a new automation
const handleAutomationCreate = useCallback(async (data: CreateAutomationRequest): Promise<Automation | null> => {
setCreatingAutomation(true);
setCreateError(null);
try {
// Validate required fields - mandateId and featureInstanceId must be provided
if (!data.mandateId || !data.featureInstanceId) {
throw new Error('mandateId and featureInstanceId are required');
}
// Convert placeholders to ensure all values are strings
if (data.placeholders) {
const convertedPlaceholders: Record<string, string> = {};
for (const [key, value] of Object.entries(data.placeholders)) {
if (value === null || value === undefined) {
convertedPlaceholders[key] = '';
} else if (typeof value === 'object') {
convertedPlaceholders[key] = JSON.stringify(value);
} else {
convertedPlaceholders[key] = String(value);
}
}
data.placeholders = convertedPlaceholders;
}
const newAutomation = await createAutomationApi(request, data);
return newAutomation;
} catch (error: any) {
console.error('Error creating automation:', error);
setCreateError(error.message || 'Failed to create automation');
return null;
} finally {
setCreatingAutomation(false);
}
}, [request]);
// Update an existing automation
const handleAutomationUpdate = useCallback(async (
automationId: string,
data: UpdateAutomationRequest
): Promise<boolean> => {
setUpdateError(null);
try {
await updateAutomationApi(request, automationId, data);
return true;
} catch (error: any) {
console.error('Error updating automation:', error);
setUpdateError(error.message || 'Failed to update automation');
return false;
}
}, [request]);
// Delete an automation
const handleAutomationDelete = useCallback(async (automationId: string): Promise<boolean> => {
setDeletingAutomations(prev => new Set(prev).add(automationId));
setDeleteError(null);
try {
await deleteAutomationApi(request, automationId);
return true;
} catch (error: any) {
console.error('Error deleting automation:', error);
setDeleteError(error.message || 'Failed to delete automation');
return false;
} finally {
setDeletingAutomations(prev => {
const newSet = new Set(prev);
newSet.delete(automationId);
return newSet;
});
}
}, [request]);
// Execute an automation
const handleAutomationExecute = useCallback(async (automationId: string): Promise<any> => {
setExecutingAutomations(prev => new Set(prev).add(automationId));
try {
const result = await executeAutomationApi(request, automationId);
return result;
} catch (error: any) {
console.error('Error executing automation:', error);
throw error;
} finally {
setExecutingAutomations(prev => {
const newSet = new Set(prev);
newSet.delete(automationId);
return newSet;
});
}
}, [request]);
// Toggle automation active status
// NOTE: Backend PUT expects full AutomationDefinition object including id
const handleAutomationToggleActive = useCallback(async (
automationId: string,
currentActive: boolean,
fullAutomation?: Automation
): Promise<boolean> => {
try {
// Build full update data - backend expects AutomationDefinition with all fields
const sourceAutomation = fullAutomation || await fetchAutomationApi(request, automationId);
// Backend requires id in body to match URL parameter
const updateData = {
id: automationId, // MUST match URL parameter
mandateId: sourceAutomation.mandateId,
featureInstanceId: sourceAutomation.featureInstanceId,
label: sourceAutomation.label,
schedule: sourceAutomation.schedule,
template: typeof sourceAutomation.template === 'object'
? JSON.stringify(sourceAutomation.template)
: sourceAutomation.template,
placeholders: sourceAutomation.placeholders || {},
active: !currentActive
};
await updateAutomationApi(request, automationId, updateData as any);
return true;
} catch (error: any) {
console.error('Error toggling automation active status:', error);
return false;
}
}, [request]);
// Generic inline update handler for FormGeneratorTable
// NOTE: Backend PUT requires full object, so we merge changes with existing row data
const handleInlineUpdate = useCallback(async (
automationId: string,
changes: Partial<Automation>,
existingRow?: Automation
) => {
if (!existingRow) {
throw new Error('Existing row data required for inline update');
}
try {
// Merge changes with existing row data and send all required fields
const updateData = {
id: automationId, // MUST match URL parameter
mandateId: existingRow.mandateId,
featureInstanceId: existingRow.featureInstanceId,
label: existingRow.label,
schedule: existingRow.schedule,
template: typeof existingRow.template === 'object'
? JSON.stringify(existingRow.template)
: existingRow.template,
placeholders: existingRow.placeholders || {},
// Apply the changes (e.g., active: true/false)
...changes
};
await updateAutomationApi(request, automationId, updateData as any);
return { success: true };
} catch (error: any) {
console.error('Error in inline update:', error);
throw new Error(error.message || 'Failed to update');
}
}, [request]);
// Fetch templates
const fetchTemplates = useCallback(async (): Promise<AutomationTemplate[]> => {
try {
return await fetchTemplatesApi(request);
} catch (error: any) {
console.error('Error fetching templates:', error);
return [];
}
}, [request]);
return {
handleAutomationCreate,
handleAutomationUpdate,
handleAutomationDelete,
handleAutomationExecute,
handleAutomationToggleActive,
handleInlineUpdate,
fetchTemplates,
deletingAutomations,
creatingAutomation,
executingAutomations,
deleteError,
createError,
updateError
};
}
// ============================================================================
// AUTOMATION TEMPLATES (DB) HOOK
// ============================================================================
/**
* Hook for managing AutomationTemplates from database
*/
export function useAutomationTemplates() {
const [templates, setTemplates] = useState<AutomationTemplate[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [pagination, setPagination] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const { checkPermission } = usePermissions();
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const fetchTemplates = useCallback(async (params?: any) => {
setLoading(true);
setError(null);
try {
const data = await fetchTemplatesApi(request, params);
if (data && typeof data === 'object' && 'items' in data) {
setTemplates(Array.isArray(data.items) ? data.items : []);
if (data.pagination) setPagination(data.pagination);
} else {
setTemplates(Array.isArray(data) ? data : []);
setPagination(null);
}
} catch (e: any) {
console.error('Error fetching templates:', e);
setError(e.message || 'Failed to fetch templates');
setTemplates([]);
setPagination(null);
} finally {
setLoading(false);
}
}, [request]);
const fetchAttributes = useCallback(async () => {
try {
const attrs = await fetchAutomationTemplateAttributes(request);
setAttributes(attrs);
return attrs;
} catch (e: any) {
console.error('Error fetching template attributes:', e);
setAttributes([]);
return [];
}
}, [request]);
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'data.automation.AutomationTemplate');
setPermissions(perms);
return perms;
} catch (e: any) {
console.error('Error fetching template permissions:', e);
const defaultPerms: UserPermissions = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
setPermissions(defaultPerms);
return defaultPerms;
}
}, [checkPermission]);
const getTemplate = useCallback(async (templateId: string) => {
return await fetchAutomationTemplateById(request, templateId);
}, [request]);
const createTemplate = useCallback(async (data: Omit<AutomationTemplate, 'id' | '_createdAt' | '_createdBy'>) => {
return await createAutomationTemplateApi(request, data);
}, [request]);
const updateTemplate = useCallback(async (templateId: string, data: Partial<AutomationTemplate>) => {
return await updateAutomationTemplateApi(request, templateId, data);
}, [request]);
const deleteTemplate = useCallback(async (templateId: string) => {
await deleteAutomationTemplateApi(request, templateId);
}, [request]);
const duplicateTemplate = useCallback(async (templateId: string) => {
const response = await request({ url: `/api/automation-templates/${templateId}/duplicate`, method: 'post' });
return response;
}, [request]);
const refetch = useCallback(async () => {
await Promise.all([
fetchTemplates(),
fetchAttributes(),
fetchPermissions()
]);
}, [fetchTemplates, fetchAttributes, fetchPermissions]);
return {
templates,
data: templates,
attributes,
loading,
error,
permissions,
pagination,
refetch,
fetchTemplates,
fetchAttributes,
fetchPermissions,
getTemplate,
createTemplate,
updateTemplate,
deleteTemplate,
duplicateTemplate,
};
}
// ============================================================================
// WORKFLOW ACTIONS HOOK
// ============================================================================
/**
* Hook for fetching available workflow actions (for Actions panel)
*/
export function useWorkflowActions() {
const [actions, setActions] = useState<WorkflowAction[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const fetchActions = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fetchWorkflowActionsApi(request);
setActions(data);
} catch (e: any) {
console.error('Error fetching workflow actions:', e);
setError(e.message || 'Failed to fetch actions');
setActions([]);
} finally {
setLoading(false);
}
}, [request]);
return {
actions,
loading,
error,
fetchActions
};
}