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