Files
policy-ui/app/composables/useSalesPipeline.ts
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

317 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sales pipeline tracker — per-deal stage + form completion tracking.
* Persisted in localStorage. Each deal flows:
* Customer → Get Quotes → [waiting] → Present Quotes → [waiting] → Solicitud → Emission
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
export type PipelineStage =
| 'customer'
| 'get_quotes'
| 'waiting_carriers'
| 'present_quotes'
| 'waiting_client'
| 'solicitud'
| 'emission'
export type FormStatus = 'not_started' | 'in_progress' | 'complete'
export interface DealForm {
id: string
label: string
/** 0100 */
completionPct: number
status: FormStatus
requiredFields: number
completedFields: number
}
export interface SalesDeal {
id: string
customerId: string
customerName: string
productLine: string
currentStage: PipelineStage
/** Stages that have been fully completed */
completedStages: PipelineStage[]
/** ISO timestamps for when each stage was entered */
stageTimestamps: Partial<Record<PipelineStage, string>>
/** Forms assigned to this deal, keyed by stage */
forms: Partial<Record<PipelineStage, DealForm[]>>
/** Optional carrier info */
carrier?: string
carrierProduct?: string
/** Bind token linking compare → solicitud */
bindToken?: string
createdAt: string
updatedAt: string
}
const KEY = 'policy-ui-sales-pipeline-v1'
/** Ordered stages for rendering */
export const PIPELINE_STAGES: { id: PipelineStage; label: string; isWaiting: boolean }[] = [
{ id: 'customer', label: 'Customer', isWaiting: false },
{ id: 'get_quotes', label: 'Get Quotes', isWaiting: false },
{ id: 'waiting_carriers', label: 'Awaiting Carriers', isWaiting: true },
{ id: 'present_quotes', label: 'Present Quotes', isWaiting: false },
{ id: 'waiting_client', label: 'Awaiting Client', isWaiting: true },
{ id: 'solicitud', label: 'Solicitud', isWaiting: false },
{ id: 'emission', label: 'Emission', isWaiting: false },
]
function stageIndex(stage: PipelineStage): number {
return PIPELINE_STAGES.findIndex(s => s.id === stage)
}
/** Default forms per stage (seeded when deal enters a stage) */
function defaultFormsForStage(stage: PipelineStage, productLine: string): DealForm[] {
switch (stage) {
case 'customer':
return [
{ id: 'client-info', label: 'Client information', completionPct: 0, status: 'not_started', requiredFields: 8, completedFields: 0 },
{ id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 0, status: 'not_started', requiredFields: 3, completedFields: 0 },
]
case 'get_quotes':
return [
{ id: 'quote-request', label: 'Quote request form', completionPct: 0, status: 'not_started', requiredFields: 12, completedFields: 0 },
{ id: 'risk-details', label: `${productLine} risk details`, completionPct: 0, status: 'not_started', requiredFields: 10, completedFields: 0 },
]
case 'solicitud':
return [
{ id: 'solicitud-form', label: 'Solicitud de seguro', completionPct: 0, status: 'not_started', requiredFields: 18, completedFields: 0 },
{ id: 'payment-auth', label: 'Payment authorization', completionPct: 0, status: 'not_started', requiredFields: 5, completedFields: 0 },
{ id: 'beneficiaries', label: 'Beneficiary designation', completionPct: 0, status: 'not_started', requiredFields: 4, completedFields: 0 },
]
case 'emission':
return [
{ id: 'policy-review', label: 'Policy review checklist', completionPct: 0, status: 'not_started', requiredFields: 6, completedFields: 0 },
]
default:
return []
}
}
/** Seed demo deals */
const SEED_DEALS: SalesDeal[] = [
{
id: 'deal-001',
customerId: 'cust-001',
customerName: 'María Elena Pérez Solano',
productLine: 'Auto',
currentStage: 'waiting_carriers',
completedStages: ['customer', 'get_quotes'],
stageTimestamps: {
customer: '2026-04-02T09:00:00Z',
get_quotes: '2026-04-02T09:30:00Z',
waiting_carriers: '2026-04-02T10:00:00Z',
},
forms: {
customer: [
{ id: 'client-info', label: 'Client information', completionPct: 100, status: 'complete', requiredFields: 8, completedFields: 8 },
{ id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 100, status: 'complete', requiredFields: 3, completedFields: 3 },
],
get_quotes: [
{ id: 'quote-request', label: 'Quote request form', completionPct: 100, status: 'complete', requiredFields: 12, completedFields: 12 },
{ id: 'risk-details', label: 'Auto risk details', completionPct: 100, status: 'complete', requiredFields: 10, completedFields: 10 },
],
},
createdAt: '2026-04-02T09:00:00Z',
updatedAt: '2026-04-02T10:00:00Z',
},
{
id: 'deal-002',
customerId: 'cust-002',
customerName: 'Roberto Jiménez Mora',
productLine: 'Life',
currentStage: 'solicitud',
completedStages: ['customer', 'get_quotes', 'waiting_carriers', 'present_quotes', 'waiting_client'],
stageTimestamps: {
customer: '2026-03-28T11:00:00Z',
get_quotes: '2026-03-28T11:30:00Z',
waiting_carriers: '2026-03-28T12:00:00Z',
present_quotes: '2026-04-01T14:00:00Z',
waiting_client: '2026-04-01T15:00:00Z',
solicitud: '2026-04-03T09:00:00Z',
},
forms: {
customer: [
{ id: 'client-info', label: 'Client information', completionPct: 100, status: 'complete', requiredFields: 8, completedFields: 8 },
{ id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 100, status: 'complete', requiredFields: 3, completedFields: 3 },
],
get_quotes: [
{ id: 'quote-request', label: 'Quote request form', completionPct: 100, status: 'complete', requiredFields: 12, completedFields: 12 },
{ id: 'risk-details', label: 'Life risk details', completionPct: 100, status: 'complete', requiredFields: 10, completedFields: 10 },
],
solicitud: [
{ id: 'solicitud-form', label: 'Solicitud de seguro', completionPct: 72, status: 'in_progress', requiredFields: 18, completedFields: 13 },
{ id: 'payment-auth', label: 'Payment authorization', completionPct: 40, status: 'in_progress', requiredFields: 5, completedFields: 2 },
{ id: 'beneficiaries', label: 'Beneficiary designation', completionPct: 0, status: 'not_started', requiredFields: 4, completedFields: 0 },
],
},
carrier: 'ASSA',
carrierProduct: 'Universal II',
bindToken: 'bind-abc123',
createdAt: '2026-03-28T11:00:00Z',
updatedAt: '2026-04-03T09:00:00Z',
},
{
id: 'deal-003',
customerId: 'cust-005',
customerName: 'Sofía Rojas Delgado',
productLine: 'Auto',
currentStage: 'get_quotes',
completedStages: ['customer'],
stageTimestamps: {
customer: '2026-04-05T08:00:00Z',
get_quotes: '2026-04-05T08:15:00Z',
},
forms: {
customer: [
{ id: 'client-info', label: 'Client information', completionPct: 100, status: 'complete', requiredFields: 8, completedFields: 8 },
{ id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 67, status: 'in_progress', requiredFields: 3, completedFields: 2 },
],
get_quotes: [
{ id: 'quote-request', label: 'Quote request form', completionPct: 50, status: 'in_progress', requiredFields: 12, completedFields: 6 },
{ id: 'risk-details', label: 'Auto risk details', completionPct: 0, status: 'not_started', requiredFields: 10, completedFields: 0 },
],
},
createdAt: '2026-04-05T08:00:00Z',
updatedAt: '2026-04-05T08:15:00Z',
},
]
export function useSalesPipeline() {
const deals = useLocalStorageRef<SalesDeal[]>(KEY, () => [])
// Seed on first use
if (import.meta.client && deals.value.length === 0) {
deals.value = [...SEED_DEALS]
}
/** Get a deal by ID */
function getDeal(dealId: string) {
return deals.value.find(d => d.id === dealId)
}
/** Get deals for a specific customer */
function getDealsForCustomer(customerId: string) {
return deals.value.filter(d => d.customerId === customerId)
}
/** Get the active (most recent non-emission) deal for a customer */
function getActiveDeal(customerId: string) {
return deals.value
.filter(d => d.customerId === customerId && d.currentStage !== 'emission')
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0]
}
/** Create a new deal */
function createDeal(customerId: string, customerName: string, productLine: string): SalesDeal {
const now = new Date().toISOString()
const deal: SalesDeal = {
id: 'deal-' + (crypto.randomUUID?.() ?? String(Date.now())).slice(0, 8),
customerId,
customerName,
productLine,
currentStage: 'customer',
completedStages: [],
stageTimestamps: { customer: now },
forms: { customer: defaultFormsForStage('customer', productLine) },
createdAt: now,
updatedAt: now,
}
deals.value = [deal, ...deals.value]
return deal
}
/** Advance deal to the next stage */
function advanceStage(dealId: string) {
const deal = deals.value.find(d => d.id === dealId)
if (!deal) return
const currentIdx = stageIndex(deal.currentStage)
const nextStage = PIPELINE_STAGES[currentIdx + 1]
if (!nextStage) return
const now = new Date().toISOString()
deal.completedStages = [...new Set([...deal.completedStages, deal.currentStage])]
deal.currentStage = nextStage.id
deal.stageTimestamps = { ...deal.stageTimestamps, [nextStage.id]: now }
deal.updatedAt = now
// Seed forms for the new stage if not already present
if (!deal.forms[nextStage.id]) {
deal.forms = { ...deal.forms, [nextStage.id]: defaultFormsForStage(nextStage.id, deal.productLine) }
}
// Trigger reactivity
deals.value = [...deals.value]
}
/** Set deal to a specific stage (e.g., when quotes arrive) */
function setStage(dealId: string, stage: PipelineStage) {
const deal = deals.value.find(d => d.id === dealId)
if (!deal) return
const now = new Date().toISOString()
// Mark all stages before the target as completed
const targetIdx = stageIndex(stage)
const completed = PIPELINE_STAGES.slice(0, targetIdx).map(s => s.id)
deal.completedStages = [...new Set([...deal.completedStages, ...completed])]
deal.currentStage = stage
deal.stageTimestamps = { ...deal.stageTimestamps, [stage]: now }
deal.updatedAt = now
if (!deal.forms[stage]) {
deal.forms = { ...deal.forms, [stage]: defaultFormsForStage(stage, deal.productLine) }
}
deals.value = [...deals.value]
}
/** Update form completion within a deal */
function updateFormProgress(dealId: string, stage: PipelineStage, formId: string, completedFields: number) {
const deal = deals.value.find(d => d.id === dealId)
if (!deal) return
const stageForms = deal.forms[stage]
if (!stageForms) return
const form = stageForms.find(f => f.id === formId)
if (!form) return
form.completedFields = Math.min(completedFields, form.requiredFields)
form.completionPct = form.requiredFields > 0 ? Math.round((form.completedFields / form.requiredFields) * 100) : 100
form.status = form.completionPct === 0 ? 'not_started' : form.completionPct === 100 ? 'complete' : 'in_progress'
deal.updatedAt = new Date().toISOString()
deals.value = [...deals.value]
}
/** Remove a deal */
function removeDeal(dealId: string) {
deals.value = deals.value.filter(d => d.id !== dealId)
}
/** Computed: stage completion percentage for a deal's current stage forms */
function stageFormProgress(deal: SalesDeal, stage: PipelineStage): number {
const forms = deal.forms[stage]
if (!forms || forms.length === 0) return 0
const totalRequired = forms.reduce((s, f) => s + f.requiredFields, 0)
const totalCompleted = forms.reduce((s, f) => s + f.completedFields, 0)
return totalRequired > 0 ? Math.round((totalCompleted / totalRequired) * 100) : 100
}
return {
deals,
getDeal,
getDealsForCustomer,
getActiveDeal,
createDeal,
advanceStage,
setStage,
updateFormProgress,
removeDeal,
stageFormProgress,
}
}