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>
124 lines
4.0 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
} |