From 67482f6629c7e7b73be1eff55bdac1a4ded1e131 Mon Sep 17 00:00:00 2001 From: Jordan Weingarten Date: Thu, 16 Apr 2026 11:11:44 -0500 Subject: [PATCH] WIP jordan --- .claude/launch.json | 11 + .claude/start-dev.sh | 4 + app.config.ts | 167 + app/assets/css/main.css | 554 +++ app/components/AppBackToHome.vue | 7 + .../account/AccountThemeSection.vue | 129 + app/components/home/DashboardWidgetBlocks.vue | 55 + app/components/layout/AppCommandSearch.vue | 439 +++ app/components/layout/AppTopBar.vue | 339 ++ .../quotes/QuoteComparativeLayout.vue | 168 + app/components/quotes/auto/AcceptanceStep.vue | 121 + .../quotes/auto/CustomerVehicleStep.vue | 154 + app/components/quotes/auto/SetupStep.vue | 97 + .../quotes/auto/SolicitQuotesStep.vue | 97 + .../quotes/health/AcceptanceStep.vue | 127 + app/components/quotes/health/SetupStep.vue | 250 ++ .../quotes/health/SolicitQuotesStep.vue | 90 + app/components/quotes/life/AcceptanceStep.vue | 149 + app/components/quotes/life/SetupStep.vue | 233 ++ .../quotes/life/SolicitQuotesStep.vue | 90 + app/components/sales/SalesFlowIndicator.vue | 171 + app/components/sales/SalesPipelineBar.vue | 447 +++ app/composables/useAlertConfig.ts | 176 + app/composables/useAnalytics.ts | 147 + app/composables/useAppShellLayout.ts | 32 + app/composables/useAppTheme.ts | 46 + app/composables/useAutoQuoteDraft.ts | 30 + app/composables/useBrokerageBranding.ts | 31 + app/composables/useClientFavorites.ts | 35 + app/composables/useClientRegistrationModel.ts | 47 + app/composables/useColectivos.ts | 641 ++++ app/composables/useCustomerAttention.ts | 228 ++ app/composables/useCustomerProfileVault.ts | 18 + app/composables/useDashboardHomeWidgets.ts | 273 ++ app/composables/useEmissionsQueue.ts | 52 + app/composables/useFormsCatalog.ts | 174 + app/composables/useHealthQuoteDraft.ts | 33 + app/composables/useLifeQuoteDraft.ts | 36 + app/composables/usePageTitle.ts | 6 + app/composables/usePdfFieldMappings.ts | 12 + app/composables/usePolicyRegistrationModel.ts | 88 + app/composables/useProfileLayouts.ts | 205 + app/composables/useProviderContactEmails.ts | 26 + app/composables/useQuickLeads.ts | 46 + .../useQuoteRequestEmailEnabled.ts | 37 + app/composables/useQuoteSession.ts | 77 + app/composables/useReferralChannels.ts | 67 + app/composables/useSalesPipeline.ts | 316 ++ app/composables/useSidebarFeatures.ts | 23 + app/composables/useSuperAdmin.ts | 21 + app/composables/useSupportTickets.ts | 184 + app/composables/useWelcomeDashboard.ts | 25 + app/data/auto-quote-intake.ts | 70 + app/data/form-field-groups.json | 41 + app/data/forms-catalog.json | 116 + app/data/health-quote-intake.ts | 53 + app/data/life-quote-intake.ts | 45 + app/data/mock-analytics.ts | 363 ++ app/data/mock-claims.ts | 550 +++ app/data/mock-customers.ts | 461 +++ app/data/mock-renewals.ts | 660 ++++ app/data/mock-support.ts | 699 ++++ app/data/pdf-field-mappings.json | 20 + app/data/quotes-overview.mock.ts | 214 ++ app/data/roles-seguros.ts | 35 + app/data/taxonomy.ts | 34 + app/layouts/default.vue | 435 ++- app/pages/account/index.vue | 88 + app/pages/ai-tools/case-assistant.vue | 18 + app/pages/ai-tools/email-writer.vue | 18 + app/pages/ai-tools/policy-comparator.vue | 18 + app/pages/ai-tools/sales-factory.vue | 158 + app/pages/analysis/index.vue | 459 +++ app/pages/calendar.vue | 1514 ++++++++ app/pages/claims/[id].vue | 1198 ++++++ app/pages/claims/index.vue | 506 +++ app/pages/claims/intake/[token].vue | 672 ++++ app/pages/claims/settings.vue | 416 ++ app/pages/collections/index.vue | 2724 +++++++++++++ app/pages/customers/[id].vue | 2406 +++++++++++- app/pages/customers/index.vue | 400 +- app/pages/customers/new.vue | 283 +- app/pages/home2.vue | 777 ++++ app/pages/index.vue | 3364 ++++++++++++++++- app/pages/onboarding/active-leads/new.vue | 14 + app/pages/onboarding/emissions/index.vue | 364 ++ app/pages/onboarding/index.vue | 251 ++ app/pages/onboarding/policy-upload/new.vue | 727 ++++ app/pages/onboarding/potential-leads/new.vue | 14 + app/pages/onboarding/solicitud.vue | 311 ++ app/pages/policies/[id].vue | 1769 +++++++++ .../policies/{ => app}/[application_id].vue | 22 +- app/pages/policies/book.vue | 836 ++++ app/pages/policies/groups/index.vue | 965 +++++ app/pages/policies/index.vue | 1370 ++++++- app/pages/policies/new.vue | 28 +- app/pages/providers/[provider_id].vue | 30 +- app/pages/providers/index.vue | 6 +- app/pages/providers/new.vue | 6 +- app/pages/quotes/auto/index.vue | 291 ++ app/pages/quotes/compare.vue | 1001 +++++ app/pages/quotes/custom/index.vue | 169 + app/pages/quotes/general-risk/index.vue | 321 ++ app/pages/quotes/health/index.vue | 311 ++ app/pages/quotes/index.vue | 1267 +++++++ app/pages/quotes/life/index.vue | 311 ++ app/pages/quotes/mission-control.vue | 662 ++++ app/pages/quotes/new.vue | 994 +++++ app/pages/registration/client.vue | 417 ++ app/pages/registration/policy.vue | 234 ++ app/pages/renewals/[id].vue | 837 ++++ app/pages/renewals/index.vue | 499 +++ app/pages/sales/leads/index.vue | 770 ++++ app/pages/sales/quick-lead.vue | 519 +++ app/pages/settings/agents.vue | 1036 +++++ app/pages/settings/alerts.vue | 616 +++ app/pages/settings/appearance.vue | 7 + app/pages/settings/customer-attention.vue | 369 ++ app/pages/settings/forms/index.vue | 255 ++ app/pages/settings/index.vue | 179 + app/pages/settings/organization.vue | 178 + app/pages/settings/permissions.vue | 233 ++ app/pages/settings/personalization.vue | 7 + app/pages/settings/profile-layouts.vue | 391 ++ app/pages/settings/providers.vue | 73 + app/pages/settings/quote-requests.vue | 42 + app/pages/settings/referral-channels.vue | 431 +++ app/pages/settings/support-routing.vue | 231 ++ app/pages/support/[id].vue | 752 ++++ app/pages/support/collectivos/[id].vue | 2321 ++++++++++++ app/pages/support/collectivos/index.vue | 1211 ++++++ app/pages/support/index.vue | 436 +++ app/pages/tasks/[id].vue | 20 +- app/pages/tasks/index.vue | 6 +- app/pages/workstation/claims.vue | 18 + app/pages/workstation/collections.vue | 18 + app/pages/workstation/collectivos.vue | 329 ++ app/pages/workstation/customer-service.vue | 18 + app/pages/workstation/facturacion.vue | 114 + app/pages/workstation/renewals.vue | 18 + app/plugins/open-fetch-policy-auth.ts | 20 + app/plugins/theme-hydrate.client.ts | 11 + app/types/app-theme.ts | 14 + app/types/auto-quote-intake.ts | 46 + app/types/branding.ts | 10 + app/types/brokerage-registration.ts | 69 + app/types/customer-profile.ts | 55 + app/types/form-catalog.ts | 40 + app/types/form-field-groups.ts | 11 + app/types/health-quote-intake.ts | 40 + app/types/life-quote-intake.ts | 42 + app/types/pdf-field-mapping.ts | 9 + app/types/provider-contacts.ts | 58 + app/types/quote-view-model.ts | 48 + app/types/roles.ts | 15 + app/types/welcome-dashboard.ts | 38 + app/utils/refDebounced.ts | 18 + app/utils/useLocalStorageRef.ts | 40 + docs/CODEBASE_BUNDLE.md | 155 + nuxt.config.ts | 36 +- openapi/stub.json | 47 + public/forms/README.txt | 1 + scripts/import-forms.mjs | 61 + 163 files changed, 50627 insertions(+), 728 deletions(-) create mode 100644 .claude/launch.json create mode 100755 .claude/start-dev.sh create mode 100644 app.config.ts create mode 100644 app/components/AppBackToHome.vue create mode 100644 app/components/account/AccountThemeSection.vue create mode 100644 app/components/home/DashboardWidgetBlocks.vue create mode 100644 app/components/layout/AppCommandSearch.vue create mode 100644 app/components/layout/AppTopBar.vue create mode 100644 app/components/quotes/QuoteComparativeLayout.vue create mode 100644 app/components/quotes/auto/AcceptanceStep.vue create mode 100644 app/components/quotes/auto/CustomerVehicleStep.vue create mode 100644 app/components/quotes/auto/SetupStep.vue create mode 100644 app/components/quotes/auto/SolicitQuotesStep.vue create mode 100644 app/components/quotes/health/AcceptanceStep.vue create mode 100644 app/components/quotes/health/SetupStep.vue create mode 100644 app/components/quotes/health/SolicitQuotesStep.vue create mode 100644 app/components/quotes/life/AcceptanceStep.vue create mode 100644 app/components/quotes/life/SetupStep.vue create mode 100644 app/components/quotes/life/SolicitQuotesStep.vue create mode 100644 app/components/sales/SalesFlowIndicator.vue create mode 100644 app/components/sales/SalesPipelineBar.vue create mode 100644 app/composables/useAlertConfig.ts create mode 100644 app/composables/useAnalytics.ts create mode 100644 app/composables/useAppShellLayout.ts create mode 100644 app/composables/useAppTheme.ts create mode 100644 app/composables/useAutoQuoteDraft.ts create mode 100644 app/composables/useBrokerageBranding.ts create mode 100644 app/composables/useClientFavorites.ts create mode 100644 app/composables/useClientRegistrationModel.ts create mode 100644 app/composables/useColectivos.ts create mode 100644 app/composables/useCustomerAttention.ts create mode 100644 app/composables/useCustomerProfileVault.ts create mode 100644 app/composables/useDashboardHomeWidgets.ts create mode 100644 app/composables/useEmissionsQueue.ts create mode 100644 app/composables/useFormsCatalog.ts create mode 100644 app/composables/useHealthQuoteDraft.ts create mode 100644 app/composables/useLifeQuoteDraft.ts create mode 100644 app/composables/usePageTitle.ts create mode 100644 app/composables/usePdfFieldMappings.ts create mode 100644 app/composables/usePolicyRegistrationModel.ts create mode 100644 app/composables/useProfileLayouts.ts create mode 100644 app/composables/useProviderContactEmails.ts create mode 100644 app/composables/useQuickLeads.ts create mode 100644 app/composables/useQuoteRequestEmailEnabled.ts create mode 100644 app/composables/useQuoteSession.ts create mode 100644 app/composables/useReferralChannels.ts create mode 100644 app/composables/useSalesPipeline.ts create mode 100644 app/composables/useSidebarFeatures.ts create mode 100644 app/composables/useSuperAdmin.ts create mode 100644 app/composables/useSupportTickets.ts create mode 100644 app/composables/useWelcomeDashboard.ts create mode 100644 app/data/auto-quote-intake.ts create mode 100644 app/data/form-field-groups.json create mode 100644 app/data/forms-catalog.json create mode 100644 app/data/health-quote-intake.ts create mode 100644 app/data/life-quote-intake.ts create mode 100644 app/data/mock-analytics.ts create mode 100644 app/data/mock-claims.ts create mode 100644 app/data/mock-customers.ts create mode 100644 app/data/mock-renewals.ts create mode 100644 app/data/mock-support.ts create mode 100644 app/data/pdf-field-mappings.json create mode 100644 app/data/quotes-overview.mock.ts create mode 100644 app/data/roles-seguros.ts create mode 100644 app/data/taxonomy.ts create mode 100644 app/pages/account/index.vue create mode 100644 app/pages/ai-tools/case-assistant.vue create mode 100644 app/pages/ai-tools/email-writer.vue create mode 100644 app/pages/ai-tools/policy-comparator.vue create mode 100644 app/pages/ai-tools/sales-factory.vue create mode 100644 app/pages/analysis/index.vue create mode 100644 app/pages/calendar.vue create mode 100644 app/pages/claims/[id].vue create mode 100644 app/pages/claims/index.vue create mode 100644 app/pages/claims/intake/[token].vue create mode 100644 app/pages/claims/settings.vue create mode 100644 app/pages/collections/index.vue create mode 100644 app/pages/home2.vue create mode 100644 app/pages/onboarding/active-leads/new.vue create mode 100644 app/pages/onboarding/emissions/index.vue create mode 100644 app/pages/onboarding/index.vue create mode 100644 app/pages/onboarding/policy-upload/new.vue create mode 100644 app/pages/onboarding/potential-leads/new.vue create mode 100644 app/pages/onboarding/solicitud.vue create mode 100644 app/pages/policies/[id].vue rename app/pages/policies/{ => app}/[application_id].vue (95%) create mode 100644 app/pages/policies/book.vue create mode 100644 app/pages/policies/groups/index.vue create mode 100644 app/pages/quotes/auto/index.vue create mode 100644 app/pages/quotes/compare.vue create mode 100644 app/pages/quotes/custom/index.vue create mode 100644 app/pages/quotes/general-risk/index.vue create mode 100644 app/pages/quotes/health/index.vue create mode 100644 app/pages/quotes/index.vue create mode 100644 app/pages/quotes/life/index.vue create mode 100644 app/pages/quotes/mission-control.vue create mode 100644 app/pages/quotes/new.vue create mode 100644 app/pages/registration/client.vue create mode 100644 app/pages/registration/policy.vue create mode 100644 app/pages/renewals/[id].vue create mode 100644 app/pages/renewals/index.vue create mode 100644 app/pages/sales/leads/index.vue create mode 100644 app/pages/sales/quick-lead.vue create mode 100644 app/pages/settings/agents.vue create mode 100644 app/pages/settings/alerts.vue create mode 100644 app/pages/settings/appearance.vue create mode 100644 app/pages/settings/customer-attention.vue create mode 100644 app/pages/settings/forms/index.vue create mode 100644 app/pages/settings/index.vue create mode 100644 app/pages/settings/organization.vue create mode 100644 app/pages/settings/permissions.vue create mode 100644 app/pages/settings/personalization.vue create mode 100644 app/pages/settings/profile-layouts.vue create mode 100644 app/pages/settings/providers.vue create mode 100644 app/pages/settings/quote-requests.vue create mode 100644 app/pages/settings/referral-channels.vue create mode 100644 app/pages/settings/support-routing.vue create mode 100644 app/pages/support/[id].vue create mode 100644 app/pages/support/collectivos/[id].vue create mode 100644 app/pages/support/collectivos/index.vue create mode 100644 app/pages/support/index.vue create mode 100644 app/pages/workstation/claims.vue create mode 100644 app/pages/workstation/collections.vue create mode 100644 app/pages/workstation/collectivos.vue create mode 100644 app/pages/workstation/customer-service.vue create mode 100644 app/pages/workstation/facturacion.vue create mode 100644 app/pages/workstation/renewals.vue create mode 100644 app/plugins/open-fetch-policy-auth.ts create mode 100644 app/plugins/theme-hydrate.client.ts create mode 100644 app/types/app-theme.ts create mode 100644 app/types/auto-quote-intake.ts create mode 100644 app/types/branding.ts create mode 100644 app/types/brokerage-registration.ts create mode 100644 app/types/customer-profile.ts create mode 100644 app/types/form-catalog.ts create mode 100644 app/types/form-field-groups.ts create mode 100644 app/types/health-quote-intake.ts create mode 100644 app/types/life-quote-intake.ts create mode 100644 app/types/pdf-field-mapping.ts create mode 100644 app/types/provider-contacts.ts create mode 100644 app/types/quote-view-model.ts create mode 100644 app/types/roles.ts create mode 100644 app/types/welcome-dashboard.ts create mode 100644 app/utils/refDebounced.ts create mode 100644 app/utils/useLocalStorageRef.ts create mode 100644 docs/CODEBASE_BUNDLE.md create mode 100644 openapi/stub.json create mode 100644 public/forms/README.txt create mode 100644 scripts/import-forms.mjs diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..71e6239 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "policy-ui", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3737 + } + ] +} diff --git a/.claude/start-dev.sh b/.claude/start-dev.sh new file mode 100755 index 0000000..0bb65cd --- /dev/null +++ b/.claude/start-dev.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export PATH="/nix/store/cx0jd5mwhn7ihryyv0qp9d33f8s1iwb7-nodejs-24.13.0/bin:$PATH" +cd /Users/jordanweingarten/Dev/policy-ui +exec npm run dev diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 0000000..1dc4ae8 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,167 @@ +export default defineAppConfig({ + ui: { + colors: { + primary: 'primary', + neutral: 'stone' + } + }, + + welcomeDashboard: { + greetingName: 'User', + productName: 'Segur-OS Beta', + subtitle: + 'Orientation for today — pipeline health, receivables, and what needs attention before close of business.', + + dailyTasks: [ + { id: 't1', title: 'Review carrier responses in General Support queue', emphasis: true }, + { id: 't2', title: 'Follow up on 3 quotes pending bind' }, + { id: 't3', title: 'Approve 2 manual policy uploads from onboarding' }, + { id: 't4', title: 'Check renewal diary for policies expiring in 60 days' } + ], + + alerts: [ + { + id: 'a1', + message: '2 policies in renewal window need repricing approval.', + tone: 'warning' + }, + { + id: 'a2', + message: 'Carrier X API credential expires in 14 days — rotate in Settings.', + tone: 'info' + }, + { + id: 'a3', + message: 'Collections: 4 accounts over 60 days past due.', + tone: 'error' + } + ], + + performanceKpis: [ + { + id: 'ms', + label: 'New business (MTD)', + value: '42 policies', + hint: 'Bound this month', + change: '+6 vs prior month', + changeTone: 'positive' + }, + { + id: 'mr', + label: 'Premium written (MTD)', + value: '$1.24M', + hint: 'GWP recognized', + change: '+4.2% vs prior month', + changeTone: 'positive' + }, + { + id: 'ren', + label: 'Renewals in flight', + value: '128', + hint: 'Quoted, not yet bound', + change: '18 due ≤30d', + changeTone: 'neutral' + }, + { + id: 'late', + label: 'AR >30 days', + value: '$186K', + hint: 'Outstanding receivables', + change: '−$12K vs last week', + changeTone: 'positive' + } + ], + + ceoKpis: [ + { + id: 'gwp', + label: 'GWP (YTD)', + value: '$14.8M', + hint: 'Gross written premium', + change: 'On plan', + changeTone: 'neutral' + }, + { + id: 'if', + label: 'Policies in force', + value: '3,842', + hint: 'Active policies', + change: '+2.1% YoY', + changeTone: 'positive' + }, + { + id: 'lr', + label: 'Loss ratio', + value: '58.4%', + hint: 'Trailing 12 months', + change: 'Within appetite', + changeTone: 'neutral' + }, + { + id: 'pipe', + label: 'Quoted pipeline', + value: '$2.06M', + hint: 'Not yet bound', + change: 'Weighted ~$1.1M', + changeTone: 'neutral' + } + ], + + quickLinks: [ + { + label: 'Quotes', + to: '/quotes', + icon: 'i-heroicons-chart-bar-square', + description: 'Auto, health, life, general risk & custom — comparative and single-quote entry points.' + }, + { + label: 'Sales', + to: '/onboarding', + icon: 'i-heroicons-clipboard-document-list', + description: 'Pipeline — leads, quotes, solicitudes, emissions, uploads.' + }, + { + label: 'Customers', + to: '/customers', + icon: 'i-heroicons-users', + description: 'CRM — relationships and household / corporate profiles.' + }, + { + label: 'Policies', + to: '/policies', + icon: 'i-heroicons-rectangle-stack', + description: 'Cartera — applications, quotes received, and bound policies.' + }, + { + label: 'Settings', + to: '/settings', + icon: 'i-heroicons-cog-6-tooth', + description: 'Branding, providers & carriers, permissions, forms catalog, and workspace setup.' + }, + { + label: 'Support inbox', + to: '/support', + icon: 'i-heroicons-chat-bubble-left-right', + description: 'Carrier inbox — quotes, solicitations, and follow-ups.' + }, + { + label: 'Claims', + to: '/claims', + icon: 'i-heroicons-exclamation-triangle', + description: 'Losses and filings — tie to policies and carriers.' + }, + { + label: 'Collections', + to: '/collections', + icon: 'i-heroicons-banknotes', + description: 'Receivables and recovery workflows.' + }, + { + label: 'Renewals', + to: '/renewals', + icon: 'i-heroicons-arrow-path', + description: 'Expiry diary and retention campaigns.' + } + ] + } +}) diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 7c95c6f..b4691e8 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -1,2 +1,556 @@ @import "tailwindcss"; @import "@nuxt/ui"; + +/* ── Nuxt UI primary color — teal ───────────────────────────────────────── */ +@theme { + --color-primary-50: #eefbfb; + --color-primary-100: #d4f3f4; + --color-primary-200: #a8e7e9; + --color-primary-300: #6dd5d8; + --color-primary-400: #33bdc2; + --color-primary-500: #019fa6; + --color-primary-600: #018388; + --color-primary-700: #01696f; + --color-primary-800: #015358; + --color-primary-900: #014044; + --color-primary-950: #002a2d; + /* Hijack green so Nuxt UI var(--color-green-*, fallback) resolves here */ + --color-green-50: #eefbfb; + --color-green-100: #d4f3f4; + --color-green-200: #a8e7e9; + --color-green-300: #6dd5d8; + --color-green-400: #33bdc2; + --color-green-500: #019fa6; + --color-green-600: #018388; + --color-green-700: #01696f; + --color-green-800: #015358; + --color-green-900: #014044; + --color-green-950: #002a2d; +} +:root, :host { + --ui-color-primary-50: #eefbfb !important; + --ui-color-primary-100: #d4f3f4 !important; + --ui-color-primary-200: #a8e7e9 !important; + --ui-color-primary-300: #6dd5d8 !important; + --ui-color-primary-400: #33bdc2 !important; + --ui-color-primary-500: #019fa6 !important; + --ui-color-primary-600: #018388 !important; + --ui-color-primary-700: #01696f !important; + --ui-color-primary-800: #015358 !important; + --ui-color-primary-900: #014044 !important; + --ui-color-primary-950: #002a2d !important; + --ui-color-neutral-50: #f8f7f4 !important; + --ui-color-neutral-100: #f5f3ef !important; + --ui-color-neutral-200: #e8e6e2 !important; + --ui-color-neutral-300: #d0d0cc !important; + --ui-color-neutral-400: #a0a09c !important; + --ui-color-neutral-500: #8a8a86 !important; + --ui-color-neutral-600: #6b6b68 !important; + --ui-color-neutral-700: #4a4a48 !important; + --ui-color-neutral-800: #2e2e2c !important; + --ui-color-neutral-900: #1a1a18 !important; + --ui-color-neutral-950: #0f0f0e !important; +} + +/* ── Override Tailwind/Nuxt UI default ring color ────────────────────────── */ +:root { + --tw-ring-color: var(--brand, #0d5c63) !important; +} + +/* ── Design tokens (normalized app chrome) ───────────────────────────────── */ +:root, +[data-theme="light"] { + color-scheme: light; + --page-bg: #f8f7f4; + --surface: #ffffff; + --surface-elevated: #ffffff; + --sidebar-bg: #f5f3ef; + --sidebar-border: rgba(0, 0, 0, 0.04); + --topbar-bg: rgba(248, 247, 244, 0.94); + --text-primary: #1a1a18; + --text-secondary: #6b6b68; + --text-muted: #a0a09c; + --text-faint: #c0c0bc; + --brand: #01696f; + --brand-hover: #015358; + --brand-soft: rgba(1, 105, 111, 0.08); + --brand-faint: rgba(1, 105, 111, 0.04); + --nav-active-bg: rgba(1, 105, 111, 0.08); + --nav-active-fg: #01696f; + --nav-hover-bg: rgba(0, 0, 0, 0.05); + --logo-chrome-bg: linear-gradient(135deg, rgba(1, 105, 111, 0.05) 0%, rgba(255, 255, 255, 0) 55%); + --logo-blend: multiply; + --accent-ridge: rgba(1, 105, 111, 0.25); + + /* ── Extended tokens ── */ + --input-bg: #ffffff; + --input-border: rgba(0, 0, 0, 0.08); + --input-focus-ring: rgba(1, 105, 111, 0.20); + --input-placeholder: #a0a09c; + --card-border: rgba(0, 0, 0, 0.06); + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + --card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04); + --badge-muted-bg: rgba(0, 0, 0, 0.05); + --badge-muted-fg: #6b6b68; + --divider: rgba(0, 0, 0, 0.06); + --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand); + --btn-neutral-bg: #f5f3ef; + --btn-neutral-border: rgba(0, 0, 0, 0.12); + --btn-neutral-hover: #eeedea; + --success: #01696f; + --success-soft: rgba(1, 105, 111, 0.08); + --warning: #964219; + --warning-soft: rgba(150, 66, 25, 0.08); + --error: #c13838; + --error-soft: rgba(193, 56, 56, 0.08); + --info: #01696f; + --info-soft: rgba(1, 105, 111, 0.06); + --urgent: #dc2626; + --urgent-soft: rgba(220, 38, 38, 0.06); + --pending: #d97706; + --pending-soft: rgba(217, 119, 6, 0.06); + --skeleton: rgba(0, 0, 0, 0.04); + --scrollbar-thumb: rgba(0, 0, 0, 0.12); + --scrollbar-track: transparent; +} + +[data-theme="purple"] { + color-scheme: light; + --page-bg: #f3f1ee; + --surface: #f9f8f6; + --surface-elevated: #ffffff; + --sidebar-bg: #f5f3f0; + --sidebar-border: rgba(100, 90, 80, 0.10); + --topbar-bg: rgba(245, 243, 240, 0.92); + --text-primary: #1a1a1a; + --text-muted: #6b5f55; + --brand: #5b3a8c; + --brand-hover: #4a2d75; + --brand-soft: rgba(91, 58, 140, 0.10); + --brand-faint: rgba(91, 58, 140, 0.05); + --nav-active-bg: rgba(91, 58, 140, 0.07); + --nav-active-fg: #5b3a8c; + --nav-hover-bg: rgba(100, 90, 80, 0.04); + --logo-chrome-bg: linear-gradient(135deg, rgba(91, 58, 140, 0.06) 0%, rgba(255, 255, 255, 0) 55%); + --logo-blend: multiply; + --accent-ridge: rgba(91, 58, 140, 0.28); + + --input-bg: #ffffff; + --input-border: rgba(100, 90, 80, 0.16); + --input-focus-ring: rgba(91, 58, 140, 0.25); + --input-placeholder: #8c857d; + --card-border: rgba(100, 90, 80, 0.10); + --card-shadow: 0 1px 2px rgba(100, 90, 80, 0.06); + --card-shadow-hover: 0 4px 12px rgba(100, 90, 80, 0.10), 0 1px 3px rgba(100, 90, 80, 0.06); + --badge-muted-bg: rgba(100, 90, 80, 0.08); + --badge-muted-fg: #6b5f55; + --divider: rgba(100, 90, 80, 0.08); + --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand); + --btn-neutral-bg: #f5f3f0; + --btn-neutral-border: rgba(100, 90, 80, 0.14); + --btn-neutral-hover: #edeae6; + --success: #0f7b5f; + --success-soft: rgba(15, 123, 95, 0.10); + --warning: #c27b1a; + --warning-soft: rgba(194, 123, 26, 0.10); + --error: #c13838; + --error-soft: rgba(193, 56, 56, 0.10); + --info: #5b3a8c; + --info-soft: rgba(91, 58, 140, 0.08); + --urgent: #dc2626; + --urgent-soft: rgba(220, 38, 38, 0.06); + --pending: #c27b1a; + --pending-soft: rgba(194, 123, 26, 0.06); + --skeleton: rgba(100, 90, 80, 0.06); + --scrollbar-thumb: rgba(100, 90, 80, 0.18); + --scrollbar-track: transparent; +} + +[data-theme="dark"] { + color-scheme: dark; + --page-bg: #111110; + --surface: #1a1918; + --surface-elevated: #222120; + --sidebar-bg: #161514; + --sidebar-border: rgba(168, 162, 155, 0.10); + --topbar-bg: rgba(22, 21, 20, 0.94); + --text-primary: #e8e4df; + --text-muted: #8c857d; + --brand: #2dd4bf; + --brand-hover: #5eead4; + --brand-soft: rgba(45, 212, 191, 0.12); + --brand-faint: rgba(45, 212, 191, 0.06); + --nav-active-bg: rgba(45, 212, 191, 0.10); + --nav-active-fg: #5eead4; + --nav-hover-bg: rgba(168, 162, 155, 0.06); + --logo-chrome-bg: linear-gradient(135deg, rgba(45, 212, 191, 0.08) 0%, rgba(255, 255, 255, 0) 50%); + --logo-blend: screen; + --accent-ridge: rgba(45, 212, 191, 0.35); + + --input-bg: #222120; + --input-border: rgba(168, 162, 155, 0.14); + --input-focus-ring: rgba(45, 212, 191, 0.30); + --input-placeholder: #6b6560; + --card-border: rgba(168, 162, 155, 0.08); + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.30); + --card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.20); + --badge-muted-bg: rgba(168, 162, 155, 0.10); + --badge-muted-fg: #8c857d; + --divider: rgba(168, 162, 155, 0.06); + --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand); + --btn-neutral-bg: #222120; + --btn-neutral-border: rgba(168, 162, 155, 0.12); + --btn-neutral-hover: #2a2928; + --success: #2dd4a8; + --success-soft: rgba(45, 212, 168, 0.12); + --warning: #f0b429; + --warning-soft: rgba(240, 180, 41, 0.12); + --error: #f87171; + --error-soft: rgba(248, 113, 113, 0.12); + --info: #2dd4bf; + --info-soft: rgba(45, 212, 191, 0.08); + --urgent: #f87171; + --urgent-soft: rgba(248, 113, 113, 0.10); + --pending: #f0b429; + --pending-soft: rgba(240, 180, 41, 0.10); + --skeleton: rgba(168, 162, 155, 0.06); + --scrollbar-thumb: rgba(168, 162, 155, 0.18); + --scrollbar-track: transparent; +} + +[data-theme="dark-purple"] { + color-scheme: dark; + --page-bg: #13111a; + --surface: #1c1926; + --surface-elevated: #252230; + --sidebar-bg: #17141e; + --sidebar-border: rgba(160, 148, 180, 0.10); + --topbar-bg: rgba(23, 20, 30, 0.94); + --text-primary: #ede8f4; + --text-muted: #918899; + --brand: #b898e8; + --brand-hover: #d0b8f8; + --brand-soft: rgba(184, 152, 232, 0.12); + --brand-faint: rgba(184, 152, 232, 0.06); + --nav-active-bg: rgba(184, 152, 232, 0.10); + --nav-active-fg: #d0b8f8; + --nav-hover-bg: rgba(160, 148, 180, 0.06); + --logo-chrome-bg: linear-gradient(135deg, rgba(184, 152, 232, 0.08) 0%, rgba(255, 255, 255, 0) 50%); + --logo-blend: screen; + --accent-ridge: rgba(184, 152, 232, 0.30); + + --input-bg: #252230; + --input-border: rgba(160, 148, 180, 0.14); + --input-focus-ring: rgba(184, 152, 232, 0.30); + --input-placeholder: #6b6075; + --card-border: rgba(160, 148, 180, 0.08); + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.30); + --card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.20); + --badge-muted-bg: rgba(160, 148, 180, 0.10); + --badge-muted-fg: #918899; + --divider: rgba(160, 148, 180, 0.06); + --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand); + --btn-neutral-bg: #252230; + --btn-neutral-border: rgba(160, 148, 180, 0.12); + --btn-neutral-hover: #2e2a3a; + --success: #34d399; + --success-soft: rgba(52, 211, 153, 0.12); + --warning: #f0b429; + --warning-soft: rgba(240, 180, 41, 0.12); + --error: #f87171; + --error-soft: rgba(248, 113, 113, 0.12); + --info: #b898e8; + --info-soft: rgba(184, 152, 232, 0.08); + --urgent: #f87171; + --urgent-soft: rgba(248, 113, 113, 0.10); + --pending: #f0b429; + --pending-soft: rgba(240, 180, 41, 0.10); + --skeleton: rgba(160, 148, 180, 0.06); + --scrollbar-thumb: rgba(160, 148, 180, 0.18); + --scrollbar-track: transparent; +} + +/* ── Global reset & base ─────────────────────────────────────────────────── */ +html { + background-color: var(--page-bg); + color: var(--text-primary); + transition: background-color 150ms ease, color 150ms ease; +} + +/* Smooth theme transitions */ +body, +.app-sidebar-link, +aside, +header, +main, +[class*="rounded-xl"] { + transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, box-shadow 150ms ease; +} + +/* ── Custom scrollbar ────────────────────────────────────────────────────── */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--scrollbar-track); } +::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 999px; } +::-webkit-scrollbar-thumb:hover { background: var(--brand-soft); } + +/* ── Nuxt UI component overrides (theme-aware) ──────────────────────────── */ + +/* Buttons — global refinements for Nuxt UI buttons */ +[data-theme] button, +[data-theme] [role="button"], +[data-theme] a[class*="UButton"], +[data-theme] .ui-button-solid-primary, +[data-theme] button[class*="bg-[var(--brand)]"] { + transition: all 150ms ease; + border-radius: 0.5rem; + letter-spacing: -0.005em; +} +/* Solid primary buttons — deeper shadow for depth */ +[data-theme] [class*="bg-primary"] { + box-shadow: 0 1px 2px color-mix(in srgb, var(--brand) 20%, transparent); +} +[data-theme] [class*="bg-primary"]:hover { + box-shadow: 0 2px 6px color-mix(in srgb, var(--brand) 28%, transparent); +} + +/* Inputs & selects */ +[data-theme] input:not([type="checkbox"]):not([type="radio"]), +[data-theme] textarea, +[data-theme] select, +[data-theme] [role="combobox"] { + background-color: var(--input-bg) !important; + border-color: var(--input-border) !important; + color: var(--text-primary) !important; + transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease !important; +} +[data-theme] input:not([type="checkbox"]):not([type="radio"]):focus, +[data-theme] textarea:focus, +[data-theme] select:focus, +[data-theme] [role="combobox"]:focus { + border-color: var(--brand) !important; + box-shadow: 0 0 0 3px var(--input-focus-ring) !important; +} +[data-theme] input::placeholder, +[data-theme] textarea::placeholder { + color: var(--input-placeholder) !important; +} + +/* Cards — universal card treatment */ +.app-card { + background: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); + transition: border-color 150ms ease, box-shadow 150ms ease; +} +.app-card:hover { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); +} +.app-card-interactive:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + border-color: rgba(0, 0, 0, 0.10); +} + +/* Badge helpers */ +.app-badge { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.02em; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + transition: background-color 150ms ease, color 150ms ease; +} + +/* Dividers */ +.app-divider { + border-color: var(--divider); +} + +/* ── Logo: soften white box PNGs — blend into sidebar chrome ─────────────── */ +.app-logo-chrome { + background: var(--logo-chrome-bg); + border-radius: 0.75rem; + padding: 0.35rem 0.6rem; + isolation: isolate; +} +.app-logo-chrome img { + mix-blend-mode: var(--logo-blend); + max-height: 2rem; + width: auto; + max-width: 10rem; + object-fit: contain; + object-position: left center; + filter: contrast(1.05) saturate(1.02); +} + +/* ── Typography helpers ──────────────────────────────────────────────────── */ + +.app-heading { + color: var(--text-primary); + font-weight: 600; + letter-spacing: -0.01em; +} + +.app-body-muted { + color: var(--text-secondary); + line-height: 1.6; +} + +/* Tabular nums on dashboard container */ +.app-dashboard-nums { + font-variant-numeric: tabular-nums; +} + +/* ── Ridge accent on chrome buttons (top bar) ────────────────────────────── */ +.app-chrome-btn { + position: relative; + overflow: visible; +} +.app-chrome-btn::after { + content: ""; + position: absolute; + top: 2px; + right: 2px; + width: 7px; + height: 7px; + border-radius: 2px; + background: linear-gradient(135deg, var(--accent-ridge) 0%, transparent 55%); + opacity: 0.85; + pointer-events: none; +} + +/* ── Sidebar navigation ──────────────────────────────────────────────────── */ +.app-sidebar-link { + border-radius: 8px; + transition: background-color 150ms ease; + outline: none !important; +} +.app-sidebar-link:hover { + background-color: rgba(0, 0, 0, 0.05); +} +.app-sidebar-link:focus-visible { + outline: 2px solid var(--brand) !important; + outline-offset: -1px; +} + +.app-sidebar-link-active { + background-color: rgba(0, 0, 0, 0.045); + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Sidebar child link active — text only, no pill */ +.app-sidebar-child-active { + color: #01696f !important; + font-weight: 500; + background: transparent !important; +} + +/* Section header labels in sidebar */ +.app-sidebar-section-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #a0a09c; + margin-top: 24px; + margin-bottom: 8px; + padding: 0 12px; + user-select: none; +} + +/* ── KPI card treatment ──────────────────────────────────────────────────── */ +.app-kpi-card { + background: var(--surface); + border: 1px solid var(--card-border); + border-radius: 0.75rem; + padding: 0.875rem 1rem; + box-shadow: var(--card-shadow); + transition: box-shadow 150ms ease, border-color 150ms ease; +} +.app-kpi-card:hover { + box-shadow: var(--card-shadow-hover); +} + +/* ── Quick-link tiles ────────────────────────────────────────────────────── */ +.app-quick-link { + background: var(--surface); + border: 1px solid var(--card-border); + border-radius: 0.75rem; + padding: 1.25rem; + box-shadow: var(--card-shadow); + transition: all 150ms ease; +} +.app-quick-link:hover { + border-color: var(--brand); + box-shadow: var(--card-shadow-hover); +} +.app-quick-link:active { + transform: translateY(0); +} + +/* ── Theme picker card ───────────────────────────────────────────────────── */ +.app-theme-card { + background: var(--surface); + border: 1px solid var(--card-border); + border-radius: 0.75rem; + padding: 1.25rem; + box-shadow: var(--card-shadow); + transition: all 150ms ease; + cursor: pointer; +} +.app-theme-card:hover { + box-shadow: var(--card-shadow-hover); + border-color: var(--brand); +} +.app-theme-card-selected { + border-color: var(--brand) !important; + box-shadow: 0 0 0 2px var(--brand), var(--card-shadow); +} + +/* ── Animated skeleton ───────────────────────────────────────────────────── */ +@keyframes app-shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.app-skeleton { + background: linear-gradient(90deg, var(--skeleton) 25%, transparent 50%, var(--skeleton) 75%); + background-size: 200% 100%; + animation: app-shimmer 1.8s ease-in-out infinite; + border-radius: 0.375rem; +} + +/* ── Focus visible ring ──────────────────────────────────────────────────── */ +.app-focus-ring:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} + +/* Kill all default blue focus rings — use brand color everywhere */ +*:focus { + outline-color: var(--brand) !important; +} +*:focus-visible { + outline-color: var(--brand) !important; +} +button:focus, +a:focus, +[role="button"]:focus, +button:focus-visible, +a:focus-visible, +[role="button"]:focus-visible { + outline: 2px solid color-mix(in srgb, var(--brand) 50%, transparent) !important; + outline-offset: 2px; +} +/* Sidebar buttons — suppress focus outline in favor of hover state */ +.app-sidebar-link:focus:not(:focus-visible) { + outline: none !important; +} + +/* ── Entrance animations (disabled per spec — no entrance animations) ──── */ +.app-animate-in { /* no-op */ } +.app-stagger > * { /* no-op */ } diff --git a/app/components/AppBackToHome.vue b/app/components/AppBackToHome.vue new file mode 100644 index 0000000..b94f38a --- /dev/null +++ b/app/components/AppBackToHome.vue @@ -0,0 +1,7 @@ + diff --git a/app/components/account/AccountThemeSection.vue b/app/components/account/AccountThemeSection.vue new file mode 100644 index 0000000..9789749 --- /dev/null +++ b/app/components/account/AccountThemeSection.vue @@ -0,0 +1,129 @@ + + + diff --git a/app/components/home/DashboardWidgetBlocks.vue b/app/components/home/DashboardWidgetBlocks.vue new file mode 100644 index 0000000..8316266 --- /dev/null +++ b/app/components/home/DashboardWidgetBlocks.vue @@ -0,0 +1,55 @@ + + + diff --git a/app/components/layout/AppCommandSearch.vue b/app/components/layout/AppCommandSearch.vue new file mode 100644 index 0000000..b89b145 --- /dev/null +++ b/app/components/layout/AppCommandSearch.vue @@ -0,0 +1,439 @@ + + + + + diff --git a/app/components/layout/AppTopBar.vue b/app/components/layout/AppTopBar.vue new file mode 100644 index 0000000..30d63f6 --- /dev/null +++ b/app/components/layout/AppTopBar.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/app/components/quotes/QuoteComparativeLayout.vue b/app/components/quotes/QuoteComparativeLayout.vue new file mode 100644 index 0000000..b11f0d0 --- /dev/null +++ b/app/components/quotes/QuoteComparativeLayout.vue @@ -0,0 +1,168 @@ + + + diff --git a/app/components/quotes/auto/AcceptanceStep.vue b/app/components/quotes/auto/AcceptanceStep.vue new file mode 100644 index 0000000..3a4a68a --- /dev/null +++ b/app/components/quotes/auto/AcceptanceStep.vue @@ -0,0 +1,121 @@ + + + diff --git a/app/components/quotes/auto/CustomerVehicleStep.vue b/app/components/quotes/auto/CustomerVehicleStep.vue new file mode 100644 index 0000000..9a731d4 --- /dev/null +++ b/app/components/quotes/auto/CustomerVehicleStep.vue @@ -0,0 +1,154 @@ + + + diff --git a/app/components/quotes/auto/SetupStep.vue b/app/components/quotes/auto/SetupStep.vue new file mode 100644 index 0000000..bcf440d --- /dev/null +++ b/app/components/quotes/auto/SetupStep.vue @@ -0,0 +1,97 @@ + + + diff --git a/app/components/quotes/auto/SolicitQuotesStep.vue b/app/components/quotes/auto/SolicitQuotesStep.vue new file mode 100644 index 0000000..3298ae1 --- /dev/null +++ b/app/components/quotes/auto/SolicitQuotesStep.vue @@ -0,0 +1,97 @@ + + + diff --git a/app/components/quotes/health/AcceptanceStep.vue b/app/components/quotes/health/AcceptanceStep.vue new file mode 100644 index 0000000..57d0160 --- /dev/null +++ b/app/components/quotes/health/AcceptanceStep.vue @@ -0,0 +1,127 @@ + + + diff --git a/app/components/quotes/health/SetupStep.vue b/app/components/quotes/health/SetupStep.vue new file mode 100644 index 0000000..d6778e2 --- /dev/null +++ b/app/components/quotes/health/SetupStep.vue @@ -0,0 +1,250 @@ + + + diff --git a/app/components/quotes/health/SolicitQuotesStep.vue b/app/components/quotes/health/SolicitQuotesStep.vue new file mode 100644 index 0000000..3f3f5c6 --- /dev/null +++ b/app/components/quotes/health/SolicitQuotesStep.vue @@ -0,0 +1,90 @@ + + + diff --git a/app/components/quotes/life/AcceptanceStep.vue b/app/components/quotes/life/AcceptanceStep.vue new file mode 100644 index 0000000..8ecefcc --- /dev/null +++ b/app/components/quotes/life/AcceptanceStep.vue @@ -0,0 +1,149 @@ + + + diff --git a/app/components/quotes/life/SetupStep.vue b/app/components/quotes/life/SetupStep.vue new file mode 100644 index 0000000..6dee215 --- /dev/null +++ b/app/components/quotes/life/SetupStep.vue @@ -0,0 +1,233 @@ + + + diff --git a/app/components/quotes/life/SolicitQuotesStep.vue b/app/components/quotes/life/SolicitQuotesStep.vue new file mode 100644 index 0000000..555c91e --- /dev/null +++ b/app/components/quotes/life/SolicitQuotesStep.vue @@ -0,0 +1,90 @@ + + + diff --git a/app/components/sales/SalesFlowIndicator.vue b/app/components/sales/SalesFlowIndicator.vue new file mode 100644 index 0000000..4c72992 --- /dev/null +++ b/app/components/sales/SalesFlowIndicator.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/app/components/sales/SalesPipelineBar.vue b/app/components/sales/SalesPipelineBar.vue new file mode 100644 index 0000000..98dffec --- /dev/null +++ b/app/components/sales/SalesPipelineBar.vue @@ -0,0 +1,447 @@ + + + + + diff --git a/app/composables/useAlertConfig.ts b/app/composables/useAlertConfig.ts new file mode 100644 index 0000000..fd1d37f --- /dev/null +++ b/app/composables/useAlertConfig.ts @@ -0,0 +1,176 @@ +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('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) { + const id = `cr${++_counter}` + config.value.customRules.push({ ...rule, id }) + } + + function updateCustomRule(id: string, patch: Partial) { + 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, + } +} diff --git a/app/composables/useAnalytics.ts b/app/composables/useAnalytics.ts new file mode 100644 index 0000000..e0301f1 --- /dev/null +++ b/app/composables/useAnalytics.ts @@ -0,0 +1,147 @@ +/** + * 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('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, + } +} diff --git a/app/composables/useAppShellLayout.ts b/app/composables/useAppShellLayout.ts new file mode 100644 index 0000000..ea86c7c --- /dev/null +++ b/app/composables/useAppShellLayout.ts @@ -0,0 +1,32 @@ +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 + } +} diff --git a/app/composables/useAppTheme.ts b/app/composables/useAppTheme.ts new file mode 100644 index 0000000..38c4948 --- /dev/null +++ b/app/composables/useAppTheme.ts @@ -0,0 +1,46 @@ +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('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 + } +} diff --git a/app/composables/useAutoQuoteDraft.ts b/app/composables/useAutoQuoteDraft.ts new file mode 100644 index 0000000..6e25fd8 --- /dev/null +++ b/app/composables/useAutoQuoteDraft.ts @@ -0,0 +1,30 @@ +import type { AutoQuoteDraft } from '~/types/auto-quote-intake' + +export function emptyAutoQuoteDraft(): AutoQuoteDraft { + return { + quoteMode: null, + segment: null, + client: { + fullName: '', + email: '', + phone: '', + documentId: '', + organizationName: '' + }, + vehicle: { + subRamo: '', + clase: '', + uso: '', + marca: '', + modelo: '', + placa: '', + year: '', + capacidadPasajeros: '', + valorVehiculo: '' + }, + solicit: { + carrierIds: [], + planIds: [] + } + } +} diff --git a/app/composables/useBrokerageBranding.ts b/app/composables/useBrokerageBranding.ts new file mode 100644 index 0000000..a9b652b --- /dev/null +++ b/app/composables/useBrokerageBranding.ts @@ -0,0 +1,31 @@ +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 + } +} diff --git a/app/composables/useClientFavorites.ts b/app/composables/useClientFavorites.ts new file mode 100644 index 0000000..fc875f2 --- /dev/null +++ b/app/composables/useClientFavorites.ts @@ -0,0 +1,35 @@ +/** + * 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(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 } +} diff --git a/app/composables/useClientRegistrationModel.ts b/app/composables/useClientRegistrationModel.ts new file mode 100644 index 0000000..05742cf --- /dev/null +++ b/app/composables/useClientRegistrationModel.ts @@ -0,0 +1,47 @@ +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() + } +} diff --git a/app/composables/useColectivos.ts b/app/composables/useColectivos.ts new file mode 100644 index 0000000..61cb028 --- /dev/null +++ b/app/composables/useColectivos.ts @@ -0,0 +1,641 @@ +/** + * 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(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, + } +} diff --git a/app/composables/useCustomerAttention.ts b/app/composables/useCustomerAttention.ts new file mode 100644 index 0000000..a71cec2 --- /dev/null +++ b/app/composables/useCustomerAttention.ts @@ -0,0 +1,228 @@ +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('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) { + 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) { + 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, + } +} diff --git a/app/composables/useCustomerProfileVault.ts b/app/composables/useCustomerProfileVault.ts new file mode 100644 index 0000000..642dcc1 --- /dev/null +++ b/app/composables/useCustomerProfileVault.ts @@ -0,0 +1,18 @@ +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 } +} diff --git a/app/composables/useDashboardHomeWidgets.ts b/app/composables/useDashboardHomeWidgets.ts new file mode 100644 index 0000000..62fbaa8 --- /dev/null +++ b/app/composables/useDashboardHomeWidgets.ts @@ -0,0 +1,273 @@ +/** + * 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() + 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 = { + 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 } +> = { + 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): Record { + return { ...w } +} + +export function useDashboardHomeWidgets() { + const activePreset = ref('executive_manager') + const widgets = ref>( + cloneWidgets(DASHBOARD_ROLE_PRESETS.executive_manager.widgets) + ) + const widgetOrder = ref([...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> + 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 + } +} diff --git a/app/composables/useEmissionsQueue.ts b/app/composables/useEmissionsQueue.ts new file mode 100644 index 0000000..58109be --- /dev/null +++ b/app/composables/useEmissionsQueue.ts @@ -0,0 +1,52 @@ +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(KEY, () => []) + + function enqueue( + entry: Omit & { 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 } +} diff --git a/app/composables/useFormsCatalog.ts b/app/composables/useFormsCatalog.ts new file mode 100644 index 0000000..41605f0 --- /dev/null +++ b/app/composables/useFormsCatalog.ts @@ -0,0 +1,174 @@ +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 = { + 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 +): FormFieldGroupDef[] { + const ids = new Set() + 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 { + const m = new Map() + 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() + 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() + 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() + 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 + } +} diff --git a/app/composables/useHealthQuoteDraft.ts b/app/composables/useHealthQuoteDraft.ts new file mode 100644 index 0000000..885d622 --- /dev/null +++ b/app/composables/useHealthQuoteDraft.ts @@ -0,0 +1,33 @@ +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: [] + } + } +} diff --git a/app/composables/useLifeQuoteDraft.ts b/app/composables/useLifeQuoteDraft.ts new file mode 100644 index 0000000..3dc0327 --- /dev/null +++ b/app/composables/useLifeQuoteDraft.ts @@ -0,0 +1,36 @@ +import type { LifeQuoteDraft } from '~/types/life-quote-intake' + +export function emptyLifeQuoteDraft(): LifeQuoteDraft { + return { + quoteMode: null, + segment: null, + client: { + fullName: '', + email: '', + phone: '', + documentId: '', + organizationName: '' + }, + life: { + dateOfBirth: '', + age: '', + gender: '', + smoker: false, + preexistingConditions: false, + preexistingDetails: '', + coverageAmount: '', + coverageTerm: '', + beneficiaryName: '', + beneficiaryRelationship: '' + }, + forms: { + medicalQuestionnaire: false, + beneficiaryDesignation: false, + groupCensus: false + }, + solicit: { + carrierIds: [], + planIds: [] + } + } +} diff --git a/app/composables/usePageTitle.ts b/app/composables/usePageTitle.ts new file mode 100644 index 0000000..2805d3c --- /dev/null +++ b/app/composables/usePageTitle.ts @@ -0,0 +1,6 @@ +export function usePageTitle(title: string) { + useHead({ + title, + titleTemplate: (t) => (t ? `${t} · Policy UI` : 'Policy UI') + }) +} diff --git a/app/composables/usePdfFieldMappings.ts b/app/composables/usePdfFieldMappings.ts new file mode 100644 index 0000000..e71c81e --- /dev/null +++ b/app/composables/usePdfFieldMappings.ts @@ -0,0 +1,12 @@ +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 } +} diff --git a/app/composables/usePolicyRegistrationModel.ts b/app/composables/usePolicyRegistrationModel.ts new file mode 100644 index 0000000..18e3a76 --- /dev/null +++ b/app/composables/usePolicyRegistrationModel.ts @@ -0,0 +1,88 @@ +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) { + 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 } + ) +} diff --git a/app/composables/useProfileLayouts.ts b/app/composables/useProfileLayouts.ts new file mode 100644 index 0000000..bb550d8 --- /dev/null +++ b/app/composables/useProfileLayouts.ts @@ -0,0 +1,205 @@ +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 = { + 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(LAYOUTS_KEY, defaultLayouts) + + const activeLayoutId = useLocalStorageRef<{ id: string }>(ACTIVE_KEY, () => ({ id: 'general_service' })) + + const activeLayout = computed(() => { + 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(() => + [...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) { + layouts.value = [ + ...layouts.value, + { ...layout, isCustom: true }, + ] + } + + function updateLayout(id: string, partial: Partial) { + 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, + } +} diff --git a/app/composables/useProviderContactEmails.ts b/app/composables/useProviderContactEmails.ts new file mode 100644 index 0000000..c79ccd9 --- /dev/null +++ b/app/composables/useProviderContactEmails.ts @@ -0,0 +1,26 @@ +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 + } +} diff --git a/app/composables/useQuickLeads.ts b/app/composables/useQuickLeads.ts new file mode 100644 index 0000000..ebacd15 --- /dev/null +++ b/app/composables/useQuickLeads.ts @@ -0,0 +1,46 @@ +/** + * 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(KEY, () => []) + + function addLead(entry: Omit) { + 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 } +} diff --git a/app/composables/useQuoteRequestEmailEnabled.ts b/app/composables/useQuoteRequestEmailEnabled.ts new file mode 100644 index 0000000..25542a2 --- /dev/null +++ b/app/composables/useQuoteRequestEmailEnabled.ts @@ -0,0 +1,37 @@ +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 + } +} diff --git a/app/composables/useQuoteSession.ts b/app/composables/useQuoteSession.ts new file mode 100644 index 0000000..981bf82 --- /dev/null +++ b/app/composables/useQuoteSession.ts @@ -0,0 +1,77 @@ +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 } +} diff --git a/app/composables/useReferralChannels.ts b/app/composables/useReferralChannels.ts new file mode 100644 index 0000000..699bb08 --- /dev/null +++ b/app/composables/useReferralChannels.ts @@ -0,0 +1,67 @@ +/** + * 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(KEY, () => []) + + // Seed on first use + if (import.meta.client && channels.value.length === 0) { + channels.value = [...SEED_CHANNELS] + } + + function addChannel(entry: Omit) { + 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>) { + 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 } +} diff --git a/app/composables/useSalesPipeline.ts b/app/composables/useSalesPipeline.ts new file mode 100644 index 0000000..1937463 --- /dev/null +++ b/app/composables/useSalesPipeline.ts @@ -0,0 +1,316 @@ +/** + * Sales pipeline tracker — per-deal stage + form completion tracking. + * Persisted in localStorage. Each deal flows: + * Customer → Get Quotes → [waiting] → Present Quotes → [waiting] → Solicitud → Emission + */ +import { useLocalStorageRef } from '~/utils/useLocalStorageRef' + +export type PipelineStage = + | 'customer' + | 'get_quotes' + | 'waiting_carriers' + | 'present_quotes' + | 'waiting_client' + | 'solicitud' + | 'emission' + +export type FormStatus = 'not_started' | 'in_progress' | 'complete' + +export interface DealForm { + id: string + label: string + /** 0–100 */ + completionPct: number + status: FormStatus + requiredFields: number + completedFields: number +} + +export interface SalesDeal { + id: string + customerId: string + customerName: string + productLine: string + currentStage: PipelineStage + /** Stages that have been fully completed */ + completedStages: PipelineStage[] + /** ISO timestamps for when each stage was entered */ + stageTimestamps: Partial> + /** Forms assigned to this deal, keyed by stage */ + forms: Partial> + /** Optional carrier info */ + carrier?: string + carrierProduct?: string + /** Bind token linking compare → solicitud */ + bindToken?: string + createdAt: string + updatedAt: string +} + +const KEY = 'policy-ui-sales-pipeline-v1' + +/** Ordered stages for rendering */ +export const PIPELINE_STAGES: { id: PipelineStage; label: string; isWaiting: boolean }[] = [ + { id: 'customer', label: 'Customer', isWaiting: false }, + { id: 'get_quotes', label: 'Get Quotes', isWaiting: false }, + { id: 'waiting_carriers', label: 'Awaiting Carriers', isWaiting: true }, + { id: 'present_quotes', label: 'Present Quotes', isWaiting: false }, + { id: 'waiting_client', label: 'Awaiting Client', isWaiting: true }, + { id: 'solicitud', label: 'Solicitud', isWaiting: false }, + { id: 'emission', label: 'Emission', isWaiting: false }, +] + +function stageIndex(stage: PipelineStage): number { + return PIPELINE_STAGES.findIndex(s => s.id === stage) +} + +/** Default forms per stage (seeded when deal enters a stage) */ +function defaultFormsForStage(stage: PipelineStage, productLine: string): DealForm[] { + switch (stage) { + case 'customer': + return [ + { id: 'client-info', label: 'Client information', completionPct: 0, status: 'not_started', requiredFields: 8, completedFields: 0 }, + { id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 0, status: 'not_started', requiredFields: 3, completedFields: 0 }, + ] + case 'get_quotes': + return [ + { id: 'quote-request', label: 'Quote request form', completionPct: 0, status: 'not_started', requiredFields: 12, completedFields: 0 }, + { id: 'risk-details', label: `${productLine} risk details`, completionPct: 0, status: 'not_started', requiredFields: 10, completedFields: 0 }, + ] + case 'solicitud': + return [ + { id: 'solicitud-form', label: 'Solicitud de seguro', completionPct: 0, status: 'not_started', requiredFields: 18, completedFields: 0 }, + { id: 'payment-auth', label: 'Payment authorization', completionPct: 0, status: 'not_started', requiredFields: 5, completedFields: 0 }, + { id: 'beneficiaries', label: 'Beneficiary designation', completionPct: 0, status: 'not_started', requiredFields: 4, completedFields: 0 }, + ] + case 'emission': + return [ + { id: 'policy-review', label: 'Policy review checklist', completionPct: 0, status: 'not_started', requiredFields: 6, completedFields: 0 }, + ] + default: + return [] + } +} + +/** Seed demo deals */ +const SEED_DEALS: SalesDeal[] = [ + { + id: 'deal-001', + customerId: 'cust-001', + customerName: 'María Elena Pérez Solano', + productLine: 'Auto', + currentStage: 'waiting_carriers', + completedStages: ['customer', 'get_quotes'], + stageTimestamps: { + customer: '2026-04-02T09:00:00Z', + get_quotes: '2026-04-02T09:30:00Z', + waiting_carriers: '2026-04-02T10:00:00Z', + }, + forms: { + customer: [ + { id: 'client-info', label: 'Client information', completionPct: 100, status: 'complete', requiredFields: 8, completedFields: 8 }, + { id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 100, status: 'complete', requiredFields: 3, completedFields: 3 }, + ], + get_quotes: [ + { id: 'quote-request', label: 'Quote request form', completionPct: 100, status: 'complete', requiredFields: 12, completedFields: 12 }, + { id: 'risk-details', label: 'Auto risk details', completionPct: 100, status: 'complete', requiredFields: 10, completedFields: 10 }, + ], + }, + createdAt: '2026-04-02T09:00:00Z', + updatedAt: '2026-04-02T10:00:00Z', + }, + { + id: 'deal-002', + customerId: 'cust-002', + customerName: 'Roberto Jiménez Mora', + productLine: 'Life', + currentStage: 'solicitud', + completedStages: ['customer', 'get_quotes', 'waiting_carriers', 'present_quotes', 'waiting_client'], + stageTimestamps: { + customer: '2026-03-28T11:00:00Z', + get_quotes: '2026-03-28T11:30:00Z', + waiting_carriers: '2026-03-28T12:00:00Z', + present_quotes: '2026-04-01T14:00:00Z', + waiting_client: '2026-04-01T15:00:00Z', + solicitud: '2026-04-03T09:00:00Z', + }, + forms: { + customer: [ + { id: 'client-info', label: 'Client information', completionPct: 100, status: 'complete', requiredFields: 8, completedFields: 8 }, + { id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 100, status: 'complete', requiredFields: 3, completedFields: 3 }, + ], + get_quotes: [ + { id: 'quote-request', label: 'Quote request form', completionPct: 100, status: 'complete', requiredFields: 12, completedFields: 12 }, + { id: 'risk-details', label: 'Life risk details', completionPct: 100, status: 'complete', requiredFields: 10, completedFields: 10 }, + ], + solicitud: [ + { id: 'solicitud-form', label: 'Solicitud de seguro', completionPct: 72, status: 'in_progress', requiredFields: 18, completedFields: 13 }, + { id: 'payment-auth', label: 'Payment authorization', completionPct: 40, status: 'in_progress', requiredFields: 5, completedFields: 2 }, + { id: 'beneficiaries', label: 'Beneficiary designation', completionPct: 0, status: 'not_started', requiredFields: 4, completedFields: 0 }, + ], + }, + carrier: 'ASSA', + carrierProduct: 'Universal II', + bindToken: 'bind-abc123', + createdAt: '2026-03-28T11:00:00Z', + updatedAt: '2026-04-03T09:00:00Z', + }, + { + id: 'deal-003', + customerId: 'cust-005', + customerName: 'Sofía Rojas Delgado', + productLine: 'Auto', + currentStage: 'get_quotes', + completedStages: ['customer'], + stageTimestamps: { + customer: '2026-04-05T08:00:00Z', + get_quotes: '2026-04-05T08:15:00Z', + }, + forms: { + customer: [ + { id: 'client-info', label: 'Client information', completionPct: 100, status: 'complete', requiredFields: 8, completedFields: 8 }, + { id: 'kyc-docs', label: 'KYC / ID documents', completionPct: 67, status: 'in_progress', requiredFields: 3, completedFields: 2 }, + ], + get_quotes: [ + { id: 'quote-request', label: 'Quote request form', completionPct: 50, status: 'in_progress', requiredFields: 12, completedFields: 6 }, + { id: 'risk-details', label: 'Auto risk details', completionPct: 0, status: 'not_started', requiredFields: 10, completedFields: 0 }, + ], + }, + createdAt: '2026-04-05T08:00:00Z', + updatedAt: '2026-04-05T08:15:00Z', + }, +] + +export function useSalesPipeline() { + const deals = useLocalStorageRef(KEY, () => []) + + // Seed on first use + if (import.meta.client && deals.value.length === 0) { + deals.value = [...SEED_DEALS] + } + + /** Get a deal by ID */ + function getDeal(dealId: string) { + return deals.value.find(d => d.id === dealId) + } + + /** Get deals for a specific customer */ + function getDealsForCustomer(customerId: string) { + return deals.value.filter(d => d.customerId === customerId) + } + + /** Get the active (most recent non-emission) deal for a customer */ + function getActiveDeal(customerId: string) { + return deals.value + .filter(d => d.customerId === customerId && d.currentStage !== 'emission') + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0] + } + + /** Create a new deal */ + function createDeal(customerId: string, customerName: string, productLine: string): SalesDeal { + const now = new Date().toISOString() + const deal: SalesDeal = { + id: 'deal-' + (crypto.randomUUID?.() ?? String(Date.now())).slice(0, 8), + customerId, + customerName, + productLine, + currentStage: 'customer', + completedStages: [], + stageTimestamps: { customer: now }, + forms: { customer: defaultFormsForStage('customer', productLine) }, + createdAt: now, + updatedAt: now, + } + deals.value = [deal, ...deals.value] + return deal + } + + /** Advance deal to the next stage */ + function advanceStage(dealId: string) { + const deal = deals.value.find(d => d.id === dealId) + if (!deal) return + + const currentIdx = stageIndex(deal.currentStage) + const nextStage = PIPELINE_STAGES[currentIdx + 1] + if (!nextStage) return + + const now = new Date().toISOString() + deal.completedStages = [...new Set([...deal.completedStages, deal.currentStage])] + deal.currentStage = nextStage.id + deal.stageTimestamps = { ...deal.stageTimestamps, [nextStage.id]: now } + deal.updatedAt = now + + // Seed forms for the new stage if not already present + if (!deal.forms[nextStage.id]) { + deal.forms = { ...deal.forms, [nextStage.id]: defaultFormsForStage(nextStage.id, deal.productLine) } + } + + // Trigger reactivity + deals.value = [...deals.value] + } + + /** Set deal to a specific stage (e.g., when quotes arrive) */ + function setStage(dealId: string, stage: PipelineStage) { + const deal = deals.value.find(d => d.id === dealId) + if (!deal) return + + const now = new Date().toISOString() + // Mark all stages before the target as completed + const targetIdx = stageIndex(stage) + const completed = PIPELINE_STAGES.slice(0, targetIdx).map(s => s.id) + deal.completedStages = [...new Set([...deal.completedStages, ...completed])] + deal.currentStage = stage + deal.stageTimestamps = { ...deal.stageTimestamps, [stage]: now } + deal.updatedAt = now + + if (!deal.forms[stage]) { + deal.forms = { ...deal.forms, [stage]: defaultFormsForStage(stage, deal.productLine) } + } + + deals.value = [...deals.value] + } + + /** Update form completion within a deal */ + function updateFormProgress(dealId: string, stage: PipelineStage, formId: string, completedFields: number) { + const deal = deals.value.find(d => d.id === dealId) + if (!deal) return + const stageForms = deal.forms[stage] + if (!stageForms) return + const form = stageForms.find(f => f.id === formId) + if (!form) return + + form.completedFields = Math.min(completedFields, form.requiredFields) + form.completionPct = form.requiredFields > 0 ? Math.round((form.completedFields / form.requiredFields) * 100) : 100 + form.status = form.completionPct === 0 ? 'not_started' : form.completionPct === 100 ? 'complete' : 'in_progress' + deal.updatedAt = new Date().toISOString() + + deals.value = [...deals.value] + } + + /** Remove a deal */ + function removeDeal(dealId: string) { + deals.value = deals.value.filter(d => d.id !== dealId) + } + + /** Computed: stage completion percentage for a deal's current stage forms */ + function stageFormProgress(deal: SalesDeal, stage: PipelineStage): number { + const forms = deal.forms[stage] + if (!forms || forms.length === 0) return 0 + const totalRequired = forms.reduce((s, f) => s + f.requiredFields, 0) + const totalCompleted = forms.reduce((s, f) => s + f.completedFields, 0) + return totalRequired > 0 ? Math.round((totalCompleted / totalRequired) * 100) : 100 + } + + return { + deals, + getDeal, + getDealsForCustomer, + getActiveDeal, + createDeal, + advanceStage, + setStage, + updateFormProgress, + removeDeal, + stageFormProgress, + } +} diff --git a/app/composables/useSidebarFeatures.ts b/app/composables/useSidebarFeatures.ts new file mode 100644 index 0000000..2fa4359 --- /dev/null +++ b/app/composables/useSidebarFeatures.ts @@ -0,0 +1,23 @@ +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 | null = null + +export function useSidebarFeatures() { + if (!_shared) { + _shared = useLocalStorageRef(KEY, () => ({ + showWorkstations: false, + showAiTools: false, + showLeadsHub: true, + })) + } + return _shared +} diff --git a/app/composables/useSuperAdmin.ts b/app/composables/useSuperAdmin.ts new file mode 100644 index 0000000..cd2a8de --- /dev/null +++ b/app/composables/useSuperAdmin.ts @@ -0,0 +1,21 @@ +/** + * 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 } +} diff --git a/app/composables/useSupportTickets.ts b/app/composables/useSupportTickets.ts new file mode 100644 index 0000000..f49ad05 --- /dev/null +++ b/app/composables/useSupportTickets.ts @@ -0,0 +1,184 @@ +/** + * 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 + routingRules: RoutingRule[] +} + +function buildDefaults(): SupportState { + return { + tickets: [...MOCK_SUPPORT_TICKETS], + details: { ...MOCK_TICKET_DETAILS }, + routingRules: [...MOCK_ROUTING_RULES], + } +} + +export function useSupportTickets() { + const state = useLocalStorageRef('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) { + 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 = { + 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) { + 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, + } +} diff --git a/app/composables/useWelcomeDashboard.ts b/app/composables/useWelcomeDashboard.ts new file mode 100644 index 0000000..8ada842 --- /dev/null +++ b/app/composables/useWelcomeDashboard.ts @@ -0,0 +1,25 @@ +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 { + const app = useAppConfig() + const { saved: branding } = useBrokerageBranding() + + return computed((): WelcomeDashboardConfig => { + const base = (app.welcomeDashboard ?? {}) as Partial + 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 ?? [] + } + }) +} diff --git a/app/data/auto-quote-intake.ts b/app/data/auto-quote-intake.ts new file mode 100644 index 0000000..304a99a --- /dev/null +++ b/app/data/auto-quote-intake.ts @@ -0,0 +1,70 @@ +/** Auto quoting — option lists aligned with grid filters / production LOB vocabulary (ES labels). */ + +export const AUTO_RAMO_LABEL = 'Vehículos' + +/** First option is empty — pair with placeholder “Select one” in UI */ +export const AUTO_SUB_RAMO_OPTIONS: { label: string; value: string }[] = [ + { label: 'Manual', value: 'manual' }, + { label: 'Cobertura completa', value: 'cobertura_completa' }, + { label: 'Daños a terceros', value: 'danos_terceros' } +] + +export const AUTO_CLASE_OPTIONS: { label: string; value: string }[] = [ + { label: 'Pickup', value: 'pickup' }, + { label: 'Mula', value: 'mula' }, + { label: 'Van', value: 'van' }, + { label: 'MiniVan', value: 'minivan' }, + { label: 'Panel', value: 'panel' }, + { label: 'Camión', value: 'camion' }, + { label: 'Camioneta', value: 'camioneta' }, + { label: 'Sedan', value: 'sedan' }, + { label: 'Cabezal', value: 'cabezal' }, + { label: 'Bus (0-15)', value: 'bus_0_15' }, + { label: 'Bus (16-30)', value: 'bus_16_30' }, + { label: 'Bus (+30)', value: 'bus_30_plus' }, + { label: 'Liviano', value: 'liviano' }, + { label: 'Mediano', value: 'mediano' }, + { label: 'Pesado', value: 'pesado' }, + { label: 'Moto', value: 'moto' } +] + +export const AUTO_USO_OPTIONS: { label: string; value: string }[] = [ + { label: 'Particular', value: 'particular' }, + { label: 'Comercial', value: 'comercial' } +] + +export const AUTO_MARCA_OPTIONS: { label: string; value: string }[] = [ + { label: 'Toyota', value: 'toyota' } +] + +export const AUTO_MODELO_OPTIONS: { label: string; value: string }[] = [ + { label: 'Cellica', value: 'cellica' }, + { label: 'Corolla', value: 'corolla' } +] + +const currentYear = new Date().getFullYear() + +export const AUTO_YEAR_OPTIONS: { label: string; value: string }[] = [ + ...Array.from({ length: 22 }, (_, i) => { + const y = String(currentYear - i) + return { label: y, value: y } + }) +] + +/** + * Carriers available for solicit — in production, matches providers configured under Settings / Providers + * (each has a quoting email on file). + */ +export const AUTO_QUOTE_CARRIERS: { id: string; name: string; detail: string }[] = [ + { id: 'mapfre', name: 'Mapfre', detail: 'Quoting email on file in provider profile' }, + { id: 'seguros_aurora', name: 'Seguros Aurora', detail: 'Quoting email on file in provider profile' }, + { id: 'continental', name: 'Continental', detail: 'Quoting email on file in provider profile' }, + { id: 'internacional', name: 'Internacional', detail: 'Quoting email on file in provider profile' } +] + +/** Predetermined plan packages — comparative mode builds side-by-side rows from these */ +export const AUTO_COVERAGE_PLANS: { id: string; label: string; hint: string }[] = [ + { id: 'cc_full', label: 'Cobertura completa', hint: 'Collision, comprehensive, liability' }, + { id: 'dat', label: 'Daños a terceros', hint: 'Third-party liability' }, + { id: 'plan_corporate', label: 'Paquete corporativo', hint: 'Fleet-friendly endorsements' } +] diff --git a/app/data/form-field-groups.json b/app/data/form-field-groups.json new file mode 100644 index 0000000..56faaff --- /dev/null +++ b/app/data/form-field-groups.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "groups": [ + { + "id": "kyc_identity", + "title": "Identificación", + "description": "Cédula, pasaporte, PEP según SSRP", + "fieldKeys": ["full_name", "document_id", "document_expiry", "nationality"] + }, + { + "id": "life_risk_health", + "title": "Salud y hábitos (vida)", + "description": "Declaración de salud, tabaco, deportes de riesgo", + "fieldKeys": ["smoking", "height_cm", "weight_kg", "medical_conditions"] + }, + { + "id": "auto_vehicle", + "title": "Vehículo", + "description": "Placa, uso, valor, accesorios", + "fieldKeys": ["plate", "vin", "year", "use_type", "declared_value"] + }, + { + "id": "health_local_cover", + "title": "Salud local · cobertura", + "description": "Red, deducible, preexistencias", + "fieldKeys": ["network", "deductible_usd", "preexisting_disclosure"] + }, + { + "id": "health_intl_cover", + "title": "Salud internacional", + "description": "Zona de cobertura, repatriación, USA cover", + "fieldKeys": ["coverage_zone", "usa_cover", "repatriation"] + }, + { + "id": "home_property", + "title": "Propiedad (hogar)", + "description": "Ubicación, construcción, suma contenido", + "fieldKeys": ["address", "construction_type", "contents_sum"] + } + ] +} diff --git a/app/data/forms-catalog.json b/app/data/forms-catalog.json new file mode 100644 index 0000000..7d075ed --- /dev/null +++ b/app/data/forms-catalog.json @@ -0,0 +1,116 @@ +{ + "version": 1, + "rows": [ + { + "id": 39, + "description": "1031083 GE_ADB_GEN_OPTIMA_DIG 250507", + "insurerSlugs": ["optima"], + "subRamoKey": "danos_terceros_completa", + "subRamoLabel": "Daños a terceros · Cobertura completa", + "personKinds": "both", + "productLine": "auto_full_coverage", + "fileUrl": "/forms/README.txt", + "fileLabel": "17569365751031083-GE_ADB_GEN_OPTIMA.pdf", + "badge": 39, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "auto_vehicle"] + }, + { + "id": 38, + "description": "1031082 GE_ADB_GEN_OPTIMA_DAT", + "insurerSlugs": ["optima"], + "subRamoKey": "vehiculos_dat", + "subRamoLabel": "Vehículos · Daños a terceros (DAT)", + "personKinds": "both", + "productLine": "auto_dat_liability", + "fileUrl": "/forms/README.txt", + "fileLabel": "OPTIMA_DAT_solicitud.pdf", + "badge": 38, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "auto_vehicle"] + }, + { + "id": 37, + "description": "MAPFRE_MUNDIAL_AUTO_FULL", + "insurerSlugs": ["mapfre"], + "subRamoKey": "danos_terceros_completa", + "subRamoLabel": "Daños a terceros · Cobertura completa", + "personKinds": "both", + "productLine": "auto_full_coverage", + "fileUrl": "/forms/README.txt", + "fileLabel": "MAPFRE_MUNDIAL_FOREVER_PLUS.pdf", + "badge": 37, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "auto_vehicle"] + }, + { + "id": 36, + "description": "ASSA_VIDA_UNIVERSAL_PACK", + "insurerSlugs": ["assa"], + "subRamoKey": "vida_universal", + "subRamoLabel": "Vida universal · Protección y ahorro", + "personKinds": "natural", + "productLine": "life", + "fileUrl": "/forms/README.txt", + "fileLabel": "ASSA_Universal_II_illustration.pdf", + "badge": 36, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "life_risk_health"] + }, + { + "id": 35, + "description": "SALUD_LOCAL_KYC", + "insurerSlugs": ["mapfre", "assa"], + "subRamoKey": "salud_local", + "subRamoLabel": "Salud local", + "personKinds": "both", + "productLine": "health_local", + "fileUrl": "/forms/README.txt", + "fileLabel": "KYC_salud_local.pdf", + "badge": 35, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "health_local_cover"] + }, + { + "id": 34, + "description": "SALUD_INTERNACIONAL_KYC", + "insurerSlugs": ["palig", "mapfre"], + "subRamoKey": "salud_internacional", + "subRamoLabel": "Salud internacional / IPMI", + "personKinds": "both", + "productLine": "health_international", + "fileUrl": "/forms/README.txt", + "fileLabel": "IPMI_solicitud.pdf", + "badge": 34, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "health_intl_cover"] + }, + { + "id": 33, + "description": "CEDULA_NATURAL", + "insurerSlugs": ["optima", "mapfre", "assa", "acerta", "fedpa", "ancon"], + "subRamoKey": "any", + "subRamoLabel": "Cualquier ramo (identificación)", + "personKinds": "natural", + "productLine": null, + "fileUrl": "/forms/README.txt", + "fileLabel": "cedula_ejemplo.png", + "kind": "identity", + "fieldGroupIds": ["kyc_identity"] + }, + { + "id": 32, + "description": "ACERTA_VEH_FULL_1012013", + "insurerSlugs": ["acerta"], + "subRamoKey": "danos_terceros_completa", + "subRamoLabel": "Daños a terceros · Cobertura completa", + "personKinds": "both", + "productLine": "auto_full_coverage", + "fileUrl": "/forms/README.txt", + "fileLabel": "GE_ATC_GEN_ACERTA_DIG.pdf", + "badge": 32, + "kind": "carrier_pdf", + "fieldGroupIds": ["kyc_identity", "auto_vehicle"] + } + ] +} diff --git a/app/data/health-quote-intake.ts b/app/data/health-quote-intake.ts new file mode 100644 index 0000000..80d7697 --- /dev/null +++ b/app/data/health-quote-intake.ts @@ -0,0 +1,53 @@ +/** Health quoting — mock carriers, plans, and reference rate table (age bands). */ + +export const HEALTH_QUOTE_CARRIERS: { id: string; name: string; detail: string; hasPublishedRateTable?: boolean }[] = [ + { + id: 'vida_plena', + name: 'Vida Plena Salud', + detail: 'Quoting email on file · published age-banded table', + hasPublishedRateTable: true + }, + { + id: 'salud_global', + name: 'Salud Global', + detail: 'Quoting email on file', + hasPublishedRateTable: false + }, + { + id: 'integral_med', + name: 'Integral Medical', + detail: 'Quoting email on file', + hasPublishedRateTable: true + } +] + +export const HEALTH_COVERAGE_PLANS: { id: string; label: string; hint: string }[] = [ + { id: 'local_base', label: 'Local · Base', hint: 'In-country network, standard deductible' }, + { id: 'local_plus', label: 'Local · Plus', hint: 'Broader network, lower copay' }, + { id: 'intl_major', label: 'International · Major medical', hint: 'Evacuation + US/EU coverage tier' } +] + +export const HEALTH_COVERAGE_AREA: { label: string; value: string }[] = [ + { label: 'Local', value: 'local' }, + { label: 'International', value: 'international' } +] + +export const HEALTH_NETWORK_TIER: { label: string; value: string }[] = [ + { label: 'Preferred / cerrado', value: 'preferred' }, + { label: 'Open / amplio', value: 'open' } +] + +export const HEALTH_DEDUCTIBLE: { label: string; value: string }[] = [ + { label: '$500', value: '500' }, + { label: '$1,000', value: '1000' }, + { label: '$2,500', value: '2500' } +] + +/** Mock age-band premium table (USD/mo) — some carriers publish this instead of email-only quotes */ +export const HEALTH_AGE_BAND_REFERENCE: { ageBand: string; employee: number; spouse: number; children: number }[] = [ + { ageBand: '0–17', employee: 0, spouse: 0, children: 118 }, + { ageBand: '18–29', employee: 142, spouse: 198, children: 0 }, + { ageBand: '30–44', employee: 186, spouse: 251, children: 0 }, + { ageBand: '45–54', employee: 264, spouse: 318, children: 0 }, + { ageBand: '55–64', employee: 352, spouse: 401, children: 0 } +] diff --git a/app/data/life-quote-intake.ts b/app/data/life-quote-intake.ts new file mode 100644 index 0000000..cab3a09 --- /dev/null +++ b/app/data/life-quote-intake.ts @@ -0,0 +1,45 @@ +/** Life quoting — mock carriers, plans, and option lists. */ + +export const LIFE_QUOTE_CARRIERS: { id: string; name: string; detail: string }[] = [ + { id: 'vida_plena', name: 'Vida Plena', detail: 'Quoting email on file in provider profile' }, + { id: 'seguros_del_pacifico', name: 'Seguros del Pacífico', detail: 'Quoting email on file in provider profile' }, + { id: 'continental_life', name: 'Continental Life', detail: 'Quoting email on file in provider profile' } +] + +export const LIFE_COVERAGE_PLANS: { id: string; label: string; hint: string }[] = [ + { id: 'term_basic', label: 'Term life · Basic', hint: 'Level-premium term, standard coverage' }, + { id: 'term_plus', label: 'Term life · Plus', hint: 'Term with accidental-death rider' }, + { id: 'whole_life', label: 'Whole life', hint: 'Permanent coverage with cash-value component' }, + { id: 'keyman', label: 'Key person', hint: 'Business-owned policy on key employee' } +] + +export const LIFE_GENDER_OPTIONS: { label: string; value: string }[] = [ + { label: 'Male', value: 'male' }, + { label: 'Female', value: 'female' } +] + +export const LIFE_COVERAGE_TERM_OPTIONS: { label: string; value: string }[] = [ + { label: '10 years', value: '10' }, + { label: '15 years', value: '15' }, + { label: '20 years', value: '20' }, + { label: '30 years', value: '30' }, + { label: 'Whole life', value: 'whole' } +] + +export const LIFE_COVERAGE_AMOUNT_OPTIONS: { label: string; value: string }[] = [ + { label: '$25,000', value: '25000' }, + { label: '$50,000', value: '50000' }, + { label: '$100,000', value: '100000' }, + { label: '$250,000', value: '250000' }, + { label: '$500,000', value: '500000' }, + { label: '$1,000,000', value: '1000000' } +] + +export const LIFE_BENEFICIARY_RELATIONSHIP_OPTIONS: { label: string; value: string }[] = [ + { label: 'Spouse', value: 'spouse' }, + { label: 'Child', value: 'child' }, + { label: 'Parent', value: 'parent' }, + { label: 'Sibling', value: 'sibling' }, + { label: 'Business entity', value: 'business' }, + { label: 'Other', value: 'other' } +] diff --git a/app/data/mock-analytics.ts b/app/data/mock-analytics.ts new file mode 100644 index 0000000..6340e55 --- /dev/null +++ b/app/data/mock-analytics.ts @@ -0,0 +1,363 @@ +// ─── Business Analytics — Types, Labels, Mock Data ─────────────────────────── + +export type AnalyticsTimePoint = { m: string; v: number; display: string } +export type AnalyticsDomainId = 'production' | 'claims' | 'pipeline' | 'service' +export type AnalyticsChartType = 'line' | 'bar' | 'area' + +export interface AnalyticsMetric { + id: string + domain: AnalyticsDomainId + label: string + unit: string + data12m: AnalyticsTimePoint[] + change: string + changeTone: 'positive' | 'negative' | 'neutral' + defaultChartType: AnalyticsChartType +} + +export interface AnalyticsKpiSummary { + id: string + label: string + value: string + change: string + changeTone: 'positive' | 'negative' | 'neutral' + hint: string + sparkline: number[] +} + +// ─── Domain Labels ─────────────────────────────────────────────────────────── + +export const ANALYTICS_DOMAIN_LABELS: Record = { + production: 'Producción', + claims: 'Siniestros', + pipeline: 'Pipeline', + service: 'Servicio', +} + +// ─── Headline KPI Summaries ────────────────────────────────────────────────── + +export const ANALYTICS_KPI_SUMMARIES: AnalyticsKpiSummary[] = [ + { id: 'gwp', label: 'GWP Written', value: '$5.41M', change: '+6.2%', changeTone: 'positive', hint: 'Gross written premium YTD', sparkline: [72, 68, 76, 74, 81, 88, 85, 90, 87, 92, 95, 98] }, + { id: 'policies', label: 'Active Policies', value: '342', change: '+12', changeTone: 'positive', hint: 'Currently active policies', sparkline: [290, 295, 298, 305, 310, 315, 318, 322, 328, 332, 338, 342] }, + { id: 'loss-ratio', label: 'Loss Ratio', value: '58%', change: '-3.1%', changeTone: 'positive', hint: 'Claims paid / earned premium', sparkline: [68, 72, 65, 63, 60, 58, 61, 59, 57, 60, 58, 58] }, + { id: 'retention', label: 'Retention Rate', value: '91%', change: '+1.2%', changeTone: 'positive', hint: 'Client renewal rate', sparkline: [86, 87, 88, 87, 89, 90, 89, 90, 91, 90, 91, 91] }, + { id: 'open-claims', label: 'Open Claims', value: '7', change: '-2', changeTone: 'positive', hint: 'Currently unresolved claims', sparkline: [12, 11, 10, 9, 11, 10, 8, 9, 8, 7, 8, 7] }, + { id: 'pipeline-value', label: 'Pipeline Value', value: '$6.2M', change: '+$820K', changeTone: 'positive', hint: 'Open quoted premium', sparkline: [42, 45, 48, 50, 52, 55, 53, 56, 58, 60, 62, 65] }, +] + +// ─── Metrics: Production & Revenue ─────────────────────────────────────────── + +const productionMetrics: AnalyticsMetric[] = [ + { + id: 'gwp', domain: 'production', label: 'GWP Written', unit: '$', + change: '+6.2%', changeTone: 'positive', defaultChartType: 'area', + data12m: [ + { m: 'May', v: 62, display: '$3.82M' }, { m: 'Jun', v: 65, display: '$4.01M' }, + { m: 'Jul', v: 60, display: '$3.70M' }, { m: 'Aug', v: 68, display: '$4.19M' }, + { m: 'Sep', v: 71, display: '$4.38M' }, { m: 'Oct', v: 72, display: '$4.52M' }, + { m: 'Nov', v: 68, display: '$4.28M' }, { m: 'Dec', v: 76, display: '$4.71M' }, + { m: 'Jan', v: 74, display: '$4.61M' }, { m: 'Feb', v: 81, display: '$4.98M' }, + { m: 'Mar', v: 88, display: '$5.41M' }, { m: 'Apr', v: 91, display: '$5.58M' }, + ], + }, + { + id: 'premium-by-lob', domain: 'production', label: 'Premium by LOB', unit: '$', + change: '+4.8%', changeTone: 'positive', defaultChartType: 'bar', + data12m: [ + { m: 'Auto', v: 85, display: '$1.82M' }, { m: 'Health', v: 72, display: '$1.54M' }, + { m: 'Life', v: 48, display: '$1.03M' }, { m: 'Property', v: 35, display: '$750K' }, + { m: 'Marine', v: 18, display: '$386K' }, { m: 'Liability', v: 12, display: '$257K' }, + { m: 'Surety', v: 8, display: '$171K' }, { m: 'Travel', v: 5, display: '$107K' }, + { m: 'Other', v: 3, display: '$64K' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + ], + }, + { + id: 'new-biz-vs-renewal', domain: 'production', label: 'New Biz vs Renewal', unit: '$', + change: '+$820K', changeTone: 'positive', defaultChartType: 'bar', + data12m: [ + { m: 'May', v: 45, display: '$1.8M' }, { m: 'Jun', v: 48, display: '$1.9M' }, + { m: 'Jul', v: 42, display: '$1.7M' }, { m: 'Aug', v: 52, display: '$2.1M' }, + { m: 'Sep', v: 55, display: '$2.2M' }, { m: 'Oct', v: 52, display: '$2.4M' }, + { m: 'Nov', v: 48, display: '$2.2M' }, { m: 'Dec', v: 62, display: '$2.9M' }, + { m: 'Jan', v: 58, display: '$2.7M' }, { m: 'Feb', v: 64, display: '$3.0M' }, + { m: 'Mar', v: 70, display: '$3.2M' }, { m: 'Apr', v: 73, display: '$3.4M' }, + ], + }, + { + id: 'commission-revenue', domain: 'production', label: 'Commission Revenue', unit: '$', + change: '+8.4%', changeTone: 'positive', defaultChartType: 'area', + data12m: [ + { m: 'May', v: 48, display: '$590K' }, { m: 'Jun', v: 50, display: '$615K' }, + { m: 'Jul', v: 46, display: '$565K' }, { m: 'Aug', v: 53, display: '$652K' }, + { m: 'Sep', v: 55, display: '$680K' }, { m: 'Oct', v: 55, display: '$680K' }, + { m: 'Nov', v: 52, display: '$640K' }, { m: 'Dec', v: 60, display: '$740K' }, + { m: 'Jan', v: 58, display: '$715K' }, { m: 'Feb', v: 65, display: '$800K' }, + { m: 'Mar', v: 72, display: '$886K' }, { m: 'Apr', v: 75, display: '$923K' }, + ], + }, + { + id: 'policies-bound', domain: 'production', label: 'Policies Bound', unit: '#', + change: '+12', changeTone: 'positive', defaultChartType: 'bar', + data12m: [ + { m: 'May', v: 28, display: '28' }, { m: 'Jun', v: 30, display: '30' }, + { m: 'Jul', v: 25, display: '25' }, { m: 'Aug', v: 33, display: '33' }, + { m: 'Sep', v: 35, display: '35' }, { m: 'Oct', v: 32, display: '32' }, + { m: 'Nov', v: 28, display: '28' }, { m: 'Dec', v: 35, display: '35' }, + { m: 'Jan', v: 38, display: '38' }, { m: 'Feb', v: 36, display: '36' }, + { m: 'Mar', v: 42, display: '42' }, { m: 'Apr', v: 44, display: '44' }, + ], + }, + { + id: 'avg-premium', domain: 'production', label: 'Avg Premium', unit: '$', + change: '+2.1%', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 52, display: '$12.4K' }, { m: 'Jun', v: 53, display: '$12.6K' }, + { m: 'Jul', v: 51, display: '$12.1K' }, { m: 'Aug', v: 54, display: '$12.8K' }, + { m: 'Sep', v: 55, display: '$13.1K' }, { m: 'Oct', v: 56, display: '$13.3K' }, + { m: 'Nov', v: 55, display: '$13.1K' }, { m: 'Dec', v: 57, display: '$13.6K' }, + { m: 'Jan', v: 56, display: '$13.3K' }, { m: 'Feb', v: 58, display: '$13.8K' }, + { m: 'Mar', v: 59, display: '$14.0K' }, { m: 'Apr', v: 60, display: '$14.3K' }, + ], + }, +] + +// ─── Metrics: Claims & Loss ────────────────────────────────────────────────── + +const claimsMetrics: AnalyticsMetric[] = [ + { + id: 'claims-count', domain: 'claims', label: 'Claims Opened', unit: '#', + change: '-2', changeTone: 'positive', defaultChartType: 'bar', + data12m: [ + { m: 'May', v: 8, display: '8' }, { m: 'Jun', v: 6, display: '6' }, + { m: 'Jul', v: 10, display: '10' }, { m: 'Aug', v: 7, display: '7' }, + { m: 'Sep', v: 9, display: '9' }, { m: 'Oct', v: 8, display: '8' }, + { m: 'Nov', v: 6, display: '6' }, { m: 'Dec', v: 5, display: '5' }, + { m: 'Jan', v: 7, display: '7' }, { m: 'Feb', v: 8, display: '8' }, + { m: 'Mar', v: 6, display: '6' }, { m: 'Apr', v: 5, display: '5' }, + ], + }, + { + id: 'loss-ratio', domain: 'claims', label: 'Loss Ratio', unit: '%', + change: '-3.1%', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 66, display: '66%' }, { m: 'Jun', v: 64, display: '64%' }, + { m: 'Jul', v: 70, display: '70%' }, { m: 'Aug', v: 67, display: '67%' }, + { m: 'Sep', v: 65, display: '65%' }, { m: 'Oct', v: 68, display: '68%' }, + { m: 'Nov', v: 72, display: '72%' }, { m: 'Dec', v: 65, display: '65%' }, + { m: 'Jan', v: 63, display: '63%' }, { m: 'Feb', v: 60, display: '60%' }, + { m: 'Mar', v: 58, display: '58%' }, { m: 'Apr', v: 57, display: '57%' }, + ], + }, + { + id: 'avg-resolution-days', domain: 'claims', label: 'Avg Resolution', unit: 'days', + change: '-4d', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 38, display: '38d' }, { m: 'Jun', v: 35, display: '35d' }, + { m: 'Jul', v: 40, display: '40d' }, { m: 'Aug', v: 36, display: '36d' }, + { m: 'Sep', v: 34, display: '34d' }, { m: 'Oct', v: 32, display: '32d' }, + { m: 'Nov', v: 30, display: '30d' }, { m: 'Dec', v: 28, display: '28d' }, + { m: 'Jan', v: 31, display: '31d' }, { m: 'Feb', v: 29, display: '29d' }, + { m: 'Mar', v: 27, display: '27d' }, { m: 'Apr', v: 26, display: '26d' }, + ], + }, + { + id: 'reserve-trend', domain: 'claims', label: 'Reserve Trend', unit: '$', + change: '-$45K', changeTone: 'positive', defaultChartType: 'area', + data12m: [ + { m: 'May', v: 75, display: '$380K' }, { m: 'Jun', v: 70, display: '$355K' }, + { m: 'Jul', v: 80, display: '$405K' }, { m: 'Aug', v: 72, display: '$365K' }, + { m: 'Sep', v: 68, display: '$345K' }, { m: 'Oct', v: 65, display: '$330K' }, + { m: 'Nov', v: 60, display: '$304K' }, { m: 'Dec', v: 58, display: '$294K' }, + { m: 'Jan', v: 62, display: '$314K' }, { m: 'Feb', v: 57, display: '$289K' }, + { m: 'Mar', v: 55, display: '$279K' }, { m: 'Apr', v: 52, display: '$264K' }, + ], + }, + { + id: 'claims-by-status', domain: 'claims', label: 'Claims by Status', unit: '#', + change: '', changeTone: 'neutral', defaultChartType: 'bar', + data12m: [ + { m: 'Open', v: 3, display: '3' }, { m: 'Review', v: 3, display: '3' }, + { m: 'Docs', v: 1, display: '1' }, { m: 'Approved', v: 2, display: '2' }, + { m: 'Denied', v: 0, display: '0' }, { m: 'Closed', v: 1, display: '1' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + ], + }, + { + id: 'severity-trend', domain: 'claims', label: 'Avg Severity', unit: '$', + change: '-$2.1K', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 65, display: '$18.5K' }, { m: 'Jun', v: 60, display: '$17.1K' }, + { m: 'Jul', v: 72, display: '$20.5K' }, { m: 'Aug', v: 63, display: '$17.9K' }, + { m: 'Sep', v: 58, display: '$16.5K' }, { m: 'Oct', v: 55, display: '$15.7K' }, + { m: 'Nov', v: 52, display: '$14.8K' }, { m: 'Dec', v: 50, display: '$14.2K' }, + { m: 'Jan', v: 54, display: '$15.4K' }, { m: 'Feb', v: 48, display: '$13.7K' }, + { m: 'Mar', v: 46, display: '$13.1K' }, { m: 'Apr', v: 44, display: '$12.5K' }, + ], + }, +] + +// ─── Metrics: Sales Pipeline ───────────────────────────────────────────────── + +const pipelineMetrics: AnalyticsMetric[] = [ + { + id: 'conversion-rate', domain: 'pipeline', label: 'Conversion Rate', unit: '%', + change: '+2.8%', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 28, display: '28%' }, { m: 'Jun', v: 30, display: '30%' }, + { m: 'Jul', v: 27, display: '27%' }, { m: 'Aug', v: 32, display: '32%' }, + { m: 'Sep', v: 31, display: '31%' }, { m: 'Oct', v: 33, display: '33%' }, + { m: 'Nov', v: 30, display: '30%' }, { m: 'Dec', v: 35, display: '35%' }, + { m: 'Jan', v: 34, display: '34%' }, { m: 'Feb', v: 36, display: '36%' }, + { m: 'Mar', v: 38, display: '38%' }, { m: 'Apr', v: 39, display: '39%' }, + ], + }, + { + id: 'pipeline-value', domain: 'pipeline', label: 'Pipeline Value', unit: '$', + change: '+$820K', changeTone: 'positive', defaultChartType: 'area', + data12m: [ + { m: 'May', v: 42, display: '$3.8M' }, { m: 'Jun', v: 45, display: '$4.1M' }, + { m: 'Jul', v: 40, display: '$3.6M' }, { m: 'Aug', v: 48, display: '$4.4M' }, + { m: 'Sep', v: 52, display: '$4.7M' }, { m: 'Oct', v: 55, display: '$5.0M' }, + { m: 'Nov', v: 50, display: '$4.5M' }, { m: 'Dec', v: 58, display: '$5.3M' }, + { m: 'Jan', v: 56, display: '$5.1M' }, { m: 'Feb', v: 60, display: '$5.5M' }, + { m: 'Mar', v: 65, display: '$5.9M' }, { m: 'Apr', v: 68, display: '$6.2M' }, + ], + }, + { + id: 'lead-sources', domain: 'pipeline', label: 'Lead Sources', unit: '#', + change: '+18%', changeTone: 'positive', defaultChartType: 'bar', + data12m: [ + { m: 'Referral', v: 85, display: '85' }, { m: 'Walk-in', v: 42, display: '42' }, + { m: 'WhatsApp', v: 38, display: '38' }, { m: 'Google', v: 28, display: '28' }, + { m: 'Instagram', v: 22, display: '22' }, { m: 'Facebook', v: 15, display: '15' }, + { m: 'Partner', v: 12, display: '12' }, { m: 'Event', v: 8, display: '8' }, + { m: 'Other', v: 6, display: '6' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + ], + }, + { + id: 'quote-to-bind-days', domain: 'pipeline', label: 'Quote-to-Bind', unit: 'days', + change: '-2d', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 18, display: '18d' }, { m: 'Jun', v: 16, display: '16d' }, + { m: 'Jul', v: 20, display: '20d' }, { m: 'Aug', v: 15, display: '15d' }, + { m: 'Sep', v: 14, display: '14d' }, { m: 'Oct', v: 13, display: '13d' }, + { m: 'Nov', v: 15, display: '15d' }, { m: 'Dec', v: 12, display: '12d' }, + { m: 'Jan', v: 14, display: '14d' }, { m: 'Feb', v: 11, display: '11d' }, + { m: 'Mar', v: 10, display: '10d' }, { m: 'Apr', v: 9, display: '9d' }, + ], + }, + { + id: 'agent-performance', domain: 'pipeline', label: 'Agent Performance', unit: '#', + change: '', changeTone: 'neutral', defaultChartType: 'bar', + data12m: [ + { m: 'Ana R.', v: 88, display: '44 pólizas' }, { m: 'Marco V.', v: 72, display: '36 pólizas' }, + { m: 'Carlos V.', v: 64, display: '32 pólizas' }, { m: 'María F.', v: 56, display: '28 pólizas' }, + { m: 'Luis G.', v: 40, display: '20 pólizas' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + { m: '', v: 0, display: '' }, { m: '', v: 0, display: '' }, + ], + }, + { + id: 'win-rate', domain: 'pipeline', label: 'Win Rate', unit: '%', + change: '+3.5%', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 35, display: '35%' }, { m: 'Jun', v: 38, display: '38%' }, + { m: 'Jul', v: 33, display: '33%' }, { m: 'Aug', v: 40, display: '40%' }, + { m: 'Sep', v: 42, display: '42%' }, { m: 'Oct', v: 41, display: '41%' }, + { m: 'Nov', v: 39, display: '39%' }, { m: 'Dec', v: 44, display: '44%' }, + { m: 'Jan', v: 43, display: '43%' }, { m: 'Feb', v: 46, display: '46%' }, + { m: 'Mar', v: 48, display: '48%' }, { m: 'Apr', v: 50, display: '50%' }, + ], + }, +] + +// ─── Metrics: Customer & Service ───────────────────────────────────────────── + +const serviceMetrics: AnalyticsMetric[] = [ + { + id: 'support-volume', domain: 'service', label: 'Tickets Opened', unit: '#', + change: '+15%', changeTone: 'negative', defaultChartType: 'bar', + data12m: [ + { m: 'May', v: 22, display: '22' }, { m: 'Jun', v: 25, display: '25' }, + { m: 'Jul', v: 28, display: '28' }, { m: 'Aug', v: 24, display: '24' }, + { m: 'Sep', v: 30, display: '30' }, { m: 'Oct', v: 27, display: '27' }, + { m: 'Nov', v: 32, display: '32' }, { m: 'Dec', v: 20, display: '20' }, + { m: 'Jan', v: 26, display: '26' }, { m: 'Feb', v: 29, display: '29' }, + { m: 'Mar', v: 34, display: '34' }, { m: 'Apr', v: 36, display: '36' }, + ], + }, + { + id: 'sla-compliance', domain: 'service', label: 'SLA Compliance', unit: '%', + change: '+4%', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 78, display: '78%' }, { m: 'Jun', v: 80, display: '80%' }, + { m: 'Jul', v: 76, display: '76%' }, { m: 'Aug', v: 82, display: '82%' }, + { m: 'Sep', v: 84, display: '84%' }, { m: 'Oct', v: 83, display: '83%' }, + { m: 'Nov', v: 85, display: '85%' }, { m: 'Dec', v: 88, display: '88%' }, + { m: 'Jan', v: 86, display: '86%' }, { m: 'Feb', v: 89, display: '89%' }, + { m: 'Mar', v: 90, display: '90%' }, { m: 'Apr', v: 92, display: '92%' }, + ], + }, + { + id: 'avg-response-time', domain: 'service', label: 'Avg Response Time', unit: 'hrs', + change: '-1.2h', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 72, display: '5.8h' }, { m: 'Jun', v: 68, display: '5.4h' }, + { m: 'Jul', v: 75, display: '6.0h' }, { m: 'Aug', v: 65, display: '5.2h' }, + { m: 'Sep', v: 60, display: '4.8h' }, { m: 'Oct', v: 55, display: '4.4h' }, + { m: 'Nov', v: 52, display: '4.2h' }, { m: 'Dec', v: 48, display: '3.8h' }, + { m: 'Jan', v: 50, display: '4.0h' }, { m: 'Feb', v: 45, display: '3.6h' }, + { m: 'Mar', v: 42, display: '3.4h' }, { m: 'Apr', v: 40, display: '3.2h' }, + ], + }, + { + id: 'retention-rate', domain: 'service', label: 'Retention Rate', unit: '%', + change: '+1.2%', changeTone: 'positive', defaultChartType: 'area', + data12m: [ + { m: 'May', v: 84, display: '84%' }, { m: 'Jun', v: 85, display: '85%' }, + { m: 'Jul', v: 86, display: '86%' }, { m: 'Aug', v: 87, display: '87%' }, + { m: 'Sep', v: 88, display: '88%' }, { m: 'Oct', v: 88, display: '88%' }, + { m: 'Nov', v: 87, display: '87%' }, { m: 'Dec', v: 89, display: '89%' }, + { m: 'Jan', v: 90, display: '90%' }, { m: 'Feb', v: 90, display: '90%' }, + { m: 'Mar', v: 91, display: '91%' }, { m: 'Apr', v: 91, display: '91%' }, + ], + }, + { + id: 'nps-score', domain: 'service', label: 'NPS Score', unit: '#', + change: '+5', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 42, display: '42' }, { m: 'Jun', v: 44, display: '44' }, + { m: 'Jul', v: 40, display: '40' }, { m: 'Aug', v: 46, display: '46' }, + { m: 'Sep', v: 48, display: '48' }, { m: 'Oct', v: 50, display: '50' }, + { m: 'Nov', v: 48, display: '48' }, { m: 'Dec', v: 52, display: '52' }, + { m: 'Jan', v: 54, display: '54' }, { m: 'Feb', v: 55, display: '55' }, + { m: 'Mar', v: 58, display: '58' }, { m: 'Apr', v: 60, display: '60' }, + ], + }, + { + id: 'ticket-resolution-days', domain: 'service', label: 'Ticket Resolution', unit: 'days', + change: '-0.8d', changeTone: 'positive', defaultChartType: 'line', + data12m: [ + { m: 'May', v: 55, display: '4.4d' }, { m: 'Jun', v: 52, display: '4.2d' }, + { m: 'Jul', v: 58, display: '4.6d' }, { m: 'Aug', v: 50, display: '4.0d' }, + { m: 'Sep', v: 48, display: '3.8d' }, { m: 'Oct', v: 45, display: '3.6d' }, + { m: 'Nov', v: 42, display: '3.4d' }, { m: 'Dec', v: 40, display: '3.2d' }, + { m: 'Jan', v: 43, display: '3.4d' }, { m: 'Feb', v: 38, display: '3.0d' }, + { m: 'Mar', v: 36, display: '2.9d' }, { m: 'Apr', v: 34, display: '2.7d' }, + ], + }, +] + +// ─── Combined Export ───────────────────────────────────────────────────────── + +export const ANALYTICS_METRICS: AnalyticsMetric[] = [ + ...productionMetrics, + ...claimsMetrics, + ...pipelineMetrics, + ...serviceMetrics, +] diff --git a/app/data/mock-claims.ts b/app/data/mock-claims.ts new file mode 100644 index 0000000..6900801 --- /dev/null +++ b/app/data/mock-claims.ts @@ -0,0 +1,550 @@ +// ── Claims Management — Types & Mock Data ─────────────────────────────────── + +export type CarrierStatus = + | 'fnol_submitted' + | 'acknowledged' + | 'investigation' + | 'documentation_pending' + | 'reserved' + | 'negotiation' + | 'settlement_offered' + | 'closed' + +export type BrokerWorkflowStatus = + | 'waiting_carrier' + | 'waiting_insured_docs' + | 'needs_escalation' + | 'client_update_overdue' + | 'ready_to_close' + +export type ClaimPriority = 'critical' | 'high' | 'medium' | 'low' + +export type PartyRole = 'insured' | 'adjuster' | 'carrier_contact' | 'handler' | 'attorney' + +export interface ClaimParty { + id: string + role: PartyRole + name: string + initials: string + email?: string + phone?: string + company?: string + unreadComms: number +} + +export type TaskType = 'document' | 'communication' | 'escalation' | 'general' +export type TaskStatus = 'open' | 'in_progress' | 'overdue' | 'done' + +export interface ClaimTask { + id: string + title: string + status: TaskStatus + assignee: string + dueDate: string + slaPercent: number + type: TaskType + isSystemSuggested?: boolean + dismissedUntil?: string | null +} + +export type CommType = 'email' | 'call' | 'note' | 'system' + +export interface ClaimCommEntry { + id: string + type: CommType + partyId: string + from: string + to?: string + subject?: string + body: string + timestamp: string + threadId?: string + aiDigest?: string +} + +export type DocCategory = 'fnol' | 'evidence' | 'estimates' | 'correspondence' | 'settlement' + +export interface ClaimDocument { + id: string + name: string + category: DocCategory + uploadedBy: string + uploadedAt: string + size: string + required: boolean + received: boolean +} + +export type IntakeStatus = 'not_sent' | 'sent' | 'in_progress' | 'completed' +export type GeneratedFormStatus = 'draft' | 'ready_for_signature' | 'signed' | 'submitted' + +export interface GeneratedForm { + id: string + carrierFormName: string + carrier: string + lob: string + status: GeneratedFormStatus + generatedAt: string + signedAt: string | null +} + +export type FinancialType = 'reserve_change' | 'payment' | 'subrogation' | 'expense' + +export interface ClaimFinancialEntry { + id: string + type: FinancialType + date: string + amount: number + description: string + annotation?: string +} + +export interface ClaimDetail { + id: string + customerId: string + customerName: string + policyId: string + policyNumber: string + carrier: string + lob: string + type: string + carrierStatus: CarrierStatus + workflowStatus: BrokerWorkflowStatus + priority: ClaimPriority + dateFiled: string + daysOpen: number + handler: string + reservedAmount: number + paidAmount: number + parties: ClaimParty[] + tasks: ClaimTask[] + communications: ClaimCommEntry[] + documents: ClaimDocument[] + financials: ClaimFinancialEntry[] + aiRecap: string + aiRecapSourceCount: number + keyDates: { label: string; date: string; done: boolean }[] + reserveHistory: { date: string; amount: number; annotation: string }[] + intakeToken: string | null + intakeStatus: IntakeStatus + intakeSentAt: string | null + intakeCompletedAt: string | null + generatedForms: GeneratedForm[] +} + +// ── Label Maps ────────────────────────────────────────────────────────────── + +export const CARRIER_STATUS_LABELS: Record = { + fnol_submitted: 'FNOL Submitted', + acknowledged: 'Acknowledged', + investigation: 'Investigation', + documentation_pending: 'Documentation Pending', + reserved: 'Reserved', + negotiation: 'Negotiation', + settlement_offered: 'Settlement Offered', + closed: 'Closed', +} + +export const WORKFLOW_STATUS_LABELS: Record = { + waiting_carrier: 'Waiting on Carrier', + waiting_insured_docs: 'Waiting on Insured Docs', + needs_escalation: 'Needs Escalation', + client_update_overdue: 'Client Update Overdue', + ready_to_close: 'Ready to Close', +} + +export const PRIORITY_LABELS: Record = { + critical: 'Critical', + high: 'High', + medium: 'Medium', + low: 'Low', +} + +export const TASK_STATUS_LABELS: Record = { + open: 'Open', + in_progress: 'In Progress', + overdue: 'Overdue', + done: 'Done', +} + +export const DOC_CATEGORY_LABELS: Record = { + fnol: 'FNOL & Notice', + evidence: 'Evidence & Photos', + estimates: 'Estimates & Appraisals', + correspondence: 'Correspondence', + settlement: 'Settlement', +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +export function slaColor(percent: number): 'green' | 'amber' | 'red' { + if (percent >= 100) return 'red' + if (percent >= 75) return 'amber' + return 'green' +} + +export function fmtClaimMoney(n: number): string { + if (n >= 1_000_000) return '$' + (n / 1_000_000).toFixed(1) + 'M' + if (n >= 1_000) return '$' + (n / 1_000).toFixed(1) + 'K' + return '$' + n.toLocaleString() +} + +// ── Mock Data ─────────────────────────────────────────────────────────────── + +const clm0048: ClaimDetail = { + id: 'CLM-0048', + customerId: 'corp-hotel-pacifico', + customerName: 'Hotel Pacífico S.A.', + policyId: 'POL-2024-HP-001', + policyNumber: 'PROP-2024-HP-001', + carrier: 'ASSA', + lob: 'General Risk', + type: 'Fire damage — kitchen wing', + carrierStatus: 'investigation', + workflowStatus: 'waiting_carrier', + priority: 'critical', + dateFiled: '2026-04-05', + daysOpen: 3, + handler: 'Ana R.', + reservedAmount: 128_000, + paidAmount: 0, + + parties: [ + { id: 'p1', role: 'insured', name: 'Carlos Montero', initials: 'CM', email: 'cmontero@hotelpacifico.cr', phone: '+506 2643-1200', company: 'Hotel Pacífico S.A.', unreadComms: 2 }, + { id: 'p2', role: 'adjuster', name: 'Roberto Méndez', initials: 'RM', email: 'rmendez@peritajes.cr', phone: '+506 8844-2200', company: 'Peritajes CR', unreadComms: 0 }, + { id: 'p3', role: 'carrier_contact', name: 'Lucía Vargas', initials: 'LV', email: 'lvargas@assa.cr', phone: '+506 2222-5000', company: 'ASSA', unreadComms: 1 }, + { id: 'p4', role: 'handler', name: 'Ana Ramírez', initials: 'AR', email: 'ana.r@seguros.cr', phone: '+506 8855-3300', unreadComms: 0 }, + ], + + tasks: [ + { id: 't1', title: 'Upload police/fire report', status: 'overdue', assignee: 'Ana R.', dueDate: '2026-04-07', slaPercent: 110, type: 'document' }, + { id: 't2', title: 'Follow up with adjuster — no site visit scheduled', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-09', slaPercent: 60, type: 'communication' }, + { id: 't3', title: 'Send initial status update to insured', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-08', slaPercent: 85, type: 'communication' }, + { id: 't4', title: 'Carrier non-response 3 days — escalate?', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-08', slaPercent: 90, type: 'escalation', isSystemSuggested: true }, + { id: 't5', title: 'Request preliminary damage estimate from adjuster', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-12', slaPercent: 30, type: 'general' }, + { id: 't6', title: 'Confirm policy coverage for fire peril', status: 'done', assignee: 'Ana R.', dueDate: '2026-04-06', slaPercent: 100, type: 'general' }, + ], + + communications: [ + { id: 'c1', type: 'system', partyId: 'p4', from: 'System', body: 'Claim CLM-0048 created. FNOL submitted to ASSA.', timestamp: '2026-04-05T09:15:00' }, + { id: 'c2', type: 'email', partyId: 'p1', from: 'Carlos Montero', to: 'Ana Ramírez', subject: 'Fire Incident — Hotel Pacífico Kitchen Wing', body: 'Ana, the fire started around 2am in the kitchen exhaust system. The fire department responded within 20 minutes. The kitchen wing sustained significant structural damage, and the adjacent dining area has smoke and water damage. We have temporarily closed the restaurant. Attached are initial photos from the scene. We need to process this claim urgently as we are losing revenue daily.', timestamp: '2026-04-05T10:30:00', threadId: 'th1' }, + { id: 'c3', type: 'email', partyId: 'p4', from: 'Ana Ramírez', to: 'Lucía Vargas', subject: 'FNOL — Hotel Pacífico Fire Claim CLM-0048', body: 'Lucía, please find attached the FNOL for Hotel Pacífico. Fire damage to kitchen wing on April 5. Policy PROP-2024-HP-001 covers fire peril with $500K limit. Requesting immediate adjuster assignment. This is a high-value commercial client with business interruption exposure.', timestamp: '2026-04-05T11:45:00', threadId: 'th2' }, + { id: 'c4', type: 'call', partyId: 'p1', from: 'Ana Ramírez', body: 'Called Carlos Montero to confirm FNOL was submitted. Discussed initial documentation needed: fire report, photos, inventory of damaged equipment. Carlos will send equipment list by end of day. Advised to keep all receipts for temporary repairs.', timestamp: '2026-04-05T14:00:00' }, + { id: 'c5', type: 'email', partyId: 'p3', from: 'Lucía Vargas', to: 'Ana Ramírez', subject: 'RE: FNOL — Hotel Pacífico Fire Claim CLM-0048', body: 'Ana, claim received and logged under ASSA reference FI-2026-04412. We are assigning adjuster Roberto Méndez from Peritajes CR. He will contact you to schedule site inspection.', timestamp: '2026-04-06T09:00:00', threadId: 'th2', aiDigest: 'ASSA acknowledged claim as FI-2026-04412. Adjuster Roberto Méndez (Peritajes CR) assigned. Site inspection pending scheduling.' }, + { id: 'c6', type: 'system', partyId: 'p4', from: 'System', body: 'Carrier status updated: FNOL Submitted → Acknowledged', timestamp: '2026-04-06T09:05:00' }, + { id: 'c7', type: 'note', partyId: 'p4', from: 'Ana Ramírez', body: 'Reviewed policy. Fire peril covered. Business interruption sublimit $200K with 48h waiting period. Need to flag BI exposure to carrier early — hotel restaurant closure = significant daily revenue loss.', timestamp: '2026-04-06T10:30:00' }, + { id: 'c8', type: 'email', partyId: 'p2', from: 'Roberto Méndez', to: 'Ana Ramírez', subject: 'Site Inspection — Hotel Pacífico', body: 'Good morning Ana. I have been assigned to inspect the fire damage at Hotel Pacífico. Could you coordinate with the insured for access? I am available Thursday or Friday this week.', timestamp: '2026-04-07T08:00:00', threadId: 'th3' }, + { id: 'c9', type: 'email', partyId: 'p4', from: 'Ana Ramírez', to: 'Roberto Méndez', subject: 'RE: Site Inspection — Hotel Pacífico', body: 'Roberto, Thursday works. I will confirm with the hotel and send you the contact details. Please plan for approximately 3 hours — the damage area covers the kitchen wing and adjacent dining area.', timestamp: '2026-04-07T09:15:00', threadId: 'th3' }, + { id: 'c10', type: 'system', partyId: 'p4', from: 'System', body: 'Carrier status updated: Acknowledged → Investigation', timestamp: '2026-04-07T10:00:00' }, + { id: 'c11', type: 'email', partyId: 'p1', from: 'Carlos Montero', to: 'Ana Ramírez', subject: 'RE: Fire Incident — Equipment Inventory', body: 'Ana, attached is the damaged equipment inventory as requested. Total estimated replacement value approximately $85,000. Also including photos of the structural damage to the exhaust hood and ceiling. The fire inspector\'s preliminary report should be ready by Friday.', timestamp: '2026-04-07T16:00:00', threadId: 'th1', aiDigest: 'Insured provided equipment inventory ($85K estimated). Structural damage photos attached. Fire inspector report expected Friday.' }, + { id: 'c12', type: 'note', partyId: 'p4', from: 'Ana Ramírez', body: 'Equipment inventory received — $85K. Combined with structural estimates, total exposure could exceed $150K. May need to flag for reserve increase once adjuster report comes in. BI claim will be separate — need to start documenting daily revenue loss.', timestamp: '2026-04-08T08:30:00' }, + ], + + documents: [ + { id: 'd1', name: 'FNOL-CLM0048.pdf', category: 'fnol', uploadedBy: 'Ana R.', uploadedAt: '2026-04-05', size: '245 KB', required: true, received: true }, + { id: 'd2', name: 'Policy-Declarations-HP.pdf', category: 'fnol', uploadedBy: 'Ana R.', uploadedAt: '2026-04-05', size: '1.2 MB', required: true, received: true }, + { id: 'd3', name: 'Initial-Photos-Kitchen.zip', category: 'evidence', uploadedBy: 'Carlos Montero', uploadedAt: '2026-04-05', size: '18.4 MB', required: true, received: true }, + { id: 'd4', name: 'Equipment-Inventory.xlsx', category: 'estimates', uploadedBy: 'Carlos Montero', uploadedAt: '2026-04-07', size: '89 KB', required: true, received: true }, + { id: 'd5', name: 'Structural-Damage-Photos.zip', category: 'evidence', uploadedBy: 'Carlos Montero', uploadedAt: '2026-04-07', size: '24.1 MB', required: false, received: true }, + { id: 'd6', name: 'ASSA-Acknowledgment-FI202604412.pdf', category: 'correspondence', uploadedBy: 'Ana R.', uploadedAt: '2026-04-06', size: '156 KB', required: false, received: true }, + { id: 'd7', name: 'Fire-Inspector-Report.pdf', category: 'evidence', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd8', name: 'Adjuster-Preliminary-Estimate.pdf', category: 'estimates', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd9', name: 'Business-Interruption-Docs.pdf', category: 'estimates', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd10', name: 'Police-Fire-Report.pdf', category: 'fnol', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + ], + + financials: [ + { id: 'f1', type: 'reserve_change', date: '2026-04-05', amount: 128_000, description: 'Initial reserve set', annotation: 'Based on preliminary damage assessment and policy limits' }, + { id: 'f2', type: 'expense', date: '2026-04-06', amount: 1_500, description: 'Adjuster assignment fee — Peritajes CR', annotation: 'Standard commercial property rate' }, + { id: 'f3', type: 'expense', date: '2026-04-07', amount: 450, description: 'Emergency structural assessment', annotation: 'Required for safety clearance' }, + ], + + aiRecap: 'Siniestro por incendio en el ala de cocina del Hotel Pacífico, reportado el 5 de abril de 2026. El fuego se originó en el sistema de extracción de la cocina alrededor de las 2am. Los bomberos respondieron en 20 minutos. Daños significativos a la estructura de la cocina y daños por humo y agua en el comedor adyacente. El restaurante está cerrado temporalmente.\n\nASSA acusó recibo el 6 de abril (ref. FI-2026-04412) y asignó al ajustador Roberto Méndez de Peritajes CR. La inspección del sitio está pendiente de programar para esta semana.\n\nEl asegurado proporcionó un inventario de equipos dañados valorado en ~$85K. La exposición total podría superar $150K una vez que se complete la evaluación estructural. Hay exposición adicional por interrupción de negocio (sublímite de $200K con período de espera de 48h).\n\nPendiente: reporte del inspector de bomberos (esperado viernes), estimación preliminar del ajustador, documentación de pérdida de ingresos diarios para reclamo de BI.', + aiRecapSourceCount: 14, + + keyDates: [ + { label: 'FNOL Filed', date: '2026-04-05', done: true }, + { label: 'Carrier Acknowledged', date: '2026-04-06', done: true }, + { label: 'Adjuster Assigned', date: '2026-04-06', done: true }, + { label: 'Investigation Started', date: '2026-04-07', done: true }, + { label: 'Site Inspection', date: '2026-04-10', done: false }, + { label: 'Preliminary Estimate', date: '', done: false }, + { label: 'Reserve Review', date: '', done: false }, + { label: 'Settlement', date: '', done: false }, + ], + + reserveHistory: [ + { date: '2026-04-05', amount: 128_000, annotation: 'Initial reserve — fire damage assessment pending' }, + ], + intakeToken: 'tk_hp_048_a3f1', + intakeStatus: 'completed', + intakeSentAt: '2026-04-05T14:30:00Z', + intakeCompletedAt: '2026-04-06T09:12:00Z', + generatedForms: [ + { id: 'gf-048-1', carrierFormName: 'Aviso de Pérdida — ASSA', carrier: 'ASSA', lob: 'General Risk', status: 'ready_for_signature', generatedAt: '2026-04-06T10:00:00Z', signedAt: null }, + ], +} + +const clm0047: ClaimDetail = { + id: 'CLM-0047', + customerId: 'corp-empresa-abc', + customerName: 'Empresa ABC S.A.', + policyId: 'POL-2024-ABC-FLEET', + policyNumber: 'AUTO-2024-FLEET-007', + carrier: 'Qualitas', + lob: 'Auto', + type: 'Auto collision — fleet vehicle', + carrierStatus: 'documentation_pending', + workflowStatus: 'waiting_insured_docs', + priority: 'high', + dateFiled: '2026-04-03', + daysOpen: 5, + handler: 'Marco V.', + reservedAmount: 14_200, + paidAmount: 0, + + parties: [ + { id: 'p1', role: 'insured', name: 'Fernando Solano', initials: 'FS', email: 'fsolano@empresaabc.cr', phone: '+506 2255-8800', company: 'Empresa ABC S.A.', unreadComms: 0 }, + { id: 'p2', role: 'adjuster', name: 'Patricia Ulate', initials: 'PU', email: 'pulate@qualitas.cr', phone: '+506 2233-4400', company: 'Qualitas', unreadComms: 1 }, + { id: 'p3', role: 'carrier_contact', name: 'Diego Mora', initials: 'DM', email: 'dmora@qualitas.cr', phone: '+506 2233-4401', company: 'Qualitas', unreadComms: 0 }, + { id: 'p4', role: 'handler', name: 'Marco Vargas', initials: 'MV', email: 'marco.v@seguros.cr', phone: '+506 8866-4400', unreadComms: 0 }, + ], + + tasks: [ + { id: 't1', title: 'Obtain police report from insured', status: 'overdue', assignee: 'Marco V.', dueDate: '2026-04-06', slaPercent: 120, type: 'document' }, + { id: 't2', title: 'Submit repair estimates to carrier', status: 'open', assignee: 'Marco V.', dueDate: '2026-04-10', slaPercent: 50, type: 'document' }, + { id: 't3', title: 'Confirm driver was authorized fleet operator', status: 'open', assignee: 'Marco V.', dueDate: '2026-04-09', slaPercent: 65, type: 'general' }, + { id: 't4', title: 'Send client status update — 5 days no communication', status: 'open', assignee: 'Marco V.', dueDate: '2026-04-08', slaPercent: 92, type: 'communication', isSystemSuggested: true }, + { id: 't5', title: 'Request adjuster photos from body shop', status: 'done', assignee: 'Marco V.', dueDate: '2026-04-05', slaPercent: 100, type: 'general' }, + ], + + communications: [ + { id: 'c1', type: 'system', partyId: 'p4', from: 'System', body: 'Claim CLM-0047 created. FNOL submitted to Qualitas.', timestamp: '2026-04-03T08:30:00' }, + { id: 'c2', type: 'email', partyId: 'p1', from: 'Fernando Solano', to: 'Marco Vargas', subject: 'Fleet Vehicle Accident — Unit 07', body: 'Marco, one of our delivery trucks (Unit 07, plates SJO-7744) was involved in a collision yesterday on Ruta 27 near Escazú. The driver (José Mora) is fine but the front end is heavily damaged. The vehicle was towed to Taller Central in La Uruca. Police were called and a report was filed.', timestamp: '2026-04-03T09:00:00', threadId: 'th1' }, + { id: 'c3', type: 'email', partyId: 'p4', from: 'Marco Vargas', to: 'Diego Mora', subject: 'FNOL — Empresa ABC Fleet Collision CLM-0047', body: 'Diego, submitting FNOL for fleet collision. Policy AUTO-2024-FLEET-007, Unit 07 (SJO-7744). Collision on Ruta 27, 2 April. Vehicle at Taller Central, La Uruca. Police report filed. Requesting adjuster assignment.', timestamp: '2026-04-03T10:15:00', threadId: 'th2' }, + { id: 'c4', type: 'email', partyId: 'p3', from: 'Diego Mora', to: 'Marco Vargas', subject: 'RE: FNOL — Empresa ABC Fleet Collision', body: 'Marco, claim received under Qualitas ref QAC-2026-1182. Adjuster Patricia Ulate will handle. She will coordinate directly with the body shop for inspection. Please provide the police report and driver authorization docs at your earliest convenience.', timestamp: '2026-04-04T08:00:00', threadId: 'th2', aiDigest: 'Qualitas acknowledged as QAC-2026-1182. Adjuster Patricia Ulate assigned. Requesting police report and driver authorization docs.' }, + { id: 'c5', type: 'call', partyId: 'p2', from: 'Marco Vargas', body: 'Called Patricia Ulate. She confirmed she will visit Taller Central on Monday for inspection. Needs police report before she can proceed with estimate. Estimated repair range $12K–$16K based on initial description.', timestamp: '2026-04-04T14:30:00' }, + { id: 'c6', type: 'system', partyId: 'p4', from: 'System', body: 'Carrier status updated: Acknowledged → Documentation Pending', timestamp: '2026-04-05T09:00:00' }, + { id: 'c7', type: 'note', partyId: 'p4', from: 'Marco Vargas', body: 'Insured has not yet sent police report. Called Fernando twice, went to voicemail. Will try again tomorrow. Fleet policy requires authorized driver confirmation — need to get signed driver roster from HR department.', timestamp: '2026-04-06T16:00:00' }, + { id: 'c8', type: 'email', partyId: 'p2', from: 'Patricia Ulate', to: 'Marco Vargas', subject: 'Body Shop Photos — Unit 07', body: 'Marco, I visited Taller Central this morning. Photos attached. Front bumper, hood, radiator, and right fender all need replacement. Frame appears straight — no structural damage. Preliminary estimate pending receipt of police report to confirm fault assignment.', timestamp: '2026-04-07T11:00:00', threadId: 'th3', aiDigest: 'Adjuster inspected vehicle. Damage: bumper, hood, radiator, right fender. No structural damage. Estimate pending police report for fault assignment.' }, + ], + + documents: [ + { id: 'd1', name: 'FNOL-CLM0047.pdf', category: 'fnol', uploadedBy: 'Marco V.', uploadedAt: '2026-04-03', size: '198 KB', required: true, received: true }, + { id: 'd2', name: 'Fleet-Policy-Declarations.pdf', category: 'fnol', uploadedBy: 'Marco V.', uploadedAt: '2026-04-03', size: '890 KB', required: true, received: true }, + { id: 'd3', name: 'Accident-Scene-Photos.zip', category: 'evidence', uploadedBy: 'Fernando Solano', uploadedAt: '2026-04-03', size: '12.6 MB', required: true, received: true }, + { id: 'd4', name: 'Body-Shop-Inspection-Photos.zip', category: 'evidence', uploadedBy: 'Patricia Ulate', uploadedAt: '2026-04-07', size: '8.9 MB', required: false, received: true }, + { id: 'd5', name: 'Police-Report.pdf', category: 'fnol', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd6', name: 'Driver-Authorization-Roster.pdf', category: 'fnol', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd7', name: 'Repair-Estimate-TallerCentral.pdf', category: 'estimates', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + ], + + financials: [ + { id: 'f1', type: 'reserve_change', date: '2026-04-03', amount: 14_200, description: 'Initial reserve set', annotation: 'Based on typical fleet collision range' }, + { id: 'f2', type: 'expense', date: '2026-04-03', amount: 85, description: 'Towing — Ruta 27 to Taller Central' }, + ], + + aiRecap: 'Colisión de vehículo de flota (Unidad 07, SJO-7744) de Empresa ABC en Ruta 27 cerca de Escazú el 2 de abril. El conductor José Mora no resultó herido. El vehículo fue remolcado a Taller Central en La Uruca.\n\nQualitas acusó recibo (ref QAC-2026-1182) y asignó a la ajustadora Patricia Ulate. Ella inspeccionó el vehículo el 7 de abril — daños en bumper, capó, radiador y guardafango derecho. Sin daño estructural. Estimación pendiente del reporte policial para determinar culpa.\n\nBloqueadores: el asegurado no ha proporcionado el reporte policial (vencido) ni la documentación de autorización del conductor. Se han hecho múltiples intentos de contacto sin respuesta.', + aiRecapSourceCount: 9, + + keyDates: [ + { label: 'FNOL Filed', date: '2026-04-03', done: true }, + { label: 'Carrier Acknowledged', date: '2026-04-04', done: true }, + { label: 'Adjuster Assigned', date: '2026-04-04', done: true }, + { label: 'Vehicle Inspection', date: '2026-04-07', done: true }, + { label: 'Police Report Due', date: '2026-04-06', done: false }, + { label: 'Repair Estimate', date: '', done: false }, + { label: 'Settlement', date: '', done: false }, + ], + + reserveHistory: [ + { date: '2026-04-03', amount: 14_200, annotation: 'Initial reserve — typical fleet collision' }, + ], + intakeToken: 'tk_abc_047_b7e2', + intakeStatus: 'in_progress', + intakeSentAt: '2026-04-04T11:00:00Z', + intakeCompletedAt: null, + generatedForms: [], +} + +const clm0043: ClaimDetail = { + id: 'CLM-0043', + customerId: 'corp-supermercado-tico', + customerName: 'Supermercado Tico S.A.', + policyId: 'POL-2023-ST-GL', + policyNumber: 'GL-2023-ST-001', + carrier: 'INS', + lob: 'General Risk', + type: 'Liability — customer injury in store', + carrierStatus: 'negotiation', + workflowStatus: 'client_update_overdue', + priority: 'high', + dateFiled: '2026-03-17', + daysOpen: 22, + handler: 'Ana R.', + reservedAmount: 45_000, + paidAmount: 0, + + parties: [ + { id: 'p1', role: 'insured', name: 'Jorge Calvo', initials: 'JC', email: 'jcalvo@supertico.cr', phone: '+506 2244-9900', company: 'Supermercado Tico S.A.', unreadComms: 3 }, + { id: 'p2', role: 'adjuster', name: 'Sandra Pérez', initials: 'SP', email: 'sperez@ins.go.cr', phone: '+506 2287-6600', company: 'INS', unreadComms: 0 }, + { id: 'p3', role: 'carrier_contact', name: 'Miguel Hernández', initials: 'MH', email: 'mhernandez@ins.go.cr', phone: '+506 2287-6601', company: 'INS', unreadComms: 0 }, + { id: 'p4', role: 'handler', name: 'Ana Ramírez', initials: 'AR', email: 'ana.r@seguros.cr', phone: '+506 8855-3300', unreadComms: 0 }, + { id: 'p5', role: 'attorney', name: 'Lic. Gabriela Rojas', initials: 'GR', email: 'grojas@bufeterojas.cr', phone: '+506 2255-1100', company: 'Bufete Rojas & Asociados', unreadComms: 1 }, + ], + + tasks: [ + { id: 't1', title: 'Send client status update — 8 days overdue', status: 'overdue', assignee: 'Ana R.', dueDate: '2026-03-31', slaPercent: 140, type: 'communication' }, + { id: 't2', title: 'Review settlement offer from INS — $38K proposed', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-10', slaPercent: 55, type: 'general' }, + { id: 't3', title: 'Coordinate with attorney on counter-offer strategy', status: 'in_progress', assignee: 'Ana R.', dueDate: '2026-04-09', slaPercent: 70, type: 'communication' }, + { id: 't4', title: 'Upload updated medical records from claimant', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-11', slaPercent: 40, type: 'document' }, + { id: 't5', title: 'Client update overdue — escalate?', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-08', slaPercent: 100, type: 'escalation', isSystemSuggested: true }, + { id: 't6', title: 'File FNOL with carrier', status: 'done', assignee: 'Ana R.', dueDate: '2026-03-17', slaPercent: 100, type: 'general' }, + { id: 't7', title: 'Obtain incident report from store manager', status: 'done', assignee: 'Ana R.', dueDate: '2026-03-19', slaPercent: 100, type: 'document' }, + { id: 't8', title: 'Upload CCTV footage', status: 'done', assignee: 'Ana R.', dueDate: '2026-03-22', slaPercent: 100, type: 'document' }, + ], + + communications: [ + { id: 'c1', type: 'system', partyId: 'p4', from: 'System', body: 'Claim CLM-0043 created. FNOL submitted to INS.', timestamp: '2026-03-17T10:00:00' }, + { id: 'c2', type: 'email', partyId: 'p1', from: 'Jorge Calvo', to: 'Ana Ramírez', subject: 'Customer Slip and Fall — Produce Section', body: 'Ana, we had an incident on March 16. A customer (Marta Solís) slipped on a wet floor in the produce section and sustained a hip injury. She was taken to Hospital CIMA by ambulance. She has retained an attorney. We have CCTV footage and the incident report from our store manager.', timestamp: '2026-03-17T10:30:00', threadId: 'th1' }, + { id: 'c3', type: 'email', partyId: 'p4', from: 'Ana Ramírez', to: 'Miguel Hernández', subject: 'FNOL — Supermercado Tico Liability Claim CLM-0043', body: 'Miguel, submitting FNOL for a general liability claim. Customer slip and fall injury in the produce section. Incident March 16. Claimant retained attorney. We have CCTV footage and incident report. Policy GL-2023-ST-001 with $1M per-occurrence limit.', timestamp: '2026-03-17T11:30:00', threadId: 'th2' }, + { id: 'c4', type: 'email', partyId: 'p3', from: 'Miguel Hernández', to: 'Ana Ramírez', subject: 'RE: FNOL — Supermercado Tico Liability', body: 'Ana, claim acknowledged under INS ref LB-2026-0388. Sandra Pérez assigned as adjuster. Please forward CCTV footage and incident report. Given attorney involvement, we are fast-tracking investigation.', timestamp: '2026-03-18T09:00:00', threadId: 'th2', aiDigest: 'INS acknowledged claim as LB-2026-0388. Adjuster Sandra Pérez assigned. Fast-tracking due to attorney involvement. Requesting CCTV and incident report.' }, + { id: 'c5', type: 'call', partyId: 'p5', from: 'Lic. Gabriela Rojas', body: 'Incoming call from claimant\'s attorney. Informed that Marta Solís underwent hip surgery. Medical expenses to date approximately $28K. Attorney indicated client is seeking $50K total including pain and suffering. Requested we expedite the claim process.', timestamp: '2026-03-20T14:00:00' }, + { id: 'c6', type: 'note', partyId: 'p4', from: 'Ana Ramírez', body: 'Attorney demanding $50K. Medical expenses $28K. Need to review CCTV carefully — if the wet floor sign was properly placed, liability may be disputed. Sent CCTV to INS adjuster for review.', timestamp: '2026-03-20T15:30:00' }, + { id: 'c7', type: 'system', partyId: 'p4', from: 'System', body: 'Carrier status updated: Investigation → Reserved. Reserve set at $45,000.', timestamp: '2026-03-25T09:00:00' }, + { id: 'c8', type: 'email', partyId: 'p2', from: 'Sandra Pérez', to: 'Ana Ramírez', subject: 'Investigation Update — CLM-0043', body: 'Ana, we reviewed the CCTV footage. The wet floor sign was visible but positioned slightly away from the actual wet area. This creates partial liability exposure. We are setting reserve at $45K. Our recommendation is to negotiate settlement in the $35K–$40K range to avoid litigation costs.', timestamp: '2026-03-25T10:00:00', threadId: 'th4', aiDigest: 'INS reviewed CCTV. Wet floor sign was present but mispositioned — partial liability. Reserve set $45K. Recommends settling $35K–$40K to avoid litigation costs.' }, + { id: 'c9', type: 'email', partyId: 'p4', from: 'Ana Ramírez', to: 'Jorge Calvo', subject: 'Claim Update — Liability Assessment', body: 'Jorge, INS has completed their initial investigation. The CCTV review indicates the wet floor sign, while present, was not optimally positioned. This creates some liability exposure. INS is recommending a negotiated settlement. I will discuss strategy with you once we have a formal offer from the carrier. Please call me at your convenience to discuss.', timestamp: '2026-03-25T14:00:00', threadId: 'th5' }, + { id: 'c10', type: 'email', partyId: 'p3', from: 'Miguel Hernández', to: 'Ana Ramírez', subject: 'Settlement Offer — CLM-0043', body: 'Ana, INS is prepared to offer $38,000 to settle this claim. This includes medical expenses ($28K) plus $10K for pain and suffering. Please communicate this to the insured and the claimant\'s attorney. We believe this is a fair offer given the shared liability circumstances.', timestamp: '2026-04-01T09:00:00', threadId: 'th6', aiDigest: 'INS offering $38K settlement ($28K medical + $10K pain/suffering). Considers shared liability. Awaiting broker/attorney response.' }, + { id: 'c11', type: 'system', partyId: 'p4', from: 'System', body: 'Carrier status updated: Reserved → Negotiation', timestamp: '2026-04-01T09:05:00' }, + { id: 'c12', type: 'call', partyId: 'p5', from: 'Ana Ramírez', body: 'Called attorney Gabriela Rojas to discuss $38K offer. Attorney says client won\'t accept less than $45K. Agreed to prepare counter-proposal for $42K based on additional medical documentation showing ongoing rehabilitation needs.', timestamp: '2026-04-02T11:00:00' }, + ], + + documents: [ + { id: 'd1', name: 'FNOL-CLM0043.pdf', category: 'fnol', uploadedBy: 'Ana R.', uploadedAt: '2026-03-17', size: '212 KB', required: true, received: true }, + { id: 'd2', name: 'GL-Policy-Declarations.pdf', category: 'fnol', uploadedBy: 'Ana R.', uploadedAt: '2026-03-17', size: '1.1 MB', required: true, received: true }, + { id: 'd3', name: 'Incident-Report-StoreManager.pdf', category: 'evidence', uploadedBy: 'Jorge Calvo', uploadedAt: '2026-03-18', size: '340 KB', required: true, received: true }, + { id: 'd4', name: 'CCTV-Footage-ProduceSection.mp4', category: 'evidence', uploadedBy: 'Jorge Calvo', uploadedAt: '2026-03-19', size: '245 MB', required: true, received: true }, + { id: 'd5', name: 'Medical-Records-MartaSolis.pdf', category: 'evidence', uploadedBy: 'Lic. Gabriela Rojas', uploadedAt: '2026-03-21', size: '4.2 MB', required: true, received: true }, + { id: 'd6', name: 'INS-Investigation-Report.pdf', category: 'correspondence', uploadedBy: 'Sandra Pérez', uploadedAt: '2026-03-25', size: '890 KB', required: false, received: true }, + { id: 'd7', name: 'Settlement-Offer-38K.pdf', category: 'settlement', uploadedBy: 'Ana R.', uploadedAt: '2026-04-01', size: '156 KB', required: false, received: true }, + { id: 'd8', name: 'Updated-Medical-Records.pdf', category: 'evidence', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd9', name: 'Counter-Offer-Response.pdf', category: 'settlement', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + { id: 'd10', name: 'Signed-Release-Form.pdf', category: 'settlement', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + ], + + financials: [ + { id: 'f1', type: 'reserve_change', date: '2026-03-17', amount: 30_000, description: 'Initial reserve set', annotation: 'Preliminary — slip and fall with attorney involvement' }, + { id: 'f2', type: 'reserve_change', date: '2026-03-25', amount: 45_000, description: 'Reserve increased', annotation: 'CCTV review shows partial liability — increased from $30K' }, + { id: 'f3', type: 'expense', date: '2026-03-18', amount: 200, description: 'Incident scene documentation' }, + { id: 'f4', type: 'expense', date: '2026-03-25', amount: 750, description: 'Legal consultation — liability assessment' }, + ], + + aiRecap: 'Reclamo de responsabilidad civil por caída de cliente (Marta Solís) en la sección de productos del Supermercado Tico el 16 de marzo. La clienta sufrió una lesión de cadera que requirió cirugía. Ha contratado abogada (Lic. Gabriela Rojas).\n\nINS completó la investigación el 25 de marzo. La revisión del CCTV muestra que el letrero de piso mojado estaba presente pero mal posicionado — esto crea responsabilidad parcial. La reserva se incrementó de $30K a $45K.\n\nINS ofreció $38K para liquidar ($28K gastos médicos + $10K dolor y sufrimiento). La abogada de la reclamante rechazó — exige mínimo $45K. Se está preparando contraoferta de $42K basada en documentación médica adicional que muestra necesidades de rehabilitación continua.\n\nACCIÓN REQUERIDA: Actualización al cliente pendiente hace 8 días. El asegurado (Jorge Calvo) no ha sido informado del estado de la negociación.', + aiRecapSourceCount: 18, + + keyDates: [ + { label: 'Incident Date', date: '2026-03-16', done: true }, + { label: 'FNOL Filed', date: '2026-03-17', done: true }, + { label: 'Carrier Acknowledged', date: '2026-03-18', done: true }, + { label: 'Investigation Complete', date: '2026-03-25', done: true }, + { label: 'Reserve Set ($45K)', date: '2026-03-25', done: true }, + { label: 'Settlement Offered ($38K)', date: '2026-04-01', done: true }, + { label: 'Counter-Offer Deadline', date: '2026-04-10', done: false }, + { label: 'Settlement or Litigation', date: '', done: false }, + ], + + reserveHistory: [ + { date: '2026-03-17', amount: 30_000, annotation: 'Initial reserve — slip and fall with legal representation' }, + { date: '2026-03-25', amount: 45_000, annotation: 'Increased after CCTV review — partial liability exposure confirmed' }, + ], + intakeToken: 'tk_st_043_c9d4', + intakeStatus: 'completed', + intakeSentAt: '2026-03-17T16:00:00Z', + intakeCompletedAt: '2026-03-18T08:45:00Z', + generatedForms: [ + { id: 'gf-043-1', carrierFormName: 'Aviso de Pérdida — Mapfre', carrier: 'Mapfre', lob: 'General Risk', status: 'submitted', generatedAt: '2026-03-18T10:00:00Z', signedAt: '2026-03-19T14:30:00Z' }, + ], +} + +const clm0045: ClaimDetail = { + id: 'CLM-0045', + customerId: 'corp-clinica-sanjose', + customerName: 'Clínica San José', + policyId: 'POL-2024-CSJ-LIFE', + policyNumber: 'LIFE-2024-CSJ-001', + carrier: 'Pan-American Life', + lob: 'Life', + type: 'Surgery pre-authorization', + carrierStatus: 'reserved', + workflowStatus: 'waiting_carrier', + priority: 'high', + dateFiled: '2026-03-27', + daysOpen: 12, + handler: 'Ana R.', + reservedAmount: 23_500, + paidAmount: 0, + + parties: [ + { id: 'p1', role: 'insured', name: 'Dr. Ricardo Blanco', initials: 'RB', email: 'rblanco@clinicasj.cr', phone: '+506 2290-1500', company: 'Clínica San José', unreadComms: 0 }, + { id: 'p2', role: 'carrier_contact', name: 'Elena Cordero', initials: 'EC', email: 'ecordero@palig.com', phone: '+506 2201-3300', company: 'Pan-American Life', unreadComms: 0 }, + { id: 'p3', role: 'handler', name: 'Ana Ramírez', initials: 'AR', email: 'ana.r@seguros.cr', phone: '+506 8855-3300', unreadComms: 0 }, + ], + + tasks: [ + { id: 't1', title: 'Follow up on pre-authorization decision — 5 days pending', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-09', slaPercent: 78, type: 'communication' }, + { id: 't2', title: 'Confirm surgical facility is in-network', status: 'done', assignee: 'Ana R.', dueDate: '2026-03-29', slaPercent: 100, type: 'general' }, + { id: 't3', title: 'Upload specialist referral letter', status: 'done', assignee: 'Ana R.', dueDate: '2026-03-30', slaPercent: 100, type: 'document' }, + ], + + communications: [ + { id: 'c1', type: 'system', partyId: 'p3', from: 'System', body: 'Claim CLM-0045 created. Pre-authorization request submitted to Pan-American Life.', timestamp: '2026-03-27T09:00:00' }, + { id: 'c2', type: 'email', partyId: 'p1', from: 'Dr. Ricardo Blanco', to: 'Ana Ramírez', subject: 'Surgery Pre-Auth Request — Group Policy', body: 'Ana, we need pre-authorization for knee replacement surgery for one of our covered employees (María del Carmen Vega, DOB 15/07/1968). The procedure is scheduled for April 15 at Hospital Metropolitano. Attached are the specialist referral, diagnostic imaging, and treatment plan.', timestamp: '2026-03-27T09:30:00', threadId: 'th1' }, + { id: 'c3', type: 'email', partyId: 'p3', from: 'Ana Ramírez', to: 'Elena Cordero', subject: 'Pre-Auth Request — Clínica San José Group CLM-0045', body: 'Elena, submitting pre-authorization for knee replacement surgery under group policy LIFE-2024-CSJ-001. Patient María del Carmen Vega. Surgery scheduled April 15 at Hospital Metropolitano (in-network confirmed). All supporting documentation attached.', timestamp: '2026-03-27T11:00:00', threadId: 'th2' }, + { id: 'c4', type: 'email', partyId: 'p2', from: 'Elena Cordero', to: 'Ana Ramírez', subject: 'RE: Pre-Auth Request — Clínica San José Group', body: 'Ana, request received and under medical review. Reference PA-2026-0455. Standard review period is 5-7 business days. We may request additional documentation if needed.', timestamp: '2026-03-28T10:00:00', threadId: 'th2', aiDigest: 'Pan-American Life acknowledged pre-auth request as PA-2026-0455. Under medical review. 5-7 business day standard review period.' }, + { id: 'c5', type: 'call', partyId: 'p1', from: 'Ana Ramírez', body: 'Called Dr. Blanco to confirm submission. Advised that standard review is 5-7 business days. He is concerned about timing — surgery scheduled April 15 and patient has been waiting 3 months. Will escalate if no response by April 4.', timestamp: '2026-03-28T14:00:00' }, + { id: 'c6', type: 'system', partyId: 'p3', from: 'System', body: 'Carrier status updated: Documentation Pending → Reserved. Reserve set at $23,500.', timestamp: '2026-04-02T09:00:00' }, + { id: 'c7', type: 'note', partyId: 'p3', from: 'Ana Ramírez', body: 'Reserve set at $23,500 — this is the estimated surgery cost. No decision yet on pre-authorization. 5 business days have passed. Will follow up with Elena tomorrow if no response.', timestamp: '2026-04-03T16:00:00' }, + ], + + documents: [ + { id: 'd1', name: 'PreAuth-Request-CLM0045.pdf', category: 'fnol', uploadedBy: 'Ana R.', uploadedAt: '2026-03-27', size: '178 KB', required: true, received: true }, + { id: 'd2', name: 'Specialist-Referral-Letter.pdf', category: 'fnol', uploadedBy: 'Dr. Ricardo Blanco', uploadedAt: '2026-03-27', size: '95 KB', required: true, received: true }, + { id: 'd3', name: 'Diagnostic-Imaging-KneeMRI.pdf', category: 'evidence', uploadedBy: 'Dr. Ricardo Blanco', uploadedAt: '2026-03-27', size: '15.2 MB', required: true, received: true }, + { id: 'd4', name: 'Treatment-Plan.pdf', category: 'evidence', uploadedBy: 'Dr. Ricardo Blanco', uploadedAt: '2026-03-27', size: '240 KB', required: true, received: true }, + { id: 'd5', name: 'PALIG-Acknowledgment.pdf', category: 'correspondence', uploadedBy: 'Ana R.', uploadedAt: '2026-03-28', size: '112 KB', required: false, received: true }, + { id: 'd6', name: 'PreAuth-Decision-Letter.pdf', category: 'correspondence', uploadedBy: '', uploadedAt: '', size: '', required: true, received: false }, + ], + + financials: [ + { id: 'f1', type: 'reserve_change', date: '2026-04-02', amount: 23_500, description: 'Reserve set — estimated surgery cost', annotation: 'Knee replacement at Hospital Metropolitano' }, + ], + + aiRecap: 'Solicitud de preautorización para cirugía de reemplazo de rodilla para empleada María del Carmen Vega bajo la póliza grupal de Clínica San José. Cirugía programada para el 15 de abril en Hospital Metropolitano (red confirmada).\n\nPan-American Life acusó recibo el 28 de marzo (ref PA-2026-0455). Período de revisión estándar 5-7 días hábiles. La reserva se estableció en $23,500 el 2 de abril.\n\nHan pasado 7 días hábiles sin decisión. El Dr. Blanco está preocupado por el cronograma — la paciente ha esperado 3 meses. Se necesita seguimiento urgente con la aseguradora para obtener decisión antes de la fecha de cirugía.', + aiRecapSourceCount: 8, + + keyDates: [ + { label: 'Pre-Auth Submitted', date: '2026-03-27', done: true }, + { label: 'Carrier Acknowledged', date: '2026-03-28', done: true }, + { label: 'Review Period Ends', date: '2026-04-07', done: false }, + { label: 'Decision Expected', date: '2026-04-09', done: false }, + { label: 'Surgery Scheduled', date: '2026-04-15', done: false }, + ], + + reserveHistory: [ + { date: '2026-04-02', amount: 23_500, annotation: 'Reserve set — estimated surgery cost at Hospital Metropolitano' }, + ], + intakeToken: null, + intakeStatus: 'not_sent', + intakeSentAt: null, + intakeCompletedAt: null, + generatedForms: [], +} + +// ── Export ─────────────────────────────────────────────────────────────────── + +export const MOCK_CLAIM_DETAILS: Record = { + 'CLM-0048': clm0048, + 'CLM-0047': clm0047, + 'CLM-0043': clm0043, + 'CLM-0045': clm0045, +} diff --git a/app/data/mock-customers.ts b/app/data/mock-customers.ts new file mode 100644 index 0000000..c422a4a --- /dev/null +++ b/app/data/mock-customers.ts @@ -0,0 +1,461 @@ +/** + * Mock customer data for visual design & demo. + * Each client has realistic policies, claims, payments, and activity. + */ + +export type MockPolicy = { + id: string + line: string + carrier: string + product: string + premium: number + status: 'Active' | 'Pending' | 'Lapsed' | 'Cancelled' + renewal: string + icon: string + details?: string + referralChannel?: string +} + +export type MockClaim = { + id: string + policy: string + type: string + date: string + amount: number + status: 'In progress' | 'Resolved' | 'Denied' | 'Under review' +} + +export type MockPayment = { + date: string + amount: number + policy: string + method: string + status: 'Paid' | 'Pending' | 'Overdue' | 'Failed' +} + +export type MockActivityEvent = { + date: string + text: string + type: 'claim' | 'payment' | 'renewal' | 'quote' | 'note' | 'policy' | 'onboarding' +} + +/** + * Customer tier — derived from data completeness & policy status: + * quick_lead — minimal capture (name + phone/email only) + * lead — profile info but no policies yet + * customer — has at least one active policy + * cancelled — had policies but all cancelled / lapsed + */ +export type CustomerTier = 'quick_lead' | 'lead' | 'customer' | 'cancelled' + +export type MockCustomer = { + id: string + name: string + initials: string + type: 'Individual' | 'Corporate' + documentId: string + email: string + phone: string + birthDate: string + gender: string + address: string + since: string + agent: string + preferredLang: string + tags: string[] + policies: MockPolicy[] + claims: MockClaim[] + payments: MockPayment[] + activity: MockActivityEvent[] + paymentStatus: 'Current' | 'Overdue' | 'Grace period' | 'N/A' +} + +/** Derive tier from customer data */ +export function customerTier(c: MockCustomer): CustomerTier { + if (c.policies.length === 0 && (!c.documentId || c.documentId === '—') && !c.address) return 'quick_lead' + if (c.policies.length === 0) return 'lead' + const hasActive = c.policies.some(p => p.status === 'Active' || p.status === 'Pending') + if (!hasActive) return 'cancelled' + return 'customer' +} + +/* ────────────────────────────────────────────── */ +/* 1 · María Elena Pérez Solano */ +/* ────────────────────────────────────────────── */ +const maria: MockCustomer = { + id: 'cust-001', + name: 'María Elena Pérez Solano', + initials: 'MP', + type: 'Individual', + documentId: '1-0456-0812', + email: 'maria.perez@email.com', + phone: '+506 8834-2291', + birthDate: '1988-03-14', + gender: 'Female', + address: 'San José, Escazú, Trejos Montealegre', + since: '2021-06-10', + agent: 'Ana R.', + preferredLang: 'Spanish', + tags: ['VIP', 'Referral source'], + paymentStatus: 'Current', + policies: [ + { id: 'POL-2024-4412', line: 'Auto', carrier: 'ASSA', product: '2023 Toyota RAV4 — SJO-4412', premium: 1840, status: 'Active', renewal: '2025-06-15', icon: 'i-heroicons-truck', details: 'Comprehensive • $500 deductible', referralChannel: 'Direct walk-in' }, + { id: 'POL-2024-7788', line: 'Life', carrier: 'INS', product: 'Individual health plan', premium: 3200, status: 'Active', renewal: '2025-09-01', icon: 'i-heroicons-heart', details: 'Gold tier • Dental included', referralChannel: 'Direct walk-in' }, + { id: 'POL-2023-1190', line: 'Life', carrier: 'Mapfre', product: 'Term life — 20yr, $250K', premium: 960, status: 'Active', renewal: '2026-01-10', icon: 'i-heroicons-shield-check', details: 'Beneficiary: Carlos Pérez (spouse)', referralChannel: 'Cross-sell' }, + ], + claims: [ + { id: 'CL-2891', policy: 'POL-2024-4412', type: 'Collision', date: '2025-02-18', amount: 4200, status: 'In progress' }, + { id: 'CL-2204', policy: 'POL-2024-7788', type: 'Medical reimbursement', date: '2024-08-05', amount: 1100, status: 'Resolved' }, + ], + payments: [ + { date: '2025-04-01', amount: 540, policy: 'POL-2024-4412', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-04-01', amount: 267, policy: 'POL-2024-7788', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-03-01', amount: 540, policy: 'POL-2024-4412', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-03-01', amount: 267, policy: 'POL-2024-7788', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-03-01', amount: 80, policy: 'POL-2023-1190', method: 'Transfer', status: 'Paid' }, + ], + activity: [ + { date: 'Today', text: 'Auto claim CL-2891 — adjuster report uploaded', type: 'claim' }, + { date: 'Yesterday', text: 'Health premium payment confirmed ($267)', type: 'payment' }, + { date: 'Mar 28', text: 'Renewal notice sent for Auto policy', type: 'renewal' }, + { date: 'Mar 15', text: 'Quote requested: Home insurance', type: 'quote' }, + { date: 'Feb 18', text: 'Collision claim filed — CL-2891', type: 'claim' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 2 · Roberto Jiménez Mora */ +/* ────────────────────────────────────────────── */ +const roberto: MockCustomer = { + id: 'cust-002', + name: 'Roberto Jiménez Mora', + initials: 'RJ', + type: 'Individual', + documentId: '3-0321-0654', + email: 'roberto.jimenez@correo.cr', + phone: '+506 7012-8845', + birthDate: '1975-11-22', + gender: 'Male', + address: 'Heredia, Belén, La Asunción', + since: '2019-02-15', + agent: 'Ana R.', + preferredLang: 'Spanish', + tags: ['Long-term client'], + paymentStatus: 'Current', + policies: [ + { id: 'POL-2023-3301', line: 'Auto', carrier: 'Qualitas', product: '2021 Hyundai Tucson — HER-9901', premium: 1520, status: 'Active', renewal: '2025-08-20', icon: 'i-heroicons-truck', details: 'Comprehensive • $750 deductible', referralChannel: 'Referral — client' }, + { id: 'POL-2023-3302', line: 'Auto', carrier: 'Qualitas', product: '2019 Honda CRV — HER-6632', premium: 1280, status: 'Active', renewal: '2025-08-20', icon: 'i-heroicons-truck', details: 'Comprehensive • $750 deductible • Spouse vehicle', referralChannel: 'Referral — client' }, + { id: 'POL-2022-1010', line: 'Home', carrier: 'ASSA', product: 'Homeowner — Belén residence', premium: 890, status: 'Active', renewal: '2025-11-01', icon: 'i-heroicons-home-modern', details: 'Dwelling $185K • Contents $40K • Earthquake included', referralChannel: 'Cross-sell' }, + { id: 'POL-2024-5500', line: 'Life', carrier: 'Pan-American Life', product: 'Whole life — $150K', premium: 1440, status: 'Active', renewal: '2026-02-15', icon: 'i-heroicons-shield-check', details: 'Beneficiary: Lucía Jiménez (spouse)', referralChannel: 'Cross-sell' }, + { id: 'POL-2024-5501', line: 'General Risk', carrier: 'INS', product: 'Personal umbrella — $1M', premium: 420, status: 'Active', renewal: '2025-12-01', icon: 'i-heroicons-shield-exclamation', details: 'Excess liability over auto + home', referralChannel: 'Cross-sell' }, + ], + claims: [ + { id: 'CL-1840', policy: 'POL-2022-1010', type: 'Water damage', date: '2024-04-12', amount: 6800, status: 'Resolved' }, + { id: 'CL-2105', policy: 'POL-2023-3301', type: 'Windshield', date: '2024-11-28', amount: 450, status: 'Resolved' }, + ], + payments: [ + { date: '2025-04-01', amount: 233, policy: 'POL-2023-3301', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-04-01', amount: 197, policy: 'POL-2023-3302', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-04-01', amount: 74, policy: 'POL-2022-1010', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-04-01', amount: 120, policy: 'POL-2024-5500', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-03-01', amount: 233, policy: 'POL-2023-3301', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-03-01', amount: 197, policy: 'POL-2023-3302', method: 'Auto-debit', status: 'Paid' }, + ], + activity: [ + { date: 'Yesterday', text: 'Monthly premium auto-debited ($624)', type: 'payment' }, + { date: 'Mar 30', text: 'Umbrella policy annual review scheduled', type: 'renewal' }, + { date: 'Mar 15', text: 'Quote requested: Teen driver add-on', type: 'quote' }, + { date: 'Feb 20', text: 'Home policy endorsement — added jewelry rider', type: 'policy' }, + { date: 'Jan 10', text: 'Windshield claim CL-2105 resolved ($450)', type: 'claim' }, + { date: 'Dec 15', text: 'Year-end portfolio review completed', type: 'note' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 3 · Carolina Fallas Vargas */ +/* ────────────────────────────────────────────── */ +const carolina: MockCustomer = { + id: 'cust-003', + name: 'Carolina Fallas Vargas', + initials: 'CF', + type: 'Individual', + documentId: '2-0589-0177', + email: 'carolina.fallas@gmail.com', + phone: '+506 6198-3340', + birthDate: '1992-07-30', + gender: 'Female', + address: 'Cartago, Paraíso, Orosí', + since: '2023-09-05', + agent: 'Marco V.', + preferredLang: 'Spanish', + tags: ['New client'], + paymentStatus: 'Current', + policies: [ + { id: 'POL-2024-8810', line: 'Auto', carrier: 'INS', product: '2024 Kia Sportage — CAR-1177', premium: 1650, status: 'Active', renewal: '2025-09-05', icon: 'i-heroicons-truck', details: 'Comprehensive • $500 deductible', referralChannel: 'Facebook campaign' }, + { id: 'POL-2024-8811', line: 'Home', carrier: 'ASSA', product: "Renter's insurance — Paraíso apt", premium: 320, status: 'Active', renewal: '2025-09-05', icon: 'i-heroicons-home-modern', details: 'Contents $25K • Liability $100K', referralChannel: 'Facebook campaign' }, + ], + claims: [], + payments: [ + { date: '2025-04-01', amount: 138, policy: 'POL-2024-8810', method: 'Credit card', status: 'Paid' }, + { date: '2025-04-01', amount: 27, policy: 'POL-2024-8811', method: 'Credit card', status: 'Paid' }, + { date: '2025-03-01', amount: 138, policy: 'POL-2024-8810', method: 'Credit card', status: 'Paid' }, + { date: '2025-03-01', amount: 27, policy: 'POL-2024-8811', method: 'Credit card', status: 'Paid' }, + ], + activity: [ + { date: 'Mar 25', text: 'Monthly payment processed ($165)', type: 'payment' }, + { date: 'Mar 10', text: 'Renter policy — updated inventory list', type: 'policy' }, + { date: 'Feb 05', text: 'Welcome call completed by Marco V.', type: 'onboarding' }, + { date: 'Sep 05', text: 'Policies issued — Auto + Renter', type: 'policy' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 4 · Luis Andrés Solís Calderón */ +/* ────────────────────────────────────────────── */ +const luis: MockCustomer = { + id: 'cust-004', + name: 'Luis Andrés Solís Calderón', + initials: 'LS', + type: 'Individual', + documentId: '1-1102-0398', + email: 'luis.solis@outlook.com', + phone: '+506 8455-7721', + birthDate: '1968-01-09', + gender: 'Male', + address: 'San José, Santa Ana, Pozos', + since: '2017-11-20', + agent: 'Ana R.', + preferredLang: 'Spanish', + tags: ['High-value', 'Referral source', 'Multi-line'], + paymentStatus: 'Overdue', + policies: [ + { id: 'POL-2022-2200', line: 'Auto', carrier: 'Qualitas', product: '2022 BMW X5 — SJO-2200', premium: 3200, status: 'Active', renewal: '2025-05-01', icon: 'i-heroicons-truck', details: 'Comprehensive • $1,000 deductible', referralChannel: 'Referral — client' }, + { id: 'POL-2022-2201', line: 'Auto', carrier: 'Qualitas', product: '2020 Mercedes GLC — SJO-7788', premium: 2800, status: 'Active', renewal: '2025-05-01', icon: 'i-heroicons-truck', details: 'Comprehensive • $1,000 deductible • Spouse vehicle', referralChannel: 'Referral — client' }, + { id: 'POL-2020-0055', line: 'Home', carrier: 'ASSA', product: 'Homeowner — Santa Ana residence', premium: 2100, status: 'Active', renewal: '2025-07-15', icon: 'i-heroicons-home-modern', details: 'Dwelling $420K • Contents $95K • Flood + Earthquake', referralChannel: 'Cross-sell' }, + { id: 'POL-2021-0750', line: 'Life', carrier: 'Pan-American Life', product: 'Whole life — $500K', premium: 4800, status: 'Active', renewal: '2025-11-20', icon: 'i-heroicons-shield-check', details: 'Beneficiaries: Patricia Calderón (60%), Children (40%)', referralChannel: 'Google Ads' }, + { id: 'POL-2023-6600', line: 'Life', carrier: 'Blue Cross', product: 'Family health — Platinum', premium: 8400, status: 'Active', renewal: '2025-10-01', icon: 'i-heroicons-heart', details: '4 members • Dental + Vision • Intl coverage', referralChannel: 'Google Ads' }, + { id: 'POL-2023-6601', line: 'General Risk', carrier: 'INS', product: 'Personal umbrella — $2M', premium: 680, status: 'Active', renewal: '2025-12-01', icon: 'i-heroicons-shield-exclamation', details: 'Excess liability over auto + home', referralChannel: 'Cross-sell' }, + ], + claims: [ + { id: 'CL-3020', policy: 'POL-2022-2200', type: 'Collision — rear-end', date: '2025-03-08', amount: 9500, status: 'In progress' }, + { id: 'CL-2650', policy: 'POL-2023-6600', type: 'Surgery reimbursement', date: '2024-10-15', amount: 12400, status: 'Resolved' }, + { id: 'CL-2102', policy: 'POL-2020-0055', type: 'Storm damage — roof', date: '2024-06-22', amount: 8200, status: 'Resolved' }, + ], + payments: [ + { date: '2025-04-01', amount: 1832, policy: 'Multiple', method: 'Auto-debit', status: 'Failed' }, + { date: '2025-03-01', amount: 1832, policy: 'Multiple', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-02-01', amount: 1832, policy: 'Multiple', method: 'Auto-debit', status: 'Paid' }, + { date: '2025-01-01', amount: 1832, policy: 'Multiple', method: 'Auto-debit', status: 'Paid' }, + ], + activity: [ + { date: 'Today', text: 'ALERT: April payment failed — auto-debit declined', type: 'payment' }, + { date: 'Yesterday', text: 'Collision claim CL-3020 — repair estimate received ($9,500)', type: 'claim' }, + { date: 'Mar 20', text: 'Auto policies up for renewal May 1 — quote comparison started', type: 'renewal' }, + { date: 'Mar 08', text: 'Collision claim filed — CL-3020 (BMW rear-end)', type: 'claim' }, + { date: 'Feb 15', text: 'Annual portfolio review — recommended umbrella increase', type: 'note' }, + { date: 'Jan 20', text: 'Health claim CL-2650 resolved — $12,400 reimbursed', type: 'claim' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 5 · Sofía Campos Rojas */ +/* ────────────────────────────────────────────── */ +const sofia: MockCustomer = { + id: 'cust-005', + name: 'Sofía Campos Rojas', + initials: 'SC', + type: 'Individual', + documentId: '4-0220-0561', + email: 'sofia.campos@icloud.com', + phone: '+506 7233-0098', + birthDate: '1995-12-03', + gender: 'Female', + address: 'Guanacaste, Liberia, Centro', + since: '2024-01-15', + agent: 'Marco V.', + preferredLang: 'Spanish', + tags: ['Young professional'], + paymentStatus: 'Grace period', + policies: [ + { id: 'POL-2024-9901', line: 'Auto', carrier: 'INS', product: '2024 Mazda CX-30 — GUA-0098', premium: 1380, status: 'Active', renewal: '2026-01-15', icon: 'i-heroicons-truck', details: 'Comprehensive • $500 deductible', referralChannel: 'Instagram campaign' }, + { id: 'POL-2024-9902', line: 'Life', carrier: 'Mapfre', product: 'Term life — 10yr, $100K', premium: 360, status: 'Active', renewal: '2026-01-15', icon: 'i-heroicons-shield-check', details: 'Beneficiary: Elena Rojas (mother)', referralChannel: 'Cross-sell' }, + ], + claims: [ + { id: 'CL-3101', policy: 'POL-2024-9901', type: 'Fender bender', date: '2025-03-20', amount: 1800, status: 'Under review' }, + ], + payments: [ + { date: '2025-04-01', amount: 145, policy: 'POL-2024-9901', method: 'Transfer', status: 'Pending' }, + { date: '2025-03-01', amount: 145, policy: 'POL-2024-9901', method: 'Transfer', status: 'Paid' }, + { date: '2025-03-01', amount: 30, policy: 'POL-2024-9902', method: 'Transfer', status: 'Paid' }, + { date: '2025-02-01', amount: 145, policy: 'POL-2024-9901', method: 'Transfer', status: 'Paid' }, + ], + activity: [ + { date: 'Today', text: 'Grace period — April auto premium not yet received', type: 'payment' }, + { date: 'Mar 22', text: 'Fender bender claim CL-3101 filed', type: 'claim' }, + { date: 'Mar 01', text: 'Monthly payment received ($175)', type: 'payment' }, + { date: 'Jan 15', text: 'Policies renewed — Auto + Life', type: 'renewal' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 6 · Quick Lead — Diego Herrera */ +/* ────────────────────────────────────────────── */ +const diego: MockCustomer = { + id: 'cust-006', + name: 'Diego Herrera', + initials: 'DH', + type: 'Individual', + documentId: '—', + email: 'diego.h@gmail.com', + phone: '+506 6100-4422', + birthDate: '', + gender: '', + address: '', + since: '2026-03-28', + agent: 'Marco V.', + preferredLang: 'Spanish', + tags: ['Quick lead', 'Referral'], + paymentStatus: 'N/A', + policies: [], + claims: [], + payments: [], + activity: [ + { date: 'Mar 28', text: 'Quick lead captured — referred by Roberto Jiménez', type: 'onboarding' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 7 · Quick Lead — Valeria Núñez */ +/* ────────────────────────────────────────────── */ +const valeria: MockCustomer = { + id: 'cust-007', + name: 'Valeria Núñez', + initials: 'VN', + type: 'Individual', + documentId: '—', + email: '', + phone: '+506 8899-1100', + birthDate: '', + gender: '', + address: '', + since: '2026-04-01', + agent: 'Ana R.', + preferredLang: 'Spanish', + tags: ['Quick lead', 'Walk-in'], + paymentStatus: 'N/A', + policies: [], + claims: [], + payments: [], + activity: [ + { date: 'Apr 01', text: 'Walk-in lead — interested in auto insurance', type: 'onboarding' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 8 · Lead — Andrés Mora Villalobos */ +/* ────────────────────────────────────────────── */ +const andres: MockCustomer = { + id: 'cust-008', + name: 'Andrés Mora Villalobos', + initials: 'AM', + type: 'Individual', + documentId: '1-1450-0221', + email: 'andres.mora@empresa.cr', + phone: '+506 7744-5566', + birthDate: '1990-05-18', + gender: 'Male', + address: 'San José, Moravia, San Vicente', + since: '2026-03-15', + agent: 'Ana R.', + preferredLang: 'Spanish', + tags: ['Lead', 'Corporate referral'], + paymentStatus: 'N/A', + policies: [], + claims: [], + payments: [], + activity: [ + { date: 'Mar 15', text: 'Lead created — needs auto + health quotes', type: 'onboarding' }, + { date: 'Mar 20', text: 'Discovery call completed — family of 3', type: 'note' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 9 · Lead — Corporación Tecnológica del Valle */ +/* ────────────────────────────────────────────── */ +const corpTech: MockCustomer = { + id: 'cust-009', + name: 'Corporación Tecnológica del Valle', + initials: 'CT', + type: 'Corporate', + documentId: '3-101-789456', + email: 'rrhh@corptech.cr', + phone: '+506 2234-8800', + birthDate: '', + gender: '', + address: 'Heredia, Heredia, Zona Franca', + since: '2026-02-10', + agent: 'Marco V.', + preferredLang: 'Spanish', + tags: ['Lead', 'Corporate', 'Group health prospect'], + paymentStatus: 'N/A', + policies: [], + claims: [], + payments: [], + activity: [ + { date: 'Feb 10', text: 'Corporate lead — 45 employees, group health RFP', type: 'onboarding' }, + { date: 'Mar 05', text: 'Census received — quoting in progress', type: 'quote' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* 10 · Cancelled — Fernando Arias Blanco */ +/* ────────────────────────────────────────────── */ +const fernando: MockCustomer = { + id: 'cust-010', + name: 'Fernando Arias Blanco', + initials: 'FA', + type: 'Individual', + documentId: '1-0892-0344', + email: 'fernando.arias@hotmail.com', + phone: '+506 8322-0017', + birthDate: '1982-09-14', + gender: 'Male', + address: 'Alajuela, San Carlos, Ciudad Quesada', + since: '2020-04-01', + agent: 'Ana R.', + preferredLang: 'Spanish', + tags: ['Cancelled', 'Win-back opportunity'], + paymentStatus: 'N/A', + policies: [ + { id: 'POL-2020-1100', line: 'Auto', carrier: 'INS', product: '2018 Toyota Hilux — ALJ-4400', premium: 1200, status: 'Cancelled', renewal: '2024-04-01', icon: 'i-heroicons-truck', details: 'Cancelled — non-payment', referralChannel: 'Direct walk-in' }, + { id: 'POL-2021-2200', line: 'Home', carrier: 'ASSA', product: 'Homeowner — Ciudad Quesada', premium: 650, status: 'Lapsed', renewal: '2024-06-15', icon: 'i-heroicons-home-modern', details: 'Lapsed — did not renew', referralChannel: 'Direct walk-in' }, + ], + claims: [ + { id: 'CL-1200', policy: 'POL-2020-1100', type: 'Theft attempt', date: '2023-08-10', amount: 3200, status: 'Resolved' }, + ], + payments: [ + { date: '2024-01-01', amount: 154, policy: 'POL-2020-1100', method: 'Transfer', status: 'Paid' }, + { date: '2024-02-01', amount: 154, policy: 'POL-2020-1100', method: 'Transfer', status: 'Overdue' }, + { date: '2024-03-01', amount: 154, policy: 'POL-2020-1100', method: 'Transfer', status: 'Overdue' }, + ], + activity: [ + { date: 'Apr 01', text: 'Auto policy cancelled — 3 months unpaid', type: 'policy' }, + { date: 'Jun 15', text: 'Home policy lapsed — did not renew', type: 'policy' }, + { date: 'Aug 20', text: 'Win-back call attempted — no answer', type: 'note' }, + ], +} + +/* ────────────────────────────────────────────── */ +/* Exports */ +/* ────────────────────────────────────────────── */ + +export const MOCK_CUSTOMERS: MockCustomer[] = [maria, roberto, carolina, luis, sofia, diego, valeria, andres, corpTech, fernando] + +export const MOCK_CUSTOMERS_BY_ID: Record = Object.fromEntries( + MOCK_CUSTOMERS.map((c) => [c.id, c]) +) + +/** Helper to format currency */ +export function fmtMoney(n: number): string { + return `$${n.toLocaleString()}` +} diff --git a/app/data/mock-renewals.ts b/app/data/mock-renewals.ts new file mode 100644 index 0000000..1adc754 --- /dev/null +++ b/app/data/mock-renewals.ts @@ -0,0 +1,660 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// RENEWALS — Data Layer +// ═══════════════════════════════════════════════════════════════════════════════ + +// --- Carrier Status --- +export type CarrierRenewalStatus = + | 'pending' + | 'terms_received' + | 'remarketing' + | 'bound' + | 'declined' + | 'lapsed' + +// --- Broker Workflow Status --- +export type BrokerRenewalStatus = + | 'unreviewed' + | 'under_review' + | 'proposal_sent' + | 'awaiting_client_response' + | 'awaiting_payment' + | 'closed_renewed' + | 'closed_remarketed' + | 'closed_cancelled' + | 'not_renewing' + +export type RenewalPriority = 'critical' | 'high' | 'medium' | 'low' +export type RetentionRisk = 'high' | 'medium' | 'low' + +export type CancellationReason = + | 'price' + | 'service' + | 'competitor' + | 'coverage_gap' + | 'business_closed' + | 'non_payment' + | 'carrier_declined' + | 'other' + +export interface CancellationData { + reason: CancellationReason + reasonDetail: string | null + competitor: string | null + competitorPremium: number | null + recoverable: boolean + exitDate: string +} + +export interface RenewalTask { + id: string + title: string + type: 'review' | 'send_proposal' | 'follow_up' | 'collect_payment' | 'escalation' + status: 'open' | 'in_progress' | 'overdue' | 'done' + assignee: string + dueDate: string + aiGenerated: boolean + slaPercent: number +} + +export interface RenewalCommunication { + id: string + type: 'email' | 'call' | 'note' | 'system' + direction: 'inbound' | 'outbound' | 'internal' + from: string + to: string | null + subject: string | null + body: string + aiDigest: string | null + templateUsed: string | null + timestamp: string + partyRole: string +} + +export interface RenewalDocument { + id: string + name: string + category: 'current_policy' | 'renewal_terms' | 'proposal_sent' | + 'client_confirmation' | 'payment_receipt' | 'loss_runs' | + 'correspondence' | 'cancellation' + uploadedBy: string + uploadedAt: string + required: boolean + fulfilled: boolean +} + +export interface RenewalParty { + id: string + role: 'insured' | 'carrier_rep' | 'producer' | 'handler' | 'ai_agent' + name: string + company: string | null + phone: string | null + email: string | null + hasUnread: boolean +} + +export interface RenewalQuote { + id: string + carrier: string + premium: number + currency: 'USD' | 'CRC' + coverageAmount: number + deductible: number + receivedAt: string + recommended: boolean + notes: string | null +} + +export interface CoverageLine { + name: string + currentAmount: number | string + renewalAmount: number | string | null + delta: string | null + flag: 'increase' | 'decrease' | 'same' | 'new' | 'removed' | null +} + +export interface PolicyComparison { + currentPremium: number + renewalPremium: number | null + premiumDelta: number | null + currentDeductible: number + renewalDeductible: number | null + deductibleDelta: number | null + coverageLines: CoverageLine[] + aiAnalysis: string | null +} + +export interface EmailTemplate { + id: string + name: string + subject: string + body: string + stage: BrokerRenewalStatus + lob: string | 'all' + isDefault: boolean +} + +export interface RenewalHistoryEntry { + year: number + carrier: string + premium: number + outcome: 'renewed' | 'remarketed' | 'cancelled' | 'new' +} + +// --- List item --- +export interface Renewal { + id: string + policyId: string + policyNumber: string + customerId: string + customerName: string + customerType: 'individual' | 'corporate' + carrier: string + lob: string + currentPremium: number + renewalPremium: number | null + premiumDelta: number | null + currency: 'USD' | 'CRC' + expiryDate: string + daysUntilExpiry: number + carrierStatus: CarrierRenewalStatus + brokerStatus: BrokerRenewalStatus + priority: RenewalPriority + retentionRisk: RetentionRisk + lossRatio: number + yearsAsClient: number + openClaims: number + assignedTo: string + lastContactDate: string | null + slaPercent: number + outstandingBalance: number + paymentStatus: 'current' | 'overdue' | 'grace_period' +} + +// --- Detail --- +export interface RenewalDetail extends Renewal { + claimIds: string[] + parties: RenewalParty[] + tasks: RenewalTask[] + communications: RenewalCommunication[] + documents: RenewalDocument[] + comparison: PolicyComparison | null + quotes: RenewalQuote[] + renewalHistory: RenewalHistoryEntry[] + cancellationData: CancellationData | null + commissionRate: number + commissionAmount: number + aiRenewalBrief: string | null + aiTalkTrack: string[] | null + aiRetentionFactors: string[] | null +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Label Maps +// ═══════════════════════════════════════════════════════════════════════════════ + +export const carrierStatusLabels: Record = { + pending: 'Pendiente', + terms_received: 'Términos Recibidos', + remarketing: 'En Remarketing', + bound: 'Vinculada', + declined: 'Declinada', + lapsed: 'Vencida', +} + +export const brokerStatusLabels: Record = { + unreviewed: 'Sin Revisar', + under_review: 'En Revisión', + proposal_sent: 'Propuesta Enviada', + awaiting_client_response: 'Esperando Cliente', + awaiting_payment: 'Esperando Pago', + closed_renewed: 'Renovada', + closed_remarketed: 'Remarketing Exitoso', + closed_cancelled: 'Cancelada', + not_renewing: 'No Renueva', +} + +export const priorityLabels: Record = { + critical: 'Crítica', + high: 'Alta', + medium: 'Media', + low: 'Baja', +} + +export const retentionRiskLabels: Record = { + high: 'Alto', + medium: 'Medio', + low: 'Bajo', +} + +export const cancellationReasonLabels: Record = { + price: 'Precio', + service: 'Servicio', + competitor: 'Competencia', + coverage_gap: 'Cobertura insuficiente', + business_closed: 'Cierre de negocio', + non_payment: 'Falta de pago', + carrier_declined: 'Aseguradora declinó', + other: 'Otro', +} + +export const expiryBuckets = { + expired: { label: 'Vencidas', min: -Infinity, max: -1 }, + this_week: { label: 'Esta semana', min: 0, max: 7 }, + thirty_days: { label: '30 días', min: 8, max: 30 }, + sixty_days: { label: '60 días', min: 31, max: 60 }, + ninety_days: { label: '90 días', min: 61, max: 90 }, + future: { label: '90+ días', min: 91, max: Infinity }, +} + +export const templateMergeFields = [ + '{{customer_name}}', '{{policy_number}}', '{{lob}}', '{{carrier}}', + '{{current_premium}}', '{{renewal_premium}}', '{{premium_delta}}', + '{{expiry_date}}', '{{coverage_amount}}', '{{deductible}}', + '{{producer_name}}', '{{producer_phone}}', '{{producer_email}}', + '{{company_name}}', '{{loss_ratio}}', '{{years_as_client}}', +] + +// ═══════════════════════════════════════════════════════════════════════════════ +// Helpers +// ═══════════════════════════════════════════════════════════════════════════════ + +export function slaColor(percent: number): 'green' | 'amber' | 'red' { + if (percent >= 100) return 'red' + if (percent >= 75) return 'amber' + return 'green' +} + +export function expiryBucket(days: number): string { + for (const [key, b] of Object.entries(expiryBuckets)) { + if (days >= b.min && days <= b.max) return key + } + return 'future' +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Mock Email Templates +// ═══════════════════════════════════════════════════════════════════════════════ + +export const MOCK_EMAIL_TEMPLATES: EmailTemplate[] = [ + { + id: 'tpl-1', name: 'Aviso de Renovación', stage: 'under_review', lob: 'all', isDefault: true, + subject: 'Su póliza {{policy_number}} próxima a vencer', + body: 'Estimado/a {{customer_name}},\n\nLe informamos que su póliza {{policy_number}} de {{lob}} con {{carrier}} vence el {{expiry_date}}.\n\nPrima actual: {{current_premium}}\n\nNos pondremos en contacto próximamente con los términos de renovación.\n\nSaludos cordiales,\n{{producer_name}}', + }, + { + id: 'tpl-2', name: 'Propuesta de Renovación', stage: 'proposal_sent', lob: 'all', isDefault: true, + subject: 'Términos de renovación — {{policy_number}}', + body: 'Estimado/a {{customer_name}},\n\nAdjunto los términos de renovación para su póliza {{policy_number}}:\n\n• Prima actual: {{current_premium}}\n• Prima renovación: {{renewal_premium}} ({{premium_delta}})\n• Cobertura: {{coverage_amount}}\n• Deducible: {{deductible}}\n\nPor favor confirme si desea proceder con la renovación.\n\nQuedamos atentos,\n{{producer_name}}\n{{producer_phone}}', + }, + { + id: 'tpl-3', name: 'Seguimiento #1', stage: 'awaiting_client_response', lob: 'all', isDefault: true, + subject: 'Recordatorio: renovación de póliza {{policy_number}}', + body: 'Estimado/a {{customer_name}},\n\nLe recordamos que estamos a la espera de su confirmación para renovar la póliza {{policy_number}} que vence el {{expiry_date}}.\n\nPor favor responda a este correo o comuníquese al {{producer_phone}} para evitar cualquier lapso en su cobertura.\n\nSaludos,\n{{producer_name}}', + }, + { + id: 'tpl-4', name: 'Seguimiento #2', stage: 'awaiting_client_response', lob: 'all', isDefault: true, + subject: 'Segundo aviso: su póliza {{policy_number}} vence el {{expiry_date}}', + body: 'Estimado/a {{customer_name}},\n\nEste es un segundo aviso sobre la renovación de su póliza {{policy_number}}. La fecha de vencimiento es {{expiry_date}} y necesitamos su confirmación a la brevedad posible.\n\nSin su confirmación, la póliza podría vencer sin renovación, dejándole sin cobertura.\n\nPor favor contáctenos de inmediato.\n\n{{producer_name}}\n{{producer_email}} | {{producer_phone}}', + }, + { + id: 'tpl-5', name: 'Confirmación de Pago', stage: 'awaiting_payment', lob: 'all', isDefault: true, + subject: 'Pago pendiente — renovación {{policy_number}}', + body: 'Estimado/a {{customer_name}},\n\nGracias por confirmar la renovación de su póliza {{policy_number}}.\n\nPara completar el proceso, le solicitamos realizar el pago de {{renewal_premium}} a la brevedad.\n\nUna vez recibido el pago, procederemos con la emisión de su nueva póliza.\n\nSaludos,\n{{producer_name}}', + }, + { + id: 'tpl-6', name: 'Renovación Completada', stage: 'closed_renewed', lob: 'all', isDefault: true, + subject: 'Confirmación de renovación — {{policy_number}}', + body: 'Estimado/a {{customer_name}},\n\nNos complace confirmar que su póliza {{policy_number}} ha sido renovada exitosamente con {{carrier}}.\n\nNueva vigencia: {{expiry_date}}\nPrima: {{renewal_premium}}\n\nAdjunto encontrará su nueva póliza. No dude en contactarnos para cualquier consulta.\n\nGracias por su confianza,\n{{producer_name}}\n{{company_name}}', + }, +] + +// ═══════════════════════════════════════════════════════════════════════════════ +// Mock Renewals (Pipeline List) +// ═══════════════════════════════════════════════════════════════════════════════ + +export const MOCK_RENEWALS: Renewal[] = [ + { + id: 'REN-001', policyId: 'POL-0955', policyNumber: 'PROP-2024-HP-001', + customerId: 'CUS-001', customerName: 'Hotel Pacífico Resort', customerType: 'corporate', + carrier: 'ASSA', lob: 'Auto', currentPremium: 18500, renewalPremium: 22570, + premiumDelta: 22, currency: 'USD', expiryDate: '2026-04-11', daysUntilExpiry: 3, + carrierStatus: 'terms_received', brokerStatus: 'awaiting_client_response', + priority: 'critical', retentionRisk: 'high', lossRatio: 0.42, yearsAsClient: 5, + openClaims: 1, assignedTo: 'Marco V.', lastContactDate: '2026-04-03', + slaPercent: 95, outstandingBalance: 0, paymentStatus: 'current', + }, + { + id: 'REN-002', policyId: 'POL-1034', policyNumber: 'GR-2024-CM-001', + customerId: 'CUS-002', customerName: 'Constructora Montes', customerType: 'corporate', + carrier: 'Mapfre', lob: 'General Risk', currentPremium: 32000, renewalPremium: null, + premiumDelta: null, currency: 'USD', expiryDate: '2026-04-22', daysUntilExpiry: 14, + carrierStatus: 'pending', brokerStatus: 'under_review', + priority: 'high', retentionRisk: 'medium', lossRatio: 0.15, yearsAsClient: 3, + openClaims: 0, assignedTo: 'Ana R.', lastContactDate: '2026-04-01', + slaPercent: 55, outstandingBalance: 0, paymentStatus: 'current', + }, + { + id: 'REN-003', policyId: 'POL-1077', policyNumber: 'LIFE-2024-CR-001', + customerId: 'CUS-003', customerName: 'Carmen Ruiz', customerType: 'individual', + carrier: 'Pan-American Life', lob: 'Life', currentPremium: 2800, renewalPremium: 2940, + premiumDelta: 5, currency: 'USD', expiryDate: '2026-04-15', daysUntilExpiry: 7, + carrierStatus: 'terms_received', brokerStatus: 'awaiting_payment', + priority: 'medium', retentionRisk: 'low', lossRatio: 0, yearsAsClient: 8, + openClaims: 0, assignedTo: 'Ana R.', lastContactDate: '2026-04-06', + slaPercent: 30, outstandingBalance: 0, paymentStatus: 'current', + }, + { + id: 'REN-004', policyId: 'POL-1110', policyNumber: 'AUTO-2024-ABC-001', + customerId: 'CUS-004', customerName: 'Empresa ABC S.A.', customerType: 'corporate', + carrier: 'Qualitas', lob: 'Auto', currentPremium: 22400, renewalPremium: null, + premiumDelta: null, currency: 'USD', expiryDate: '2026-05-08', daysUntilExpiry: 30, + carrierStatus: 'pending', brokerStatus: 'unreviewed', + priority: 'low', retentionRisk: 'low', lossRatio: 0.08, yearsAsClient: 2, + openClaims: 0, assignedTo: 'Unassigned', lastContactDate: null, + slaPercent: 10, outstandingBalance: 0, paymentStatus: 'current', + }, + { + id: 'REN-005', policyId: 'POL-0888', policyNumber: 'HEALTH-2024-CSJ-001', + customerId: 'CUS-005', customerName: 'Clínica San José', customerType: 'corporate', + carrier: 'ASSA', lob: 'Health', currentPremium: 45000, renewalPremium: null, + premiumDelta: null, currency: 'USD', expiryDate: '2026-04-01', daysUntilExpiry: -7, + carrierStatus: 'lapsed', brokerStatus: 'closed_cancelled', + priority: 'high', retentionRisk: 'high', lossRatio: 0.65, yearsAsClient: 4, + openClaims: 0, assignedTo: 'Marco V.', lastContactDate: '2026-03-28', + slaPercent: 100, outstandingBalance: 8500, paymentStatus: 'overdue', + }, + { + id: 'REN-006', policyId: 'POL-1200', policyNumber: 'GR-2024-TN-001', + customerId: 'CUS-006', customerName: 'Transportes del Norte', customerType: 'corporate', + carrier: 'ASSA', lob: 'General Risk', currentPremium: 38000, renewalPremium: null, + premiumDelta: null, currency: 'USD', expiryDate: '2026-06-07', daysUntilExpiry: 60, + carrierStatus: 'pending', brokerStatus: 'unreviewed', + priority: 'high', retentionRisk: 'high', lossRatio: 0.55, yearsAsClient: 6, + openClaims: 1, assignedTo: 'Unassigned', lastContactDate: null, + slaPercent: 15, outstandingBalance: 21000, paymentStatus: 'overdue', + }, +] + +// ═══════════════════════════════════════════════════════════════════════════════ +// Mock Renewal Details +// ═══════════════════════════════════════════════════════════════════════════════ + +const ren001Detail: RenewalDetail = { + ...MOCK_RENEWALS[0], + claimIds: ['CLM-0048'], + parties: [ + { id: 'rp-1', role: 'insured', name: 'Carlos Montero', company: 'Hotel Pacífico Resort', phone: '+507 6700-1234', email: 'cmontero@hotelpacífico.com', hasUnread: true }, + { id: 'rp-2', role: 'carrier_rep', name: 'Lucía Vargas', company: 'ASSA', phone: '+507 6600-9876', email: 'lvargas@assa.com', hasUnread: false }, + { id: 'rp-3', role: 'handler', name: 'Marco V.', company: null, phone: null, email: 'marco@segur-os.com', hasUnread: false }, + ], + tasks: [ + { id: 'rt-1', title: 'Follow up with client — no response in 5 days', type: 'follow_up', status: 'overdue', assignee: 'Marco V.', dueDate: '2026-04-06', aiGenerated: true, slaPercent: 110 }, + { id: 'rt-2', title: 'Prepare counter-proposal (premium increase justification)', type: 'send_proposal', status: 'open', assignee: 'Marco V.', dueDate: '2026-04-09', aiGenerated: false, slaPercent: 80 }, + { id: 'rt-3', title: 'Review loss runs before presenting to client', type: 'review', status: 'done', assignee: 'Marco V.', dueDate: '2026-04-02', aiGenerated: false, slaPercent: 20 }, + ], + communications: [ + { id: 'rc-1', type: 'email', direction: 'outbound', from: 'Marco V.', to: 'Carlos Montero', subject: 'Términos de renovación — PROP-2024-HP-001', body: 'Estimado Carlos, adjunto los términos de renovación de su póliza de auto. La prima pasa de $18,500 a $22,570 (+22%) debido al reclamo abierto y al ajuste de mercado. Quedo atento a su respuesta.', aiDigest: null, templateUsed: 'tpl-2', timestamp: '2026-04-03T10:30:00', partyRole: 'insured' }, + { id: 'rc-2', type: 'email', direction: 'inbound', from: 'Lucía Vargas', to: 'Marco V.', subject: 'RE: Renewal terms HP-001', body: 'Marco, adjunto los términos definitivos para Hotel Pacífico. El aumento refleja el siniestro abierto CLM-0048 y el ajuste de tarifa general para flota comercial.', aiDigest: 'ASSA confirma aumento de 22% justificado por siniestro abierto y ajuste de tarifa.', templateUsed: null, timestamp: '2026-04-02T15:45:00', partyRole: 'carrier_rep' }, + { id: 'rc-3', type: 'note', direction: 'internal', from: 'Marco V.', to: null, subject: null, body: 'Client not responding to emails. Called twice, went to voicemail. Need to escalate if no response by Apr 8.', aiDigest: null, templateUsed: null, timestamp: '2026-04-06T09:00:00', partyRole: 'handler' }, + { id: 'rc-4', type: 'system', direction: 'internal', from: 'System', to: null, subject: null, body: 'AI escalation: Client non-response exceeds 5 days. Recommended action: send Seguimiento #1 template.', aiDigest: null, templateUsed: null, timestamp: '2026-04-08T08:00:00', partyRole: 'ai_agent' }, + ], + documents: [ + { id: 'rd-1', name: 'Póliza actual — PROP-2024-HP-001.pdf', category: 'current_policy', uploadedBy: 'System', uploadedAt: '2026-03-01', required: true, fulfilled: true }, + { id: 'rd-2', name: 'Términos renovación ASSA 2026.pdf', category: 'renewal_terms', uploadedBy: 'Lucía Vargas', uploadedAt: '2026-04-02', required: true, fulfilled: true }, + { id: 'rd-3', name: 'Propuesta enviada a cliente.pdf', category: 'proposal_sent', uploadedBy: 'Marco V.', uploadedAt: '2026-04-03', required: true, fulfilled: true }, + { id: 'rd-4', name: 'Loss runs 2023-2025.pdf', category: 'loss_runs', uploadedBy: 'ASSA', uploadedAt: '2026-04-01', required: true, fulfilled: true }, + { id: 'rd-5', name: 'Client confirmation', category: 'client_confirmation', uploadedBy: '', uploadedAt: '', required: true, fulfilled: false }, + { id: 'rd-6', name: 'Payment receipt', category: 'payment_receipt', uploadedBy: '', uploadedAt: '', required: true, fulfilled: false }, + ], + comparison: { + currentPremium: 18500, renewalPremium: 22570, premiumDelta: 22, + currentDeductible: 500, renewalDeductible: 750, deductibleDelta: 50, + coverageLines: [ + { name: 'Responsabilidad Civil', currentAmount: 100000, renewalAmount: 100000, delta: '=', flag: 'same' }, + { name: 'Daños Propios', currentAmount: 45000, renewalAmount: 45000, delta: '=', flag: 'same' }, + { name: 'Robo Total', currentAmount: 45000, renewalAmount: 40000, delta: '-11%', flag: 'decrease' }, + { name: 'Asistencia Vial', currentAmount: 'Incluida', renewalAmount: 'Incluida', delta: '=', flag: 'same' }, + { name: 'Vidrios', currentAmount: 2000, renewalAmount: 2500, delta: '+25%', flag: 'increase' }, + ], + aiAnalysis: 'El aumento de 22% se justifica por el siniestro abierto CLM-0048 ($128K reservado) y el ajuste de tarifa general de ASSA para flotas comerciales (+8% promedio). El deducible subió 50%. La cobertura de Robo Total baja 11% — recomendar al cliente mantener nivel actual. Se detecta oportunidad de negociar Vidrios si se acepta deducible mayor.', + }, + quotes: [], + renewalHistory: [ + { year: 2025, carrier: 'ASSA', premium: 18500, outcome: 'renewed' }, + { year: 2024, carrier: 'ASSA', premium: 16200, outcome: 'renewed' }, + { year: 2023, carrier: 'Qualitas', premium: 15800, outcome: 'remarketed' }, + { year: 2022, carrier: 'Qualitas', premium: 14500, outcome: 'renewed' }, + ], + cancellationData: null, + commissionRate: 15, commissionAmount: 3385.50, + aiRenewalBrief: 'Hotel Pacífico es un cliente corporativo de 5 años con prima actual de $18,500 en Auto. ASSA ofrece renovación a $22,570 (+22%), justificado por siniestro abierto CLM-0048 con $128K reservados y ajuste de tarifa de flota. El cliente no ha respondido en 5 días. El deducible sube de $500 a $750. Loss ratio de 42% está por encima del promedio para esta línea. Comisión proyectada: $3,385. Riesgo de retención ALTO.', + aiTalkTrack: [ + 'Abrir con empatía: reconocer que un aumento de 22% es significativo y que entendemos su preocupación.', + 'Explicar el contexto: el siniestro abierto impacta directamente la tarificación. ASSA subió tarifas de flota 8% en general.', + 'Destacar valor: 5 años como cliente, historial mayormente limpio. Hemos negociado mantener la cobertura de RC en $100K sin cambio.', + 'Proponer alternativa: si acepta deducible de $1,000 en vez de $750, podemos negociar bajar prima a ~$21,500.', + 'Urgencia: la póliza vence el 11 de abril. Sin confirmación antes del 9, hay riesgo de lapso.', + ], + aiRetentionFactors: [ + '5 años como cliente — relación establecida', + 'Siniestro abierto CLM-0048 dificulta remarketing', + 'Flota comercial con cobertura especializada — pocas opciones en mercado local', + 'Balance al día, historial de pago puntual', + 'Riesgo: aumento de 22% puede motivar búsqueda de alternativas', + ], +} + +const ren005Detail: RenewalDetail = { + ...MOCK_RENEWALS[4], + claimIds: [], + parties: [ + { id: 'rp-10', role: 'insured', name: 'Dr. Alejandro Solís', company: 'Clínica San José', phone: '+507 6500-3456', email: 'asolis@clinicasanjose.com', hasUnread: false }, + { id: 'rp-11', role: 'handler', name: 'Marco V.', company: null, phone: null, email: 'marco@segur-os.com', hasUnread: false }, + ], + tasks: [ + { id: 'rt-10', title: 'Document cancellation reason', type: 'review', status: 'done', assignee: 'Marco V.', dueDate: '2026-04-02', aiGenerated: false, slaPercent: 100 }, + { id: 'rt-11', title: 'Win-back outreach — 30 day follow-up', type: 'follow_up', status: 'open', assignee: 'Marco V.', dueDate: '2026-05-01', aiGenerated: true, slaPercent: 20 }, + ], + communications: [ + { id: 'rc-10', type: 'email', direction: 'inbound', from: 'Dr. Alejandro Solís', to: 'Marco V.', subject: 'Decisión sobre renovación', body: 'Marco, lamentablemente hemos decidido no renovar con ASSA. Recibimos una oferta más competitiva de Mapfre a $38,500 con mejores términos de copago. Agradecemos el servicio durante estos 4 años.', aiDigest: 'Cliente cancela por oferta competidora de Mapfre a $38,500 (vs $45,000 actual). Menciona mejores copagos.', templateUsed: null, timestamp: '2026-03-28T11:00:00', partyRole: 'insured' }, + { id: 'rc-11', type: 'note', direction: 'internal', from: 'Marco V.', to: null, subject: null, body: 'Lost to Mapfre. Client says their copay terms are better. ASSA was unable to match. Client open to reconsidering in 12 months if terms change. Marked as recoverable.', aiDigest: null, templateUsed: null, timestamp: '2026-03-29T09:00:00', partyRole: 'handler' }, + ], + documents: [ + { id: 'rd-10', name: 'Póliza actual — HEALTH-2024-CSJ-001.pdf', category: 'current_policy', uploadedBy: 'System', uploadedAt: '2026-02-15', required: true, fulfilled: true }, + { id: 'rd-11', name: 'Cancellation email from client.pdf', category: 'cancellation', uploadedBy: 'Marco V.', uploadedAt: '2026-03-28', required: false, fulfilled: true }, + ], + comparison: null, + quotes: [], + renewalHistory: [ + { year: 2025, carrier: 'ASSA', premium: 45000, outcome: 'cancelled' }, + { year: 2024, carrier: 'ASSA', premium: 42000, outcome: 'renewed' }, + { year: 2023, carrier: 'ASSA', premium: 38000, outcome: 'renewed' }, + { year: 2022, carrier: 'ASSA', premium: 35000, outcome: 'new' }, + ], + cancellationData: { + reason: 'competitor', + reasonDetail: 'Mapfre offered better copay terms and lower premium', + competitor: 'Mapfre', + competitorPremium: 38500, + recoverable: true, + exitDate: '2026-04-01', + }, + commissionRate: 12, commissionAmount: 0, + aiRenewalBrief: 'Clínica San José — cliente corporativo de 4 años, póliza de Salud Grupal con ASSA por $45,000. Cancelada por competencia: Mapfre ofreció $38,500 con mejores copagos. Loss ratio 65% (alto). El cliente expresó apertura a reconsiderar en 12 meses. Marcado como recuperable para win-back.', + aiTalkTrack: null, + aiRetentionFactors: [ + 'Loss ratio de 65% — difícil competir en precio', + 'Mapfre ofreció $6,500 menos con mejores copagos', + 'Cliente indica apertura a reconsiderar en 12 meses', + 'Balance pendiente de $8,500 complica relación', + ], +} + +const ren002Detail: RenewalDetail = { + ...MOCK_RENEWALS[1], + claimIds: [], + parties: [ + { id: 'rp-20', role: 'insured', name: 'Ing. Ricardo Montes', company: 'Constructora Montes', phone: '+507 6800-2222', email: 'rmontes@constructoramontes.com', hasUnread: false }, + { id: 'rp-21', role: 'carrier_rep', name: 'Fernando Gil', company: 'Mapfre', phone: '+507 6600-3333', email: 'fgil@mapfre.com', hasUnread: false }, + { id: 'rp-22', role: 'handler', name: 'Ana R.', company: null, phone: null, email: 'ana@segur-os.com', hasUnread: false }, + ], + tasks: [ + { id: 'rt-20', title: 'Request renewal terms from Mapfre', type: 'review', status: 'in_progress', assignee: 'Ana R.', dueDate: '2026-04-10', aiGenerated: false, slaPercent: 55 }, + { id: 'rt-21', title: 'Review last year loss runs', type: 'review', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-12', aiGenerated: false, slaPercent: 40 }, + ], + communications: [ + { id: 'rc-20', type: 'email', direction: 'outbound', from: 'Ana R.', to: 'Fernando Gil', subject: 'Solicitud de términos — GR-2024-CM-001', body: 'Fernando, buen día. Solicitamos los términos de renovación para la póliza GR-2024-CM-001 de Constructora Montes, vigente hasta el 22 de abril. Agradecemos envío a la brevedad.', aiDigest: null, templateUsed: null, timestamp: '2026-04-01T14:00:00', partyRole: 'carrier_rep' }, + ], + documents: [ + { id: 'rd-20', name: 'Póliza actual — GR-2024-CM-001.pdf', category: 'current_policy', uploadedBy: 'System', uploadedAt: '2026-03-15', required: true, fulfilled: true }, + { id: 'rd-21', name: 'Loss runs 2024-2025.pdf', category: 'loss_runs', uploadedBy: 'Mapfre', uploadedAt: '2026-03-20', required: true, fulfilled: true }, + { id: 'rd-22', name: 'Renewal terms', category: 'renewal_terms', uploadedBy: '', uploadedAt: '', required: true, fulfilled: false }, + ], + comparison: null, + quotes: [], + renewalHistory: [ + { year: 2025, carrier: 'Mapfre', premium: 32000, outcome: 'renewed' }, + { year: 2024, carrier: 'Mapfre', premium: 29000, outcome: 'renewed' }, + { year: 2023, carrier: 'ASSA', premium: 27500, outcome: 'remarketed' }, + ], + cancellationData: null, + commissionRate: 12, commissionAmount: 3840, + aiRenewalBrief: 'Constructora Montes — cliente corporativo de 3 años, General Risk con Mapfre por $32,000. Términos de renovación aún no recibidos. Loss ratio bajo (15%). Sin reclamos abiertos. Buena relación con carrier. Se espera renovación sin complicaciones pero se necesitan los términos antes del 15 de abril para cumplir SLA.', + aiTalkTrack: null, + aiRetentionFactors: [ + 'Loss ratio de 15% — excelente para la línea', + 'Sin reclamos abiertos', + '3 años como cliente, relación estable', + 'Riesgo moderado: constructora puede buscar cotizaciones competitivas', + ], +} + +const ren003Detail: RenewalDetail = { + ...MOCK_RENEWALS[2], + claimIds: [], + parties: [ + { id: 'rp-30', role: 'insured', name: 'Carmen Ruiz', company: null, phone: '+507 6400-5555', email: 'carmen.ruiz@gmail.com', hasUnread: false }, + { id: 'rp-31', role: 'handler', name: 'Ana R.', company: null, phone: null, email: 'ana@segur-os.com', hasUnread: false }, + ], + tasks: [ + { id: 'rt-30', title: 'Collect payment — client confirmed renewal', type: 'collect_payment', status: 'open', assignee: 'Ana R.', dueDate: '2026-04-12', aiGenerated: false, slaPercent: 30 }, + ], + communications: [ + { id: 'rc-30', type: 'email', direction: 'outbound', from: 'Ana R.', to: 'Carmen Ruiz', subject: 'Confirmación de pago — LIFE-2024-CR-001', body: 'Estimada Carmen, gracias por confirmar la renovación de su póliza de vida. La prima para el nuevo período es de $2,940. Por favor realice la transferencia para completar el proceso.', aiDigest: null, templateUsed: 'tpl-5', timestamp: '2026-04-06T10:00:00', partyRole: 'insured' }, + { id: 'rc-31', type: 'email', direction: 'inbound', from: 'Carmen Ruiz', to: 'Ana R.', subject: 'RE: Renovación vida', body: 'Ana, confirmo que deseo renovar. Haré la transferencia esta semana.', aiDigest: 'Cliente confirma renovación, promete pago esta semana.', templateUsed: null, timestamp: '2026-04-05T16:30:00', partyRole: 'insured' }, + ], + documents: [ + { id: 'rd-30', name: 'Póliza actual — LIFE-2024-CR-001.pdf', category: 'current_policy', uploadedBy: 'System', uploadedAt: '2026-03-01', required: true, fulfilled: true }, + { id: 'rd-31', name: 'Términos Pan-American Life 2026.pdf', category: 'renewal_terms', uploadedBy: 'Pan-American Life', uploadedAt: '2026-03-25', required: true, fulfilled: true }, + { id: 'rd-32', name: 'Propuesta enviada.pdf', category: 'proposal_sent', uploadedBy: 'Ana R.', uploadedAt: '2026-03-28', required: true, fulfilled: true }, + { id: 'rd-33', name: 'Client confirmation email.pdf', category: 'client_confirmation', uploadedBy: 'Ana R.', uploadedAt: '2026-04-05', required: true, fulfilled: true }, + { id: 'rd-34', name: 'Payment receipt', category: 'payment_receipt', uploadedBy: '', uploadedAt: '', required: true, fulfilled: false }, + ], + comparison: { + currentPremium: 2800, renewalPremium: 2940, premiumDelta: 5, + currentDeductible: 0, renewalDeductible: 0, deductibleDelta: 0, + coverageLines: [ + { name: 'Vida Individual', currentAmount: 100000, renewalAmount: 100000, delta: '=', flag: 'same' }, + { name: 'Muerte Accidental', currentAmount: 50000, renewalAmount: 50000, delta: '=', flag: 'same' }, + { name: 'Incapacidad Total', currentAmount: 100000, renewalAmount: 100000, delta: '=', flag: 'same' }, + ], + aiAnalysis: 'Aumento de 5% es estándar para ajuste de edad en seguro de vida. Coberturas sin cambio. Renovación rutinaria sin complicaciones.', + }, + quotes: [], + renewalHistory: [ + { year: 2025, carrier: 'Pan-American Life', premium: 2800, outcome: 'renewed' }, + { year: 2024, carrier: 'Pan-American Life', premium: 2650, outcome: 'renewed' }, + { year: 2023, carrier: 'Pan-American Life', premium: 2500, outcome: 'renewed' }, + { year: 2022, carrier: 'Pan-American Life', premium: 2350, outcome: 'renewed' }, + { year: 2021, carrier: 'Pan-American Life', premium: 2200, outcome: 'renewed' }, + { year: 2020, carrier: 'Pan-American Life', premium: 2050, outcome: 'renewed' }, + { year: 2019, carrier: 'Pan-American Life', premium: 1900, outcome: 'renewed' }, + { year: 2018, carrier: 'Pan-American Life', premium: 1750, outcome: 'new' }, + ], + cancellationData: null, + commissionRate: 20, commissionAmount: 588, + aiRenewalBrief: 'Carmen Ruiz — cliente individual de 8 años, póliza de Vida con Pan-American Life. Aumento de 5% por ajuste de edad, estándar. Cliente confirmó renovación, pendiente cobro de $2,940. Excelente historial de retención. Sin reclamos.', + aiTalkTrack: null, + aiRetentionFactors: ['8 años como cliente — altamente leal', 'Sin reclamos, loss ratio 0%', 'Aumento de 5% es mínimo y esperado'], +} + +const ren004Detail: RenewalDetail = { + ...MOCK_RENEWALS[3], + claimIds: [], + parties: [ + { id: 'rp-40', role: 'insured', name: 'Lic. Patricia Vega', company: 'Empresa ABC S.A.', phone: '+507 6300-7777', email: 'pvega@empresaabc.com', hasUnread: false }, + ], + tasks: [ + { id: 'rt-40', title: 'Assign handler and begin review', type: 'review', status: 'open', assignee: 'Unassigned', dueDate: '2026-04-15', aiGenerated: true, slaPercent: 10 }, + ], + communications: [], + documents: [ + { id: 'rd-40', name: 'Póliza actual — AUTO-2024-ABC-001.pdf', category: 'current_policy', uploadedBy: 'System', uploadedAt: '2026-04-01', required: true, fulfilled: true }, + ], + comparison: null, + quotes: [], + renewalHistory: [ + { year: 2025, carrier: 'Qualitas', premium: 22400, outcome: 'renewed' }, + { year: 2024, carrier: 'Qualitas', premium: 21000, outcome: 'new' }, + ], + cancellationData: null, + commissionRate: 15, commissionAmount: 3360, + aiRenewalBrief: 'Empresa ABC S.A. — flota Auto con Qualitas por $22,400. Renovación sin asignar, 30 días hasta vencimiento. Sin reclamos, loss ratio 8%. Necesita asignación inmediata para cumplir SLA.', + aiTalkTrack: null, + aiRetentionFactors: ['Loss ratio bajo (8%)', '2 años como cliente', 'Sin complicaciones previas'], +} + +const ren006Detail: RenewalDetail = { + ...MOCK_RENEWALS[5], + claimIds: ['CLM-0043'], + parties: [ + { id: 'rp-60', role: 'insured', name: 'Gerardo Núñez', company: 'Transportes del Norte', phone: '+507 6200-8888', email: 'gnunez@transportesnorte.com', hasUnread: true }, + ], + tasks: [ + { id: 'rt-60', title: 'Assign handler — high-risk renewal', type: 'review', status: 'open', assignee: 'Unassigned', dueDate: '2026-04-20', aiGenerated: true, slaPercent: 15 }, + { id: 'rt-61', title: 'Address $21K outstanding balance before renewal', type: 'escalation', status: 'open', assignee: 'Unassigned', dueDate: '2026-04-18', aiGenerated: true, slaPercent: 20 }, + ], + communications: [ + { id: 'rc-60', type: 'system', direction: 'internal', from: 'System', to: null, subject: null, body: 'AI alert: Transportes del Norte has $21,000 outstanding balance and an open claim (CLM-0043). This renewal requires early attention due to high retention risk.', aiDigest: null, templateUsed: null, timestamp: '2026-04-08T08:00:00', partyRole: 'ai_agent' }, + ], + documents: [ + { id: 'rd-60', name: 'Póliza actual — GR-2024-TN-001.pdf', category: 'current_policy', uploadedBy: 'System', uploadedAt: '2026-03-15', required: true, fulfilled: true }, + ], + comparison: null, + quotes: [], + renewalHistory: [ + { year: 2025, carrier: 'ASSA', premium: 38000, outcome: 'renewed' }, + { year: 2024, carrier: 'ASSA', premium: 35000, outcome: 'renewed' }, + { year: 2023, carrier: 'ASSA', premium: 32000, outcome: 'renewed' }, + { year: 2022, carrier: 'ASSA', premium: 28000, outcome: 'renewed' }, + { year: 2021, carrier: 'ASSA', premium: 25000, outcome: 'renewed' }, + { year: 2020, carrier: 'ASSA', premium: 22000, outcome: 'new' }, + ], + cancellationData: null, + commissionRate: 12, commissionAmount: 4560, + aiRenewalBrief: 'Transportes del Norte — cliente corporativo de 6 años, General Risk con ASSA por $38,000. ALERTA: balance pendiente de $21,000 y reclamo abierto CLM-0043 ($45K reservado). Loss ratio de 55% dificulta la negociación. Necesita atención temprana a pesar de tener 60 días hasta vencimiento. Comisión en riesgo: $4,560.', + aiTalkTrack: [ + 'Primero resolver el tema del balance pendiente de $21K antes de hablar de renovación.', + 'Reconocer que es un cliente de 6 años con historial generalmente positivo.', + 'El reclamo abierto CLM-0043 impactará los términos — preparar al cliente para posible aumento.', + 'Proponer plan de pago para el balance pendiente como condición para negociar mejores términos de renovación.', + 'Si ASSA sube prima significativamente, tener plan B de remarketing con Mapfre.', + ], + aiRetentionFactors: [ + '6 años como cliente — relación de largo plazo', + 'Balance pendiente de $21K — señal de alerta', + 'Reclamo abierto CLM-0043 por $45K reservado', + 'Loss ratio 55% — por encima del promedio', + 'Comisión de $4,560 en riesgo', + 'Flota de transporte — pocas opciones en mercado local', + ], +} + +export const MOCK_RENEWAL_DETAILS: Record = { + 'REN-001': ren001Detail, + 'REN-002': ren002Detail, + 'REN-003': ren003Detail, + 'REN-004': ren004Detail, + 'REN-005': ren005Detail, + 'REN-006': ren006Detail, +} diff --git a/app/data/mock-support.ts b/app/data/mock-support.ts new file mode 100644 index 0000000..a59104e --- /dev/null +++ b/app/data/mock-support.ts @@ -0,0 +1,699 @@ +// ─── Support Ticket System — Types, Labels, Helpers, Mock Data ─────────────── + +// ── Channel ── +export type SupportChannel = 'whatsapp' | 'email' | 'phone' | 'walk_in' | 'web_form' + +// ── Routing Tier ── +export type RoutingTier = 'tier1_auto' | 'tier2_rule' | 'tier3_open' + +// ── Ticket Status ── +export type TicketStatus = 'open' | 'in_progress' | 'pending_customer' | 'resolved' + +// ── Priority ── +export type TicketPriority = 'urgent' | 'high' | 'medium' | 'low' + +// ── Intent Category (LLM classification) ── +export type IntentCategory = + | 'payment_inquiry' + | 'claim_report' + | 'sales_interest' + | 'doc_request' + | 'policy_question' + | 'complaint' + | 'endorsement' + | 'certificate_request' + | 'general' + +// ── Routed Queue ── +export type RoutedQueue = 'collections' | 'claims' | 'sales' | 'renewals' | 'operations' | 'open_pool' + +// ── Message Types ── +export type MessageType = 'whatsapp' | 'email' | 'phone_note' | 'internal_note' | 'system' +export type MessageDirection = 'inbound' | 'outbound' | 'internal' + +// ── Routing Rule Type ── +export type RoutingRuleType = 'customer_match' | 'lob_specialist' | 'keyword_intent' | 'doc_auto_fulfill' + +// ─── Interfaces ────────────────────────────────────────────────────────────── + +export interface TicketMessage { + id: string + type: MessageType + direction: MessageDirection + from: string + to: string | null + subject: string | null + body: string + timestamp: string + aiDigest: string | null + deliveryStatus?: 'sent' | 'delivered' | 'read' +} + +export interface SupportTicket { + id: string + subject: string + channel: SupportChannel + status: TicketStatus + priority: TicketPriority + intentCategory: IntentCategory + routingTier: RoutingTier + routedQueue: RoutedQueue + customerId: string | null + customerName: string + policyId: string | null + policyNumber: string | null + assignedTo: string | null + createdAt: string + updatedAt: string + daysOpen: number + slaPercent: number + messageCount: number + lastMessagePreview: string +} + +export interface SupportTicketDetail extends SupportTicket { + customerEmail: string | null + customerPhone: string | null + linkedPolicies: { id: string; number: string; carrier: string; lob: string; status: string; renewal: string }[] + linkedClaims: { id: string; type: string; status: string }[] + messages: TicketMessage[] + aiSummary: string + aiSuggestedIntent: IntentCategory + aiConfidence: number + routingTrace: { step: string; result: string; timestamp: string }[] +} + +export interface RoutingRule { + id: string + tier: RoutingTier + type: RoutingRuleType + name: string + condition: string + targetQueue: RoutedQueue + targetAgent: string | null + enabled: boolean + priority: number +} + +// ─── Label Maps ────────────────────────────────────────────────────────────── + +export const CHANNEL_LABELS: Record = { + whatsapp: 'WhatsApp', + email: 'Email', + phone: 'Teléfono', + walk_in: 'Presencial', + web_form: 'Formulario Web', +} + +export const CHANNEL_ICONS: Record = { + whatsapp: 'i-heroicons-chat-bubble-left-ellipsis', + email: 'i-heroicons-envelope', + phone: 'i-heroicons-phone', + walk_in: 'i-heroicons-building-storefront', + web_form: 'i-heroicons-globe-alt', +} + +export const STATUS_LABELS: Record = { + open: 'Abierto', + in_progress: 'En Proceso', + pending_customer: 'Esperando Cliente', + resolved: 'Resuelto', +} + +export const PRIORITY_LABELS: Record = { + urgent: 'Urgente', + high: 'Alta', + medium: 'Media', + low: 'Baja', +} + +export const INTENT_LABELS: Record = { + payment_inquiry: 'Consulta de Pago', + claim_report: 'Reporte de Siniestro', + sales_interest: 'Interés en Seguros', + doc_request: 'Solicitud de Documento', + policy_question: 'Consulta de Póliza', + complaint: 'Queja', + endorsement: 'Endoso', + certificate_request: 'Certificado', + general: 'General', +} + +export const TIER_LABELS: Record = { + tier1_auto: 'Tier 1 — Auto', + tier2_rule: 'Tier 2 — Regla', + tier3_open: 'Tier 3 — Pool', +} + +export const QUEUE_LABELS: Record = { + collections: 'Cobranza', + claims: 'Siniestros', + sales: 'Ventas', + renewals: 'Renovaciones', + operations: 'Operaciones', + open_pool: 'Pool Abierto', +} + +export const MESSAGE_TYPE_LABELS: Record = { + whatsapp: 'WhatsApp', + email: 'Email', + phone_note: 'Nota de Llamada', + internal_note: 'Nota Interna', + system: 'Sistema', +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +export function slaColor(percent: number): 'green' | 'amber' | 'red' { + if (percent >= 100) return 'red' + if (percent >= 75) return 'amber' + return 'green' +} + +export function channelIcon(ch: SupportChannel): string { + return CHANNEL_ICONS[ch] ?? 'i-heroicons-question-mark-circle' +} + +export function tierBadgeClass(tier: RoutingTier): string { + switch (tier) { + case 'tier1_auto': return 'sp-tier-1' + case 'tier2_rule': return 'sp-tier-2' + case 'tier3_open': return 'sp-tier-3' + default: return '' + } +} + +export function fmtDate(d: string): string { + return new Date(d).toLocaleDateString('es-PA', { month: 'short', day: 'numeric', year: 'numeric' }) +} + +export function fmtTime(d: string): string { + return new Date(d).toLocaleTimeString('es-PA', { hour: '2-digit', minute: '2-digit' }) +} + +export function fmtDateTime(d: string): string { + return `${fmtDate(d)} ${fmtTime(d)}` +} + +// ─── Mock Tickets (List) ───────────────────────────────────────────────────── + +export const MOCK_SUPPORT_TICKETS: SupportTicket[] = [ + { + id: 'SP-001', + subject: 'Consulta sobre pago pendiente de póliza auto', + channel: 'whatsapp', + status: 'resolved', + priority: 'medium', + intentCategory: 'payment_inquiry', + routingTier: 'tier2_rule', + routedQueue: 'collections', + customerId: 'cust-001', + customerName: 'María Elena Pérez Solano', + policyId: 'POL-2024-4412', + policyNumber: 'AUTO-2024-4412', + assignedTo: 'Carlos Villalba', + createdAt: '2026-04-01T09:15:00', + updatedAt: '2026-04-01T14:30:00', + daysOpen: 0, + slaPercent: 45, + messageCount: 6, + lastMessagePreview: 'Perfecto, ya quedó registrado el pago. Gracias María Elena.', + }, + { + id: 'SP-002', + subject: 'Reporte de accidente vehicular — Toyota RAV4', + channel: 'email', + status: 'in_progress', + priority: 'high', + intentCategory: 'claim_report', + routingTier: 'tier2_rule', + routedQueue: 'claims', + customerId: 'cust-004', + customerName: 'Luis Andrés Solís Calderón', + policyId: 'POL-2022-2200', + policyNumber: 'AUTO-2022-2200', + assignedTo: 'María Fernanda Ortiz', + createdAt: '2026-04-03T11:22:00', + updatedAt: '2026-04-07T16:45:00', + daysOpen: 5, + slaPercent: 62, + messageCount: 5, + lastMessagePreview: 'Adjunto las fotos del daño al vehículo tomadas esta mañana.', + }, + { + id: 'SP-003', + subject: 'Solicitud de certificado de seguro para banco', + channel: 'walk_in', + status: 'resolved', + priority: 'low', + intentCategory: 'certificate_request', + routingTier: 'tier1_auto', + routedQueue: 'operations', + customerId: 'cust-002', + customerName: 'Roberto Jiménez Mora', + policyId: 'POL-2023-3301', + policyNumber: 'AUTO-2023-3301', + assignedTo: 'Ana R.', + createdAt: '2026-04-04T10:00:00', + updatedAt: '2026-04-04T10:35:00', + daysOpen: 0, + slaPercent: 20, + messageCount: 3, + lastMessagePreview: 'Certificado generado y entregado en mano.', + }, + { + id: 'SP-004', + subject: 'Cotización seguro auto para BMW X3 2025', + channel: 'whatsapp', + status: 'open', + priority: 'medium', + intentCategory: 'sales_interest', + routingTier: 'tier2_rule', + routedQueue: 'sales', + customerId: null, + customerName: 'Número desconocido (+507 6789-0123)', + policyId: null, + policyNumber: null, + assignedTo: 'María Fernanda Ortiz', + createdAt: '2026-04-07T08:30:00', + updatedAt: '2026-04-07T08:30:00', + daysOpen: 1, + slaPercent: 35, + messageCount: 3, + lastMessagePreview: 'Buenas, cuánto sale un seguro todo riesgo para un BMW X3 2025?', + }, + { + id: 'SP-005', + subject: 'Queja por demora en proceso de siniestro CLM-0048', + channel: 'email', + status: 'pending_customer', + priority: 'high', + intentCategory: 'complaint', + routingTier: 'tier3_open', + routedQueue: 'open_pool', + customerId: 'cust-001', + customerName: 'María Elena Pérez Solano', + policyId: 'POL-2024-4412', + policyNumber: 'AUTO-2024-4412', + assignedTo: 'Carlos Villalba', + createdAt: '2026-04-02T15:00:00', + updatedAt: '2026-04-06T09:20:00', + daysOpen: 6, + slaPercent: 88, + messageCount: 7, + lastMessagePreview: 'Estamos pendientes de su respuesta con los documentos adicionales.', + }, + { + id: 'SP-006', + subject: 'Consulta plan salud colectivo — Grupo Agrícola del Sur', + channel: 'web_form', + status: 'in_progress', + priority: 'medium', + intentCategory: 'policy_question', + routingTier: 'tier1_auto', + routedQueue: 'operations', + customerId: 'cust-010', + customerName: 'Fernando Arias Blanco', + policyId: null, + policyNumber: null, + assignedTo: 'Ana R.', + createdAt: '2026-04-06T14:10:00', + updatedAt: '2026-04-07T11:00:00', + daysOpen: 2, + slaPercent: 50, + messageCount: 4, + lastMessagePreview: 'Le envío las opciones de cobertura para su grupo.', + }, + { + id: 'SP-007', + subject: 'Mensaje ambiguo — "necesito ayuda con mi seguro"', + channel: 'whatsapp', + status: 'open', + priority: 'medium', + intentCategory: 'general', + routingTier: 'tier3_open', + routedQueue: 'open_pool', + customerId: null, + customerName: 'Número desconocido (+507 6234-5678)', + policyId: null, + policyNumber: null, + assignedTo: null, + createdAt: '2026-04-08T07:45:00', + updatedAt: '2026-04-08T07:45:00', + daysOpen: 0, + slaPercent: 15, + messageCount: 1, + lastMessagePreview: 'Hola buenos días, necesito ayuda con mi seguro, me pueden atender?', + }, + { + id: 'SP-008', + subject: 'Endoso — agregar conductor a póliza auto', + channel: 'email', + status: 'in_progress', + priority: 'medium', + intentCategory: 'endorsement', + routingTier: 'tier2_rule', + routedQueue: 'operations', + customerId: 'cust-004', + customerName: 'Luis Andrés Solís Calderón', + policyId: 'POL-2022-2201', + policyNumber: 'AUTO-2022-2201', + assignedTo: 'Ana R.', + createdAt: '2026-04-05T09:00:00', + updatedAt: '2026-04-07T15:30:00', + daysOpen: 3, + slaPercent: 55, + messageCount: 5, + lastMessagePreview: 'Necesitamos la cédula y licencia del conductor adicional.', + }, + { + id: 'SP-009', + subject: 'URGENTE — Choque frontal Toyota Hilux, requiere grúa', + channel: 'whatsapp', + status: 'open', + priority: 'urgent', + intentCategory: 'claim_report', + routingTier: 'tier1_auto', + routedQueue: 'claims', + customerId: 'cust-010', + customerName: 'Fernando Arias Blanco', + policyId: 'POL-2020-1100', + policyNumber: 'AUTO-2020-1100', + assignedTo: 'Carlos Villalba', + createdAt: '2026-04-08T06:15:00', + updatedAt: '2026-04-08T06:45:00', + daysOpen: 0, + slaPercent: 40, + messageCount: 4, + lastMessagePreview: 'Ya coordiné la grúa, llega en 20 minutos a tu ubicación.', + }, + { + id: 'SP-010', + subject: 'Seguimiento renovación póliza vida — vence 30 abril', + channel: 'phone', + status: 'resolved', + priority: 'low', + intentCategory: 'policy_question', + routingTier: 'tier2_rule', + routedQueue: 'renewals', + customerId: 'cust-002', + customerName: 'Roberto Jiménez Mora', + policyId: 'POL-2023-3301', + policyNumber: 'AUTO-2023-3301', + assignedTo: 'María Fernanda Ortiz', + createdAt: '2026-04-03T16:00:00', + updatedAt: '2026-04-04T09:00:00', + daysOpen: 0, + slaPercent: 30, + messageCount: 3, + lastMessagePreview: 'Cliente confirmó renovación. Proceso completado.', + }, +] + +// ─── Mock Ticket Details ───────────────────────────────────────────────────── + +const sp001Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[0], + customerEmail: 'maria.perez@gmail.com', + customerPhone: '+507 6123-4567', + linkedPolicies: [ + { id: 'POL-2024-4412', number: 'AUTO-2024-4412', carrier: 'ASSA', lob: 'Auto', status: 'Active', renewal: '2027-03-15' }, + ], + linkedClaims: [], + messages: [ + { id: 'msg-001-1', type: 'whatsapp', direction: 'inbound', from: 'María Elena Pérez', to: null, subject: null, body: 'Hola buenas, tengo una consulta sobre mi pago. Me llegó un recibo pero ya hice la transferencia la semana pasada. Pueden verificar?', timestamp: '2026-04-01T09:15:00', aiDigest: 'Customer inquiring about payment status — claims transfer was already made.' }, + { id: 'msg-001-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Ticket creado automáticamente. Intent detectado: Consulta de Pago (92% confianza). Enrutado a Cobranza.', timestamp: '2026-04-01T09:15:05', aiDigest: null }, + { id: 'msg-001-3', type: 'whatsapp', direction: 'outbound', from: 'Carlos Villalba', to: 'María Elena Pérez', subject: null, body: 'Buenos días María Elena! Déjame verificar con el departamento de cobranza. ¿Me puede compartir el comprobante de la transferencia?', timestamp: '2026-04-01T09:45:00', aiDigest: null, deliveryStatus: 'read' }, + { id: 'msg-001-4', type: 'whatsapp', direction: 'inbound', from: 'María Elena Pérez', to: null, subject: null, body: 'Claro, aquí está el comprobante. Fue el 25 de marzo por $460.', timestamp: '2026-04-01T10:02:00', aiDigest: null }, + { id: 'msg-001-5', type: 'internal_note', direction: 'internal', from: 'Carlos Villalba', to: null, subject: null, body: 'Verificado con contabilidad. Pago recibido el 26/03 — recibo cruzado con período anterior. Se ajusta en sistema.', timestamp: '2026-04-01T13:00:00', aiDigest: null }, + { id: 'msg-001-6', type: 'whatsapp', direction: 'outbound', from: 'Carlos Villalba', to: 'María Elena Pérez', subject: null, body: 'Perfecto, ya quedó registrado el pago. Gracias María Elena. El recibo que le llegó era del mes anterior, ya se corrigió. Cualquier cosa me avisa!', timestamp: '2026-04-01T14:30:00', aiDigest: null, deliveryStatus: 'read' }, + ], + aiSummary: 'Cliente consultó sobre un pago que ya había realizado. Se verificó con contabilidad que la transferencia fue recibida y el recibo cruzado con un período anterior fue corregido. Ticket resuelto satisfactoriamente.', + aiSuggestedIntent: 'payment_inquiry', + aiConfidence: 0.92, + routingTrace: [ + { step: 'Channel Detection', result: 'WhatsApp — número registrado: +507 6123-4567', timestamp: '2026-04-01T09:15:01' }, + { step: 'Customer Match', result: 'Matched: María Elena Pérez Solano (cust-001)', timestamp: '2026-04-01T09:15:02' }, + { step: 'Intent Classification', result: 'payment_inquiry (92% confidence) — keywords: "pago", "recibo", "transferencia"', timestamp: '2026-04-01T09:15:03' }, + { step: 'Rule Fired', result: 'Payment intent → Collections queue', timestamp: '2026-04-01T09:15:04' }, + { step: 'Agent Assignment', result: 'Carlos Villalba (assigned broker for cust-001)', timestamp: '2026-04-01T09:15:05' }, + ], +} + +const sp002Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[1], + customerEmail: 'luis.solis@outlook.com', + customerPhone: '+507 6555-1234', + linkedPolicies: [ + { id: 'POL-2022-2200', number: 'AUTO-2022-2200', carrier: 'Qualitas', lob: 'Auto', status: 'Active', renewal: '2027-01-15' }, + { id: 'POL-2022-2201', number: 'AUTO-2022-2201', carrier: 'Qualitas', lob: 'Auto', status: 'Active', renewal: '2027-01-15' }, + ], + linkedClaims: [ + { id: 'CLM-0048', type: 'Collision', status: 'Under Review' }, + ], + messages: [ + { id: 'msg-002-1', type: 'email', direction: 'inbound', from: 'luis.solis@outlook.com', to: 'soporte@seguros.com', subject: 'Reporte de accidente — BMW X5 SJO-2200', body: 'Estimados, les informo que tuve un accidente vehicular el día de hoy en la autopista Próspero Fernández. Mi vehículo BMW X5 placas SJO-2200 sufrió daños frontales tras una colisión. Adjunto fotos del daño. Por favor indiquen los pasos a seguir para el reclamo.', timestamp: '2026-04-03T11:22:00', aiDigest: 'Customer reporting vehicle collision on Próspero Fernández highway. BMW X5, front damage. Requesting claim process guidance.' }, + { id: 'msg-002-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Ticket creado. Intent: claim_report (97%). Enrutado a Siniestros. Claim CLM-0048 vinculado automáticamente.', timestamp: '2026-04-03T11:22:10', aiDigest: null }, + { id: 'msg-002-3', type: 'email', direction: 'outbound', from: 'maria.ortiz@seguros.com', to: 'luis.solis@outlook.com', subject: 'Re: Reporte de accidente — BMW X5 SJO-2200', body: 'Estimado Luis, lamentamos mucho el accidente. Ya creamos su reclamo bajo el número CLM-0048. Necesitamos:\n\n1. Parte policial (si aplica)\n2. Fotos adicionales del otro vehículo\n3. Datos del otro conductor\n\nQuedo atenta.', timestamp: '2026-04-03T14:00:00', aiDigest: null }, + { id: 'msg-002-4', type: 'internal_note', direction: 'internal', from: 'María Fernanda Ortiz', to: null, subject: null, body: 'Cliente VIP — 2 pólizas activas, $5,800/yr premium. Priorizar este caso. Contactar ajustador para inspección rápida.', timestamp: '2026-04-04T08:30:00', aiDigest: null }, + { id: 'msg-002-5', type: 'email', direction: 'inbound', from: 'luis.solis@outlook.com', to: 'soporte@seguros.com', subject: 'Re: Reporte de accidente — BMW X5 SJO-2200', body: 'Adjunto las fotos del daño al vehículo tomadas esta mañana. El parte policial lo tengo listo para mañana. El otro conductor no quiso dar sus datos, pero tengo la placa.', timestamp: '2026-04-07T16:45:00', aiDigest: null }, + ], + aiSummary: 'Accidente vehicular reportado por correo. BMW X5, daños frontales por colisión en autopista. Claim CLM-0048 creado y vinculado. Pendiente: parte policial y datos del otro conductor. Cliente VIP con 2 pólizas — priorizar.', + aiSuggestedIntent: 'claim_report', + aiConfidence: 0.97, + routingTrace: [ + { step: 'Channel Detection', result: 'Email — luis.solis@outlook.com', timestamp: '2026-04-03T11:22:01' }, + { step: 'Customer Match', result: 'Matched: Luis Andrés Solís Calderón (cust-004)', timestamp: '2026-04-03T11:22:02' }, + { step: 'Intent Classification', result: 'claim_report (97%) — keywords: "accidente", "daños", "colisión", "reclamo"', timestamp: '2026-04-03T11:22:03' }, + { step: 'Rule Fired', result: 'Claim intent → Claims queue', timestamp: '2026-04-03T11:22:04' }, + { step: 'Policy Link', result: 'Auto-linked POL-2022-2200 (BMW X5, matching plate SJO-2200)', timestamp: '2026-04-03T11:22:05' }, + ], +} + +const sp003Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[2], + customerEmail: 'roberto.jimenez@gmail.com', + customerPhone: '+507 6222-8899', + linkedPolicies: [ + { id: 'POL-2023-3301', number: 'AUTO-2023-3301', carrier: 'Qualitas', lob: 'Auto', status: 'Active', renewal: '2027-04-01' }, + ], + linkedClaims: [], + messages: [ + { id: 'msg-003-1', type: 'phone_note', direction: 'inbound', from: 'Roberto Jiménez Mora', to: null, subject: null, body: 'Cliente se presentó en oficina solicitando certificado de seguro vigente para trámite bancario. Necesita documento hoy.', timestamp: '2026-04-04T10:00:00', aiDigest: null }, + { id: 'msg-003-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Cliente conocido (cust-002). Tier 1 auto-route: solicitud de certificado → Operaciones. Auto-fulfill iniciado.', timestamp: '2026-04-04T10:00:05', aiDigest: null }, + { id: 'msg-003-3', type: 'internal_note', direction: 'internal', from: 'Ana R.', to: null, subject: null, body: 'Certificado generado y entregado en mano. Cliente satisfecho. Ticket cerrado.', timestamp: '2026-04-04T10:35:00', aiDigest: null }, + ], + aiSummary: 'Solicitud de certificado de seguro vigente para trámite bancario. Cliente conocido, auto-enrutado a operaciones. Certificado generado y entregado presencialmente en 35 minutos.', + aiSuggestedIntent: 'certificate_request', + aiConfidence: 0.99, + routingTrace: [ + { step: 'Walk-in Registration', result: 'Registered by front desk', timestamp: '2026-04-04T10:00:01' }, + { step: 'Customer Match', result: 'Matched: Roberto Jiménez Mora (cust-002)', timestamp: '2026-04-04T10:00:02' }, + { step: 'Intent Classification', result: 'certificate_request (99%) — direct request', timestamp: '2026-04-04T10:00:03' }, + { step: 'Tier 1 Auto-Fulfill', result: 'Certificate generation triggered for POL-2023-3301', timestamp: '2026-04-04T10:00:05' }, + ], +} + +const sp004Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[3], + customerEmail: null, + customerPhone: '+507 6789-0123', + linkedPolicies: [], + linkedClaims: [], + messages: [ + { id: 'msg-004-1', type: 'whatsapp', direction: 'inbound', from: '+507 6789-0123', to: null, subject: null, body: 'Buenas, cuánto sale un seguro todo riesgo para un BMW X3 2025? Nuevo, recién importado.', timestamp: '2026-04-07T08:30:00', aiDigest: 'Unknown number asking for comprehensive auto insurance quote for new BMW X3 2025.' }, + { id: 'msg-004-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Número no registrado. Intent: sales_interest (89%). Enrutado a Ventas.', timestamp: '2026-04-07T08:30:05', aiDigest: null }, + { id: 'msg-004-3', type: 'whatsapp', direction: 'outbound', from: 'María Fernanda Ortiz', to: '+507 6789-0123', subject: null, body: 'Buenos días! Con gusto le cotizamos. Para darle las mejores opciones necesito:\n\n1. Nombre completo\n2. Cédula o pasaporte\n3. Valor del vehículo según factura\n4. Uso (particular o comercial)\n\nQuedo atenta!', timestamp: '2026-04-07T09:15:00', aiDigest: null, deliveryStatus: 'delivered' }, + ], + aiSummary: 'Lead entrante vía WhatsApp — persona desconocida consultando precio de seguro todo riesgo para BMW X3 2025 nuevo. Asignado a ventas. Pendiente respuesta del prospecto con datos personales.', + aiSuggestedIntent: 'sales_interest', + aiConfidence: 0.89, + routingTrace: [ + { step: 'Channel Detection', result: 'WhatsApp — +507 6789-0123', timestamp: '2026-04-07T08:30:01' }, + { step: 'Customer Match', result: 'No match — unknown number', timestamp: '2026-04-07T08:30:02' }, + { step: 'Intent Classification', result: 'sales_interest (89%) — keywords: "cuánto sale", "seguro todo riesgo", "nuevo"', timestamp: '2026-04-07T08:30:03' }, + { step: 'Rule Fired', result: 'Sales intent → Sales queue', timestamp: '2026-04-07T08:30:04' }, + ], +} + +const sp005Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[4], + customerEmail: 'maria.perez@gmail.com', + customerPhone: '+507 6123-4567', + linkedPolicies: [ + { id: 'POL-2024-4412', number: 'AUTO-2024-4412', carrier: 'ASSA', lob: 'Auto', status: 'Active', renewal: '2027-03-15' }, + ], + linkedClaims: [ + { id: 'CLM-0048', type: 'Collision', status: 'Under Review' }, + ], + messages: [ + { id: 'msg-005-1', type: 'email', direction: 'inbound', from: 'maria.perez@gmail.com', to: 'soporte@seguros.com', subject: 'QUEJA — demora inaceptable en siniestro CLM-0048', body: 'Estimados, llevo más de 3 semanas esperando respuesta sobre mi siniestro CLM-0048. He llamado varias veces y nadie me da una respuesta clara. Esto es inaceptable. Necesito que alguien con autoridad me contacte hoy mismo o procederé con una queja formal ante la Superintendencia.', timestamp: '2026-04-02T15:00:00', aiDigest: 'Frustrated customer filing formal complaint about claim CLM-0048 delays. Threatening regulatory escalation.' }, + { id: 'msg-005-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Intent ambiguo: complaint (76%) / claim_report (18%). No cumple regla de enrutamiento directo. Enviado a Pool Abierto.', timestamp: '2026-04-02T15:00:05', aiDigest: null }, + { id: 'msg-005-3', type: 'internal_note', direction: 'internal', from: 'Carlos Villalba', to: null, subject: null, body: 'Asigno este ticket a mí. Conozco a la clienta — tiene razón, el claim lleva demasiado tiempo. Voy a escalar con el ajustador de ASSA directamente.', timestamp: '2026-04-02T16:30:00', aiDigest: null }, + { id: 'msg-005-4', type: 'email', direction: 'outbound', from: 'carlos.villalba@seguros.com', to: 'maria.perez@gmail.com', subject: 'Re: QUEJA — demora inaceptable en siniestro CLM-0048', body: 'Estimada María Elena, le pido disculpas sinceras por la demora. He escalado personalmente su caso con el ajustador de ASSA y tenemos reunión mañana a primera hora. Le daré un update antes del mediodía del viernes. Entiendo su frustración y estoy comprometido a resolverlo.', timestamp: '2026-04-02T17:00:00', aiDigest: null }, + { id: 'msg-005-5', type: 'internal_note', direction: 'internal', from: 'Carlos Villalba', to: null, subject: null, body: 'Reunión con ajustador ASSA — dicen que falta un documento del taller. Ya lo solicité. Debería llegar mañana.', timestamp: '2026-04-04T10:00:00', aiDigest: null }, + { id: 'msg-005-6', type: 'email', direction: 'outbound', from: 'carlos.villalba@seguros.com', to: 'maria.perez@gmail.com', subject: 'Re: QUEJA — Actualización siniestro CLM-0048', body: 'María Elena, actualización: ASSA necesita un documento adicional del taller donde se hizo la reparación. ¿Podría solicitarle al taller el presupuesto final firmado y sellado? Con eso podemos cerrar la revisión esta semana.', timestamp: '2026-04-04T14:00:00', aiDigest: null }, + { id: 'msg-005-7', type: 'email', direction: 'outbound', from: 'carlos.villalba@seguros.com', to: 'maria.perez@gmail.com', subject: 'Re: QUEJA — Seguimiento documentos pendientes', body: 'Hola María Elena, solo un seguimiento amable — estamos pendientes de su respuesta con los documentos adicionales del taller para avanzar con ASSA. Quedo a la orden.', timestamp: '2026-04-06T09:20:00', aiDigest: null }, + ], + aiSummary: 'Queja formal por demora en siniestro CLM-0048 (colisión). Cliente amenaza con escalar a Superintendencia. Broker Carlos Villalba se asignó y escaló con ajustador ASSA. Pendiente: documento del taller (presupuesto firmado) para cerrar revisión. Estado: esperando respuesta del cliente.', + aiSuggestedIntent: 'complaint', + aiConfidence: 0.76, + routingTrace: [ + { step: 'Channel Detection', result: 'Email — maria.perez@gmail.com', timestamp: '2026-04-02T15:00:01' }, + { step: 'Customer Match', result: 'Matched: María Elena Pérez Solano (cust-001)', timestamp: '2026-04-02T15:00:02' }, + { step: 'Intent Classification', result: 'Ambiguous — complaint (76%), claim_report (18%)', timestamp: '2026-04-02T15:00:03' }, + { step: 'No Rule Match', result: 'Confidence below threshold for auto-routing', timestamp: '2026-04-02T15:00:04' }, + { step: 'Routed to Open Pool', result: 'Tier 3 — awaiting manual triage', timestamp: '2026-04-02T15:00:05' }, + ], +} + +const sp006Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[5], + customerEmail: 'fernando.arias@transportesnorte.cr', + customerPhone: '+507 6888-4444', + linkedPolicies: [ + { id: 'POL-2020-1100', number: 'AUTO-2020-1100', carrier: 'INS', lob: 'Auto', status: 'Active', renewal: '2026-12-15' }, + { id: 'POL-2021-2200', number: 'HOME-2021-2200', carrier: 'ASSA', lob: 'Home', status: 'Lapsed', renewal: '—' }, + ], + linkedClaims: [], + messages: [ + { id: 'msg-006-1', type: 'email', direction: 'inbound', from: 'fernando.arias@transportesnorte.cr', to: 'soporte@seguros.com', subject: 'Consulta — plan salud colectivo para nuestro grupo', body: 'Buenos días, soy Fernando Arias de Transportes del Norte. Quiero explorar opciones de seguro de salud colectivo para nuestros empleados (aprox 50 personas). ¿Qué opciones tienen disponibles y cuál sería el proceso?', timestamp: '2026-04-06T14:10:00', aiDigest: 'Existing corporate customer inquiring about group health insurance for ~50 employees.' }, + { id: 'msg-006-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Cliente conocido (cust-010). Web form con LOB=Health. Tier 1: auto-routed a broker asignado (Ana R.).', timestamp: '2026-04-06T14:10:05', aiDigest: null }, + { id: 'msg-006-3', type: 'email', direction: 'outbound', from: 'ana.r@seguros.com', to: 'fernando.arias@transportesnorte.cr', subject: 'Re: Consulta — plan salud colectivo', body: 'Estimado Fernando, qué gusto saludarlo! Con mucho gusto le ayudamos con el plan colectivo de salud. Tenemos opciones con Vida Plena, Salud Global e Integral Medical. Le envío las opciones de cobertura para su grupo con los detalles de cada plan.', timestamp: '2026-04-06T16:00:00', aiDigest: null }, + { id: 'msg-006-4', type: 'internal_note', direction: 'internal', from: 'Ana R.', to: null, subject: null, body: 'Cliente ya tiene 2 pólizas (1 activa auto, 1 lapsed home). Oportunidad de cross-sell. Preparar propuesta colectiva con las 3 aseguradoras de salud.', timestamp: '2026-04-07T11:00:00', aiDigest: null }, + ], + aiSummary: 'Cliente corporativo existente (Transportes del Norte) consultando sobre plan de salud colectivo para ~50 empleados. Auto-enrutado por Tier 1 a broker asignado. Oportunidad de cross-sell identificada — ya tiene póliza auto activa.', + aiSuggestedIntent: 'policy_question', + aiConfidence: 0.85, + routingTrace: [ + { step: 'Channel Detection', result: 'Web Form — LOB: Health', timestamp: '2026-04-06T14:10:01' }, + { step: 'Customer Match', result: 'Matched: Fernando Arias Blanco (cust-010)', timestamp: '2026-04-06T14:10:02' }, + { step: 'Tier 1 Auto-Route', result: 'Known customer + LOB form → assigned broker', timestamp: '2026-04-06T14:10:03' }, + { step: 'Agent Assignment', result: 'Ana R. (assigned broker for cust-010)', timestamp: '2026-04-06T14:10:05' }, + ], +} + +const sp007Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[6], + customerEmail: null, + customerPhone: '+507 6234-5678', + linkedPolicies: [], + linkedClaims: [], + messages: [ + { id: 'msg-007-1', type: 'whatsapp', direction: 'inbound', from: '+507 6234-5678', to: null, subject: null, body: 'Hola buenos días, necesito ayuda con mi seguro, me pueden atender?', timestamp: '2026-04-08T07:45:00', aiDigest: 'Ambiguous request — "need help with my insurance". No specific intent detectable. Unknown customer.' }, + ], + aiSummary: 'Mensaje ambiguo de número desconocido. No se puede determinar intención específica. Requiere triage manual por operador del pool abierto.', + aiSuggestedIntent: 'general', + aiConfidence: 0.34, + routingTrace: [ + { step: 'Channel Detection', result: 'WhatsApp — +507 6234-5678', timestamp: '2026-04-08T07:45:01' }, + { step: 'Customer Match', result: 'No match — unknown number', timestamp: '2026-04-08T07:45:02' }, + { step: 'Intent Classification', result: 'Ambiguous — general (34%), policy_question (28%), sales_interest (22%)', timestamp: '2026-04-08T07:45:03' }, + { step: 'No Rule Match', result: 'Low confidence across all intents', timestamp: '2026-04-08T07:45:04' }, + { step: 'Routed to Open Pool', result: 'Tier 3 — unassigned, awaiting manual triage', timestamp: '2026-04-08T07:45:05' }, + ], +} + +const sp008Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[7], + customerEmail: 'luis.solis@outlook.com', + customerPhone: '+507 6555-1234', + linkedPolicies: [ + { id: 'POL-2022-2201', number: 'AUTO-2022-2201', carrier: 'Qualitas', lob: 'Auto', status: 'Active', renewal: '2027-01-15' }, + ], + linkedClaims: [], + messages: [ + { id: 'msg-008-1', type: 'email', direction: 'inbound', from: 'luis.solis@outlook.com', to: 'soporte@seguros.com', subject: 'Agregar conductor adicional a póliza AUTO-2022-2201', body: 'Estimados, necesito agregar a mi esposa como conductor adicional en mi póliza AUTO-2022-2201 (Mercedes GLC). ¿Qué documentos necesitan y cuál es el costo adicional?', timestamp: '2026-04-05T09:00:00', aiDigest: 'Customer requesting driver addition endorsement to auto policy. Asking about requirements and cost.' }, + { id: 'msg-008-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Intent: endorsement (94%). Enrutado a Operaciones.', timestamp: '2026-04-05T09:00:05', aiDigest: null }, + { id: 'msg-008-3', type: 'email', direction: 'outbound', from: 'ana.r@seguros.com', to: 'luis.solis@outlook.com', subject: 'Re: Agregar conductor adicional a póliza AUTO-2022-2201', body: 'Estimado Luis, con gusto procesamos el endoso. Necesitamos:\n\n1. Cédula de su esposa (copia)\n2. Licencia de conducir vigente (copia)\n3. Fecha de nacimiento\n\nEl costo adicional depende del perfil — generalmente entre $50-$120/año. Le confirmo el monto exacto una vez tenga los documentos.', timestamp: '2026-04-05T11:30:00', aiDigest: null }, + { id: 'msg-008-4', type: 'email', direction: 'inbound', from: 'luis.solis@outlook.com', to: 'soporte@seguros.com', subject: 'Re: Agregar conductor adicional', body: 'Adjunto cédula y licencia de mi esposa Carolina Méndez de Solís. Su fecha de nacimiento es 15 de mayo de 1988.', timestamp: '2026-04-06T14:00:00', aiDigest: null }, + { id: 'msg-008-5', type: 'internal_note', direction: 'internal', from: 'Ana R.', to: null, subject: null, body: 'Documentos recibidos. Solicitando cotización de endoso a Qualitas. Pendiente respuesta del carrier para confirmar monto.', timestamp: '2026-04-07T15:30:00', aiDigest: null }, + ], + aiSummary: 'Solicitud de endoso para agregar conductor adicional (esposa) a póliza auto Mercedes GLC. Documentos recibidos (cédula + licencia). Pendiente cotización del carrier Qualitas para confirmar costo adicional.', + aiSuggestedIntent: 'endorsement', + aiConfidence: 0.94, + routingTrace: [ + { step: 'Channel Detection', result: 'Email — luis.solis@outlook.com', timestamp: '2026-04-05T09:00:01' }, + { step: 'Customer Match', result: 'Matched: Luis Andrés Solís Calderón (cust-004)', timestamp: '2026-04-05T09:00:02' }, + { step: 'Intent Classification', result: 'endorsement (94%) — keywords: "agregar", "conductor adicional", "endoso"', timestamp: '2026-04-05T09:00:03' }, + { step: 'Rule Fired', result: 'Endorsement intent → Operations queue', timestamp: '2026-04-05T09:00:04' }, + ], +} + +const sp009Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[8], + customerEmail: 'fernando.arias@transportesnorte.cr', + customerPhone: '+507 6888-4444', + linkedPolicies: [ + { id: 'POL-2020-1100', number: 'AUTO-2020-1100', carrier: 'INS', lob: 'Auto', status: 'Active', renewal: '2026-12-15' }, + ], + linkedClaims: [], + messages: [ + { id: 'msg-009-1', type: 'whatsapp', direction: 'inbound', from: 'Fernando Arias', to: null, subject: null, body: 'URGENTE Carlos!! Acabo de tener un choque frontal con la Hilux en la vía Interamericana. El carro no enciende y necesito grúa YA. Estoy en el km 42 dirección David.', timestamp: '2026-04-08T06:15:00', aiDigest: 'URGENT: Known customer reporting front collision with Toyota Hilux. Vehicle disabled, requesting tow truck. Location: km 42 Interamericana highway.' }, + { id: 'msg-009-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'URGENTE — Cliente conocido (cust-010). Intent: claim_report (99%). Tier 1 auto-route a broker asignado. Prioridad: URGENTE.', timestamp: '2026-04-08T06:15:05', aiDigest: null }, + { id: 'msg-009-3', type: 'whatsapp', direction: 'outbound', from: 'Carlos Villalba', to: 'Fernando Arias', subject: null, body: 'Fernando tranquilo, ya estoy en eso. ¿Estás bien? ¿Hay heridos? Voy a coordinar la grúa ahora mismo.', timestamp: '2026-04-08T06:20:00', aiDigest: null, deliveryStatus: 'read' }, + { id: 'msg-009-4', type: 'whatsapp', direction: 'outbound', from: 'Carlos Villalba', to: 'Fernando Arias', subject: null, body: 'Ya coordiné la grúa, llega en 20 minutos a tu ubicación. INS tiene cobertura de asistencia en carretera incluida en tu póliza. ¿Necesitas que llame a alguien más?', timestamp: '2026-04-08T06:45:00', aiDigest: null, deliveryStatus: 'delivered' }, + ], + aiSummary: 'Emergencia: choque frontal con Toyota Hilux en vía Interamericana km 42. Vehículo inhabilitado. Broker coordinó grúa inmediatamente (20 min ETA). Asistencia en carretera cubierta por póliza INS. Pendiente: confirmar estado del conductor y crear claim formal.', + aiSuggestedIntent: 'claim_report', + aiConfidence: 0.99, + routingTrace: [ + { step: 'Channel Detection', result: 'WhatsApp — +507 6888-4444 (Fernando Arias)', timestamp: '2026-04-08T06:15:01' }, + { step: 'Customer Match', result: 'Matched: Fernando Arias Blanco (cust-010)', timestamp: '2026-04-08T06:15:02' }, + { step: 'Intent Classification', result: 'claim_report (99%) — keywords: "choque", "grúa", "URGENTE"', timestamp: '2026-04-08T06:15:03' }, + { step: 'Priority Escalation', result: 'Auto-set URGENT — emergency keywords detected', timestamp: '2026-04-08T06:15:04' }, + { step: 'Tier 1 Auto-Route', result: 'Known customer → assigned broker Carlos Villalba', timestamp: '2026-04-08T06:15:05' }, + ], +} + +const sp010Detail: SupportTicketDetail = { + ...MOCK_SUPPORT_TICKETS[9], + customerEmail: 'roberto.jimenez@gmail.com', + customerPhone: '+507 6222-8899', + linkedPolicies: [ + { id: 'POL-2023-3301', number: 'AUTO-2023-3301', carrier: 'Qualitas', lob: 'Auto', status: 'Active', renewal: '2027-04-01' }, + ], + linkedClaims: [], + messages: [ + { id: 'msg-010-1', type: 'phone_note', direction: 'inbound', from: 'Roberto Jiménez Mora', to: null, subject: null, body: 'Cliente llamó consultando sobre su renovación que vence el 30 de abril. Quiere saber si hay cambio en prima y si puede agregar cobertura de robo.', timestamp: '2026-04-03T16:00:00', aiDigest: 'Customer calling about upcoming renewal (Apr 30). Asking about premium changes and adding theft coverage.' }, + { id: 'msg-010-2', type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: 'Intent: policy_question (78%) / sales_interest (15%). Keyword "renovación" → Renewals queue.', timestamp: '2026-04-03T16:00:05', aiDigest: null }, + { id: 'msg-010-3', type: 'internal_note', direction: 'internal', from: 'María Fernanda Ortiz', to: null, subject: null, body: 'Cliente confirmó renovación con cobertura adicional de robo. Prima aumenta $180/año. Proceso completado — renovación efectiva 1 mayo 2027.', timestamp: '2026-04-04T09:00:00', aiDigest: null }, + ], + aiSummary: 'Seguimiento telefónico sobre renovación próxima (30 abril). Cliente confirmó renovación con adición de cobertura de robo (+$180/año). Proceso completado.', + aiSuggestedIntent: 'policy_question', + aiConfidence: 0.78, + routingTrace: [ + { step: 'Channel Detection', result: 'Phone call logged', timestamp: '2026-04-03T16:00:01' }, + { step: 'Customer Match', result: 'Matched: Roberto Jiménez Mora (cust-002)', timestamp: '2026-04-03T16:00:02' }, + { step: 'Intent Classification', result: 'policy_question (78%) — keyword: "renovación"', timestamp: '2026-04-03T16:00:03' }, + { step: 'Rule Fired', result: 'Renewal keyword → Renewals queue', timestamp: '2026-04-03T16:00:04' }, + ], +} + +export const MOCK_TICKET_DETAILS: Record = { + 'SP-001': sp001Detail, + 'SP-002': sp002Detail, + 'SP-003': sp003Detail, + 'SP-004': sp004Detail, + 'SP-005': sp005Detail, + 'SP-006': sp006Detail, + 'SP-007': sp007Detail, + 'SP-008': sp008Detail, + 'SP-009': sp009Detail, + 'SP-010': sp010Detail, +} + +// ─── Mock Routing Rules ────────────────────────────────────────────────────── + +export const MOCK_ROUTING_RULES: RoutingRule[] = [ + // Tier 1 + { id: 'rule-001', tier: 'tier1_auto', type: 'customer_match', name: 'Known Customer → Assigned Broker', condition: 'When incoming message matches a registered customer, route to their assigned broker', targetQueue: 'operations', targetAgent: null, enabled: true, priority: 1 }, + { id: 'rule-002', tier: 'tier1_auto', type: 'lob_specialist', name: 'Web Form LOB → Specialist', condition: 'Web form with LOB selected routes to LOB specialist team', targetQueue: 'operations', targetAgent: null, enabled: true, priority: 2 }, + { id: 'rule-003', tier: 'tier1_auto', type: 'doc_auto_fulfill', name: 'Certificate Auto-Fulfill', condition: 'Certificate requests from known customers auto-generate and deliver', targetQueue: 'operations', targetAgent: null, enabled: true, priority: 3 }, + // Tier 2 + { id: 'rule-004', tier: 'tier2_rule', type: 'keyword_intent', name: 'Payment Keywords → Collections', condition: 'Keywords: pago, factura, cobro, recibo, transferencia, mora', targetQueue: 'collections', targetAgent: null, enabled: true, priority: 10 }, + { id: 'rule-005', tier: 'tier2_rule', type: 'keyword_intent', name: 'Claim Keywords → Claims', condition: 'Keywords: siniestro, accidente, robo, daño, choque, grúa, colisión', targetQueue: 'claims', targetAgent: null, enabled: true, priority: 11 }, + { id: 'rule-006', tier: 'tier2_rule', type: 'keyword_intent', name: 'Sales Keywords → Sales', condition: 'Keywords: cotización, seguro nuevo, precio, cuánto sale, cobertura', targetQueue: 'sales', targetAgent: null, enabled: true, priority: 12 }, + { id: 'rule-007', tier: 'tier2_rule', type: 'keyword_intent', name: 'Renewal Keywords → Renewals', condition: 'Keywords: renovación, vencimiento, prórroga, vigencia', targetQueue: 'renewals', targetAgent: null, enabled: true, priority: 13 }, + { id: 'rule-008', tier: 'tier2_rule', type: 'keyword_intent', name: 'Endorsement Keywords → Operations', condition: 'Keywords: endoso, agregar, modificar, cambio de beneficiario', targetQueue: 'operations', targetAgent: null, enabled: true, priority: 14 }, +] diff --git a/app/data/pdf-field-mappings.json b/app/data/pdf-field-mappings.json new file mode 100644 index 0000000..bd6f557 --- /dev/null +++ b/app/data/pdf-field-mappings.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "mappings": [ + { + "catalogFormId": 33, + "fields": { + "full_name": "txtNombreCompleto", + "document_id": "txtCedula" + } + }, + { + "catalogFormId": 39, + "fields": { + "plate": "txtPlaca", + "vin": "txtChasis", + "declared_value": "numValor" + } + } + ] +} diff --git a/app/data/quotes-overview.mock.ts b/app/data/quotes-overview.mock.ts new file mode 100644 index 0000000..44b5ff6 --- /dev/null +++ b/app/data/quotes-overview.mock.ts @@ -0,0 +1,214 @@ +/** + * Mock pipeline data for Quotes overview (mission control). Replace with API when ready. + */ + +export type QuotePipelineScopeFilter = 'global' | 'corporate' | 'personal' + +export type QuoteOverviewLob = 'auto' | 'health' | 'life' | 'general_risk' | 'custom' + +export type QuotePipelineStageId = 'intake' | 'quoted' | 'proposal' | 'bind' | 'handoff' + +/** + * Unified pipeline stage IDs — mirrors PipelineStage from useSalesPipeline. + * Used by the quotes overview kanban so both views share one vocabulary. + */ +export type UnifiedStageId = + | 'customer' + | 'get_quotes' + | 'waiting_carriers' + | 'present_quotes' + | 'waiting_client' + | 'solicitud' + | 'emission' + +export type MockPipelineQuote = { + id: string + customerLabel: string + /** Corporate vs personal — drives pipeline scope filters */ + party: 'corporate' | 'personal' + lob: QuoteOverviewLob + /** Sub-path: single, comparativo, fleet, collective, etc. */ + pathLabel: string + stage: QuotePipelineStageId + /** Unified pipeline stage — same vocabulary as sales pipeline */ + unifiedStage: UnifiedStageId + owner: string + formsDone: number + formsTotal: number + /** Customer has been notified / has portal activity (mock) */ + customerInformed: boolean +} + +export const QUOTE_PIPELINE_STAGES: { id: QuotePipelineStageId; label: string; hint: string }[] = [ + { id: 'intake', label: 'Customer Profile', hint: 'Qualify & rate inputs' }, + { id: 'quoted', label: 'Quote Prep', hint: 'Options with carrier' }, + { id: 'proposal', label: 'Acceptance Pending', hint: 'Out to customer' }, + { id: 'bind', label: 'Emission Pending', hint: 'Submit & conditions' }, + { id: 'handoff', label: 'Going Live!', hint: 'Policy / solicitud' } +] + +/** Unified pipeline stages — same as sales pipeline. Used in overview chart. */ +export const UNIFIED_PIPELINE_STAGES: { id: UnifiedStageId; 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 }, +] + +export const QUOTE_LOB_OPTIONS: { value: QuoteOverviewLob | 'all'; label: string }[] = [ + { value: 'all', label: 'All lines' }, + { value: 'auto', label: 'Auto' }, + { value: 'health', label: 'Health' }, + { value: 'life', label: 'Life' }, + { value: 'general_risk', label: 'General risk' }, + { value: 'custom', label: 'Custom' } +] + +export const MOCK_PIPELINE_QUOTES: MockPipelineQuote[] = [ + { + id: 'q-101', + customerLabel: 'Transportes Delta S.A.', + party: 'corporate', + lob: 'auto', + pathLabel: 'Fleet', + stage: 'quoted', + unifiedStage: 'waiting_carriers', + owner: 'A. Morales', + formsDone: 2, + formsTotal: 5, + customerInformed: true + }, + { + id: 'q-102', + customerLabel: 'María Fernández', + party: 'personal', + lob: 'auto', + pathLabel: 'Comparativo', + stage: 'proposal', + unifiedStage: 'present_quotes', + owner: 'L. Chen', + formsDone: 4, + formsTotal: 4, + customerInformed: true + }, + { + id: 'q-103', + customerLabel: 'J. Pérez', + party: 'personal', + lob: 'auto', + pathLabel: 'Single', + stage: 'intake', + unifiedStage: 'customer', + owner: 'L. Chen', + formsDone: 0, + formsTotal: 3, + customerInformed: false + }, + { + id: 'q-201', + customerLabel: 'Clínica Norte', + party: 'corporate', + lob: 'health', + pathLabel: 'Collective', + stage: 'bind', + unifiedStage: 'solicitud', + owner: 'R. Vega', + formsDone: 6, + formsTotal: 8, + customerInformed: true + }, + { + id: 'q-202', + customerLabel: 'Familia Ortega', + party: 'personal', + lob: 'health', + pathLabel: 'Family', + stage: 'quoted', + unifiedStage: 'get_quotes', + owner: 'A. Morales', + formsDone: 3, + formsTotal: 6, + customerInformed: true + }, + { + id: 'q-203', + customerLabel: 'Startup Labs', + party: 'corporate', + lob: 'health', + pathLabel: 'Travel', + stage: 'intake', + unifiedStage: 'customer', + owner: 'R. Vega', + formsDone: 1, + formsTotal: 4, + customerInformed: false + }, + { + id: 'q-301', + customerLabel: 'Holdings Centro', + party: 'corporate', + lob: 'life', + pathLabel: 'Key person', + stage: 'proposal', + unifiedStage: 'waiting_client', + owner: 'L. Chen', + formsDone: 2, + formsTotal: 5, + customerInformed: true + }, + { + id: 'q-302', + customerLabel: 'Carlos Méndez', + party: 'personal', + lob: 'life', + pathLabel: 'Individual', + stage: 'handoff', + unifiedStage: 'emission', + owner: 'A. Morales', + formsDone: 5, + formsTotal: 5, + customerInformed: true + }, + { + id: 'q-401', + customerLabel: 'Retail Plaza', + party: 'corporate', + lob: 'general_risk', + pathLabel: 'Corporate', + stage: 'quoted', + unifiedStage: 'waiting_carriers', + owner: 'R. Vega', + formsDone: 3, + formsTotal: 7, + customerInformed: false + }, + { + id: 'q-402', + customerLabel: 'Ana Ríos', + party: 'personal', + lob: 'general_risk', + pathLabel: 'Personal', + stage: 'intake', + unifiedStage: 'get_quotes', + owner: 'L. Chen', + formsDone: 1, + formsTotal: 4, + customerInformed: true + }, + { + id: 'q-501', + customerLabel: 'Phone-in — manual rate', + party: 'personal', + lob: 'custom', + pathLabel: 'Manual entry', + stage: 'proposal', + unifiedStage: 'present_quotes', + owner: 'R. Vega', + formsDone: 0, + formsTotal: 2, + customerInformed: false + } +] diff --git a/app/data/roles-seguros.ts b/app/data/roles-seguros.ts new file mode 100644 index 0000000..054a62b --- /dev/null +++ b/app/data/roles-seguros.ts @@ -0,0 +1,35 @@ +import type { RoleRow, SegurosPermissionKey } from '~/types/roles' + +function all(v: boolean): Record { + return { + profile: v, + portfolio: v, + layers: v, + tasks: v, + billing: v, + analytics: v, + support: v + } +} + +export const ROLES_SEGUROS_SEED: RoleRow[] = [ + { id: 5, description: 'Agente de Ventas', active: true, seguros: all(true) }, + { id: 4, description: 'Supervisor de Ventas', active: true, seguros: all(true) }, + { id: 3, description: 'Configurador', active: true, seguros: all(false) }, + { id: 2, description: 'Supervisor', active: true, seguros: all(false) }, + { id: 1, description: 'Superadministrator', active: true, seguros: all(false) } +] + +export const SEGUROS_PERMISSION_COLUMNS: { + key: SegurosPermissionKey + icon: string + label: string +}[] = [ + { key: 'profile', icon: 'i-heroicons-user', label: 'Perfil' }, + { key: 'portfolio', icon: 'i-heroicons-folder', label: 'Cartera / pólizas' }, + { key: 'layers', icon: 'i-heroicons-squares-2x2', label: 'Capas / duplicados' }, + { key: 'tasks', icon: 'i-heroicons-clipboard-document-check', label: 'Tareas / checklist' }, + { key: 'billing', icon: 'i-heroicons-currency-dollar', label: 'Cobros / primas' }, + { key: 'analytics', icon: 'i-heroicons-chart-pie', label: 'Indicadores' }, + { key: 'support', icon: 'i-heroicons-chat-bubble-left-right', label: 'Soporte' } +] diff --git a/app/data/taxonomy.ts b/app/data/taxonomy.ts new file mode 100644 index 0000000..52ebdc0 --- /dev/null +++ b/app/data/taxonomy.ts @@ -0,0 +1,34 @@ +export const INSURER_SLUGS = [ + 'acerta', + 'assa', + 'ancon', + 'fedpa', + 'mapfre', + 'optima', + 'palig' +] as const + +export type InsurerSlug = (typeof INSURER_SLUGS)[number] + +export const PRODUCT_LINE_SLUGS = [ + 'life', + 'health_local', + 'health_international', + 'auto_full_coverage', + 'auto_dat_liability', + 'home', + 'general_liability', + 'any' +] as const + +export type ProductLineSlug = (typeof PRODUCT_LINE_SLUGS)[number] + +export const INSURER_LABEL: Record = { + acerta: 'ACERTA', + assa: 'ASSA', + ancon: 'ANCON', + fedpa: 'FEDPA', + mapfre: 'MAPFRE', + optima: 'OPTIMA', + palig: 'PALIG' +} diff --git a/app/layouts/default.vue b/app/layouts/default.vue index b71f9c7..d7184a6 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -1,69 +1,392 @@ + + + + diff --git a/app/pages/account/index.vue b/app/pages/account/index.vue new file mode 100644 index 0000000..9b5b1d9 --- /dev/null +++ b/app/pages/account/index.vue @@ -0,0 +1,88 @@ + + + diff --git a/app/pages/ai-tools/case-assistant.vue b/app/pages/ai-tools/case-assistant.vue new file mode 100644 index 0000000..63447f6 --- /dev/null +++ b/app/pages/ai-tools/case-assistant.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/pages/ai-tools/email-writer.vue b/app/pages/ai-tools/email-writer.vue new file mode 100644 index 0000000..9607c4c --- /dev/null +++ b/app/pages/ai-tools/email-writer.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/pages/ai-tools/policy-comparator.vue b/app/pages/ai-tools/policy-comparator.vue new file mode 100644 index 0000000..f0ec96b --- /dev/null +++ b/app/pages/ai-tools/policy-comparator.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/pages/ai-tools/sales-factory.vue b/app/pages/ai-tools/sales-factory.vue new file mode 100644 index 0000000..0738e13 --- /dev/null +++ b/app/pages/ai-tools/sales-factory.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/app/pages/analysis/index.vue b/app/pages/analysis/index.vue new file mode 100644 index 0000000..34e2757 --- /dev/null +++ b/app/pages/analysis/index.vue @@ -0,0 +1,459 @@ + + + + + diff --git a/app/pages/calendar.vue b/app/pages/calendar.vue new file mode 100644 index 0000000..3ee790e --- /dev/null +++ b/app/pages/calendar.vue @@ -0,0 +1,1514 @@ + + + + + diff --git a/app/pages/customers/[id].vue b/app/pages/customers/[id].vue index 9da0e71..ff99b38 100644 --- a/app/pages/customers/[id].vue +++ b/app/pages/customers/[id].vue @@ -1,215 +1,2289 @@