import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; import { fetchPrompts as fetchPromptsApi, fetchPromptById as fetchPromptByIdApi, createPrompt as createPromptApi, updatePrompt as updatePromptApi, deletePrompt as deletePromptApi, type Prompt, type UpdatePromptData, type AttributeDefinition, type PaginationParams } from '../api/promptApi'; // Re-export types for backward compatibility export type { Prompt, AttributeDefinition, PaginationParams }; // Re-export AttributeOption for backward compatibility export interface AttributeOption { value: string | number; label: string; } // Prompts list hook export function usePrompts() { const [prompts, setPrompts] = 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 [groupLayout, setGroupLayout] = useState(null); const [appliedView, setAppliedView] = useState<{ viewKey?: string; displayName?: string } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { const response = await api.get('/api/attributes/Prompt'); // Extract attributes from response - check if response.data.attributes exists, otherwise check if response.data is an array let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; } else if (Array.isArray(response.data)) { attrs = response.data; } else if (response.data && typeof response.data === 'object') { // Try to find any array property in the response const keys = Object.keys(response.data); for (const key of keys) { if (Array.isArray(response.data[key])) { attrs = response.data[key]; break; } } } setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, []); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'Prompt'); 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 fetchGroupSectionSummaries = useCallback( async (base: { search?: string; filters?: Record; sort?: Array<{ field: string; direction: string }>; viewKey?: string | null; groupField: string; groupDirection?: 'asc' | 'desc'; }) => { const pObj: Record = { page: 1, pageSize: 25, groupByLevels: [ { field: base.groupField, nullLabel: '—', direction: base.groupDirection || 'asc', }, ], }; if (base.search) (pObj as { search?: string }).search = base.search; if (base.filters && Object.keys(base.filters).length) (pObj as { filters: typeof base.filters }).filters = base.filters; if (base.sort?.length) (pObj as { sort: typeof base.sort }).sort = base.sort; if (base.viewKey) (pObj as { viewKey: string }).viewKey = base.viewKey; const { data } = await api.get('/api/prompts', { params: { mode: 'groupSummary', pagination: JSON.stringify(pObj) }, }); return Array.isArray(data?.groups) ? data.groups : []; }, [], ); const refetchForSection = useCallback( async ( paginationParams: any, sectionFilter: Record, parentColumnFilters?: Record, ) => { const mergedFilters = { ...(parentColumnFilters || {}), ...(paginationParams.filters || {}), ...sectionFilter, }; const pObj: Record = { page: paginationParams.page, pageSize: paginationParams.pageSize, filters: mergedFilters, groupByLevels: [], }; if (paginationParams.sort) (pObj as { sort: unknown }).sort = paginationParams.sort; if (paginationParams.search) (pObj as { search: string }).search = paginationParams.search; if (paginationParams.viewKey) (pObj as { viewKey: string }).viewKey = paginationParams.viewKey; const { data } = await api.get('/api/prompts', { params: { pagination: JSON.stringify(pObj) }, }); if (data && typeof data === 'object' && 'items' in data) { return { items: data.items, pagination: data.pagination }; } return { items: [], pagination: null }; }, [], ); const fetchPrompts = useCallback(async (params?: PaginationParams) => { try { const data = await fetchPromptsApi(request, params); // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; setPrompts(items); if (data.pagination) { setPagination(data.pagination); } setGroupLayout(data.groupLayout ?? null); setAppliedView(data.appliedView ?? null); } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; setPrompts(items); setPagination(null); setGroupLayout(null); setAppliedView(null); } } catch (error: any) { // Error is already handled by useApiRequest setPrompts([]); setPagination(null); setGroupLayout(null); setAppliedView(null); } }, [request]); // Optimistically remove a prompt from the local state const removeOptimistically = (promptId: string) => { setPrompts(prevPrompts => prevPrompts.filter(prompt => prompt.id !== promptId)); }; // Optimistically update a prompt in the local state const updateOptimistically = (promptId: string, updateData: { name: string; content: string }) => { setPrompts(prevPrompts => prevPrompts.map(prompt => prompt.id === promptId ? { ...prompt, ...updateData } : prompt ) ); }; // Fetch a single prompt by ID const fetchPromptById = useCallback(async (promptId: string): Promise => { return await fetchPromptByIdApi(request, promptId); }, [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; 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', 'sysCreatedBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Check if this is a content field first - check this BEFORE type mapping const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content') || attr.name.toLowerCase().includes('description'); // Map backend attribute type to form field type // IMPORTANT: Check for content fields FIRST before other type checks 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; if (isContentField) { // Force content fields to ALWAYS be textarea, regardless of attribute type fieldType = 'textarea'; } else 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 => ({ value: opt.value, label: String(opt.label ?? opt.value) })); } 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 => ({ value: opt.value, label: String(opt.label ?? opt.value) })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'text') { // Check if it should be textarea based on name if (attr.name.toLowerCase().includes('text') || attr.name.toLowerCase().includes('note')) { fieldType = 'textarea'; } else { fieldType = '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; // Match create button configuration for prompts if (attr.name === 'name') { required = true; validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return 'Prompt name cannot be empty'; } if (typeof value === 'string' && value.length > 100) { return 'Prompt name cannot exceed 100 characters'; } return null; }; } else if (isContentField || attr.name === 'content') { required = true; minRows = 6; // Match create button: minRows: 6 maxRows = 12; // Match create button: maxRows: 12 validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return 'Prompt content cannot be empty'; } if (typeof value === 'string' && value.length > 10000) { return 'Prompt content cannot exceed 10,000 characters'; } return null; }; } else if (fieldType === 'textarea') { // Default textarea settings for other textarea fields 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 }; }); // Ensure we always return at least the basic fields if attributes exist // This handles cases where all fields might be filtered out if (editableFields.length === 0 && attributes.length > 0) { // If all fields were filtered out, include all attributes as editable string fields // Match create button configuration return attributes.map(attr => { const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content'); const fieldType = isContentField ? 'textarea' as const : 'string' as const; let required = false; 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 'Prompt name cannot be empty'; } if (typeof value === 'string' && value.length > 100) { return 'Prompt name cannot exceed 100 characters'; } return null; }; } else if (isContentField) { required = true; minRows = 6; // Match create button maxRows = 12; // Match create button validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return 'Prompt content cannot be empty'; } if (typeof value === 'string' && value.length > 10000) { return 'Prompt content cannot exceed 10,000 characters'; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: true, required, validator, minRows, maxRows }; }); } return editableFields; }, [attributes]); // Generate create fields from attributes dynamically // For prompts, the create form is essentially the same as edit form const generateCreateFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; placeholder?: string; }> => { if (!attributes || attributes.length === 0) { return []; } const createFields = attributes .filter(attr => { // Filter out non-editable fields and auto-generated fields for create forms if (attr.readonly === true || attr.editable === false) { return false; } // Filter out ID fields and other auto-generated fields const nonEditableFields = ['id', 'mandateId', 'sysCreatedBy', '_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 minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; // Map backend types to form field types // Cast to string to handle all possible backend type values const attrType = attr.type as string; if (attrType === 'checkbox' || attrType === 'boolean') { fieldType = 'boolean'; } else if (attrType === 'email') { fieldType = 'email'; } else if (attrType === 'timestamp' || attrType === 'date' || attrType === 'time') { fieldType = 'date'; } else if (attrType === 'textarea') { fieldType = 'textarea'; // Set default rows for textarea fields minRows = 6; maxRows = 12; } else if (attr.name === 'content' || attr.name.toLowerCase().includes('content')) { // Content fields should be textarea fieldType = 'textarea'; minRows = 6; maxRows = 12; } // Determine if required and build validator const required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; // Required string validation if (required && (fieldType === 'string' || fieldType === 'textarea')) { validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return `${attr.label} is required`; } if (attr.name === 'name' && typeof value === 'string' && value.length > 100) { return 'Prompt name cannot exceed 100 characters'; } if (attr.name === 'content' && typeof value === 'string' && value.length > 10000) { return 'Prompt content cannot exceed 10,000 characters'; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, required, validator, minRows, maxRows }; }); return createFields; }, [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 useEffect(() => { fetchAttributes(); fetchPermissions(); }, [fetchAttributes, fetchPermissions]); // Initial fetch useEffect(() => { fetchPrompts(); }, [fetchPrompts]); return { prompts, loading, error, refetch: fetchPrompts, removeOptimistically, updateOptimistically, attributes, permissions, pagination, groupLayout, appliedView, fetchPromptById, generateEditFieldsFromAttributes, generateCreateFieldsFromAttributes, ensureAttributesLoaded, fetchGroupSectionSummaries, refetchForSection, }; } // Prompt operations hook export function usePromptOperations() { const [deletingPrompts, setDeletingPrompts] = useState>(new Set()); const [creatingPrompt, setCreatingPrompt] = useState(false); const { request, isLoading } = useApiRequest(); const [deleteError, setDeleteError] = useState(null); const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); const handlePromptDelete = async (promptId: string) => { setDeleteError(null); setDeletingPrompts(prev => new Set(prev).add(promptId)); try { await deletePromptApi(request, promptId); // 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); return false; } finally { setDeletingPrompts(prev => { const newSet = new Set(prev); newSet.delete(promptId); return newSet; }); } }; const handlePromptCreate = async (promptData: { name: string; content: string }) => { setCreateError(null); setCreatingPrompt(true); try { // mandateId wird nicht mehr vom Client gesendet // Das Backend bestimmt den Kontext über die instanceId im Request const requestBody = { name: promptData.name, content: promptData.content }; const newPrompt = await createPromptApi(request, requestBody); return { success: true, promptData: newPrompt }; } catch (error: any) { setCreateError(error.message); return { success: false, error: error.message }; } finally { setCreatingPrompt(false); } }; const handlePromptUpdate = async (promptId: string, updateData: Record, _originalData?: any) => { setUpdateError(null); try { // Pass all provided fields (supports partial inline updates like isSystem toggle) const { id, mandateId, sysCreatedBy, sysCreatedAt, sysModifiedAt, _permissions, ...requestBody } = updateData; const updatedPrompt = await updatePromptApi(request, promptId, requestBody as UpdatePromptData); return { success: true, promptData: updatedPrompt }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update prompt'; 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 }; } }; // Generic inline update handler for FormGeneratorTable const handleInlineUpdate = async (promptId: string, changes: Record) => { const result = await handlePromptUpdate(promptId, changes); if (!result.success) { throw new Error(result.error || 'Failed to update'); } return result; }; return { deletingPrompts, creatingPrompt, deleteError, createError, updateError, handlePromptDelete, handlePromptCreate, handlePromptUpdate, handleInlineUpdate, isLoading }; }