feat(polizas): OCR capture for insurance policy PDFs
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m43s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m0s

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>
This commit is contained in:
rmancinasandClaude Opus 5 committed 2026-08-01 14:07:29 -07:00
1 parent 5bce0e4c94
commit 5e9cb12fba
22 files changed
+3203 -136

No files matched your search

@@ -0,0 +1,16 @@
"use client";
import { AppShell } from "@/components/AppShell";
import { PolicyOcrReview } from "@/components/PolicyOcrReview";
export default function PolicyOcrBatchPage({
params,
}: {
params: { id: string };
}) {
return (
<AppShell>
<PolicyOcrReview id={params.id} />
</AppShell>
);
}
+21
View File
@@ -0,0 +1,21 @@
"use client";
import { AppShell } from "@/components/AppShell";
import { PolicyCaptura } from "@/components/PolicyCaptura";
/**
* OCR mode of the policy intake screen. Drops the GMX PDF, walks through
* per-page review, confirms. Same wrapper as `/polizas/nuevo` (manual)
* with `initialMode="auto"`, so the tab strip is identical and swapping
* modes doesn't drop state.
*
* Sister route `/polizas/captura/[id]` is the batch review screen once a
* batch is uploaded.
*/
export default function CapturaOcrPage() {
return (
<AppShell>
<PolicyCaptura initialMode="auto" />
</AppShell>
);
}
+9 -28
View File
@@ -1,41 +1,22 @@
"use client";
import { Suspense } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { AppShell } from "@/components/AppShell";
import { PolicyForm } from "@/components/PolicyForm";
import { useCan } from "@/lib/abilities";
import { PolicyCaptura } from "@/components/PolicyCaptura";
/**
* Manual mode of the policy intake screen. Shares the tab wrapper with
* `/polizas/captura` (OCR mode) so staff can swap between the two without
* losing their place. Customer picker comes from the `?customerId=`
* / `?customerName=` query string — used by `/clientes/[id]` when staff
* creates a policy from a customer detail page.
*/
export default function NuevaPolizaPage() {
return (
<AppShell>
<Suspense fallback={null}>
<NuevaPoliza />
<PolicyCaptura initialMode="manual" />
</Suspense>
</AppShell>
);
}
function NuevaPoliza() {
const allowed = useCan("policy:create");
const params = useSearchParams();
const customerId = params.get("customerId") ?? undefined;
const customerName = params.get("customerName") ?? undefined;
return (
<>
<div className="page-head">
<Link href="/polizas" className="back-link">← Pólizas</Link>
<h1 className="page-title">Nueva póliza</h1>
</div>
{allowed ? (
<PolicyForm fixedCustomerId={customerId} fixedCustomerName={customerName} />
) : (
<div className="state-box state-error">
No tiene permisos para crear pólizas.
</div>
)}
</>
);
}
+6
View File
@@ -57,6 +57,7 @@ export default function PolizasPage() {
function PolizasBrowser() {
const canCreate = useCan("policy:create");
const canIngest = useCan("policy:ingest");
const [stats, setStats] = useState<PolicyStats | null>(null);
const [facets, setFacets] = useState<PolicyFacets | null>(null);
@@ -135,6 +136,11 @@ function PolizasBrowser() {
{ slug: "vigente", label: "Por vencer (Incen.)", params: { typeName: "INCEN" } },
]}
/>
{canIngest && (
<Link href="/polizas/captura" className="btn btn-outline">
+ Captura OCR
</Link>
)}
{canCreate && (
<Link href="/polizas/nuevo" className="btn btn-primary">+ Nueva póliza</Link>
)}