WIP jordan

This commit is contained in:
Jordan Weingarten
2026-04-16 11:11:44 -05:00
parent ff2d7b18b5
commit 67482f6629
163 changed files with 50627 additions and 728 deletions

View File

@@ -0,0 +1,316 @@
/**
* 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,
}
}