553 lines
19 KiB
Vue
553 lines
19 KiB
Vue
<template>
|
|
<div class="container mx-auto px-4 py-6">
|
|
<p class="text-lg font-bold mb-4">{{ caseInstance.caseName }}</p>
|
|
|
|
<div v-if="loading" class="text-blue-500">Loading...</div>
|
|
<div v-if="error" class="text-red-500">{{ error }}</div>
|
|
|
|
<div v-if="forms.length > 0">
|
|
<!-- Tab Navigation -->
|
|
<div class="border-b border-gray-200 mb-6">
|
|
<nav class="flex -mb-px">
|
|
<button
|
|
v-for="(form, index) in forms"
|
|
:key="index"
|
|
@click="activeTabIndex = index"
|
|
class="py-4 px-6 font-medium text-sm border-b-2 whitespace-nowrap"
|
|
:class="[
|
|
activeTabIndex === index
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
]"
|
|
>
|
|
<div class="flex items-center">
|
|
<div class="w-6 h-6 rounded-full bg-gray-200 flex items-center justify-center mr-2"
|
|
:class="{ 'bg-primary text-white': activeTabIndex === index }">
|
|
{{ index + 1 }}
|
|
</div>
|
|
{{ form.formName || `Form ${index + 1}` }}
|
|
</div>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<div v-for="(form, index) in forms" :key="`content-${index}`" v-show="activeTabIndex === index">
|
|
<h2 class="text-xl font-semibold mb-4">{{ form.formName || `Form ${index + 1}` }}</h2>
|
|
<p class="text-gray-600 mb-1">{{ form.description || 'Please complete this form step' }}</p>
|
|
|
|
<!-- Conditional Logic Engine -->
|
|
<ConditionalLogicEngine
|
|
:form-components="form.formComponents"
|
|
:form-data="formData[index]"
|
|
@script-generated="handleScriptGenerated"
|
|
/>
|
|
|
|
<!-- Form content -->
|
|
<FormKit
|
|
type="form"
|
|
:id="`form-${form.formID}`"
|
|
v-model="formData[index]"
|
|
@submit="handleSubmit(index)"
|
|
:actions="false"
|
|
:incomplete-message="false"
|
|
validation-visibility="submit"
|
|
>
|
|
<div class="grid-preview-container">
|
|
<template v-if="form.formComponents && form.formComponents.length > 0">
|
|
<div
|
|
v-for="(component, compIndex) in form.formComponents"
|
|
:key="`component-${compIndex}`"
|
|
:style="{
|
|
gridColumn: component.props?.gridColumn || 'span 12'
|
|
}"
|
|
>
|
|
<!-- Standard FormKit inputs -->
|
|
<FormKit
|
|
v-if="isStandardInput(component.type)"
|
|
:type="component.type"
|
|
:name="component.props?.name"
|
|
:label="component.props?.label"
|
|
:help="component.props?.help"
|
|
:placeholder="component.props?.placeholder"
|
|
:validation="component.props?.validation"
|
|
:options="component.props?.options"
|
|
:value="component.props?.value"
|
|
:class="component.props?.width ? `w-${component.props.width}` : 'w-full'"
|
|
class="mb-4"
|
|
/>
|
|
|
|
<!-- Heading -->
|
|
<div v-else-if="component.type === 'heading'" class="py-2 mb-4">
|
|
<component
|
|
:is="`h${component.props?.level || 2}`"
|
|
class="font-semibold"
|
|
:class="{
|
|
'text-2xl': component.props?.level === 2,
|
|
'text-xl': component.props?.level === 3,
|
|
'text-lg': component.props?.level === 4
|
|
}"
|
|
>
|
|
{{ component.props?.value || 'Heading Text' }}
|
|
</component>
|
|
</div>
|
|
|
|
<!-- Paragraph -->
|
|
<div v-else-if="component.type === 'paragraph'" class="py-2 mb-4">
|
|
<p class="text-gray-600">{{ component.props?.value || 'Paragraph text goes here' }}</p>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div v-else-if="component.type === 'divider'" class="py-2 mb-4">
|
|
<hr class="border-t border-gray-200">
|
|
</div>
|
|
|
|
<!-- Info Display -->
|
|
<div v-else-if="component.type === 'info-display'" class="mb-4">
|
|
<div
|
|
class="p-4 rounded-lg"
|
|
:class="{ 'border': component.props?.showBorder }"
|
|
:style="{ backgroundColor: component.props?.backgroundColor || '#f8fafc' }"
|
|
>
|
|
<h3 class="font-medium mb-2">{{ component.props?.title || 'Information' }}</h3>
|
|
<div
|
|
:class="{
|
|
'grid grid-cols-2 gap-4': component.props?.layout === 'grid',
|
|
'flex flex-col space-y-2': component.props?.layout === 'vertical' || !component.props?.layout,
|
|
'flex flex-row flex-wrap gap-4': component.props?.layout === 'horizontal'
|
|
}"
|
|
>
|
|
<div
|
|
v-for="(field, fieldIndex) in component.props?.fields"
|
|
:key="`field-${fieldIndex}`"
|
|
class="text-sm"
|
|
>
|
|
<span class="text-gray-600">{{ field.label }}:</span>
|
|
<span class="ml-2 font-medium">{{ field.value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Button -->
|
|
<div v-else-if="component.type === 'button'" class="py-2 mb-4">
|
|
<RsButton
|
|
:type="component.props?.buttonType || 'button'"
|
|
:variant="component.props?.variant || 'primary'"
|
|
:size="component.props?.size || 'md'"
|
|
:disabled="component.props?.disabled || false"
|
|
>
|
|
{{ component.props?.label || 'Button' }}
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div v-else class="text-center py-8 text-gray-500">
|
|
No form components found.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit button if not already included in the form -->
|
|
<!-- <FormKit
|
|
v-if="!hasSubmitButton(form)"
|
|
type="submit"
|
|
label="Submit"
|
|
:disabled="submitting"
|
|
:classes="{
|
|
input: submitting ? 'opacity-75 cursor-wait' : ''
|
|
}"
|
|
class="mt-6"
|
|
/> -->
|
|
<div v-if="submitting" class="text-center mt-2 text-sm text-blue-500">
|
|
Submitting form...
|
|
</div>
|
|
</FormKit>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Buttons -->
|
|
<div class="flex justify-between mt-6">
|
|
<button
|
|
@click="prevStep"
|
|
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
|
:disabled="activeTabIndex === 0"
|
|
:class="{ 'opacity-50 cursor-not-allowed': activeTabIndex === 0 }"
|
|
>
|
|
Previous
|
|
</button>
|
|
<button
|
|
@click="nextStep"
|
|
class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark"
|
|
:disabled="activeTabIndex === forms.length - 1"
|
|
:class="{ 'opacity-50 cursor-not-allowed': activeTabIndex === forms.length - 1 }"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="!loading && !error">
|
|
<p class="text-gray-500">No forms available.</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, ref, computed, watch, nextTick } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import RsButton from '~/components/RsButton.vue'
|
|
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue'
|
|
|
|
const loading = ref(false)
|
|
const error = ref(null)
|
|
const route = useRoute()
|
|
const forms = ref([])
|
|
const caseInstance = ref([])
|
|
const activeTabIndex = ref(0)
|
|
const formData = ref([])
|
|
const submitting = ref(false)
|
|
|
|
// Computed property for current form
|
|
const currentForm = computed(() => {
|
|
return forms.value[activeTabIndex.value] || null
|
|
})
|
|
|
|
// Navigation methods
|
|
const nextStep = () => {
|
|
if (activeTabIndex.value < forms.value.length - 1) {
|
|
activeTabIndex.value++
|
|
}
|
|
}
|
|
|
|
const prevStep = () => {
|
|
if (activeTabIndex.value > 0) {
|
|
activeTabIndex.value--
|
|
}
|
|
}
|
|
|
|
// Check if component type is a standard FormKit input
|
|
const isStandardInput = (type) => {
|
|
const standardInputs = [
|
|
'text', 'email', 'password', 'number', 'tel', 'url',
|
|
'textarea', 'select', 'checkbox', 'radio', 'date',
|
|
'time', 'datetime-local', 'file', 'color', 'range',
|
|
'otp', 'mask', 'dropzone', 'switch'
|
|
]
|
|
return standardInputs.includes(type)
|
|
}
|
|
|
|
// Check if form has a submit button component
|
|
const hasSubmitButton = (form) => {
|
|
if (!form.formComponents) return false
|
|
return form.formComponents.some(comp =>
|
|
comp.type === 'button' &&
|
|
comp.props?.buttonType === 'submit'
|
|
)
|
|
}
|
|
|
|
// Utility functions for conditional logic
|
|
const getField = (fieldName) => {
|
|
const currentFormData = formData.value[activeTabIndex.value] || {}
|
|
return currentFormData[fieldName]
|
|
}
|
|
|
|
const showField = (fieldName) => {
|
|
const field = document.querySelector(`[name="${fieldName}"]`)
|
|
if (field) {
|
|
const outerDiv = field.closest('.formkit-outer')
|
|
if (outerDiv) {
|
|
outerDiv.style.display = ''
|
|
outerDiv.classList.remove('hidden')
|
|
}
|
|
}
|
|
}
|
|
|
|
const hideField = (fieldName) => {
|
|
const field = document.querySelector(`[name="${fieldName}"]`)
|
|
if (field) {
|
|
const outerDiv = field.closest('.formkit-outer')
|
|
if (outerDiv) {
|
|
outerDiv.style.display = 'none'
|
|
outerDiv.classList.add('hidden')
|
|
}
|
|
}
|
|
}
|
|
|
|
const enableField = (fieldName) => {
|
|
const field = document.querySelector(`[name="${fieldName}"]`)
|
|
if (field) {
|
|
field.removeAttribute('disabled')
|
|
const outerDiv = field.closest('.formkit-outer')
|
|
if (outerDiv) {
|
|
outerDiv.classList.remove('opacity-50', 'pointer-events-none')
|
|
}
|
|
}
|
|
}
|
|
|
|
const disableField = (fieldName) => {
|
|
const field = document.querySelector(`[name="${fieldName}"]`)
|
|
if (field) {
|
|
field.setAttribute('disabled', 'disabled')
|
|
const outerDiv = field.closest('.formkit-outer')
|
|
if (outerDiv) {
|
|
outerDiv.classList.add('opacity-50', 'pointer-events-none')
|
|
}
|
|
}
|
|
}
|
|
|
|
const onFieldChange = (fieldName, callback) => {
|
|
const field = document.querySelector(`[name="${fieldName}"]`)
|
|
if (field) {
|
|
// Remove existing listeners first to prevent duplicates
|
|
field.removeEventListener('change', callback)
|
|
field.removeEventListener('input', callback)
|
|
// Add new listeners
|
|
field.addEventListener('change', callback)
|
|
field.addEventListener('input', callback)
|
|
}
|
|
}
|
|
|
|
// Function to evaluate and apply conditional logic for all fields
|
|
const evaluateAllFieldConditions = () => {
|
|
const currentForm = forms.value[activeTabIndex.value]
|
|
if (!currentForm?.formComponents) return
|
|
|
|
currentForm.formComponents.forEach(component => {
|
|
if (component.props?.conditionalLogic?.enabled) {
|
|
const { conditions, action, operator = 'and' } = component.props.conditionalLogic
|
|
const fieldName = component.props.name
|
|
|
|
// Evaluate all conditions
|
|
const results = conditions.map(condition => {
|
|
const fieldValue = getField(condition.field)
|
|
|
|
switch (condition.operator) {
|
|
case 'equals':
|
|
return fieldValue === condition.value
|
|
case 'not_equals':
|
|
return fieldValue !== condition.value
|
|
case 'contains':
|
|
return String(fieldValue || '').includes(condition.value)
|
|
case 'not_contains':
|
|
return !String(fieldValue || '').includes(condition.value)
|
|
case 'is_empty':
|
|
return !fieldValue || fieldValue === ''
|
|
case 'is_not_empty':
|
|
return fieldValue && fieldValue !== ''
|
|
case 'greater_than':
|
|
return Number(fieldValue) > Number(condition.value)
|
|
case 'less_than':
|
|
return Number(fieldValue) < Number(condition.value)
|
|
default:
|
|
return false
|
|
}
|
|
})
|
|
|
|
// Check if conditions are met based on operator
|
|
const conditionsMet = operator === 'and'
|
|
? results.every(result => result)
|
|
: results.some(result => result)
|
|
|
|
// Apply visibility/state based on conditions
|
|
if (conditionsMet) {
|
|
if (action === 'show') showField(fieldName)
|
|
else if (action === 'hide') hideField(fieldName)
|
|
else if (action === 'enable') enableField(fieldName)
|
|
else if (action === 'disable') disableField(fieldName)
|
|
} else {
|
|
if (action === 'show') hideField(fieldName)
|
|
else if (action === 'hide') showField(fieldName)
|
|
else if (action === 'enable') disableField(fieldName)
|
|
else if (action === 'disable') enableField(fieldName)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Watch for form data changes to re-evaluate conditional logic
|
|
watch(() => formData.value[activeTabIndex.value], () => {
|
|
nextTick(() => {
|
|
evaluateAllFieldConditions()
|
|
})
|
|
}, { deep: true })
|
|
|
|
// Watch for active tab changes to re-evaluate conditional logic
|
|
watch(activeTabIndex, () => {
|
|
nextTick(() => {
|
|
evaluateAllFieldConditions()
|
|
})
|
|
})
|
|
|
|
// Watch for forms data to initialize conditional logic
|
|
watch(forms, (newForms) => {
|
|
if (newForms.length > 0) {
|
|
nextTick(() => {
|
|
evaluateAllFieldConditions()
|
|
})
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Handle script generated from ConditionalLogicEngine
|
|
const handleScriptGenerated = (script) => {
|
|
if (!script) return
|
|
|
|
try {
|
|
// Create a function with access to our utility functions
|
|
const evalScript = new Function(
|
|
'getField',
|
|
'showField',
|
|
'hideField',
|
|
'enableField',
|
|
'disableField',
|
|
'onFieldChange',
|
|
script
|
|
)
|
|
|
|
// Execute the script with our utility functions
|
|
evalScript(
|
|
getField,
|
|
showField,
|
|
hideField,
|
|
enableField,
|
|
disableField,
|
|
onFieldChange
|
|
)
|
|
} catch (err) {
|
|
console.error('Error executing conditional logic script:', err)
|
|
}
|
|
}
|
|
|
|
// Handle form submission
|
|
const handleSubmit = async (formIndex) => {
|
|
try {
|
|
submitting.value = true
|
|
console.log(`Form ${formIndex + 1} submitted:`, formData.value[formIndex])
|
|
|
|
// Example submission logic - replace with your actual API endpoint
|
|
const response = await $fetch(`/api/cases/${route.params.id}/forms/${forms.value[formIndex].formID}/submit`, {
|
|
method: 'POST',
|
|
body: {
|
|
formData: formData.value[formIndex]
|
|
}
|
|
})
|
|
|
|
if (response.success) {
|
|
// Move to next form if available
|
|
if (formIndex < forms.value.length - 1) {
|
|
activeTabIndex.value = formIndex + 1
|
|
}
|
|
} else {
|
|
throw new Error(response.error || 'Form submission failed')
|
|
}
|
|
} catch (err) {
|
|
console.error('Error submitting form:', err)
|
|
error.value = err.message || 'Failed to submit form'
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
// Methods
|
|
const fetchCaseInstance = async (caseId) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
// Fetch case instance and all related forms using the API endpoint
|
|
const response = await $fetch(`/api/cases/${caseId}/forms`);
|
|
if (!response.success) {
|
|
throw new Error(response.error || 'Failed to load case instance and forms');
|
|
} else {
|
|
caseInstance.value = response.caseInstance
|
|
forms.value = response.forms
|
|
console.log(response.forms)
|
|
|
|
// Initialize formData array with default values for each form
|
|
formData.value = forms.value.map(form => {
|
|
const defaultData = {};
|
|
|
|
// Set default values for form fields
|
|
form.formComponents?.forEach(component => {
|
|
// For select fields with options, set first option as default
|
|
if (component.type === 'select' && component.props.options?.length > 0) {
|
|
defaultData[component.props.name] = component.props.options[0].value;
|
|
}
|
|
// Add more default value logic for other field types if needed
|
|
});
|
|
|
|
return defaultData;
|
|
});
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Error fetching case instance and forms:', err);
|
|
error.value = err.message || 'Failed to load forms';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Lifecycle hooks
|
|
onMounted(() => {
|
|
const caseId = route.params.id;
|
|
if (caseId) {
|
|
fetchCaseInstance(caseId);
|
|
} else {
|
|
error.value = 'No case ID provided';
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.grid-preview-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(12, 1fr);
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.grid-preview-container > div {
|
|
grid-column: span 9;
|
|
}
|
|
|
|
/* Apply width classes */
|
|
.w-25 { width: 25%; }
|
|
.w-33 { width: 33.333%; }
|
|
.w-50 { width: 50%; }
|
|
.w-66 { width: 66.666%; }
|
|
.w-75 { width: 75%; }
|
|
.w-100 { width: 100%; }
|
|
|
|
/* Match form-builder styling */
|
|
:deep(.formkit-outer) {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
:deep(.formkit-label) {
|
|
font-weight: 500;
|
|
color: #374151;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
:deep(.formkit-input) {
|
|
width: 100%;
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 0.375rem;
|
|
background-color: #fff;
|
|
color: #1f2937;
|
|
}
|
|
|
|
:deep(.formkit-input:focus) {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 1px #3b82f6;
|
|
}
|
|
|
|
:deep(.formkit-help) {
|
|
font-size: 0.875rem;
|
|
color: #6b7280;
|
|
margin-top: 0.25rem;
|
|
}
|
|
</style> |