728 lines
23 KiB
Vue
728 lines
23 KiB
Vue
<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>
|