2025-06-16 00:52:49 +08:00

360 lines
12 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-4">{{ form.description || 'Please complete this form step' }}</p>
<!-- 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 } from 'vue'
import { useRoute } from 'vue-router'
import RsButton from '~/components/RsButton.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'
)
}
// 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 empty objects for each form
formData.value = forms.value.map(() => ({}))
}
} 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: 1rem;
}
.grid-preview-container > div {
grid-column: span 12;
}
/* 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>