WIP jordan

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

View File

@@ -0,0 +1,727 @@
<script setup lang="ts">
usePageTitle('Nombramiento')
const intakeMode = ref<'scan' | 'manual'>('scan')
const customerMode = ref<'existing' | 'new'>('existing')
const customerSearch = ref('')
const uploadState = ref<'idle' | 'uploading' | 'processing' | 'review'>('idle')
const fileName = ref('')
const dragOver = ref(false)
/* ── Extracted policy data (mock — would come from AI model) ── */
const extracted = reactive({
policyNumber: '',
carrier: '',
lob: '',
effectiveDate: '',
expirationDate: '',
premium: '',
insuredName: '',
insuredId: '',
insuredEmail: '',
insuredPhone: '',
currentBroker: '',
coverageSummary: '',
customerMatch: null as null | 'existing' | 'new',
matchedCustomerId: null as null | string,
matchedCustomerName: null as null | string,
confidence: 0,
})
function simulateUpload(name: string) {
fileName.value = name
uploadState.value = 'uploading'
setTimeout(() => {
uploadState.value = 'processing'
setTimeout(() => {
// Simulate AI extraction results
extracted.policyNumber = 'POL-2024-88412'
extracted.carrier = 'ASSA Compania de Seguros'
extracted.lob = 'Auto'
extracted.effectiveDate = '2024-06-15'
extracted.expirationDate = '2025-06-15'
extracted.premium = '$1,840.00'
extracted.insuredName = 'María Elena Pérez Solano'
extracted.insuredId = '1-0456-0812'
extracted.insuredEmail = 'maria.perez@email.com'
extracted.insuredPhone = '+506 8834-2291'
extracted.currentBroker = 'Seguros Internacionales S.A.'
extracted.coverageSummary = 'Comprehensive auto coverage, $50K liability, $25K collision, roadside assistance included.'
extracted.customerMatch = 'existing'
extracted.matchedCustomerId = 'C-1042'
extracted.matchedCustomerName = 'María Pérez'
extracted.confidence = 94
uploadState.value = 'review'
}, 2000)
}, 1200)
}
function onFileSelect(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (file) simulateUpload(file.name)
}
function onDrop(e: DragEvent) {
e.preventDefault()
dragOver.value = false
const file = e.dataTransfer?.files?.[0]
if (file) simulateUpload(file.name)
}
function reset() {
uploadState.value = 'idle'
fileName.value = ''
extracted.policyNumber = ''
extracted.carrier = ''
extracted.customerMatch = null
extracted.confidence = 0
}
const toast = useToast()
function confirmTransfer() {
toast.add({
title: 'Nombramiento initiated',
description: `Broker of record transfer started for ${extracted.policyNumber}. The customer profile has been updated.`,
color: 'success'
})
reset()
}
</script>
<template>
<div class="mx-auto max-w-4xl space-y-6 pb-12">
<!-- Back + header -->
<div class="flex flex-wrap items-center justify-between gap-3">
<NuxtLink to="/onboarding" class="inline-flex">
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">
Sales Pipeline
</UButton>
</NuxtLink>
</div>
<div class="max-w-2xl">
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Nombramiento</h1>
<p class="mt-2 text-[14px] leading-relaxed text-[var(--text-muted)]">
Register a policy and become the broker of record. Scan a document with AI or enter details manually, then link to an existing customer or create a new one.
</p>
</div>
<!-- Intake mode toggle -->
<div v-if="uploadState === 'idle'" class="flex flex-col gap-4">
<div class="nom-mode-toggle">
<button
type="button"
class="nom-mode-btn"
:class="intakeMode === 'scan' ? 'nom-mode-active' : 'nom-mode-inactive'"
@click="intakeMode = 'scan'"
>
<UIcon name="i-heroicons-sparkles" style="width: 16px; height: 16px;" />
AI scan
</button>
<button
type="button"
class="nom-mode-btn"
:class="intakeMode === 'manual' ? 'nom-mode-active' : 'nom-mode-inactive'"
@click="intakeMode = 'manual'"
>
<UIcon name="i-heroicons-pencil-square" style="width: 16px; height: 16px;" />
Manual entry
</button>
</div>
<!-- Customer association -->
<div class="nom-customer-section">
<p class="nom-label">Customer</p>
<div class="mt-2 flex gap-2">
<button
type="button"
class="nom-customer-btn"
:class="customerMode === 'existing' ? 'nom-customer-active' : 'nom-customer-inactive'"
@click="customerMode = 'existing'"
>
<UIcon name="i-heroicons-user-circle" style="width: 16px; height: 16px;" />
Existing customer
</button>
<button
type="button"
class="nom-customer-btn"
:class="customerMode === 'new' ? 'nom-customer-active' : 'nom-customer-inactive'"
@click="customerMode = 'new'"
>
<UIcon name="i-heroicons-user-plus" style="width: 16px; height: 16px;" />
New customer
</button>
</div>
<div v-if="customerMode === 'existing'" class="mt-3">
<UInput
v-model="customerSearch"
icon="i-heroicons-magnifying-glass"
placeholder="Search by name, ID, or email..."
size="sm"
class="max-w-sm"
/>
<p class="mt-1.5 text-[11px] text-[var(--text-muted)]">Select the customer this policy belongs to. AI scan will also attempt automatic matching.</p>
</div>
<div v-else class="mt-3">
<p class="text-[12px] text-[var(--text-muted)]">A new customer profile will be created from the policy details.</p>
</div>
</div>
</div>
<!-- AI SCAN PATH -->
<!-- Upload zone idle state (scan mode) -->
<div
v-if="uploadState === 'idle' && intakeMode === 'scan'"
class="nom-upload-zone"
:class="{ 'nom-upload-zone-active': dragOver }"
@dragover.prevent="dragOver = true"
@dragleave="dragOver = false"
@drop="onDrop"
>
<div class="flex flex-col items-center gap-3 text-center">
<div class="nom-icon-ring">
<UIcon name="i-heroicons-document-arrow-up" style="width: 24px; height: 24px;" />
</div>
<div>
<p class="text-[14px] font-medium text-[var(--text-primary)]">Upload policy document</p>
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
Drop a PDF here, or click to browse. AI will read the policy and extract all fields.
</p>
</div>
<label class="nom-browse-btn">
Browse files
<input type="file" accept=".pdf,.png,.jpg,.jpeg" class="sr-only" @change="onFileSelect" />
</label>
<p class="text-[11px] text-[var(--text-muted)] opacity-60">PDF, PNG, or JPG up to 25 MB</p>
</div>
</div>
<!-- MANUAL ENTRY PATH -->
<div v-if="uploadState === 'idle' && intakeMode === 'manual'" class="nom-data-card">
<div class="nom-data-header">
<p class="text-[14px] font-semibold text-[var(--text-primary)]">Policy details</p>
<p class="text-[13px] text-[var(--text-muted)]">Enter the policy information manually. All fields can be edited later.</p>
</div>
<div class="nom-data-grid">
<div class="nom-field">
<label class="nom-label">Policy number</label>
<UInput placeholder="e.g. POL-2024-00001" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Carrier</label>
<UInput placeholder="Carrier name" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Line of business</label>
<USelect :items="[{ label: 'Auto', value: 'auto' }, { label: 'Health', value: 'health' }, { label: 'Life', value: 'life' }, { label: 'General Risk', value: 'general-risk' }, { label: 'Other', value: 'other' }]" placeholder="Select..." size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Premium</label>
<UInput placeholder="$0.00" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Effective date</label>
<UInput size="sm" type="date" />
</div>
<div class="nom-field">
<label class="nom-label">Expiration date</label>
<UInput size="sm" type="date" />
</div>
<div class="nom-field">
<label class="nom-label">Previous broker</label>
<UInput placeholder="Outgoing brokerage (if any)" size="sm" />
</div>
</div>
<div class="nom-data-divider" />
<div class="nom-data-grid" v-if="customerMode === 'new'">
<div class="nom-field">
<label class="nom-label">Insured name</label>
<UInput placeholder="Full legal name" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">ID number</label>
<UInput placeholder="Cédula or passport" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Email</label>
<UInput placeholder="email@example.com" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Phone</label>
<UInput placeholder="+506 0000-0000" size="sm" />
</div>
</div>
<div v-else class="px-5 pb-2">
<p class="text-[12px] text-[var(--text-muted)] italic">Customer details will be pulled from the selected existing profile.</p>
</div>
<div class="nom-data-divider" />
<div class="px-5 pb-5">
<label class="nom-label">Coverage notes</label>
<UTextarea placeholder="Optional — describe coverage, limits, deductibles..." size="sm" :rows="2" class="mt-1.5" />
</div>
</div>
<!-- Manual entry actions -->
<div v-if="uploadState === 'idle' && intakeMode === 'manual'" class="flex flex-wrap items-center justify-end gap-2">
<UButton color="neutral" variant="outline">Save as draft</UButton>
<UButton color="primary">Register policy</UButton>
</div>
<!-- Uploading state -->
<div v-else-if="uploadState === 'uploading'" class="nom-status-card">
<div class="flex items-center gap-3">
<div class="nom-spinner" />
<div>
<p class="text-[14px] font-medium text-[var(--text-primary)]">Uploading {{ fileName }}</p>
<p class="mt-0.5 text-[13px] text-[var(--text-muted)]">Sending document to processing pipeline...</p>
</div>
</div>
</div>
<!-- Processing state -->
<div v-else-if="uploadState === 'processing'" class="nom-status-card">
<div class="flex items-center gap-3">
<div class="nom-spinner" />
<div>
<p class="text-[14px] font-medium text-[var(--text-primary)]">AI is reading the policy</p>
<p class="mt-0.5 text-[13px] text-[var(--text-muted)]">Extracting insured details, coverage terms, carrier info, and matching against existing customers...</p>
</div>
</div>
</div>
<!-- Review state extracted data -->
<template v-else-if="uploadState === 'review'">
<!-- Confidence bar -->
<div class="nom-confidence-strip">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-sparkles" style="width: 16px; height: 16px; color: #01696f;" />
<span class="text-[13px] font-medium text-[var(--text-primary)]">AI extraction complete</span>
</div>
<div class="flex items-center gap-3">
<div class="nom-confidence-bar-track">
<div class="nom-confidence-bar-fill" :style="`width: ${extracted.confidence}%`" />
</div>
<span class="nom-confidence-badge">{{ extracted.confidence }}%</span>
<span class="text-[10px] font-semibold uppercase" :style="extracted.confidence >= 90 ? 'color: #059669' : extracted.confidence >= 70 ? 'color: #d97706' : 'color: #dc2626'">
{{ extracted.confidence >= 90 ? 'High' : extracted.confidence >= 70 ? 'Medium' : 'Low' }}
</span>
</div>
</div>
<!-- Customer match -->
<div v-if="extracted.customerMatch === 'existing'" class="nom-match-card nom-match-existing">
<UIcon name="i-heroicons-user-circle" style="width: 20px; height: 20px; flex-shrink: 0;" />
<div class="min-w-0 flex-1">
<p class="text-[13px] font-medium text-[var(--text-primary)]">
Matched to existing customer: <strong>{{ extracted.matchedCustomerName }}</strong>
<span class="text-[var(--text-muted)]"> ({{ extracted.matchedCustomerId }})</span>
</p>
<p class="mt-0.5 text-[12px] text-[var(--text-muted)]">This policy will be added to their existing profile.</p>
</div>
<UButton size="xs" color="neutral" variant="soft">Change</UButton>
</div>
<div v-else-if="extracted.customerMatch === 'new'" class="nom-match-card nom-match-new">
<UIcon name="i-heroicons-user-plus" style="width: 20px; height: 20px; flex-shrink: 0;" />
<div class="min-w-0 flex-1">
<p class="text-[13px] font-medium text-[var(--text-primary)]">New customer will be created</p>
<p class="mt-0.5 text-[12px] text-[var(--text-muted)]">No matching customer found. A new profile will be set up from the extracted data.</p>
</div>
</div>
<!-- Extracted fields -->
<div class="nom-data-card">
<div class="nom-data-header">
<p class="text-[14px] font-semibold text-[var(--text-primary)]">Policy details</p>
<p class="text-[13px] text-[var(--text-muted)]">Review and correct any fields before initiating the transfer.</p>
</div>
<div class="nom-data-grid">
<div class="nom-field">
<label class="nom-label">Policy number</label>
<UInput :model-value="extracted.policyNumber" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Carrier</label>
<UInput :model-value="extracted.carrier" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Line of business</label>
<UInput :model-value="extracted.lob" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Premium</label>
<UInput :model-value="extracted.premium" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Effective date</label>
<UInput :model-value="extracted.effectiveDate" size="sm" type="date" />
</div>
<div class="nom-field">
<label class="nom-label">Expiration date</label>
<UInput :model-value="extracted.expirationDate" size="sm" type="date" />
</div>
<div class="nom-field">
<label class="nom-label">Current broker</label>
<UInput :model-value="extracted.currentBroker" size="sm" />
</div>
</div>
<div class="nom-data-divider" />
<div class="nom-data-grid">
<div class="nom-field">
<label class="nom-label">Insured name</label>
<UInput :model-value="extracted.insuredName" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">ID number</label>
<UInput :model-value="extracted.insuredId" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Email</label>
<UInput :model-value="extracted.insuredEmail" size="sm" />
</div>
<div class="nom-field">
<label class="nom-label">Phone</label>
<UInput :model-value="extracted.insuredPhone" size="sm" />
</div>
</div>
<div class="nom-data-divider" />
<div class="px-5 pb-5">
<label class="nom-label">Coverage summary</label>
<UTextarea :model-value="extracted.coverageSummary" size="sm" :rows="2" class="mt-1.5" />
</div>
</div>
<!-- Actions -->
<div class="flex flex-wrap items-center justify-between gap-3">
<UButton color="neutral" variant="soft" @click="reset">
Start over
</UButton>
<div class="flex gap-2">
<UButton color="neutral" variant="outline">
Save as draft
</UButton>
<UButton color="primary" @click="confirmTransfer">
Initiate transfer
</UButton>
</div>
</div>
</template>
<!-- How it works -->
<div v-if="uploadState === 'idle'" class="nom-info-section">
<p class="text-[13px] font-semibold text-[var(--text-primary)]">How it works</p>
<ol class="nom-steps">
<li>
<span class="nom-step-num">1</span>
<div>
<p class="text-[13px] font-medium text-[var(--text-primary)]">Upload the policy</p>
<p class="text-[12px] text-[var(--text-muted)]">Drop a PDF or image of the policy from the outgoing brokerage.</p>
</div>
</li>
<li>
<span class="nom-step-num">2</span>
<div>
<p class="text-[13px] font-medium text-[var(--text-primary)]">AI extracts the data</p>
<p class="text-[12px] text-[var(--text-muted)]">Policy number, carrier, coverage, insured details, and dates are read automatically.</p>
</div>
</li>
<li>
<span class="nom-step-num">3</span>
<div>
<p class="text-[13px] font-medium text-[var(--text-primary)]">Customer matching</p>
<p class="text-[12px] text-[var(--text-muted)]">The system checks if the insured is an existing customer or creates a new profile.</p>
</div>
</li>
<li>
<span class="nom-step-num">4</span>
<div>
<p class="text-[13px] font-medium text-[var(--text-primary)]">Review and transfer</p>
<p class="text-[12px] text-[var(--text-muted)]">Verify the extracted fields, then initiate the broker of record change.</p>
</div>
</li>
</ol>
</div>
</div>
</template>
<style scoped>
/* ── Mode toggle ── */
.nom-mode-toggle {
display: inline-flex;
gap: 2px;
padding: 3px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.04);
}
.nom-mode-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 150ms ease;
}
.nom-mode-active {
background: #ffffff;
color: var(--text-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.nom-mode-inactive {
background: transparent;
color: var(--text-muted);
}
.nom-mode-inactive:hover {
color: var(--text-primary);
}
/* ── Customer association ── */
.nom-customer-section {
padding: 16px 20px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: var(--surface);
}
.nom-customer-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
border: 1px solid;
cursor: pointer;
transition: all 150ms ease;
}
.nom-customer-active {
background: rgba(1, 105, 111, 0.06);
border-color: rgba(1, 105, 111, 0.2);
color: #01696f;
}
.nom-customer-inactive {
background: transparent;
border-color: rgba(0, 0, 0, 0.08);
color: var(--text-muted);
}
.nom-customer-inactive:hover {
border-color: rgba(0, 0, 0, 0.15);
color: var(--text-primary);
}
/* ── Upload drop zone ── */
.nom-upload-zone {
display: flex;
align-items: center;
justify-content: center;
min-height: 220px;
padding: 40px 24px;
border: 1.5px dashed rgba(0, 0, 0, 0.12);
border-radius: 12px;
background: var(--surface);
transition: border-color 150ms ease, background 150ms ease;
cursor: pointer;
}
.nom-upload-zone:hover,
.nom-upload-zone-active {
border-color: #01696f;
background: rgba(1, 105, 111, 0.02);
}
.nom-icon-ring {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(1, 105, 111, 0.06);
color: #01696f;
}
.nom-browse-btn {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
color: #01696f;
background: rgba(1, 105, 111, 0.08);
cursor: pointer;
transition: background 150ms ease;
}
.nom-browse-btn:hover {
background: rgba(1, 105, 111, 0.14);
}
/* ── Status card (uploading / processing) ── */
.nom-status-card {
padding: 24px;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: var(--surface);
}
.nom-spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(1, 105, 111, 0.15);
border-top-color: #01696f;
border-radius: 50%;
animation: nom-spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes nom-spin {
to { transform: rotate(360deg); }
}
/* ── Confidence strip ── */
.nom-confidence-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
background: rgba(1, 105, 111, 0.04);
border: 1px solid rgba(1, 105, 111, 0.1);
}
.nom-confidence-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 9999px;
font-size: 11px;
font-weight: 600;
background: rgba(1, 105, 111, 0.1);
color: #01696f;
}
.nom-confidence-bar-track {
width: 80px;
height: 6px;
border-radius: 3px;
background: rgba(0,0,0,0.06);
overflow: hidden;
}
.nom-confidence-bar-fill {
height: 100%;
border-radius: 3px;
background: #01696f;
transition: width 600ms ease;
}
/* ── Customer match cards ── */
.nom-match-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 10px;
border: 1px solid;
}
.nom-match-existing {
background: rgba(1, 105, 111, 0.03);
border-color: rgba(1, 105, 111, 0.1);
color: #01696f;
}
.nom-match-new {
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0, 0, 0, 0.08);
color: var(--text-muted);
}
/* ── Data card ── */
.nom-data-card {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
overflow: hidden;
}
.nom-data-header {
padding: 20px 20px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.nom-data-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 20px;
}
@media (max-width: 639px) {
.nom-data-grid { grid-template-columns: 1fr; }
}
.nom-data-divider {
height: 1px;
background: rgba(0, 0, 0, 0.06);
margin: 0 20px;
}
.nom-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.nom-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #8a8a86;
}
/* ── Info section ── */
.nom-info-section {
padding: 20px;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: var(--surface);
}
.nom-steps {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 16px;
list-style: none;
padding: 0;
}
.nom-steps li {
display: flex;
align-items: flex-start;
gap: 12px;
}
.nom-step-num {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 8px;
background: rgba(1, 105, 111, 0.06);
color: #01696f;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
margin-top: 1px;
}
</style>