/** * 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 /** 0–100 */ 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> /** Forms assigned to this deal, keyed by stage */ forms: Partial> /** 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(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, } }