Files
policy-ui/app/pages/onboarding/solicitud.vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

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>