// Dashboard + New Analysis modal + Buy Credits modal. // ─── Dashboard ──────────────────────────────────────────────────────────────── function Dashboard({ usage, analyses, onOpenNew, onOpenBuy, onOpenAnalise }) { const last30 = useMemo(() => { const m = { COMPLETE: 0, INPROGRESS: 0, FAILED: 0 }; const cutoff = Date.now() - 30 * 86400000; for (const a of analyses) { if (new Date(a.created_at).getTime() < cutoff) continue; if (a.status === 'COMPLETE') m.COMPLETE++; else if (a.status === 'FAILED') m.FAILED++; else m.INPROGRESS++; } return m; }, [analyses]); const recents = analyses.slice(0, 10); const lowBalance = usage.credit_balance < 3; return (
{/* Low balance banner */} {lowBalance && (
Saldo baixo — recarregue antes do próximo prazo.
Restam apenas {usage.credit_balance} créditos. Pagamento por PIX é instantâneo.
)} {/* Credit card hero */}
Saldo de créditos · não expira
{usage.credit_balance} créditos disponíveis
{usage.last_purchase ? <>Última recarga: {usage.last_purchase.credits} créditos em {fmtDate.format(new Date(usage.last_purchase.at))} · cada análise consome 1 crédito : <>Cada análise consome 1 crédito · créditos não têm prazo de validade}
{usage.plan_label && (
Pacote atual: {usage.plan_label}
)}
{/* Month stats */}
0 ? 'text-danger-700' : 'text-ink-900'} />
{/* Recents table */}

Análises recentes

Últimas 10 análises do escritório.

{recents.length === 0 ? ( ) : (
{recents.map((a) => ( onOpenAnalise(a.id)} className="border-b border-ink-100 last:border-0 hover:bg-ink-50 cursor-pointer transition-colors" > ))}
Data Requerente Tipo Status Viabilidade ID
{fmtDateTime.format(new Date(a.created_at))} {a.requerente_nome || } Revisional Veículo {a.pericia_version_label} #{shortId(a.id)}
)}
); } function EmptyState({ onOpenNew }) { return (

Nenhuma análise ainda

Envie seu primeiro contrato em PDF para gerar planilha, painel e memória de cálculo.

); } // ─── New Analysis Modal ─────────────────────────────────────────────────────── function NewAnalysisModal({ open, onClose, usage, periciaTypes, onSubmit }) { const [file, setFile] = useState(null); const [tipo, setTipo] = useState(periciaTypes[0]?.code || ''); const [requerente, setReq] = useState(''); const [dragOver, setDragOver] = useState(false); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const inputRef = useRef(null); useEffect(() => { if (open) { setFile(null); setReq(''); setError(null); setDragOver(false); } }, [open]); const sufficient = usage.credit_balance > 0; const idempKey = useMemo(() => uuid(), [open]); const acceptFile = (f) => { setError(null); if (!f) return; if (f.type && f.type !== 'application/pdf' && !f.name.toLowerCase().endsWith('.pdf')) { setError('Apenas arquivos PDF são aceitos. Converta antes de enviar.'); return; } if (f.size > 50 * 1024 * 1024) { setError('Arquivo excede o limite de 50 MB. Reduza ou divida o PDF.'); return; } setFile(f); }; const submit = () => { if (!file || !sufficient || submitting) return; setSubmitting(true); setTimeout(() => { onSubmit({ file, pericia_type_code: tipo, requerente_nome: requerente.trim() || null, idempotency_key: idempKey, }); setSubmitting(false); }, 600); }; const fmtBytes = (n) => { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / 1024 / 1024).toFixed(2)} MB`; }; return (
{/* Dropzone */}
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); acceptFile(e.dataTransfer.files?.[0]); }} onClick={() => inputRef.current?.click()} className={`relative border-2 border-dashed rounded-lg px-5 py-8 text-center cursor-pointer transition-colors ${ dragOver ? 'border-navy-800 bg-navy-50' : file ? 'border-ok-200 bg-ok-50/50' : 'border-ink-200 bg-ink-50/60 hover:border-navy-800/40 hover:bg-navy-50/40' }`} > acceptFile(e.target.files?.[0])} /> {file ? (
{file.name}
{fmtBytes(file.size)} · PDF
) : (
Arraste o PDF do contrato ou selecione
PDF até 50 MB · texto extraível preferencial
)}
{error && (
{error}
)} setReq(e.target.value)} /> {/* Quota notice */}
{sufficient ? : } {sufficient ? (
Esta análise consumirá 1 crédito. Saldo atual: {' '}{usage.credit_balance} → ficará {' '}{usage.credit_balance - 1}.
) : (
Saldo zerado. Compre créditos antes de enviar uma nova análise.
)}
Idempotency-Key {idempKey}
{!sufficient ? ( ) : ( )}
); } // ─── Buy Credits Modal ──────────────────────────────────────────────────────── function BuyCreditsModal({ open, onClose, onPurchase }) { const [picked, setPicked] = useState('p100'); const [method, setMethod] = useState('pix'); const packages = [ { id: 'p20', credits: 20, price: 197.00, unit: 9.85, highlight: null, extra: null }, { id: 'p100', credits: 100, price: 697.00, unit: 6.97, highlight: 'Mais popular', extra: null }, { id: 'p500', credits: 500, price: 2497.00, unit: 4.99, highlight: null, extra: 'Prioridade na fila' }, ]; const methods = [ { id: 'pix', label: 'PIX', time: 'Instantâneo', tag: 'Recomendado' }, { id: 'cartao', label: 'Cartão', time: 'Recorrente mensal' }, ]; const selected = packages.find((p) => p.id === picked); return (
{packages.map((p) => { const active = p.id === picked; return ( ); })}
Método de pagamento
{methods.map((m) => { const active = method === m.id; return ( ); })}
Resumo: {selected.credits} créditos · {method}
{fmtBRL.format(selected.price)}
Nota fiscal eletrônica emitida em até 24h após pagamento.
); } // Modal de senha pra PDF protegido: o backend devolveu 422 PDF_PASSWORD_REQUIRED // e o submit original foi suspenso. Aqui o usuário digita a senha e a UI retenta // o mesmo upload via onSubmit (que repassa pra submitNewAnalysis(bundle, {pdf_password})). function PdfPasswordModal({ open, prompt, onClose, onSubmit }) { const [password, setPassword] = useState(''); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (open) { setPassword(''); setSubmitting(false); } }, [open, prompt]); if (!open || !prompt) return null; const submit = (e) => { e?.preventDefault?.(); if (!password.trim() || submitting) return; setSubmitting(true); onSubmit(prompt.bundle, { pdf_password: password }); }; const fileName = prompt.bundle?.file?.name || 'contrato.pdf'; return (

O arquivo {fileName} está protegido por senha. Informe a senha para que possamos abrir o PDF e processar a análise.

A senha é usada apenas para abrir o documento neste momento — ela não fica armazenada.

setPassword(e.target.value)} placeholder="Digite a senha" className="w-full px-3 py-2.5 text-[14px] border border-ink-200 rounded-md focus:border-navy-800 focus:ring-1 focus:ring-navy-800/20" /> {prompt.error && (
{prompt.error}
)}
); } Object.assign(window, { Dashboard, NewAnalysisModal, BuyCreditsModal, PdfPasswordModal });