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

252 lines
12 KiB
Vue

<script setup lang="ts">
usePageTitle('Sales Pipeline')
type Stage = 'Lead' | 'Qualified' | 'Proposal' | 'Negotiation' | 'Won'
interface Deal {
id: string
customer: string
product: string
line: 'Auto' | 'Health' | 'Life' | 'General Risk' | 'Custom'
premium: number
agent: string
stage: Stage
daysInStage: number
urgent: boolean
note?: string
}
const deals: Deal[] = [
{ id: 'D-001', customer: 'María Pérez', product: 'Auto Individual', line: 'Auto', premium: 1200, agent: 'Ana R.', stage: 'Lead', daysInStage: 1, urgent: false },
{ id: 'D-002', customer: 'Empresa ABC S.A.', product: 'Fleet Auto', line: 'Auto', premium: 18400, agent: 'Carlos M.', stage: 'Lead', daysInStage: 3, urgent: false },
{ id: 'D-003', customer: 'Jorge Herrera', product: 'Vida Individual', line: 'Life', premium: 3200, agent: 'Ana R.', stage: 'Lead', daysInStage: 0, urgent: false, note: 'Referral from client D-051' },
{ id: 'D-004', customer: 'Clínica San José', product: 'Salud Grupal', line: 'Health', premium: 42000, agent: 'Luis F.', stage: 'Qualified', daysInStage: 5, urgent: false },
{ id: 'D-005', customer: 'Carmen Ruiz', product: 'Salud Individual', line: 'Health', premium: 2800, agent: 'Ana R.', stage: 'Qualified', daysInStage: 2, urgent: false },
{ id: 'D-006', customer: 'Constructora Delta', product: 'Todo Riesgo', line: 'General Risk', premium: 55000, agent: 'Carlos M.', stage: 'Qualified', daysInStage: 7, urgent: true, note: 'RFQ deadline Friday' },
{ id: 'D-007', customer: 'Rodrigo Blanco', product: 'Vida + Accidentes', line: 'Life', premium: 4100, agent: 'Luis F.', stage: 'Qualified', daysInStage: 4, urgent: false },
{ id: 'D-008', customer: 'Hotel Pacífico', product: 'Incendio y Robo', line: 'General Risk', premium: 28000, agent: 'Carlos M.', stage: 'Proposal', daysInStage: 6, urgent: false },
{ id: 'D-009', customer: 'Supermercado Tico', product: 'Responsabilidad Civil', line: 'General Risk', premium: 9800, agent: 'Ana R.', stage: 'Proposal', daysInStage: 9, urgent: true, note: 'Follow up required today' },
{ id: 'D-010', customer: 'Isabel Mora', product: 'Auto Individual', line: 'Auto', premium: 980, agent: 'Luis F.', stage: 'Proposal', daysInStage: 3, urgent: false },
{ id: 'D-011', customer: 'Banco Regional', product: 'Colectivo Vida', line: 'Life', premium: 120000, agent: 'Carlos M.', stage: 'Negotiation', daysInStage: 14, urgent: true, note: 'Legal review pending' },
{ id: 'D-012', customer: 'Farmacia Salud', product: 'Todo Riesgo', line: 'General Risk', premium: 17500, agent: 'Luis F.', stage: 'Negotiation', daysInStage: 8, urgent: false },
{ id: 'D-013', customer: 'Andrea Cascante', product: 'Salud Individual', line: 'Health', premium: 2200, agent: 'Ana R.', stage: 'Won', daysInStage: 0, urgent: false },
{ id: 'D-014', customer: 'Transportes del Sur', product: 'Fleet Auto', line: 'Auto', premium: 24000, agent: 'Carlos M.', stage: 'Won', daysInStage: 0, urgent: false },
{ id: 'D-015', customer: 'Manuel Torres', product: 'Vida Individual', line: 'Life', premium: 5600, agent: 'Luis F.', stage: 'Won', daysInStage: 0, urgent: false },
]
const search = ref('')
const filterLine = ref<string>('all')
const filterAgent = ref<string>('all')
const lineOptions = [
{ label: 'All Lines', value: 'all' },
{ label: 'Auto', value: 'Auto' },
{ label: 'Health', value: 'Health' },
{ label: 'Life', value: 'Life' },
{ label: 'General Risk', value: 'General Risk' },
{ label: 'Custom', value: 'Custom' },
]
const agentOptions = [
{ label: 'All Agents', value: 'all' },
{ label: 'Ana R.', value: 'Ana R.' },
{ label: 'Carlos M.', value: 'Carlos M.' },
{ label: 'Luis F.', value: 'Luis F.' },
]
const stages: Stage[] = ['Lead', 'Qualified', 'Proposal', 'Negotiation', 'Won']
const stageLabel: Record<Stage, string> = {
Lead: 'Lead',
Qualified: 'Data Collection',
Proposal: 'Quotation',
Negotiation: 'Solicitud',
Won: 'Emision',
}
const stageConfig: Record<Stage, { color: string; dot: string; headerBg: string }> = {
Lead: { color: 'text-[var(--text-muted)]', dot: 'bg-[var(--text-muted)]', headerBg: 'bg-[var(--surface)] border-[var(--card-border)]' },
Qualified: { color: 'text-[var(--brand)]', dot: 'bg-[var(--brand)]', headerBg: 'bg-[var(--brand-faint)] border-[var(--brand-soft)]' },
Proposal: { color: 'text-violet-700', dot: 'bg-violet-400', headerBg: 'bg-violet-50 border-violet-200' },
Negotiation: { color: 'text-amber-700', dot: 'bg-amber-400', headerBg: 'bg-amber-50 border-amber-200' },
Won: { color: 'text-emerald-700', dot: 'bg-emerald-500', headerBg: 'bg-emerald-50 border-emerald-200' },
}
const lineColors: Record<string, string> = {
Auto: 'bg-[var(--brand-soft)] text-[var(--brand)]',
Health: 'bg-emerald-100 text-emerald-700',
Life: 'bg-violet-100 text-violet-700',
'General Risk': 'bg-amber-100 text-amber-700',
Custom: 'bg-[var(--badge-muted-bg)] text-[var(--text-muted)]',
}
const filteredDeals = computed(() => {
return deals.filter(d => {
const q = search.value.toLowerCase()
const matchSearch = !q || d.customer.toLowerCase().includes(q) || d.product.toLowerCase().includes(q) || d.id.toLowerCase().includes(q)
const matchLine = filterLine.value === 'all' || d.line === filterLine.value
const matchAgent = filterAgent.value === 'all' || d.agent === filterAgent.value
return matchSearch && matchLine && matchAgent
})
})
function stageDeals(stage: Stage) {
return filteredDeals.value.filter(d => d.stage === stage)
}
function stageTotal(stage: Stage) {
return stageDeals(stage).reduce((s, d) => s + d.premium, 0)
}
function fmt(n: number) {
return n >= 1000 ? `$${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}k` : `$${n}`
}
function daysLabel(n: number) {
if (n === 0) return 'Today'
return `${n}d`
}
</script>
<template>
<div class="flex flex-col gap-5">
<!-- Header -->
<div class="flex items-start justify-between">
<div>
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Sales Pipeline</h1>
<p class="mt-1 text-[13px] text-[var(--text-muted)]">{{ filteredDeals.length }} opportunities · {{ fmt(filteredDeals.reduce((s, d) => s + d.premium, 0)) }} total premium</p>
</div>
<div class="flex gap-2">
<NuxtLink to="/onboarding/solicitud">
<UButton size="sm" color="primary" icon="i-heroicons-plus">New Solicitud</UButton>
</NuxtLink>
<NuxtLink to="/sales/quick-lead">
<UButton size="sm" color="primary" variant="soft" icon="i-heroicons-user-plus">Quick Lead</UButton>
</NuxtLink>
</div>
</div>
<!-- Search + Filters -->
<div class="flex flex-wrap items-center gap-3 rounded-xl border border-[var(--card-border)] bg-[var(--surface)] px-4 py-3 shadow-sm ring-1 ring-[var(--surface)]">
<UInput
v-model="search"
icon="i-heroicons-magnifying-glass"
placeholder="Search customer, product, ID…"
size="sm"
class="w-64"
/>
<USelect
v-model="filterLine"
:items="lineOptions"
size="sm"
class="w-36"
/>
<USelect
v-model="filterAgent"
:items="agentOptions"
size="sm"
class="w-36"
/>
<div class="ml-auto flex items-center gap-2 text-[12px] text-[var(--text-muted)]">
<span
v-for="stage in stages"
:key="stage"
class="flex items-center gap-1"
>
<span class="h-2 w-2 rounded-full" :class="stageConfig[stage].dot" />
<span class="text-[11px]">{{ stageLabel[stage] }}</span>
<span class="font-semibold">{{ stageDeals(stage).length }}</span>
<span class="text-[var(--text-muted)] opacity-50 last:hidden">·</span>
</span>
</div>
</div>
<!-- Kanban board -->
<div class="flex gap-3 overflow-x-auto pb-2">
<div
v-for="stage in stages"
:key="stage"
class="flex w-[230px] shrink-0 flex-col gap-2"
>
<!-- Column header -->
<div
class="flex items-center justify-between rounded-lg border px-3 py-2"
:class="stageConfig[stage].headerBg"
>
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full" :class="stageConfig[stage].dot" />
<span class="text-[13px] font-semibold" :class="stageConfig[stage].color">{{ stageLabel[stage] }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-[11px] text-[var(--text-muted)]">{{ fmt(stageTotal(stage)) }}</span>
<span class="flex h-5 min-w-[20px] items-center justify-center rounded-full bg-[var(--surface)]/70 px-1.5 text-[11px] font-semibold text-[var(--text-muted)] ring-1 ring-[var(--card-border)]/60">
{{ stageDeals(stage).length }}
</span>
</div>
</div>
<!-- Empty state -->
<div
v-if="stageDeals(stage).length === 0"
class="rounded-xl border border-dashed border-[var(--card-border)] bg-[var(--surface)]/50 px-3 py-8 text-center"
>
<p class="text-[12px] text-[var(--text-muted)] opacity-70">No deals in this stage</p>
</div>
<!-- Deal cards -->
<div
v-for="deal in stageDeals(stage)"
:key="deal.id"
class="group rounded-xl border px-3.5 py-3 shadow-sm ring-1 transition hover:shadow-md"
:class="deal.urgent
? 'border-rose-300 bg-rose-50/50 ring-rose-100 hover:border-rose-400'
: 'border-[var(--card-border)] bg-[var(--surface)] ring-[var(--surface)] hover:border-[var(--brand)]/30'"
>
<!-- Customer + product line badge -->
<div class="flex items-start justify-between gap-1">
<p class="text-[13px] font-semibold leading-snug text-[var(--text-primary)]">{{ deal.customer }}</p>
<span
class="ml-1 shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-semibold"
:class="lineColors[deal.line]"
>{{ deal.line }}</span>
</div>
<!-- Product -->
<p class="mt-0.5 text-[12px] text-[var(--text-muted)]">{{ deal.product }}</p>
<!-- Note -->
<p v-if="deal.note" class="mt-1.5 text-[11px] italic text-amber-600">{{ deal.note }}</p>
<!-- Meta row -->
<div class="mt-2.5 flex items-center justify-between border-t border-[var(--divider)] pt-2">
<div class="flex items-center gap-1.5">
<!-- Urgent flag -->
<span
v-if="deal.urgent"
class="inline-flex items-center gap-1 rounded-full bg-rose-100 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-rose-600"
>
<span class="h-1.5 w-1.5 rounded-full bg-rose-500" />
Urgent
</span>
<span class="text-[12px] font-bold text-[var(--text-primary)]">{{ fmt(deal.premium) }}</span>
</div>
<div class="flex items-center gap-2 text-[11px] text-[var(--text-muted)] opacity-70">
<span>{{ deal.agent }}</span>
<span>·</span>
<span>{{ daysLabel(deal.daysInStage) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Legend -->
<div class="flex flex-wrap gap-4 rounded-lg border border-[var(--card-border)]/60 bg-[var(--surface)] px-4 py-2.5 text-[11px] text-[var(--text-muted)] shadow-sm">
<span class="flex items-center gap-1.5"><span class="inline-flex items-center gap-1 rounded-full bg-rose-100 px-1.5 py-0.5 text-[9px] font-bold uppercase text-rose-600"><span class="h-1.5 w-1.5 rounded-full bg-rose-500" />Urgent</span> Action required</span>
<span class="flex items-center gap-1.5"><span class="font-bold text-[var(--text-primary)]">$18k</span> Total premium at stage</span>
<span>Days in stage shown on each card</span>
<NuxtLink to="/onboarding/emissions" class="ml-auto font-medium text-[var(--brand)] hover:text-[var(--brand)]">Emissions queue </NuxtLink>
</div>
</div>
</template>