WIP jordan
This commit is contained in:
291
app/pages/quotes/auto/index.vue
Normal file
291
app/pages/quotes/auto/index.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<script setup lang="ts">
|
||||
import { emptyAutoQuoteDraft } from '~/composables/useAutoQuoteDraft'
|
||||
import type { AutoQuoteIntakePayload, AutoQuoteMode, AutoQuoteSegment } from '~/types/auto-quote-intake'
|
||||
|
||||
/** Client-only: many Nuxt UI fields on this screen can stall hydration / main thread if SSR + client fight */
|
||||
definePageMeta({ ssr: false })
|
||||
|
||||
usePageTitle('Quotes · Auto')
|
||||
|
||||
const STEP_ORDER = ['setup', 'solicit', 'acceptance'] as const
|
||||
type StepId = (typeof STEP_ORDER)[number]
|
||||
|
||||
const STEP_LABELS: Record<StepId, string> = {
|
||||
setup: 'Quote setup',
|
||||
solicit: 'Quotes to solicit',
|
||||
acceptance: 'Acceptance'
|
||||
}
|
||||
|
||||
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',
|
||||
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 canProceedFromCustomer() {
|
||||
const c = draft.client
|
||||
if (!c.fullName.trim() || !c.email.trim()) {
|
||||
toast.add({
|
||||
title: 'Add legal name and email',
|
||||
description: 'We need them for carrier notifications.',
|
||||
color: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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 (!canProceedFromCustomer()) return false
|
||||
if (
|
||||
(draft.segment === 'corporate' || draft.segment === 'fleet') &&
|
||||
!draft.client.organizationName?.trim()
|
||||
) {
|
||||
toast.add({
|
||||
title: 'Add organization',
|
||||
description: 'Required for corporate and fleet policies.',
|
||||
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 {
|
||||
quoteMode: draft.quoteMode!,
|
||||
segment: draft.segment!,
|
||||
client: { ...draft.client },
|
||||
vehicle: { ...draft.vehicle },
|
||||
solicit: {
|
||||
carrierIds: [...draft.solicit.carrierIds],
|
||||
planIds: [...draft.solicit.planIds]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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'
|
||||
})
|
||||
} finally {
|
||||
intakeBusy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-4xl space-y-6 pb-12">
|
||||
<NuxtLink to="/quotes" class="inline-flex">
|
||||
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Back to quotes</UButton>
|
||||
</NuxtLink>
|
||||
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Auto quoting</h1>
|
||||
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
||||
Set up the risk, choose who to solicit, then accept — three steps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-x-1 gap-y-2 text-[11px] font-medium text-[var(--text-muted)] sm:text-xs"
|
||||
role="navigation"
|
||||
aria-label="Steps"
|
||||
>
|
||||
<template v-for="(s, idx) in STEP_ORDER" :key="s">
|
||||
<UIcon v-if="idx > 0" name="i-heroicons-chevron-right" class="h-3 w-3 shrink-0 opacity-40" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 rounded-full px-2 py-1 text-left transition sm:px-2.5"
|
||||
:class="
|
||||
step === s
|
||||
? 'bg-[var(--brand-soft)] text-[var(--brand)]'
|
||||
: idx <= maxStepIndex
|
||||
? 'cursor-pointer bg-[var(--sidebar-border)]/60 hover:bg-[var(--brand-soft)]/80 hover:text-[var(--brand)]'
|
||||
: 'cursor-default bg-[var(--sidebar-border)]/60 opacity-50'
|
||||
"
|
||||
:aria-current="step === s ? 'step' : undefined"
|
||||
@click.prevent.stop="onStepPillClick(idx, s)"
|
||||
>
|
||||
<span class="hidden sm:inline">{{ idx + 1 }}. {{ STEP_LABELS[s] }}</span>
|
||||
<span class="sm:hidden">{{ idx + 1 }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<UCard :ui="{ body: { padding: 'p-5 sm:p-6' } }">
|
||||
<template #header>
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]">
|
||||
Step {{ STEP_ORDER.indexOf(step) + 1 }} of {{ STEP_ORDER.length }}
|
||||
</p>
|
||||
<h2 class="mt-0.5 text-lg font-semibold text-[var(--text-primary)]">{{ STEP_LABELS[step] }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<QuotesAutoSetupStep
|
||||
v-if="step === 'setup'"
|
||||
:draft="draft"
|
||||
:mode-cards="modeCards"
|
||||
:segment-cards="segmentCards"
|
||||
/>
|
||||
|
||||
<QuotesAutoSolicitQuotesStep
|
||||
v-else-if="step === 'solicit' && draft.quoteMode"
|
||||
:draft="draft"
|
||||
:quote-mode="draft.quoteMode"
|
||||
/>
|
||||
|
||||
<QuotesAutoAcceptanceStep
|
||||
v-else-if="step === 'acceptance' && draft.quoteMode && draft.segment"
|
||||
:draft="draft"
|
||||
:quote-mode="draft.quoteMode"
|
||||
:segment="draft.segment"
|
||||
/>
|
||||
</UCard>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<UButton
|
||||
v-if="step !== 'setup'"
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
@click="goPrev"
|
||||
>
|
||||
Back
|
||||
</UButton>
|
||||
<NuxtLink v-else to="/quotes" class="inline-flex">
|
||||
<UButton color="neutral" variant="ghost" size="sm">Cancel</UButton>
|
||||
</NuxtLink>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UButton v-if="step !== 'acceptance'" type="button" color="primary" @click="goNext">
|
||||
Continue
|
||||
</UButton>
|
||||
<UButton
|
||||
v-else
|
||||
type="button"
|
||||
color="primary"
|
||||
:loading="intakeBusy"
|
||||
:disabled="intakeBusy"
|
||||
@click="finalize"
|
||||
>
|
||||
{{ quoteRequestEmailEnabled ? 'Send quote requests' : 'Save quote run' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user