312 lines
12 KiB
Vue
312 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import type { FormCatalogProductLine, FormCatalogSelection } from '~/types/form-catalog'
|
|
import { useFormsCatalog } from '~/composables/useFormsCatalog'
|
|
|
|
definePageMeta({ ssr: false })
|
|
usePageTitle('Nueva solicitud')
|
|
|
|
const route = useRoute()
|
|
const toast = useToast()
|
|
|
|
/* ── 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])
|
|
}
|
|
|
|
const {
|
|
filterRows: resolveForms,
|
|
insurerItems,
|
|
subRamoItems,
|
|
productLineItems,
|
|
fieldGroupsForMatched
|
|
} = useFormsCatalog()
|
|
|
|
const { profile, touch } = useCustomerProfileVault()
|
|
const { enqueue } = useEmissionsQueue()
|
|
|
|
const insurerSlug = ref<string | null>(null)
|
|
const subRamoKey = ref<string | null>(null)
|
|
const personKind = ref<'natural' | 'juridica'>('natural')
|
|
const productLine = ref<FormCatalogProductLine | 'any'>('any')
|
|
|
|
const subRamoOptions = computed(() => subRamoItems(insurerSlug.value))
|
|
|
|
watch(insurerSlug, () => {
|
|
subRamoKey.value = null
|
|
})
|
|
|
|
const bindToken = computed(() => {
|
|
const b = route.query.bind
|
|
return typeof b === 'string' ? b : null
|
|
})
|
|
|
|
const selection = computed(
|
|
(): FormCatalogSelection => ({
|
|
insurerSlug: insurerSlug.value,
|
|
subRamoKey: subRamoKey.value,
|
|
personKind: personKind.value,
|
|
productLine: productLine.value
|
|
})
|
|
)
|
|
|
|
const matchedForms = computed(() => resolveForms(selection.value))
|
|
const fieldGroups = computed(() => fieldGroupsForMatched(matchedForms.value))
|
|
|
|
const personItems = [
|
|
{ label: 'Natural', value: 'natural' as const },
|
|
{ label: 'Jurídica', value: 'juridica' as const }
|
|
]
|
|
|
|
async function copyLabel(label: string) {
|
|
try {
|
|
await navigator.clipboard.writeText(label)
|
|
toast.add({ title: 'Copied', color: 'success' })
|
|
} catch {
|
|
toast.add({ title: 'Could not copy', color: 'error' })
|
|
}
|
|
}
|
|
|
|
const draftSavedAt = ref<string | null>(null)
|
|
|
|
function saveProfileDraft() {
|
|
touch()
|
|
draftSavedAt.value = new Date().toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
|
|
toast.add({ title: 'Profile draft saved locally', color: 'success' })
|
|
}
|
|
|
|
function submitToEmissions() {
|
|
if (!insurerSlug.value || !subRamoKey.value) {
|
|
toast.add({ title: 'Select insurer and sub-ramo', color: 'error' })
|
|
return
|
|
}
|
|
enqueue({
|
|
customerLabel: profile.value.full_name || 'Customer',
|
|
insurerSlug: insurerSlug.value,
|
|
subRamoKey: subRamoKey.value,
|
|
productLine: String(productLine.value),
|
|
bindToken: bindToken.value ?? undefined
|
|
})
|
|
toast.add({ title: 'Added to emissions queue', color: 'success' })
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="sol mx-auto max-w-5xl space-y-6 pb-12">
|
|
<!-- Back -->
|
|
<NuxtLink to="/onboarding" class="inline-flex">
|
|
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Pipeline</UButton>
|
|
</NuxtLink>
|
|
|
|
<!-- Sales flow indicator -->
|
|
<SalesFlowIndicator current-stage="solicitud" />
|
|
|
|
<UAlert
|
|
v-if="bindToken"
|
|
color="info"
|
|
variant="soft"
|
|
title="Broker intake link"
|
|
:description="`Bind token: ${bindToken}`"
|
|
/>
|
|
|
|
<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)]">New Solicitud</h1>
|
|
<p class="mt-1 max-w-2xl text-[13px] text-[var(--text-muted)]">
|
|
Choose insurer, sub-ramo, person type, and product line. Required forms come from the
|
|
<NuxtLink to="/settings/forms" class="text-[#01696f] hover:underline">forms library</NuxtLink>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pipeline bar -->
|
|
<div v-if="activeDeals.length > 0">
|
|
<div v-if="!pipelineDeal" style="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);">
|
|
<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" style="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;cursor:pointer;" @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>
|
|
|
|
<div class="sol-card">
|
|
<div class="sol-card-head">
|
|
<UIcon name="i-heroicons-adjustments-horizontal" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Selection</span>
|
|
</div>
|
|
<div class="sol-card-body grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<UFormField label="Aseguradora" required>
|
|
<USelect
|
|
v-model="insurerSlug"
|
|
:items="insurerItems"
|
|
value-key="value"
|
|
label-key="label"
|
|
placeholder="Select…"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Sub-ramo" required>
|
|
<USelect
|
|
v-model="subRamoKey"
|
|
:items="subRamoOptions"
|
|
value-key="value"
|
|
label-key="label"
|
|
placeholder="Choose insurer first"
|
|
:disabled="!insurerSlug"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
<UFormField label="Tipo de persona">
|
|
<USelect v-model="personKind" :items="personItems" value-key="value" label-key="label" class="w-full" />
|
|
</UFormField>
|
|
<UFormField
|
|
label="Product line"
|
|
description="Required for health (local/intl) and auto (full vs DAT). Use “Any” for generic rows only."
|
|
>
|
|
<USelect
|
|
v-model="productLine"
|
|
:items="productLineItems"
|
|
value-key="value"
|
|
label-key="label"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="fieldGroups.length" class="sol-card">
|
|
<div class="sol-card-head">
|
|
<UIcon name="i-heroicons-rectangle-group" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Field groups (review / autofill)</span>
|
|
</div>
|
|
<div class="sol-card-body space-y-6">
|
|
<div v-for="g in fieldGroups" :key="g.id" class="rounded-lg border border-[rgba(0,0,0,0.06)] bg-[rgba(0,0,0,0.015)] p-4">
|
|
<h3 class="text-[13px] font-semibold text-[var(--text-primary)]">{{ g.title }}</h3>
|
|
<p class="text-[11px] text-[var(--text-muted)]">{{ g.description }}</p>
|
|
<p class="mt-2 font-mono text-[10px] text-[var(--text-muted)] opacity-70">Keys: {{ g.fieldKeys.join(', ') }}</p>
|
|
</div>
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<UFormField label="Nombre completo (profile)">
|
|
<UInput v-model="profile.full_name" class="w-full" />
|
|
</UFormField>
|
|
<UFormField label="Documento ID">
|
|
<UInput v-model="profile.document_id" class="w-full" />
|
|
</UFormField>
|
|
<UFormField label="Placa (auto)">
|
|
<UInput v-model="profile.plate" class="w-full" />
|
|
</UFormField>
|
|
<UFormField label="Valor declarado">
|
|
<UInput v-model="profile.declared_value" class="w-full" />
|
|
</UFormField>
|
|
</div>
|
|
<UButton color="neutral" variant="soft" size="sm" @click="saveProfileDraft">Save profile draft</UButton>
|
|
<span v-if="draftSavedAt" class="text-[11px] text-emerald-600 font-medium">
|
|
<UIcon name="i-heroicons-check-circle" style="width: 13px; height: 13px; vertical-align: -2px;" /> Saved at {{ draftSavedAt }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sol-card">
|
|
<div class="sol-card-head">
|
|
<UIcon name="i-heroicons-document-text" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Forms to complete</span>
|
|
<span class="ml-auto text-[11px] font-medium text-[var(--text-muted)]">{{ matchedForms.length }} forms</span>
|
|
</div>
|
|
<div class="sol-card-body">
|
|
<div v-if="!insurerSlug || !subRamoKey" class="text-[13px] text-[var(--text-muted)] py-2">
|
|
Select insurer and sub-ramo to list required templates.
|
|
</div>
|
|
<div v-else-if="matchedForms.length === 0" class="text-[13px] text-amber-700 py-2">
|
|
No rows match this combination. Try another product line.
|
|
</div>
|
|
<div v-else class="space-y-2">
|
|
<div
|
|
v-for="row in matchedForms"
|
|
:key="row.id"
|
|
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-[rgba(0,0,0,0.06)] bg-[rgba(0,0,0,0.015)] px-4 py-3"
|
|
>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="font-mono text-[13px] font-semibold text-[var(--text-primary)]">{{ row.id }}</p>
|
|
<p class="truncate text-[11px] text-[var(--text-muted)]">{{ row.description }}</p>
|
|
<a
|
|
:href="row.fileUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="mt-1 inline-block break-all text-[12px] text-[#01696f] hover:underline"
|
|
>
|
|
{{ row.fileLabel }}
|
|
</a>
|
|
</div>
|
|
<div class="flex shrink-0 items-center gap-2">
|
|
<UButton
|
|
v-if="row.kind === 'identity'"
|
|
icon="i-heroicons-document-duplicate"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="xs"
|
|
aria-label="Copy file name"
|
|
@click="copyLabel(row.fileLabel)"
|
|
/>
|
|
<span v-if="row.badge != null" class="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-[rgba(1,105,111,0.08)] text-[#01696f]">{{ row.badge }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="matchedForms.length" class="sol-card-footer">
|
|
<NuxtLink to="/onboarding/emissions">
|
|
<UButton color="neutral" variant="soft" size="sm">Open emissions queue</UButton>
|
|
</NuxtLink>
|
|
<UButton color="primary" size="sm" icon="i-heroicons-paper-airplane" @click="submitToEmissions">
|
|
Send to emissions review
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.sol-section-label {
|
|
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.06em; color: #8a8a86; margin-bottom: 4px;
|
|
}
|
|
.sol-card {
|
|
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
|
|
background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
overflow: hidden;
|
|
}
|
|
.sol-card-head {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 14px 20px; border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
font-size: 13px; font-weight: 600; color: var(--text-primary);
|
|
}
|
|
.sol-card-body { padding: 20px; }
|
|
.sol-card-footer {
|
|
display: flex; justify-content: flex-end; gap: 8px;
|
|
padding: 14px 20px; border-top: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
</style>
|