"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; import { ContextReports } from "@/components/ContextReports"; import { addPolicyChild, archivePolicy, getLookups, getPolicy, policyDocumentDownloadUrl, removePolicyChild, removePolicyDocument, restorePolicy, updatePolicyChild, uploadPolicyDocument, } from "@/lib/api"; import { useCan } from "@/lib/abilities"; import { ChildCollection, type ChildConfig } from "@/components/ChildCollection"; import { expiryPhrase, formatDate, formatMoney, policyStatusLabel, premiumHeadline, SIN_NOMBRE, } from "@/lib/labels"; import type { AdjusterRow, Installment, PolicyDetail } from "@/lib/types"; export default function PolizaDetailPage({ params, }: { params: { id: string }; }) { // Next 14 passes `params` as a plain object here — no `use()` unwrapping. const { id } = params; return ( ); } function Detail({ id }: { id: string }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let alive = true; setLoading(true); setError(null); getPolicy(id) .then((d) => { if (alive) { setData(d); setLoading(false); } }) .catch((e) => { if (alive) { setError( e?.status === 404 ? "No encontramos esta póliza." : e?.message ?? "No se pudo cargar la póliza.", ); setLoading(false); } }); return () => { alive = false; }; }, [id]); if (loading) return ; if (error) return ( <>
{error}
); if (!data) return null; const reload = () => getPolicy(id).then(setData).catch(() => {}); return (
{data.installments.length > 0 && } {data.vehicles.length > 0 && } {(data.insuredDrivers.length > 0 || data.beneficiaries.length > 0) && ( )} {data.claims.length > 0 && }
); } /** Edit / archive controls for the policy header. */ function PolicyActions({ data, onChange, }: { data: PolicyDetail; onChange: () => void; }) { const canEdit = useCan("policy:update"); const canDelete = useCan("policy:delete"); const [busy, setBusy] = useState(false); const archived = data.archivedAt != null; async function toggle() { const verb = archived ? "restaurar" : "archivar"; if (!window.confirm(`¿Seguro que desea ${verb} esta póliza?`)) return; setBusy(true); try { if (archived) await restorePolicy(data.id); else await archivePolicy(data.id); onChange(); } catch (e) { window.alert((e as Error)?.message ?? "No se pudo completar la acción."); } finally { setBusy(false); } } if (!canEdit && !canDelete) return null; return (
{archived && Archivada} {canEdit && ( Editar )} {canDelete && ( )}
); } /** Editable child collections — only shown to users who can edit the policy. */ function ChildrenEditor({ data, onChange, }: { data: PolicyDetail; onChange: () => void; }) { const canEdit = useCan("policy:update"); const [adjusters, setAdjusters] = useState([]); useEffect(() => { if (canEdit) getLookups().then((l) => setAdjusters(l.adjusters)).catch(() => {}); }, [canEdit]); if (!canEdit) return null; const INSTALLMENTS: ChildConfig = { apiKind: "installments", title: "Pagos", fields: [ { key: "sequence", label: "Sec.", type: "number" }, { key: "amount", label: "Monto", type: "number" }, { key: "currency", label: "Moneda", type: "select", options: [{ value: "MXN", label: "MXN" }, { value: "USD", label: "USD" }] }, { key: "dueDate", label: "Vence", type: "date" }, { key: "paidDate", label: "Pagado", type: "date" }, { key: "checkNumber", label: "Cheque" }, { key: "isCash", label: "Efectivo", type: "checkbox" }, ], }; const VEHICLES: ChildConfig = { apiKind: "vehicles", title: "Vehículos", fields: [ { key: "make", label: "Marca" }, { key: "model", label: "Modelo" }, { key: "modelYear", label: "Año" }, { key: "licensePlate", label: "Placa" }, { key: "vinNumber", label: "VIN" }, { key: "stateCode", label: "Estado" }, ], }; const DRIVERS: ChildConfig = { apiKind: "drivers", title: "Conductores", fields: [ { key: "fullName", label: "Nombre" }, { key: "birthDate", label: "Nacimiento", type: "date" }, { key: "sex", label: "Sexo" }, { key: "occupation", label: "Ocupación" }, { key: "licenseNumber", label: "Licencia" }, { key: "licenseState", label: "Estado" }, ], }; const BENEFICIARIES: ChildConfig = { apiKind: "beneficiaries", title: "Beneficiarios", fields: [ { key: "name", label: "Nombre" }, { key: "phone", label: "Teléfono" }, { key: "email", label: "Correo" }, { key: "address", label: "Dirección" }, ], }; const CLAIMS: ChildConfig = { apiKind: "claims", title: "Siniestros", fields: [ { key: "claimType", label: "Tipo" }, { key: "incidentDate", label: "Fecha", type: "date" }, { key: "description", label: "Descripción" }, { key: "adjusterId", label: "Ajustador", type: "select", options: adjusters.map((a) => ({ value: a.id, label: a.name ?? a.company ?? a.id })) }, { key: "claimedAmount", label: "Reclamado", type: "number" }, { key: "settledAmount", label: "Pagado", type: "number" }, { key: "resolved", label: "Resuelto", type: "checkbox" }, ], }; const bind = (cfg: ChildConfig, rows: Record[]) => ( { await addPolicyChild(data.id, cfg.apiKind, p); onChange(); }} onSave={async (cid, p) => { await updatePolicyChild(data.id, cfg.apiKind, cid, p); onChange(); }} onRemove={async (cid) => { await removePolicyChild(data.id, cfg.apiKind, cid); onChange(); }} /> ); return (

Administrar detalles

{bind(INSTALLMENTS, data.installments as unknown as Record[])} {bind(VEHICLES, data.vehicles as unknown as Record[])} {bind(DRIVERS, data.insuredDrivers as unknown as Record[])} {bind(BENEFICIARIES, data.beneficiaries as unknown as Record[])} {bind(CLAIMS, data.claims as unknown as Record[])}
); } function BackLink() { return ( ← Volver a Pólizas ); } /* ------------------------------------------------------------------ Hero */ function Hero({ data }: { data: PolicyDetail }) { const premium = premiumHeadline(data); const phrase = expiryPhrase(data.daysToExpiry); const provenance = [data.legacySourceTable, data.legacyId] .filter(Boolean) .join(" #"); const facts: { label: string; value: string }[] = [ { label: "Vigencia desde", value: formatDate(data.policyFrom) }, { label: "Vigencia hasta", value: formatDate(data.policyTo) }, { label: premium.label, value: formatMoney(premium.value, data.currency) }, { label: "Moneda", value: data.currency ?? "—" }, { label: "Agente", value: data.agentName || "—" }, ]; return (

{data.policyNumber || "—"}

{[data.policyType?.name, data.insuranceProvider?.name] .filter(Boolean) .join(" · ") || "Sin ramo ni aseguradora registrados"}
{provenance && (
Origen: {provenance}
)}
{policyStatusLabel(data.status)} {phrase && data.status !== "expired" ? ` · ${phrase}` : ""} {data.liquidated ? "Liquidada" : "Sin liquidar"} {data.endorsement && ( Endoso )}
{facts.map((f) => (
{f.label}
{f.value}
))}
); } /* -------------------------------------------------------------- Cliente */ function ClienteSection({ data }: { data: PolicyDetail }) { const c = data.customer; const location = [c.city?.replace(/,\s*$/, ""), c.state] .filter(Boolean) .join(", "); return (
{c.name}
{location && {location}} {location && (c.phone || c.email) && ( · )} {(c.phone || c.mobile) && {c.phone || c.mobile}} {c.email && ( <> · {c.email} )}
Ver expediente → {data.properties.length > 0 && (
Propiedades cubiertas
{data.properties.map((p) => ( {[p.addressLine1, p.addressLine2].filter(Boolean).join(", ") || "Propiedad"} {p.zone && · Zona {p.zone}} ))}
)}
); } /* ---------------------------------------------------------- Condiciones */ function CondicionesSection({ data }: { data: PolicyDetail }) { const cur = data.currency; return (
{/* The legacy `total` is 0 or null on all but 2 of 2378 policies — only show it when it actually carries a figure. */} {data.total != null && Number(data.total) > 0 && ( )} {data.observations && (
Observaciones
{data.observations}
)} {data.notes && (
Notas
{data.notes}
)}
); } /* --------------------------------------------------------------- Pagos */ function PagosSection({ data }: { data: PolicyDetail }) { const paid = data.installments.filter((i) => i.paidDate).length; return (
{data.installments.map((inst) => ( ))}
); } function InstallmentRow({ inst }: { inst: Installment }) { const method = inst.isCash ? "Efectivo" : inst.checkNumber ? `Ref. ${inst.checkNumber}` : null; return (
{inst.sequence} {inst.paidDate ? formatDate(inst.paidDate) : "Sin pagar"} {inst.dueDate && !inst.paidDate && ( {" "} · vence {formatDate(inst.dueDate)} )} {method && ( {" "} · {method} )} {formatMoney(inst.amount, inst.currency)}
); } /* ----------------------------------------------------------- Vehículos */ function VehiculosSection({ data }: { data: PolicyDetail }) { return (
{data.vehicles.map((v) => (
{[v.make, v.model, v.modelYear].filter(Boolean).join(" ") || "Vehículo"}
{v.bodyType && {v.bodyType}} {v.licensePlate && ( Placa: {v.licensePlate} )} {v.vinNumber && ( Serie: {v.vinNumber} )} {v.engineNumber && ( Motor: {v.engineNumber} )}
))}
); } /* ------------------------------------------- Asegurados y beneficiarios */ function PersonasSection({ data }: { data: PolicyDetail }) { return (
{data.insuredDrivers.length > 0 && (
Asegurados {data.insuredDrivers.length}
{data.insuredDrivers.map((d) => (
{d.fullName || "—"} {d.licenseNumber && (
Lic. {d.licenseNumber}
)}
))}
)} {data.beneficiaries.length > 0 && (
Beneficiarios {data.beneficiaries.length}
{data.beneficiaries.map((b) => (
{b.name || "—"} {(b.phone || b.email) && (
{[b.phone, b.email].filter(Boolean).join(" · ")}
)}
))}
)}
); } /* --------------------------------------------------------- Siniestros */ function SiniestrosSection({ data }: { data: PolicyDetail }) { return (
{data.claims.map((c) => (
{c.claimType || "Siniestro"}
{c.incidentDate && ( Ocurrido: {formatDate(c.incidentDate)} )} {c.reportedDate && ( Reportado: {formatDate(c.reportedDate)} )} {c.adjuster?.name && Ajustador: {c.adjuster.name}}
{c.description && (
Descripción
{c.description}
)}
))}
); } /* -------------------------------------------------------- Coberturas */ /** The legacy tables carry per-line coverage columns the target schema does * not model; the migration preserved them verbatim in `coveragesJson`. */ function CoberturasSection({ data }: { data: PolicyDetail }) { const entries = Object.entries(data.coveragesJson ?? {}).filter( ([, v]) => v !== null && v !== "" && v !== 0, ); if (entries.length === 0) return null; return (
{entries.map(([k, v]) => (
{k}
{String(v)}
))}
Campos de cobertura conservados tal cual desde el sistema anterior.
); } /* -------------------------------------------------------- Documentos */ function DocumentosSection({ data, onChange, }: { data: PolicyDetail; onChange: () => void; }) { const canEdit = useCan("policy:update"); const [file, setFile] = useState(null); const [type, setType] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); async function upload() { if (!file) return; setBusy(true); setError(null); try { await uploadPolicyDocument(data.id, file, type.trim() || undefined); setFile(null); setType(""); onChange(); } catch (e) { setError((e as Error)?.message ?? "No se pudo subir el archivo."); } finally { setBusy(false); } } return (
{data.documents.length === 0 ? (
No hay documentos registrados para esta póliza.
) : (
{data.documents.map((d, i) => (
{d.documentType || "Documento"}
{d.storageKey || "—"}
{d.id && ( Descargar )} {canEdit && d.id && ( )}
))}
)} {canEdit && (
{error && (
{error}
)}
setType(e.target.value)} /> setFile(e.target.files?.[0] ?? null)} />
)}
); } /* ------------------------------------------------------------ helpers */ function KV({ label, value, }: { label: string; value: string | null | undefined; }) { return (
{label}
{value || "—"}
); } function SectionHead({ rule, title, count, countSuffix, }: { rule: string; title: string; count?: number; countSuffix?: string; }) { return (

{title}

{count != null && ( {count} {countSuffix ?? ""} )}
); } function DetailSkeleton() { return (
); }