1002 lines
33 KiB
Vue
1002 lines
33 KiB
Vue
<script setup lang="ts">
|
|
import type { QuoteComparativeView } from '~/types/quote-view-model'
|
|
import QuoteComparativeLayout from '~/components/quotes/QuoteComparativeLayout.vue'
|
|
|
|
definePageMeta({ ssr: false })
|
|
usePageTitle('Present Quotes')
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
/* ── Pipeline bar ── */
|
|
const { deals: allDeals } = useSalesPipeline()
|
|
const activeDealId = ref<string | null>(route.query.deal as string | null)
|
|
const activeDeals = computed(() => allDeals.value.filter(d => d.currentStage !== 'emission').slice(0, 10))
|
|
const pipelineDeal = computed(() => {
|
|
if (activeDealId.value) return allDeals.value.find(d => d.id === activeDealId.value) ?? null
|
|
return null
|
|
})
|
|
function onPipelineNavigate(stage: string) {
|
|
const stageRoutes: Record<string, string> = {
|
|
customer: '/quotes/new', get_quotes: '/quotes/new',
|
|
present_quotes: '/quotes/compare', solicitud: '/onboarding/solicitud', emission: '/onboarding/emissions',
|
|
}
|
|
if (stageRoutes[stage]) navigateTo(stageRoutes[stage])
|
|
}
|
|
|
|
/* ── Presentation mode ── */
|
|
type PresentMode = 'comparative' | 'single'
|
|
const presentMode = ref<PresentMode>('comparative')
|
|
const intakeSegment = computed(() => {
|
|
const s = route.query.segment
|
|
return typeof s === 'string' ? s : null
|
|
})
|
|
const fromAutoIntake = computed(() => route.query.from === 'auto')
|
|
const fromHealthIntake = computed(() => route.query.from === 'health')
|
|
|
|
const toast = useToast()
|
|
const { view, reset } = useQuoteSession()
|
|
const { enqueue } = useEmissionsQueue()
|
|
const pdfNote = ref('')
|
|
const pdfInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
function onPdfPick(e: Event) {
|
|
const f = (e.target as HTMLInputElement).files?.[0]
|
|
if (!f) return
|
|
if (f.type !== 'application/pdf') {
|
|
toast.add({ title: 'Please choose a PDF', color: 'error' })
|
|
return
|
|
}
|
|
pdfNote.value = `${f.name} (${(f.size / 1024).toFixed(1)} KB) — extraction not wired; use manual edit below.`
|
|
toast.add({
|
|
title: 'PDF attached',
|
|
description: 'Illustration parsing can populate the view model when the service is available.',
|
|
color: 'success'
|
|
})
|
|
}
|
|
|
|
function patchClient(field: keyof QuoteComparativeView['client'], value: string | number | boolean) {
|
|
view.value = {
|
|
...view.value,
|
|
client: { ...view.value.client, [field]: value }
|
|
}
|
|
}
|
|
|
|
function patchRequest(field: keyof QuoteComparativeView['request'], value: number | string) {
|
|
view.value = {
|
|
...view.value,
|
|
request: { ...view.value.request, [field]: value }
|
|
}
|
|
}
|
|
|
|
/* ── Single proposal mode ── */
|
|
const singleCarrierIdx = ref<number>(0)
|
|
const singleCarrier = computed(() => view.value.carriers[singleCarrierIdx.value] ?? view.value.carriers[0] ?? null)
|
|
|
|
/* ── Broker intake link (manual path) ── */
|
|
const bindToken = ref('')
|
|
|
|
function generateIntakeLink() {
|
|
const token = crypto.randomUUID?.() ?? String(Date.now())
|
|
bindToken.value = token
|
|
const url = `${typeof window !== 'undefined' ? window.location.origin : ''}/onboarding/solicitud?bind=${encodeURIComponent(token)}`
|
|
navigator.clipboard?.writeText(url).catch(() => {})
|
|
toast.add({
|
|
title: 'Intake link copied',
|
|
description: 'Share with the customer after they accept a quote.',
|
|
color: 'success'
|
|
})
|
|
}
|
|
|
|
/* ── Quote acceptance flow ── */
|
|
type AcceptanceState = 'pending' | 'selecting' | 'accepted'
|
|
const acceptState = ref<AcceptanceState>('pending')
|
|
const selectedCarrierIdx = ref<number | null>(null)
|
|
const acceptedAt = ref<string | null>(null)
|
|
const generatedEmissionIds = ref<string[]>([])
|
|
|
|
/** Which carrier the customer chose */
|
|
const acceptedCarrier = computed(() => {
|
|
if (selectedCarrierIdx.value === null) return null
|
|
return view.value.carriers[selectedCarrierIdx.value] ?? null
|
|
})
|
|
|
|
function beginAcceptance() {
|
|
acceptState.value = 'selecting'
|
|
selectedCarrierIdx.value = null
|
|
}
|
|
|
|
function cancelAcceptance() {
|
|
acceptState.value = 'pending'
|
|
selectedCarrierIdx.value = null
|
|
}
|
|
|
|
function confirmAcceptance() {
|
|
if (selectedCarrierIdx.value === null) {
|
|
toast.add({ title: 'Select the accepted option first', color: 'error' })
|
|
return
|
|
}
|
|
|
|
const carrier = view.value.carriers[selectedCarrierIdx.value]
|
|
if (!carrier) return
|
|
|
|
// Mark as accepted
|
|
acceptState.value = 'accepted'
|
|
acceptedAt.value = new Date().toISOString()
|
|
|
|
// Generate bind token
|
|
const token = crypto.randomUUID?.() ?? String(Date.now())
|
|
bindToken.value = token
|
|
|
|
// Auto-enqueue to emissions with source = 'auto'
|
|
const row = enqueue({
|
|
customerLabel: view.value.client.name || 'Customer',
|
|
insurerSlug: carrier.carrierName,
|
|
subRamoKey: 'life', // derived from quote context
|
|
productLine: carrier.productName,
|
|
bindToken: token,
|
|
source: 'auto',
|
|
carrierProduct: carrier.productName
|
|
})
|
|
|
|
generatedEmissionIds.value = [row.id]
|
|
|
|
toast.add({
|
|
title: 'Quote accepted — solicitud auto-generated',
|
|
description: `${carrier.productName} → emissions queue`,
|
|
color: 'success'
|
|
})
|
|
}
|
|
|
|
function goToEmissions() {
|
|
router.push('/onboarding/emissions')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="cmp mx-auto max-w-5xl space-y-6 pb-12">
|
|
<!-- Back -->
|
|
<div class="flex items-center justify-between">
|
|
<NuxtLink to="/quotes" class="inline-flex">
|
|
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Back to overview</UButton>
|
|
</NuxtLink>
|
|
<UButton color="neutral" variant="soft" size="sm" @click="reset()">Reset demo data</UButton>
|
|
</div>
|
|
|
|
<!-- Sales flow indicator -->
|
|
<SalesFlowIndicator current-stage="present_quotes" />
|
|
|
|
<UAlert
|
|
v-if="fromAutoIntake"
|
|
color="info"
|
|
variant="soft"
|
|
title="Ficha de auto"
|
|
:description="
|
|
intakeSegment
|
|
? `Continúe el PDF comparativo — segmento: ${intakeSegment}. Los números de abajo se pueden alinear con la ficha cuando exista API.`
|
|
: 'Continúe el PDF comparativo con los datos capturados en cotización de auto.'
|
|
"
|
|
/>
|
|
<UAlert
|
|
v-if="fromHealthIntake"
|
|
color="info"
|
|
variant="soft"
|
|
title="Health comparative"
|
|
:description="
|
|
intakeSegment
|
|
? `Continue the comparative PDF — segment: ${intakeSegment}. Align premiums with census and age-band tables when your integration is available.`
|
|
: 'Continue the comparative PDF with data captured from the health quote flow.'
|
|
"
|
|
/>
|
|
|
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Present Quotes</h1>
|
|
<p class="mt-1 max-w-2xl text-[13px] text-[var(--text-muted)]">
|
|
Build a single proposal or side-by-side comparative to present to the customer. Edit manually or attach a carrier PDF for future auto-fill.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pipeline bar -->
|
|
<div v-if="activeDeals.length > 0">
|
|
<div v-if="!pipelineDeal" class="cmp-deal-picker">
|
|
<div class="flex items-center gap-2 text-[12px] text-[var(--text-muted)]">
|
|
<UIcon name="i-heroicons-arrow-path" style="width: 13px; height: 13px; opacity: 0.5;" />
|
|
<span class="font-medium">Continue an active deal:</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2 mt-2">
|
|
<button v-for="d in activeDeals" :key="d.id" type="button" class="cmp-deal-chip" @click="activeDealId = d.id">
|
|
<span class="font-semibold">{{ d.customerName.split(' ').slice(0, 2).join(' ') }}</span>
|
|
<span style="font-size:10px;font-weight:600;padding:0 5px;border-radius:9999px;background:rgba(1,105,111,0.07);color:#01696f;">{{ d.productLine }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<template v-else>
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-[11px] font-semibold uppercase tracking-wider text-[#8a8a86]">Active Deal</span>
|
|
<button type="button" class="text-[11px] text-[var(--text-muted)] hover:text-[var(--text-primary)]" @click="activeDealId = null">Switch deal</button>
|
|
</div>
|
|
<SalesPipelineBar :deal="pipelineDeal" @navigate="onPipelineNavigate" />
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Presentation mode toggle -->
|
|
<div class="cmp-mode-bar">
|
|
<div class="cmp-mode-tabs">
|
|
<button
|
|
type="button"
|
|
class="cmp-mode-tab"
|
|
:class="presentMode === 'comparative' ? 'cmp-mode-on' : 'cmp-mode-off'"
|
|
@click="presentMode = 'comparative'"
|
|
>
|
|
<UIcon name="i-heroicons-table-cells" style="width: 14px; height: 14px;" />
|
|
Comparative
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="cmp-mode-tab"
|
|
:class="presentMode === 'single' ? 'cmp-mode-on' : 'cmp-mode-off'"
|
|
@click="presentMode = 'single'"
|
|
>
|
|
<UIcon name="i-heroicons-document-text" style="width: 14px; height: 14px;" />
|
|
Single Proposal
|
|
</button>
|
|
</div>
|
|
<p class="text-[11px] text-[var(--text-muted)]">
|
|
{{ presentMode === 'comparative'
|
|
? `${view.carriers.length} carriers side-by-side`
|
|
: 'One carrier, detailed breakdown'
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<span class="font-semibold text-[var(--text-primary)]">Ingest</span>
|
|
</template>
|
|
<div class="flex flex-wrap items-end gap-4">
|
|
<div>
|
|
<input
|
|
ref="pdfInputRef"
|
|
type="file"
|
|
accept="application/pdf"
|
|
class="hidden"
|
|
@change="onPdfPick"
|
|
/>
|
|
<UButton
|
|
color="primary"
|
|
variant="soft"
|
|
icon="i-heroicons-document-arrow-up"
|
|
@click="pdfInputRef?.click()"
|
|
>
|
|
Attach illustration PDF
|
|
</UButton>
|
|
</div>
|
|
<p v-if="pdfNote" class="text-xs text-[var(--text-muted)]">{{ pdfNote }}</p>
|
|
</div>
|
|
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
|
<UFormField label="Cliente · nombre">
|
|
<UInput
|
|
:model-value="view.client.name"
|
|
class="w-full"
|
|
@update:model-value="patchClient('name', $event)"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Edad">
|
|
<UInput
|
|
type="number"
|
|
:model-value="view.client.ageYears"
|
|
class="w-full"
|
|
@update:model-value="patchClient('ageYears', Number($event))"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Suma asegurada (USD)">
|
|
<UInput
|
|
type="number"
|
|
:model-value="view.request.sumAssuredUsd"
|
|
class="w-full"
|
|
@update:model-value="patchRequest('sumAssuredUsd', Number($event))"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Prima mensual (USD)">
|
|
<UInput
|
|
type="number"
|
|
:model-value="view.request.monthlyPremiumUsd"
|
|
class="w-full"
|
|
@update:model-value="patchRequest('monthlyPremiumUsd', Number($event))"
|
|
/>
|
|
</UFormField>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- ═══ Comparative view ═══ -->
|
|
<div v-if="presentMode === 'comparative'" class="rounded-xl border border-[var(--card-border)] bg-[var(--surface)] p-4 shadow-sm md:p-8">
|
|
<QuoteComparativeLayout :model="view" />
|
|
</div>
|
|
|
|
<!-- ═══ Single proposal view ═══ -->
|
|
<div v-if="presentMode === 'single'" class="cmp-single-wrap">
|
|
<div class="cmp-single-head">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86] mb-2">Select carrier to present</p>
|
|
<div class="cmp-single-options">
|
|
<button
|
|
v-for="(carrier, idx) in view.carriers"
|
|
:key="idx"
|
|
type="button"
|
|
class="cmp-single-option"
|
|
:class="(singleCarrierIdx ?? 0) === idx ? 'cmp-single-selected' : ''"
|
|
@click="singleCarrierIdx = idx"
|
|
>
|
|
<span class="text-[13px] font-semibold text-[var(--text-primary)]">{{ carrier.productName }}</span>
|
|
<span class="text-[11px] text-[var(--text-muted)]">{{ carrier.carrierName }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Single carrier detail card -->
|
|
<div v-if="singleCarrier" class="cmp-single-card">
|
|
<div class="cmp-single-card-head">
|
|
<div>
|
|
<p class="text-[14px] font-semibold text-[var(--text-primary)]">{{ singleCarrier.productName }}</p>
|
|
<p class="text-[12px] text-[var(--text-muted)]">{{ singleCarrier.carrierName }}</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86]">Sum assured</p>
|
|
<p class="text-[18px] font-semibold tabular-nums text-[var(--text-primary)]">${{ singleCarrier.sumAssuredUsd?.toLocaleString() }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cmp-single-client">
|
|
<div class="cmp-single-stat">
|
|
<span class="cmp-single-stat-label">Client</span>
|
|
<span class="cmp-single-stat-value">{{ view.client.name }}</span>
|
|
</div>
|
|
<div class="cmp-single-stat">
|
|
<span class="cmp-single-stat-label">Age</span>
|
|
<span class="cmp-single-stat-value">{{ view.client.ageYears }}</span>
|
|
</div>
|
|
<div class="cmp-single-stat">
|
|
<span class="cmp-single-stat-label">Monthly premium</span>
|
|
<span class="cmp-single-stat-value">${{ view.request.monthlyPremiumUsd?.toLocaleString() }}</span>
|
|
</div>
|
|
<div class="cmp-single-stat">
|
|
<span class="cmp-single-stat-label">Annual premium</span>
|
|
<span class="cmp-single-stat-value">${{ view.request.annualPremiumUsd?.toLocaleString() }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="singleCarrier.ratesLine" class="cmp-single-rates">
|
|
<UIcon name="i-heroicons-information-circle" style="width: 14px; height: 14px; color: #01696f; flex-shrink: 0;" />
|
|
<span class="text-[12px] text-[var(--text-muted)]">{{ singleCarrier.ratesLine }}</span>
|
|
</div>
|
|
|
|
<!-- Projection table -->
|
|
<div v-if="singleCarrier.cells?.length" class="cmp-single-table-wrap">
|
|
<table class="cmp-single-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Period</th>
|
|
<th>Age</th>
|
|
<th class="text-right">Guaranteed</th>
|
|
<th class="text-right">Projected</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="cell in singleCarrier.cells" :key="cell.yearLabel">
|
|
<td class="font-medium">{{ cell.yearLabel }}</td>
|
|
<td>{{ cell.ageLabel }}</td>
|
|
<td class="text-right tabular-nums">${{ cell.guaranteed?.toLocaleString() }}</td>
|
|
<td class="text-right tabular-nums font-medium" style="color: #01696f;">${{ cell.projected?.toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div v-if="singleCarrier.highlightProjectedUsd" class="cmp-single-highlight">
|
|
<div>
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86]">Projected value</p>
|
|
<p class="text-[22px] font-semibold tabular-nums" style="color: #01696f;">${{ singleCarrier.highlightProjectedUsd?.toLocaleString() }}</p>
|
|
</div>
|
|
<p v-if="singleCarrier.highlightNote" class="text-[12px] text-[var(--text-muted)] mt-1">{{ singleCarrier.highlightNote }}</p>
|
|
</div>
|
|
|
|
<div v-if="singleCarrier.footnote" class="cmp-single-footnote">
|
|
{{ singleCarrier.footnote }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════ -->
|
|
<!-- ACCEPTANCE FLOW — the core of the sales pipeline -->
|
|
<!-- ═══════════════════════════════════════════════════ -->
|
|
<div class="cmp-accept-section">
|
|
<div class="cmp-accept-head">
|
|
<div class="cmp-accept-icon" :class="acceptState === 'accepted' ? 'cmp-accept-icon-done' : ''">
|
|
<UIcon
|
|
:name="acceptState === 'accepted' ? 'i-heroicons-check-circle' : 'i-heroicons-hand-thumb-up'"
|
|
style="width: 20px; height: 20px;"
|
|
/>
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-[14px] font-semibold text-[var(--text-primary)]">
|
|
{{ acceptState === 'accepted' ? 'Quote accepted' : 'Customer acceptance' }}
|
|
</p>
|
|
<p class="mt-0.5 text-[13px] text-[var(--text-muted)]">
|
|
{{ acceptState === 'accepted'
|
|
? 'Solicitud auto-generated and sent to emissions queue.'
|
|
: 'When the customer confirms an option, mark it as accepted to auto-generate the solicitud and push it to emissions.'
|
|
}}
|
|
</p>
|
|
</div>
|
|
<span v-if="acceptState === 'accepted'" class="cmp-accept-badge-done">
|
|
<UIcon name="i-heroicons-check" style="width: 10px; height: 10px;" />
|
|
Accepted
|
|
</span>
|
|
</div>
|
|
|
|
<!-- State: pending — just the button -->
|
|
<div v-if="acceptState === 'pending'" class="cmp-accept-body">
|
|
<div class="cmp-accept-flow-hint">
|
|
<div class="cmp-flow-step">
|
|
<span class="cmp-flow-num">1</span>
|
|
<span>Customer reviews comparative</span>
|
|
</div>
|
|
<div class="cmp-flow-arrow">
|
|
<UIcon name="i-heroicons-arrow-right" style="width: 12px; height: 12px;" />
|
|
</div>
|
|
<div class="cmp-flow-step">
|
|
<span class="cmp-flow-num">2</span>
|
|
<span>Customer accepts an option</span>
|
|
</div>
|
|
<div class="cmp-flow-arrow">
|
|
<UIcon name="i-heroicons-arrow-right" style="width: 12px; height: 12px;" />
|
|
</div>
|
|
<div class="cmp-flow-step">
|
|
<span class="cmp-flow-num">3</span>
|
|
<span>Solicitud auto-sent → Emissions</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 pt-2">
|
|
<button type="button" class="cmp-accept-btn" @click="beginAcceptance">
|
|
<UIcon name="i-heroicons-hand-thumb-up" style="width: 14px; height: 14px;" />
|
|
Mark as accepted
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- State: selecting — pick the carrier option -->
|
|
<div v-if="acceptState === 'selecting'" class="cmp-accept-body">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86] mb-3">Which option did the customer accept?</p>
|
|
<div class="cmp-carrier-options">
|
|
<button
|
|
v-for="(carrier, idx) in view.carriers"
|
|
:key="idx"
|
|
type="button"
|
|
class="cmp-carrier-option"
|
|
:class="selectedCarrierIdx === idx ? 'cmp-carrier-selected' : ''"
|
|
@click="selectedCarrierIdx = idx"
|
|
>
|
|
<div class="cmp-carrier-radio">
|
|
<div v-if="selectedCarrierIdx === idx" class="cmp-carrier-radio-dot" />
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-[13px] font-semibold text-[var(--text-primary)]">{{ carrier.productName }}</p>
|
|
<p class="text-[11px] text-[var(--text-muted)] truncate">{{ carrier.carrierName }}</p>
|
|
<div class="mt-1 flex items-center gap-3 text-[11px] text-[var(--text-muted)]">
|
|
<span>SA: ${{ carrier.sumAssuredUsd?.toLocaleString() }}</span>
|
|
<span v-if="carrier.highlightProjectedUsd">Proj: ${{ carrier.highlightProjectedUsd?.toLocaleString() }}</span>
|
|
</div>
|
|
</div>
|
|
<UIcon
|
|
v-if="selectedCarrierIdx === idx"
|
|
name="i-heroicons-check-circle"
|
|
style="width: 20px; height: 20px; color: #01696f; flex-shrink: 0;"
|
|
/>
|
|
</button>
|
|
</div>
|
|
<div class="flex gap-2 pt-3">
|
|
<button type="button" class="cmp-accept-btn" :class="selectedCarrierIdx === null ? 'cmp-btn-disabled' : ''" @click="confirmAcceptance">
|
|
<UIcon name="i-heroicons-paper-airplane" style="width: 14px; height: 14px;" />
|
|
Confirm acceptance & generate solicitud
|
|
</button>
|
|
<button type="button" class="cmp-cancel-btn" @click="cancelAcceptance">Cancel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- State: accepted — confirmation -->
|
|
<div v-if="acceptState === 'accepted'" class="cmp-accept-body">
|
|
<div class="cmp-accepted-summary">
|
|
<div class="cmp-accepted-row">
|
|
<span class="cmp-accepted-label">Customer</span>
|
|
<span class="cmp-accepted-value">{{ view.client.name }}</span>
|
|
</div>
|
|
<div class="cmp-accepted-row">
|
|
<span class="cmp-accepted-label">Accepted product</span>
|
|
<span class="cmp-accepted-value">{{ acceptedCarrier?.productName }}</span>
|
|
</div>
|
|
<div class="cmp-accepted-row">
|
|
<span class="cmp-accepted-label">Carrier</span>
|
|
<span class="cmp-accepted-value">{{ acceptedCarrier?.carrierName }}</span>
|
|
</div>
|
|
<div class="cmp-accepted-row">
|
|
<span class="cmp-accepted-label">Accepted at</span>
|
|
<span class="cmp-accepted-value">{{ acceptedAt?.slice(0, 16).replace('T', ' · ') }}</span>
|
|
</div>
|
|
<div class="cmp-accepted-row">
|
|
<span class="cmp-accepted-label">Bind token</span>
|
|
<span class="cmp-accepted-value font-mono text-[11px]">{{ bindToken }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="cmp-auto-sent-banner">
|
|
<UIcon name="i-heroicons-bolt" style="width: 16px; height: 16px; color: #01696f; flex-shrink: 0;" />
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-[13px] font-semibold text-[var(--text-primary)]">Solicitud auto-generated</p>
|
|
<p class="text-[12px] text-[var(--text-muted)]">The required forms have been queued for emissions review. The customer will receive the solicitud documents for signature.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2 pt-2">
|
|
<button type="button" class="cmp-accept-btn" @click="goToEmissions">
|
|
<UIcon name="i-heroicons-queue-list" style="width: 14px; height: 14px;" />
|
|
View in emissions queue
|
|
</button>
|
|
<NuxtLink :to="`/onboarding/solicitud?bind=${bindToken}`">
|
|
<button type="button" class="cmp-cancel-btn">
|
|
<UIcon name="i-heroicons-pencil-square" style="width: 12px; height: 12px;" />
|
|
Open solicitud form
|
|
</button>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Manual path (always available) ── -->
|
|
<div class="cmp-manual-card">
|
|
<div class="cmp-manual-head">
|
|
<UIcon name="i-heroicons-link" style="width: 16px; height: 16px; color: #8a8a86;" />
|
|
<span class="text-[13px] font-semibold text-[var(--text-primary)]">Manual solicitud path</span>
|
|
</div>
|
|
<div class="cmp-manual-body">
|
|
<p class="text-[13px] text-[var(--text-muted)]">
|
|
Need to handle things manually? Generate a broker intake link or go directly to the solicitud form. This path is always available regardless of acceptance status.
|
|
</p>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<button type="button" class="cmp-accept-btn cmp-btn-neutral" @click="generateIntakeLink">
|
|
<UIcon name="i-heroicons-link" style="width: 14px; height: 14px;" />
|
|
Copy intake link
|
|
</button>
|
|
<NuxtLink to="/onboarding/solicitud">
|
|
<button type="button" class="cmp-cancel-btn">
|
|
<UIcon name="i-heroicons-arrow-right" style="width: 12px; height: 12px;" />
|
|
Open blank solicitud
|
|
</button>
|
|
</NuxtLink>
|
|
<NuxtLink v-if="bindToken && acceptState !== 'accepted'" :to="`/onboarding/solicitud?bind=${bindToken}`" target="_blank">
|
|
<button type="button" class="cmp-cancel-btn">Open with bind token</button>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── Deal picker ── */
|
|
.cmp-deal-picker {
|
|
padding: 12px 16px; border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.cmp-deal-chip {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 6px 12px; border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
|
font-size: 12px; color: var(--text-primary);
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.cmp-deal-chip:hover { border-color: rgba(1,105,111,0.2); box-shadow: 0 1px 4px rgba(0,0,0,0.04); }
|
|
|
|
/* ── Acceptance section ── */
|
|
.cmp-accept-section {
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
overflow: hidden;
|
|
}
|
|
.cmp-accept-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cmp-accept-icon {
|
|
width: 36px; height: 36px;
|
|
border-radius: 10px;
|
|
background: rgba(1,105,111,0.06);
|
|
color: #01696f;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.cmp-accept-icon-done {
|
|
background: rgba(15,123,95,0.08);
|
|
color: #0f7b5f;
|
|
}
|
|
.cmp-accept-body {
|
|
padding: 20px;
|
|
}
|
|
.cmp-accept-badge-done {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 3px 10px;
|
|
border-radius: 9999px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
background: rgba(15,123,95,0.08);
|
|
color: #0f7b5f;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ── Flow hint ── */
|
|
.cmp-accept-flow-hint {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
padding: 12px 14px;
|
|
border-radius: 8px;
|
|
background: rgba(0,0,0,0.015);
|
|
border: 1px solid rgba(0,0,0,0.04);
|
|
}
|
|
.cmp-flow-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cmp-flow-num {
|
|
width: 20px; height: 20px;
|
|
border-radius: 50%;
|
|
background: rgba(1,105,111,0.08);
|
|
color: #01696f;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.cmp-flow-arrow {
|
|
color: #c0c0bc;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
/* ── Carrier option cards ── */
|
|
.cmp-carrier-options {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
.cmp-carrier-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
text-align: left;
|
|
}
|
|
.cmp-carrier-option:hover {
|
|
border-color: rgba(1,105,111,0.15);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
}
|
|
.cmp-carrier-selected {
|
|
border-color: #01696f;
|
|
background: rgba(1,105,111,0.015);
|
|
box-shadow: 0 0 0 1px #01696f;
|
|
}
|
|
.cmp-carrier-radio {
|
|
width: 18px; height: 18px;
|
|
border-radius: 50%;
|
|
border: 2px solid rgba(0,0,0,0.15);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: border-color 150ms ease;
|
|
}
|
|
.cmp-carrier-selected .cmp-carrier-radio {
|
|
border-color: #01696f;
|
|
}
|
|
.cmp-carrier-radio-dot {
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
background: #01696f;
|
|
}
|
|
|
|
/* ── Buttons ── */
|
|
.cmp-accept-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
background: #01696f;
|
|
color: #fff;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.cmp-accept-btn:hover { background: #015458; }
|
|
.cmp-btn-disabled {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
.cmp-btn-neutral {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
.cmp-btn-neutral:hover {
|
|
border-color: rgba(0,0,0,0.2);
|
|
background: rgba(0,0,0,0.02);
|
|
}
|
|
.cmp-cancel-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.cmp-cancel-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
|
|
/* ── Accepted summary ── */
|
|
.cmp-accepted-summary {
|
|
display: grid;
|
|
gap: 6px;
|
|
padding: 14px 16px;
|
|
border-radius: 8px;
|
|
background: rgba(15,123,95,0.02);
|
|
border: 1px solid rgba(15,123,95,0.08);
|
|
}
|
|
.cmp-accepted-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
}
|
|
.cmp-accepted-label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: #8a8a86;
|
|
min-width: 120px;
|
|
flex-shrink: 0;
|
|
}
|
|
.cmp-accepted-value {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
.cmp-auto-sent-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-top: 12px;
|
|
padding: 12px 14px;
|
|
border-radius: 8px;
|
|
background: rgba(1,105,111,0.03);
|
|
border: 1px solid rgba(1,105,111,0.1);
|
|
}
|
|
|
|
/* ── Manual card ── */
|
|
.cmp-manual-card {
|
|
border-radius: 12px;
|
|
border: 1px dashed rgba(0,0,0,0.1);
|
|
background: rgba(0,0,0,0.01);
|
|
overflow: hidden;
|
|
}
|
|
.cmp-manual-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 20px;
|
|
border-bottom: 1px dashed rgba(0,0,0,0.06);
|
|
}
|
|
.cmp-manual-body {
|
|
padding: 16px 20px;
|
|
}
|
|
|
|
/* ── Process note ── */
|
|
.cmp-process-note {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 12px 16px;
|
|
border-radius: 10px;
|
|
background: rgba(124,58,237,0.03);
|
|
border: 1px solid rgba(124,58,237,0.1);
|
|
}
|
|
|
|
/* ── Mode toggle ── */
|
|
.cmp-mode-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cmp-mode-tabs {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
background: rgba(0,0,0,0.04);
|
|
}
|
|
.cmp-mode-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 7px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.cmp-mode-on {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.cmp-mode-off {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
}
|
|
.cmp-mode-off:hover { color: var(--text-primary); }
|
|
|
|
/* ── Single proposal ── */
|
|
.cmp-single-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.cmp-single-head {}
|
|
.cmp-single-options {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cmp-single-option {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 10px 16px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
text-align: left;
|
|
}
|
|
.cmp-single-option:hover {
|
|
border-color: rgba(1,105,111,0.15);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
}
|
|
.cmp-single-selected {
|
|
border-color: #01696f;
|
|
background: rgba(1,105,111,0.015);
|
|
box-shadow: 0 0 0 1px #01696f;
|
|
}
|
|
|
|
.cmp-single-card {
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
overflow: hidden;
|
|
}
|
|
.cmp-single-card-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cmp-single-client {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1px;
|
|
background: rgba(0,0,0,0.04);
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
@media (max-width: 640px) { .cmp-single-client { grid-template-columns: repeat(2, 1fr); } }
|
|
.cmp-single-stat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 14px 20px;
|
|
background: #fff;
|
|
}
|
|
.cmp-single-stat-label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
}
|
|
.cmp-single-stat-value {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.cmp-single-rates {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 24px;
|
|
background: rgba(1,105,111,0.02);
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cmp-single-table-wrap {
|
|
padding: 0;
|
|
overflow-x: auto;
|
|
}
|
|
.cmp-single-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.cmp-single-table th {
|
|
text-align: left;
|
|
padding: 10px 24px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cmp-single-table td {
|
|
padding: 12px 24px;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
}
|
|
.cmp-single-table tr:last-child td { border-bottom: none; }
|
|
.cmp-single-table tr:hover td { background: rgba(0,0,0,0.01); }
|
|
|
|
.cmp-single-highlight {
|
|
padding: 20px 24px;
|
|
border-top: 1px solid rgba(0,0,0,0.06);
|
|
background: rgba(1,105,111,0.015);
|
|
}
|
|
.cmp-single-footnote {
|
|
padding: 12px 24px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
border-top: 1px solid rgba(0,0,0,0.06);
|
|
background: rgba(0,0,0,0.01);
|
|
}
|
|
</style>
|