big refactor

This commit is contained in:
2026-04-29 16:25:11 -05:00
parent 6c411ce2b6
commit 8265fb689a
156 changed files with 15845 additions and 50373 deletions

View File

@@ -1,176 +0,0 @@
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
/* ── Types ── */
export type AlertRecipient = 'handler' | 'manager' | 'customer' | 'custom'
export interface EmailSenderConfig {
senderEmail: string
senderDisplayName: string
replyToEmail: string
}
export interface AlertThresholdEntry {
id: string
daysBefore: number
enabled: boolean
}
export interface EscalationTier {
id: string
daysOverdue: number
recipients: AlertRecipient[]
action: string
}
export interface RenewalAlertConfig {
enabled: boolean
thresholds: AlertThresholdEntry[]
}
export interface CancellationAlertConfig {
enabled: boolean
recipients: AlertRecipient[]
}
export interface LatePaymentAlertConfig {
enabled: boolean
tiers: EscalationTier[]
}
export interface CreditCardExpiryAlertConfig {
enabled: boolean
thresholds: AlertThresholdEntry[]
autoDebitOnly: boolean
}
export interface CustomAlertRule {
id: string
alertName: string
field: string
operator: 'gte' | 'lte' | 'eq' | 'gt' | 'lt' | 'contains'
value: string | number | boolean
recipients: AlertRecipient[]
enabled: boolean
}
export interface AlertConfig {
emailSender: EmailSenderConfig
renewals: RenewalAlertConfig
cancellations: CancellationAlertConfig
latePayments: LatePaymentAlertConfig
creditCardExpiry: CreditCardExpiryAlertConfig
customRules: CustomAlertRule[]
}
/* ── Defaults ── */
function defaultConfig(): AlertConfig {
return {
emailSender: {
senderEmail: 'alertas@miagencia.com',
senderDisplayName: 'Segur-OS Alertas',
replyToEmail: 'soporte@miagencia.com',
},
renewals: {
enabled: true,
thresholds: [
{ id: 'r90', daysBefore: 90, enabled: true },
{ id: 'r60', daysBefore: 60, enabled: true },
{ id: 'r30', daysBefore: 30, enabled: true },
{ id: 'r15', daysBefore: 15, enabled: true },
],
},
cancellations: {
enabled: true,
recipients: ['handler', 'manager'],
},
latePayments: {
enabled: true,
tiers: [
{ id: 'lp5', daysOverdue: 5, recipients: ['handler'], action: 'Notify assigned handler' },
{ id: 'lp15', daysOverdue: 15, recipients: ['handler', 'manager'], action: 'Notify handler + manager' },
{ id: 'lp30', daysOverdue: 30, recipients: ['handler', 'manager', 'customer'], action: 'Auto-escalate + notify customer' },
],
},
creditCardExpiry: {
enabled: true,
thresholds: [
{ id: 'cc60', daysBefore: 60, enabled: true },
{ id: 'cc30', daysBefore: 30, enabled: true },
{ id: 'cc15', daysBefore: 15, enabled: true },
],
autoDebitOnly: true,
},
customRules: [
{
id: 'cr1',
alertName: 'High-value policy renewal',
field: 'premium',
operator: 'gte',
value: 25000,
recipients: ['handler', 'manager'],
enabled: true,
},
],
}
}
/* ── Composable ── */
let _counter = 100
export function useAlertConfig() {
const config = useLocalStorageRef<AlertConfig>('policy-ui-alert-config-v1', defaultConfig)
/* ── Threshold CRUD ── */
function addThreshold(section: 'renewals' | 'creditCardExpiry', daysBefore: number) {
const id = `t${++_counter}`
config.value[section].thresholds.push({ id, daysBefore, enabled: true })
}
function removeThreshold(section: 'renewals' | 'creditCardExpiry', id: string) {
config.value[section].thresholds = config.value[section].thresholds.filter(t => t.id !== id)
}
/* ── Late payment tier CRUD ── */
function addPaymentTier(daysOverdue: number, action: string, recipients: AlertRecipient[]) {
const id = `lp${++_counter}`
config.value.latePayments.tiers.push({ id, daysOverdue, recipients, action })
}
function removePaymentTier(id: string) {
config.value.latePayments.tiers = config.value.latePayments.tiers.filter(t => t.id !== id)
}
/* ── Custom rule CRUD ── */
function addCustomRule(rule: Omit<CustomAlertRule, 'id'>) {
const id = `cr${++_counter}`
config.value.customRules.push({ ...rule, id })
}
function updateCustomRule(id: string, patch: Partial<CustomAlertRule>) {
const idx = config.value.customRules.findIndex(r => r.id === id)
if (idx !== -1) {
config.value.customRules[idx] = { ...config.value.customRules[idx], ...patch }
}
}
function removeCustomRule(id: string) {
config.value.customRules = config.value.customRules.filter(r => r.id !== id)
}
return {
config,
addThreshold,
removeThreshold,
addPaymentTier,
removePaymentTier,
addCustomRule,
updateCustomRule,
removeCustomRule,
}
}

View File

@@ -1,147 +0,0 @@
/**
* Business Analytics — composable for chart state, SVG rendering, and domain filtering.
* SVG chart math extracted from /app/pages/index.vue dashboard charts.
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
import {
ANALYTICS_METRICS,
ANALYTICS_KPI_SUMMARIES,
type AnalyticsDomainId,
type AnalyticsChartType,
type AnalyticsTimePoint,
} from '~/data/mock-analytics'
export interface ChartSvgModel {
lineD: string
areaD: string
points: { x: number; y: number; v: number }[]
bars: { x: number; y: number; w: number; h: number }[]
gridYs: number[]
viewW: number
viewH: number
padX: number
innerW: number
bottomY: number
}
interface AnalyticsState {
activeDomain: AnalyticsDomainId
chartBuilderMetric: string
chartBuilderType: AnalyticsChartType
chartBuilderRange: '3m' | '6m' | '12m'
}
function buildDefaults(): AnalyticsState {
return {
activeDomain: 'production',
chartBuilderMetric: 'gwp',
chartBuilderType: 'area',
chartBuilderRange: '6m',
}
}
export function useAnalytics() {
const state = useLocalStorageRef<AnalyticsState>('policy-ui-analytics-v1', buildDefaults)
const allMetrics = ANALYTICS_METRICS
const kpiSummaries = ANALYTICS_KPI_SUMMARIES
const domainMetrics = computed(() =>
allMetrics.filter(m => m.domain === state.value.activeDomain)
)
const chartBuilderMetricObj = computed(() =>
allMetrics.find(m => m.id === state.value.chartBuilderMetric) ?? allMetrics[0]!
)
const chartBuilderData = computed(() => {
const data = chartBuilderMetricObj.value.data12m.filter(d => d.m)
const range = state.value.chartBuilderRange
if (range === '3m') return data.slice(-3)
if (range === '6m') return data.slice(-6)
return data
})
// ── SVG chart model builder (extracted from dashboard index.vue) ──
function buildSvgModel(data: AnalyticsTimePoint[], viewW = 400, viewH = 120): ChartSvgModel {
const pts = data.map(d => d.v)
const padX = 8; const padY = 14
const innerW = viewW - padX * 2; const innerH = viewH - padY * 2
const maxV = Math.max(...pts, 1); const minV = Math.min(...pts, 0)
const span = maxV - minV || 1
const points = pts.map((p, i) => ({
x: padX + (i / Math.max(1, pts.length - 1)) * innerW,
y: padY + (1 - (p - minV) / span) * innerH,
v: p,
}))
const bottomY = padY + innerH
const first = points[0]!; const last = points[points.length - 1]!
// Line + area paths (smooth Bézier curves)
let lineD = `M ${first.x},${first.y}`
let areaD = `M ${first.x},${bottomY} L ${first.x},${first.y}`
for (let i = 1; i < points.length; i++) {
const p0 = points[i - 1]!; const p1 = points[i]!
const cx = (p0.x + p1.x) / 2
const seg = ` C ${cx},${p0.y} ${cx},${p1.y} ${p1.x},${p1.y}`
lineD += seg; areaD += seg
}
areaD += ` L ${last.x},${bottomY} Z`
// Bar geometry
const barW = Math.min(innerW / pts.length * 0.6, 40)
const bars = pts.map((p, i) => ({
x: padX + (i / Math.max(1, pts.length - 1)) * innerW - barW / 2,
y: padY + (1 - (p - minV) / span) * innerH,
w: barW,
h: ((p - minV) / span) * innerH,
}))
const gridYs = [0, 0.5, 1].map(t => padY + t * innerH)
return { lineD, areaD, points, bars, gridYs, viewW, viewH, padX, innerW, bottomY }
}
// ── Sparkline helpers ──
function sparklinePath(pts: number[], w = 112, h = 32, pad = 2): string {
const max = Math.max(...pts); const min = Math.min(...pts); const r = max - min || 1
const mapped = pts.map((p, i, arr) => ({
x: pad + (i / Math.max(1, arr.length - 1)) * (w - pad * 2),
y: pad + (1 - (p - min) / r) * (h - pad * 2),
}))
if (mapped.length < 2) return ''
let d = `M ${mapped[0]!.x},${mapped[0]!.y}`
for (let i = 0; i < mapped.length - 1; i++) {
const p0 = mapped[i]!; const p1 = mapped[i + 1]!; const cx = (p0.x + p1.x) / 2
d += ` C ${cx},${p0.y} ${cx},${p1.y} ${p1.x},${p1.y}`
}
return d
}
function sparklineArea(pts: number[], w = 112, h = 32, pad = 2): string {
const path = sparklinePath(pts, w, h, pad)
if (!path) return ''
const max = Math.max(...pts); const min = Math.min(...pts); const r = max - min || 1
const mapped = pts.map((p, i, arr) => ({
x: pad + (i / Math.max(1, arr.length - 1)) * (w - pad * 2),
y: pad + (1 - (p - min) / r) * (h - pad * 2),
}))
return `${path} L ${mapped[mapped.length - 1]!.x},${h} L ${mapped[0]!.x},${h} Z`
}
const chartBuilderSvgModel = computed(() =>
buildSvgModel(chartBuilderData.value, 400, 180)
)
return {
state,
allMetrics,
kpiSummaries,
domainMetrics,
chartBuilderMetricObj,
chartBuilderData,
chartBuilderSvgModel,
buildSvgModel,
sparklinePath,
sparklineArea,
}
}

View File

@@ -1,32 +0,0 @@
const STORAGE_KEY = 'policy-ui.sidebar.collapsed.v1'
export function useAppShellLayout() {
const sidebarCollapsed = ref(false)
onMounted(() => {
if (!import.meta.client) return
try {
sidebarCollapsed.value = localStorage.getItem(STORAGE_KEY) === '1'
} catch {
/* ignore */
}
})
watch(sidebarCollapsed, (c) => {
if (!import.meta.client) return
try {
localStorage.setItem(STORAGE_KEY, c ? '1' : '0')
} catch {
/* ignore */
}
})
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
return {
sidebarCollapsed,
toggleSidebar
}
}

View File

@@ -1,46 +0,0 @@
import type { AppThemeId } from '~/types/app-theme'
import { APP_THEME_OPTIONS } from '~/types/app-theme'
const STORAGE_KEY = 'policy-ui.theme.v1'
const VALID: AppThemeId[] = ['light', 'purple', 'dark', 'dark-purple']
function isThemeId(x: string): x is AppThemeId {
return (VALID as string[]).includes(x)
}
export function useAppTheme() {
const themeId = ref<AppThemeId>('light')
function applyTheme(id: AppThemeId) {
themeId.value = id
if (import.meta.client) {
document.documentElement.setAttribute('data-theme', id)
try {
localStorage.setItem(STORAGE_KEY, id)
} catch {
/* ignore */
}
}
}
onMounted(() => {
if (!import.meta.client) return
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw && isThemeId(raw)) {
applyTheme(raw)
return
}
} catch {
/* ignore */
}
applyTheme('light')
})
return {
themeId,
themeOptions: APP_THEME_OPTIONS,
applyTheme
}
}

View File

@@ -1,29 +0,0 @@
import type { AutoQuoteDraft } from '~/types/auto-quote-intake'
export function emptyAutoQuoteDraft(): AutoQuoteDraft {
return {
quoteMode: null,
segment: null,
insured: null,
buyer: null,
vehicle: {
subRamo: '',
clase: '',
uso: '',
marca: '',
modelo: '',
placa: '',
year: '',
capacidadPasajeros: '',
rc_limits: '',
market_value: 0,
requested_value: 0,
chassis_number: '',
engine_number: ''
},
solicit: {
carrierIds: [],
planIds: []
}
}
}

View File

@@ -1,31 +0,0 @@
import { BRANDING_STORAGE_KEY, type BrokerageBrandingState } from '~/types/branding'
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
export function defaultBrokerageBranding(): BrokerageBrandingState {
return {
companyName: '',
logoDataUrl: null,
logoFileName: '',
reportPageHeader: '',
reportPageFooter: ''
}
}
export function useBrokerageBranding() {
const saved = useLocalStorageRef(BRANDING_STORAGE_KEY, defaultBrokerageBranding)
const productDisplayName = computed(() => {
const n = saved.value.companyName?.trim()
if (n) return n
return null
})
const sidebarTitle = computed(() => productDisplayName.value ?? 'Segur-OS')
return {
saved,
productDisplayName,
sidebarTitle,
defaultBrokerageBranding
}
}

View File

