322 lines
15 KiB
Vue
322 lines
15 KiB
Vue
<script setup lang="ts">
|
|
definePageMeta({ ssr: false })
|
|
|
|
usePageTitle('Quotes · General Risk')
|
|
|
|
const STEP_ORDER = ['setup', 'details', 'review'] as const
|
|
type StepId = (typeof STEP_ORDER)[number]
|
|
|
|
const STEP_LABELS: Record<StepId, string> = {
|
|
setup: 'Quote setup',
|
|
details: 'Risk details',
|
|
review: 'Review & submit'
|
|
}
|
|
|
|
const step = ref<StepId>('setup')
|
|
const maxStepIndex = ref(0)
|
|
const busy = ref(false)
|
|
const toast = useToast()
|
|
|
|
const form = reactive({
|
|
quoteType: '' as '' | 'single' | 'comparative',
|
|
riskCategory: '' as '' | 'property' | 'liability' | 'fire' | 'all-risk' | 'commercial' | 'other',
|
|
clientName: '',
|
|
clientEmail: '',
|
|
clientPhone: '',
|
|
clientOrganization: '',
|
|
propertyAddress: '',
|
|
propertyDescription: '',
|
|
sumInsured: '',
|
|
coverageStart: '',
|
|
coverageEnd: '',
|
|
specialConditions: '',
|
|
deductible: ''
|
|
})
|
|
|
|
const inputClass = 'w-full rounded-xl border border-[var(--sidebar-border)] bg-[var(--surface)] px-3 py-2 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]'
|
|
const labelClass = 'block text-sm font-semibold text-[var(--text-muted)] mb-1.5'
|
|
|
|
function canProceedFromSetup() {
|
|
if (!form.quoteType) {
|
|
toast.add({ title: 'Choose quote type', description: 'Single or comparative.', color: 'warning' })
|
|
return false
|
|
}
|
|
if (!form.riskCategory) {
|
|
toast.add({ title: 'Choose risk category', description: 'Select the type of risk.', color: 'warning' })
|
|
return false
|
|
}
|
|
if (!form.clientName.trim() || !form.clientEmail.trim()) {
|
|
toast.add({ title: 'Client info required', description: 'Name and email are needed.', color: 'warning' })
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
function canProceedFromDetails() {
|
|
if (!form.sumInsured) {
|
|
toast.add({ title: 'Sum insured required', description: 'Enter the total sum insured.', color: 'warning' })
|
|
return false
|
|
}
|
|
if (!form.coverageStart || !form.coverageEnd) {
|
|
toast.add({ title: 'Coverage period required', description: 'Enter start and end dates.', color: 'warning' })
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
function onStepPillClick(idx: number, target: StepId) {
|
|
if (idx > maxStepIndex.value) return
|
|
step.value = 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 === 'details' && !canProceedFromDetails()) 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)
|
|
}
|
|
|
|
async function finalize() {
|
|
if (busy.value) return
|
|
busy.value = true
|
|
try {
|
|
toast.add({
|
|
title: 'General risk quote submitted',
|
|
description: 'Your quote request has been recorded and is ready for placement.',
|
|
color: 'success'
|
|
})
|
|
await nextTick()
|
|
await navigateTo('/quotes')
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
const riskCategoryLabel: Record<string, string> = {
|
|
property: 'Property', liability: 'Liability', fire: 'Fire',
|
|
'all-risk': 'All-Risk', commercial: 'Commercial', other: 'Other'
|
|
}
|
|
</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)]">General risk quoting</h1>
|
|
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
|
Property, liability, fire, all-risk, and commercial lines — set up, detail, review.
|
|
</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-medium 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>
|
|
|
|
<!-- Step 1: Quote setup -->
|
|
<div v-if="step === 'setup'" class="space-y-10">
|
|
<section class="space-y-4">
|
|
<div>
|
|
<h3 class="text-base font-semibold text-[var(--text-primary)]">Quote type</h3>
|
|
<p class="mt-1 text-sm text-[var(--text-muted)]">Single placement or comparative — comparative sends the same risk to multiple carriers.</p>
|
|
</div>
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<button
|
|
v-for="opt in [
|
|
{ id: 'single', label: 'Single quote', hint: 'One risk, one placement.', icon: 'i-heroicons-document-text' },
|
|
{ id: 'comparative', label: 'Comparative quote', hint: 'Same risk sent to multiple carriers.', icon: 'i-heroicons-document-duplicate' }
|
|
]"
|
|
:key="opt.id" type="button"
|
|
class="group rounded-xl border p-5 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]"
|
|
:class="form.quoteType === opt.id ? 'border-[var(--brand)] bg-[var(--brand-soft)] ring-1 ring-[var(--brand)]/30' : 'border-[var(--sidebar-border)] bg-[var(--surface)] hover:border-[var(--brand)]/40'"
|
|
@click="form.quoteType = opt.id as 'single' | 'comparative'"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--brand-faint)] text-[var(--brand)]">
|
|
<UIcon :name="opt.icon" class="h-5 w-5" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="font-semibold text-[var(--text-primary)]">{{ opt.label }}</p>
|
|
<p class="mt-1 text-sm text-[var(--text-muted)]">{{ opt.hint }}</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="space-y-4 border-t border-[var(--sidebar-border)] pt-10">
|
|
<div>
|
|
<h3 class="text-base font-semibold text-[var(--text-primary)]">Risk category</h3>
|
|
<p class="mt-1 text-sm text-[var(--text-muted)]">What type of risk are you quoting?</p>
|
|
</div>
|
|
<div class="grid gap-3 sm:grid-cols-3">
|
|
<button
|
|
v-for="cat in [
|
|
{ id: 'property', icon: 'i-heroicons-building-office-2' },
|
|
{ id: 'liability', icon: 'i-heroicons-shield-exclamation' },
|
|
{ id: 'fire', icon: 'i-heroicons-fire' },
|
|
{ id: 'all-risk', icon: 'i-heroicons-shield-check' },
|
|
{ id: 'commercial', icon: 'i-heroicons-building-storefront' },
|
|
{ id: 'other', icon: 'i-heroicons-ellipsis-horizontal-circle' }
|
|
]"
|
|
:key="cat.id" type="button"
|
|
class="rounded-xl border p-4 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]"
|
|
:class="form.riskCategory === cat.id ? 'border-[var(--brand)] bg-[var(--brand-soft)] ring-1 ring-[var(--brand)]/30' : 'border-[var(--sidebar-border)] bg-[var(--surface)] hover:border-[var(--brand)]/40'"
|
|
@click="form.riskCategory = cat.id as typeof form.riskCategory"
|
|
>
|
|
<UIcon :name="cat.icon" class="h-7 w-7 text-[var(--brand)]" />
|
|
<p class="mt-2 text-sm font-semibold text-[var(--text-primary)]">{{ riskCategoryLabel[cat.id] }}</p>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="space-y-4 border-t border-[var(--sidebar-border)] pt-10">
|
|
<h3 class="text-base font-semibold text-[var(--text-primary)]">Client information</h3>
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label :class="labelClass">Full name</label>
|
|
<input v-model="form.clientName" type="text" :class="inputClass" placeholder="Jane Doe" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Email</label>
|
|
<input v-model="form.clientEmail" type="email" :class="inputClass" placeholder="jane@example.com" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Phone</label>
|
|
<input v-model="form.clientPhone" type="tel" :class="inputClass" placeholder="+27 82 000 0000" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Organization</label>
|
|
<input v-model="form.clientOrganization" type="text" :class="inputClass" placeholder="Acme Corp" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<!-- Step 2: Risk details -->
|
|
<div v-else-if="step === 'details'" class="space-y-6">
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div class="sm:col-span-2">
|
|
<label :class="labelClass">Property / risk address</label>
|
|
<input v-model="form.propertyAddress" type="text" :class="inputClass" placeholder="123 Main St, Johannesburg" />
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<label :class="labelClass">Risk description</label>
|
|
<textarea v-model="form.propertyDescription" :class="inputClass" rows="3" placeholder="Describe the property or risk in detail" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Sum insured (ZAR)</label>
|
|
<input v-model="form.sumInsured" type="number" :class="inputClass" placeholder="1 000 000" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Deductible (ZAR)</label>
|
|
<input v-model="form.deductible" type="number" :class="inputClass" placeholder="10 000" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Coverage start</label>
|
|
<input v-model="form.coverageStart" type="date" :class="inputClass" />
|
|
</div>
|
|
<div>
|
|
<label :class="labelClass">Coverage end</label>
|
|
<input v-model="form.coverageEnd" type="date" :class="inputClass" />
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<label :class="labelClass">Special conditions</label>
|
|
<textarea v-model="form.specialConditions" :class="inputClass" rows="3" placeholder="Any special conditions, endorsements, or exclusions" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Review & submit -->
|
|
<div v-else-if="step === 'review'" class="space-y-5">
|
|
<div class="rounded-xl border border-[var(--sidebar-border)] bg-[var(--surface)] p-4">
|
|
<p class="text-sm font-semibold text-[var(--text-muted)] mb-3">Quote setup</p>
|
|
<dl class="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-2">
|
|
<div><dt class="text-[var(--text-muted)]">Quote type</dt><dd class="font-medium text-[var(--text-primary)] capitalize">{{ form.quoteType || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Risk category</dt><dd class="font-medium text-[var(--text-primary)]">{{ riskCategoryLabel[form.riskCategory] || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Client</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.clientName || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Email</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.clientEmail || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Phone</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.clientPhone || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Organization</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.clientOrganization || '—' }}</dd></div>
|
|
</dl>
|
|
</div>
|
|
<div class="rounded-xl border border-[var(--sidebar-border)] bg-[var(--surface)] p-4">
|
|
<p class="text-sm font-semibold text-[var(--text-muted)] mb-3">Risk details</p>
|
|
<dl class="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-2">
|
|
<div class="sm:col-span-2"><dt class="text-[var(--text-muted)]">Address</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.propertyAddress || '—' }}</dd></div>
|
|
<div class="sm:col-span-2"><dt class="text-[var(--text-muted)]">Description</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.propertyDescription || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Sum insured</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.sumInsured ? `R ${Number(form.sumInsured).toLocaleString()}` : '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Deductible</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.deductible ? `R ${Number(form.deductible).toLocaleString()}` : '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Coverage start</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.coverageStart || '—' }}</dd></div>
|
|
<div><dt class="text-[var(--text-muted)]">Coverage end</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.coverageEnd || '—' }}</dd></div>
|
|
<div v-if="form.specialConditions" class="sm:col-span-2"><dt class="text-[var(--text-muted)]">Special conditions</dt><dd class="font-medium text-[var(--text-primary)]">{{ form.specialConditions }}</dd></div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</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 !== 'review'" type="button" color="primary" @click="goNext">Continue</UButton>
|
|
<UButton
|
|
v-else
|
|
type="button"
|
|
color="primary"
|
|
:loading="busy"
|
|
:disabled="busy"
|
|
@click="finalize"
|
|
>
|
|
Finalize quote
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|