From b4eb3265c23317e74e86ce1711f42212be7e38c5 Mon Sep 17 00:00:00 2001 From: Md Afiq Iskandar Date: Thu, 10 Jul 2025 11:08:16 +0800 Subject: [PATCH] Enhance Process Builder with HTML and Subprocess Node Features - Introduced new HTML and Subprocess nodes in ProcessBuilderComponents.vue, allowing users to add custom HTML content and execute subprocesses within the process flow. - Updated ProcessFlowNodes.js to include HtmlNode and SubprocessNode components with appropriate properties and rendering logic. - Enhanced ProcessFlowCanvas.vue to manage the new node types effectively, ensuring proper integration with existing flow functionalities. - Improved index.vue to support configuration modals for HTML and Subprocess nodes, enhancing user interaction and customization options. - Refactored process management logic to accommodate new node types, ensuring seamless integration and consistent user experience across the process builder. --- .../process-flow/HtmlNodeConfiguration.vue | 700 +++++++++++ .../HtmlNodeConfigurationModal.vue | 116 ++ .../process-flow/ProcessBuilderComponents.vue | 102 +- components/process-flow/ProcessFlowCanvas.vue | 142 ++- components/process-flow/ProcessFlowNodes.js | 148 ++- .../SubprocessNodeConfiguration.vue | 204 ++++ .../SubprocessNodeConfigurationModal.vue | 101 ++ docs/json/form/customScript.js | 877 +------------- docs/json/form/formComponents.json | 1040 ++--------------- .../process-builder/processDefinition.json | 588 ++++++++-- .../process-builder/processVariables.json | 245 ++++ pages/process-builder/index.vue | 356 +++++- stores/processBuilder.js | 38 +- 13 files changed, 2585 insertions(+), 2072 deletions(-) create mode 100644 components/process-flow/HtmlNodeConfiguration.vue create mode 100644 components/process-flow/HtmlNodeConfigurationModal.vue create mode 100644 components/process-flow/SubprocessNodeConfiguration.vue create mode 100644 components/process-flow/SubprocessNodeConfigurationModal.vue diff --git a/components/process-flow/HtmlNodeConfiguration.vue b/components/process-flow/HtmlNodeConfiguration.vue new file mode 100644 index 0000000..bf98eb7 --- /dev/null +++ b/components/process-flow/HtmlNodeConfiguration.vue @@ -0,0 +1,700 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/HtmlNodeConfigurationModal.vue b/components/process-flow/HtmlNodeConfigurationModal.vue new file mode 100644 index 0000000..7ba6338 --- /dev/null +++ b/components/process-flow/HtmlNodeConfigurationModal.vue @@ -0,0 +1,116 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/ProcessBuilderComponents.vue b/components/process-flow/ProcessBuilderComponents.vue index 29ef021..2b6cb0f 100644 --- a/components/process-flow/ProcessBuilderComponents.vue +++ b/components/process-flow/ProcessBuilderComponents.vue @@ -198,13 +198,46 @@ const availableComponents = [ defaultProps: { label: 'Script Task', data: { - description: 'Data transformation script', - scriptCode: '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n', + description: 'Execute JavaScript code', + scriptCode: '', scriptLanguage: 'javascript', inputVariables: [], + outputVariables: [] + } + } + }, + { + type: 'html', + name: 'HTML Content', + category: 'Core', + icon: 'material-symbols:code', + description: 'Display custom HTML content', + defaultProps: { + label: 'HTML Content', + data: { + description: 'Custom HTML content', + htmlCode: '\n
\n

Custom HTML Content

\n

This is a custom HTML node that can display rich content.

\n
', + cssCode: '', + jsCode: '', + inputVariables: [], outputVariables: [], - continueOnError: false, - errorVariable: 'scriptError' + allowVariableAccess: true, + autoRefresh: false + } + } + }, + { + type: 'subprocess', + name: 'Sub Process', + category: 'Core', + icon: 'material-symbols:hub-outline', + description: 'Execute another process as a sub-process', + defaultProps: { + label: 'Sub Process', + data: { + description: 'Executes another process', + subprocessId: null, + subprocessName: '' } } }, @@ -289,66 +322,6 @@ const availableComponents = [ shapeType: 'text-annotation' } } - }, - { - type: 'process-group', - name: 'Process Group', - category: 'Shape', - icon: 'material-symbols:group-work', - description: 'Group container for organizing related processes', - defaultProps: { - label: '', - data: { - description: '', - width: 400, - height: 300, - backgroundColor: '#f0f9ff', - borderColor: '#0284c7', - textColor: '#0369a1', - isShape: true, - shapeType: 'process-group' - } - } - }, - { - type: 'hexagon-shape', - name: 'Hexagon', - category: 'Shape', - icon: 'material-symbols:hexagon', - description: 'Hexagon shape for special processes or decision points', - defaultProps: { - label: '', - data: { - description: '', - width: 200, - height: 150, - backgroundColor: '#f8fafc', - borderColor: '#e2e8f0', - textColor: '#475569', - isShape: true, - shapeType: 'hexagon' - } - } - }, - { - type: 'trapezoid-shape', - name: 'Trapezoid', - category: 'Shape', - icon: 'material-symbols:change-history', - description: 'Trapezoid shape for data processing or manual tasks', - defaultProps: { - label: '', - data: { - description: '', - width: 220, - height: 120, - backgroundColor: '#f8fafc', - borderColor: '#e2e8f0', - textColor: '#475569', - isShape: true, - shapeType: 'trapezoid' - } - } } ]; @@ -389,6 +362,7 @@ const onDragStart = (event, component) => { // Add a component directly via click const addComponent = (component) => { + return; // Use same format as drag operation for consistency const componentData = { type: component.type, diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index 90e56d2..1dc78e4 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -334,7 +334,6 @@ watch(() => props.initialNodes, async (newNodes, oldNodes) => { isUpdatingNodes.value = true; try { - // console.log('ProcessFlowCanvas: Updating nodes, count:', newNodes.length); // Instead of clearing all nodes, sync them intelligently const currentNodeIds = new Set(nodes.value.map(n => n.id)); @@ -393,15 +392,37 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun isUpdatingEdges.value = true; try { - // console.log('ProcessFlowCanvas: Updating edges, count:', newEdges.length, 'nodeCount:', nodeCount); // Instead of clearing all edges, sync them intelligently const currentEdgeIds = new Set(edges.value.map(e => e.id)); const newEdgeIds = new Set(newEdges.map(e => e.id)); - // Remove edges that are no longer in the new list - const edgesToRemove = edges.value.filter(edge => !newEdgeIds.has(edge.id)); + + // CRITICAL: Be more conservative about edge removal + // Only remove edges that are definitely not in the new list AND whose nodes don't exist + const edgesToRemove = edges.value.filter(edge => { + const isInNewList = newEdgeIds.has(edge.id); + + if (isInNewList) { + return false; // Don't remove if it's in the new list + } + + // Double-check that both source and target nodes still exist + const sourceExists = nodes.value.some(node => node.id === edge.source); + const targetExists = nodes.value.some(node => node.id === edge.target); + + // Only remove if nodes don't exist (orphaned edges) + const shouldRemove = !sourceExists || !targetExists; + + if (shouldRemove) { + console.log(`๐Ÿ—‘๏ธ Removing orphaned edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`); + } + + return shouldRemove; + }); + if (edgesToRemove.length > 0) { + console.log('๐Ÿ—‘๏ธ Removing edges:', edgesToRemove.map(e => `${e.source}->${e.target} (${e.id})`)); removeEdges(edgesToRemove); } @@ -412,7 +433,7 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun const targetExists = nodes.value.some(node => node.id === edge.target); if (!sourceExists || !targetExists) { - console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`); + console.warn(`โš ๏ธ Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`); return false; } @@ -423,10 +444,13 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun const edgesToAdd = validEdges.filter(edge => !currentEdgeIds.has(edge.id)); if (edgesToAdd.length > 0) { + console.log('โž• Adding new edges:', edgesToAdd.map(e => `${e.source}->${e.target} (${e.id})`)); + // Ensure all edges have proper handle specifications const edgesWithHandles = edgesToAdd.map(edge => { // IMPORTANT: If edge already has sourceHandle and targetHandle, preserve them exactly as they are if (edge.sourceHandle && edge.targetHandle) { + console.log(`๐Ÿ”— Edge ${edge.id} already has handles: ${edge.sourceHandle} -> ${edge.targetHandle}`); return edge; } @@ -457,6 +481,7 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun } } + console.log(`๐Ÿ”— Generated handles for edge ${edge.id}: ${sourceHandle} -> ${targetHandle}`); return { ...edge, sourceHandle, @@ -465,10 +490,10 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun }); addEdges([...edgesWithHandles]); // Create a copy to avoid reactivity issues - // console.log('ProcessFlowCanvas: Successfully added edges with handles:', edgesWithHandles.length); } // Update existing edges that have changed - IMPORTANT: preserve handle positions + let updatedEdgeCount = 0; newEdges.forEach(newEdge => { const existingEdge = edges.value.find(e => e.id === newEdge.id); if (existingEdge) { @@ -481,15 +506,21 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun if (hasChanges) { Object.assign(existingEdge, { label: newEdge.label, - // Preserve existing handles if they exist + // CRITICAL: Preserve existing handles if they exist sourceHandle: existingEdge.sourceHandle || newEdge.sourceHandle, targetHandle: existingEdge.targetHandle || newEdge.targetHandle, style: newEdge.style ? { ...newEdge.style } : undefined }); + updatedEdgeCount++; } } }); + + if (updatedEdgeCount > 0) { + console.log('๐Ÿ”„ Updated existing edges:', updatedEdgeCount); + } } + } finally { // Use a small delay to prevent immediate re-triggering setTimeout(() => { @@ -497,7 +528,7 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun }, 50); } } else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0 && nodeCount === 0) { - // console.log('ProcessFlowCanvas: Edges provided but no nodes yet, waiting...'); + console.log('โš ๏ธ ProcessFlowCanvas: Edges provided but no nodes yet, waiting...'); } }, { deep: true }); @@ -723,30 +754,31 @@ function removeNode(nodeId) { return nodeToRemove; } -// Add explicit sync method to manually update canvas +// Manual sync function for explicit canvas updates function syncCanvas(newNodes, newEdges) { - // Force clear the updating flags first to ensure we can process - isUpdatingNodes.value = false; - isUpdatingEdges.value = false; + console.log('๐Ÿ”„ Manual canvas sync requested - nodes:', newNodes?.length || 0, 'edges:', newEdges?.length || 0); - // Wait a moment for any ongoing operations to complete - setTimeout(() => { + // Use a small delay to ensure any pending Vue Flow operations complete + setTimeout(async () => { try { - // Sync nodes first if (newNodes && Array.isArray(newNodes)) { const currentNodeIds = new Set(nodes.value.map(n => n.id)); const newNodeIds = new Set(newNodes.map(n => n.id)); + console.log('๐Ÿ“Š Current canvas nodes:', currentNodeIds.size, 'New nodes:', newNodeIds.size); + // Remove nodes that are no longer in the new list const nodesToRemove = nodes.value.filter(node => !newNodeIds.has(node.id)); if (nodesToRemove.length > 0) { + console.log('๐Ÿ—‘๏ธ Removing nodes:', nodesToRemove.map(n => n.id)); removeNodes(nodesToRemove); } // Add new nodes that aren't already present const nodesToAdd = newNodes.filter(node => !currentNodeIds.has(node.id)); if (nodesToAdd.length > 0) { + console.log('โž• Adding new nodes:', nodesToAdd.map(n => n.id)); addNodes([...nodesToAdd]); } @@ -764,28 +796,45 @@ function syncCanvas(newNodes, newEdges) { updateNodeInternals([newNode.id]); } }); + + // Wait for nodes to be fully processed before handling edges + await nextTick(); + await new Promise(resolve => setTimeout(resolve, 100)); } - + console.log('๐Ÿ“Š Canvas state after node sync - nodes:', nodes.value.length, 'edges:', edges.value.length); - // Sync edges after nodes are updated + // Sync edges after nodes are updated - CRITICAL: Only if we have nodes if (newEdges && Array.isArray(newEdges) && nodes.value.length > 0) { const currentEdgeIds = new Set(edges.value.map(e => e.id)); const newEdgeIds = new Set(newEdges.map(e => e.id)); - + console.log('๐Ÿ“Š Current canvas edges:', currentEdgeIds.size, 'New edges:', newEdgeIds.size); + + // CRITICAL: Only remove edges that are definitely not in the new list + // Be more conservative about edge removal to prevent accidental deletions + const edgesToRemove = edges.value.filter(edge => { + const shouldRemove = !newEdgeIds.has(edge.id); + if (shouldRemove) { + // Double-check that both source and target nodes still exist + const sourceExists = nodes.value.some(node => node.id === edge.source); + const targetExists = nodes.value.some(node => node.id === edge.target); + + // Only remove if the edge is truly not needed OR if nodes don't exist + return !sourceExists || !targetExists; + } + return false; + }); - // Remove edges that are no longer in the new list - const edgesToRemove = edges.value.filter(edge => !newEdgeIds.has(edge.id)); if (edgesToRemove.length > 0) { - + console.log('๐Ÿ—‘๏ธ Removing edges:', edgesToRemove.map(e => `${e.source}->${e.target} (${e.id})`)); removeEdges(edgesToRemove); } // Add new edges that aren't already present const edgesToAdd = newEdges.filter(edge => !currentEdgeIds.has(edge.id)); if (edgesToAdd.length > 0) { - + console.log('โž• Adding new edges:', edgesToAdd.map(e => `${e.source}->${e.target} (${e.id})`)); // Verify nodes exist and add handles const validEdges = edgesToAdd.filter(edge => { @@ -793,19 +842,19 @@ function syncCanvas(newNodes, newEdges) { const targetExists = nodes.value.some(node => node.id === edge.target); if (!sourceExists || !targetExists) { - console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`); + console.warn(`โš ๏ธ Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`); return false; } return true; }); - + console.log('โœ… Valid edges to add:', validEdges.length); const edgesWithHandles = validEdges.map(edge => { // If edge already has sourceHandle and targetHandle, use them if (edge.sourceHandle && edge.targetHandle) { - + console.log(`๐Ÿ”— Edge ${edge.id} already has handles: ${edge.sourceHandle} -> ${edge.targetHandle}`); return edge; } @@ -833,36 +882,51 @@ function syncCanvas(newNodes, newEdges) { } } - + console.log(`๐Ÿ”— Generated handles for edge ${edge.id}: ${sourceHandle} -> ${targetHandle}`); return { ...edge, sourceHandle, targetHandle }; }); if (edgesWithHandles.length > 0) { - + console.log('โœ… Adding edges with handles:', edgesWithHandles.length); addEdges([...edgesWithHandles]); } } - // Update existing edges + // Update existing edges - preserve handles and only update changed properties + let updatedEdgeCount = 0; newEdges.forEach(newEdge => { const existingEdge = edges.value.find(e => e.id === newEdge.id); if (existingEdge) { - Object.assign(existingEdge, { - label: newEdge.label, - sourceHandle: newEdge.sourceHandle, - targetHandle: newEdge.targetHandle, - style: newEdge.style ? { ...newEdge.style } : undefined - }); + // Check if update is needed + const needsUpdate = ( + existingEdge.label !== newEdge.label || + JSON.stringify(existingEdge.style) !== JSON.stringify(newEdge.style) + ); + + if (needsUpdate) { + Object.assign(existingEdge, { + label: newEdge.label, + // CRITICAL: Preserve existing handles if they exist + sourceHandle: existingEdge.sourceHandle || newEdge.sourceHandle, + targetHandle: existingEdge.targetHandle || newEdge.targetHandle, + style: newEdge.style ? { ...newEdge.style } : undefined + }); + updatedEdgeCount++; + } } }); + + if (updatedEdgeCount > 0) { + console.log('๐Ÿ”„ Updated existing edges:', updatedEdgeCount); + } } else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0) { - console.warn('Cannot add edges: nodes not ready. Node count:', nodes.value.length); + console.warn('โš ๏ธ Cannot add edges: nodes not ready. Node count:', nodes.value.length); } - + console.log('โœ… Canvas sync completed - final state: nodes:', nodes.value.length, 'edges:', edges.value.length); } catch (error) { - console.error('Error during canvas sync:', error); + console.error('โŒ Error during canvas sync:', error); } }, 50); // Small delay to allow any pending operations to complete } @@ -1187,6 +1251,10 @@ function fromObject(flowObject) { color: #607D8B; } +:deep(.node-html .custom-node-icon .material-icons) { + color: #0ea5e9; +} + :deep(.custom-node-title) { font-weight: 500; flex-grow: 1; diff --git a/components/process-flow/ProcessFlowNodes.js b/components/process-flow/ProcessFlowNodes.js index cd82aea..23ccb73 100644 --- a/components/process-flow/ProcessFlowNodes.js +++ b/components/process-flow/ProcessFlowNodes.js @@ -144,6 +144,11 @@ const CustomNode = markRaw({ defaultBorder = '#0ea5e9'; defaultText = '#0284c7'; break; + case 'subprocess': + defaultBg = '#f0fdfa'; + defaultBorder = '#14b8a6'; + defaultText = '#134e4a'; + break; } } @@ -444,6 +449,67 @@ export const ScriptNode = markRaw({ } }); +// HTML node +export const HtmlNode = markRaw({ + props: ['id', 'type', 'label', 'selected', 'data'], + computed: { + nodeLabel() { + // Get label from either prop or data, with fallback + return this.label || (this.data && this.data.label) || 'HTML Content'; + }, + hasHtmlContent() { + return !!this.data?.htmlCode; + }, + hasCssContent() { + return !!this.data?.cssCode; + }, + hasJsContent() { + return !!this.data?.jsCode; + }, + contentSummary() { + const parts = []; + if (this.hasHtmlContent) parts.push('HTML'); + if (this.hasCssContent) parts.push('CSS'); + if (this.hasJsContent) parts.push('JS'); + + return parts.length > 0 ? parts.join(' + ') : 'Empty'; + } + }, + render() { + return h(CustomNode, { + id: this.id, + type: 'html', + label: this.nodeLabel, + selected: this.selected, + data: this.data, + onClick: () => this.$emit('node-click', this.id) + }, { + icon: () => h('i', { class: 'material-icons text-blue-500' }, 'code'), + default: () => h('div', { class: 'node-details' }, [ + h('p', { class: 'node-description' }, this.data?.description || 'Custom HTML content'), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Content:'), + h('span', { + class: 'node-rule-detail-value ml-1 font-medium text-blue-600' + }, 'HTML') + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Status:'), + h('span', { + class: 'node-rule-detail-value ml-1 font-medium text-gray-600' + }, this.hasHtmlContent ? 'Configured' : 'Not configured') + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Variables:'), + h('span', { + class: 'node-rule-detail-value ml-1 font-medium text-blue-600' + }, this.data?.allowVariableAccess ? 'Enabled' : 'Disabled') + ]) + ]) + }); + } +}); + // API Call node export const ApiCallNode = markRaw({ props: ['id', 'type', 'label', 'selected', 'data'], @@ -650,8 +716,52 @@ export const NotificationNode = markRaw({ } }); +// Subprocess node +export const SubprocessNode = markRaw({ + props: ['id', 'type', 'label', 'selected', 'data'], + computed: { + nodeLabel() { + return this.label || (this.data && this.data.label) || 'Sub Process'; + }, + subprocessName() { + return this.data?.subprocessName || 'None selected'; + }, + isConfigured() { + return !!this.data?.subprocessId; + } + }, + render() { + return h(CustomNode, { + id: this.id, + type: 'subprocess', + label: this.nodeLabel, + selected: this.selected, + data: this.data, + onClick: () => this.$emit('node-click', this.id) + }, { + icon: () => h('i', { class: 'material-icons text-teal-500' }, 'hub'), + default: () => h('div', { class: 'node-details' }, [ + h('p', { class: 'node-description' }, this.data?.description || 'Executes another process'), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Process:'), + h('span', { + class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-teal-600' : 'node-rule-detail-value ml-1 italic text-gray-400' + }, this.subprocessName) + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Status:'), + h('span', { + class: 'node-rule-detail-value ml-1 font-medium', + 'class': this.isConfigured ? 'text-green-600' : 'text-red-600' + }, this.isConfigured ? 'Configured' : 'Not configured') + ]) + ]) + }); + } +}); + // Shape Components (Design Elements) -const HorizontalSwimlaneShape = { +const HorizontalSwimlaneShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -693,9 +803,9 @@ const HorizontalSwimlaneShape = { ` -}; +}); -const VerticalSwimlaneShape = { +const VerticalSwimlaneShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -739,9 +849,9 @@ const VerticalSwimlaneShape = { ` -}; +}); -const RectangleShape = { +const RectangleShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -783,9 +893,9 @@ const RectangleShape = { ` -}; +}); -const TextAnnotationShape = { +const TextAnnotationShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -828,9 +938,9 @@ const TextAnnotationShape = { ` -}; +}); -const ProcessGroupShape = { +const ProcessGroupShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -873,10 +983,10 @@ const ProcessGroupShape = { ` -}; +}); // Hexagon Shape Component -const HexagonShape = { +const HexagonShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -909,10 +1019,10 @@ const HexagonShape = { ` -}; +}); // Trapezoid Shape Component -const TrapezoidShape = { +const TrapezoidShape = markRaw({ props: ['id', 'data', 'selected', 'label'], computed: { shapeStyle() { @@ -945,7 +1055,7 @@ const TrapezoidShape = { ` -}; +}); // Export the node types object to use with Vue Flow export const nodeTypes = { @@ -957,6 +1067,8 @@ export const nodeTypes = { 'business-rule': BusinessRuleNode, api: ApiCallNode, notification: NotificationNode, + html: HtmlNode, // Add the new HtmlNode to the nodeTypes object + subprocess: SubprocessNode, // Shape nodes 'swimlane-horizontal': HorizontalSwimlaneShape, 'swimlane-vertical': VerticalSwimlaneShape, @@ -1586,6 +1698,14 @@ export const nodeStyles = ` border-left: 4px solid #6b7280; /* Gray border to match icon color */ } +/* HTML node styling */ +.node-html { + border-left: 4px solid #0ea5e9; /* Blue border to match icon color */ + background-color: #f0f9ff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + /* Shape node styles */ .shape-node { position: relative; diff --git a/components/process-flow/SubprocessNodeConfiguration.vue b/components/process-flow/SubprocessNodeConfiguration.vue new file mode 100644 index 0000000..73e8aaf --- /dev/null +++ b/components/process-flow/SubprocessNodeConfiguration.vue @@ -0,0 +1,204 @@ + + + \ No newline at end of file diff --git a/components/process-flow/SubprocessNodeConfigurationModal.vue b/components/process-flow/SubprocessNodeConfigurationModal.vue new file mode 100644 index 0000000..aa53af0 --- /dev/null +++ b/components/process-flow/SubprocessNodeConfigurationModal.vue @@ -0,0 +1,101 @@ + + + \ No newline at end of file diff --git a/docs/json/form/customScript.js b/docs/json/form/customScript.js index 0c4299b..babe8a0 100644 --- a/docs/json/form/customScript.js +++ b/docs/json/form/customScript.js @@ -1,878 +1,41 @@ -this.hideField("form_jeniskp_1"); -this.hideField("form_jeniskp_2"); -this.hideField("form_jeniskp_3"); - -this.onFieldChange("select_1", (value) => { - this.hideField("form_jeniskp_1"); - this.hideField("form_jeniskp_2"); - this.hideField("form_jeniskp_3"); - if (value && value.trim()) { - if (value == "jeniskp_1") this.showField("form_jeniskp_1"); - if (value == "jeniskp_2") this.showField("form_jeniskp_2"); - if (value == "jeniskp_3") this.showField("form_jeniskp_3"); +// Conditional logic for showing 'Nyatakan keperluan lain' field +onFieldChange("keperluan_mendesak", (value) => { + if (Array.isArray(value) && value.includes("lain_lain")) { + showField("keperluan_lain_nyatakan"); + } else { + hideField("keperluan_lain_nyatakan"); + setField("keperluan_lain_nyatakan", ""); } }); -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); +// Show success message on form load +showInfo( + "Sila lengkapkan semua maklumat yang diperlukan untuk penilaian awal." +); // Conditional Logic Script -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); - -// Hide "Nyatakan Hubungan Lain-lain" initially -this.hideField("hubungan_lain_nyatakan"); - -// Show/hide relationship specification field -this.onFieldChange("hubungan_keluarga", (value) => { - if (value && value.includes("lain_lain")) { - this.showField("hubungan_lain_nyatakan"); - } else { - this.hideField("hubungan_lain_nyatakan"); - } -}); - -// Hide "Sebab Pembayaran Tunai" initially -this.hideField("sebab_tunai"); - -// Show/hide cash payment reason field -this.onFieldChange("cara_pembayaran", (value) => { - if (value && value.includes("tunai")) { - this.showField("sebab_tunai"); - } else { - this.hideField("sebab_tunai"); - } -}); - -// Hide education specification field initially -this.hideField("pendidikan_lain_tanggungan"); - -// Show/hide education specification field -this.onFieldChange("pendidikan_tertinggi_tanggungan", (value) => { - if (value && value.includes("lain_lain")) { - this.showField("pendidikan_lain_tanggungan"); - } else { - this.hideField("pendidikan_lain_tanggungan"); - } -}); - -// Hide school information initially -this.hideField("maklumat_sekolah"); - -// Show/hide school information based on schooling status -this.onFieldChange("bersekolah_tanggungan", (value) => { - if (value === "ya") { - this.showField("maklumat_sekolah"); - } else { - this.hideField("maklumat_sekolah"); - } -}); - -// Handle repeating group conditional logic for each dependent -this.onFieldChange("tanggungan_maklumat", (value) => { - if (value && Array.isArray(value)) { - value.forEach((item, index) => { - // Handle race specification for each dependent - if (item.bangsa_tanggungan !== "lain_lain") { - // Hide the specification field for this item - const fieldName = `tanggungan_maklumat[${index}].bangsa_lain_tanggungan`; - // Note: Repeating group field hiding requires specific handling - } - }); - } -}); - -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); - -// Conditional logic for field: hubungan_lain_nyatakan -onFieldChange("hubungan_keluarga", function () { +// Conditional logic for field: keperluan_lain_nyatakan +onFieldChange("keperluan_mendesak", function () { if ( - !String(getField("hubungan_keluarga") || "") + String(getField("keperluan_mendesak") || "") .toLowerCase() .includes("lain_lain".toLowerCase()) ) { - hideField("hubungan_lain_nyatakan"); + showField("keperluan_lain_nyatakan"); } else { - showField("hubungan_lain_nyatakan"); + hideField("keperluan_lain_nyatakan"); } }); -// Initial evaluation for field: hubungan_lain_nyatakan +// Initial evaluation for field: keperluan_lain_nyatakan (function () { if ( - !String(getField("hubungan_keluarga") || "") + String(getField("keperluan_mendesak") || "") .toLowerCase() .includes("lain_lain".toLowerCase()) ) { - hideField("hubungan_lain_nyatakan"); + showField("keperluan_lain_nyatakan"); } else { - showField("hubungan_lain_nyatakan"); - } -})(); - -// Conditional logic for field: sebab_tunai -onFieldChange("cara_pembayaran", function () { - if ( - !String(getField("cara_pembayaran") || "") - .toLowerCase() - .includes("tunai".toLowerCase()) - ) { - hideField("sebab_tunai"); - } else { - showField("sebab_tunai"); - } -}); - -// Initial evaluation for field: sebab_tunai -(function () { - if ( - !String(getField("cara_pembayaran") || "") - .toLowerCase() - .includes("tunai".toLowerCase()) - ) { - hideField("sebab_tunai"); - } else { - showField("sebab_tunai"); - } -})(); - -// Conditional logic for field: pendidikan_lain_tanggungan -onFieldChange("pendidikan_tertinggi_tanggungan", function () { - if ( - !String(getField("pendidikan_tertinggi_tanggungan") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("pendidikan_lain_tanggungan"); - } else { - showField("pendidikan_lain_tanggungan"); - } -}); - -// Initial evaluation for field: pendidikan_lain_tanggungan -(function () { - if ( - !String(getField("pendidikan_tertinggi_tanggungan") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("pendidikan_lain_tanggungan"); - } else { - showField("pendidikan_lain_tanggungan"); - } -})(); - -// Conditional logic for field: maklumat_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("maklumat_sekolah"); - } else { - showField("maklumat_sekolah"); - } -}); - -// Initial evaluation for field: maklumat_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("maklumat_sekolah"); - } else { - showField("maklumat_sekolah"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); - -// Conditional logic for field: hubungan_lain_nyatakan -onFieldChange("hubungan_keluarga", function () { - if ( - !String(getField("hubungan_keluarga") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("hubungan_lain_nyatakan"); - } else { - showField("hubungan_lain_nyatakan"); - } -}); - -// Initial evaluation for field: hubungan_lain_nyatakan -(function () { - if ( - !String(getField("hubungan_keluarga") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("hubungan_lain_nyatakan"); - } else { - showField("hubungan_lain_nyatakan"); - } -})(); - -// Conditional logic for field: bangsa_lain_tanggungan -onFieldChange("bangsa_tanggungan", function () { - if (getField("bangsa_tanggungan") !== "lain_lain") { - hideField("bangsa_lain_tanggungan"); - } else { - showField("bangsa_lain_tanggungan"); - } -}); - -// Initial evaluation for field: bangsa_lain_tanggungan -(function () { - if (getField("bangsa_tanggungan") !== "lain_lain") { - hideField("bangsa_lain_tanggungan"); - } else { - showField("bangsa_lain_tanggungan"); - } -})(); - -// Conditional logic for field: sebab_tunai -onFieldChange("cara_pembayaran", function () { - if ( - !String(getField("cara_pembayaran") || "") - .toLowerCase() - .includes("tunai".toLowerCase()) - ) { - hideField("sebab_tunai"); - } else { - showField("sebab_tunai"); - } -}); - -// Initial evaluation for field: sebab_tunai -(function () { - if ( - !String(getField("cara_pembayaran") || "") - .toLowerCase() - .includes("tunai".toLowerCase()) - ) { - hideField("sebab_tunai"); - } else { - showField("sebab_tunai"); - } -})(); - -// Conditional logic for field: pendidikan_lain_tanggungan -onFieldChange("pendidikan_tertinggi_tanggungan", function () { - if ( - !String(getField("pendidikan_tertinggi_tanggungan") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("pendidikan_lain_tanggungan"); - } else { - showField("pendidikan_lain_tanggungan"); - } -}); - -// Initial evaluation for field: pendidikan_lain_tanggungan -(function () { - if ( - !String(getField("pendidikan_tertinggi_tanggungan") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("pendidikan_lain_tanggungan"); - } else { - showField("pendidikan_lain_tanggungan"); - } -})(); - -// Conditional logic for field: nama_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("nama_sekolah"); - } else { - showField("nama_sekolah"); - } -}); - -// Initial evaluation for field: nama_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("nama_sekolah"); - } else { - showField("nama_sekolah"); - } -})(); - -// Conditional logic for field: alamat_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("alamat_sekolah"); - } else { - showField("alamat_sekolah"); - } -}); - -// Initial evaluation for field: alamat_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("alamat_sekolah"); - } else { - showField("alamat_sekolah"); - } -})(); - -// Conditional logic for field: daerah_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("daerah_sekolah"); - } else { - showField("daerah_sekolah"); - } -}); - -// Initial evaluation for field: daerah_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("daerah_sekolah"); - } else { - showField("daerah_sekolah"); - } -})(); - -// Conditional logic for field: negeri_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("negeri_sekolah"); - } else { - showField("negeri_sekolah"); - } -}); - -// Initial evaluation for field: negeri_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("negeri_sekolah"); - } else { - showField("negeri_sekolah"); - } -})(); - -// Conditional logic for field: poskod_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("poskod_sekolah"); - } else { - showField("poskod_sekolah"); - } -}); - -// Initial evaluation for field: poskod_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("poskod_sekolah"); - } else { - showField("poskod_sekolah"); - } -})(); -// Conditional Logic Script - -// Conditional logic for field: nyatakan_lain2 -onFieldChange("radio_bangsa", function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -}); - -// Initial evaluation for field: nyatakan_lain2 -(function () { - if (getField("radio_bangsa") !== "lain") { - hideField("nyatakan_lain2"); - } else { - showField("nyatakan_lain2"); - } -})(); - -// Conditional logic for field: text_14 -onFieldChange("radio_pendidikan", function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -}); - -// Initial evaluation for field: text_14 -(function () { - if (getField("radio_pendidikan") !== "lain") { - hideField("text_14"); - } else { - showField("text_14"); - } -})(); - -// Conditional logic for field: hubungan_lain_nyatakan -onFieldChange("hubungan_keluarga", function () { - if ( - !String(getField("hubungan_keluarga") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("hubungan_lain_nyatakan"); - } else { - showField("hubungan_lain_nyatakan"); - } -}); - -// Initial evaluation for field: hubungan_lain_nyatakan -(function () { - if ( - !String(getField("hubungan_keluarga") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("hubungan_lain_nyatakan"); - } else { - showField("hubungan_lain_nyatakan"); - } -})(); - -// Conditional logic for field: bangsa_lain_tanggungan -onFieldChange("bangsa_tanggungan", function () { - if (getField("bangsa_tanggungan") !== "lain_lain") { - hideField("bangsa_lain_tanggungan"); - } else { - showField("bangsa_lain_tanggungan"); - } -}); - -// Initial evaluation for field: bangsa_lain_tanggungan -(function () { - if (getField("bangsa_tanggungan") !== "lain_lain") { - hideField("bangsa_lain_tanggungan"); - } else { - showField("bangsa_lain_tanggungan"); - } -})(); - -// Conditional logic for field: sebab_tunai -onFieldChange("cara_pembayaran", function () { - if ( - !String(getField("cara_pembayaran") || "") - .toLowerCase() - .includes("tunai".toLowerCase()) - ) { - hideField("sebab_tunai"); - } else { - showField("sebab_tunai"); - } -}); - -// Initial evaluation for field: sebab_tunai -(function () { - if ( - !String(getField("cara_pembayaran") || "") - .toLowerCase() - .includes("tunai".toLowerCase()) - ) { - hideField("sebab_tunai"); - } else { - showField("sebab_tunai"); - } -})(); - -// Conditional logic for field: pendidikan_lain_tanggungan -onFieldChange("pendidikan_tertinggi_tanggungan", function () { - if ( - !String(getField("pendidikan_tertinggi_tanggungan") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("pendidikan_lain_tanggungan"); - } else { - showField("pendidikan_lain_tanggungan"); - } -}); - -// Initial evaluation for field: pendidikan_lain_tanggungan -(function () { - if ( - !String(getField("pendidikan_tertinggi_tanggungan") || "") - .toLowerCase() - .includes("lain_lain".toLowerCase()) - ) { - hideField("pendidikan_lain_tanggungan"); - } else { - showField("pendidikan_lain_tanggungan"); - } -})(); - -// Conditional logic for field: nama_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("nama_sekolah"); - } else { - showField("nama_sekolah"); - } -}); - -// Initial evaluation for field: nama_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("nama_sekolah"); - } else { - showField("nama_sekolah"); - } -})(); - -// Conditional logic for field: alamat_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("alamat_sekolah"); - } else { - showField("alamat_sekolah"); - } -}); - -// Initial evaluation for field: alamat_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("alamat_sekolah"); - } else { - showField("alamat_sekolah"); - } -})(); - -// Conditional logic for field: daerah_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("daerah_sekolah"); - } else { - showField("daerah_sekolah"); - } -}); - -// Initial evaluation for field: daerah_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("daerah_sekolah"); - } else { - showField("daerah_sekolah"); - } -})(); - -// Conditional logic for field: negeri_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("negeri_sekolah"); - } else { - showField("negeri_sekolah"); - } -}); - -// Initial evaluation for field: negeri_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("negeri_sekolah"); - } else { - showField("negeri_sekolah"); - } -})(); - -// Conditional logic for field: poskod_sekolah -onFieldChange("bersekolah_tanggungan", function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("poskod_sekolah"); - } else { - showField("poskod_sekolah"); - } -}); - -// Initial evaluation for field: poskod_sekolah -(function () { - if (getField("bersekolah_tanggungan") !== "ya") { - hideField("poskod_sekolah"); - } else { - showField("poskod_sekolah"); + hideField("keperluan_lain_nyatakan"); } })(); diff --git a/docs/json/form/formComponents.json b/docs/json/form/formComponents.json index 2516ad2..8c4ce10 100644 --- a/docs/json/form/formComponents.json +++ b/docs/json/form/formComponents.json @@ -2,9 +2,9 @@ { "type": "heading", "props": { - "name": "heading_2", - "level": 2, - "value": "SEKSYEN A", + "name": "heading_main", + "level": 1, + "value": "Penilaian Awal", "width": "100%", "gridColumn": "span 12", "conditionalLogic": { @@ -16,829 +16,10 @@ } }, { - "type": "heading", + "type": "paragraph", "props": { - "name": "heading_1", - "level": 3, - "value": "1. MAKLUMAT PENDAFTARAN ASNAF (KETUA KELUARGA)", - "width": "100%", - "gridColumn": "span 12", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "help": "Untuk Mualaf, nama mengikut kad pengenalan", - "name": "text_3", - "type": "text", - "label": "Nama", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "placeholder": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "select", - "props": { - "help": "", - "name": "select_1", - "type": "select", - "label": "Jenis Kad Pengenalan", - "width": "100%", - "options": [ - { "label": "Sila Pilih Jenis Kad Pengenalan", "value": "jeniskp" }, - { "label": "MyKad/MyKid", "value": "jeniskp_1" }, - { "label": "No. K/P/Polis/Tentera/No. Pasport", "value": "jeniskp_2" }, - { "label": "No. Sijil Beranak", "value": "jeniskp_3" } - ], - "gridColumn": "span 12", - "validation": "required", - "placeholder": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "help": "", - "name": "form_jeniskp_1", - "type": "text", - "label": "MyKad/MyKid", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "placeholder": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "help": "", - "name": "form_jeniskp_2", - "type": "text", - "label": "No. K/P/Polis/Tentera/No. Pasport", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "placeholder": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "help": "", - "name": "form_jeniskp_3", - "type": "text", - "label": "No. Sijil Beranak", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "placeholder": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "help": "Isi Jika Berkenaan", - "name": "text_warganegara", - "type": "text", - "label": "Warganegara", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "radio", - "props": { - "help": "", - "name": "radio_jantina", - "type": "radio", - "label": "Jantina", - "width": "50%", - "options": [ - { "label": "Lelaki", "value": "lelaki" }, - { "label": "Perempuan", "value": "perempuan" } - ], - "gridColumn": "span 6", - "validation": "required", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "select", - "props": { - "help": "", - "name": "radio_bangsa", - "type": "select", - "label": "Bangsa", - "width": "50%", - "options": [ - { "label": "Melayu", "value": "melayu" }, - { "label": "Cina", "value": "cina" }, - { "label": "India", "value": "india" }, - { "label": "Lain-lain", "value": "lain" } - ], - "gridColumn": "span 6", - "validation": "required", - "placeholder": "Select an option" - } - }, - { - "type": "text", - "props": { - "help": "", - "name": "nyatakan_lain2", - "type": "text", - "label": "Nyatakan (Bangsa Lain-Lain)", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Enter text...", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { "field": "radio_bangsa", "value": "lain", "operator": "not_equals" } - ] - } - } - }, - { - "type": "radio", - "props": { - "help": "", - "name": "radio_9_copy", - "type": "radio", - "label": "Bersekolah", - "width": "50%", - "options": [ - { "label": "Ya", "value": "ya" }, - { "label": "Tidak", "value": "tidak" } - ], - "gridColumn": "span 6", - "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "select", - "props": { - "help": "", - "name": "radio_pendidikan", - "type": "select", - "label": "Pendidikan Tertinggi", - "width": "50%", - "options": [ - { "label": "Peringkat Rendah", "value": "rendah" }, - { "label": "SRP/PMR", "value": "srp" }, - { "label": "SPM", "value": "spm" }, - { "label": "Sijil", "value": "sijil" }, - { "label": "Diploma", "value": "diploma" }, - { "label": "STPM", "value": "stpm" }, - { "label": "Ijazah", "value": "ijazah" }, - { "label": "Lain-Lain", "value": "lain" } - ], - "gridColumn": "span 6", - "validation": "required", - "placeholder": "Select an option" - } - }, - { - "type": "text", - "props": { - "help": "", - "name": "text_14", - "type": "text", - "label": "Nyatakan (Pendidikan Tertinggi Lain-Lain)", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Enter text...", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "radio_pendidikan", - "value": "lain", - "operator": "not_equals" - } - ] - } - } - }, - { - "type": "date", - "props": { - "help": "Isi Jika Berkenaan", - "name": "date_masukislam", - "type": "date", - "label": "Tarikh Masuk Islam", - "width": "50%", - "gridColumn": "span 6", - "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "date", - "props": { - "help": "Isi Jika Berkenaan", - "name": "date_masukislam_copy", - "type": "date", - "label": "Tarikh Mula Kelas fardu Ain Muallaf (KFAM)", - "width": "50%", - "gridColumn": "span 6", - "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "select", - "props": { - "help": "", - "name": "select_statusperkahwinan", - "type": "select", - "label": "Status Perkahwinan", - "width": "100%", - "options": [ - { "label": "Berkahwin", "value": "berkahwin" }, - { "label": "Ibu Tinggal/Bapa Tinggal", "value": "ibubapatunggal" }, - { "label": "Bujang", "value": "bujang" }, - { "label": "Duda", "value": "duda" }, - { "label": "Janda", "value": "janda" }, - { "label": "Balu", "value": "balu" } - ], - "gridColumn": "span 12", - "validation": "required", - "placeholder": "Select an option", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "heading", - "props": { - "name": "heading_seksyen_b", - "level": 2, - "value": "SEKSYEN B", - "width": "100%", - "gridColumn": "span 12", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "heading", - "props": { - "name": "heading_tanggungan", - "level": 3, - "value": "1. MAKLUMAT PERIBADI TANGGUNGAN (Ruangan WAJIB diisi)", - "width": "100%", - "gridColumn": "span 12", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "checkbox", - "props": { - "help": "Pilih semua yang berkenaan", - "name": "hubungan_keluarga", - "type": "checkbox", - "label": "Hubungan dengan Pemohon/Asnaf", - "width": "100%", - "options": [ - { "label": "Pasangan Pemohon", "value": "pasangan" }, - { "label": "Isteri Kedua", "value": "isteri_kedua" }, - { "label": "Isteri Ketiga", "value": "isteri_ketiga" }, - { "label": "Isteri Keempat", "value": "isteri_keempat" }, - { "label": "Ipar", "value": "ipar" }, - { "label": "Abang", "value": "abang" }, - { "label": "Bapa", "value": "bapa" }, - { "label": "Ibu", "value": "ibu" }, - { "label": "Kakak", "value": "kakak" }, - { "label": "Adik", "value": "adik" }, - { "label": "Anak", "value": "anak" }, - { "label": "Cucu", "value": "cucu" }, - { "label": "Bapa Mertua", "value": "bapa_mertua" }, - { "label": "Ibu Mertua", "value": "ibu_mertua" }, - { "label": "Lain-lain (Sila Nyatakan)", "value": "lain_lain" } - ], - "gridColumn": "span 12", - "validation": "required" - } - }, - { - "type": "text", - "props": { - "name": "hubungan_lain_nyatakan", - "type": "text", - "label": "Nyatakan Hubungan Lain-lain", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Sila nyatakan hubungan lain-lain", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "hubungan_keluarga", - "value": "lain_lain", - "operator": "not_contains" - } - ] - } - } - }, - { - "type": "text", - "props": { - "name": "nama_tanggungan", - "type": "text", - "label": "Nama (Untuk Mualaf, nama mengikut kad pengenalan)", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "placeholder": "Masukkan nama lengkap", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "select", - "props": { - "name": "jenis_kad_tanggungan", - "type": "select", - "label": "Jenis Pengenalan", - "width": "100%", - "options": [ - { "label": "Sila Pilih Jenis Pengenalan", "value": "" }, - { "label": "MyKad/MyKid", "value": "mykad" }, - { "label": "No. K/P/Polis/Tentera/No. Pasport", "value": "kp_polis" }, - { "label": "No. Sijil Beranak", "value": "sijil_beranak" } - ], - "gridColumn": "span 12", - "validation": "required", - "placeholder": "Sila pilih jenis pengenalan", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "no_pengenalan_tanggungan", - "type": "text", - "label": "No. Pengenalan", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "placeholder": "Masukkan nombor pengenalan", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "radio", - "props": { - "name": "jantina_tanggungan", - "type": "radio", - "label": "Jantina", - "width": "100%", - "options": [ - { "label": "Lelaki", "value": "lelaki" }, - { "label": "Perempuan", "value": "perempuan" } - ], - "gridColumn": "span 12", - "validation": "required", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "date", - "props": { - "name": "tarikh_lahir_tanggungan", - "type": "date", - "label": "Tarikh Lahir", - "width": "100%", - "gridColumn": "span 12", - "validation": "required", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "tempat_lahir_tanggungan", - "type": "text", - "label": "Tempat Lahir", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Masukkan tempat lahir", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "select", - "props": { - "name": "bangsa_tanggungan", - "type": "select", - "label": "Bangsa", - "width": "100%", - "options": [ - { "label": "Sila Pilih Bangsa", "value": "" }, - { "label": "Melayu", "value": "melayu" }, - { "label": "Cina", "value": "cina" }, - { "label": "India", "value": "india" }, - { "label": "Lain-lain (Sila Nyatakan)", "value": "lain_lain" } - ], - "gridColumn": "span 12", - "validation": "required", - "placeholder": "Sila pilih bangsa", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "bangsa_lain_tanggungan", - "type": "text", - "label": "Nyatakan Bangsa Lain-lain", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Sila nyatakan bangsa lain-lain", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "bangsa_tanggungan", - "value": "lain_lain", - "operator": "not_equals" - } - ] - } - } - }, - { - "type": "select", - "props": { - "name": "status_kahwin_tanggungan", - "type": "select", - "label": "Status Perkahwinan", - "width": "100%", - "options": [ - { "label": "Sila Pilih Status Perkahwinan", "value": "" }, - { "label": "Berkahwin", "value": "berkahwin" }, - { "label": "Ibu Tinggal/Bapa Tinggal", "value": "ibu_bapa_tinggal" }, - { "label": "Bujang", "value": "bujang" }, - { "label": "Duda", "value": "duda" }, - { "label": "Janda", "value": "janda" }, - { "label": "Balu", "value": "balu" } - ], - "gridColumn": "span 12", - "validation": "", - "placeholder": "Sila pilih status perkahwinan", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "date", - "props": { - "help": "Jika Berkenaan", - "name": "tarikh_masuk_islam_tanggungan", - "type": "date", - "label": "Tarikh Masuk Islam", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "date", - "props": { - "help": "Jika Berkenaan", - "name": "tarikh_mula_kfam_tanggungan", - "type": "date", - "label": "Tarikh Mula KFAM", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "help": "Jika Berkenaan", - "name": "warganegara_tanggungan", - "type": "text", - "label": "Warganegara", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Masukkan warganegara", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "tempat_menetap_tanggungan", - "type": "text", - "label": "Tempoh Menetap Di Selangor", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Contoh: 5 Tahun", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "no_telefon_tanggungan", - "type": "text", - "label": "No. Telefon/Telefon Bimbit", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Contoh: 03-12345678", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "heading", - "props": { - "name": "heading_maklumat_perbankan", - "level": 3, - "value": "MAKLUMAT PERBANKAN (Jika Berkenaan)", - "width": "100%", - "gridColumn": "span 12", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "nama_pemegang_akaun", - "type": "text", - "label": "Nama Pemegang Akaun", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Masukkan nama pemegang akaun", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "nama_bank", - "type": "text", - "label": "Bank", - "width": "50%", - "gridColumn": "span 6", - "validation": "", - "placeholder": "Masukkan nama bank", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "no_akaun_bank", - "type": "text", - "label": "No. Akaun Bank", - "width": "50%", - "gridColumn": "span 6", - "validation": "", - "placeholder": "Masukkan nombor akaun", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "checkbox", - "props": { - "name": "cara_pembayaran", - "type": "checkbox", - "label": "Cara Pembayaran", - "width": "100%", - "options": [ - { "label": "Akaun", "value": "akaun" }, - { "label": "Cek", "value": "cek" }, - { "label": "Tunai, Nyatakan Sebab", "value": "tunai" }, - { "label": "Uzur/Sakit", "value": "uzur_sakit" }, - { "label": "Muflis", "value": "muflis" }, - { "label": "Disenarai Hitam", "value": "disenarai_hitam" } - ], - "gridColumn": "span 12", - "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } - } - }, - { - "type": "text", - "props": { - "name": "sebab_tunai", - "type": "text", - "label": "Sebab Pembayaran Tunai", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Sila nyatakan sebab", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "cara_pembayaran", - "value": "tunai", - "operator": "not_contains" - } - ] - } - } - }, - { - "type": "heading", - "props": { - "name": "heading_pendidikan", - "level": 3, - "value": "2. PENDIDIKAN", + "name": "paragraph_intro", + "value": "Sila lengkapkan borang penilaian awal ini untuk membantu kami memahami keperluan anda dengan lebih baik.", "width": "100%", "gridColumn": "span 12", "conditionalLogic": { @@ -852,9 +33,10 @@ { "type": "radio", "props": { - "name": "bersekolah_tanggungan", + "help": "Sila nyatakan sama ada anda mempunyai komitmen kewangan yang tinggi", + "name": "komitmen_pembiayaan", "type": "radio", - "label": "Bersekolah", + "label": "1. Adakah tuan/puan mempunyai komitmen dan pembiayaan melibatkan kos yang tinggi?", "width": "100%", "options": [ { "label": "Ya", "value": "ya" }, @@ -873,32 +55,26 @@ { "type": "checkbox", "props": { - "name": "pendidikan_tertinggi_tanggungan", + "help": "Sila pilih keperluan yang mendesak. Boleh pilih lebih daripada satu.", + "name": "keperluan_mendesak", "type": "checkbox", - "label": "Pendidikan Tertinggi", + "label": "2. Apakah keperluan tuan/puan mendesak sekarang ini?", "width": "100%", "options": [ - { "label": "Peringkat Rendah", "value": "peringkat_rendah" }, - { "label": "SRP/PMR", "value": "srp_pmr" }, - { "label": "SPM", "value": "spm" }, - { "label": "STPM", "value": "stpm" }, - { "label": "Pra Sekolah", "value": "pra_sekolah" }, - { "label": "Sekolah Rendah Kebangsaan", "value": "srk" }, - { "label": "Sekolah Menengah Kebangsaan", "value": "smk" }, - { "label": "Sekolah Menengah Agama", "value": "sma" }, - { "label": "Sijil", "value": "sijil" }, - { "label": "Diploma", "value": "diploma" }, - { "label": "Ijazah", "value": "ijazah" }, - { "label": "Lain-Lain (Sila Nyatakan)", "value": "lain_lain" }, + { "label": "Perubatan Kritikal", "value": "perubatan_kritikal" }, + { "label": "Bencana", "value": "bencana" }, + { "label": "Kematian", "value": "kematian" }, { - "label": "Sekolah Rendah Kebangsaan dan Agama", - "value": "srk_agama" + "label": "Konflik Keluarga (tiada tempat bergantung)", + "value": "konflik_keluarga" }, - { "label": "IPTA/IPTS", "value": "ipta_ipts" }, - { "label": "Maahad Tahfiz", "value": "maahad_tahfiz" } + { "label": "Tiada Tempat Tinggal", "value": "tiada_tempat_tinggal" }, + { "label": "Tunggakan Bil Utiliti", "value": "tunggakan_utiliti" }, + { "label": "Selain dari di atas", "value": "lain_lain" }, + { "label": "Tidak mendesak", "value": "tidak_mendesak" } ], "gridColumn": "span 12", - "validation": "", + "validation": "required", "conditionalLogic": { "action": "show", "enabled": false, @@ -907,164 +83,98 @@ } } }, - { - "type": "text", - "props": { - "name": "pendidikan_lain_tanggungan", - "type": "text", - "label": "Nyatakan Pendidikan Lain-lain", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Sila nyatakan pendidikan lain-lain", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "pendidikan_tertinggi_tanggungan", - "value": "lain_lain", - "operator": "not_contains" - } - ] - } - } - }, - { - "type": "text", - "props": { - "name": "nama_sekolah", - "type": "text", - "label": "Nama Sekolah/Institusi", - "width": "100%", - "gridColumn": "span 12", - "validation": "", - "placeholder": "Masukkan nama sekolah/institusi", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "bersekolah_tanggungan", - "value": "ya", - "operator": "not_equals" - } - ] - } - } - }, { "type": "textarea", "props": { - "name": "alamat_sekolah", + "help": "Sila nyatakan keperluan lain yang tidak disenaraikan di atas", + "name": "keperluan_lain_nyatakan", + "rows": 3, "type": "textarea", - "label": "Alamat Sekolah/Institusi", + "label": "Nyatakan keperluan lain:", "width": "100%", "gridColumn": "span 12", "validation": "", - "placeholder": "Masukkan alamat lengkap sekolah/institusi", + "placeholder": "Sila nyatakan keperluan anda...", "conditionalLogic": { - "action": "hide", + "action": "show", "enabled": true, "operator": "and", "conditions": [ { - "field": "bersekolah_tanggungan", - "value": "ya", - "operator": "not_equals" + "field": "keperluan_mendesak", + "value": "lain_lain", + "operator": "contains" } ] } } }, { - "type": "text", + "type": "file", "props": { - "name": "daerah_sekolah", - "type": "text", - "label": "Daerah", - "width": "50%", - "gridColumn": "span 6", + "help": "Sila muat naik dokumen yang berkaitan dengan permohonan anda. Format yang diterima: PDF, DOC, DOCX, JPG, PNG. Saiz maksimum: 10MB", + "name": "dokumen_berkaitan", + "type": "file", + "label": "3. Upload dokumen yang berkaitan", + "width": "100%", + "accept": ".pdf,.doc,.docx,.jpg,.jpeg,.png", + "multiple": true, + "gridColumn": "span 12", "validation": "", - "placeholder": "Masukkan daerah", "conditionalLogic": { - "action": "hide", - "enabled": true, + "action": "show", + "enabled": false, "operator": "and", - "conditions": [ - { - "field": "bersekolah_tanggungan", - "value": "ya", - "operator": "not_equals" - } - ] + "conditions": [] } } }, { - "type": "text", + "type": "textarea", "props": { - "name": "negeri_sekolah", - "type": "text", - "label": "Negeri", - "width": "50%", - "gridColumn": "span 6", - "validation": "", - "placeholder": "Masukkan negeri", - "conditionalLogic": { - "action": "hide", - "enabled": true, - "operator": "and", - "conditions": [ - { - "field": "bersekolah_tanggungan", - "value": "ya", - "operator": "not_equals" - } - ] - } - } - }, - { - "type": "text", - "props": { - "name": "poskod_sekolah", - "type": "text", - "label": "Poskod", + "help": "Sila berikan catatan tambahan yang boleh membantu kami memahami permohonan anda", + "name": "catatan_tambahan", + "rows": 5, + "type": "textarea", + "label": "4. Catatan tambahan berkaitan permohonan", "width": "100%", "gridColumn": "span 12", "validation": "", - "placeholder": "Masukkan poskod", + "placeholder": "Sila berikan catatan tambahan...", "conditionalLogic": { - "action": "hide", - "enabled": true, + "action": "show", + "enabled": false, "operator": "and", - "conditions": [ - { - "field": "bersekolah_tanggungan", - "value": "ya", - "operator": "not_equals" - } - ] + "conditions": [] } } }, { - "type": "radio", + "type": "divider", "props": { - "name": "tinggal_bersama_keluarga", - "type": "radio", - "label": "Tinggal Bersama Keluarga", + "name": "divider_submit", "width": "100%", - "options": [ - { "label": "Ya", "value": "ya" }, - { "label": "Tidak", "value": "tidak" }, - { "label": "Asrama", "value": "asrama" } - ], "gridColumn": "span 12", - "validation": "", + "conditionalLogic": { + "action": "show", + "enabled": false, + "operator": "and", + "conditions": [] + } + } + }, + { + "type": "button", + "props": { + "name": "submit_button", + "size": "lg", + "label": "Hantar Penilaian Awal", + "width": "100%", + "onClick": "", + "variant": "primary", + "disabled": false, + "buttonType": "submit", + "gridColumn": "span 12", "conditionalLogic": { "action": "show", "enabled": false, diff --git a/docs/json/process-builder/processDefinition.json b/docs/json/process-builder/processDefinition.json index 418c560..b77aded 100644 --- a/docs/json/process-builder/processDefinition.json +++ b/docs/json/process-builder/processDefinition.json @@ -1,18 +1,51 @@ { "edges": [ { - "id": "start-1751870920411-form-1751870928350-1751954902366", + "id": "start-1751870920411-form-1752471000000-1752110219601", "data": {}, "type": "smoothstep", "label": "", "source": "start-1751870920411", - "target": "form-1751870928350", + "target": "form-1752471000000", "animated": true, "sourceHandle": "start-1751870920411-right", + "targetHandle": "form-1752471000000-left" + }, + { + "id": "form-1752471000000-api-1752471000010-1752110221444", + "data": {}, + "type": "smoothstep", + "label": "", + "source": "form-1752471000000", + "target": "api-1752471000010", + "animated": true, + "sourceHandle": "form-1752471000000-bottom", + "targetHandle": "api-1752471000010-top" + }, + { + "id": "api-1752471000010-script-1752471000020-1752110222889", + "data": {}, + "type": "smoothstep", + "label": "", + "source": "api-1752471000010", + "target": "script-1752471000020", + "animated": true, + "sourceHandle": "api-1752471000010-right", + "targetHandle": "script-1752471000020-left" + }, + { + "id": "script-1752471000020-form-1751870928350-1752110513125", + "data": {}, + "type": "smoothstep", + "label": "", + "source": "script-1752471000020", + "target": "form-1751870928350", + "animated": true, + "sourceHandle": "script-1752471000020-right", "targetHandle": "form-1751870928350-left" }, { - "id": "form-1751870928350-api-1751871528249-1751954924255", + "id": "form-1751870928350-api-1751871528249-1752110547688", "data": {}, "type": "smoothstep", "label": "", @@ -23,7 +56,7 @@ "targetHandle": "api-1751871528249-top" }, { - "id": "api-1751871528249-script-1751871635000-1751954926618", + "id": "api-1751871528249-script-1751871635000-1752110661170", "data": {}, "type": "smoothstep", "label": "", @@ -34,103 +67,59 @@ "targetHandle": "script-1751871635000-left" }, { - "id": "form-1751871700000-script-1751871635000-1751954928000", + "id": "script-1751871635000-api-1752114771983-1752114797295", "data": {}, "type": "smoothstep", "label": "", - "source": "form-1751871700000", - "target": "script-1751871635000", + "source": "script-1751871635000", + "target": "api-1752114771983", "animated": true, - "sourceHandle": "form-1751871700000-bottom", - "targetHandle": "script-1751871635000-top" + "sourceHandle": "script-1751871635000-right", + "targetHandle": "api-1752114771983-left" }, { - "id": "api-1751871750000-form-1751871700000-1751954936240", + "id": "api-1752114771983-html-1752109761532-1752114821740", "data": {}, "type": "smoothstep", "label": "", - "source": "api-1751871750000", - "target": "form-1751871700000", + "source": "api-1752114771983", + "target": "html-1752109761532", "animated": true, - "sourceHandle": "api-1751871750000-bottom", - "targetHandle": "form-1751871700000-top" + "sourceHandle": "api-1752114771983-right", + "targetHandle": "html-1752109761532-left" }, { - "id": "api-1751871750000-script-1751871770000-1751954938889", + "id": "start-1751870920411-form-1753000000000-1752115210580", "data": {}, "type": "smoothstep", "label": "", - "source": "api-1751871750000", - "target": "script-1751871770000", + "source": "start-1751870920411", + "target": "form-1753000000000", "animated": true, - "sourceHandle": "api-1751871750000-right", - "targetHandle": "script-1751871770000-left" + "sourceHandle": "start-1751870920411-right", + "targetHandle": "form-1753000000000-left" }, { - "id": "script-1751871770000-gateway-1751871800000-1751954943222", + "id": "form-1753000000000-api-1753000000001-1752115217959", "data": {}, "type": "smoothstep", "label": "", - "source": "script-1751871770000", - "target": "gateway-1751871800000", + "source": "form-1753000000000", + "target": "api-1753000000001", "animated": true, - "sourceHandle": "script-1751871770000-bottom", - "targetHandle": "gateway-1751871800000-left" + "sourceHandle": "form-1753000000000-bottom", + "targetHandle": "api-1753000000001-top" }, { - "id": "gateway-1751871800000-business-rule-1751871900000-1751954958263", - "data": {}, - "type": "smoothstep", - "label": "Ya", - "source": "gateway-1751871800000", - "target": "business-rule-1751871900000", - "animated": true, - "sourceHandle": "gateway-1751871800000-right", - "targetHandle": "business-rule-1751871900000-left" - }, - { - "id": "gateway-1751871800000-notification-1751872000000-1751954960514", - "data": {}, - "type": "smoothstep", - "label": "Tidak", - "source": "gateway-1751871800000", - "target": "notification-1751872000000", - "animated": true, - "sourceHandle": "gateway-1751871800000-right", - "targetHandle": "notification-1751872000000-left" - }, - { - "id": "business-rule-1751871900000-notification-1751871950000-1751954963756", + "id": "api-1753000000001-script-1753000000002-1752115222952", "data": {}, "type": "smoothstep", "label": "", - "source": "business-rule-1751871900000", - "target": "notification-1751871950000", + "source": "api-1753000000001", + "target": "script-1753000000002", "animated": true, - "sourceHandle": "business-rule-1751871900000-right", - "targetHandle": "notification-1751871950000-left" - }, - { - "id": "notification-1751871950000-end-1751872100000-1751954966017", - "data": {}, - "type": "smoothstep", - "label": "", - "source": "notification-1751871950000", - "target": "end-1751872100000", - "animated": true, - "sourceHandle": "notification-1751871950000-bottom", - "targetHandle": "end-1751872100000-top" - }, - { - "id": "notification-1751872000000-end-1751872100000-1751954967691", - "data": {}, - "type": "smoothstep", - "label": "", - "source": "notification-1751872000000", - "target": "end-1751872100000", - "animated": true, - "sourceHandle": "notification-1751872000000-right", - "targetHandle": "end-1751872100000-left" + "sourceHandle": "api-1753000000001-right", + "targetHandle": "script-1753000000002-left" } ], "nodes": [ @@ -139,7 +128,165 @@ "data": { "label": "Start", "description": "Process start point" }, "type": "start", "label": "Start", - "position": { "x": 210, "y": 180 } + "position": { "x": 120, "y": -495 } + }, + { + "id": "form-1752471000000", + "data": { + "label": "Penilaian Awal", + "formId": 3, + "formName": "Penilaian Awal", + "formUuid": "8e07fc8f-a160-478a-85fd-fa3364401544", + "description": "Form: Penilaian Awal untuk permohonan bantuan", + "assignedRoles": [], + "assignedUsers": [], + "inputMappings": [], + "assignmentType": "public", + "outputMappings": [ + { + "formField": "komitmen_pembiayaan", + "processVariable": "komitmenKosTinggi" + }, + { + "formField": "keperluan_mendesak", + "processVariable": "keperluanMendesak" + }, + { + "formField": "keperluan_lain_nyatakan", + "processVariable": "keperluanLainNyatakan" + }, + { + "formField": "dokumen_berkaitan", + "processVariable": "dokumenBerkaitan" + }, + { + "formField": "catatan_tambahan", + "processVariable": "catatanTambahan" + } + ], + "fieldConditions": [], + "assignmentVariable": "", + "assignmentVariableType": "user_id" + }, + "type": "form", + "label": "Penilaian Awal", + "position": { "x": 450, "y": -525 } + }, + { + "id": "api-1752471000010", + "data": { + "label": "Submit Penilaian Awal API", + "apiUrl": "https://jsonplaceholder.typicode.com/posts", + "headers": "{ \"Content-Type\": \"application/json\" }", + "apiMethod": "POST", + "description": "Submit penilaian awal data to external system", + "requestBody": "{\n \"komitmenKosTinggi\": \"{komitmenKosTinggi}\",\n \"keperluanMendesak\": \"{keperluanMendesak}\",\n \"keperluanLainNyatakan\": \"{keperluanLainNyatakan}\",\n \"dokumenBerkaitan\": \"{dokumenBerkaitan}\",\n \"catatanTambahan\": \"{catatanTambahan}\"\n}", + "errorVariable": "penilaianAwalApiError", + "outputVariable": "penilaianAwalApiResponse", + "continueOnError": false + }, + "type": "api", + "label": "Submit Penilaian Awal API", + "position": { "x": 450, "y": -345 } + }, + { + "id": "script-1752471000020", + "data": { + "label": "Process Penilaian Awal Response", + "scriptCode": "// Extract important data from Penilaian Awal API response\nconst apiData = processVariables.penilaianAwalApiResponse;\n\nif (apiData && apiData.data) {\n // Generate a reference number for the assessment\n processVariables.penilaianAwalId = apiData.data.id || 'PA-' + Date.now();\n \n // Process the high cost commitment answer\n processVariables.hasHighCostCommitment = processVariables.komitmenKosTinggi === 'ya';\n \n // Process urgent needs\n if (Array.isArray(processVariables.keperluanMendesak)) {\n // Set flags for specific urgent needs\n processVariables.hasUrgentMedicalNeed = processVariables.keperluanMendesak.includes('perubatan_kritikal');\n processVariables.hasDisasterNeed = processVariables.keperluanMendesak.includes('bencana');\n processVariables.hasDeathRelatedNeed = processVariables.keperluanMendesak.includes('kematian');\n processVariables.hasFamilyConflict = processVariables.keperluanMendesak.includes('konflik_keluarga');\n processVariables.hasHomelessness = processVariables.keperluanMendesak.includes('tiada_tempat_tinggal');\n processVariables.hasUtilityArrears = processVariables.keperluanMendesak.includes('tunggakan_utiliti');\n processVariables.hasOtherNeeds = processVariables.keperluanMendesak.includes('lain_lain');\n processVariables.hasNoUrgentNeeds = processVariables.keperluanMendesak.includes('tidak_mendesak');\n \n // Calculate urgency score based on selected needs\n let urgencyScore = 0;\n if (processVariables.hasUrgentMedicalNeed) urgencyScore += 5;\n if (processVariables.hasDisasterNeed) urgencyScore += 5;\n if (processVariables.hasDeathRelatedNeed) urgencyScore += 4;\n if (processVariables.hasFamilyConflict) urgencyScore += 3;\n if (processVariables.hasHomelessness) urgencyScore += 5;\n if (processVariables.hasUtilityArrears) urgencyScore += 2;\n if (processVariables.hasOtherNeeds) urgencyScore += 1;\n if (processVariables.hasNoUrgentNeeds) urgencyScore = 0;\n \n processVariables.urgencyScore = urgencyScore;\n processVariables.urgencyLevel = urgencyScore >= 5 ? 'high' : (urgencyScore >= 3 ? 'medium' : 'low');\n }\n \n // Check if documents were uploaded\n processVariables.hasUploadedDocuments = processVariables.dokumenBerkaitan && \n Array.isArray(processVariables.dokumenBerkaitan) && \n processVariables.dokumenBerkaitan.length > 0;\n \n // Set status for next step\n processVariables.penilaianAwalStatus = 'completed';\n processVariables.readyForPersonalInfo = true;\n \n console.log('Penilaian Awal processed successfully:', {\n penilaianAwalId: processVariables.penilaianAwalId,\n urgencyLevel: processVariables.urgencyLevel,\n urgencyScore: processVariables.urgencyScore,\n hasHighCostCommitment: processVariables.hasHighCostCommitment,\n hasUploadedDocuments: processVariables.hasUploadedDocuments\n });\n} else {\n // Handle API error case\n processVariables.penilaianAwalStatus = 'failed';\n processVariables.readyForPersonalInfo = false;\n processVariables.penilaianAwalError = 'Failed to submit penilaian awal';\n}", + "description": "Process the penilaian awal form data and API response", + "errorVariable": "penilaianAwalScriptError", + "inputVariables": [ + "penilaianAwalApiResponse", + "komitmenKosTinggi", + "keperluanMendesak", + "keperluanLainNyatakan", + "dokumenBerkaitan", + "catatanTambahan" + ], + "scriptLanguage": "javascript", + "continueOnError": false, + "outputVariables": [ + { + "name": "penilaianAwalId", + "type": "string", + "description": "Generated ID for the initial assessment" + }, + { + "name": "hasHighCostCommitment", + "type": "boolean", + "description": "Whether applicant has high cost commitments" + }, + { + "name": "urgencyScore", + "type": "number", + "description": "Calculated urgency score based on needs" + }, + { + "name": "urgencyLevel", + "type": "string", + "description": "Urgency level (high/medium/low)" + }, + { + "name": "hasUrgentMedicalNeed", + "type": "boolean", + "description": "Whether applicant has urgent medical needs" + }, + { + "name": "hasDisasterNeed", + "type": "boolean", + "description": "Whether applicant has disaster-related needs" + }, + { + "name": "hasDeathRelatedNeed", + "type": "boolean", + "description": "Whether applicant has death-related needs" + }, + { + "name": "hasFamilyConflict", + "type": "boolean", + "description": "Whether applicant has family conflict" + }, + { + "name": "hasHomelessness", + "type": "boolean", + "description": "Whether applicant is homeless" + }, + { + "name": "hasUtilityArrears", + "type": "boolean", + "description": "Whether applicant has utility arrears" + }, + { + "name": "hasOtherNeeds", + "type": "boolean", + "description": "Whether applicant has other needs" + }, + { + "name": "hasNoUrgentNeeds", + "type": "boolean", + "description": "Whether applicant has no urgent needs" + }, + { + "name": "hasUploadedDocuments", + "type": "boolean", + "description": "Whether documents were uploaded" + }, + { + "name": "penilaianAwalStatus", + "type": "string", + "description": "Status of initial assessment submission" + }, + { + "name": "readyForPersonalInfo", + "type": "boolean", + "description": "Whether ready for personal info form" + } + ] + }, + "type": "script", + "label": "Process Penilaian Awal Response", + "position": { "x": 780, "y": -345 } }, { "id": "form-1751870928350", @@ -287,7 +434,7 @@ }, "type": "form", "label": "Borang Maklumat Peribadi", - "position": { "x": 375, "y": 120 } + "position": { "x": 1275, "y": -525 } }, { "id": "api-1751871528249", @@ -304,7 +451,7 @@ }, "type": "api", "label": "Submit Profile API", - "position": { "x": 375, "y": 360 } + "position": { "x": 1275, "y": -345 } }, { "id": "script-1751871635000", @@ -383,7 +530,7 @@ }, "type": "script", "label": "Process API Response", - "position": { "x": 720, "y": 360 } + "position": { "x": 1590, "y": -345 } }, { "id": "form-1751871700000", @@ -425,7 +572,7 @@ }, "type": "form", "label": "Borang Semak Dokumen", - "position": { "x": 720, "y": 120 } + "position": { "x": 885, "y": 675 } }, { "id": "api-1751871750000", @@ -442,7 +589,7 @@ }, "type": "api", "label": "Submit Document Verification API", - "position": { "x": 720, "y": -105 } + "position": { "x": 1050, "y": 510 } }, { "id": "script-1751871770000", @@ -520,7 +667,7 @@ }, "type": "script", "label": "Process Verification Response", - "position": { "x": 1020, "y": -105 } + "position": { "x": 630, "y": 525 } }, { "id": "gateway-1751871800000", @@ -562,7 +709,7 @@ }, "type": "gateway", "label": "Lengkap?", - "position": { "x": 1125, "y": 195 } + "position": { "x": 1350, "y": 315 } }, { "id": "business-rule-1751871900000", @@ -687,7 +834,7 @@ }, "type": "business-rule", "label": "Analisis Had Kifayah", - "position": { "x": 1485, "y": 45 } + "position": { "x": 1665, "y": 120 } }, { "id": "notification-1751871950000", @@ -731,12 +878,281 @@ }, "type": "end", "label": "End", - "position": { "x": 1950, "y": 420 } + "position": { "x": 1935, "y": 390 } + }, + { + "id": "html-1752109761532", + "data": { + "label": "Family Tree", + "shape": "rectangle", + "jsCode": "", + "cssCode": "", + "htmlCode": "\n
\n

Custom HTML Content

\n

This is a custom HTML node that can display rich content.

\n
", + "textColor": "#333333", + "autoRefresh": false, + "borderColor": "#dddddd", + "description": "Family Tree for Borang", + "inputVariables": [], + "backgroundColor": "#ffffff", + "outputVariables": [], + "allowVariableAccess": true + }, + "type": "html", + "label": "Family Tree", + "position": { "x": 2385, "y": -360 } + }, + { + "id": "rectangle-shape-1752110224921", + "data": { + "label": "", + "shape": "rectangle", + "width": 650, + "height": 400, + "isShape": true, + "shapeType": "rectangle", + "textColor": "#374151", + "borderColor": "#16a34a", + "description": "", + "backgroundColor": "#e8f5e9" + }, + "type": "rectangle-shape", + "label": "", + "position": { "x": 375, "y": -570 } + }, + { + "id": "text-annotation-1752110279700", + "data": { + "label": "NF-NAS-PRF-AS-PA", + "shape": "rectangle", + "width": 200, + "height": 80, + "isShape": true, + "shapeType": "text-annotation", + "textColor": "#92400e", + "borderColor": "#fbbf24", + "description": "Pernilaian Awal", + "backgroundColor": "#fffbeb" + }, + "type": "text-annotation", + "label": "BF-NAS-PRF-AS-PA", + "position": { "x": 810, "y": -555 } + }, + { + "id": "rectangle-shape-1752110492897", + "data": { + "label": "", + "shape": "rectangle", + "width": 650, + "height": 400, + "isShape": true, + "shapeType": "rectangle", + "textColor": "#374151", + "borderColor": "#16a34a", + "description": "", + "backgroundColor": "#e8f5e9" + }, + "type": "rectangle-shape", + "label": "", + "position": { "x": 1185, "y": -570 } + }, + { + "id": "text-annotation-1752110562983", + "data": { + "label": "BF-NAS-PRF-AS-QS-02", + "shape": "rectangle", + "width": 200, + "height": 80, + "isShape": true, + "shapeType": "text-annotation", + "textColor": "#92400e", + "borderColor": "#fbbf24", + "description": "Isi Borang Permohonan Online", + "backgroundColor": "#fffbeb" + }, + "type": "text-annotation", + "label": "BF-NAS-PRF-AS-QS-02", + "position": { "x": 1620, "y": -555 } + }, + { + "id": "rectangle-shape-1752114739551", + "data": { + "label": "", + "shape": "rectangle", + "width": 650, + "height": 400, + "isShape": true, + "shapeType": "rectangle", + "textColor": "#374151", + "borderColor": "#16a34a", + "description": "", + "backgroundColor": "#e8f5e9" + }, + "type": "rectangle-shape", + "label": "", + "position": { "x": 1995, "y": -570 } + }, + { + "id": "api-1752114771983", + "data": { + "label": "API Call", + "shape": "rectangle", + "apiUrl": "", + "headers": "{ \"Content-Type\": \"application/json\" }", + "apiMethod": "GET", + "textColor": "#1e40af", + "borderColor": "#3b82f6", + "description": "External API call", + "requestBody": "", + "errorVariable": "apiError", + "outputVariable": "apiResponse", + "backgroundColor": "#eff6ff", + "continueOnError": false + }, + "type": "api", + "label": "Called Family Tree", + "position": { "x": 2070, "y": -345 } + }, + { + "id": "text-annotation-1752114833800", + "data": { + "label": "", + "shape": "rectangle", + "width": 200, + "height": 80, + "isShape": true, + "shapeType": "text-annotation", + "textColor": "#92400e", + "borderColor": "#fbbf24", + "description": "Family Tree", + "backgroundColor": "#fffbeb" + }, + "type": "text-annotation", + "label": "BF-NAS-PRF-AS-FM", + "position": { "x": 2430, "y": -555 } + }, + { + "id": "form-1753000000000", + "data": { + "label": "Carian Profil", + "formId": 4, + "formName": "Carian Profil", + "formUuid": "4e07fc8f-a160-478a-85fd-fa3364401545", + "description": "Skrin carian asnaf atau login", + "assignedRoles": [], + "assignedUsers": [], + "inputMappings": [], + "assignmentType": "public", + "outputMappings": [ + { "formField": "search_type", "processVariable": "carianSearchType" }, + { "formField": "search_id", "processVariable": "carianSearchId" }, + { "formField": "login_id", "processVariable": "carianLoginId" }, + { + "formField": "login_password", + "processVariable": "carianLoginPassword" + } + ], + "fieldConditions": [], + "assignmentVariable": "", + "assignmentVariableType": "user_id" + }, + "type": "form", + "label": "Carian Profil", + "position": { "x": 450, "y": -15 } + }, + { + "id": "api-1753000000001", + "data": { + "label": "Submit Carian Profil API", + "apiUrl": "https://api.example.com/profiles/search", + "headers": "{ \"Content-Type\": \"application/json\" }", + "apiMethod": "POST", + "description": "Submit profile search or login credentials", + "requestBody": "{\n \"searchType\": \"{carianSearchType}\",\n \"searchId\": \"{carianSearchId}\",\n \"loginId\": \"{carianLoginId}\",\n \"password\": \"{carianLoginPassword}\"\n}", + "errorVariable": "carianProfilApiError", + "outputVariable": "carianProfilApiResponse", + "continueOnError": false + }, + "type": "api", + "label": "Submit Carian Profil API", + "position": { "x": 450, "y": 180 } + }, + { + "id": "script-1753000000002", + "data": { + "label": "Process Carian Profil Response", + "scriptCode": "// Process API response from profile search/login\nconst response = processVariables.carianProfilApiResponse;\n\nif (response && response.data) {\n if (response.data.loginSuccess) {\n processVariables.loginSuccess = true;\n processVariables.profileData = response.data.profile;\n processVariables.carianProfilStatus = 'login_successful';\n } else if (response.data.profileFound) {\n processVariables.profileFound = true;\n processVariables.profileData = response.data.profile;\n processVariables.carianProfilStatus = 'profile_found';\n } else {\n processVariables.profileFound = false;\n processVariables.loginSuccess = false;\n processVariables.carianProfilStatus = 'not_found';\n }\n} else {\n processVariables.carianProfilStatus = 'error';\n processVariables.carianProfilScriptError = 'Invalid or empty API response';\n}", + "description": "Process the response from the Carian Profil API", + "errorVariable": "carianProfilScriptError", + "inputVariables": ["carianProfilApiResponse"], + "scriptLanguage": "javascript", + "continueOnError": false, + "outputVariables": [ + { + "name": "profileFound", + "type": "boolean", + "description": "Indicates if a profile was found via search" + }, + { + "name": "loginSuccess", + "type": "boolean", + "description": "Indicates if the asnaf login was successful" + }, + { + "name": "profileData", + "type": "object", + "description": "The retrieved profile data" + }, + { + "name": "carianProfilStatus", + "type": "string", + "description": "The status of the profile search/login action" + } + ] + }, + "type": "script", + "label": "Process Carian Profil Response", + "position": { "x": 780, "y": 180 } + }, + { + "id": "rectangle-shape-1752115136908", + "data": { + "label": "", + "shape": "rectangle", + "width": 650, + "height": 400, + "isShape": true, + "shapeType": "rectangle", + "textColor": "#374151", + "borderColor": "#16a34a", + "description": "", + "backgroundColor": "#e8f5e9" + }, + "type": "rectangle-shape", + "label": "", + "position": { "x": 375, "y": -45 } + }, + { + "id": "text-annotation-1752115184991", + "data": { + "label": "", + "shape": "rectangle", + "width": 200, + "height": 80, + "isShape": true, + "shapeType": "text-annotation", + "textColor": "#92400e", + "borderColor": "#fbbf24", + "description": "Carian Profil", + "backgroundColor": "#fffbeb" + }, + "type": "text-annotation", + "label": "BF-NAS-PRF-AS-QS-01", + "position": { "x": 810, "y": -30 } } ], "viewport": { - "x": -117.3519061583577, - "y": 343.2355816226784, - "zoom": 0.7722385141739979 + "x": -104.1414298310864, + "y": 273.7689874210555, + "zoom": 0.402665859661672 } } diff --git a/docs/json/process-builder/processVariables.json b/docs/json/process-builder/processVariables.json index 377bade..b007680 100644 --- a/docs/json/process-builder/processVariables.json +++ b/docs/json/process-builder/processVariables.json @@ -95,6 +95,13 @@ "value": "123456789012", "description": "Bank account number from Section B" }, + "profileData": { + "name": "profileData", + "type": "object", + "scope": "global", + "value": null, + "description": "Data of the profile found" + }, "radioBangsa": { "name": "radioBangsa", "type": "string", @@ -137,6 +144,13 @@ "value": "", "description": "Birth certificate number from Section A" }, + "loginSuccess": { + "name": "loginSuccess", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Flag indicating successful login" + }, "okuAllowance": { "name": "okuAllowance", "type": "number", @@ -151,6 +165,13 @@ "value": true, "description": "Whether payment processing is ready" }, + "profileFound": { + "name": "profileFound", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Flag indicating if a profile was found" + }, "profileScore": { "name": "profileScore", "type": "number", @@ -165,6 +186,20 @@ "value": "lelaki", "description": "Gender from Section A" }, + "urgencyLevel": { + "name": "urgencyLevel", + "type": "string", + "scope": "global", + "value": null, + "description": "Urgency level (high/medium/low)" + }, + "urgencyScore": { + "name": "urgencyScore", + "type": "number", + "scope": "global", + "value": null, + "description": "Calculated urgency score based on needs" + }, "alamatSekolah": { "name": "alamatSekolah", "type": "string", @@ -186,6 +221,13 @@ "value": "APP-1751871528249", "description": "Generated application ID from script processing" }, + "carianLoginId": { + "name": "carianLoginId", + "type": "string", + "scope": "global", + "value": null, + "description": "Asnaf ID for login" + }, "daerahSekolah": { "name": "daerahSekolah", "type": "string", @@ -214,6 +256,13 @@ "value": true, "description": "Whether applicant has dependents" }, + "hasOtherNeeds": { + "name": "hasOtherNeeds", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has other needs" + }, "householdType": { "name": "householdType", "type": "string", @@ -270,6 +319,13 @@ "value": ["akaun"], "description": "Payment method from Section B" }, + "carianSearchId": { + "name": "carianSearchId", + "type": "string", + "scope": "global", + "value": null, + "description": "IC or Foreign ID used for profile search" + }, "dateMasukislam": { "name": "dateMasukislam", "type": "string", @@ -305,6 +361,34 @@ "value": null, "description": "Risk assessment result from verification" }, + "catatanTambahan": { + "name": "catatanTambahan", + "type": "string", + "scope": "global", + "value": null, + "description": "Catatan tambahan berkaitan permohonan" + }, + "hasDisasterNeed": { + "name": "hasDisasterNeed", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has disaster-related needs" + }, + "hasHomelessness": { + "name": "hasHomelessness", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant is homeless" + }, + "penilaianAwalId": { + "name": "penilaianAwalId", + "type": "string", + "scope": "global", + "value": null, + "description": "Generated ID for the initial assessment" + }, "radioPendidikan": { "name": "radioPendidikan", "type": "string", @@ -340,6 +424,20 @@ "value": "melayu", "description": "Dependent race from Section B" }, + "carianSearchType": { + "name": "carianSearchType", + "type": "string", + "scope": "global", + "value": null, + "description": "Type of search for Carian Profil (e.g., 'ic_search', 'login')" + }, + "dokumenBerkaitan": { + "name": "dokumenBerkaitan", + "type": "array", + "scope": "global", + "value": null, + "description": "Upload dokumen yang berkaitan" + }, "eligibilityScore": { "name": "eligibilityScore", "type": "number", @@ -347,6 +445,13 @@ "value": null, "description": "Eligibility score from verification API" }, + "hasNoUrgentNeeds": { + "name": "hasNoUrgentNeeds", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has no urgent needs" + }, "hubunganKeluarga": { "name": "hubunganKeluarga", "type": "array", @@ -389,6 +494,20 @@ "value": true, "description": "Whether documents verification is required" }, + "hasFamilyConflict": { + "name": "hasFamilyConflict", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has family conflict" + }, + "hasUtilityArrears": { + "name": "hasUtilityArrears", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has utility arrears" + }, "jantinaTanggungan": { "name": "jantinaTanggungan", "type": "string", @@ -396,6 +515,20 @@ "value": "perempuan", "description": "Dependent gender from Section B" }, + "keperluanMendesak": { + "name": "keperluanMendesak", + "type": "array", + "scope": "global", + "value": null, + "description": "Apakah keperluan tuan/puan mendesak sekarang ini?" + }, + "komitmenKosTinggi": { + "name": "komitmenKosTinggi", + "type": "string", + "scope": "global", + "value": null, + "description": "Adakah tuan/puan mempunyai komitmen dan pembiayaan melibatkan kos yang tinggi?" + }, "namaPemegangAkaun": { "name": "namaPemegangAkaun", "type": "string", @@ -431,6 +564,13 @@ "value": null, "description": "Notes from document verification process" }, + "carianProfilStatus": { + "name": "carianProfilStatus", + "type": "string", + "scope": "global", + "value": null, + "description": "Status of the Carian Profil step (e.g., 'found', 'not_found', 'login_success')" + }, "childcareAllowance": { "name": "childcareAllowance", "type": "number", @@ -459,6 +599,13 @@ "value": "mykad", "description": "Dependent ID type from Section B" }, + "penilaianAwalError": { + "name": "penilaianAwalError", + "type": "string", + "scope": "global", + "value": null, + "description": "Error message from penilaian awal processing" + }, "verificationStatus": { "name": "verificationStatus", "type": "string", @@ -473,6 +620,20 @@ "value": null, "description": "Whether process can proceed to kifayah analysis" }, + "carianLoginPassword": { + "name": "carianLoginPassword", + "type": "string", + "scope": "global", + "value": null, + "description": "Password for asnaf login" + }, + "hasDeathRelatedNeed": { + "name": "hasDeathRelatedNeed", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has death-related needs" + }, "noTelefonTanggungan": { "name": "noTelefonTanggungan", "type": "string", @@ -480,6 +641,13 @@ "value": "03-12345678", "description": "Dependent phone number from Section B" }, + "penilaianAwalStatus": { + "name": "penilaianAwalStatus", + "type": "string", + "scope": "global", + "value": null, + "description": "Status of initial assessment submission" + }, "spouseKifayahAmount": { "name": "spouseKifayahAmount", "type": "decimal", @@ -501,6 +669,27 @@ "value": "ya", "description": "Dependent currently studying from Section B" }, + "carianProfilApiError": { + "name": "carianProfilApiError", + "type": "object", + "scope": "global", + "value": null, + "description": "Error from Carian Profil API call" + }, + "hasUploadedDocuments": { + "name": "hasUploadedDocuments", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether documents were uploaded" + }, + "hasUrgentMedicalNeed": { + "name": "hasUrgentMedicalNeed", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has urgent medical needs" + }, "hubunganLainNyatakan": { "name": "hubunganLainNyatakan", "type": "string", @@ -515,6 +704,13 @@ "value": null, "description": "Error from kifayah analysis API call" }, + "readyForPersonalInfo": { + "name": "readyForPersonalInfo", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether ready for personal info form" + }, "verificationApiError": { "name": "verificationApiError", "type": "object", @@ -522,6 +718,20 @@ "value": null, "description": "Error from document verification API call" }, + "hasHighCostCommitment": { + "name": "hasHighCostCommitment", + "type": "boolean", + "scope": "global", + "value": null, + "description": "Whether applicant has high cost commitments" + }, + "keperluanLainNyatakan": { + "name": "keperluanLainNyatakan", + "type": "string", + "scope": "global", + "value": null, + "description": "Keperluan lain yang dinyatakan oleh pemohon" + }, "kifayahAnalysisResult": { "name": "kifayahAnalysisResult", "type": "object", @@ -529,6 +739,13 @@ "value": null, "description": "Result from kifayah eligibility analysis API" }, + "penilaianAwalApiError": { + "name": "penilaianAwalApiError", + "type": "object", + "scope": "global", + "value": null, + "description": "API error from Submit Penilaian Awal API" + }, "tarikhLahirTanggungan": { "name": "tarikhLahirTanggungan", "type": "string", @@ -585,6 +802,20 @@ "value": "ya", "description": "Dependent lives with family from Section B" }, + "carianProfilApiResponse": { + "name": "carianProfilApiResponse", + "type": "object", + "scope": "global", + "value": null, + "description": "API response from Carian Profil submission" + }, + "carianProfilScriptError": { + "name": "carianProfilScriptError", + "type": "object", + "scope": "global", + "value": null, + "description": "Error from Carian Profil script execution" + }, "chronicIllnessAllowance": { "name": "chronicIllnessAllowance", "type": "number", @@ -662,6 +893,20 @@ "value": "", "description": "Dependent other education specification from Section B" }, + "penilaianAwalApiResponse": { + "name": "penilaianAwalApiResponse", + "type": "object", + "scope": "global", + "value": null, + "description": "API response from Submit Penilaian Awal API" + }, + "penilaianAwalScriptError": { + "name": "penilaianAwalScriptError", + "type": "object", + "scope": "global", + "value": null, + "description": "Error from Penilaian Awal script execution" + }, "tarikhMulaKfamTanggungan": { "name": "tarikhMulaKfamTanggungan", "type": "string", diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 979b558..0d394f1 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -18,6 +18,8 @@ import BusinessRuleNodeConfiguration from '~/components/process-flow/BusinessRul import BusinessRuleNodeConfigurationModal from '~/components/process-flow/BusinessRuleNodeConfigurationModal.vue'; import NotificationNodeConfigurationModal from '~/components/process-flow/NotificationNodeConfigurationModal.vue'; import ScriptNodeConfigurationModal from '~/components/process-flow/ScriptNodeConfigurationModal.vue'; +import HtmlNodeConfigurationModal from '~/components/process-flow/HtmlNodeConfigurationModal.vue'; +import SubprocessNodeConfigurationModal from '~/components/process-flow/SubprocessNodeConfigurationModal.vue'; import ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue'; import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue'; import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue'; @@ -86,6 +88,8 @@ const showGatewayConfigModal = ref(false); const showBusinessRuleConfigModal = ref(false); const showNotificationConfigModal = ref(false); const showScriptConfigModal = ref(false); +const showHtmlConfigModal = ref(false); +const showSubprocessConfigModal = ref(false); const showTemplatesModal = ref(false); const showProcessSettings = ref(false); const showDropdown = ref(false); @@ -252,6 +256,22 @@ const components = [ iconColor: 'text-gray-500', data: { description: 'Script execution', language: 'JavaScript', shape: 'rectangle', backgroundColor: '#f9fafb', borderColor: '#6b7280', textColor: '#374151' } }, + { + type: 'html', + label: 'HTML', + icon: 'html', + iconColor: 'text-blue-500', + data: { + description: 'Custom HTML content', + htmlCode: '\n
\n

Custom HTML Content

\n

This is a custom HTML node that can display rich content.

\n
', + cssCode: '', + jsCode: '', + shape: 'rectangle', + backgroundColor: '#e0f2fe', + borderColor: '#0ea5e9', + textColor: '#0c4a6e' + } + }, { type: 'business-rule', label: 'Business Rule', @@ -266,6 +286,21 @@ const components = [ iconColor: 'text-blue-500', data: { description: 'Send notifications', shape: 'rectangle', backgroundColor: '#f0f9ff', borderColor: '#0ea5e9', textColor: '#0284c7' } }, + { + type: 'subprocess', + label: 'Sub Process', + icon: 'hub', + iconColor: 'text-teal-500', + data: { + description: 'Executes another process', + subprocessId: null, + subprocessName: '', + shape: 'rectangle', + backgroundColor: '#f0fdfa', + borderColor: '#14b8a6', + textColor: '#134e4a' + } + }, { type: 'end', label: 'End Point', @@ -387,6 +422,7 @@ const getNodeIcon = (nodeType) => { 'script': 'code', 'business-rule': 'rule', 'notification': 'notifications', + 'subprocess': 'hub', 'start': 'play_circle_filled', 'end': 'stop_circle', 'swimlane-horizontal': 'view-stream', @@ -530,7 +566,6 @@ const gatewayAvailableVariables = computed(() => { description: v.description || '' })); - // console.log('Gateway available variables:', allVars); return allVars; }); @@ -607,6 +642,9 @@ const onNodeSelected = (node) => { case 'notification': defaultColors = { backgroundColor: '#f0f9ff', borderColor: '#0ea5e9', textColor: '#0284c7' }; break; + case 'subprocess': + defaultColors = { backgroundColor: '#f0fdfa', borderColor: '#14b8a6', textColor: '#134e4a' }; + break; } if (!nodeCopy.data.backgroundColor) { @@ -751,6 +789,9 @@ const resetNodeColors = () => { case 'notification': defaultColors = { backgroundColor: '#f0f9ff', borderColor: '#0ea5e9', textColor: '#0284c7' }; break; + case 'subprocess': + defaultColors = { backgroundColor: '#f0fdfa', borderColor: '#14b8a6', textColor: '#134e4a' }; + break; } selectedNodeData.value.data.backgroundColor = defaultColors.backgroundColor; @@ -802,16 +843,17 @@ const onNodesChange = (changes, currentNodes) => { // Skip processing during component addition to avoid conflicts if (isAddingComponent.value) { - return; + return; } // Handle position changes (only when dragging is complete) const positionChanges = {}; - const hasPositionChanges = changes - .filter(change => change.type === 'position' && change.position && !change.dragging) - .forEach(change => { - positionChanges[change.id] = change.position; - }); + const positionChangesList = changes + .filter(change => change.type === 'position' && change.position && !change.dragging); + + positionChangesList.forEach(change => { + positionChanges[change.id] = change.position; + }); if (Object.keys(positionChanges).length > 0) { processStore.updateNodePositions(positionChanges); @@ -833,6 +875,16 @@ const onNodesChange = (changes, currentNodes) => { } } + // Handle node additions (this should be rare since we add nodes through the store first) + const addedNodes = changes + .filter(change => change.type === 'add') + .map(change => change.id); + + if (addedNodes.length > 0) { + // These nodes are already in the canvas, so we don't need to add them to the store + // unless they're not already there + } + // REMOVED: Don't overwrite selectedNodeData from canvas changes to preserve local edits // This was causing property panel changes to be lost when the canvas updated // The canvasNodes computed and its watcher will handle synchronization properly @@ -842,6 +894,11 @@ const onNodesChange = (changes, currentNodes) => { const onEdgesChange = (changes, currentEdges) => { if (!changes || !currentEdges) return; + // Skip processing during component addition to avoid conflicts + if (isAddingComponent.value) { + return; + } + // Handle edge removals const removedEdges = changes .filter(change => change.type === 'remove') @@ -872,7 +929,9 @@ const onEdgesChange = (changes, currentEdges) => { label: edge.label || '', type: edge.type || 'smoothstep', animated: edge.animated !== undefined ? edge.animated : true, - data: edge.data || {} + data: edge.data || {}, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle }); } }); @@ -1226,7 +1285,7 @@ const onAddComponent = async (component) => { ...component.data, // Ensure shape is set for new nodes shape: component.data.shape || (component.type === 'gateway' ? 'diamond' : 'rectangle'), - // Ensure default colors are set for new nodes (use component defaults which are now type-specific) + // Ensure default colors are set for new nodes backgroundColor: component.data.backgroundColor, borderColor: component.data.borderColor, textColor: component.data.textColor @@ -1234,30 +1293,78 @@ const onAddComponent = async (component) => { }; // Add the node to the process store - processStore.addNode(newNode); + await processStore.addNode(newNode); - // Wait for the next tick to ensure the store update is complete + // Wait for store update and next render cycle await nextTick(); + await new Promise(resolve => setTimeout(resolve, 50)); - // Explicitly sync the canvas with current store state - if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) { - processFlowCanvas.value.syncCanvas( - processStore.currentProcess.nodes, - processStore.currentProcess.edges - ); + // CRITICAL FIX: Instead of calling syncCanvas (which can cause edge removal/re-addition), + // we'll add the node directly to the canvas and preserve existing edges + if (processFlowCanvas.value) { + try { + // Get the fresh node from store (with any store-side modifications) + const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id); + + if (freshNode && processFlowCanvas.value.addNode) { + // Add only the new node to the canvas directly + processFlowCanvas.value.addNode(freshNode); + + // Wait for the node to be added to the canvas + await nextTick(); + await new Promise(resolve => setTimeout(resolve, 50)); + + // Select the newly added node after it's stable + onNodeSelected(freshNode); + + console.log('โœ… Successfully added new node without affecting existing edges'); + } else { + console.warn('โš ๏ธ Fresh node not found in store, falling back to full sync'); + // Fallback to full sync if something went wrong + const currentNodes = processStore.currentProcess?.nodes || []; + const currentEdges = processStore.currentProcess?.edges || []; + + if (processFlowCanvas.value.syncCanvas) { + processFlowCanvas.value.syncCanvas(currentNodes, currentEdges); + + await nextTick(); + await new Promise(resolve => setTimeout(resolve, 50)); + + const addedNode = currentNodes.find(n => n.id === newNode.id); + if (addedNode) { + onNodeSelected(addedNode); + } + } + } + } catch (error) { + console.error('โŒ Error adding node to canvas:', error); + + // Fallback to full sync if direct addition fails + const currentNodes = processStore.currentProcess?.nodes || []; + const currentEdges = processStore.currentProcess?.edges || []; + + if (processFlowCanvas.value.syncCanvas) { + console.log('๐Ÿ”„ Falling back to full canvas sync due to error'); + processFlowCanvas.value.syncCanvas(currentNodes, currentEdges); + + await nextTick(); + await new Promise(resolve => setTimeout(resolve, 50)); + + const addedNode = currentNodes.find(n => n.id === newNode.id); + if (addedNode) { + onNodeSelected(addedNode); + } + } + } } - - // Select the newly added node - onNodeSelected(newNode); - - } catch (error) { console.error('Error adding component:', error); + toast.error('Failed to add component. Please try again.'); } finally { - // Reset the flag after a short delay to allow canvas to stabilize + // Reset the flag after a longer delay to ensure canvas is stable setTimeout(() => { isAddingComponent.value = false; - }, 100); + }, 200); } }; @@ -1498,6 +1605,61 @@ const handleScriptNodeUpdate = async (updatedData) => { } }; +// Handle HTML node update +const handleHtmlNodeUpdate = async (updatedData) => { + if (selectedNodeData.value && selectedNodeData.value.type === 'html') { + // Make sure to update the label both in data and at the root level + const newLabel = updatedData.label || 'HTML Content'; + + // Update the data + selectedNodeData.value.data = { + ...updatedData, + label: newLabel // Ensure label is in data + }; + + // Also update the root label + selectedNodeData.value.label = newLabel; + + // Add output variables to the process + if (updatedData.outputVariables && Array.isArray(updatedData.outputVariables)) { + updatedData.outputVariables.forEach(output => { + if (output.name && output.name.trim()) { + processStore.addProcessVariable({ + name: output.name, + type: output.type || 'string', + scope: 'global', + value: null, + description: output.description || `Output from ${newLabel}` + }); + } + }); + } + + // Update the node in store and refresh + await updateNodeInStore(); + } +}; + +// Handle Sub-process node update +const handleSubprocessNodeUpdate = async (updatedData) => { + if (selectedNodeData.value && selectedNodeData.value.type === 'subprocess') { + // Make sure to update the label both in data and at the root level + const newLabel = updatedData.label || 'Sub Process'; + + // Update the data + selectedNodeData.value.data = { + ...updatedData, + label: newLabel // Ensure label is in data + }; + + // Also update the root label + selectedNodeData.value.label = newLabel; + + // Update the node in store and refresh + await updateNodeInStore(); + } +}; + // Handle process restoration from history const handleProcessRestored = (restoredProcess) => { // The process has been restored in the backend, so we need to reload it @@ -1618,6 +1780,51 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => { } }, 200); // Allow time for canvas to initialize }, { immediate: false }); + +// Duplicate Node logic +const duplicateNode = async () => { + if (!selectedNodeData.value) return; + const node = selectedNodeData.value; + // Prevent duplication for start/end nodes and shapes + if (node.type === 'start' || node.type === 'end' || node.data?.isShape) return; + + // Deep copy node + const newNode = JSON.parse(JSON.stringify(node)); + newNode.id = `${node.type}_${Date.now()}_${Math.floor(Math.random() * 10000)}`; + // Offset position + newNode.position = { + x: (node.position?.x || 100) + 40, + y: (node.position?.y || 100) + 40 + }; + // Update label + if (newNode.label) { + newNode.label = newNode.label.endsWith(' (Copy)') ? newNode.label : `${newNode.label} (Copy)`; + } + if (newNode.data && newNode.data.label) { + newNode.data.label = newNode.data.label.endsWith(' (Copy)') ? newNode.data.label : `${newNode.data.label} (Copy)`; + } + // Remove any edge/connection-specific data if present + delete newNode.selected; + delete newNode.dragging; + + // Add to store only + await processStore.addNode(newNode); + await nextTick(); + // Select the new node from the store + const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id); + if (freshNode) { + onNodeSelected(freshNode); + } +}; + +// Add computed property for action visibility +const canShowNodeActions = computed(() => { + return ( + selectedNodeData.value && + selectedNodeData.value.type !== 'start' && + selectedNodeData.value.type !== 'end' + ); +});