Files
policy-ui/app/pages/quotes/new.vue
2026-04-29 16:25:11 -05:00

701 lines
27 KiB
Vue

<script setup lang="ts">
import { z } from 'zod'
usePageTitle('New Quote')
const route = useRoute()
const router = useRouter()
const activeTab = computed(() => route.query.tab || 'car')
const tabs = [
{ id: 'car', label: 'Auto', icon: 'i-heroicons-truck', description: 'Motor, fleet & bind' },
{ id: 'life', label: 'Life', icon: 'i-heroicons-shield-check', description: 'Individual & corporate' },
{ id: 'fire_structure', label: 'Fire Structure', icon: 'i-heroicons-building-office-2', description: 'Building coverage' },
{ id: 'fire_contents', label: 'Fire Contents', icon: 'i-heroicons-building-office-2', description: 'Contents coverage' }
]
function setTab(tabId: string) {
router.push({ query: { tab: tabId } })
}
const autoForm = reactive({
customerSelection: {
selectedCustomer: null as any,
useSameForBuyer: true,
selectedBuyer: null as any
},
customerSearch: '',
vehicle: {
plate: '',
make: '',
model: '',
year: new Date().getFullYear(),
use_type: 'private',
car_type: 'sedan',
rc_limits: {
bodily_injury: 0,
property_damage: 0
},
market_value: 0,
requested_value: 0,
chassis_number: '',
engine_number: ''
},
providerSearch: '',
selectedProviders: [] as string[]
})
const lifeForm = reactive({
customerSelection: {
selectedCustomer: null as any,
useSameForBuyer: true,
selectedBuyer: null as any
},
customerSearch: '',
life: {
coverage_type: 'banking',
coverage_amount: 0,
coverage_years: 10,
smoker: false,
medications: [] as string[],
surgeries: [] as string[],
weight: 0,
height: 0
},
providerSearch: '',
selectedProviders: [] as string[]
})
const fireStructureForm = reactive({
customerSelection: {
selectedCustomer: null as any,
useSameForBuyer: true,
selectedBuyer: null as any
},
customerSearch: '',
property: {
location: '',
property_value: 0,
property_use: '',
security_measures: [] as string[],
market_value: 0
},
providerSearch: '',
selectedProviders: [] as string[]
})
const fireContentsForm = reactive({
customerSelection: {
selectedCustomer: null as any,
useSameForBuyer: true,
selectedBuyer: null as any
},
customerSearch: '',
contents: {
location: '',
contents_value: 0,
property_use: '',
security_measures: [] as string[],
high_value_items: [] as Array<{ description: string; value: number; type: string }>
},
providerSearch: '',
selectedProviders: [] as string[]
})
function getForm(tabId: string) {
switch (tabId) {
case 'car': return autoForm
case 'life': return lifeForm
case 'fire_structure': return fireStructureForm
case 'fire_contents': return fireContentsForm
default: return autoForm
}
}
const autoSchema = z.object({
vehicle: z.object({
plate: z.string().min(1, 'License plate is required'),
make: z.string().min(1, 'Make is required'),
model: z.string().min(1, 'Model is required'),
year: z.number().min(1900).max(new Date().getFullYear() + 1),
use_type: z.enum(['private', 'commercial', 'bus', 'taxi', 'school']),
car_type: z.enum(['sedan', 'suv', 'hatchback', 'coupe', 'convertible', 'pickup', 'van', 'minivan', 'truck']),
rc_limits: z.object({
bodily_injury: z.number().min(0),
property_damage: z.number().min(0)
}),
market_value: z.number().min(0),
requested_value: z.number().min(0),
chassis_number: z.string().optional(),
engine_number: z.string().optional()
})
})
const lifeSchema = z.object({
life: z.object({
coverage_type: z.enum(['banking', 'protection']),
coverage_amount: z.number().min(0),
coverage_years: z.number().min(1).max(100),
smoker: z.boolean(),
medications: z.array(z.string()).optional(),
surgeries: z.array(z.string()).optional(),
weight: z.number().min(0),
height: z.number().min(0)
})
})
const fireStructureSchema = z.object({
property: z.object({
location: z.string().min(1, 'Location is required'),
property_value: z.number().min(0),
property_use: z.string().min(1, 'Property use is required'),
security_measures: z.array(z.string()).optional(),
market_value: z.number().min(0)
})
})
const fireContentsSchema = z.object({
contents: z.object({
location: z.string().min(1, 'Location is required'),
contents_value: z.number().min(0),
property_use: z.string().min(1, 'Property use is required'),
security_measures: z.array(z.string()).optional(),
high_value_items: z.array(z.object({
description: z.string(),
value: z.number(),
type: z.string()
})).optional()
})
})
function getSchema(tabId: string) {
switch (tabId) {
case 'car': return autoSchema
case 'life': return lifeSchema
case 'fire_structure': return fireStructureSchema
case 'fire_contents': return fireContentsSchema
default: return autoSchema
}
}
const autoFormRef = ref()
const lifeFormRef = ref()
const fireStructureFormRef = ref()
const fireContentsFormRef = ref()
const isSubmitting = ref(false)
const submitError = ref('')
function getFormRef(tabId: string) {
switch (tabId) {
case 'car': return autoFormRef
case 'life': return lifeFormRef
case 'fire_structure': return fireStructureFormRef
case 'fire_contents': return fireContentsFormRef
default: return autoFormRef
}
}
async function submitQuote() {
submitError.value = ''
const formRef = getFormRef(activeTab.value)
if (formRef.value) {
try {
await formRef.value.validate()
} catch (error) {
submitError.value = 'Please fix the validation errors before submitting'
return
}
}
const form = getForm(activeTab.value)
if (!form.customerSelection.selectedCustomer) {
submitError.value = 'Please select a customer'
return
}
if (form.selectedProviders.length === 0) {
submitError.value = 'Please select at least one provider'
return
}
isSubmitting.value = true
try {
const payload = {
policy_type: activeTab.value,
insured: {
type: form.customerSelection.selectedCustomer.customer_type,
...(form.customerSelection.selectedCustomer.customer_type === 'individual' ? {
name: `${form.customerSelection.selectedCustomer.first_name} ${form.customerSelection.selectedCustomer.last_name}`,
date_of_birth: form.customerSelection.selectedCustomer.birth_date,
document_id: form.customerSelection.selectedCustomer.document_id,
gender: form.customerSelection.selectedCustomer.gender,
address: form.customerSelection.selectedCustomer.address,
phone: form.customerSelection.selectedCustomer.phone,
email: form.customerSelection.selectedCustomer.email
} : {
company_name: form.customerSelection.selectedCustomer.legal_name,
ruc: form.customerSelection.selectedCustomer.ruc,
legal_rep_name: form.customerSelection.selectedCustomer.legal_rep_name,
legal_rep_document: form.customerSelection.selectedCustomer.legal_rep_document_id
})
},
buyer: form.customerSelection.useSameForBuyer ? {
type: form.customerSelection.selectedCustomer.customer_type,
...(form.customerSelection.selectedCustomer.customer_type === 'individual' ? {
name: `${form.customerSelection.selectedCustomer.first_name} ${form.customerSelection.selectedCustomer.last_name}`,
date_of_birth: form.customerSelection.selectedCustomer.birth_date,
document_id: form.customerSelection.selectedCustomer.document_id,
gender: form.customerSelection.selectedCustomer.gender,
address: form.customerSelection.selectedCustomer.address,
phone: form.customerSelection.selectedCustomer.phone,
email: form.customerSelection.selectedCustomer.email
} : {
company_name: form.customerSelection.selectedCustomer.legal_name,
ruc: form.customerSelection.selectedCustomer.ruc,
legal_rep_name: form.customerSelection.selectedCustomer.legal_rep_name,
legal_rep_document: form.customerSelection.selectedCustomer.legal_rep_document_id
})
} : {
type: form.customerSelection.selectedBuyer.customer_type,
...(form.customerSelection.selectedBuyer.customer_type === 'individual' ? {
name: `${form.customerSelection.selectedBuyer.first_name} ${form.customerSelection.selectedBuyer.last_name}`,
date_of_birth: form.customerSelection.selectedBuyer.birth_date,
document_id: form.customerSelection.selectedCustomer.document_id,
gender: form.customerSelection.selectedCustomer.gender,
address: form.customerSelection.selectedCustomer.address,
phone: form.customerSelection.selectedCustomer.phone,
email: form.customerSelection.selectedCustomer.email
} : {
company_name: form.customerSelection.selectedBuyer.legal_name,
ruc: form.customerSelection.selectedBuyer.ruc,
legal_rep_name: form.customerSelection.selectedBuyer.legal_rep_name,
legal_rep_document: form.customerSelection.selectedBuyer.legal_rep_document_id
})
},
policy_details: getPolicyDetails(form),
selected_providers: form.selectedProviders.map(id => ({
provider_id: id,
email: ''
}))
}
const { data, error } = await usePolicy('/policies', {
method: 'POST',
body: payload
})
if (error.value) {
submitError.value = `Failed to create quote: ${error.value.message || 'Unknown error'}`
return
}
if (data.value) {
router.push(`/policies/${data.value.application_id}`)
}
} catch (error) {
submitError.value = `Failed to create quote: ${error instanceof Error ? error.message : 'Unknown error'}`
} finally {
isSubmitting.value = false
}
}
function getPolicyDetails(form: any) {
switch (activeTab.value) {
case 'car':
return {
plate: form.vehicle.plate,
make: form.vehicle.make,
model: form.vehicle.model,
year: form.vehicle.year,
use_type: form.vehicle.use_type,
car_type: form.vehicle.car_type,
chassis_number: form.vehicle.chassis_number,
engine_number: form.vehicle.engine_number,
rc_limits: form.vehicle.rc_limits,
market_value: form.vehicle.market_value,
requested_value: form.vehicle.requested_value
}
case 'life':
return {
coverage_type: form.life.coverage_type,
coverage_amount: form.life.coverage_amount,
coverage_years: form.life.coverage_years,
smoker: form.life.smoker,
medications: form.life.medications,
surgeries: form.life.surgeries,
weight: form.life.weight,
height: form.life.height
}
case 'fire_structure':
return {
location: form.property.location,
property_value: form.property.property_value,
property_use: form.property.property_use,
security_measures: form.property.security_measures,
market_value: form.property.market_value
}
case 'fire_contents':
return {
location: form.contents.location,
contents_value: form.contents.contents_value,
property_use: form.contents.property_use,
security_measures: form.contents.security_measures,
high_value_items: form.contents.high_value_items
}
default:
return {}
}
}
</script>
<template>
<div class="max-w-4xl mx-auto">
<div class="mb-8">
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 rounded-lg bg-[var(--brand)] flex items-center justify-center">
<UIcon :name="tabs.find(t => t.id === activeTab)?.icon" class="w-5 h-5 text-white" />
</div>
<div>
<h1 class="text-2xl font-semibold text-[var(--text-primary)]">New {{ tabs.find(t => t.id === activeTab)?.label || 'Insurance' }} Quote</h1>
<p class="text-[var(--text-muted)]">{{ tabs.find(t => t.id === activeTab)?.description || 'Create a new insurance quote' }}</p>
</div>
</div>
</div>
<UForm
v-if="activeTab === 'car'"
ref="autoFormRef"
:schema="autoSchema"
:state="autoForm"
@submit="submitQuote"
>
<UAccordion :items="[
{
label: 'Customer Selection',
icon: 'i-heroicons-user',
content: '',
value: 'customer',
defaultOpen: true
},
{
label: 'Vehicle Information',
icon: 'i-heroicons-truck',
content: '',
value: 'vehicle',
defaultOpen: true
},
{
label: 'Provider Selection',
icon: 'i-heroicons-building-office',
content: '',
value: 'provider',
defaultOpen: true
}
]">
<template #body="{ item }">
<div v-if="item.value === 'customer'" class="p-5">
<CustomerSelector
v-model="autoForm.customerSelection"
v-model:search="autoForm.customerSearch"
/>
</div>
<div v-if="item.value === 'vehicle'" class="p-5">
<div class="grid grid-cols-2 gap-4">
<UFormField name="vehicle.plate" label="License Plate" description="Vehicle license plate number" required>
<UInput v-model="autoForm.vehicle.plate" placeholder="ABC-123" size="lg" />
</UFormField>
<UFormField name="vehicle.make" label="Make" description="Vehicle manufacturer" required>
<UInput v-model="autoForm.vehicle.make" placeholder="Toyota" size="lg" />
</UFormField>
<UFormField name="vehicle.model" label="Model" description="Vehicle model name" required>
<UInput v-model="autoForm.vehicle.model" placeholder="Corolla" size="lg" />
</UFormField>
<UFormField name="vehicle.year" label="Year" description="Vehicle manufacturing year" required>
<UInput v-model="autoForm.vehicle.year" type="number" size="lg" />
</UFormField>
<UFormField name="vehicle.use_type" label="Use Type" description="Primary use of the vehicle" required>
<USelectMenu v-model="autoForm.vehicle.use_type" :items="[
{ label: 'Private', value: 'private' },
{ label: 'Commercial', value: 'commercial' },
{ label: 'Bus', value: 'bus' },
{ label: 'Taxi', value: 'taxi' },
{ label: 'School', value: 'school' }
]" size="lg" />
</UFormField>
<UFormField name="vehicle.car_type" label="Car Type" description="Vehicle body type" required>
<USelectMenu v-model="autoForm.vehicle.car_type" :items="[
{ label: 'Sedan', value: 'sedan' },
{ label: 'SUV', value: 'suv' },
{ label: 'Hatchback', value: 'hatchback' },
{ label: 'Coupe', value: 'coupe' },
{ label: 'Convertible', value: 'convertible' },
{ label: 'Pickup', value: 'pickup' },
{ label: 'Van', value: 'van' },
{ label: 'Minivan', value: 'minivan' },
{ label: 'Truck', value: 'truck' }
]" size="lg" />
</UFormField>
<UFormField name="vehicle.rc_limits.bodily_injury" label="Bodily Injury Limits" description="Third-party bodily injury coverage">
<UInput v-model="autoForm.vehicle.rc_limits.bodily_injury" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="vehicle.rc_limits.property_damage" label="Property Damage Limits" description="Third-party property damage coverage">
<UInput v-model="autoForm.vehicle.rc_limits.property_damage" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="vehicle.market_value" label="Market Value" description="Current market value of the vehicle">
<UInput v-model="autoForm.vehicle.market_value" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="vehicle.requested_value" label="Requested Value" description="Insured value for the vehicle">
<UInput v-model="autoForm.vehicle.requested_value" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="vehicle.chassis_number" label="Chassis Number" description="Vehicle chassis number (optional)">
<UInput v-model="autoForm.vehicle.chassis_number" placeholder="Optional" size="lg" />
</UFormField>
<UFormField name="vehicle.engine_number" label="Engine Number" description="Vehicle engine number (optional)">
<UInput v-model="autoForm.vehicle.engine_number" placeholder="Optional" size="lg" />
</UFormField>
</div>
</div>
<div v-if="item.value === 'provider'" class="p-5">
<ProviderSelector
v-model:search="autoForm.providerSearch"
v-model:selected="autoForm.selectedProviders"
/>
</div>
</template>
</UAccordion>
</UForm>
<UForm
v-if="activeTab === 'life'"
ref="lifeFormRef"
:schema="lifeSchema"
:state="lifeForm"
@submit="submitQuote"
>
<UAccordion :items="[
{
label: 'Customer Selection',
icon: 'i-heroicons-user',
content: '',
value: 'customer',
defaultOpen: true
},
{
label: 'Life Insurance Details',
icon: 'i-heroicons-shield-check',
content: '',
value: 'life',
defaultOpen: true
},
{
label: 'Provider Selection',
icon: 'i-heroicons-building-office',
content: '',
value: 'provider',
defaultOpen: true
}
]">
<template #body="{ item }">
<div v-if="item.value === 'customer'" class="p-5">
<CustomerSelector
v-model="lifeForm.customerSelection"
v-model:search="lifeForm.customerSearch"
/>
</div>
<div v-if="item.value === 'life'" class="p-5">
<div class="grid grid-cols-2 gap-4">
<UFormField name="life.coverage_type" label="Coverage Type" description="Type of life insurance coverage" required>
<USelectMenu v-model="lifeForm.life.coverage_type" :items="[
{ label: 'Banking', value: 'banking' },
{ label: 'Protection', value: 'protection' }
]" size="lg" />
</UFormField>
<UFormField name="life.coverage_amount" label="Coverage Amount" description="Amount of coverage in local currency">
<UInput v-model="lifeForm.life.coverage_amount" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="life.coverage_years" label="Coverage Years" description="Duration of the policy in years" required>
<UInput v-model="lifeForm.life.coverage_years" type="number" placeholder="10" size="lg" />
</UFormField>
<UFormField name="life.smoker" label="Smoker" description="Does the insured smoke tobacco?">
<UCheckbox v-model="lifeForm.life.smoker" label="Yes" />
</UFormField>
<UFormField name="life.medications" label="Medications" description="List any current medications (one per line)" class="col-span-2">
<ArrayInput
v-model="lifeForm.life.medications"
placeholder="Aspirin&#10;Lisinopril&#10;Metformin"
/>
</UFormField>
<UFormField name="life.surgeries" label="Surgeries" description="List any past surgeries (one per line)" class="col-span-2">
<ArrayInput
v-model="lifeForm.life.surgeries"
placeholder="Appendectomy, 2015&#10;Cataract surgery, 2020"
/>
</UFormField>
<UFormField name="life.weight" label="Weight (kg)" description="Weight in kilograms">
<UInput v-model="lifeForm.life.weight" type="number" placeholder="70" size="lg" />
</UFormField>
<UFormField name="life.height" label="Height (cm)" description="Height in centimeters">
<UInput v-model="lifeForm.life.height" type="number" placeholder="175" size="lg" />
</UFormField>
</div>
</div>
<div v-if="item.value === 'provider'" class="p-5">
<ProviderSelector
v-model:search="lifeForm.providerSearch"
v-model:selected="lifeForm.selectedProviders"
/>
</div>
</template>
</UAccordion>
</UForm>
<UForm
v-if="activeTab === 'fire_structure'"
ref="fireStructureFormRef"
:schema="fireStructureSchema"
:state="fireStructureForm"
@submit="submitQuote"
>
<UAccordion :items="[
{
label: 'Customer Selection',
icon: 'i-heroicons-user',
content: '',
value: 'customer',
defaultOpen: true
},
{
label: 'Property Information',
icon: 'i-heroicons-building-office-2',
content: '',
value: 'property',
defaultOpen: true
},
{
label: 'Provider Selection',
icon: 'i-heroicons-building-office',
content: '',
value: 'provider',
defaultOpen: true
}
]">
<template #body="{ item }">
<div v-if="item.value === 'customer'" class="p-5">
<CustomerSelector
v-model="fireStructureForm.customerSelection"
v-model:search="fireStructureForm.customerSearch"
/>
</div>
<div v-if="item.value === 'property'" class="p-5">
<div class="grid grid-cols-2 gap-4">
<UFormField name="property.location" label="Location" description="Full property address" required>
<UInput v-model="fireStructureForm.property.location" placeholder="Full address" size="lg" />
</UFormField>
<UFormField name="property.property_value" label="Property Value" description="Current property value">
<UInput v-model="fireStructureForm.property.property_value" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="property.property_use" label="Property Use" description="How the property is used" required>
<UInput v-model="fireStructureForm.property.property_use" placeholder="e.g., residential, commercial" size="lg" />
</UFormField>
<UFormField name="property.market_value" label="Market Value" description="Current market value of the property">
<UInput v-model="fireStructureForm.property.market_value" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="property.security_measures" label="Security Measures" description="Security measures installed (one per line)" class="col-span-2">
<ArrayInput
v-model="fireStructureForm.property.security_measures"
placeholder="security_system&#10;sprinklers&#10;fire_extinguisher&#10;alarm_system"
/>
</UFormField>
</div>
</div>
<div v-if="item.value === 'provider'" class="p-5">
<ProviderSelector
v-model:search="fireStructureForm.providerSearch"
v-model:selected="fireStructureForm.selectedProviders"
/>
</div>
</template>
</UAccordion>
</UForm>
<UForm
v-if="activeTab === 'fire_contents'"
ref="fireContentsFormRef"
:schema="fireContentsSchema"
:state="fireContentsForm"
@submit="submitQuote"
>
<UAccordion :items="[
{
label: 'Customer Selection',
icon: 'i-heroicons-user',
content: '',
value: 'customer',
defaultOpen: true
},
{
label: 'Contents Information',
icon: 'i-heroicons-building-office-2',
content: '',
value: 'contents',
defaultOpen: true
},
{
label: 'Provider Selection',
icon: 'i-heroicons-building-office',
content: '',
value: 'provider',
defaultOpen: true
}
]">
<template #body="{ item }">
<div v-if="item.value === 'customer'" class="p-5">
<CustomerSelector
v-model="fireContentsForm.customerSelection"
v-model:search="fireContentsForm.customerSearch"
/>
</div>
<div v-if="item.value === 'contents'" class="p-5">
<div class="grid grid-cols-2 gap-4">
<UFormField name="contents.location" label="Location" description="Property address for contents coverage" required>
<UInput v-model="fireContentsForm.contents.location" placeholder="Full address" size="lg" />
</UFormField>
<UFormField name="contents.contents_value" label="Contents Value" description="Total value of all contents">
<UInput v-model="fireContentsForm.contents.contents_value" type="number" placeholder="0" size="lg" />
</UFormField>
<UFormField name="contents.property_use" label="Property Use" description="How the property is used" required>
<UInput v-model="fireContentsForm.contents.property_use" placeholder="e.g., residential, commercial" size="lg" />
</UFormField>
<UFormField name="contents.security_measures" label="Security Measures" description="Security measures installed (one per line)" class="col-span-2">
<ArrayInput
v-model="fireContentsForm.contents.security_measures"
placeholder="security_system&#10;sprinklers&#10;fire_extinguisher&#10;alarm_system"
/>
</UFormField>
</div>
</div>
<div v-if="item.value === 'provider'" class="p-5">
<ProviderSelector
v-model:search="fireContentsForm.providerSearch"
v-model:selected="fireContentsForm.selectedProviders"
/>
</div>
</template>
</UAccordion>
</UForm>
<div v-if="submitError" class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p class="text-red-600 text-sm">{{ submitError }}</p>
</div>
<div class="flex justify-end gap-3 mt-8 pt-6 border-t border-[var(--card-border)]">
<UButton color="neutral" variant="outline" size="lg" @click="router.back()" :disabled="isSubmitting">Cancel</UButton>
<UButton color="primary" size="lg" @click="submitQuote" :loading="isSubmitting" :disabled="isSubmitting">Create Quote</UButton>
</div>
</div>
</template>