WIP jordan
This commit is contained in:
316
app/composables/useSalesPipeline.ts
Normal file
316
app/composables/useSalesPipeline.ts
Normal 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
|
||||
/** 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<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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user