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:
2026-04-27 14:56:53 -05:00
parent 67482f6629
commit a2eb1f3789
154 changed files with 10346 additions and 51 deletions

View File

@@ -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',