@@ -1,35 +0,0 @@
/**
* Client favorites — star customers for quick dashboard access.
* Persisted in localStorage. Stores customer IDs.
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
const KEY = 'policy-ui-client-favorites-v1'
export function useClientFavorites() {
const favoriteIds = useLocalStorageRef<string[]>(KEY, () => [])
function isFavorite(customerId: string) {
return favoriteIds.value.includes(customerId)
}
function toggleFavorite(customerId: string) {
if (isFavorite(customerId)) {
favoriteIds.value = favoriteIds.value.filter(id => id !== customerId)
} else {
favoriteIds.value = [customerId, ...favoriteIds.value]
}
}
function addFavorite(customerId: string) {
if (!isFavorite(customerId)) {
favoriteIds.value = [customerId, ...favoriteIds.value]
}
}
function removeFavorite(customerId: string) {
favoriteIds.value = favoriteIds.value.filter(id => id !== customerId)
}
return { favoriteIds, isFavorite, toggleFavorite, addFavorite, removeFavorite }
}

View File

@@ -1,47 +0,0 @@
import type { ClientCaptureMeta, ClientRegistrationNatural } from '~/types/brokerage-registration'
export function createEmptyClientRegistration(): ClientRegistrationNatural {
return {
id: '',
economicGroupId: '',
conglomerateId: '',
personType: 'natural',
apellidoPaterno: '',
apellidoMaterno: '',
primerNombre: '',
segundoNombre: '',
fechaNacimiento: '',
tipoIdentificacion: '',
cedulaOPasaporte: '',
telefonoCelular: '',
correoElectronicoPersonal: '',
ocupacion: '',
procedencia: '',
detalle: '',
descripcion: ''
}
}
export function useClientCaptureMeta(): ClientCaptureMeta {
const now = new Date()
return {
operadorId: '32',
operadorNombre: 'Jordan',
fechaCaptura: now.toISOString(),
progresoCapturaPct: 6,
estado: ''
}
}
export function toIndividualCustomerBody(r: ClientRegistrationNatural) {
const last = [r.apellidoPaterno, r.apellidoMaterno].filter(Boolean).join(' ').trim()
return {
first_name: [r.primerNombre, r.segundoNombre].filter(Boolean).join(' ').trim() || r.primerNombre,
last_name: last || '-',
email: r.correoElectronicoPersonal.trim(),
phone: r.telefonoCelular.trim(),
birth_date: r.fechaNacimiento,
gender: '',
document_id: r.cedulaOPasaporte.trim()
}
}

View File

@@ -1,641 +0,0 @@
/**
* Colectivos (group accounts) — data backbone for the entire module.
* Manages group accounts, members, documents, billing, and service requests.
* Persisted in localStorage via useLocalStorageRef.
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
/* ── Core Types ── */
export type ColectivoStatus = 'quoting' | 'onboarding' | 'active' | 'renewal_due' | 'suspended' | 'cancelled'
export type MemberStatus = 'active' | 'pending_enrollment' | 'pending_docs' | 'excluded' | 'on_leave'
export type ServiceRequestType = 'inclusion' | 'exclusion' | 'claim' | 'billing' | 'certificate' | 'amendment'
export type ServiceRequestStatus = 'open' | 'in_progress' | 'pending_carrier' | 'pending_client' | 'resolved' | 'cancelled'
export type DocumentCategory = 'policy' | 'contract' | 'endorsement' | 'certificate' | 'amendment' | 'census' | 'siniestralidad' | 'enrollment' | 'correspondence' | 'other'
export type BillingStatus = 'upcoming' | 'invoiced' | 'paid' | 'overdue' | 'disputed' | 'reconciled'
export interface ColectivoMember {
id: string
name: string
documentId: string
email: string
phone: string
role: string
department: string
enrollmentDate: string
status: MemberStatus
tier: string
dependents: number
pendingDocs: string[]
formsCompleted: number
formsTotal: number
}
export interface ColectivoDocument {
id: string
name: string
category: DocumentCategory
uploadedBy: string
uploadedAt: string
fileSize: string
fileType: string
version: number
notes: string
}
export interface BillingCycle {
id: string
period: string
dueDate: string
status: BillingStatus
invoiceAmount: number
paidAmount: number
carrierRef: string
membersBilled: number
membersExpected: number
discrepancy: number
notes: string
}
export interface ServiceRequest {
id: string
type: ServiceRequestType
subject: string
status: ServiceRequestStatus
priority: 'low' | 'medium' | 'high' | 'urgent'
assignee: string
created: string
updated: string
memberName?: string
notes: string
}
export interface ColectivoAccount {
id: string
name: string
ruc: string
lob: string
product: string
carrier: string
status: ColectivoStatus
contactName: string
contactEmail: string
contactPhone: string
hrContactName: string
hrContactEmail: string
effectiveDate: string
renewalDate: string
onboardingDate: string
totalMembers: number
activeMembersCount: number
dependentsCount: number
pendingEnrollment: number
monthlyPremium: number
annualPremium: number
commissionPct: number
agent: string
members: ColectivoMember[]
documents: ColectivoDocument[]
billingCycles: BillingCycle[]
serviceRequests: ServiceRequest[]
recentActivity: { date: string; text: string; type: string; actor: string }[]
hasUrgentIssues: boolean
outstandingClaims: number
pendingTasks: number
}
/* ── Mock Data ── */
function buildDefaultAccounts(): ColectivoAccount[] {
return [
// ── 1. Banco Regional ──
{
id: 'col-001',
name: 'Banco Regional S.A.',
ruc: '80012345-6',
lob: 'Health',
product: 'Salud Corporativa Elite',
carrier: 'Vida Plena',
status: 'active',
contactName: 'Roberto Méndez',
contactEmail: 'rmendez@bancoregional.com.py',
contactPhone: '+595 21 410-2200',
hrContactName: 'Silvia Acosta',
hrContactEmail: 'sacosta@bancoregional.com.py',
effectiveDate: '2025-07-01',
renewalDate: '2026-05-23',
onboardingDate: '2025-06-15',
totalMembers: 412,
activeMembersCount: 398,
dependentsCount: 687,
pendingEnrollment: 4,
monthlyPremium: 10000,
annualPremium: 120000,
commissionPct: 12,
agent: 'Carlos Villalba',
members: [
{ id: 'mbr-001-01', name: 'Roberto Méndez', documentId: '3.456.789', email: 'rmendez@bancoregional.com.py', phone: '+595 981 222-001', role: 'Director General', department: 'Directorio', enrollmentDate: '2025-07-01', status: 'active', tier: 'Executive', dependents: 3, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-001-02', name: 'Silvia Acosta', documentId: '4.123.456', email: 'sacosta@bancoregional.com.py', phone: '+595 981 222-002', role: 'Gerente RRHH', department: 'Recursos Humanos', enrollmentDate: '2025-07-01', status: 'active', tier: 'Plus', dependents: 2, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-001-03', name: 'Jorge Ramírez', documentId: '2.987.654', email: 'jramirez@bancoregional.com.py', phone: '+595 981 222-003', role: 'Analista de Créditos', department: 'Banca Comercial', enrollmentDate: '2025-07-15', status: 'active', tier: 'Basic', dependents: 1, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-001-04', name: 'María Elena Torres', documentId: '5.321.098', email: 'metorres@bancoregional.com.py', phone: '+595 981 222-004', role: 'Cajera Principal', department: 'Operaciones', enrollmentDate: '2025-07-01', status: 'active', tier: 'Basic', dependents: 2, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-001-05', name: 'Fernando López', documentId: '1.654.321', email: 'flopez@bancoregional.com.py', phone: '+595 981 222-005', role: 'Gerente de Sucursal', department: 'Sucursales', enrollmentDate: '2025-08-01', status: 'active', tier: 'Plus', dependents: 4, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-001-06', name: 'Patricia Benítez', documentId: '6.789.012', email: 'pbenitez@bancoregional.com.py', phone: '+595 981 222-006', role: 'Oficial de Cumplimiento', department: 'Legal', enrollmentDate: '2025-09-01', status: 'pending_docs', tier: 'Plus', dependents: 1, pendingDocs: ['Certificado médico', 'Formulario de dependientes'], formsCompleted: 2, formsTotal: 4 },
{ id: 'mbr-001-07', name: 'Luis Giménez', documentId: '3.210.987', email: 'lgimenez@bancoregional.com.py', phone: '+595 981 222-007', role: 'Desarrollador Senior', department: 'Tecnología', enrollmentDate: '2026-01-15', status: 'active', tier: 'Basic', dependents: 0, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-001-08', name: 'Ana Cristina Duarte', documentId: '7.654.321', email: 'acduarte@bancoregional.com.py', phone: '+595 981 222-008', role: 'Asistente Ejecutiva', department: 'Directorio', enrollmentDate: '2025-07-01', status: 'on_leave', tier: 'Executive', dependents: 2, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
],
documents: [
{ id: 'doc-001-01', name: 'Póliza Colectiva 2025-2026', category: 'policy', uploadedBy: 'Carlos Villalba', uploadedAt: '2025-06-28', fileSize: '4.2 MB', fileType: 'PDF', version: 2, notes: 'Versión final firmada' },
{ id: 'doc-001-02', name: 'Contrato de Intermediación', category: 'contract', uploadedBy: 'Carlos Villalba', uploadedAt: '2025-06-20', fileSize: '1.8 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-001-03', name: 'Censo Marzo 2026', category: 'census', uploadedBy: 'Silvia Acosta', uploadedAt: '2026-03-05', fileSize: '856 KB', fileType: 'XLSX', version: 1, notes: 'Incluye 3 nuevas altas' },
{ id: 'doc-001-04', name: 'Endoso #3 - Inclusiones Feb 2026', category: 'endorsement', uploadedBy: 'Carlos Villalba', uploadedAt: '2026-02-18', fileSize: '320 KB', fileType: 'PDF', version: 1, notes: '8 inclusiones procesadas' },
{ id: 'doc-001-05', name: 'Reporte Siniestralidad Q1 2026', category: 'siniestralidad', uploadedBy: 'Vida Plena', uploadedAt: '2026-04-02', fileSize: '2.1 MB', fileType: 'PDF', version: 1, notes: 'Siniestralidad al 68%' },
{ id: 'doc-001-06', name: 'Certificado Individual - R. Méndez', category: 'certificate', uploadedBy: 'Carlos Villalba', uploadedAt: '2025-07-10', fileSize: '145 KB', fileType: 'PDF', version: 1, notes: '' },
],
billingCycles: [
{ id: 'bill-001-01', period: 'January 2026', dueDate: '2026-01-15', status: 'paid', invoiceAmount: 10000, paidAmount: 10000, carrierRef: 'VP-2026-0412-01', membersBilled: 410, membersExpected: 410, discrepancy: 0, notes: '' },
{ id: 'bill-001-02', period: 'February 2026', dueDate: '2026-02-15', status: 'paid', invoiceAmount: 10200, paidAmount: 10200, carrierRef: 'VP-2026-0412-02', membersBilled: 412, membersExpected: 412, discrepancy: 0, notes: 'Incluyó 2 altas' },
{ id: 'bill-001-03', period: 'March 2026', dueDate: '2026-03-15', status: 'paid', invoiceAmount: 10200, paidAmount: 10200, carrierRef: 'VP-2026-0412-03', membersBilled: 412, membersExpected: 412, discrepancy: 0, notes: '' },
{ id: 'bill-001-04', period: 'April 2026', dueDate: '2026-04-15', status: 'invoiced', invoiceAmount: 10200, paidAmount: 0, carrierRef: 'VP-2026-0412-04', membersBilled: 412, membersExpected: 412, discrepancy: 0, notes: 'Factura enviada al cliente' },
{ id: 'bill-001-05', period: 'May 2026', dueDate: '2026-05-15', status: 'upcoming', invoiceAmount: 10200, paidAmount: 0, carrierRef: '', membersBilled: 0, membersExpected: 412, discrepancy: 0, notes: '' },
],
serviceRequests: [
{ id: 'sr-001-01', type: 'claim', subject: 'Reclamo cobertura cirugía - Expediente #4521', status: 'pending_carrier', priority: 'urgent', assignee: 'Carlos Villalba', created: '2026-03-28', updated: '2026-04-05', memberName: 'Fernando López', notes: 'Carrier solicitó documentación adicional del hospital. Plazo vence 04/12.' },
{ id: 'sr-001-02', type: 'inclusion', subject: 'Alta de 3 nuevos empleados - Sucursal Este', status: 'in_progress', priority: 'high', assignee: 'Carlos Villalba', created: '2026-04-01', updated: '2026-04-06', notes: 'Faltan formularios de 1 empleado.' },
{ id: 'sr-001-03', type: 'certificate', subject: 'Certificados individuales para viaje corporativo', status: 'open', priority: 'urgent', assignee: 'Carlos Villalba', created: '2026-04-07', updated: '2026-04-07', notes: 'Necesitan 12 certificados para viaje el 04/18.' },
{ id: 'sr-001-04', type: 'billing', subject: 'Consulta sobre diferencia en factura Enero', status: 'resolved', priority: 'low', assignee: 'Carlos Villalba', created: '2026-01-22', updated: '2026-02-03', notes: 'Diferencia por ajuste de prima. Aclarado con RRHH.' },
],
recentActivity: [
{ date: '2026-04-07', text: 'Solicitud urgente de certificados para viaje corporativo', type: 'service_request', actor: 'Silvia Acosta' },
{ date: '2026-04-06', text: 'Actualización en reclamo #4521 - carrier solicita más docs', type: 'claim_update', actor: 'Vida Plena' },
{ date: '2026-04-05', text: 'Factura de Abril enviada al cliente', type: 'billing', actor: 'Sistema' },
{ date: '2026-04-01', text: 'Nueva solicitud de inclusión: 3 empleados Sucursal Este', type: 'inclusion', actor: 'Silvia Acosta' },
{ date: '2026-03-28', text: 'Reclamo de cirugía elevado a urgente', type: 'claim_update', actor: 'Carlos Villalba' },
{ date: '2026-03-05', text: 'Censo de Marzo recibido y cargado', type: 'document', actor: 'Silvia Acosta' },
{ date: '2026-02-18', text: 'Endoso #3 procesado - 8 inclusiones', type: 'endorsement', actor: 'Carlos Villalba' },
],
hasUrgentIssues: true,
outstandingClaims: 2,
pendingTasks: 5,
},
// ── 2. Clínica San José ──
{
id: 'col-002',
name: 'Clínica San José',
ruc: '80034567-1',
lob: 'Health',
product: 'Salud Integral Empresarial',
carrier: 'Salud Global',
status: 'active',
contactName: 'Dr. Marcelo Insfrán',
contactEmail: 'minsfran@clinicasanjose.com.py',
contactPhone: '+595 21 550-3300',
hrContactName: 'Laura Paredes',
hrContactEmail: 'lparedes@clinicasanjose.com.py',
effectiveDate: '2025-09-01',
renewalDate: '2026-09-01',
onboardingDate: '2025-08-15',
totalMembers: 88,
activeMembersCount: 88,
dependentsCount: 142,
pendingEnrollment: 0,
monthlyPremium: 3500,
annualPremium: 42000,
commissionPct: 10,
agent: 'María Fernanda Ortiz',
members: [
{ id: 'mbr-002-01', name: 'Dr. Marcelo Insfrán', documentId: '1.234.567', email: 'minsfran@clinicasanjose.com.py', phone: '+595 982 333-001', role: 'Director Médico', department: 'Dirección', enrollmentDate: '2025-09-01', status: 'active', tier: 'Executive', dependents: 3, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-002-02', name: 'Laura Paredes', documentId: '2.345.678', email: 'lparedes@clinicasanjose.com.py', phone: '+595 982 333-002', role: 'Jefa de RRHH', department: 'Administración', enrollmentDate: '2025-09-01', status: 'active', tier: 'Plus', dependents: 2, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-002-03', name: 'Dra. Carolina Fleitas', documentId: '3.456.012', email: 'cfleitas@clinicasanjose.com.py', phone: '+595 982 333-003', role: 'Pediatra', department: 'Pediatría', enrollmentDate: '2025-09-01', status: 'active', tier: 'Plus', dependents: 1, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-002-04', name: 'Enf. Rosa Martínez', documentId: '4.567.890', email: 'rmartinez@clinicasanjose.com.py', phone: '+595 982 333-004', role: 'Enfermera Jefa', department: 'Enfermería', enrollmentDate: '2025-09-15', status: 'active', tier: 'Basic', dependents: 2, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-002-05', name: 'Carlos Ruiz', documentId: '5.678.901', email: 'cruiz@clinicasanjose.com.py', phone: '+595 982 333-005', role: 'Técnico de Laboratorio', department: 'Laboratorio', enrollmentDate: '2025-10-01', status: 'active', tier: 'Basic', dependents: 0, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-002-06', name: 'Gabriela Sánchez', documentId: '6.789.012', email: 'gsanchez@clinicasanjose.com.py', phone: '+595 982 333-006', role: 'Recepcionista', department: 'Atención al Paciente', enrollmentDate: '2025-09-01', status: 'active', tier: 'Basic', dependents: 1, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
],
documents: [
{ id: 'doc-002-01', name: 'Póliza Colectiva Clínica SJ 2025-2026', category: 'policy', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2025-08-28', fileSize: '3.1 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-002-02', name: 'Contrato de Servicios', category: 'contract', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2025-08-20', fileSize: '1.4 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-002-03', name: 'Censo Actualizado Q1 2026', category: 'census', uploadedBy: 'Laura Paredes', uploadedAt: '2026-03-28', fileSize: '420 KB', fileType: 'XLSX', version: 1, notes: 'Sin cambios respecto al período anterior' },
],
billingCycles: [
{ id: 'bill-002-01', period: 'February 2026', dueDate: '2026-02-01', status: 'paid', invoiceAmount: 3500, paidAmount: 3500, carrierRef: 'SG-2026-088-02', membersBilled: 88, membersExpected: 88, discrepancy: 0, notes: '' },
{ id: 'bill-002-02', period: 'March 2026', dueDate: '2026-03-01', status: 'paid', invoiceAmount: 3500, paidAmount: 3500, carrierRef: 'SG-2026-088-03', membersBilled: 88, membersExpected: 88, discrepancy: 0, notes: '' },
{ id: 'bill-002-03', period: 'April 2026', dueDate: '2026-04-01', status: 'paid', invoiceAmount: 3500, paidAmount: 3500, carrierRef: 'SG-2026-088-04', membersBilled: 88, membersExpected: 88, discrepancy: 0, notes: '' },
],
serviceRequests: [
{ id: 'sr-002-01', type: 'certificate', subject: 'Renovación de certificados anuales', status: 'resolved', priority: 'medium', assignee: 'María Fernanda Ortiz', created: '2026-02-10', updated: '2026-02-20', notes: 'Todos los certificados emitidos y entregados.' },
{ id: 'sr-002-02', type: 'amendment', subject: 'Actualización de coberturas de maternidad', status: 'resolved', priority: 'low', assignee: 'María Fernanda Ortiz', created: '2026-01-15', updated: '2026-02-01', notes: 'Endoso emitido por carrier.' },
],
recentActivity: [
{ date: '2026-04-01', text: 'Factura de Abril pagada a tiempo', type: 'billing', actor: 'Laura Paredes' },
{ date: '2026-03-28', text: 'Censo Q1 2026 cargado - sin cambios', type: 'document', actor: 'Laura Paredes' },
{ date: '2026-02-20', text: 'Certificados anuales entregados', type: 'service_request', actor: 'María Fernanda Ortiz' },
{ date: '2026-02-01', text: 'Endoso de maternidad procesado', type: 'endorsement', actor: 'Salud Global' },
{ date: '2026-01-15', text: 'Solicitud de actualización de coberturas', type: 'service_request', actor: 'Dr. Marcelo Insfrán' },
],
hasUrgentIssues: false,
outstandingClaims: 0,
pendingTasks: 0,
},
// ── 3. ITSA Corp ──
{
id: 'col-003',
name: 'ITSA Corp S.A.',
ruc: '80056789-3',
lob: 'Disability',
product: 'Protección Laboral Integral',
carrier: 'Continental Life',
status: 'onboarding',
contactName: 'Ing. Andrés Caballero',
contactEmail: 'acaballero@itsacorp.com.py',
contactPhone: '+595 21 620-1100',
hrContactName: 'Verónica Meza',
hrContactEmail: 'vmeza@itsacorp.com.py',
effectiveDate: '2026-05-01',
renewalDate: '2027-05-01',
onboardingDate: '2026-03-15',
totalMembers: 230,
activeMembersCount: 210,
dependentsCount: 0,
pendingEnrollment: 15,
monthlyPremium: 2625,
annualPremium: 31500,
commissionPct: 8,
agent: 'Carlos Villalba',
members: [
{ id: 'mbr-003-01', name: 'Ing. Andrés Caballero', documentId: '1.111.222', email: 'acaballero@itsacorp.com.py', phone: '+595 983 444-001', role: 'CEO', department: 'Dirección', enrollmentDate: '2026-03-20', status: 'active', tier: 'Executive', dependents: 0, pendingDocs: [], formsCompleted: 5, formsTotal: 5 },
{ id: 'mbr-003-02', name: 'Verónica Meza', documentId: '2.222.333', email: 'vmeza@itsacorp.com.py', phone: '+595 983 444-002', role: 'Gerente RRHH', department: 'RRHH', enrollmentDate: '2026-03-20', status: 'active', tier: 'Plus', dependents: 0, pendingDocs: [], formsCompleted: 5, formsTotal: 5 },
{ id: 'mbr-003-03', name: 'Diego Portillo', documentId: '3.333.444', email: 'dportillo@itsacorp.com.py', phone: '+595 983 444-003', role: 'Operario Línea A', department: 'Producción', enrollmentDate: '2026-03-25', status: 'pending_enrollment', tier: 'Basic', dependents: 0, pendingDocs: ['Declaración de salud', 'Copia de CI', 'Formulario de inscripción'], formsCompleted: 1, formsTotal: 5 },
{ id: 'mbr-003-04', name: 'Sandra Lezcano', documentId: '4.444.555', email: 'slezcano@itsacorp.com.py', phone: '+595 983 444-004', role: 'Supervisora de Calidad', department: 'Calidad', enrollmentDate: '2026-03-22', status: 'active', tier: 'Plus', dependents: 0, pendingDocs: [], formsCompleted: 5, formsTotal: 5 },
{ id: 'mbr-003-05', name: 'Ramón Villasboa', documentId: '5.555.666', email: 'rvillasboa@itsacorp.com.py', phone: '+595 983 444-005', role: 'Técnico de Mantenimiento', department: 'Mantenimiento', enrollmentDate: '2026-04-01', status: 'pending_enrollment', tier: 'Basic', dependents: 0, pendingDocs: ['Declaración de salud', 'Formulario de inscripción'], formsCompleted: 2, formsTotal: 5 },
{ id: 'mbr-003-06', name: 'Claudia Estigarribia', documentId: '6.666.777', email: 'cestigarribia@itsacorp.com.py', phone: '+595 983 444-006', role: 'Contadora', department: 'Finanzas', enrollmentDate: '2026-03-20', status: 'active', tier: 'Plus', dependents: 0, pendingDocs: [], formsCompleted: 5, formsTotal: 5 },
{ id: 'mbr-003-07', name: 'Miguel Ayala', documentId: '7.777.888', email: 'mayala@itsacorp.com.py', phone: '+595 983 444-007', role: 'Jefe de Planta', department: 'Producción', enrollmentDate: '2026-03-20', status: 'active', tier: 'Plus', dependents: 0, pendingDocs: [], formsCompleted: 5, formsTotal: 5 },
{ id: 'mbr-003-08', name: 'Lorena Cáceres', documentId: '8.888.999', email: 'lcaceres@itsacorp.com.py', phone: '+595 983 444-008', role: 'Asistente Administrativa', department: 'Administración', enrollmentDate: '2026-04-03', status: 'pending_docs', tier: 'Basic', dependents: 0, pendingDocs: ['Certificado de antecedentes'], formsCompleted: 4, formsTotal: 5 },
],
documents: [
{ id: 'doc-003-01', name: 'Propuesta Continental Life - Disability', category: 'contract', uploadedBy: 'Carlos Villalba', uploadedAt: '2026-03-10', fileSize: '2.8 MB', fileType: 'PDF', version: 1, notes: 'Propuesta aceptada por cliente' },
{ id: 'doc-003-02', name: 'Censo Inicial ITSA Corp', category: 'census', uploadedBy: 'Verónica Meza', uploadedAt: '2026-03-18', fileSize: '1.2 MB', fileType: 'XLSX', version: 2, notes: 'V2 - corregidos datos de 12 empleados' },
{ id: 'doc-003-03', name: 'Formularios de Inscripción (Lote 1)', category: 'enrollment', uploadedBy: 'Verónica Meza', uploadedAt: '2026-03-25', fileSize: '15.4 MB', fileType: 'PDF', version: 1, notes: '180 formularios escaneados' },
{ id: 'doc-003-04', name: 'Declaraciones de Salud (Lote 1)', category: 'enrollment', uploadedBy: 'Verónica Meza', uploadedAt: '2026-03-28', fileSize: '22.1 MB', fileType: 'PDF', version: 1, notes: '175 declaraciones recibidas' },
],
billingCycles: [
{ id: 'bill-003-01', period: 'May 2026', dueDate: '2026-05-01', status: 'upcoming', invoiceAmount: 2625, paidAmount: 0, carrierRef: '', membersBilled: 0, membersExpected: 230, discrepancy: 0, notes: 'Primer ciclo de facturación' },
{ id: 'bill-003-02', period: 'June 2026', dueDate: '2026-06-01', status: 'upcoming', invoiceAmount: 2625, paidAmount: 0, carrierRef: '', membersBilled: 0, membersExpected: 230, discrepancy: 0, notes: '' },
{ id: 'bill-003-03', period: 'July 2026', dueDate: '2026-07-01', status: 'upcoming', invoiceAmount: 2625, paidAmount: 0, carrierRef: '', membersBilled: 0, membersExpected: 230, discrepancy: 0, notes: '' },
],
serviceRequests: [
{ id: 'sr-003-01', type: 'inclusion', subject: 'Completar inscripción de 15 empleados pendientes', status: 'in_progress', priority: 'high', assignee: 'Carlos Villalba', created: '2026-04-01', updated: '2026-04-07', notes: 'RRHH está recopilando formularios faltantes. Fecha límite: 04/15.' },
{ id: 'sr-003-02', type: 'amendment', subject: 'Solicitud de inclusión de cobertura dental', status: 'open', priority: 'medium', assignee: 'Carlos Villalba', created: '2026-04-05', updated: '2026-04-05', notes: 'Cliente consulta costo adicional para rider dental.' },
{ id: 'sr-003-03', type: 'certificate', subject: 'Emisión de certificados individuales - Lote inicial', status: 'pending_carrier', priority: 'medium', assignee: 'Carlos Villalba', created: '2026-03-28', updated: '2026-04-03', notes: 'Carrier procesando 210 certificados.' },
],
recentActivity: [
{ date: '2026-04-07', text: 'Seguimiento de formularios pendientes con RRHH', type: 'onboarding', actor: 'Carlos Villalba' },
{ date: '2026-04-05', text: 'Nueva solicitud: consulta sobre rider dental', type: 'service_request', actor: 'Ing. Andrés Caballero' },
{ date: '2026-04-03', text: 'Certificados enviados a Continental Life para emisión', type: 'service_request', actor: 'Carlos Villalba' },
{ date: '2026-03-28', text: 'Lote 1 de declaraciones de salud cargado (175)', type: 'document', actor: 'Verónica Meza' },
{ date: '2026-03-25', text: 'Lote 1 de formularios de inscripción cargado (180)', type: 'document', actor: 'Verónica Meza' },
{ date: '2026-03-20', text: 'Onboarding iniciado - primeros empleados registrados', type: 'onboarding', actor: 'Carlos Villalba' },
],
hasUrgentIssues: false,
outstandingClaims: 0,
pendingTasks: 18,
},
// ── 4. Municipalidad Central ──
{
id: 'col-004',
name: 'Municipalidad Central',
ruc: '80078901-5',
lob: 'Health',
product: 'Salud Pública Municipal',
carrier: 'Vida Plena',
status: 'active',
contactName: 'Lic. Gustavo Ferreira',
contactEmail: 'gferreira@muniasuncion.gov.py',
contactPhone: '+595 21 440-5500',
hrContactName: 'Norma Jiménez',
hrContactEmail: 'njimenez@muniasuncion.gov.py',
effectiveDate: '2025-01-01',
renewalDate: '2026-01-01',
onboardingDate: '2024-11-15',
totalMembers: 640,
activeMembersCount: 625,
dependentsCount: 1120,
pendingEnrollment: 8,
monthlyPremium: 16500,
annualPremium: 198000,
commissionPct: 9,
agent: 'Carlos Villalba',
members: [
{ id: 'mbr-004-01', name: 'Lic. Gustavo Ferreira', documentId: '1.010.101', email: 'gferreira@muniasuncion.gov.py', phone: '+595 984 555-001', role: 'Secretario General', department: 'Secretaría General', enrollmentDate: '2025-01-01', status: 'active', tier: 'Executive', dependents: 4, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-004-02', name: 'Norma Jiménez', documentId: '2.020.202', email: 'njimenez@muniasuncion.gov.py', phone: '+595 984 555-002', role: 'Directora de RRHH', department: 'RRHH', enrollmentDate: '2025-01-01', status: 'active', tier: 'Plus', dependents: 3, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-004-03', name: 'Pedro Gauto', documentId: '3.030.303', email: 'pgauto@muniasuncion.gov.py', phone: '+595 984 555-003', role: 'Inspector de Obras', department: 'Obras Públicas', enrollmentDate: '2025-01-01', status: 'active', tier: 'Basic', dependents: 2, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-004-04', name: 'María Bogado', documentId: '4.040.404', email: 'mbogado@muniasuncion.gov.py', phone: '+595 984 555-004', role: 'Asistente Social', department: 'Acción Social', enrollmentDate: '2025-02-01', status: 'active', tier: 'Basic', dependents: 1, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-004-05', name: 'Juan Arce', documentId: '5.050.505', email: 'jarce@muniasuncion.gov.py', phone: '+595 984 555-005', role: 'Conductor', department: 'Transporte', enrollmentDate: '2025-01-15', status: 'active', tier: 'Basic', dependents: 3, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-004-06', name: 'Blanca Ovelar', documentId: '6.060.606', email: 'bovelar@muniasuncion.gov.py', phone: '+595 984 555-006', role: 'Contadora Municipal', department: 'Finanzas', enrollmentDate: '2025-01-01', status: 'active', tier: 'Plus', dependents: 2, pendingDocs: [], formsCompleted: 4, formsTotal: 4 },
{ id: 'mbr-004-07', name: 'Raúl Cabrera', documentId: '7.070.707', email: 'rcabrera@muniasuncion.gov.py', phone: '+595 984 555-007', role: 'Jardinero Municipal', department: 'Espacios Verdes', enrollmentDate: '2025-03-01', status: 'pending_docs', tier: 'Basic', dependents: 1, pendingDocs: ['Formulario de dependientes actualizado'], formsCompleted: 3, formsTotal: 4 },
],
documents: [
{ id: 'doc-004-01', name: 'Póliza Colectiva Municipal 2025', category: 'policy', uploadedBy: 'Carlos Villalba', uploadedAt: '2024-12-20', fileSize: '5.8 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-004-02', name: 'Convenio Marco Municipalidad-Brokerage', category: 'contract', uploadedBy: 'Carlos Villalba', uploadedAt: '2024-11-25', fileSize: '3.2 MB', fileType: 'PDF', version: 1, notes: 'Aprobado por resolución municipal #2024-1820' },
{ id: 'doc-004-03', name: 'Censo Febrero 2026', category: 'census', uploadedBy: 'Norma Jiménez', uploadedAt: '2026-02-28', fileSize: '2.1 MB', fileType: 'XLSX', version: 1, notes: '8 altas, 3 bajas' },
{ id: 'doc-004-04', name: 'Endoso #5 - Ajuste Feb 2026', category: 'endorsement', uploadedBy: 'Carlos Villalba', uploadedAt: '2026-03-10', fileSize: '280 KB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-004-05', name: 'Reporte Siniestralidad 2025 Anual', category: 'siniestralidad', uploadedBy: 'Vida Plena', uploadedAt: '2026-02-15', fileSize: '4.5 MB', fileType: 'PDF', version: 1, notes: 'Siniestralidad 82% - bandera amarilla' },
{ id: 'doc-004-06', name: 'Carta de Reclamo - Diferencia Facturación', category: 'correspondence', uploadedBy: 'Blanca Ovelar', uploadedAt: '2026-03-22', fileSize: '95 KB', fileType: 'PDF', version: 1, notes: 'Reclamo formal por discrepancia en marzo' },
{ id: 'doc-004-07', name: 'Endoso #6 - Inclusiones Mar 2026', category: 'endorsement', uploadedBy: 'Carlos Villalba', uploadedAt: '2026-04-01', fileSize: '310 KB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-004-08', name: 'Censo Marzo 2026', category: 'census', uploadedBy: 'Norma Jiménez', uploadedAt: '2026-03-31', fileSize: '2.2 MB', fileType: 'XLSX', version: 1, notes: '5 altas nuevas' },
],
billingCycles: [
{ id: 'bill-004-01', period: 'January 2026', dueDate: '2026-01-10', status: 'paid', invoiceAmount: 16200, paidAmount: 16200, carrierRef: 'VP-2026-0640-01', membersBilled: 635, membersExpected: 635, discrepancy: 0, notes: '' },
{ id: 'bill-004-02', period: 'February 2026', dueDate: '2026-02-10', status: 'paid', invoiceAmount: 16350, paidAmount: 16350, carrierRef: 'VP-2026-0640-02', membersBilled: 638, membersExpected: 638, discrepancy: 0, notes: '' },
{ id: 'bill-004-03', period: 'March 2026', dueDate: '2026-03-10', status: 'disputed', invoiceAmount: 16700, paidAmount: 0, carrierRef: 'VP-2026-0640-03', membersBilled: 648, membersExpected: 640, discrepancy: 8, notes: 'Carrier facturó 8 miembros de más. Cliente reclama diferencia de $208.' },
{ id: 'bill-004-04', period: 'April 2026', dueDate: '2026-04-10', status: 'overdue', invoiceAmount: 16500, paidAmount: 0, carrierRef: 'VP-2026-0640-04', membersBilled: 640, membersExpected: 640, discrepancy: 0, notes: 'Cliente retiene pago hasta resolución de disputa de Marzo.' },
{ id: 'bill-004-05', period: 'May 2026', dueDate: '2026-05-10', status: 'upcoming', invoiceAmount: 16500, paidAmount: 0, carrierRef: '', membersBilled: 0, membersExpected: 640, discrepancy: 0, notes: '' },
{ id: 'bill-004-06', period: 'June 2026', dueDate: '2026-06-10', status: 'upcoming', invoiceAmount: 16500, paidAmount: 0, carrierRef: '', membersBilled: 0, membersExpected: 640, discrepancy: 0, notes: '' },
],
serviceRequests: [
{ id: 'sr-004-01', type: 'billing', subject: 'Discrepancia facturación Marzo - 8 miembros de más', status: 'in_progress', priority: 'high', assignee: 'Carlos Villalba', created: '2026-03-15', updated: '2026-04-06', notes: 'Carrier reconoce error. Nota de crédito en proceso. Cliente retiene pago de Abril hasta resolución.' },
{ id: 'sr-004-02', type: 'inclusion', subject: 'Alta de 5 nuevos empleados - Marzo 2026', status: 'resolved', priority: 'medium', assignee: 'Carlos Villalba', created: '2026-03-05', updated: '2026-03-18', notes: 'Endoso procesado. Todos los certificados emitidos.' },
{ id: 'sr-004-03', type: 'exclusion', subject: 'Baja de 3 empleados retirados', status: 'resolved', priority: 'medium', assignee: 'Carlos Villalba', created: '2026-02-20', updated: '2026-03-01', notes: 'Bajas procesadas en endoso #5.' },
{ id: 'sr-004-04', type: 'claim', subject: 'Reclamo hospitalización - Expediente #7810', status: 'pending_carrier', priority: 'medium', assignee: 'Carlos Villalba', created: '2026-03-20', updated: '2026-04-02', memberName: 'Pedro Gauto', notes: 'Documentación completa enviada a carrier.' },
{ id: 'sr-004-05', type: 'amendment', subject: 'Solicitud de mejora de cobertura oftalmológica', status: 'open', priority: 'low', assignee: 'Carlos Villalba', created: '2026-04-03', updated: '2026-04-03', notes: 'Sindicato solicitó mejoras. Pendiente cotización de carrier.' },
],
recentActivity: [
{ date: '2026-04-06', text: 'Carrier confirmó nota de crédito en proceso por discrepancia Marzo', type: 'billing', actor: 'Vida Plena' },
{ date: '2026-04-03', text: 'Sindicato solicita mejora en cobertura oftalmológica', type: 'service_request', actor: 'Norma Jiménez' },
{ date: '2026-04-01', text: 'Endoso #6 procesado - inclusiones de Marzo', type: 'endorsement', actor: 'Carlos Villalba' },
{ date: '2026-03-31', text: 'Censo Marzo 2026 recibido', type: 'document', actor: 'Norma Jiménez' },
{ date: '2026-03-22', text: 'Carta formal de reclamo por diferencia en facturación', type: 'correspondence', actor: 'Blanca Ovelar' },
{ date: '2026-03-15', text: 'Discrepancia detectada en factura de Marzo: 8 miembros de más', type: 'billing', actor: 'Carlos Villalba' },
{ date: '2026-03-10', text: 'Endoso #5 procesado', type: 'endorsement', actor: 'Carlos Villalba' },
{ date: '2026-02-15', text: 'Reporte de siniestralidad anual 2025 recibido - 82%', type: 'document', actor: 'Vida Plena' },
],
hasUrgentIssues: false,
outstandingClaims: 1,
pendingTasks: 4,
},
// ── 5. Grupo Agrícola del Sur ──
{
id: 'col-005',
name: 'Grupo Agrícola del Sur S.A.',
ruc: '80090123-8',
lob: 'Life',
product: 'Vida Grupal Protección Familiar',
carrier: 'Seguros del Pacífico',
status: 'renewal_due',
contactName: 'Ing. Agr. Héctor Bogado',
contactEmail: 'hbogado@gagricsur.com.py',
contactPhone: '+595 71 205-600',
hrContactName: 'Celeste Riveros',
hrContactEmail: 'criveros@gagricsur.com.py',
effectiveDate: '2025-05-01',
renewalDate: '2026-05-01',
onboardingDate: '2025-04-10',
totalMembers: 175,
activeMembersCount: 170,
dependentsCount: 310,
pendingEnrollment: 0,
monthlyPremium: 4833,
annualPremium: 58000,
commissionPct: 11,
agent: 'María Fernanda Ortiz',
members: [
{ id: 'mbr-005-01', name: 'Ing. Agr. Héctor Bogado', documentId: '1.515.151', email: 'hbogado@gagricsur.com.py', phone: '+595 985 666-001', role: 'Director General', department: 'Dirección', enrollmentDate: '2025-05-01', status: 'active', tier: 'Executive', dependents: 4, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-005-02', name: 'Celeste Riveros', documentId: '2.525.252', email: 'criveros@gagricsur.com.py', phone: '+595 985 666-002', role: 'Gerente de RRHH', department: 'RRHH', enrollmentDate: '2025-05-01', status: 'active', tier: 'Plus', dependents: 2, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-005-03', name: 'Tomás Aquino', documentId: '3.535.353', email: 'taquino@gagricsur.com.py', phone: '+595 985 666-003', role: 'Capataz de Campo', department: 'Operaciones de Campo', enrollmentDate: '2025-05-01', status: 'active', tier: 'Basic', dependents: 3, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-005-04', name: 'Rosa Benítez', documentId: '4.545.454', email: 'rbenitez@gagricsur.com.py', phone: '+595 985 666-004', role: 'Ingeniera Agrónoma', department: 'Técnica', enrollmentDate: '2025-05-15', status: 'active', tier: 'Plus', dependents: 1, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-005-05', name: 'Óscar Domínguez', documentId: '5.555.565', email: 'odominguez@gagricsur.com.py', phone: '+595 985 666-005', role: 'Chofer de Camión', department: 'Logística', enrollmentDate: '2025-06-01', status: 'active', tier: 'Basic', dependents: 2, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-005-06', name: 'Luz Marina Espínola', documentId: '6.565.656', email: 'lespinola@gagricsur.com.py', phone: '+595 985 666-006', role: 'Contadora', department: 'Administración', enrollmentDate: '2025-05-01', status: 'active', tier: 'Plus', dependents: 1, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-005-07', name: 'Esteban Villalba', documentId: '7.575.757', email: 'evillalba@gagricsur.com.py', phone: '+595 985 666-007', role: 'Peón Rural', department: 'Operaciones de Campo', enrollmentDate: '2025-07-01', status: 'excluded', tier: 'Basic', dependents: 0, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
],
documents: [
{ id: 'doc-005-01', name: 'Póliza Vida Grupal 2025-2026', category: 'policy', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2025-04-28', fileSize: '3.5 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-005-02', name: 'Contrato de Intermediación', category: 'contract', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2025-04-15', fileSize: '1.6 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-005-03', name: 'Censo Actualizado Marzo 2026', category: 'census', uploadedBy: 'Celeste Riveros', uploadedAt: '2026-03-20', fileSize: '680 KB', fileType: 'XLSX', version: 1, notes: '1 exclusión (Villalba, E.) por renuncia' },
{ id: 'doc-005-04', name: 'Propuesta de Renovación 2026-2027', category: 'other', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2026-03-25', fileSize: '2.2 MB', fileType: 'PDF', version: 1, notes: 'Incluye 3 opciones de carrier' },
{ id: 'doc-005-05', name: 'Siniestralidad Acumulada 2025-2026', category: 'siniestralidad', uploadedBy: 'Seguros del Pacífico', uploadedAt: '2026-03-15', fileSize: '1.8 MB', fileType: 'PDF', version: 1, notes: 'Siniestralidad al 45% - muy favorable' },
],
billingCycles: [
{ id: 'bill-005-01', period: 'February 2026', dueDate: '2026-02-15', status: 'paid', invoiceAmount: 4833, paidAmount: 4833, carrierRef: 'SP-2026-0175-02', membersBilled: 175, membersExpected: 175, discrepancy: 0, notes: '' },
{ id: 'bill-005-02', period: 'March 2026', dueDate: '2026-03-15', status: 'paid', invoiceAmount: 4810, paidAmount: 4810, carrierRef: 'SP-2026-0175-03', membersBilled: 174, membersExpected: 174, discrepancy: 0, notes: '1 baja procesada' },
{ id: 'bill-005-03', period: 'April 2026', dueDate: '2026-04-15', status: 'invoiced', invoiceAmount: 4810, paidAmount: 0, carrierRef: 'SP-2026-0175-04', membersBilled: 174, membersExpected: 174, discrepancy: 0, notes: '' },
],
serviceRequests: [
{ id: 'sr-005-01', type: 'claim', subject: 'Reclamo fallecimiento - Beneficiario Flia. Aquino', status: 'in_progress', priority: 'high', assignee: 'María Fernanda Ortiz', created: '2026-02-10', updated: '2026-04-01', memberName: 'Tomás Aquino', notes: 'Documentación de siniestro completa. Carrier en revisión. Monto: Gs. 350.000.000.' },
{ id: 'sr-005-02', type: 'exclusion', subject: 'Baja por renuncia - Esteban Villalba', status: 'resolved', priority: 'low', assignee: 'María Fernanda Ortiz', created: '2026-03-01', updated: '2026-03-15', memberName: 'Esteban Villalba', notes: 'Procesado en endoso.' },
{ id: 'sr-005-03', type: 'amendment', subject: 'Propuesta de renovación 2026-2027 - Negociación', status: 'in_progress', priority: 'high', assignee: 'María Fernanda Ortiz', created: '2026-03-25', updated: '2026-04-05', notes: 'Presentadas 3 opciones. Cliente evaluando. Reunión programada para 04/12.' },
],
recentActivity: [
{ date: '2026-04-05', text: 'Seguimiento de propuesta de renovación con cliente', type: 'renewal', actor: 'María Fernanda Ortiz' },
{ date: '2026-04-01', text: 'Carrier actualiza estado de reclamo Flia. Aquino', type: 'claim_update', actor: 'Seguros del Pacífico' },
{ date: '2026-03-25', text: 'Propuesta de renovación enviada al cliente (3 opciones)', type: 'renewal', actor: 'María Fernanda Ortiz' },
{ date: '2026-03-20', text: 'Censo actualizado recibido', type: 'document', actor: 'Celeste Riveros' },
{ date: '2026-03-15', text: 'Reporte de siniestralidad recibido - 45%', type: 'document', actor: 'Seguros del Pacífico' },
{ date: '2026-03-01', text: 'Solicitud de baja: Esteban Villalba por renuncia', type: 'exclusion', actor: 'Celeste Riveros' },
],
hasUrgentIssues: false,
outstandingClaims: 1,
pendingTasks: 3,
},
// ── 6. Tech Solutions S.A. ──
{
id: 'col-006',
name: 'Tech Solutions S.A.',
ruc: '80101234-2',
lob: 'Health',
product: 'Salud Digital Premium',
carrier: 'Integral Medical',
status: 'active',
contactName: 'Lic. Pamela Giménez',
contactEmail: 'pgimenez@techsolutions.com.py',
contactPhone: '+595 21 730-8800',
hrContactName: 'Rodrigo Sanabria',
hrContactEmail: 'rsanabria@techsolutions.com.py',
effectiveDate: '2025-11-01',
renewalDate: '2026-11-01',
onboardingDate: '2025-10-15',
totalMembers: 62,
activeMembersCount: 60,
dependentsCount: 85,
pendingEnrollment: 2,
monthlyPremium: 3125,
annualPremium: 37500,
commissionPct: 10,
agent: 'María Fernanda Ortiz',
members: [
{ id: 'mbr-006-01', name: 'Lic. Pamela Giménez', documentId: '1.616.161', email: 'pgimenez@techsolutions.com.py', phone: '+595 986 777-001', role: 'CEO', department: 'Dirección', enrollmentDate: '2025-11-01', status: 'active', tier: 'Executive', dependents: 2, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-006-02', name: 'Rodrigo Sanabria', documentId: '2.626.262', email: 'rsanabria@techsolutions.com.py', phone: '+595 986 777-002', role: 'People & Culture Lead', department: 'People', enrollmentDate: '2025-11-01', status: 'active', tier: 'Plus', dependents: 1, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-006-03', name: 'Matías Recalde', documentId: '3.636.363', email: 'mrecalde@techsolutions.com.py', phone: '+595 986 777-003', role: 'CTO', department: 'Engineering', enrollmentDate: '2025-11-01', status: 'active', tier: 'Executive', dependents: 3, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-006-04', name: 'Sofía Cardozo', documentId: '4.646.464', email: 'scardozo@techsolutions.com.py', phone: '+595 986 777-004', role: 'UX Designer', department: 'Design', enrollmentDate: '2025-11-15', status: 'active', tier: 'Plus', dependents: 0, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-006-05', name: 'Alejandro Núñez', documentId: '5.656.565', email: 'anunez@techsolutions.com.py', phone: '+595 986 777-005', role: 'Full Stack Developer', department: 'Engineering', enrollmentDate: '2025-12-01', status: 'active', tier: 'Basic', dependents: 1, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-006-06', name: 'Valeria Ocampos', documentId: '6.666.676', email: 'vocampos@techsolutions.com.py', phone: '+595 986 777-006', role: 'QA Engineer', department: 'Engineering', enrollmentDate: '2026-01-15', status: 'active', tier: 'Basic', dependents: 0, pendingDocs: [], formsCompleted: 3, formsTotal: 3 },
{ id: 'mbr-006-07', name: 'Nicolás Franco', documentId: '7.676.767', email: 'nfranco@techsolutions.com.py', phone: '+595 986 777-007', role: 'DevOps Engineer', department: 'Engineering', enrollmentDate: '2026-03-01', status: 'pending_enrollment', tier: 'Basic', dependents: 0, pendingDocs: ['Formulario de inscripción'], formsCompleted: 2, formsTotal: 3 },
{ id: 'mbr-006-08', name: 'Carolina Espínola', documentId: '8.686.868', email: 'cespinola@techsolutions.com.py', phone: '+595 986 777-008', role: 'Product Manager', department: 'Product', enrollmentDate: '2026-03-15', status: 'pending_enrollment', tier: 'Plus', dependents: 1, pendingDocs: ['Formulario de inscripción', 'Declaración de salud'], formsCompleted: 1, formsTotal: 3 },
],
documents: [
{ id: 'doc-006-01', name: 'Póliza Salud Digital Premium 2025-2026', category: 'policy', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2025-10-28', fileSize: '2.9 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-006-02', name: 'Contrato de Intermediación', category: 'contract', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2025-10-20', fileSize: '1.3 MB', fileType: 'PDF', version: 1, notes: '' },
{ id: 'doc-006-03', name: 'Censo Q1 2026', category: 'census', uploadedBy: 'Rodrigo Sanabria', uploadedAt: '2026-03-30', fileSize: '310 KB', fileType: 'XLSX', version: 1, notes: '2 nuevas altas pendientes' },
{ id: 'doc-006-04', name: 'Endoso #2 - Inclusiones Ene 2026', category: 'endorsement', uploadedBy: 'María Fernanda Ortiz', uploadedAt: '2026-01-25', fileSize: '185 KB', fileType: 'PDF', version: 1, notes: '' },
],
billingCycles: [
{ id: 'bill-006-01', period: 'February 2026', dueDate: '2026-02-05', status: 'paid', invoiceAmount: 3050, paidAmount: 3050, carrierRef: 'IM-2026-062-02', membersBilled: 60, membersExpected: 60, discrepancy: 0, notes: '' },
{ id: 'bill-006-02', period: 'March 2026', dueDate: '2026-03-05', status: 'paid', invoiceAmount: 3050, paidAmount: 3050, carrierRef: 'IM-2026-062-03', membersBilled: 60, membersExpected: 60, discrepancy: 0, notes: '' },
{ id: 'bill-006-03', period: 'April 2026', dueDate: '2026-04-05', status: 'paid', invoiceAmount: 3125, paidAmount: 3125, carrierRef: 'IM-2026-062-04', membersBilled: 62, membersExpected: 62, discrepancy: 0, notes: 'Incluye 2 nuevos miembros pendientes de inscripción formal' },
],
serviceRequests: [
{ id: 'sr-006-01', type: 'inclusion', subject: 'Alta de 2 nuevos empleados - Marzo 2026', status: 'in_progress', priority: 'medium', assignee: 'María Fernanda Ortiz', created: '2026-03-10', updated: '2026-04-02', notes: 'Faltan formularios de inscripción. People & Culture dará seguimiento.' },
{ id: 'sr-006-02', type: 'certificate', subject: 'Certificado para trámite de visa - S. Cardozo', status: 'resolved', priority: 'medium', assignee: 'María Fernanda Ortiz', created: '2026-03-18', updated: '2026-03-22', memberName: 'Sofía Cardozo', notes: 'Certificado emitido y enviado.' },
],
recentActivity: [
{ date: '2026-04-05', text: 'Factura Abril pagada a tiempo', type: 'billing', actor: 'Rodrigo Sanabria' },
{ date: '2026-04-02', text: 'Seguimiento de formularios pendientes para nuevas altas', type: 'service_request', actor: 'María Fernanda Ortiz' },
{ date: '2026-03-30', text: 'Censo Q1 2026 cargado', type: 'document', actor: 'Rodrigo Sanabria' },
{ date: '2026-03-22', text: 'Certificado de visa emitido para Sofía Cardozo', type: 'service_request', actor: 'María Fernanda Ortiz' },
{ date: '2026-03-10', text: 'Solicitud de alta para 2 nuevos empleados', type: 'inclusion', actor: 'Rodrigo Sanabria' },
],
hasUrgentIssues: false,
outstandingClaims: 0,
pendingTasks: 2,
},
]
}
/* ── Composable ── */
const KEY = 'policy-ui-colectivos-v1'
export function useColectivos() {
const accounts = useLocalStorageRef<ColectivoAccount[]>(KEY, buildDefaultAccounts)
/* ── Lookups ── */
function getAccount(id: string): ColectivoAccount | undefined {
return accounts.value.find(a => a.id === id)
}
function getAccountMembers(accountId: string): ColectivoMember[] {
return getAccount(accountId)?.members ?? []
}
function getAccountServiceRequests(accountId: string): ServiceRequest[] {
return getAccount(accountId)?.serviceRequests ?? []
}
/* ── Filtered lists ── */
const activeAccounts = computed(() =>
accounts.value.filter(a => a.status === 'active'),
)
const onboardingAccounts = computed(() =>
accounts.value.filter(a => a.status === 'onboarding'),
)
/* ── Aggregate stats ── */
const totalMembers = computed(() =>
accounts.value.reduce((sum, a) => sum + a.totalMembers, 0),
)
const totalDependents = computed(() =>
accounts.value.reduce((sum, a) => sum + a.dependentsCount, 0),
)
const totalPremium = computed(() =>
accounts.value.reduce((sum, a) => sum + a.annualPremium, 0),
)
const urgentIssuesCount = computed(() =>
accounts.value.filter(a => a.hasUrgentIssues).length,
)
return {
accounts,
getAccount,
getAccountMembers,
getAccountServiceRequests,
activeAccounts,
onboardingAccounts,
totalMembers,
totalDependents,
totalPremium,
urgentIssuesCount,
}
}

View File

@@ -1,228 +0,0 @@
import { computed } from 'vue'
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
/* ── Types ── */
export type ServiceTierId = string
export interface ServiceTier {
id: ServiceTierId
name: string
color: string
icon: string
description: string
minScore: number
benefits: string[]
}
export interface AttentionRule {
id: string
field: 'premium' | 'policy_count' | 'commission' | 'collectivo_member' | 'multi_line' | 'tenure_years' | 'has_private_policies'
operator: 'gte' | 'lte' | 'eq' | 'gt' | 'lt'
value: number | boolean
points: number
label: string
}
export interface CustomerAttentionConfig {
tiers: ServiceTier[]
rules: AttentionRule[]
autoClassify: boolean
}
/* ── Defaults ── */
function defaultTiers(): ServiceTier[] {
return [
{
id: 'platinum',
name: 'Platinum',
color: '#7c3aed',
icon: 'i-heroicons-star',
description: 'VIP multi-line clients with highest lifetime value',
minScore: 80,
benefits: ['Priority claims handling', 'Dedicated account manager', 'Annual review', 'Renewal negotiation priority'],
},
{
id: 'gold',
name: 'Gold',
color: '#d4a017',
icon: 'i-heroicons-trophy',
description: 'Established clients with strong portfolio',
minScore: 55,
benefits: ['Priority support', 'Proactive renewal outreach', 'Cross-sell consultation'],
},
{
id: 'silver',
name: 'Silver',
color: '#6b7280',
icon: 'i-heroicons-shield-check',
description: 'Active clients with growth potential',
minScore: 30,
benefits: ['Standard support', 'Regular check-ins'],
},
{
id: 'standard',
name: 'Standard',
color: '#01696f',
icon: 'i-heroicons-user',
description: 'All active customers',
minScore: 0,
benefits: ['Standard service'],
},
]
}
function defaultRules(): AttentionRule[] {
return [
{ id: 'r1', field: 'premium', operator: 'gte', value: 5000, points: 25, label: 'Annual premium >= $5,000' },
{ id: 'r2', field: 'premium', operator: 'gte', value: 10000, points: 40, label: 'Annual premium >= $10,000' },
{ id: 'r3', field: 'policy_count', operator: 'gte', value: 3, points: 15, label: '3+ active policies' },
{ id: 'r4', field: 'policy_count', operator: 'gte', value: 5, points: 25, label: '5+ active policies' },
{ id: 'r5', field: 'multi_line', operator: 'gte', value: 3, points: 20, label: 'Multi-line (3+ different lines)' },
{ id: 'r6', field: 'tenure_years', operator: 'gte', value: 3, points: 10, label: 'Client for 3+ years' },
{ id: 'r7', field: 'tenure_years', operator: 'gte', value: 5, points: 20, label: 'Client for 5+ years' },
{ id: 'r8', field: 'collectivo_member', operator: 'eq', value: true, points: 15, label: 'Collectivo member with private policies' },
{ id: 'r9', field: 'commission', operator: 'gte', value: 1000, points: 10, label: 'Annual commission >= $1,000' },
]
}
function defaultConfig(): CustomerAttentionConfig {
return {
tiers: defaultTiers(),
rules: defaultRules(),
autoClassify: true,
}
}
/* ── Customer input shape ── */
export interface CustomerAttentionInput {
totalPremium: number
policyCount: number
lineCount: number
tenureYears: number
isCollectivoMember: boolean
hasPrivatePolicies: boolean
estimatedCommission: number
}
/* ── Rule evaluation ── */
function evaluateRule(rule: AttentionRule, customer: CustomerAttentionInput): boolean {
let fieldValue: number | boolean
switch (rule.field) {
case 'premium':
fieldValue = customer.totalPremium
break
case 'policy_count':
fieldValue = customer.policyCount
break
case 'commission':
fieldValue = customer.estimatedCommission
break
case 'multi_line':
fieldValue = customer.lineCount
break
case 'tenure_years':
fieldValue = customer.tenureYears
break
case 'collectivo_member':
// Special: collectivo member rule only matches if also has private policies
return rule.value === true && customer.isCollectivoMember && customer.hasPrivatePolicies
case 'has_private_policies':
return typeof rule.value === 'boolean' ? customer.hasPrivatePolicies === rule.value : false
default:
return false
}
if (typeof fieldValue === 'boolean' || typeof rule.value === 'boolean') return false
switch (rule.operator) {
case 'gte': return (fieldValue as number) >= (rule.value as number)
case 'gt': return (fieldValue as number) > (rule.value as number)
case 'lte': return (fieldValue as number) <= (rule.value as number)
case 'lt': return (fieldValue as number) < (rule.value as number)
case 'eq': return (fieldValue as number) === (rule.value as number)
default: return false
}
}
/* ── Composable ── */
export function useCustomerAttention() {
const config = useLocalStorageRef<CustomerAttentionConfig>('policy-ui-customer-attention-v1', defaultConfig)
const tiers = computed(() => [...config.value.tiers].sort((a, b) => b.minScore - a.minScore))
const rules = computed(() => config.value.rules)
function getScoreForCustomer(customer: CustomerAttentionInput): number {
let score = 0
for (const rule of config.value.rules) {
if (evaluateRule(rule, customer)) {
score += rule.points
}
}
return score
}
function getTierForCustomer(customer: CustomerAttentionInput): ServiceTier {
const score = getScoreForCustomer(customer)
const sorted = [...config.value.tiers].sort((a, b) => b.minScore - a.minScore)
for (const tier of sorted) {
if (score >= tier.minScore) return tier
}
// Fallback to lowest tier
return sorted[sorted.length - 1] ?? config.value.tiers[0]
}
/* ── Tier CRUD ── */
function addTier(tier: ServiceTier) {
config.value.tiers.push(tier)
}
function updateTier(id: string, patch: Partial<ServiceTier>) {
const idx = config.value.tiers.findIndex(t => t.id === id)
if (idx !== -1) {
config.value.tiers[idx] = { ...config.value.tiers[idx], ...patch }
}
}
function removeTier(id: string) {
config.value.tiers = config.value.tiers.filter(t => t.id !== id)
}
/* ── Rule CRUD ── */
function addRule(rule: AttentionRule) {
config.value.rules.push(rule)
}
function updateRule(id: string, patch: Partial<AttentionRule>) {
const idx = config.value.rules.findIndex(r => r.id === id)
if (idx !== -1) {
config.value.rules[idx] = { ...config.value.rules[idx], ...patch }
}
}
function removeRule(id: string) {
config.value.rules = config.value.rules.filter(r => r.id !== id)
}
return {
config,
tiers,
rules,
getScoreForCustomer,
getTierForCustomer,
addTier,
updateTier,
removeTier,
addRule,
updateRule,
removeRule,
}
}

View File

@@ -1,18 +0,0 @@
import { emptyCustomerProfile, type CustomerProfileVault } from '~/types/customer-profile'
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
const KEY = 'policy-ui-customer-profile-vault-v1'
export function useCustomerProfileVault() {
const profile = useLocalStorageRef(KEY, () => emptyCustomerProfile())
function touch() {
profile.value.updatedAt = new Date().toISOString()
}
function reset() {
profile.value = emptyCustomerProfile()
}
return { profile, touch, reset }
}

View File

@@ -1,137 +0,0 @@
/**
* Composable for managing customer selection in quote flows
* Handles insured and buyer selection with validation
*/
export function useCustomerSelection() {
const selectedCustomer = ref<any>(null) // Auto-generated type from useCustomer
const useSameForBuyer = ref(true)
const selectedBuyer = ref<any>(null)
/**
* Convert customer-service customer to policy-service insured/buyer
* Maps customer fields to policy-service structure
*/
const toPolicyPerson = (customer: any) => {
if (customer.customer_type === 'corporate') {
return {
type: 'corporate',
company_name: customer.legal_name,
ruc: customer.ruc,
legal_rep_name: customer.legal_rep_name,
legal_rep_document: customer.legal_rep_document_id,
email: customer.email,
phone: customer.phone,
address: customer.address
}
}
return {
type: 'individual',
name: `${customer.first_name} ${customer.last_name}`.trim(),
date_of_birth: customer.birth_date,
document_id: customer.document_id,
email: customer.email,
phone: customer.phone,
address: customer.address
}
}
/**
* Get insured person from selected customer
*/
const insured = computed(() => {
if (!selectedCustomer.value) return null
return toPolicyPerson(selectedCustomer.value)
})
/**
* Get buyer person (either same as insured or different)
*/
const buyer = computed(() => {
if (useSameForBuyer.value) {
return insured.value
}
if (!selectedBuyer.value) return null
return toPolicyPerson(selectedBuyer.value)
})
/**
* Validate customer has required fields for policy submission
*/
const validateCustomer = (customer: any) => {
const missing: string[] = []
if (customer.customer_type === 'corporate') {
if (!customer.legal_name) missing.push('legal_name')
if (!customer.ruc) missing.push('ruc')
if (!customer.legal_rep_name) missing.push('legal_rep_name')
if (!customer.legal_rep_document_id) missing.push('legal_rep_document_id')
} else {
if (!customer.first_name) missing.push('first_name')
if (!customer.last_name) missing.push('last_name')
if (!customer.birth_date) missing.push('birth_date')
if (!customer.document_id) missing.push('document_id')
}
return { valid: missing.length === 0, missing }
}
/**
* Check if insured is valid
*/
const isInsuredValid = computed(() => {
if (!selectedCustomer.value) return false
return validateCustomer(selectedCustomer.value).valid
})
/**
* Check if buyer is valid
*/
const isBuyerValid = computed(() => {
if (useSameForBuyer.value) {
return isInsuredValid.value
}
if (!selectedBuyer.value) return false
return validateCustomer(selectedBuyer.value).valid
})
/**
* Get validation errors
*/
const validationErrors = computed(() => {
const errors: { insured: string[]; buyer: string[] } = { insured: [], buyer: [] }
if (selectedCustomer.value) {
const validation = validateCustomer(selectedCustomer.value)
errors.insured = validation.missing
}
if (!useSameForBuyer.value && selectedBuyer.value) {
const validation = validateCustomer(selectedBuyer.value)
errors.buyer = validation.missing
}
return errors
})
/**
* Reset selection
*/
function reset() {
selectedCustomer.value = null
selectedBuyer.value = null
useSameForBuyer.value = true
}
return {
selectedCustomer,
selectedBuyer,
useSameForBuyer,
insured,
buyer,
isInsuredValid,
isBuyerValid,
validationErrors,
reset
}
}

View File

@@ -1,273 +0,0 @@
/**
* Home dashboard widget visibility — role presets + per-widget toggles.
* Persisted locally until per-user API exists.
*/
export type DashboardWidgetId =
| 'hero'
| 'milestone'
| 'performance'
| 'tasks_alerts'
| 'charts'
| 'brokerage_health'
| 'quotes_line'
| 'notes'
| 'calendar'
| 'quick_leads'
| 'sales_leads'
| 'client_favorites'
| 'drafts'
export type DashboardRolePresetId =
| 'sales_manager'
| 'executive_manager'
| 'director'
| 'financial'
| 'admin_manager'
| 'customer_service_manager'
export type DashboardWidgetMeta = {
id: DashboardWidgetId
label: string
description: string
}
export const DASHBOARD_WIDGETS: DashboardWidgetMeta[] = [
{ id: 'hero', label: 'Welcome banner', description: 'Greeting, CTAs, workspace strip' },
{ id: 'milestone', label: 'MTD milestone', description: 'Plan vs actual snapshot' },
{ id: 'tasks_alerts', label: 'Tasks & alerts', description: 'Daily work + exceptions' },
{ id: 'performance', label: 'Today at a glance', description: 'Headline KPIs + sparklines' },
{ id: 'charts', label: 'Charts', description: 'GWP trend & quoted pipeline' },
{ id: 'brokerage_health', label: 'Brokerage health', description: 'YTD / trailing book metrics' },
{ id: 'quotes_line', label: 'Sent quotes', description: 'Sortable list of quotes sent to clients' },
{ id: 'notes', label: 'Notes', description: 'Personal scratchpad and reminders' },
{ id: 'calendar', label: 'Calendar', description: 'Agenda, renewals, alerts & reminders' },
{ id: 'quick_leads', label: 'Quick leads', description: 'Recent quick leads from the last 10 days' },
{ id: 'sales_leads', label: 'Sales leads', description: 'All leads by source — filter by channel, campaign, or API' },
{ id: 'client_favorites', label: 'Favorite clients', description: 'Starred clients for quick access' },
{ id: 'drafts', label: 'Drafts', description: 'Resume in-progress quotes, solicitudes & registrations' }
]
const STORAGE_KEY = 'policy-ui.dashboard.widgets.v4'
export const DEFAULT_WIDGET_ORDER: DashboardWidgetId[] = DASHBOARD_WIDGETS.map((w) => w.id)
function normalizeWidgetOrder(raw: unknown): DashboardWidgetId[] {
const base = [...DEFAULT_WIDGET_ORDER]
if (!Array.isArray(raw)) return base
const seen = new Set<DashboardWidgetId>()
const out: DashboardWidgetId[] = []
for (const x of raw) {
if (typeof x === 'string' && base.includes(x as DashboardWidgetId) && !seen.has(x as DashboardWidgetId)) {
const id = x as DashboardWidgetId
seen.add(id)
out.push(id)
}
}
for (const id of base) {
if (!seen.has(id)) out.push(id)
}
return out
}
const ALL_ON: Record<DashboardWidgetId, boolean> = {
hero: true,
milestone: true,
performance: false,
tasks_alerts: true,
charts: true,
brokerage_health: true,
quotes_line: true,
notes: true,
calendar: true,
quick_leads: true,
sales_leads: true,
client_favorites: true,
drafts: true
}
export const DASHBOARD_ROLE_PRESETS: Record<
DashboardRolePresetId,
{ label: string; hint: string; widgets: Record<DashboardWidgetId, boolean> }
> = {
sales_manager: {
label: 'Sales manager',
hint: 'Pipeline, tasks, quotes — lighter book-of-business tile.',
widgets: { ...ALL_ON, brokerage_health: false }
},
executive_manager: {
label: 'Executive manager',
hint: 'Balanced operational + book view.',
widgets: { ...ALL_ON }
},
director: {
label: 'Director',
hint: 'Strategic KPIs & health; fewer operational tiles.',
widgets: {
...ALL_ON,
tasks_alerts: false,
quotes_line: false
}
},
financial: {
label: 'Financial',
hint: 'Premium, AR, health metrics; fewer sales shortcuts.',
widgets: {
...ALL_ON,
quotes_line: false,
tasks_alerts: true,
performance: false,
brokerage_health: true,
charts: true,
sales_leads: false
}
},
admin_manager: {
label: 'Admin / operations',
hint: 'Permissions, forms, and carrier setup — fewer quote shortcuts.',
widgets: {
...ALL_ON,
quotes_line: false,
charts: false,
brokerage_health: true,
sales_leads: false
}
},
customer_service_manager: {
label: 'Customer service manager',
hint: 'Queues, tasks, and exceptions — lighter GWP / book tiles.',
widgets: {
...ALL_ON,
charts: false,
brokerage_health: false,
quotes_line: false
}
}
}
/** Stable order for selects. */
export const DASHBOARD_PRESET_ORDER: DashboardRolePresetId[] = [
'sales_manager',
'executive_manager',
'director',
'financial',
'admin_manager',
'customer_service_manager'
]
function cloneWidgets(w: Record<DashboardWidgetId, boolean>): Record<DashboardWidgetId, boolean> {
return { ...w }
}
export function useDashboardHomeWidgets() {
const activePreset = ref<DashboardRolePresetId>('executive_manager')
const widgets = ref<Record<DashboardWidgetId, boolean>>(
cloneWidgets(DASHBOARD_ROLE_PRESETS.executive_manager.widgets)
)
const widgetOrder = ref<DashboardWidgetId[]>([...DEFAULT_WIDGET_ORDER])
const layoutUnlocked = ref(false)
const hydrated = ref(false)
function persist() {
if (typeof localStorage === 'undefined') return
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
preset: activePreset.value,
widgets: widgets.value,
widgetOrder: widgetOrder.value,
layoutUnlocked: layoutUnlocked.value
})
)
} catch {
/* quota */
}
}
function load() {
if (typeof localStorage === 'undefined') return
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return
const data = JSON.parse(raw) as {
preset?: DashboardRolePresetId
widgets?: Partial<Record<DashboardWidgetId, boolean>>
widgetOrder?: DashboardWidgetId[]
layoutUnlocked?: boolean
}
if (data.preset && DASHBOARD_ROLE_PRESETS[data.preset]) {
activePreset.value = data.preset
}
if (data.widgets) {
const merged = { ...widgets.value }
for (const k of Object.keys(merged) as DashboardWidgetId[]) {
if (data.widgets[k] !== undefined) merged[k] = data.widgets[k]!
}
widgets.value = merged
}
widgetOrder.value = normalizeWidgetOrder(data.widgetOrder)
if (typeof data.layoutUnlocked === 'boolean') {
layoutUnlocked.value = data.layoutUnlocked
}
} catch {
/* ignore */
}
}
onMounted(() => {
load()
hydrated.value = true
})
watch(
[activePreset, widgets, widgetOrder, layoutUnlocked],
() => {
if (hydrated.value) persist()
},
{ deep: true }
)
const isPresetDirty = computed(() => {
const preset = DASHBOARD_ROLE_PRESETS[activePreset.value]
if (!preset) return false
return DASHBOARD_WIDGETS.some((w) => widgets.value[w.id] !== preset.widgets[w.id])
})
function applyPreset(id: DashboardRolePresetId) {
activePreset.value = id
const p = DASHBOARD_ROLE_PRESETS[id]
if (p) widgets.value = cloneWidgets(p.widgets)
}
function setWidget(id: DashboardWidgetId, on: boolean) {
widgets.value = { ...widgets.value, [id]: on }
}
function reapplySelectedPreset() {
applyPreset(activePreset.value)
}
function reorderWidgets(fromId: DashboardWidgetId, toId: DashboardWidgetId) {
if (fromId === toId) return
const arr = [...widgetOrder.value]
const fromI = arr.indexOf(fromId)
const toI = arr.indexOf(toId)
if (fromI === -1 || toI === -1) return
arr.splice(fromI, 1)
arr.splice(toI, 0, fromId)
widgetOrder.value = arr
}
return {
widgets,
widgetOrder,
layoutUnlocked,
activePreset,
isPresetDirty,
applyPreset,
setWidget,
reapplySelectedPreset,
reorderWidgets
}
}

View File

@@ -1,52 +0,0 @@
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
export type EmissionItem = {
id: string
createdAt: string
customerLabel: string
insurerSlug: string
subRamoKey: string
productLine: string
status: 'pending_review' | 'approved' | 'sent_to_insurer' | 'in_force'
bindToken?: string
/** 'auto' = generated from quote acceptance, 'manual' = created from solicitud form */
source?: 'auto' | 'manual'
/** Carrier product name when auto-generated from comparative */
carrierProduct?: string
}
const KEY = 'policy-ui-emissions-queue-v1'
export function useEmissionsQueue() {
const items = useLocalStorageRef<EmissionItem[]>(KEY, () => [])
function enqueue(
entry: Omit<EmissionItem, 'id' | 'createdAt' | 'status'> & { status?: EmissionItem['status'] }
) {
const row: EmissionItem = {
id: crypto.randomUUID?.() ?? String(Date.now()),
createdAt: new Date().toISOString(),
status: entry.status ?? 'pending_review',
...entry
}
items.value = [row, ...items.value]
return row
}
function approve(id: string) {
const i = items.value.find((x) => x.id === id)
if (i) i.status = 'approved'
}
function sendToInsurer(id: string) {
const i = items.value.find((x) => x.id === id)
if (i) i.status = 'sent_to_insurer'
}
function markInForce(id: string) {
const i = items.value.find((x) => x.id === id)
if (i) i.status = 'in_force'
}
return { items, enqueue, approve, sendToInsurer, markInForce }
}

View File

@@ -1,174 +0,0 @@
import catalogJson from '~/data/forms-catalog.json'
import fieldGroupsJson from '~/data/form-field-groups.json'
import type {
FormCatalogFile,
FormCatalogProductLine,
FormCatalogRow,
FormCatalogSelection
} from '~/types/form-catalog'
import type { FormFieldGroupDef, FormFieldGroupsFile } from '~/types/form-field-groups'
const catalog = catalogJson as FormCatalogFile
const fieldGroupsFile = fieldGroupsJson as FormFieldGroupsFile
const PRODUCT_LINE_LABELS: Record<FormCatalogProductLine, string> = {
life: 'Life',
health_local: 'Health · local',
health_international: 'Health · international',
auto_full_coverage: 'Auto · full coverage',
auto_dat_liability: 'Auto · DAT (liability)',
home: 'Home',
general_liability: 'General liability',
any: 'Any / not specified'
}
const ALL_PRODUCT_LINES: FormCatalogProductLine[] = [
'life',
'health_local',
'health_international',
'auto_full_coverage',
'auto_dat_liability',
'home',
'general_liability',
'any'
]
export function productLineLabel(line: FormCatalogProductLine | null | undefined): string {
if (line == null || line === 'any') return '—'
return PRODUCT_LINE_LABELS[line] ?? String(line)
}
function personMatches(row: FormCatalogRow, person: 'natural' | 'juridica'): boolean {
if (row.personKinds === 'both') return true
return row.personKinds === person
}
function productLineMatches(row: FormCatalogRow, sel: FormCatalogSelection): boolean {
const rowPl = row.productLine
const selPl = sel.productLine
if (selPl === null || selPl === 'any') {
return rowPl == null
}
if (rowPl == null) return true
return rowPl === selPl
}
function subRamoMatches(row: FormCatalogRow, subRamoKey: string | null): boolean {
if (!subRamoKey) return false
if (row.subRamoKey === 'any') return true
return row.subRamoKey === subRamoKey
}
export function filterRows(all: FormCatalogRow[], sel: FormCatalogSelection): FormCatalogRow[] {
if (!sel.insurerSlug || !sel.subRamoKey) return []
return all.filter((row) => {
if (!row.insurerSlugs.includes(sel.insurerSlug!)) return false
if (!subRamoMatches(row, sel.subRamoKey)) return false
if (!personMatches(row, sel.personKind)) return false
if (!productLineMatches(row, sel)) return false
return true
})
}
export function resolveFieldGroupsForRows(
matched: FormCatalogRow[],
groupMap: Map<string, FormFieldGroupDef>
): FormFieldGroupDef[] {
const ids = new Set<string>()
for (const r of matched) {
for (const id of r.fieldGroupIds ?? []) ids.add(id)
}
return [...ids]
.map((id) => groupMap.get(id))
.filter((g): g is FormFieldGroupDef => g != null)
}
export function buildFormMapIndex(rows: FormCatalogRow[]): Map<string, number[]> {
const m = new Map<string, number[]>()
for (const r of rows) {
for (const ins of r.insurerSlugs) {
const pl = r.productLine ?? ''
const key = `${ins}|${r.subRamoKey}|${pl}`
const list = m.get(key) ?? []
list.push(r.id)
m.set(key, list)
}
}
return m
}
function insurerSlugToLabel(slug: string): string {
return slug
.split('_')
.map((w) => w.slice(0, 1).toUpperCase() + w.slice(1))
.join(' ')
}
function buildInsurerItems(rows: FormCatalogRow[]) {
const set = new Set<string>()
for (const r of rows) {
for (const s of r.insurerSlugs) set.add(s)
}
return [...set]
.sort()
.map((value) => ({ label: insurerSlugToLabel(value), value }))
}
function buildSubRamoItems(rows: FormCatalogRow[], insurerSlug: string | null) {
if (!insurerSlug) return []
const map = new Map<string, string>()
for (const r of rows) {
if (!r.insurerSlugs.includes(insurerSlug)) continue
if (r.subRamoKey === 'any') continue
map.set(r.subRamoKey, r.subRamoLabel)
}
return [...map.entries()]
.sort((a, b) => a[1].localeCompare(b[1]))
.map(([value, label]) => ({ label, value }))
}
function productLineSelectOptions() {
return ALL_PRODUCT_LINES.map((value) => ({
label: PRODUCT_LINE_LABELS[value],
value
}))
}
export function useFormsCatalog() {
const rows = computed(() => catalog.rows)
const version = computed(() => catalog.version)
const groupById = computed(() => {
const m = new Map<string, FormFieldGroupDef>()
for (const g of fieldGroupsFile.groups) m.set(g.id, g)
return m
})
const insurerItems = computed(() => buildInsurerItems(catalog.rows))
function subRamoItems(insurerSlug: string | null) {
return buildSubRamoItems(catalog.rows, insurerSlug)
}
const productLineItems = productLineSelectOptions()
function fieldGroupsForMatched(matched: FormCatalogRow[]) {
return resolveFieldGroupsForRows(matched, groupById.value)
}
return {
catalog,
rows,
version,
fieldGroupsVersion: computed(() => fieldGroupsFile.version),
filterRows: (sel: FormCatalogSelection) => filterRows(catalog.rows, sel),
fieldGroupsForMatched,
buildFormMapIndex: () => buildFormMapIndex(catalog.rows),
insurerItems,
subRamoItems,
productLineItems,
productLineLabel,
insurerSlugToLabel
}
}

View File

@@ -1,33 +0,0 @@
import type { HealthQuoteDraft } from '~/types/health-quote-intake'
export function emptyHealthQuoteDraft(): HealthQuoteDraft {
return {
quoteMode: null,
segment: null,
client: {
fullName: '',
email: '',
phone: '',
documentId: '',
organizationName: ''
},
health: {
coverageArea: '',
networkTier: '',
deductible: '',
dateOfBirth: '',
age: '',
preexistingConditions: false,
preexistingDetails: ''
},
forms: {
medicalQuestionnaire: false,
beneficiaryDesignation: false,
groupCensus: false
},
solicit: {
carrierIds: [],
planIds: []
}
}
}

View File

@@ -1,29 +0,0 @@
import type { LifeQuoteDraft } from '~/types/life-quote-intake'
export function emptyLifeQuoteDraft(): LifeQuoteDraft {
return {
quoteMode: null,
segment: null,
insured: null,
buyer: null,
life: {
coverage_type: 'banking',
coverage_amount: 0,
coverage_years: 10,
smoker: false,
medications: '',
surgeries: '',
weight: 0,
height: 0
},
forms: {
medicalQuestionnaire: false,
beneficiaryDesignation: false,
groupCensus: false
},
solicit: {
carrierIds: [],
planIds: []
}
}
}

View File

@@ -1,6 +1,7 @@
export function usePageTitle(title: string) {
export function usePageTitle(title: string | (() => string)) {
const computedTitle = typeof title === 'function' ? computed(title) : ref(title)
useHead({
title,
titleTemplate: (t) => (t ? `${t} · Policy UI` : 'Policy UI')
title: computed(() => `${computedTitle.value} · Segur-OS`)
})
}

View File

@@ -1,12 +0,0 @@
import type { PdfFieldMappingFile } from '~/types/pdf-field-mapping'
import mappingsJson from '~/data/pdf-field-mappings.json'
const file = mappingsJson as PdfFieldMappingFile
export function usePdfFieldMappings() {
function mappingForCatalogFormId(catalogFormId: number) {
return file.mappings.find((m) => m.catalogFormId === catalogFormId) ?? null
}
return { version: file.version, mappingForCatalogFormId, mappings: file.mappings }
}

View File

@@ -1,67 +0,0 @@
/**
* Composable for policy API operations
* Handles quote submission and acceptance
*/
export function usePolicyApi() {
const { $policy } = useNuxtApp()
const toast = useToast()
const router = useRouter()
/**
* Submit a policy quote request
*/
async function submitPolicyQuote(payload: {
policy_type: 'car' | 'life' | 'fire_structure' | 'fire_contents'
insured: any
buyer: any
policy_details: any
selected_providers: Array<{ provider_id: string; email: string }>
}) {
try {
const data = await $policy('/policies', {
method: 'POST',
body: payload
}) as any
toast.add({ title: 'Quote submitted successfully', color: 'green' })
return data
} catch (e: any) {
toast.add({
title: 'Failed to submit quote',
description: e?.data?.error ?? e.message,
color: 'red'
})
throw e
}
}
/**
* Accept a quote plan and trigger solicitation
*/
async function acceptQuote(applicationId: string, acceptedPlanId: string, acceptedBy: string) {
try {
const data = await $policy(`/policies/${applicationId}/accept`, {
method: 'POST',
body: {
accepted_plan_id: acceptedPlanId,
accepted_by: acceptedBy
}
}) as any
toast.add({ title: 'Plan accepted successfully', color: 'green' })
return data
} catch (e: any) {
toast.add({
title: 'Failed to accept plan',
description: e?.data?.error ?? e.message,
color: 'red'
})
throw e
}
}
return {
submitPolicyQuote,
acceptQuote
}
}

View File

@@ -1,88 +0,0 @@
import type { Ref } from 'vue'
import type { PolicyInstallmentRow, PolicyRegistration } from '~/types/brokerage-registration'
import { POLICY_DRAFT_STORAGE_KEY } from '~/types/brokerage-registration'
export function createEmptyPolicyRegistration(): PolicyRegistration {
return {
mintPolicyNumber: '',
contratanteId: '',
ramo: '',
subRamo: '',
aseguradora: '',
producto: '',
agencia: '',
numeroPolizaProveedor: '',
acreedor: '',
fechaEmision: '',
inicioVigencia: '',
finVigencia: '',
comisiones: [
{ idx: 1, agenteId: '', porcentaje: '' },
{ idx: 2, agenteId: '', porcentaje: '' },
{ idx: 3, agenteId: '', porcentaje: '' }
],
formaPago: '',
valorAsegurado: '',
primaBruta: '',
impuestoPct: '6',
primaNeta: '',
numCuotas: 10,
cuotas: [],
cotizacionMintId: '',
pdfCotizacionNombre: '',
pdfPolizaNombre: '',
notas: ''
}
}
export function rebuildInstallmentSchedule(p: PolicyRegistration): PolicyInstallmentRow[] {
const n = Math.max(1, Math.min(60, Math.floor(p.numCuotas) || 1))
const start = p.inicioVigencia ? new Date(p.inicioVigencia) : new Date()
const base = Number.isNaN(start.getTime()) ? new Date() : start
const per = p.primaBruta
? (Number.parseFloat(String(p.primaBruta).replace(/[^0-9.-]/g, '')) || 0) / n
: 0
const rows: PolicyInstallmentRow[] = []
for (let i = 0; i < n; i++) {
const d = new Date(base)
d.setMonth(d.getMonth() + i)
rows.push({
n: i + 1,
fechaVencimiento: d.toISOString().slice(0, 16),
prima: per > 0 ? per.toFixed(2) : ''
})
}
return rows
}
export function setFinOneYearAfterInicio(p: PolicyRegistration) {
if (!p.inicioVigencia) return
const d = new Date(p.inicioVigencia)
if (Number.isNaN(d.getTime())) return
d.setFullYear(d.getFullYear() + 1)
p.finVigencia = d.toISOString().slice(0, 16)
}
export function usePolicyDraftPersistence(form: Ref<PolicyRegistration>) {
if (import.meta.server) return
try {
const raw = localStorage.getItem(POLICY_DRAFT_STORAGE_KEY)
if (raw) {
const parsed = JSON.parse(raw) as PolicyRegistration
form.value = { ...createEmptyPolicyRegistration(), ...parsed, cuotas: parsed.cuotas ?? [] }
}
} catch {
/* ignore */
}
watch(
form,
(v) => {
try {
localStorage.setItem(POLICY_DRAFT_STORAGE_KEY, JSON.stringify(v))
} catch {
/* ignore */
}
},
{ deep: true }
)
}

View File

@@ -1,205 +0,0 @@
import { computed } from 'vue'
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
/* ── Types ── */
export type ProfileRole = 'sales' | 'claims' | 'renewals' | 'general_service' | 'management' | 'superadmin'
export interface ProfileSection {
id: string
label: string
visible: boolean
order: number
}
export interface ProfileLayout {
id: string
role: ProfileRole | string
name: string
description: string
icon: string
sections: ProfileSection[]
defaultTab: 'policies' | 'claims' | 'payments' | 'activity' | 'history' | 'relationships' | 'notes'
isCustom: boolean
}
/* ── Section catalog ── */
const ALL_SECTION_IDS = [
'orientation',
'kpi_strip',
'quick_policies',
'service_actions',
'personal_details',
'tabbed_content',
'documents',
] as const
const SECTION_LABELS: Record<string, string> = {
orientation: 'Account Orientation',
kpi_strip: 'KPI Strip',
quick_policies: 'Quick Policies',
service_actions: 'Service Actions',
personal_details: 'Personal Details',
tabbed_content: 'Tabbed Content',
documents: 'Documents',
}
function makeSections(order: string[], hidden: string[] = []): ProfileSection[] {
return order.map((id, i) => ({
id,
label: SECTION_LABELS[id] ?? id,
visible: !hidden.includes(id),
order: i,
}))
}
/* ── Built-in layouts ── */
function defaultLayouts(): ProfileLayout[] {
return [
{
id: 'sales',
role: 'sales',
name: 'Sales',
description: 'Focus on policies, quotes, and pipeline.',
icon: 'i-heroicons-currency-dollar',
sections: makeSections([
'orientation', 'quick_policies', 'kpi_strip', 'tabbed_content',
'service_actions', 'personal_details', 'documents',
]),
defaultTab: 'policies',
isCustom: false,
},
{
id: 'claims',
role: 'claims',
name: 'Claims',
description: 'Focus on claims and service actions.',
icon: 'i-heroicons-shield-exclamation',
sections: makeSections([
'service_actions', 'orientation', 'kpi_strip', 'tabbed_content',
'quick_policies', 'personal_details', 'documents',
]),
defaultTab: 'claims',
isCustom: false,
},
{
id: 'renewals',
role: 'renewals',
name: 'Renewals',
description: 'Focus on upcoming events and policies.',
icon: 'i-heroicons-arrow-path',
sections: makeSections([
'orientation', 'quick_policies', 'kpi_strip', 'tabbed_content',
'service_actions', 'personal_details', 'documents',
]),
defaultTab: 'policies',
isCustom: false,
},
{
id: 'general_service',
role: 'general_service',
name: 'General Service',
description: 'Balanced default for service representatives.',
icon: 'i-heroicons-lifebuoy',
sections: makeSections([
'orientation', 'kpi_strip', 'quick_policies', 'service_actions',
'personal_details', 'tabbed_content', 'documents',
]),
defaultTab: 'policies',
isCustom: false,
},
{
id: 'management',
role: 'management',
name: 'Management',
description: 'KPIs first, everything visible.',
icon: 'i-heroicons-chart-bar',
sections: makeSections([
'kpi_strip', 'orientation', 'service_actions', 'quick_policies',
'tabbed_content', 'personal_details', 'documents',
]),
defaultTab: 'history',
isCustom: false,
},
{
id: 'superadmin',
role: 'superadmin',
name: 'Superadmin',
description: 'Everything visible, history focus.',
icon: 'i-heroicons-cog-8-tooth',
sections: makeSections([
'kpi_strip', 'orientation', 'service_actions', 'quick_policies',
'tabbed_content', 'personal_details', 'documents',
]),
defaultTab: 'history',
isCustom: false,
},
]
}
const LAYOUTS_KEY = 'policy-ui-profile-layouts-v1'
const ACTIVE_KEY = 'policy-ui-active-profile-layout-v1'
/* ── Composable ── */
export function useProfileLayouts() {
const layouts = useLocalStorageRef<ProfileLayout[]>(LAYOUTS_KEY, defaultLayouts)
const activeLayoutId = useLocalStorageRef<{ id: string }>(ACTIVE_KEY, () => ({ id: 'general_service' }))
const activeLayout = computed<ProfileLayout>(() => {
const found = layouts.value.find(l => l.id === activeLayoutId.value.id)
return found ?? layouts.value[0] ?? defaultLayouts()[3] // fallback to general_service
})
const sortedSections = computed<ProfileSection[]>(() =>
[...activeLayout.value.sections]
.filter(s => s.visible)
.sort((a, b) => a.order - b.order)
)
function setActiveLayout(id: string) {
activeLayoutId.value = { id }
}
function addCustomLayout(layout: Omit<ProfileLayout, 'isCustom'>) {
layouts.value = [
...layouts.value,
{ ...layout, isCustom: true },
]
}
function updateLayout(id: string, partial: Partial<ProfileLayout>) {
layouts.value = layouts.value.map(l =>
l.id === id ? { ...l, ...partial } : l
)
}
function removeCustomLayout(id: string) {
const target = layouts.value.find(l => l.id === id)
if (!target || !target.isCustom) return
layouts.value = layouts.value.filter(l => l.id !== id)
if (activeLayoutId.value.id === id) {
activeLayoutId.value = { id: 'general_service' }
}
}
function resetToDefaults() {
layouts.value = defaultLayouts()
activeLayoutId.value = { id: 'general_service' }
}
return {
layouts,
activeLayoutId: computed(() => activeLayoutId.value.id),
activeLayout,
sortedSections,
setActiveLayout,
addCustomLayout,
updateLayout,
removeCustomLayout,
resetToDefaults,
}
}

View File

@@ -1,26 +0,0 @@
import {
emptyProviderContacts,
PROVIDER_EMAIL_ROLE_LABEL,
PROVIDER_EMAIL_ROLE_ORDER,
type ProviderContactEmails,
type ProviderEmailRole
} from '~/types/provider-contacts'
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
function storageKey(providerId: string) {
return `policy-ui-provider-contacts-v1-${providerId}`
}
export function useProviderContactEmails(providerId: string) {
const emails = useLocalStorageRef(storageKey(providerId), emptyProviderContacts)
function label(r: ProviderEmailRole) {
return PROVIDER_EMAIL_ROLE_LABEL[r]
}
return {
emails,
roles: PROVIDER_EMAIL_ROLE_ORDER,
label
}
}

View File

@@ -1,46 +0,0 @@
/**
* Quick lead capture list — persisted in localStorage.
* Used by the Quick Lead form and dashboard widget.
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
export interface QuickLead {
id: string
name: string
phone: string
email: string
product: string
source: string
priority: 'normal' | 'high' | 'urgent'
note: string
agent: string
createdAt: string
}
const KEY = 'policy-ui-quick-leads-v1'
export function useQuickLeads() {
const leads = useLocalStorageRef<QuickLead[]>(KEY, () => [])
function addLead(entry: Omit<QuickLead, 'id' | 'createdAt'>) {
const lead: QuickLead = {
id: crypto.randomUUID?.() ?? String(Date.now()),
createdAt: new Date().toISOString(),
...entry,
}
leads.value = [lead, ...leads.value]
return lead
}
function removeLead(id: string) {
leads.value = leads.value.filter((l) => l.id !== id)
}
/** Leads from the last N days */
function recentLeads(days: number) {
const cutoff = Date.now() - days * 86_400_000
return leads.value.filter((l) => new Date(l.createdAt).getTime() >= cutoff)
}
return { leads, addLead, removeLead, recentLeads }
}

View File

@@ -1,37 +0,0 @@
const STORAGE_KEY = 'policy-ui.quote-request-email.v1'
/**
* When false, quote flows record the intent locally but do not describe outbound provider emails
* (use when rates come from in-app tables, AI, or carrier APIs instead).
*/
export function useQuoteRequestEmailEnabled() {
const quoteRequestEmailEnabled = ref(true)
function read() {
if (!import.meta.client) return
try {
const v = localStorage.getItem(STORAGE_KEY)
if (v === '0') quoteRequestEmailEnabled.value = false
else if (v === '1') quoteRequestEmailEnabled.value = true
} catch {
/* ignore */
}
}
function setQuoteRequestEmailEnabled(v: boolean) {
quoteRequestEmailEnabled.value = v
if (!import.meta.client) return
try {
localStorage.setItem(STORAGE_KEY, v ? '1' : '0')
} catch {
/* ignore */
}
}
onMounted(() => read())
return {
quoteRequestEmailEnabled,
setQuoteRequestEmailEnabled
}
}

View File

@@ -1,77 +0,0 @@
import type { QuoteComparativeView } from '~/types/quote-view-model'
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
const KEY = 'policy-ui-quote-session-v1'
function defaultView(): QuoteComparativeView {
return {
title: 'ANÁLISIS COMPARATIVO · VIDA UNIVERSAL',
subtitle: 'Protección & Ahorro',
tagline:
'Comparativa de aseguradoras — valores garantizados y proyectados. Prima mensual fija de referencia.',
quoteDateIso: new Date().toISOString().slice(0, 10),
validDays: 30,
client: {
name: 'María Claudia Piña Ríos',
ageYears: 30,
gender: 'Femenino',
smoker: false,
riskClass: 'Estándar',
occupation: 'Administrativo'
},
request: {
sumAssuredUsd: 100_000,
monthlyPremiumUsd: 75,
annualPremiumUsd: 900,
benefitTypeLabel: 'Opción B — Creciente',
additionalCoverageLabel: 'No contratadas',
initialDepositLabel: 'No aplica'
},
carriers: [
{
carrierName: 'ASSA COMPAÑÍA DE SEGUROS, S.A.',
productName: 'ASSA Universal II',
ratesLine: 'T. garantizada 3.5% · T. corriente 4.0%',
sumAssuredUsd: 100_000,
footnote: 'Vigente hasta edad 91',
cells: [
{ yearLabel: 'Año 10', ageLabel: '40', guaranteed: 8200, projected: 12400 },
{ yearLabel: 'Año 20', ageLabel: '50', guaranteed: 15200, projected: 24100 },
{ yearLabel: 'Año 30', ageLabel: '60', guaranteed: 21000, projected: 38900 },
{ yearLabel: 'Edad 65', ageLabel: '65', guaranteed: 24500, projected: 36859 }
],
highlightProjectedUsd: 36859,
highlightNote: 'Valor proyectado a la edad de referencia'
},
{
carrierName: 'ASSA COMPAÑÍA DE SEGUROS, S.A.',
productName: 'ASSA Vida Segura',
ratesLine: 'T. garantizada 4.0% · T. corriente 4.0%',
sumAssuredUsd: 100_000,
footnote: 'Vence a los 70 años',
cells: [
{ yearLabel: 'Año 10', ageLabel: '40', guaranteed: 7800, projected: 11800 },
{ yearLabel: 'Año 20', ageLabel: '50', guaranteed: 14100, projected: 22800 },
{ yearLabel: 'Año 30', ageLabel: '60', guaranteed: 19800, projected: 35200 },
{ yearLabel: 'Edad 65', ageLabel: '65', guaranteed: 22100, projected: 33100 }
],
highlightProjectedUsd: 33100,
highlightNote: 'Revisar vigencia del producto'
}
],
accumulatedPremiumsUsd: [9000, 18_000, 27_000, 31_500],
advisorColumns: [
'Retorno garantizado a los 65: comparar valores acumulados vs primas pagadas.',
'Protección de largo plazo: priorizar vigencia del seguro hasta edad avanzada.',
'Coberturas opcionales (cáncer, cardiovascular, etc.) no incluidas en esta cotización.'
]
}
}
export function useQuoteSession() {
const view = useLocalStorageRef(KEY, defaultView)
function reset() {
view.value = defaultView()
}
return { view, reset, defaultView }
}

View File

@@ -1,67 +0,0 @@
/**
* Referral channel registry — persisted in localStorage.
* Used across the app: quick leads, customer registration, reporting.
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
export interface ReferralChannel {
id: string
name: string
type: 'person' | 'company' | 'digital' | 'event' | 'other'
contactName: string
contactPhone: string
contactEmail: string
note: string
active: boolean
createdAt: string
}
const KEY = 'policy-ui-referral-channels-v1'
const SEED_CHANNELS: ReferralChannel[] = [
{ id: 'ref-001', name: 'Roberto Jiménez', type: 'person', contactName: 'Roberto Jiménez', contactPhone: '+506 8834-2291', contactEmail: 'rjimenez@email.com', note: 'Long-time VIP client — strong auto & life referrals', active: true, createdAt: '2024-06-10T10:00:00Z' },
{ id: 'ref-002', name: 'Constructora Delta', type: 'company', contactName: 'Ing. Carlos Mora', contactPhone: '+506 2245-8800', contactEmail: 'cmora@delta.cr', note: 'Construction company — fleet and liability leads', active: true, createdAt: '2024-08-15T10:00:00Z' },
{ id: 'ref-003', name: 'Instagram Ads', type: 'digital', contactName: '', contactPhone: '', contactEmail: '', note: 'Paid social campaigns — auto & health focus', active: true, createdAt: '2025-01-10T10:00:00Z' },
{ id: 'ref-004', name: 'Google Ads', type: 'digital', contactName: '', contactPhone: '', contactEmail: '', note: 'Search campaigns — high intent leads', active: true, createdAt: '2025-01-10T10:00:00Z' },
{ id: 'ref-005', name: 'Expo Comercio 2025', type: 'event', contactName: 'Comité Organizador', contactPhone: '+506 2222-0000', contactEmail: 'info@expocomercio.cr', note: 'Annual trade expo — collected 40+ contacts', active: false, createdAt: '2025-03-20T10:00:00Z' },
{ id: 'ref-006', name: 'Cámara de Comercio', type: 'company', contactName: 'Patricia Arias', contactPhone: '+506 2233-5500', contactEmail: 'parias@camara.cr', note: 'Chamber of commerce partnership — corporate referrals', active: true, createdAt: '2024-11-05T10:00:00Z' },
{ id: 'ref-007', name: 'Walk-in / Oficina', type: 'other', contactName: '', contactPhone: '', contactEmail: '', note: 'Foot traffic to main office', active: true, createdAt: '2024-01-01T10:00:00Z' },
]
export function useReferralChannels() {
const channels = useLocalStorageRef<ReferralChannel[]>(KEY, () => [])
// Seed on first use
if (import.meta.client && channels.value.length === 0) {
channels.value = [...SEED_CHANNELS]
}
function addChannel(entry: Omit<ReferralChannel, 'id' | 'createdAt'>) {
const channel: ReferralChannel = {
id: 'ref-' + (crypto.randomUUID?.() ?? String(Date.now())).slice(0, 8),
createdAt: new Date().toISOString(),
...entry,
}
channels.value = [channel, ...channels.value]
return channel
}
function updateChannel(id: string, updates: Partial<Omit<ReferralChannel, 'id' | 'createdAt'>>) {
channels.value = channels.value.map(c =>
c.id === id ? { ...c, ...updates } : c
)
}
function removeChannel(id: string) {
channels.value = channels.value.filter(c => c.id !== id)
}
const activeChannels = computed(() => channels.value.filter(c => c.active))
/** Flat list for use in dropdowns */
const channelOptions = computed(() =>
activeChannels.value.map(c => ({ label: c.name, value: c.id }))
)
return { channels, activeChannels, channelOptions, addChannel, updateChannel, removeChannel }
}

View File

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

View File

@@ -1,23 +0,0 @@
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
import type { Ref } from 'vue'
interface SidebarFeatures {
showWorkstations: boolean
showAiTools: boolean
showLeadsHub: boolean
}
const KEY = 'policy-ui-sidebar-features-v1'
let _shared: Ref<SidebarFeatures> | null = null
export function useSidebarFeatures() {
if (!_shared) {
_shared = useLocalStorageRef<SidebarFeatures>(KEY, () => ({
showWorkstations: false,
showAiTools: false,
showLeadsHub: true,
}))
}
return _shared
}

View File

@@ -1,21 +0,0 @@
/**
* Tenant-level admin (company logo, legal name, etc.).
* Wire to real roles/claims when auth exists. Until then:
* - localStorage `policy-ui.superadmin` = `1` | `0`
* - in dev, defaults to true when the key is unset so the org screen is reachable
*/
export function useSuperAdmin() {
const isSuperAdmin = computed(() => {
if (!import.meta.client) return false
try {
const v = localStorage.getItem('policy-ui.superadmin')
if (v === '1') return true
if (v === '0') return false
return import.meta.dev
} catch {
return false
}
})
return { isSuperAdmin }
}

View File

@@ -1,184 +0,0 @@
/**
* Support Tickets — composable for queue state, filtering, CRUD, and mock routing.
* Persisted in localStorage via useLocalStorageRef.
*/
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
import {
type SupportTicket,
type SupportTicketDetail,
type TicketMessage,
type TicketStatus,
type SupportChannel,
type RoutingTier,
type RoutedQueue,
type RoutingRule,
MOCK_SUPPORT_TICKETS,
MOCK_TICKET_DETAILS,
MOCK_ROUTING_RULES,
} from '~/data/mock-support'
interface SupportState {
tickets: SupportTicket[]
details: Record<string, SupportTicketDetail>
routingRules: RoutingRule[]
}
function buildDefaults(): SupportState {
return {
tickets: [...MOCK_SUPPORT_TICKETS],
details: { ...MOCK_TICKET_DETAILS },
routingRules: [...MOCK_ROUTING_RULES],
}
}
export function useSupportTickets() {
const state = useLocalStorageRef<SupportState>('policy-ui-support-tickets-v1', buildDefaults)
// ── Computed: filtered lists ──
const openTickets = computed(() => state.value.tickets.filter(t => t.status === 'open'))
const unresolvedCount = computed(() => state.value.tickets.filter(t => t.status !== 'resolved').length)
const breachedCount = computed(() => state.value.tickets.filter(t => t.slaPercent >= 100).length)
const unassignedCount = computed(() => state.value.tickets.filter(t => !t.assignedTo).length)
const openPoolCount = computed(() => state.value.tickets.filter(t => t.routedQueue === 'open_pool').length)
const inProgressCount = computed(() => state.value.tickets.filter(t => t.status === 'in_progress').length)
const kpis = computed(() => {
const all = state.value.tickets
const unresolved = all.filter(t => t.status !== 'resolved')
const avgDaysOpen = unresolved.length
? Math.round(unresolved.reduce((sum, t) => sum + t.daysOpen, 0) / unresolved.length)
: 0
return {
total: all.length,
open: openTickets.value.length,
inProgress: inProgressCount.value,
breached: breachedCount.value,
avgDaysOpen,
unassigned: unassignedCount.value,
openPool: openPoolCount.value,
}
})
// ── CRUD ──
function updateStatus(ticketId: string, status: TicketStatus) {
const ticket = state.value.tickets.find(t => t.id === ticketId)
if (ticket) {
ticket.status = status
ticket.updatedAt = new Date().toISOString().slice(0, 10)
}
const detail = state.value.details[ticketId]
if (detail) {
detail.status = status
detail.updatedAt = new Date().toISOString().slice(0, 10)
}
}
function assignTicket(ticketId: string, agent: string) {
const ticket = state.value.tickets.find(t => t.id === ticketId)
if (ticket) {
ticket.assignedTo = agent
ticket.updatedAt = new Date().toISOString().slice(0, 10)
}
const detail = state.value.details[ticketId]
if (detail) {
detail.assignedTo = agent
detail.updatedAt = new Date().toISOString().slice(0, 10)
detail.messages.push({
id: `msg-${Date.now()}`,
type: 'system',
direction: 'internal',
from: 'Sistema',
to: null,
subject: null,
body: `Ticket asignado a ${agent}`,
timestamp: new Date().toISOString(),
aiDigest: null,
})
}
}
function addMessage(ticketId: string, message: Omit<TicketMessage, 'id' | 'timestamp'>) {
const detail = state.value.details[ticketId]
if (!detail) return
const msg: TicketMessage = {
...message,
id: `msg-${Date.now()}`,
timestamp: new Date().toISOString(),
}
detail.messages.push(msg)
detail.messageCount = detail.messages.length
detail.updatedAt = new Date().toISOString().slice(0, 10)
detail.lastMessagePreview = msg.body.slice(0, 80)
// sync summary ticket
const ticket = state.value.tickets.find(t => t.id === ticketId)
if (ticket) {
ticket.messageCount = detail.messageCount
ticket.updatedAt = detail.updatedAt
ticket.lastMessagePreview = detail.lastMessagePreview
}
}
// ── Mock Routing ──
const routingKeywords: Record<RoutedQueue, string[]> = {
collections: ['pago', 'factura', 'cobro', 'recibo', 'transferencia', 'mora'],
claims: ['siniestro', 'accidente', 'robo', 'daño', 'choque', 'grúa', 'colisión'],
sales: ['cotización', 'seguro nuevo', 'precio', 'cuánto sale', 'cobertura'],
renewals: ['renovación', 'vencimiento', 'prórroga', 'vigencia'],
operations: ['endoso', 'certificado', 'modificar', 'cambio de beneficiario'],
open_pool: [],
}
function simulateRouting(channel: SupportChannel, body: string): { tier: RoutingTier; queue: RoutedQueue; confidence: number } {
const lower = body.toLowerCase()
// Tier 2: keyword matching
for (const [queue, keywords] of Object.entries(routingKeywords) as [RoutedQueue, string[]][]) {
if (!keywords.length) continue
const matched = keywords.filter(kw => lower.includes(kw))
if (matched.length > 0) {
const confidence = Math.min(0.95, 0.6 + matched.length * 0.1)
return { tier: 'tier2_rule', queue, confidence }
}
}
// Tier 3: no match → open pool
return { tier: 'tier3_open', queue: 'open_pool', confidence: 0.3 }
}
// ── Routing Rules CRUD ──
function toggleRule(ruleId: string) {
const rule = state.value.routingRules.find(r => r.id === ruleId)
if (rule) rule.enabled = !rule.enabled
}
function updateRule(ruleId: string, updates: Partial<RoutingRule>) {
const rule = state.value.routingRules.find(r => r.id === ruleId)
if (rule) Object.assign(rule, updates)
}
function getDetail(ticketId: string): SupportTicketDetail | undefined {
return state.value.details[ticketId]
}
return {
state,
// computed
openTickets,
unresolvedCount,
breachedCount,
unassignedCount,
openPoolCount,
inProgressCount,
kpis,
// CRUD
updateStatus,
assignTicket,
addMessage,
getDetail,
// routing
simulateRouting,
toggleRule,
updateRule,
}
}

View File

@@ -1,25 +0,0 @@
import type { WelcomeDashboardConfig } from '~/types/welcome-dashboard'
/**
* Home / welcome dashboard content. Reads `app.config` first; later merge runtime config or APIs.
* Brokerage company name from Settings → Personalization overrides `productName` when set.
*/
export function useWelcomeDashboard(): ComputedRef<WelcomeDashboardConfig> {
const app = useAppConfig()
const { saved: branding } = useBrokerageBranding()
return computed((): WelcomeDashboardConfig => {
const base = (app.welcomeDashboard ?? {}) as Partial<WelcomeDashboardConfig>
const fromBranding = branding.value.companyName?.trim()
return {
greetingName: base.greetingName ?? 'User',
productName: fromBranding || base.productName || 'Segur-OS Beta',
subtitle: base.subtitle ?? '',
dailyTasks: base.dailyTasks ?? [],
alerts: base.alerts ?? [],
performanceKpis: base.performanceKpis ?? [],
ceoKpis: base.ceoKpis ?? [],
quickLinks: base.quickLinks ?? []
}
})
}