411 lines
15 KiB
TypeScript
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
|
|
};
|
|
}
|