"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; import { getCustomer } from "@/lib/api"; import { domainLabel, formatDate, formatMoney, serviceKindGlyph, serviceKindLabel, sourceSystemLabel, } from "@/lib/labels"; import type { CustomerDetail, Installment, Policy, Property, Transaction, TransactionSummaryRow, } from "@/lib/types"; export default function ClienteDetailPage({ params, }: { params: { id: string }; }) { 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); getCustomer(id) .then((d) => { if (alive) { setData(d); setLoading(false); } }) .catch((e) => { if (alive) { setError( e?.status === 404 ? "No encontramos este cliente." : e?.message ?? "No se pudo cargar el cliente.", ); setLoading(false); } }); return () => { alive = false; }; }, [id]); if (loading) return ; if (error) return ( <>
{error}
); if (!data) return null; const hasUtilities = data.properties.length > 0; const hasInsurance = data.policies.length > 0; return (
); } function BackLink() { return ( ← Volver a Clientes ); } /* ------------------------------------------------------------------ Hero */ function Hero({ data, hasUtilities, hasInsurance, }: { data: CustomerDetail; hasUtilities: boolean; hasInsurance: boolean; }) { const provenance = data.legacyRefs .map((r) => `${sourceSystemLabel(r.sourceSystem)} #${r.legacyId}`) .join(" · "); const facts: { label: string; value: string }[] = [ { label: "Cliente desde", value: formatDate(data.customerSince) }, { label: "Cuota", value: data.feeAmount != null && data.feeAmount !== "" ? formatMoney(data.feeAmount, data.preferredCurrency) : "—", }, { label: "Propiedades", value: String(data.properties.length) }, { label: "Pólizas", value: String(data.policies.length) }, { label: "Moneda", value: data.preferredCurrency ?? "—" }, ]; return (

{data.name}

{provenance && (
Origen: {provenance}
)}
{hasUtilities && ( Servicios )} {hasInsurance && ( Seguros )} {data.status ? "Activo" : "Inactivo"}
{facts.map((f) => (
{f.label}
{f.value}
))}
); } /* ------------------------------------------------------- Datos del cliente */ function DatosSection({ data }: { data: CustomerDetail }) { const mxAddress = [data.addressLine1, data.addressLine2] .filter(Boolean) .join(", "); const cityLine = [ data.city?.replace(/,\s*$/, ""), data.state, data.zipCode, data.country, ] .filter(Boolean) .join(", "); const idLine = data.identificationNumber || data.identificationType ? [ data.identificationType, data.identificationNumber, data.identificationExpiration ? `vence ${formatDate(data.identificationExpiration)}` : null, ] .filter(Boolean) .join(" · ") : null; return (
{(mxAddress || cityLine) && (
Domicilio
{mxAddress &&
{mxAddress}
} {cityLine &&
{cityLine}
} {!mxAddress && !cityLine && "—"}
)} {data.notes && (
Notas
{data.notes}
)}
); } function KV({ label, value, mono, }: { label: string; value: string | null | undefined; mono?: boolean; }) { return (
{label}
{value || "—"}
); } /* ----------------------------------------------- Propiedades y servicios */ function PropiedadesSection({ properties }: { properties: Property[] }) { return (
{properties.length === 0 ? (
Este cliente no tiene propiedades registradas.
) : ( properties.map((p) => ) )}
); } function PropertyCard({ p }: { p: Property }) { const addr = [p.addressLine1, p.addressLine2].filter(Boolean).join(", "); const phones = [p.phone1, p.phone2, p.phone3].filter(Boolean); return (
{addr || "Propiedad"}
{p.zone && Zona: {p.zone}} {phones.length > 0 && ( {phones.join(" · ")} )}
{p.services.length > 0 && (
{p.services.map((s) => (
{serviceKindGlyph(s.kind)} {serviceKindLabel(s.kind)} {!s.active && ( Inactivo )}
{s.accountNumber && ( Cuenta: {s.accountNumber} )} {s.meterNumber && ( Medidor: {s.meterNumber} )} {s.route && ( Ruta: {s.route} )} {s.dueDay && Día de pago: {s.dueDay}} {s.notes && {s.notes}}
))}
)} {p.trustAccount && (
Fideicomiso
{p.trustAccount.bankName && ( Banco: {p.trustAccount.bankName} )} {p.trustAccount.trustNumber && ( No.:{" "} {p.trustAccount.trustNumber} )} {p.trustAccount.bankFee && ( Comisión:{" "} {formatMoney(p.trustAccount.bankFee, "MXN")} )} {p.trustAccount.dueDate1 && ( Vigencia:{" "} {formatDate(p.trustAccount.dueDate1)} {p.trustAccount.dueDate2 ? ` – ${formatDate(p.trustAccount.dueDate2)}` : ""} )}
)}
); } /* ------------------------------------------------------ Pólizas de seguro */ function PolizasSection({ policies }: { policies: Policy[] }) { return (
{policies.length === 0 ? (
Este cliente no tiene pólizas registradas.
) : ( policies.map((p) => ) )}
); } function PolicyCard({ p }: { p: Policy }) { const headline = p.total ?? p.netPremium; const headlineLabel = p.total ? "Total" : "Prima neta"; return (
{p.policyNumber || "—"}
{p.policyType?.name && ( {p.policyType.name} )} {p.insuranceProvider?.name && ( {p.insuranceProvider.name} )} {p.agentName && ( <> · Agente: {p.agentName} )} {p.liquidated ? "Liquidada" : "Pendiente"}
Vigencia: {formatDate(p.policyFrom)} – {formatDate(p.policyTo)}
{formatMoney(headline, p.currency)}
{headlineLabel}
{p.total && p.netPremium && (
Prima neta {formatMoney(p.netPremium, p.currency)}
)}
{p.installments.length > 0 && (
Pagos {p.installments.length}
{p.installments.map((inst) => ( ))}
)} {p.vehicles.length > 0 && (
Vehículos {p.vehicles.length}
{p.vehicles.map((v) => (
{[v.make, v.model, v.modelYear].filter(Boolean).join(" ") || "Vehículo"}
{[ v.bodyType, v.licensePlate ? `Placa ${v.licensePlate}` : null, ] .filter(Boolean) .join(" · ")}
))}
)} {p.insuredDrivers.length > 0 && (
Asegurados {p.insuredDrivers.length}
{p.insuredDrivers.map((d) => (
{d.fullName || "—"} {d.licenseNumber && (
Lic. {d.licenseNumber}
)}
))}
)} {p.beneficiaries.length > 0 && (
Beneficiarios {p.beneficiaries.length}
{p.beneficiaries.map((b) => (
{b.name || "—"} {(b.phone || b.email) && (
{[b.phone, b.email].filter(Boolean).join(" · ")}
)}
))}
)}
); } 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"} {method && ( {" "} · {method} )} {formatMoney(inst.amount, inst.currency)}
); } /* ------------------------------------------------------- Estado de cuenta */ function EstadoCuentaSection({ summary, transactions, }: { summary: TransactionSummaryRow[]; transactions: Transaction[]; }) { return (
{summary.length > 0 && (
{summary.map((row, i) => (
{domainLabel(row.domain)} · {row.currency}
{formatMoney(row.total, row.currency)}
{row.count}{" "} {row.count === 1 ? "movimiento" : "movimientos"}
))}
)}
{transactions.length === 0 ? (
Sin movimientos registrados.
) : (
{transactions.map((t) => ( ))}
Fecha Línea Tipo Referencia Concepto Monto
)} {transactions.length >= 100 && (
Mostrando los 100 movimientos más recientes.
)}
); } function TxRow({ t }: { t: Transaction }) { const num = t.amount != null ? Number(t.amount) : NaN; const sign = !Number.isNaN(num) && num < 0 ? "neg" : "pos"; const tipo = t.type?.nameEs || t.type?.nameEn || "—"; const concept = t.message || t.period || "—"; return ( {formatDate(t.transactionDate)} {domainLabel(t.domain)} {tipo} {t.reference || "—"} {concept} {formatMoney(t.amount, t.currency)} {" "} {t.currency} ); } /* ----------------------------------------------------------- Documentos */ function DocumentosSection({ data }: { data: CustomerDetail }) { type Doc = { type: string; key: string | null; scope: string }; const docs: Doc[] = []; data.properties.forEach((p) => { const label = [p.addressLine1].filter(Boolean).join("") || "Propiedad"; p.documents.forEach((d) => docs.push({ type: d.documentType || "Documento", key: d.storageKey, scope: label, }), ); }); data.policies.forEach((p) => { p.documents.forEach((d) => docs.push({ type: d.documentType || "Documento", key: d.storageKey, scope: `Póliza ${p.policyNumber ?? ""}`.trim(), }), ); }); return (
{docs.length === 0 ? (
No hay documentos registrados para este cliente.
) : ( <>
{docs.map((d, i) => (
{d.type}
{d.scope}
))}
Los archivos se almacenan en el object storage (storageKey); no se descargan desde esta vista.
)}
); } /* -------------------------------------------------------------- helpers */ function SectionHead({ rule, title, count, countSuffix, }: { rule: string; title: string; count?: number; countSuffix?: string; }) { return (

{title}

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