Enhance ComponentPreview and FormBuilderFieldSettingsModal for Improved Field Management
- Updated the ComponentPreview component to utilize a new utility function, safeGetField, for safer field value retrieval from form data, enhancing data integrity and error handling. - Added a new "Read Only" toggle option in the FormBuilderFieldSettingsModal, allowing users to set fields as non-editable, improving form configuration flexibility. - Refactored various field access patterns to ensure consistent handling of undefined or null values across components. - Improved the overall user experience by ensuring that field states are accurately reflected and managed during form previews.
This commit is contained in:
parent
8b44adf06e
commit
84e32e4dc7
@ -26,7 +26,8 @@
|
|||||||
<FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type" :name="component.props.name"
|
<FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type" :name="component.props.name"
|
||||||
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
||||||
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||||
:readonly="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
|
:readonly="component.props.readonly || isPreview"
|
||||||
|
:disabled="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
|
||||||
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
||||||
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
||||||
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
||||||
@ -162,11 +163,11 @@
|
|||||||
|
|
||||||
<!-- Functional groups (in form view) -->
|
<!-- Functional groups (in form view) -->
|
||||||
<div v-else class="repeating-groups space-y-4">
|
<div v-else class="repeating-groups space-y-4">
|
||||||
<div v-for="(group, groupIndex) in (previewFormData[component.props.name] || [])" :key="groupIndex"
|
<div v-for="(group, groupIndex) in (safeGetField(component.props.name, previewFormData) || [])" :key="groupIndex"
|
||||||
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
||||||
<div class="flex justify-between items-center mb-3">
|
<div class="flex justify-between items-center mb-3">
|
||||||
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
|
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
|
||||||
<button v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 1)"
|
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) > (component.props.minItems || 1)"
|
||||||
type="button" class="text-red-500 hover:text-red-700 text-sm" @click="removeGroupItem(groupIndex)">
|
type="button" class="text-red-500 hover:text-red-700 text-sm" @click="removeGroupItem(groupIndex)">
|
||||||
{{ component.props.removeText || 'Remove' }}
|
{{ component.props.removeText || 'Remove' }}
|
||||||
</button>
|
</button>
|
||||||
@ -181,7 +182,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 10)"
|
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) < (component.props.maxItems || 10)"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
||||||
@click="addGroupItem">
|
@click="addGroupItem">
|
||||||
@ -198,7 +199,7 @@
|
|||||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
{{ component.props.label }}
|
{{ component.props.label }}
|
||||||
<span v-if="component.props.showItemCounter" class="text-xs text-gray-500 ml-2">
|
<span v-if="component.props.showItemCounter" class="text-xs text-gray-500 ml-2">
|
||||||
({{ (previewFormData[component.props.name] || []).length }}/{{ component.props.maxItems || 20 }})
|
({{ (safeGetField(component.props.name, previewFormData) || []).length }}/{{ component.props.maxItems || 20 }})
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@ -217,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bulk Operations Toolbar (if enabled) -->
|
<!-- Bulk Operations Toolbar (if enabled) -->
|
||||||
<div v-if="component.props.bulkOperations && (previewFormData[component.props.name] || []).length > 0"
|
<div v-if="component.props.bulkOperations && (safeGetField(component.props.name, previewFormData) || []).length > 0"
|
||||||
class="mb-3 flex items-center justify-between bg-gray-50 p-2 rounded">
|
class="mb-3 flex items-center justify-between bg-gray-50 p-2 rounded">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<input type="checkbox" :checked="isAllSelected(component.props.name)"
|
<input type="checkbox" :checked="isAllSelected(component.props.name)"
|
||||||
@ -270,7 +271,7 @@
|
|||||||
:checked="isItemSelected(component.props.name, index)"
|
:checked="isItemSelected(component.props.name, index)"
|
||||||
@change="toggleItemSelection(component.props.name, index)"
|
@change="toggleItemSelection(component.props.name, index)"
|
||||||
class="mr-2 h-4 w-4 rounded border-gray-300" />
|
class="mr-2 h-4 w-4 rounded border-gray-300" />
|
||||||
<input :type="component.props.itemType || 'text'" v-model="previewFormData[component.props.name][index]"
|
<input :type="component.props.itemType || 'text'" v-model="safeGetField(component.props.name, previewFormData)[index]"
|
||||||
:placeholder="component.props.placeholder"
|
:placeholder="component.props.placeholder"
|
||||||
:class="getItemInputClasses(component.props.name, index, item)"
|
:class="getItemInputClasses(component.props.name, index, item)"
|
||||||
@blur="validateItem(component.props.name, index, item)"
|
@blur="validateItem(component.props.name, index, item)"
|
||||||
@ -279,7 +280,7 @@
|
|||||||
class="ml-2 cursor-move text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity">
|
class="ml-2 cursor-move text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
|
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<button v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 0)"
|
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) > (component.props.minItems || 0)"
|
||||||
type="button" class="ml-2 text-red-500 hover:text-red-700" @click="removeListItem(index)">
|
type="button" class="ml-2 text-red-500 hover:text-red-700" @click="removeListItem(index)">
|
||||||
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -290,7 +291,7 @@
|
|||||||
{{ validationErrors[component.props.name] }}
|
{{ validationErrors[component.props.name] }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 20)"
|
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) < (component.props.maxItems || 20)"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
||||||
@click="addListItem">
|
@click="addListItem">
|
||||||
@ -299,7 +300,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Export Button (if enabled) -->
|
<!-- Export Button (if enabled) -->
|
||||||
<div v-if="component.props.exportFormat && (previewFormData[component.props.name] || []).length > 0"
|
<div v-if="component.props.exportFormat && (safeGetField(component.props.name, previewFormData) || []).length > 0"
|
||||||
class="mt-2">
|
class="mt-2">
|
||||||
<button @click="exportItems(component.props.name, component.props.exportFormat)"
|
<button @click="exportItems(component.props.name, component.props.exportFormat)"
|
||||||
class="text-xs text-green-600 hover:text-green-800 flex items-center">
|
class="text-xs text-green-600 hover:text-green-800 flex items-center">
|
||||||
@ -316,7 +317,7 @@
|
|||||||
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
|
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
|
||||||
<RepeatingTable
|
<RepeatingTable
|
||||||
:config="component.props"
|
:config="component.props"
|
||||||
:model-value="previewFormData[component.props.name] || []"
|
:model-value="safeGetField(component.props.name, previewFormData) || []"
|
||||||
:is-preview="isPreview"
|
:is-preview="isPreview"
|
||||||
@update:model-value="updateTableData"
|
@update:model-value="updateTableData"
|
||||||
/>
|
/>
|
||||||
@ -515,6 +516,8 @@
|
|||||||
import { useNuxtApp } from '#app';
|
import { useNuxtApp } from '#app';
|
||||||
import { useFormBuilderStore } from '~/stores/formBuilder';
|
import { useFormBuilderStore } from '~/stores/formBuilder';
|
||||||
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
||||||
|
import { safeGetField } from '~/composables/safeGetField';
|
||||||
|
import { onMounted, onUnmounted, watch, computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
@ -549,6 +552,27 @@ const sectionDropStates = ref({});
|
|||||||
const showNestedSettingsModal = ref(false);
|
const showNestedSettingsModal = ref(false);
|
||||||
const selectedNestedComponent = ref(null);
|
const selectedNestedComponent = ref(null);
|
||||||
|
|
||||||
|
// Track timers and DOM elements for cleanup
|
||||||
|
let lightbox = null;
|
||||||
|
let importInput = null;
|
||||||
|
let timers = [];
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Clean up any lightbox DOM element
|
||||||
|
if (lightbox && document.body.contains(lightbox)) {
|
||||||
|
document.body.removeChild(lightbox);
|
||||||
|
lightbox = null;
|
||||||
|
}
|
||||||
|
// Clean up import input
|
||||||
|
if (importInput && document.body.contains(importInput)) {
|
||||||
|
document.body.removeChild(importInput);
|
||||||
|
importInput = null;
|
||||||
|
}
|
||||||
|
// Clear all timers
|
||||||
|
timers.forEach(t => clearTimeout(t));
|
||||||
|
timers = [];
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize data for components
|
// Initialize data for components
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Initialize search and selection data for dynamic lists
|
// Initialize search and selection data for dynamic lists
|
||||||
@ -560,7 +584,7 @@ onMounted(() => {
|
|||||||
validationErrors.value[listName] = '';
|
validationErrors.value[listName] = '';
|
||||||
|
|
||||||
// Initialize form data with default items if they exist and form data is empty
|
// Initialize form data with default items if they exist and form data is empty
|
||||||
const currentFormData = formStore.previewFormData[listName];
|
const currentFormData = safeGetField(listName, formStore.previewFormData);
|
||||||
const defaultItems = props.component.props.defaultItems;
|
const defaultItems = props.component.props.defaultItems;
|
||||||
|
|
||||||
if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) {
|
if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) {
|
||||||
@ -593,7 +617,7 @@ const addGroupItem = () => {
|
|||||||
if (!groupName) return;
|
if (!groupName) return;
|
||||||
|
|
||||||
// Get current groups or initialize empty array
|
// Get current groups or initialize empty array
|
||||||
const currentGroups = formStore.previewFormData[groupName] || [];
|
const currentGroups = safeGetField(groupName, formStore.previewFormData) || [];
|
||||||
|
|
||||||
// Create a new empty group
|
// Create a new empty group
|
||||||
const newGroup = {};
|
const newGroup = {};
|
||||||
@ -620,7 +644,7 @@ const removeGroupItem = (index) => {
|
|||||||
if (!groupName) return;
|
if (!groupName) return;
|
||||||
|
|
||||||
// Get current groups
|
// Get current groups
|
||||||
const currentGroups = [...(formStore.previewFormData[groupName] || [])];
|
const currentGroups = [...(safeGetField(groupName, formStore.previewFormData) || [])];
|
||||||
|
|
||||||
// Remove the group at the specified index
|
// Remove the group at the specified index
|
||||||
currentGroups.splice(index, 1);
|
currentGroups.splice(index, 1);
|
||||||
@ -637,7 +661,7 @@ const addListItem = () => {
|
|||||||
if (!listName) return;
|
if (!listName) return;
|
||||||
|
|
||||||
// Get current items or initialize empty array
|
// Get current items or initialize empty array
|
||||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])];
|
||||||
|
|
||||||
// Add an empty item
|
// Add an empty item
|
||||||
currentItems.push('');
|
currentItems.push('');
|
||||||
@ -654,7 +678,7 @@ const removeListItem = (index) => {
|
|||||||
if (!listName) return;
|
if (!listName) return;
|
||||||
|
|
||||||
// Get current items
|
// Get current items
|
||||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])];
|
||||||
|
|
||||||
// Remove the item at the specified index
|
// Remove the item at the specified index
|
||||||
currentItems.splice(index, 1);
|
currentItems.splice(index, 1);
|
||||||
@ -723,7 +747,7 @@ const validateItem = (listName, index, value) => {
|
|||||||
const checkDuplicates = (listName, newValue, currentIndex) => {
|
const checkDuplicates = (listName, newValue, currentIndex) => {
|
||||||
if (props.component.props.allowDuplicates) return true;
|
if (props.component.props.allowDuplicates) return true;
|
||||||
|
|
||||||
const currentItems = formStore.previewFormData[listName] || [];
|
const currentItems = safeGetField(listName, formStore.previewFormData) || [];
|
||||||
const duplicateIndex = currentItems.findIndex((item, index) =>
|
const duplicateIndex = currentItems.findIndex((item, index) =>
|
||||||
index !== currentIndex && item.toLowerCase() === newValue.toLowerCase()
|
index !== currentIndex && item.toLowerCase() === newValue.toLowerCase()
|
||||||
);
|
);
|
||||||
@ -762,7 +786,7 @@ const getItemInputClasses = (listName, index, value) => {
|
|||||||
|
|
||||||
// Filter items based on search query
|
// Filter items based on search query
|
||||||
const getFilteredItems = (listName) => {
|
const getFilteredItems = (listName) => {
|
||||||
const items = formStore.previewFormData[listName] || [];
|
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||||
const query = searchQuery.value[listName];
|
const query = searchQuery.value[listName];
|
||||||
|
|
||||||
if (!query || !props.component.props.enableSearch) {
|
if (!query || !props.component.props.enableSearch) {
|
||||||
@ -795,13 +819,13 @@ const toggleItemSelection = (listName, index) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isAllSelected = (listName) => {
|
const isAllSelected = (listName) => {
|
||||||
const items = formStore.previewFormData[listName] || [];
|
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||||
const selected = selectedItems.value[listName] || [];
|
const selected = selectedItems.value[listName] || [];
|
||||||
return items.length > 0 && selected.length === items.length;
|
return items.length > 0 && selected.length === items.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelectAll = (listName) => {
|
const toggleSelectAll = (listName) => {
|
||||||
const items = formStore.previewFormData[listName] || [];
|
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||||
|
|
||||||
if (isAllSelected(listName)) {
|
if (isAllSelected(listName)) {
|
||||||
selectedItems.value[listName] = [];
|
selectedItems.value[listName] = [];
|
||||||
@ -812,7 +836,7 @@ const toggleSelectAll = (listName) => {
|
|||||||
|
|
||||||
const deleteSelectedItems = (listName) => {
|
const deleteSelectedItems = (listName) => {
|
||||||
if (!props.component.props.confirmDelete || confirm('Are you sure you want to delete the selected items?')) {
|
if (!props.component.props.confirmDelete || confirm('Are you sure you want to delete the selected items?')) {
|
||||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])];
|
||||||
const selected = selectedItems.value[listName] || [];
|
const selected = selectedItems.value[listName] || [];
|
||||||
|
|
||||||
// Sort indices in descending order to avoid index shifting issues
|
// Sort indices in descending order to avoid index shifting issues
|
||||||
@ -834,7 +858,7 @@ const deleteSelectedItems = (listName) => {
|
|||||||
|
|
||||||
// Import/Export functionality
|
// Import/Export functionality
|
||||||
const exportItems = (listName, format) => {
|
const exportItems = (listName, format) => {
|
||||||
const items = formStore.previewFormData[listName] || [];
|
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||||
let content = '';
|
let content = '';
|
||||||
let filename = `${listName}_items`;
|
let filename = `${listName}_items`;
|
||||||
let mimeType = 'text/plain';
|
let mimeType = 'text/plain';
|
||||||
@ -867,20 +891,17 @@ const exportItems = (listName, format) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showImportModal = (listName) => {
|
const showImportModal = (listName) => {
|
||||||
// This would open a modal for importing items
|
importInput = document.createElement('input');
|
||||||
// For now, let's use a simple file input
|
importInput.type = 'file';
|
||||||
const input = document.createElement('input');
|
importInput.accept = '.txt,.csv,.json';
|
||||||
input.type = 'file';
|
importInput.onchange = (event) => {
|
||||||
input.accept = '.txt,.csv,.json';
|
|
||||||
input.onchange = (event) => {
|
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const content = e.target.result;
|
|
||||||
let importedItems = [];
|
let importedItems = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const content = e.target.result;
|
||||||
if (file.name.endsWith('.json')) {
|
if (file.name.endsWith('.json')) {
|
||||||
importedItems = JSON.parse(content);
|
importedItems = JSON.parse(content);
|
||||||
} else if (file.name.endsWith('.csv')) {
|
} else if (file.name.endsWith('.csv')) {
|
||||||
@ -888,22 +909,15 @@ const showImportModal = (listName) => {
|
|||||||
} else {
|
} else {
|
||||||
importedItems = content.split('\n').filter(Boolean);
|
importedItems = content.split('\n').filter(Boolean);
|
||||||
}
|
}
|
||||||
|
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])];
|
||||||
// Add imported items to current list
|
|
||||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
|
||||||
const newItems = [...currentItems, ...importedItems];
|
const newItems = [...currentItems, ...importedItems];
|
||||||
|
|
||||||
// Respect max items limit
|
|
||||||
const maxItems = props.component.props.maxItems || 20;
|
const maxItems = props.component.props.maxItems || 20;
|
||||||
if (newItems.length > maxItems) {
|
if (newItems.length > maxItems) {
|
||||||
newItems.splice(maxItems);
|
newItems.splice(maxItems);
|
||||||
alert(`Import limited to ${maxItems} items total.`);
|
alert(`Import limited to ${maxItems} items total.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update form data
|
|
||||||
const updatedData = { ...formStore.previewFormData, [listName]: newItems };
|
const updatedData = { ...formStore.previewFormData, [listName]: newItems };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error importing file. Please check the file format.');
|
alert('Error importing file. Please check the file format.');
|
||||||
}
|
}
|
||||||
@ -911,7 +925,7 @@ const showImportModal = (listName) => {
|
|||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
input.click();
|
importInput.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle button click
|
// Handle button click
|
||||||
@ -971,8 +985,11 @@ const handleImageClick = (event) => {
|
|||||||
const imageUrl = props.component.props.imageUrl;
|
const imageUrl = props.component.props.imageUrl;
|
||||||
if (!imageUrl) return;
|
if (!imageUrl) return;
|
||||||
|
|
||||||
// Create a lightbox effect to view the image
|
// Remove previous lightbox if exists
|
||||||
const lightbox = document.createElement('div');
|
if (lightbox && document.body.contains(lightbox)) {
|
||||||
|
document.body.removeChild(lightbox);
|
||||||
|
}
|
||||||
|
lightbox = document.createElement('div');
|
||||||
lightbox.style.position = 'fixed';
|
lightbox.style.position = 'fixed';
|
||||||
lightbox.style.top = '0';
|
lightbox.style.top = '0';
|
||||||
lightbox.style.left = '0';
|
lightbox.style.left = '0';
|
||||||
@ -994,7 +1011,10 @@ const handleImageClick = (event) => {
|
|||||||
|
|
||||||
// Add close functionality
|
// Add close functionality
|
||||||
lightbox.onclick = () => {
|
lightbox.onclick = () => {
|
||||||
document.body.removeChild(lightbox);
|
if (document.body.contains(lightbox)) {
|
||||||
|
document.body.removeChild(lightbox);
|
||||||
|
}
|
||||||
|
lightbox = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent image click from closing the lightbox
|
// Prevent image click from closing the lightbox
|
||||||
|
@ -217,6 +217,16 @@
|
|||||||
placeholder="e.g., We'll use this to send you updates about your order"
|
placeholder="e.g., We'll use this to send you updates about your order"
|
||||||
rows="2"
|
rows="2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
v-if="showField('readonly')"
|
||||||
|
type="switch"
|
||||||
|
label="Read Only"
|
||||||
|
name="readonly"
|
||||||
|
v-model="configModel.readonly"
|
||||||
|
help="Users cannot edit this field value"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1735,7 +1745,8 @@ const showField = (fieldName) => {
|
|||||||
value: ['heading', 'paragraph', 'hidden'],
|
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'],
|
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'],
|
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
|
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
||||||
@ -1779,7 +1790,7 @@ const isTextBasedField = computed(() => {
|
|||||||
// Type changing functionality
|
// Type changing functionality
|
||||||
const compatibilityGroups = {
|
const compatibilityGroups = {
|
||||||
// Text-based inputs (can switch between each other)
|
// 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)
|
// Selection inputs (can switch between each other)
|
||||||
selectionInputs: ['select', 'radio', 'checkbox'],
|
selectionInputs: ['select', 'radio', 'checkbox'],
|
||||||
|
@ -292,10 +292,13 @@ const handleFieldChange = (fieldName, newValue, oldValue) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Watch for script changes and reinitialize
|
// Watch for script changes and reinitialize
|
||||||
watch(() => props.customScript, (newScript, oldScript) => {
|
watch(() => props.customScript, async (newScript, oldScript) => {
|
||||||
if (newScript !== oldScript) {
|
if (newScript !== oldScript) {
|
||||||
isScriptInitialized.value = false;
|
isScriptInitialized.value = false;
|
||||||
initializeScript();
|
await nextTick();
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeScript();
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -325,10 +328,11 @@ watch(() => props.customCSS, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Lifecycle hooks
|
// Lifecycle hooks
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initializeScript();
|
initializeScript();
|
||||||
}, 500); // Delay to ensure parent DOM and initial props are settled
|
}, 200); // Delay to ensure all FormKit fields are loaded
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
11
composables/safeGetField.js
Normal file
11
composables/safeGetField.js
Normal file
@ -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 '';
|
||||||
|
}
|
@ -1,232 +1,273 @@
|
|||||||
// Manager Approval Form Custom Script Engine
|
// Manager Approval Form Custom Script Engine
|
||||||
// This script provides dynamic behavior for the manager approval form
|
// 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
|
// Auto-set approval date to today
|
||||||
const setApprovalDate = () => {
|
const setApprovalDate = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const formattedDate = today.toISOString().split('T')[0]; // YYYY-MM-DD format
|
const formattedDate = today.toISOString().split("T")[0]; // YYYY-MM-DD format
|
||||||
setField('approval_date', formattedDate);
|
setField("approval_date", formattedDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate and display recommended approval amounts
|
// Calculate and display recommended approval amounts
|
||||||
const calculateRecommendedAmounts = () => {
|
const calculateRecommendedAmounts = () => {
|
||||||
const totalClaimed = parseFloat(getField('total_cost_display')) || 0;
|
const totalClaimed = parseFloat(getField("total_cost_display")) || 0;
|
||||||
const policyLimit = parseFloat(getField('policy_limit_display')) || 0;
|
const policyLimit = parseFloat(getField("policy_limit_display")) || 0;
|
||||||
const overBudget = parseFloat(getField('over_budget_amount_display')) || 0;
|
const overBudget = parseFloat(getField("over_budget_amount_display")) || 0;
|
||||||
|
|
||||||
if (totalClaimed > 0 && policyLimit > 0) {
|
if (totalClaimed > 0 && policyLimit > 0) {
|
||||||
const percentageOver = ((overBudget / policyLimit) * 100).toFixed(1);
|
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
|
// Validate manager decision and provide guidance
|
||||||
const handleDecisionChange = (decision) => {
|
const handleDecisionChange = (decision) => {
|
||||||
const totalClaimed = parseFloat(getField('total_cost_display')) || 0;
|
const totalClaimed = parseFloat(getField("total_cost_display")) || 0;
|
||||||
const policyLimit = parseFloat(getField('policy_limit_display')) || 0;
|
const policyLimit = parseFloat(getField("policy_limit_display")) || 0;
|
||||||
|
|
||||||
console.log('Manager decision changed to:', decision);
|
console.log("Manager decision changed to:", decision);
|
||||||
|
|
||||||
switch(decision) {
|
switch (decision) {
|
||||||
case 'approve_full':
|
case "approve_full":
|
||||||
showField('custom_approved_amount');
|
showField("custom_approved_amount");
|
||||||
showInfo(`💰 Approving full amount: RM${totalClaimed.toFixed(2)}. You can enter a custom amount if needed.`);
|
showInfo(
|
||||||
setField('custom_approved_amount', totalClaimed.toString());
|
`💰 Approving full amount: RM${totalClaimed.toFixed(2)}. You can enter a custom amount if needed.`
|
||||||
|
);
|
||||||
|
setField("custom_approved_amount", totalClaimed.toString());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'approve_policy':
|
case "approve_policy":
|
||||||
hideField('custom_approved_amount');
|
hideField("custom_approved_amount");
|
||||||
showInfo(`⚖️ Approving policy limit only: RM${policyLimit.toFixed(2)}. Employee will be notified of the reduced amount.`);
|
showInfo(
|
||||||
|
`⚖️ Approving policy limit only: RM${policyLimit.toFixed(2)}. Employee will be notified of the reduced amount.`
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'reject':
|
case "reject":
|
||||||
hideField('custom_approved_amount');
|
hideField("custom_approved_amount");
|
||||||
showError('❌ Claim will be rejected. Please provide detailed comments explaining the rejection reason.');
|
showError(
|
||||||
|
"❌ Claim will be rejected. Please provide detailed comments explaining the rejection reason."
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
hideField('custom_approved_amount');
|
hideField("custom_approved_amount");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate custom approved amount
|
// Validate custom approved amount
|
||||||
const validateCustomAmount = (amount) => {
|
const validateCustomAmount = (amount) => {
|
||||||
const numAmount = parseFloat(amount) || 0;
|
const numAmount = parseFloat(amount) || 0;
|
||||||
const totalClaimed = parseFloat(getField('total_cost_display')) || 0;
|
const totalClaimed = parseFloat(getField("total_cost_display")) || 0;
|
||||||
const policyLimit = parseFloat(getField('policy_limit_display')) || 0;
|
const policyLimit = parseFloat(getField("policy_limit_display")) || 0;
|
||||||
|
|
||||||
if (numAmount < 0) {
|
if (numAmount < 0) {
|
||||||
showError('Approved amount cannot be negative');
|
showError("Approved amount cannot be negative");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (numAmount > totalClaimed) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (numAmount > 0 && numAmount < policyLimit) {
|
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;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate manager comments based on decision
|
// Validate manager comments based on decision
|
||||||
const validateComments = (comments, decision) => {
|
const validateComments = (comments, decision) => {
|
||||||
if (!comments || comments.trim().length < 10) {
|
if (!comments || comments.trim().length < 10) {
|
||||||
showError('Please provide detailed comments (minimum 10 characters)');
|
showError("Please provide detailed comments (minimum 10 characters)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decision === 'reject' && comments.length < 50) {
|
if (decision === "reject" && comments.length < 50) {
|
||||||
showError('Rejection requires detailed explanation (minimum 50 characters)');
|
showError(
|
||||||
|
"Rejection requires detailed explanation (minimum 50 characters)"
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decision === 'approve_full') {
|
if (decision === "approve_full") {
|
||||||
const totalClaimed = parseFloat(getField('total_cost_display')) || 0;
|
const totalClaimed = parseFloat(getField("total_cost_display")) || 0;
|
||||||
const policyLimit = parseFloat(getField('policy_limit_display')) || 0;
|
const policyLimit = parseFloat(getField("policy_limit_display")) || 0;
|
||||||
const overBudget = totalClaimed - policyLimit;
|
const overBudget = totalClaimed - policyLimit;
|
||||||
|
|
||||||
if (overBudget > 500 && !comments.toLowerCase().includes('business')) {
|
if (overBudget > 500 && !comments.toLowerCase().includes("business")) {
|
||||||
showInfo('Consider mentioning business justification for approving over-budget amounts > RM500');
|
showInfo(
|
||||||
|
"Consider mentioning business justification for approving over-budget amounts > RM500"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format manager name (title case)
|
// Format manager name (title case)
|
||||||
const formatManagerName = (name) => {
|
const formatManagerName = (name) => {
|
||||||
if (!name || typeof name !== 'string') return name;
|
if (!name || typeof name !== "string") return name;
|
||||||
|
|
||||||
return name
|
return name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(' ')
|
.split(" ")
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ');
|
.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Form validation before submission
|
// Form validation before submission
|
||||||
const validateApprovalForm = () => {
|
const validateApprovalForm = () => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
// Check required fields
|
// Check required fields
|
||||||
const requiredFields = {
|
const requiredFields = {
|
||||||
'manager_decision': 'Approval Decision',
|
manager_decision: "Approval Decision",
|
||||||
'manager_comments': 'Manager Comments',
|
manager_comments: "Manager Comments",
|
||||||
'manager_name': 'Manager Name',
|
manager_name: "Manager Name",
|
||||||
'approval_date': 'Approval Date'
|
approval_date: "Approval Date",
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(requiredFields).forEach(([fieldName, displayName]) => {
|
Object.entries(requiredFields).forEach(([fieldName, displayName]) => {
|
||||||
const value = getField(fieldName);
|
const value = getField(fieldName);
|
||||||
if (!value || value.toString().trim() === '') {
|
if (!value || value.toString().trim() === "") {
|
||||||
errors.push(`${displayName} is required`);
|
errors.push(`${displayName} is required`);
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate decision-specific requirements
|
// Validate decision-specific requirements
|
||||||
const decision = getField('manager_decision');
|
const decision = getField("manager_decision");
|
||||||
const comments = getField('manager_comments');
|
const comments = getField("manager_comments");
|
||||||
const customAmount = getField('custom_approved_amount');
|
const customAmount = getField("custom_approved_amount");
|
||||||
|
|
||||||
if (decision && comments) {
|
if (decision && comments) {
|
||||||
if (!validateComments(comments, decision)) {
|
if (!validateComments(comments, decision)) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decision === 'approve_full' && customAmount) {
|
if (decision === "approve_full" && customAmount) {
|
||||||
if (!validateCustomAmount(customAmount)) {
|
if (!validateCustomAmount(customAmount)) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate approval date is not in the future
|
// Validate approval date is not in the future
|
||||||
const approvalDate = getField('approval_date');
|
const approvalDate = getField("approval_date");
|
||||||
if (approvalDate) {
|
if (approvalDate) {
|
||||||
const selectedDate = new Date(approvalDate);
|
const selectedDate = new Date(approvalDate);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(23, 59, 59, 999); // End of today
|
today.setHours(23, 59, 59, 999); // End of today
|
||||||
|
|
||||||
if (selectedDate > today) {
|
if (selectedDate > today) {
|
||||||
errors.push('Approval date cannot be in the future');
|
errors.push("Approval date cannot be in the future");
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show validation results
|
// Show validation results
|
||||||
if (errors.length > 0) {
|
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 {
|
} else {
|
||||||
showSuccess('Form validation passed! Ready to submit approval decision.');
|
showSuccess("Form validation passed! Ready to submit approval decision.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up field change handlers
|
// Set up field change handlers
|
||||||
onFieldChange('manager_decision', (newValue) => {
|
onFieldChange("manager_decision", (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
handleDecisionChange(newValue);
|
handleDecisionChange(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onFieldChange('custom_approved_amount', (newValue) => {
|
onFieldChange("custom_approved_amount", (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
validateCustomAmount(newValue);
|
validateCustomAmount(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onFieldChange('manager_comments', (newValue) => {
|
onFieldChange("manager_comments", (newValue) => {
|
||||||
const decision = getField('manager_decision');
|
const decision = getField("manager_decision");
|
||||||
if (newValue && decision) {
|
if (newValue && decision) {
|
||||||
validateComments(newValue, decision);
|
validateComments(newValue, decision);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onFieldChange('manager_name', (newValue) => {
|
onFieldChange("manager_name", (newValue) => {
|
||||||
if (newValue && typeof newValue === 'string') {
|
if (newValue && typeof newValue === "string") {
|
||||||
const formatted = formatManagerName(newValue);
|
const formatted = formatManagerName(newValue);
|
||||||
if (formatted !== newValue) {
|
if (formatted !== newValue) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setField('manager_name', formatted);
|
setField("manager_name", formatted);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize form on load
|
// Wait until all required fields are present before running initialization
|
||||||
setTimeout(() => {
|
function waitForFields(fields, callback, maxAttempts = 20) {
|
||||||
setApprovalDate();
|
let attempts = 0;
|
||||||
calculateRecommendedAmounts();
|
function check() {
|
||||||
|
const allPresent = fields.every(f => getField(f) !== undefined);
|
||||||
// Make display fields read-only by adding visual styling
|
if (allPresent) {
|
||||||
const displayFields = [
|
callback();
|
||||||
'claim_summary', 'employee_name_display', 'department_display',
|
} else if (attempts < maxAttempts) {
|
||||||
'trip_purpose_display', 'destination_display', 'total_cost_display',
|
attempts++;
|
||||||
'policy_limit_display', 'over_budget_amount_display'
|
setTimeout(check, 100);
|
||||||
];
|
} else {
|
||||||
|
showError("Some required fields are missing. Please reload the page.");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
check();
|
||||||
showInfo('Review the claim details above and make your approval decision below.');
|
}
|
||||||
}, 1000);
|
|
||||||
|
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
|
// Expose validation function for form submission
|
||||||
window.validateManagerApprovalForm = validateApprovalForm;
|
window.validateManagerApprovalForm = validateApprovalForm;
|
||||||
|
|
||||||
console.log('Manager Approval Form Script initialized successfully');
|
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");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
@ -158,60 +158,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Total amount claimed by employee",
|
"help": "Total amount claimed by employee",
|
||||||
"name": "total_cost_display",
|
"name": "total_cost_display",
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"label": "Total Claimed Amount (RM)",
|
"label": "Total Claimed Amount (RM)",
|
||||||
"width": "33.33%",
|
"width": "33.33%",
|
||||||
"gridColumn": "span 4",
|
"gridColumn": "span 4",
|
||||||
"validation": "",
|
"validation": "",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Maximum allowed per company policy",
|
"help": "Maximum allowed per company policy",
|
||||||
"name": "policy_limit_display",
|
"name": "policy_limit_display",
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"label": "Policy Limit (RM)",
|
"label": "Policy Limit (RM)",
|
||||||
"width": "33.33%",
|
"width": "33.33%",
|
||||||
"gridColumn": "span 4",
|
"gridColumn": "span 4",
|
||||||
"validation": "",
|
"validation": "",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Amount exceeding policy limits",
|
"help": "Amount exceeding policy limits",
|
||||||
"name": "over_budget_amount_display",
|
"name": "over_budget_amount_display",
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"label": "Over Budget Amount (RM)",
|
"label": "Over Budget Amount (RM)",
|
||||||
"width": "33.33%",
|
"width": "33.33%",
|
||||||
"gridColumn": "span 4",
|
"gridColumn": "span 4",
|
||||||
"validation": "",
|
"validation": "",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -254,9 +236,18 @@
|
|||||||
"label": "Approval Decision",
|
"label": "Approval Decision",
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
"options": [
|
"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": "✅ Approve Full Amount - Employee will receive the full claimed amount",
|
||||||
{ "label": "❌ Reject Claim - Claim will be denied entirely", "value": "reject" }
|
"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",
|
"gridColumn": "span 12",
|
||||||
"validation": "required",
|
"validation": "required",
|
||||||
@ -286,8 +277,8 @@
|
|||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"field": "manager_decision",
|
"field": "manager_decision",
|
||||||
"operator": "eq",
|
"value": "approve_full",
|
||||||
"value": "approve_full"
|
"operator": "eq"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -337,10 +328,19 @@
|
|||||||
"label": "Additional Requirements",
|
"label": "Additional Requirements",
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
"options": [
|
"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": "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",
|
"gridColumn": "span 12",
|
||||||
"validation": "",
|
"validation": "",
|
||||||
@ -405,4 +405,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -234,7 +234,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Enter transportation costs (flights, trains, taxis, etc.)",
|
"help": "Enter transportation costs (flights, trains, taxis, etc.)",
|
||||||
"name": "transport_cost",
|
"name": "transport_cost",
|
||||||
@ -243,17 +243,11 @@
|
|||||||
"width": "50%",
|
"width": "50%",
|
||||||
"gridColumn": "span 6",
|
"gridColumn": "span 6",
|
||||||
"validation": "required|min:0",
|
"validation": "required|min:0",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Enter accommodation costs (hotels, lodging, etc.)",
|
"help": "Enter accommodation costs (hotels, lodging, etc.)",
|
||||||
"name": "accommodation_cost",
|
"name": "accommodation_cost",
|
||||||
@ -262,17 +256,11 @@
|
|||||||
"width": "50%",
|
"width": "50%",
|
||||||
"gridColumn": "span 6",
|
"gridColumn": "span 6",
|
||||||
"validation": "required|min:0",
|
"validation": "required|min:0",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Enter meal and food expenses",
|
"help": "Enter meal and food expenses",
|
||||||
"name": "meals_cost",
|
"name": "meals_cost",
|
||||||
@ -281,17 +269,11 @@
|
|||||||
"width": "50%",
|
"width": "50%",
|
||||||
"gridColumn": "span 6",
|
"gridColumn": "span 6",
|
||||||
"validation": "required|min:0",
|
"validation": "required|min:0",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
"help": "Enter other miscellaneous expenses",
|
"help": "Enter other miscellaneous expenses",
|
||||||
"name": "other_cost",
|
"name": "other_cost",
|
||||||
@ -300,33 +282,20 @@
|
|||||||
"width": "50%",
|
"width": "50%",
|
||||||
"gridColumn": "span 6",
|
"gridColumn": "span 6",
|
||||||
"validation": "required|min:0",
|
"validation": "required|min:0",
|
||||||
"placeholder": "0.00",
|
"placeholder": "0.00"
|
||||||
"conditionalLogic": {
|
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"props": {
|
"props": {
|
||||||
|
"help": "This will be automatically calculated from the above costs",
|
||||||
"name": "total_cost_display",
|
"name": "total_cost_display",
|
||||||
"type": "text",
|
"type": "number",
|
||||||
"label": "Total Claim Amount (RM)",
|
"label": "Total Claim Amount (RM)",
|
||||||
"value": "0.00",
|
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
"gridColumn": "span 12",
|
"gridColumn": "span 12",
|
||||||
"help": "This will be automatically calculated from the above costs",
|
|
||||||
"placeholder": "0.00",
|
|
||||||
"validation": "",
|
"validation": "",
|
||||||
"conditionalLogic": {
|
"placeholder": "0.00"
|
||||||
"action": "show",
|
|
||||||
"enabled": false,
|
|
||||||
"operator": "and",
|
|
||||||
"conditions": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -381,4 +350,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -377,9 +377,9 @@
|
|||||||
"assignedUsers": [],
|
"assignedUsers": [],
|
||||||
"inputMappings": [
|
"inputMappings": [
|
||||||
{ "processVariable": "claimSummary", "formField": "claim_summary" },
|
{ "processVariable": "claimSummary", "formField": "claim_summary" },
|
||||||
{ "processVariable": "totalCost", "formField": "total_cost" },
|
{ "processVariable": "totalCost", "formField": "total_cost_display" },
|
||||||
{ "processVariable": "reimbursementAmount", "formField": "reimbursement_amount" },
|
{ "processVariable": "overBudgetAmount", "formField": "over_budget_amount_display" },
|
||||||
{ "processVariable": "overBudgetAmount", "formField": "over_budget_amount" }
|
{ "processVariable": "totalAllowed", "formField": "policy_limit_display" }
|
||||||
],
|
],
|
||||||
"assignmentType": "roles",
|
"assignmentType": "roles",
|
||||||
"outputMappings": [
|
"outputMappings": [
|
||||||
|
@ -462,12 +462,24 @@ function substituteVariables(str, variables) {
|
|||||||
function applyInputMappings(nodeData, processVars, formData) {
|
function applyInputMappings(nodeData, processVars, formData) {
|
||||||
const { inputMappings = [] } = nodeData;
|
const { inputMappings = [] } = nodeData;
|
||||||
console.log('[Workflow] Applying input mappings:', inputMappings);
|
console.log('[Workflow] Applying input mappings:', inputMappings);
|
||||||
|
|
||||||
inputMappings.forEach(mapping => {
|
inputMappings.forEach(mapping => {
|
||||||
const { processVariable, formField } = mapping;
|
const { processVariable, formField } = mapping;
|
||||||
if (processVars[processVariable] !== undefined) {
|
if (processVars[processVariable] !== undefined) {
|
||||||
formData[formField] = processVars[processVariable];
|
let value = processVars[processVariable];
|
||||||
console.log(`[Workflow] Mapped ${processVariable} -> ${formField}:`, 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
|
// 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) {
|
if (currentForm.value?.formComponents) {
|
||||||
formStore.formComponents = currentForm.value.formComponents;
|
formStore.formComponents = currentForm.value.formComponents;
|
||||||
formStore.updatePreviewFormData(formData.value);
|
formStore.updatePreviewFormData(formData.value);
|
||||||
@ -1330,6 +1345,7 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
:form-components="currentForm.formComponents"
|
:form-components="currentForm.formComponents"
|
||||||
:form-data="formData"
|
:form-data="formData"
|
||||||
@script-generated="handleConditionalLogicGenerated"
|
@script-generated="handleConditionalLogicGenerated"
|
||||||
|
:key="currentNode.value?.id"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Form Script Engine for conditional logic + custom script -->
|
<!-- Form Script Engine for conditional logic + custom script -->
|
||||||
@ -1341,6 +1357,7 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
:form-events="currentForm.formEvents || { onLoad: true, onFieldChange: true }"
|
:form-events="currentForm.formEvents || { onLoad: true, onFieldChange: true }"
|
||||||
:script-mode="currentForm.scriptMode || 'safe'"
|
:script-mode="currentForm.scriptMode || 'safe'"
|
||||||
@field-change="handleScriptFieldChange"
|
@field-change="handleScriptFieldChange"
|
||||||
|
:key="currentNode.value?.id"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- FormKit form wrapper -->
|
<!-- FormKit form wrapper -->
|
||||||
@ -1352,10 +1369,11 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
:actions="false"
|
:actions="false"
|
||||||
:incomplete-message="false"
|
:incomplete-message="false"
|
||||||
validation-visibility="submit"
|
validation-visibility="submit"
|
||||||
|
:key="currentNode.value?.id"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-12 gap-2">
|
<div class="grid grid-cols-12 gap-2">
|
||||||
<template v-for="(component, index) in currentForm.formComponents" :key="index">
|
<template v-for="(component, index) in currentForm.formComponents" :key="index">
|
||||||
<ComponentPreview
|
<ComponentPreview
|
||||||
:component="component"
|
:component="component"
|
||||||
:is-preview="false"
|
:is-preview="false"
|
||||||
:field-states="fieldStates"
|
:field-states="fieldStates"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user