frontend_nyla/src/hooks/useAutomations.ts
2026-01-24 02:06:54 +01:00

411 lines
15 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,
type Automation,
type AutomationTemplate,
type CreateAutomationRequest,
type UpdateAutomationRequest
} from '../api/automationApi';
// Re-export types
export type { Automation, AutomationTemplate, 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', '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
const handleAutomationToggleActive = useCallback(async (
automationId: string,
currentActive: boolean
): Promise<boolean> => {
try {
await updateAutomationApi(request, automationId, { active: !currentActive });
return true;
} catch (error: any) {
console.error('Error toggling automation active status:', error);
return false;
}
}, [request]);
// Generic inline update handler for FormGeneratorTable
const handleInlineUpdate = useCallback(async (
automationId: string,
changes: Partial<Automation>,
existingRow?: any
) => {
if (!existingRow) {
throw new Error('Existing row data required for inline update');
}
const result = await handleAutomationUpdate(automationId, changes);
if (!result) {
throw new Error(updateError || 'Failed to update');
}
return { success: true };
}, [handleAutomationUpdate, updateError]);
// 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
};
}