Files
jorgecuadros-platform/apps/web/src/components/PolicyCaptura.tsx
T
rmancinasandClaude Opus 5 5e9cb12fba
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m43s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m0s
feat(polizas): OCR capture for insurance policy PDFs
Mirrors the utility statement intake on the insurance side: a policy_ocr
batch/document pair of tables, a GMX parser, a matcher keyed on
Policy.policyNumber, and a "Captura" screen under /polizas that proposes
policy -> customer for staff to confirm.

Lifts the OCR seam out of StatementsModule into its own OcrModule so
PolicyOcrModule can inject OCR_PROVIDER without taking on the rest of
the statement pipeline; StatementsModule now imports it and binds
nothing itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:07:29 -07:00

124 lines
4.0 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { PolicyForm } from "@/components/PolicyForm";
import { PolicyOcrIntake } from "@/components/PolicyOcrIntake";
import { useCan } from "@/lib/abilities";
/**
* Policy intake — mirror of `Captura.tsx` (statement OCR side): one screen,
* two ways in:
*
* - **manual** — `PolicyForm` keys every field by hand.
* - **auto** — `PolicyOcrIntake` uploads a GMX PDF, OCR proposes the
* policy, a human still confirms.
*
* Both end at the same place (a `Policy` row on a customer's file) so they
* live as two modes of one screen rather than two menu entries — exactly the
* same shape Captura uses for `ManualCheckCapture` vs `StatementIntake`.
*
* `/polizas/nuevo` opens manual, `/polizas/captura` opens auto; both render
* this component so the tab toggle works either way and an old bookmark
* still lands on the right tab.
*/
export type PolicyCaptureMode = "manual" | "auto";
const MODE_HINT: Record<PolicyCaptureMode, string> = {
manual:
"Captura cada campo a mano. Use esta opción cuando la póliza llega en papel, en un correo sin PDF legible, o cuando hay que revisar cada dato.",
auto: "Suelte el PDF descargado del portal de GMX y el sistema propondrá los campos. Nada se registra sin tu confirmación.",
};
export function PolicyCaptura({ initialMode = "manual" }: { initialMode?: PolicyCaptureMode }) {
const canCreate = useCan("policy:create");
const canIngest = useCan("policy:ingest");
// `/clientes/[id]` deep-links into /polizas/nuevo with the customer
// pre-picked so staff can fill the rest without retyping. The OCR pane
// ignores these — there's no customer to lock in until the batch is
// confirmed.
const params = useSearchParams();
const fixedCustomerId = params.get("customerId") ?? undefined;
const fixedCustomerName = params.get("customerName") ?? undefined;
// One user can land on either mode. The tab strip only renders when both
// abilities are held — a STAFF with only policy:ingest (no create) still
// sees the screen but only the OCR tab is offered.
const modes: { key: PolicyCaptureMode; label: string }[] = [
...(canCreate ? [{ key: "manual" as const, label: "Captura manual" }] : []),
...(canIngest ? [{ key: "auto" as const, label: "Captura automática (OCR)" }] : []),
];
const [mode, setMode] = useState<PolicyCaptureMode>(
modes.some((m) => m.key === initialMode) ? initialMode : (modes[0]?.key ?? "manual"),
);
if (modes.length === 0) {
return (
<div className="state-box state-error">
No tienes permiso para crear ni capturar pólizas.
</div>
);
}
return (
<>
<div className="page-head">
<Link href="/polizas" className="back-link"> Pólizas</Link>
<h1 className="page-title">Nueva póliza</h1>
<p className="eyebrow">{MODE_HINT[mode]}</p>
</div>
{modes.length > 1 && (
<div className="seg" role="tablist" style={{ marginBottom: 16 }}>
{modes.map((m) => (
<button
key={m.key}
type="button"
role="tab"
aria-selected={mode === m.key}
className={`seg-btn ${mode === m.key ? "active" : ""}`}
onClick={() => setMode(m.key)}
>
{m.label}
</button>
))}
</div>
)}
{mode === "manual" ? (
<ManualPane
fixedCustomerId={fixedCustomerId}
fixedCustomerName={fixedCustomerName}
/>
) : (
<PolicyOcrIntake />
)}
</>
);
}
function ManualPane({
fixedCustomerId,
fixedCustomerName,
}: {
fixedCustomerId?: string;
fixedCustomerName?: string;
}) {
const allowed = useCan("policy:create");
if (!allowed) {
return (
<div className="state-box state-error">
No tiene permisos para crear pólizas.
</div>
);
}
return (
<PolicyForm
fixedCustomerId={fixedCustomerId}
fixedCustomerName={fixedCustomerName}
/>
);
}