diff --git a/components/ComponentPreview.vue b/components/ComponentPreview.vue index f890135..bcbed82 100644 --- a/components/ComponentPreview.vue +++ b/components/ComponentPreview.vue @@ -26,7 +26,8 @@
-

Item {{ groupIndex + 1 }}

- @@ -181,7 +182,7 @@
-
-
-
- @@ -290,7 +291,7 @@ {{ validationErrors[component.props.name] }}
- -
@@ -1735,7 +1745,8 @@ const showField = (fieldName) => { value: ['heading', 'paragraph', 'hidden'], width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'form-section', 'info-display', 'dynamic-list', 'repeating-table', 'repeating-group'], options: ['select', 'checkbox', 'radio'], - conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'] + conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'], + readonly: ['text', 'number', 'email', 'textarea', 'mask', 'url', 'tel'] } return fieldConfig[fieldName]?.includes(props.component.type) || false @@ -1779,7 +1790,7 @@ const isTextBasedField = computed(() => { // Type changing functionality const compatibilityGroups = { // Text-based inputs (can switch between each other) - textInputs: ['text', 'textarea', 'email', 'password', 'url', 'tel', 'mask'], + textInputs: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask'], // Selection inputs (can switch between each other) selectionInputs: ['select', 'radio', 'checkbox'], diff --git a/components/FormScriptEngine.vue b/components/FormScriptEngine.vue index bffc849..aa23e5f 100644 --- a/components/FormScriptEngine.vue +++ b/components/FormScriptEngine.vue @@ -292,10 +292,13 @@ const handleFieldChange = (fieldName, newValue, oldValue) => { }; // Watch for script changes and reinitialize -watch(() => props.customScript, (newScript, oldScript) => { +watch(() => props.customScript, async (newScript, oldScript) => { if (newScript !== oldScript) { isScriptInitialized.value = false; - initializeScript(); + await nextTick(); + setTimeout(() => { + initializeScript(); + }, 200); } }); @@ -325,10 +328,11 @@ watch(() => props.customCSS, () => { }); // Lifecycle hooks -onMounted(() => { +onMounted(async () => { + await nextTick(); setTimeout(() => { initializeScript(); - }, 500); // Delay to ensure parent DOM and initial props are settled + }, 200); // Delay to ensure all FormKit fields are loaded }); onUnmounted(() => { diff --git a/composables/safeGetField.js b/composables/safeGetField.js new file mode 100644 index 0000000..3141758 --- /dev/null +++ b/composables/safeGetField.js @@ -0,0 +1,11 @@ +// Utility to safely get a field value from a form data object +export function safeGetField(field, formData) { + if (formData && Object.prototype.hasOwnProperty.call(formData, field)) { + return formData[field] !== undefined && formData[field] !== null ? formData[field] : ''; + } + if (process.env.NODE_ENV !== 'production') { + // Only warn in development + console.warn(`Field '${field}' is missing or inaccessible.`); + } + return ''; +} \ No newline at end of file diff --git a/docs/json/form/manager-approval-customScript.js b/docs/json/form/manager-approval-customScript.js index 49dd4fd..dd42bb1 100644 --- a/docs/json/form/manager-approval-customScript.js +++ b/docs/json/form/manager-approval-customScript.js @@ -1,232 +1,273 @@ // Manager Approval Form Custom Script Engine // This script provides dynamic behavior for the manager approval form -console.log('Manager Approval Form Script Loaded'); +console.log("Manager Approval Form Script Loaded"); // Auto-set approval date to today const setApprovalDate = () => { const today = new Date(); - const formattedDate = today.toISOString().split('T')[0]; // YYYY-MM-DD format - setField('approval_date', formattedDate); + const formattedDate = today.toISOString().split("T")[0]; // YYYY-MM-DD format + setField("approval_date", formattedDate); }; // Calculate and display recommended approval amounts const calculateRecommendedAmounts = () => { - const totalClaimed = parseFloat(getField('total_cost_display')) || 0; - const policyLimit = parseFloat(getField('policy_limit_display')) || 0; - const overBudget = parseFloat(getField('over_budget_amount_display')) || 0; - + const totalClaimed = parseFloat(getField("total_cost_display")) || 0; + const policyLimit = parseFloat(getField("policy_limit_display")) || 0; + const overBudget = parseFloat(getField("over_budget_amount_display")) || 0; + if (totalClaimed > 0 && policyLimit > 0) { const percentageOver = ((overBudget / policyLimit) * 100).toFixed(1); - showInfo(`This claim is ${percentageOver}% over the policy limit. Policy limit: RM${policyLimit.toFixed(2)}, Claimed: RM${totalClaimed.toFixed(2)}`); + showInfo( + `This claim is ${percentageOver}% over the policy limit. Policy limit: RM${policyLimit.toFixed(2)}, Claimed: RM${totalClaimed.toFixed(2)}` + ); } }; // Validate manager decision and provide guidance const handleDecisionChange = (decision) => { - const totalClaimed = parseFloat(getField('total_cost_display')) || 0; - const policyLimit = parseFloat(getField('policy_limit_display')) || 0; - - console.log('Manager decision changed to:', decision); - - switch(decision) { - case 'approve_full': - showField('custom_approved_amount'); - showInfo(`šŸ’° Approving full amount: RM${totalClaimed.toFixed(2)}. You can enter a custom amount if needed.`); - setField('custom_approved_amount', totalClaimed.toString()); + const totalClaimed = parseFloat(getField("total_cost_display")) || 0; + const policyLimit = parseFloat(getField("policy_limit_display")) || 0; + + console.log("Manager decision changed to:", decision); + + switch (decision) { + case "approve_full": + showField("custom_approved_amount"); + showInfo( + `šŸ’° Approving full amount: RM${totalClaimed.toFixed(2)}. You can enter a custom amount if needed.` + ); + setField("custom_approved_amount", totalClaimed.toString()); break; - - case 'approve_policy': - hideField('custom_approved_amount'); - showInfo(`āš–ļø Approving policy limit only: RM${policyLimit.toFixed(2)}. Employee will be notified of the reduced amount.`); + + case "approve_policy": + hideField("custom_approved_amount"); + showInfo( + `āš–ļø Approving policy limit only: RM${policyLimit.toFixed(2)}. Employee will be notified of the reduced amount.` + ); break; - - case 'reject': - hideField('custom_approved_amount'); - showError('āŒ Claim will be rejected. Please provide detailed comments explaining the rejection reason.'); + + case "reject": + hideField("custom_approved_amount"); + showError( + "āŒ Claim will be rejected. Please provide detailed comments explaining the rejection reason." + ); break; - + default: - hideField('custom_approved_amount'); + hideField("custom_approved_amount"); } }; // Validate custom approved amount const validateCustomAmount = (amount) => { const numAmount = parseFloat(amount) || 0; - const totalClaimed = parseFloat(getField('total_cost_display')) || 0; - const policyLimit = parseFloat(getField('policy_limit_display')) || 0; - + const totalClaimed = parseFloat(getField("total_cost_display")) || 0; + const policyLimit = parseFloat(getField("policy_limit_display")) || 0; + if (numAmount < 0) { - showError('Approved amount cannot be negative'); + showError("Approved amount cannot be negative"); return false; } - + if (numAmount > totalClaimed) { - showError(`Approved amount (RM${numAmount}) cannot exceed claimed amount (RM${totalClaimed})`); + showError( + `Approved amount (RM${numAmount}) cannot exceed claimed amount (RM${totalClaimed})` + ); return false; } - + if (numAmount > 0 && numAmount < policyLimit) { - showInfo(`Custom amount (RM${numAmount}) is less than policy limit (RM${policyLimit}). Consider approving policy limit instead.`); + showInfo( + `Custom amount (RM${numAmount}) is less than policy limit (RM${policyLimit}). Consider approving policy limit instead.` + ); } - + return true; }; // Validate manager comments based on decision const validateComments = (comments, decision) => { if (!comments || comments.trim().length < 10) { - showError('Please provide detailed comments (minimum 10 characters)'); + showError("Please provide detailed comments (minimum 10 characters)"); return false; } - - if (decision === 'reject' && comments.length < 50) { - showError('Rejection requires detailed explanation (minimum 50 characters)'); + + if (decision === "reject" && comments.length < 50) { + showError( + "Rejection requires detailed explanation (minimum 50 characters)" + ); return false; } - - if (decision === 'approve_full') { - const totalClaimed = parseFloat(getField('total_cost_display')) || 0; - const policyLimit = parseFloat(getField('policy_limit_display')) || 0; + + if (decision === "approve_full") { + const totalClaimed = parseFloat(getField("total_cost_display")) || 0; + const policyLimit = parseFloat(getField("policy_limit_display")) || 0; const overBudget = totalClaimed - policyLimit; - - if (overBudget > 500 && !comments.toLowerCase().includes('business')) { - showInfo('Consider mentioning business justification for approving over-budget amounts > RM500'); + + if (overBudget > 500 && !comments.toLowerCase().includes("business")) { + showInfo( + "Consider mentioning business justification for approving over-budget amounts > RM500" + ); } } - + return true; }; // Format manager name (title case) const formatManagerName = (name) => { - if (!name || typeof name !== 'string') return name; - + if (!name || typeof name !== "string") return name; + return name .toLowerCase() - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); }; // Form validation before submission const validateApprovalForm = () => { let isValid = true; const errors = []; - + // Check required fields const requiredFields = { - 'manager_decision': 'Approval Decision', - 'manager_comments': 'Manager Comments', - 'manager_name': 'Manager Name', - 'approval_date': 'Approval Date' + manager_decision: "Approval Decision", + manager_comments: "Manager Comments", + manager_name: "Manager Name", + approval_date: "Approval Date", }; - + Object.entries(requiredFields).forEach(([fieldName, displayName]) => { const value = getField(fieldName); - if (!value || value.toString().trim() === '') { + if (!value || value.toString().trim() === "") { errors.push(`${displayName} is required`); isValid = false; } }); - + // Validate decision-specific requirements - const decision = getField('manager_decision'); - const comments = getField('manager_comments'); - const customAmount = getField('custom_approved_amount'); - + const decision = getField("manager_decision"); + const comments = getField("manager_comments"); + const customAmount = getField("custom_approved_amount"); + if (decision && comments) { if (!validateComments(comments, decision)) { isValid = false; } } - - if (decision === 'approve_full' && customAmount) { + + if (decision === "approve_full" && customAmount) { if (!validateCustomAmount(customAmount)) { isValid = false; } } - + // Validate approval date is not in the future - const approvalDate = getField('approval_date'); + const approvalDate = getField("approval_date"); if (approvalDate) { const selectedDate = new Date(approvalDate); const today = new Date(); today.setHours(23, 59, 59, 999); // End of today - + if (selectedDate > today) { - errors.push('Approval date cannot be in the future'); + errors.push("Approval date cannot be in the future"); isValid = false; } } - + // Show validation results if (errors.length > 0) { - showError(`Please fix the following errors:\n• ${errors.join('\n• ')}`); + showError(`Please fix the following errors:\n• ${errors.join("\n• ")}`); } else { - showSuccess('Form validation passed! Ready to submit approval decision.'); + showSuccess("Form validation passed! Ready to submit approval decision."); } - + return isValid; }; // Set up field change handlers -onFieldChange('manager_decision', (newValue) => { +onFieldChange("manager_decision", (newValue) => { if (newValue) { handleDecisionChange(newValue); } }); -onFieldChange('custom_approved_amount', (newValue) => { +onFieldChange("custom_approved_amount", (newValue) => { if (newValue) { validateCustomAmount(newValue); } }); -onFieldChange('manager_comments', (newValue) => { - const decision = getField('manager_decision'); +onFieldChange("manager_comments", (newValue) => { + const decision = getField("manager_decision"); if (newValue && decision) { validateComments(newValue, decision); } }); -onFieldChange('manager_name', (newValue) => { - if (newValue && typeof newValue === 'string') { +onFieldChange("manager_name", (newValue) => { + if (newValue && typeof newValue === "string") { const formatted = formatManagerName(newValue); if (formatted !== newValue) { setTimeout(() => { - setField('manager_name', formatted); + setField("manager_name", formatted); }, 100); } } }); -// Initialize form on load -setTimeout(() => { - setApprovalDate(); - calculateRecommendedAmounts(); - - // Make display fields read-only by adding visual styling - const displayFields = [ - 'claim_summary', 'employee_name_display', 'department_display', - 'trip_purpose_display', 'destination_display', 'total_cost_display', - 'policy_limit_display', 'over_budget_amount_display' - ]; - - displayFields.forEach(fieldName => { - const fieldElement = document.querySelector(`[data-name="${fieldName}"] input`); - if (fieldElement) { - fieldElement.style.backgroundColor = '#f3f4f6'; - fieldElement.style.cursor = 'not-allowed'; - fieldElement.readOnly = true; +// Wait until all required fields are present before running initialization +function waitForFields(fields, callback, maxAttempts = 20) { + let attempts = 0; + function check() { + const allPresent = fields.every(f => getField(f) !== undefined); + if (allPresent) { + callback(); + } else if (attempts < maxAttempts) { + attempts++; + setTimeout(check, 100); + } else { + showError("Some required fields are missing. Please reload the page."); } - }); - - showInfo('Review the claim details above and make your approval decision below.'); -}, 1000); + } + check(); +} + +waitForFields( + [ + "total_cost_display", + "policy_limit_display", + "over_budget_amount_display", + // add any other required fields here + ], + () => { + setApprovalDate(); + calculateRecommendedAmounts(); + showInfo("Review the claim details above and make your approval decision below."); + } +); // Expose validation function for form submission window.validateManagerApprovalForm = validateApprovalForm; -console.log('Manager Approval Form Script initialized successfully'); \ No newline at end of file +console.log("Manager Approval Form Script initialized successfully"); + +// Conditional logic for field: custom_approved_amount +onFieldChange("manager_decision", function () { + if (getField("manager_decision") === "approve_full") { + showField("custom_approved_amount"); + } else { + hideField("custom_approved_amount"); + } +}); + +// Initial evaluation for field: custom_approved_amount +(function () { + if (getField("manager_decision") === "approve_full") { + showField("custom_approved_amount"); + } else { + hideField("custom_approved_amount"); + } +})(); diff --git a/docs/json/form/manager-approval-form.json b/docs/json/form/manager-approval-form.json index 35b0fb5..018d51e 100644 --- a/docs/json/form/manager-approval-form.json +++ b/docs/json/form/manager-approval-form.json @@ -158,60 +158,42 @@ } }, { - "type": "text", + "type": "number", "props": { "help": "Total amount claimed by employee", "name": "total_cost_display", - "type": "text", + "type": "number", "label": "Total Claimed Amount (RM)", "width": "33.33%", "gridColumn": "span 4", "validation": "", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { - "type": "text", + "type": "number", "props": { "help": "Maximum allowed per company policy", "name": "policy_limit_display", - "type": "text", + "type": "number", "label": "Policy Limit (RM)", "width": "33.33%", "gridColumn": "span 4", "validation": "", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { - "type": "text", + "type": "number", "props": { "help": "Amount exceeding policy limits", "name": "over_budget_amount_display", - "type": "text", + "type": "number", "label": "Over Budget Amount (RM)", "width": "33.33%", "gridColumn": "span 4", "validation": "", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { @@ -254,9 +236,18 @@ "label": "Approval Decision", "width": "100%", "options": [ - { "label": "āœ… Approve Full Amount - Employee will receive the full claimed amount", "value": "approve_full" }, - { "label": "āš ļø Approve Policy Limit Only - Employee will receive only the policy-allowed amount", "value": "approve_policy" }, - { "label": "āŒ Reject Claim - Claim will be denied entirely", "value": "reject" } + { + "label": "āœ… Approve Full Amount - Employee will receive the full claimed amount", + "value": "approve_full" + }, + { + "label": "āš ļø Approve Policy Limit Only - Employee will receive only the policy-allowed amount", + "value": "approve_policy" + }, + { + "label": "āŒ Reject Claim - Claim will be denied entirely", + "value": "reject" + } ], "gridColumn": "span 12", "validation": "required", @@ -286,8 +277,8 @@ "conditions": [ { "field": "manager_decision", - "operator": "eq", - "value": "approve_full" + "value": "approve_full", + "operator": "eq" } ] } @@ -337,10 +328,19 @@ "label": "Additional Requirements", "width": "100%", "options": [ - { "label": "Require additional documentation from employee", "value": "require_docs" }, - { "label": "Schedule follow-up meeting with employee", "value": "follow_up" }, + { + "label": "Require additional documentation from employee", + "value": "require_docs" + }, + { + "label": "Schedule follow-up meeting with employee", + "value": "follow_up" + }, { "label": "Flag for finance team review", "value": "finance_review" }, - { "label": "Add to employee's travel record for future reference", "value": "record_flag" } + { + "label": "Add to employee's travel record for future reference", + "value": "record_flag" + } ], "gridColumn": "span 12", "validation": "", @@ -405,4 +405,4 @@ } } } -] \ No newline at end of file +] diff --git a/docs/json/form/travel-reimbursement-form.json b/docs/json/form/travel-reimbursement-form.json index 491748a..dc6ae08 100644 --- a/docs/json/form/travel-reimbursement-form.json +++ b/docs/json/form/travel-reimbursement-form.json @@ -234,7 +234,7 @@ } }, { - "type": "text", + "type": "number", "props": { "help": "Enter transportation costs (flights, trains, taxis, etc.)", "name": "transport_cost", @@ -243,17 +243,11 @@ "width": "50%", "gridColumn": "span 6", "validation": "required|min:0", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { - "type": "text", + "type": "number", "props": { "help": "Enter accommodation costs (hotels, lodging, etc.)", "name": "accommodation_cost", @@ -262,17 +256,11 @@ "width": "50%", "gridColumn": "span 6", "validation": "required|min:0", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { - "type": "text", + "type": "number", "props": { "help": "Enter meal and food expenses", "name": "meals_cost", @@ -281,17 +269,11 @@ "width": "50%", "gridColumn": "span 6", "validation": "required|min:0", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { - "type": "text", + "type": "number", "props": { "help": "Enter other miscellaneous expenses", "name": "other_cost", @@ -300,33 +282,20 @@ "width": "50%", "gridColumn": "span 6", "validation": "required|min:0", - "placeholder": "0.00", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { - "type": "text", + "type": "number", "props": { + "help": "This will be automatically calculated from the above costs", "name": "total_cost_display", - "type": "text", + "type": "number", "label": "Total Claim Amount (RM)", - "value": "0.00", "width": "100%", "gridColumn": "span 12", - "help": "This will be automatically calculated from the above costs", - "placeholder": "0.00", "validation": "", - "conditionalLogic": { - "action": "show", - "enabled": false, - "operator": "and", - "conditions": [] - } + "placeholder": "0.00" } }, { @@ -381,4 +350,4 @@ } } } -] \ No newline at end of file +] diff --git a/docs/json/process-builder/travel-workflow-process.json b/docs/json/process-builder/travel-workflow-process.json index 884a949..8659062 100644 --- a/docs/json/process-builder/travel-workflow-process.json +++ b/docs/json/process-builder/travel-workflow-process.json @@ -377,9 +377,9 @@ "assignedUsers": [], "inputMappings": [ { "processVariable": "claimSummary", "formField": "claim_summary" }, - { "processVariable": "totalCost", "formField": "total_cost" }, - { "processVariable": "reimbursementAmount", "formField": "reimbursement_amount" }, - { "processVariable": "overBudgetAmount", "formField": "over_budget_amount" } + { "processVariable": "totalCost", "formField": "total_cost_display" }, + { "processVariable": "overBudgetAmount", "formField": "over_budget_amount_display" }, + { "processVariable": "totalAllowed", "formField": "policy_limit_display" } ], "assignmentType": "roles", "outputMappings": [ diff --git a/pages/workflow/[id].vue b/pages/workflow/[id].vue index 122051a..e211862 100644 --- a/pages/workflow/[id].vue +++ b/pages/workflow/[id].vue @@ -462,12 +462,24 @@ function substituteVariables(str, variables) { function applyInputMappings(nodeData, processVars, formData) { const { inputMappings = [] } = nodeData; console.log('[Workflow] Applying input mappings:', inputMappings); - + inputMappings.forEach(mapping => { const { processVariable, formField } = mapping; if (processVars[processVariable] !== undefined) { - formData[formField] = processVars[processVariable]; - console.log(`[Workflow] Mapped ${processVariable} -> ${formField}:`, processVars[processVariable]); + let value = processVars[processVariable]; + + // Check if the form field is a date field + const fieldDef = nodeData.formComponents?.find(f => f.props?.name === formField); + if (fieldDef && fieldDef.type === 'date' && value) { + // Convert to YYYY-MM-DD + const d = new Date(value); + if (!isNaN(d)) { + value = d.toISOString().slice(0, 10); + } + } + + formData[formField] = value; + console.log(`[Workflow] Mapped ${processVariable} -> ${formField}:`, value); } }); } @@ -915,6 +927,9 @@ watch(currentStep, async (newStep) => { } // Update form store with form components and data for ComponentPreview + // Clear previous form state to prevent stale/leaked fields + formStore.formComponents = []; + formStore.updatePreviewFormData({}); if (currentForm.value?.formComponents) { formStore.formComponents = currentForm.value.formComponents; formStore.updatePreviewFormData(formData.value); @@ -1330,6 +1345,7 @@ function getConditionGroupResult(conditionGroup, variables) { :form-components="currentForm.formComponents" :form-data="formData" @script-generated="handleConditionalLogicGenerated" + :key="currentNode.value?.id" /> @@ -1341,6 +1357,7 @@ function getConditionGroupResult(conditionGroup, variables) { :form-events="currentForm.formEvents || { onLoad: true, onFieldChange: true }" :script-mode="currentForm.scriptMode || 'safe'" @field-change="handleScriptFieldChange" + :key="currentNode.value?.id" /> @@ -1352,10 +1369,11 @@ function getConditionGroupResult(conditionGroup, variables) { :actions="false" :incomplete-message="false" validation-visibility="submit" + :key="currentNode.value?.id" >