Add nuxt-skills and update auto quotes to use new policy API structure
- Add nuxt-skills (vue, nuxt, nuxt-ui) to .claude/skills/ - Create useCustomerSelection() composable for managing insured/buyer selection - Create usePolicyApi() composable for policy API operations - Update auto quote components to use insured/buyer instead of client - Update vehicle fields: remove valorVehiculo, add market_value, requested_value, rc_limits - Make chassis_number and engine_number optional - Update auto quote types and composables to match new API structure - Update auto quote page to submit to policy API with new structure
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
AUTO_YEAR_OPTIONS
|
||||
} from '~/data/auto-quote-intake'
|
||||
import type { AutoQuoteDraft, AutoQuoteSegment } from '~/types/auto-quote-intake'
|
||||
import { useCustomerSelection } from '~/composables/useCustomerSelection'
|
||||
|
||||
const props = defineProps<{
|
||||
draft: AutoQuoteDraft
|
||||
@@ -24,36 +25,190 @@ const showOrganization = computed(
|
||||
|
||||
const inputPh =
|
||||
'w-full placeholder:text-[var(--text-muted)] placeholder:opacity-[0.55] text-[var(--text-primary)]'
|
||||
|
||||
// Customer selection
|
||||
const customerSearch = ref('')
|
||||
const debouncedCustomerSearch = refDebounced(customerSearch, 300)
|
||||
const customerPage = ref(1)
|
||||
|
||||
const { data: customersData, pending: customersPending } = useCustomer('/customers', {
|
||||
query: computed(() => ({
|
||||
'page[number]': customerPage.value,
|
||||
'page[size]': 12,
|
||||
...(debouncedCustomerSearch.value && {
|
||||
'filters[0][field]': 'search',
|
||||
'filters[0][op]': '==',
|
||||
'filters[0][value]': debouncedCustomerSearch.value
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
watch(debouncedCustomerSearch, () => { customerPage.value = 1 })
|
||||
|
||||
const customerItems = computed(() => customersData.value?.data ?? [])
|
||||
|
||||
function selectCustomer(customer: any) {
|
||||
selectedCustomer.value = customer
|
||||
}
|
||||
|
||||
function selectBuyer(customer: any) {
|
||||
selectedBuyer.value = customer
|
||||
}
|
||||
|
||||
const customerDisplayName = (c: any) =>
|
||||
c.customer_type === 'corporate'
|
||||
? (c.commercial_name || c.legal_name)
|
||||
: `${c.first_name} ${c.last_name}`
|
||||
|
||||
const customerSubtitle = (c: any) =>
|
||||
c.customer_type === 'corporate' ? c.ruc : c.email
|
||||
|
||||
// Use customer selection composable
|
||||
const {
|
||||
selectedCustomer,
|
||||
selectedBuyer,
|
||||
useSameForBuyer,
|
||||
insured,
|
||||
buyer,
|
||||
isInsuredValid,
|
||||
isBuyerValid,
|
||||
validationErrors
|
||||
} = useCustomerSelection()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-8">
|
||||
<!-- Insured Section -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Client</h2>
|
||||
<p class="mt-1 text-sm text-[var(--text-muted)]">Contact on file for this quote — we’ll use it for status and carrier emails.</p>
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<UFormField label="Legal name" required>
|
||||
<UInput v-model="draft.client.fullName" :class="inputPh" placeholder="As on government ID" />
|
||||
</UFormField>
|
||||
<UFormField label="Email" required>
|
||||
<UInput
|
||||
v-model="draft.client.email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
:class="inputPh"
|
||||
placeholder="name@company.com"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="Phone">
|
||||
<UInput v-model="draft.client.phone" type="tel" :class="inputPh" placeholder="+593 …" />
|
||||
</UFormField>
|
||||
<UFormField label="Government ID">
|
||||
<UInput v-model="draft.client.documentId" :class="inputPh" placeholder="Cédula, passport, or RUC" />
|
||||
</UFormField>
|
||||
<UFormField v-if="showOrganization" label="Organization" class="md:col-span-2">
|
||||
<UInput v-model="draft.client.organizationName" :class="inputPh" placeholder="Company or fleet name" />
|
||||
</UFormField>
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Insured</h2>
|
||||
<p class="mt-1 text-sm text-[var(--text-muted)]">Person or entity being insured — we'll use this for carrier notifications.</p>
|
||||
|
||||
<div v-if="!selectedCustomer" class="mt-5">
|
||||
<UInput
|
||||
v-model="customerSearch"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="Search by name, email, RUC..."
|
||||
class="w-full max-w-sm mb-4"
|
||||
/>
|
||||
|
||||
<div v-if="customersPending" class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div v-for="n in 3" :key="n" class="h-16 animate-pulse bg-gray-100 rounded-lg" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3 max-h-72 overflow-y-auto">
|
||||
<div
|
||||
v-for="c in customerItems"
|
||||
:key="c.id"
|
||||
class="flex items-center gap-3 p-3 border-2 rounded-lg cursor-pointer transition-all"
|
||||
:class="selectedCustomer?.id === c.id
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300 bg-white'"
|
||||
@click="selectCustomer(c)"
|
||||
>
|
||||
<UAvatar :alt="customerDisplayName(c)" size="sm" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="font-medium text-sm text-slate-800 truncate">{{ customerDisplayName(c) }}</p>
|
||||
<UBadge
|
||||
:color="c.customer_type === 'corporate' ? 'purple' : 'blue'"
|
||||
variant="soft" size="xs" class="flex-shrink-0"
|
||||
>
|
||||
{{ c.customer_type === 'corporate' ? 'Corp' : 'Ind' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 truncate">{{ customerSubtitle(c) }}</p>
|
||||
</div>
|
||||
<UIcon
|
||||
v-if="selectedCustomer?.id === c.id"
|
||||
name="i-heroicons-check-circle"
|
||||
class="w-5 h-5 text-primary-500 flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="customerItems.length === 0" class="text-center py-6 text-gray-400 text-sm">
|
||||
No customers found.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-5 p-4 bg-primary-50 border border-primary-200 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<UAvatar :alt="customerDisplayName(selectedCustomer)" size="sm" />
|
||||
<div>
|
||||
<p class="font-medium text-primary-800">{{ customerDisplayName(selectedCustomer) }}</p>
|
||||
<p class="text-xs text-primary-600">{{ customerSubtitle(selectedCustomer) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<UButton size="sm" color="neutral" variant="ghost" @click="selectedCustomer = null">
|
||||
Change
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buyer Section -->
|
||||
<div class="border-t border-[var(--sidebar-border)] pt-8">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Buyer</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<UToggle v-model="useSameForBuyer" />
|
||||
<span class="text-sm text-[var(--text-muted)]">Same as insured</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="useSameForBuyer" class="mt-1 text-sm text-[var(--text-muted)]">
|
||||
Using same person as insured
|
||||
</p>
|
||||
|
||||
<div v-else class="mt-5">
|
||||
<UInput
|
||||
v-model="customerSearch"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="Search by name, email, RUC..."
|
||||
class="w-full max-w-sm mb-4"
|
||||
/>
|
||||
|
||||
<div v-if="customersPending" class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div v-for="n in 3" :key="n" class="h-16 animate-pulse bg-gray-100 rounded-lg" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3 max-h-72 overflow-y-auto">
|
||||
<div
|
||||
v-for="c in customerItems"
|
||||
:key="c.id"
|
||||
class="flex items-center gap-3 p-3 border-2 rounded-lg cursor-pointer transition-all"
|
||||
:class="selectedBuyer?.id === c.id
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300 bg-white'"
|
||||
@click="selectBuyer(c)"
|
||||
>
|
||||
<UAvatar :alt="customerDisplayName(c)" size="sm" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="font-medium text-sm text-slate-800 truncate">{{ customerDisplayName(c) }}</p>
|
||||
<UBadge
|
||||
:color="c.customer_type === 'corporate' ? 'purple' : 'blue'"
|
||||
variant="soft" size="xs" class="flex-shrink-0"
|
||||
>
|
||||
{{ c.customer_type === 'corporate' ? 'Corp' : 'Ind' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 truncate">{{ customerSubtitle(c) }}</p>
|
||||
</div>
|
||||
<UIcon
|
||||
v-if="selectedBuyer?.id === c.id"
|
||||
name="i-heroicons-check-circle"
|
||||
class="w-5 h-5 text-primary-500 flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="customerItems.length === 0" class="text-center py-6 text-gray-400 text-sm">
|
||||
No customers found.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-[var(--sidebar-border)] pt-8">
|
||||
@@ -145,8 +300,20 @@ const inputPh =
|
||||
<UFormField label="Capacity" description="Passengers">
|
||||
<UInput v-model="draft.vehicle.capacidadPasajeros" :class="inputPh" inputmode="numeric" placeholder="—" />
|
||||
</UFormField>
|
||||
<UFormField label="Declared value" description="USD">
|
||||
<UInput v-model="draft.vehicle.valorVehiculo" :class="inputPh" inputmode="decimal" placeholder="—" />
|
||||
<UFormField label="RC limits">
|
||||
<UInput v-model="draft.vehicle.rc_limits" :class="inputPh" placeholder="e.g., 100,000" />
|
||||
</UFormField>
|
||||
<UFormField label="Market value" description="USD">
|
||||
<UInput v-model="draft.vehicle.market_value" :class="inputPh" inputmode="decimal" placeholder="—" />
|
||||
</UFormField>
|
||||
<UFormField label="Requested value" description="USD">
|
||||
<UInput v-model="draft.vehicle.requested_value" :class="inputPh" inputmode="decimal" placeholder="—" />
|
||||
</UFormField>
|
||||
<UFormField label="Chassis number" description="Optional">
|
||||
<UInput v-model="draft.vehicle.chassis_number" :class="inputPh" placeholder="—" />
|
||||
</UFormField>
|
||||
<UFormField label="Engine number" description="Optional">
|
||||
<UInput v-model="draft.vehicle.engine_number" :class="inputPh" placeholder="—" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,8 @@ export function emptyAutoQuoteDraft(): AutoQuoteDraft {
|
||||
return {
|
||||
quoteMode: null,
|
||||
segment: null,
|
||||
client: {
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
documentId: '',
|
||||
organizationName: ''
|
||||
},
|
||||
insured: null,
|
||||
buyer: null,
|
||||
vehicle: {
|
||||
subRamo: '',
|
||||
clase: '',
|
||||
@@ -20,7 +15,11 @@ export function emptyAutoQuoteDraft(): AutoQuoteDraft {
|
||||
placa: '',
|
||||
year: '',
|
||||
capacidadPasajeros: '',
|
||||
valorVehiculo: ''
|
||||
rc_limits: '',
|
||||
market_value: 0,
|
||||
requested_value: 0,
|
||||
chassis_number: '',
|
||||
engine_number: ''
|
||||
},
|
||||
solicit: {
|
||||
carrierIds: [],
|
||||
|
||||
137
app/composables/useCustomerSelection.ts
Normal file
137
app/composables/useCustomerSelection.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Composable for managing customer selection in quote flows
|
||||
* Handles insured and buyer selection with validation
|
||||
*/
|
||||
export function useCustomerSelection() {
|
||||
const selectedCustomer = ref<any>(null) // Auto-generated type from useCustomer
|
||||
const useSameForBuyer = ref(true)
|
||||
const selectedBuyer = ref<any>(null)
|
||||
|
||||
/**
|
||||
* Convert customer-service customer to policy-service insured/buyer
|
||||
* Maps customer fields to policy-service structure
|
||||
*/
|
||||
const toPolicyPerson = (customer: any) => {
|
||||
if (customer.customer_type === 'corporate') {
|
||||
return {
|
||||
type: 'corporate',
|
||||
company_name: customer.legal_name,
|
||||
ruc: customer.ruc,
|
||||
legal_rep_name: customer.legal_rep_name,
|
||||
legal_rep_document: customer.legal_rep_document_id,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
address: customer.address
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'individual',
|
||||
name: `${customer.first_name} ${customer.last_name}`.trim(),
|
||||
date_of_birth: customer.birth_date,
|
||||
document_id: customer.document_id,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
address: customer.address
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insured person from selected customer
|
||||
*/
|
||||
const insured = computed(() => {
|
||||
if (!selectedCustomer.value) return null
|
||||
return toPolicyPerson(selectedCustomer.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get buyer person (either same as insured or different)
|
||||
*/
|
||||
const buyer = computed(() => {
|
||||
if (useSameForBuyer.value) {
|
||||
return insured.value
|
||||
}
|
||||
if (!selectedBuyer.value) return null
|
||||
return toPolicyPerson(selectedBuyer.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* Validate customer has required fields for policy submission
|
||||
*/
|
||||
const validateCustomer = (customer: any) => {
|
||||
const missing: string[] = []
|
||||
|
||||
if (customer.customer_type === 'corporate') {
|
||||
if (!customer.legal_name) missing.push('legal_name')
|
||||
if (!customer.ruc) missing.push('ruc')
|
||||
if (!customer.legal_rep_name) missing.push('legal_rep_name')
|
||||
if (!customer.legal_rep_document_id) missing.push('legal_rep_document_id')
|
||||
} else {
|
||||
if (!customer.first_name) missing.push('first_name')
|
||||
if (!customer.last_name) missing.push('last_name')
|
||||
if (!customer.birth_date) missing.push('birth_date')
|
||||
if (!customer.document_id) missing.push('document_id')
|
||||
}
|
||||
|
||||
return { valid: missing.length === 0, missing }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if insured is valid
|
||||
*/
|
||||
const isInsuredValid = computed(() => {
|
||||
if (!selectedCustomer.value) return false
|
||||
return validateCustomer(selectedCustomer.value).valid
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if buyer is valid
|
||||
*/
|
||||
const isBuyerValid = computed(() => {
|
||||
if (useSameForBuyer.value) {
|
||||
return isInsuredValid.value
|
||||
}
|
||||
if (!selectedBuyer.value) return false
|
||||
return validateCustomer(selectedBuyer.value).valid
|
||||
})
|
||||
|
||||
/**
|
||||
* Get validation errors
|
||||
*/
|
||||
const validationErrors = computed(() => {
|
||||
const errors: { insured: string[]; buyer: string[] } = { insured: [], buyer: [] }
|
||||
|
||||
if (selectedCustomer.value) {
|
||||
const validation = validateCustomer(selectedCustomer.value)
|
||||
errors.insured = validation.missing
|
||||
}
|
||||
|
||||
if (!useSameForBuyer.value && selectedBuyer.value) {
|
||||
const validation = validateCustomer(selectedBuyer.value)
|
||||
errors.buyer = validation.missing
|
||||
}
|
||||
|
||||
return errors
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset selection
|
||||
*/
|
||||
function reset() {
|
||||
selectedCustomer.value = null
|
||||
selectedBuyer.value = null
|
||||
useSameForBuyer.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
selectedCustomer,
|
||||
selectedBuyer,
|
||||
useSameForBuyer,
|
||||
insured,
|
||||
buyer,
|
||||
isInsuredValid,
|
||||
isBuyerValid,
|
||||
validationErrors,
|
||||
reset
|
||||
}
|
||||
}
|
||||
67
app/composables/usePolicyApi.ts
Normal file
67
app/composables/usePolicyApi.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Composable for policy API operations
|
||||
* Handles quote submission and acceptance
|
||||
*/
|
||||
export function usePolicyApi() {
|
||||
const { $policy } = useNuxtApp()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* Submit a policy quote request
|
||||
*/
|
||||
async function submitPolicyQuote(payload: {
|
||||
policy_type: 'car' | 'life' | 'fire_structure' | 'fire_contents'
|
||||
insured: any
|
||||
buyer: any
|
||||
policy_details: any
|
||||
selected_providers: Array<{ provider_id: string; email: string }>
|
||||
}) {
|
||||
try {
|
||||
const data = await $policy('/policies', {
|
||||
method: 'POST',
|
||||
body: payload
|
||||
}) as any
|
||||
|
||||
toast.add({ title: 'Quote submitted successfully', color: 'green' })
|
||||
return data
|
||||
} catch (e: any) {
|
||||
toast.add({
|
||||
title: 'Failed to submit quote',
|
||||
description: e?.data?.error ?? e.message,
|
||||
color: 'red'
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a quote plan and trigger solicitation
|
||||
*/
|
||||
async function acceptQuote(applicationId: string, acceptedPlanId: string, acceptedBy: string) {
|
||||
try {
|
||||
const data = await $policy(`/policies/${applicationId}/accept`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
accepted_plan_id: acceptedPlanId,
|
||||
accepted_by: acceptedBy
|
||||
}
|
||||
}) as any
|
||||
|
||||
toast.add({ title: 'Plan accepted successfully', color: 'green' })
|
||||
return data
|
||||
} catch (e: any) {
|
||||
toast.add({
|
||||
title: 'Failed to accept plan',
|
||||
description: e?.data?.error ?? e.message,
|
||||
color: 'red'
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
submitPolicyQuote,
|
||||
acceptQuote
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { emptyAutoQuoteDraft } from '~/composables/useAutoQuoteDraft'
|
||||
import type { AutoQuoteIntakePayload, AutoQuoteMode, AutoQuoteSegment } from '~/types/auto-quote-intake'
|
||||
import { useCustomerSelection } from '~/composables/useCustomerSelection'
|
||||
import { usePolicyApi } from '~/composables/usePolicyApi'
|
||||
|
||||
/** Client-only: many Nuxt UI fields on this screen can stall hydration / main thread if SSR + client fight */
|
||||
definePageMeta({ ssr: false })
|
||||
@@ -26,6 +28,192 @@ const draft = reactive(emptyAutoQuoteDraft())
|
||||
const toast = useToast()
|
||||
const { quoteRequestEmailEnabled } = useQuoteRequestEmailEnabled()
|
||||
|
||||
// Use customer selection composable
|
||||
const {
|
||||
insured,
|
||||
buyer,
|
||||
isInsuredValid,
|
||||
isBuyerValid,
|
||||
validationErrors
|
||||
} = useCustomerSelection()
|
||||
|
||||
// Use policy API composable
|
||||
const { submitPolicyQuote } = usePolicyApi()
|
||||
|
||||
const modeCards: { id: AutoQuoteMode; title: string; hint: string; icon: string }[] = [
|
||||
{
|
||||
id: 'single',
|
||||
title: 'Single quote',
|
||||
hint: 'One package — we'll email carriers' quoting inboxes on file.',
|
||||
icon: 'i-heroicons-document-text'
|
||||
},
|
||||
{
|
||||
id: 'comparative_pdf',
|
||||
title: 'Comparative quote',
|
||||
hint: 'Same vehicle facts; prep plan comparisons and enter premiums when emails arrive.',
|
||||
icon: 'i-heroicons-document-duplicate'
|
||||
}
|
||||
]
|
||||
|
||||
const segmentCards: { id: AutoQuoteSegment; title: string; hint: string; icon: string }[] = [
|
||||
{
|
||||
id: 'individual',
|
||||
title: 'Individual',
|
||||
hint: 'Personal auto.',
|
||||
icon: 'i-heroicons-user'
|
||||
},
|
||||
{
|
||||
id: 'corporate',
|
||||
title: 'Corporate',
|
||||
hint: 'Business or group.',
|
||||
icon: 'i-heroicons-building-office-2'
|
||||
},
|
||||
{
|
||||
id: 'fleet',
|
||||
title: 'Fleet',
|
||||
hint: 'Fleet program.',
|
||||
icon: 'i-heroicons-truck'
|
||||
}
|
||||
]
|
||||
|
||||
function canProceedFromSetup() {
|
||||
if (!draft.quoteMode) {
|
||||
toast.add({ title: 'Choose a quote type', description: 'Single or comparative.', color: 'warning' })
|
||||
return false
|
||||
}
|
||||
if (!draft.segment) {
|
||||
toast.add({ title: 'Choose policy type', description: 'Individual, corporate, or fleet.', color: 'warning' })
|
||||
return false
|
||||
}
|
||||
if (!isInsuredValid.value) {
|
||||
toast.add({
|
||||
title: 'Complete insured information',
|
||||
description: `Missing: ${validationErrors.value.insured.join(', ')}`,
|
||||
color: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (!isBuyerValid.value) {
|
||||
toast.add({
|
||||
title: 'Complete buyer information',
|
||||
description: `Missing: ${validationErrors.value.buyer.join(', ')}`,
|
||||
color: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function canProceedFromSolicit() {
|
||||
if (draft.solicit.carrierIds.length === 0 || draft.solicit.planIds.length === 0) {
|
||||
toast.add({
|
||||
title: 'Choose carriers and plans',
|
||||
description: 'Select at least one insurance company and one coverage package.',
|
||||
color: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function goToStep(target: StepId) {
|
||||
const ti = STEP_ORDER.indexOf(target)
|
||||
if (ti > maxStepIndex.value) return
|
||||
step.value = target
|
||||
}
|
||||
|
||||
function onStepPillClick(stepIndex: number, target: StepId) {
|
||||
if (stepIndex > maxStepIndex.value) return
|
||||
goToStep(target)
|
||||
}
|
||||
|
||||
function goPrev() {
|
||||
const i = STEP_ORDER.indexOf(step.value)
|
||||
if (i <= 0) return
|
||||
step.value = STEP_ORDER[i - 1]!
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
const i = STEP_ORDER.indexOf(step.value)
|
||||
if (step.value === 'setup' && !canProceedFromSetup()) return
|
||||
if (step.value === 'solicit' && !canProceedFromSolicit()) return
|
||||
if (i >= STEP_ORDER.length - 1) return
|
||||
const next = STEP_ORDER[i + 1]!
|
||||
step.value = next
|
||||
maxStepIndex.value = Math.max(maxStepIndex.value, i + 1)
|
||||
}
|
||||
|
||||
function buildPayload(): AutoQuoteIntakePayload {
|
||||
return {
|
||||
policy_type: 'car',
|
||||
insured: insured.value,
|
||||
buyer: buyer.value,
|
||||
policy_details: { ...draft.vehicle },
|
||||
selected_providers: draft.solicit.carrierIds.map(id => ({
|
||||
provider_id: id,
|
||||
email: getProviderEmail(id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderEmail(providerId: string): string {
|
||||
// This would come from the providers API
|
||||
// For now, return a placeholder
|
||||
return `quotes@${providerId}.com`
|
||||
}
|
||||
|
||||
async function finalize() {
|
||||
if (!draft.quoteMode || !draft.segment) return
|
||||
if (intakeBusy.value) return
|
||||
intakeBusy.value = true
|
||||
try {
|
||||
const payload = buildPayload()
|
||||
const emailOn = quoteRequestEmailEnabled.value
|
||||
|
||||
if (payload.quoteMode === 'comparative_pdf') {
|
||||
toast.add({
|
||||
title: emailOn ? 'Quote requests queued' : 'Comparative run saved',
|
||||
description: emailOn
|
||||
? 'Opening the comparative sheet. Provider emails follow your Settings → Quote requests toggle.'
|
||||
: 'Emails to providers are disabled — comparative layout saved for manual or table pricing.',
|
||||
color: 'success'
|
||||
})
|
||||
await nextTick()
|
||||
await navigateTo({
|
||||
path: '/quotes/compare',
|
||||
query: { from: 'auto', segment: payload.segment }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Submit to policy API
|
||||
const data = await submitPolicyQuote(payload)
|
||||
|
||||
toast.add({
|
||||
title: emailOn ? 'Quote requests recorded' : 'Quote run saved (no emails)',
|
||||
description: emailOn
|
||||
? 'Requests can be sent to carrier quoting addresses on file when your integration is on.'
|
||||
: 'Outbound provider email is off in Settings — this request stays in-app for tables, APIs, or AI.',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
// Navigate to policy detail page
|
||||
await navigateTo(`/policies/${data.application_id}`)
|
||||
} finally {
|
||||
intakeBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const step = ref<StepId>('setup')
|
||||
/** Highest step index the user has reached (for stepper — no reactive watch loops) */
|
||||
const maxStepIndex = ref(0)
|
||||
const intakeBusy = ref(false)
|
||||
|
||||
const draft = reactive(emptyAutoQuoteDraft())
|
||||
|
||||
const toast = useToast()
|
||||
const { quoteRequestEmailEnabled } = useQuoteRequestEmailEnabled()
|
||||
|
||||
const modeCards: { id: AutoQuoteMode; title: string; hint: string; icon: string }[] = [
|
||||
{
|
||||
id: 'single',
|
||||
|
||||
@@ -2,16 +2,6 @@ export type AutoQuoteMode = 'single' | 'comparative_pdf'
|
||||
|
||||
export type AutoQuoteSegment = 'individual' | 'corporate' | 'fleet'
|
||||
|
||||
export type AutoQuoteClient = {
|
||||
fullName: string
|
||||
email: string
|
||||
phone: string
|
||||
/** Cédula / pasaporte / ID */
|
||||
documentId: string
|
||||
/** Used when segment is corporate or fleet */
|
||||
organizationName: string
|
||||
}
|
||||
|
||||
export type AutoQuoteVehicle = {
|
||||
subRamo: string
|
||||
clase: string
|
||||
@@ -21,7 +11,11 @@ export type AutoQuoteVehicle = {
|
||||
placa: string
|
||||
year: string | null
|
||||
capacidadPasajeros: string
|
||||
valorVehiculo: string
|
||||
rc_limits: string
|
||||
market_value: number
|
||||
requested_value: number
|
||||
chassis_number?: string
|
||||
engine_number?: string
|
||||
}
|
||||
|
||||
export type AutoQuoteSolicit = {
|
||||
@@ -32,15 +26,16 @@ export type AutoQuoteSolicit = {
|
||||
export type AutoQuoteDraft = {
|
||||
quoteMode: AutoQuoteMode | null
|
||||
segment: AutoQuoteSegment | null
|
||||
client: AutoQuoteClient
|
||||
insured: any | null
|
||||
buyer: any | null
|
||||
vehicle: AutoQuoteVehicle
|
||||
solicit: AutoQuoteSolicit
|
||||
}
|
||||
|
||||
export type AutoQuoteIntakePayload = {
|
||||
quoteMode: AutoQuoteMode
|
||||
segment: AutoQuoteSegment
|
||||
client: AutoQuoteClient
|
||||
vehicle: AutoQuoteVehicle
|
||||
solicit: AutoQuoteSolicit
|
||||
policy_type: 'car'
|
||||
insured: any
|
||||
buyer: any
|
||||
policy_details: AutoQuoteVehicle
|
||||
selected_providers: Array<{ provider_id: string; email: string }>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user