WIP jordan
This commit is contained in:
311
app/pages/onboarding/solicitud.vue
Normal file
311
app/pages/onboarding/solicitud.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user