"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; import { getProperty } from "@/lib/api"; import { expiryPhrase, formatDate, formatMoney, formatNumber, serviceKindGlyph, serviceKindLabel, serviceNoteLabel, SIN_NOMBRE, trustStatusLabel, } from "@/lib/labels"; import type { PropertyDetail, Service, Transaction } from "@/lib/types"; export default function PropiedadDetailPage({ 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); getProperty(id) .then((d) => { if (alive) { setData(d); setLoading(false); } }) .catch((e) => { if (alive) { setError( e?.status === 404 ? "No encontramos esta propiedad." : e?.message ?? "No se pudo cargar la propiedad.", ); setLoading(false); } }); return () => { alive = false; }; }, [id]); if (loading) return ; if (error) return ( <>
{error}
); if (!data) return null; return (
{data.policy && }
); } function BackLink() { return ( ← Volver a Propiedades ); } /* ------------------------------------------------------------------ Hero */ function Hero({ data }: { data: PropertyDetail }) { const addr = [data.addressLine1, data.addressLine2].filter(Boolean).join(", "); const phones = [data.phone1, data.phone2, data.phone3].filter(Boolean); const provenance = [data.legacySourceTable, data.legacyId] .filter(Boolean) .join(" #"); const activos = data.services.filter((s) => s.active).length; const phrase = expiryPhrase(data.daysToTrustDue); const facts: { label: string; value: string }[] = [ { label: "Cliente", value: data.customer.name }, { label: "Servicios", value: data.services.length === 0 ? "Ninguno" : `${activos} activos de ${data.services.length}`, }, { label: "Municipio", value: data.municipality || "—" }, { label: "Fideicomiso", value: data.trustAccount ? formatDate(data.trustAccount.dueDate2) : "Sin fideicomiso", }, { label: "Teléfonos", value: phones.join(" · ") || "—" }, ]; return (

{addr || "Propiedad sin dirección"}

{data.zone ? `Zona ${data.zone} · ` : ""} {data.customer.name}
{provenance && (
Origen: {provenance}
)}
{data.municipality && ( {data.municipality} )} {data.trustAccount ? ( Fideicomiso · {trustStatusLabel(data.trustStatus)} {phrase && data.trustStatus !== "expired" ? ` · ${phrase}` : ""} ) : ( Sin fideicomiso )} {data.services.length === 0 && ( Sin servicios )}
{facts.map((f) => (
{f.label}
{f.value}
))}
); } /* -------------------------------------------------------------- Cliente */ function ClienteSection({ data }: { data: PropertyDetail }) { const c = data.customer; const location = [c.city?.replace(/,\s*$/, ""), c.state] .filter(Boolean) .join(", "); return (
{c.name}
{location && {location}} {location && (c.phone || c.mobile || c.email) && ( · )} {(c.phone || c.mobile) && {c.phone || c.mobile}} {c.email && ( <> · {c.email} )} {c._count.policies > 0 && ( <> · {c._count.policies}{" "} {c._count.policies === 1 ? "póliza" : "pólizas"} )}
Ver expediente → {data.siblings.length > 0 && (
Otras propiedades de este cliente ({data.siblings.length})
{data.siblings.map((s) => ( {[s.addressLine1, s.addressLine2].filter(Boolean).join(", ") || "Propiedad"} {s.zone ? ` · Zona ${s.zone}` : ""} · {s.serviceCount}{" "} {s.serviceCount === 1 ? "servicio" : "servicios"} ))}
)}
); } /* ------------------------------------------------------------- Servicios */ function ServiciosSection({ data }: { data: PropertyDetail }) { return (
{data.services.length === 0 ? (
Esta propiedad no tiene servicios registrados.
) : (
{data.services.map((s) => ( ))}
)}
); } function ServiceCard({ s }: { s: Service }) { const noteLabel = serviceNoteLabel(s.kind); return (
{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 && ( {noteLabel ? `${noteLabel}: ` : ""} {s.notes} )}
); } /* ---------------------------------------------------------- Fideicomiso */ function FideicomisoSection({ data }: { data: PropertyDetail }) { const t = data.trustAccount; const phrase = expiryPhrase(data.daysToTrustDue); return (
{!t ? (
Esta propiedad no tiene fideicomiso registrado.
) : ( <>
La comisión bancaria se cobra cada año en el próximo vencimiento; el sistema anterior guardaba el par de fechas (vence1 / vence2) del periodo en curso y del siguiente.
)}
); } /* --------------------------------------------------------------- Póliza */ function PolizaSection({ data }: { data: PropertyDetail }) { const p = data.policy!; return (
{p.policyNumber || "—"}
{p.policyType?.name && {p.policyType.name}} {p.policyTo && ( <> · Vence {formatDate(p.policyTo)} )}
Ver póliza →
); } /* ---------------------------------------------------------- Movimientos */ function MovimientosSection({ data }: { data: PropertyDetail }) { return (
{data.customerLedger.length > 0 && (
{data.customerLedger.map((row) => (
Servicios · {row.currency}
{formatMoney(row.total, row.currency)}
{formatNumber(row.count)}{" "} {row.count === 1 ? "movimiento" : "movimientos"}
))}
)}
{data.customerTransactions.length === 0 ? (
Sin movimientos de servicios.
) : (
{data.customerTransactions.map((t) => ( ))}
Fecha Tipo Periodo Referencia Monto
)}
Los movimientos pertenecen al cliente, no a esta propiedad: el sistema anterior nunca ligó un pago a una propiedad concreta. Ver el{" "} estado de cuenta completo .
); } function TxRow({ t }: { t: Transaction }) { const num = t.amount != null ? Number(t.amount) : NaN; const sign = !Number.isNaN(num) && num < 0 ? "neg" : "pos"; return ( {formatDate(t.transactionDate)} {t.type?.nameEs || t.type?.nameEn || "—"} {t.period || "—"} {t.reference || "—"} {formatMoney(t.amount, t.currency)} {" "} {t.currency} ); } /* ----------------------------------------------------------- Documentos */ function DocumentosSection({ data }: { data: PropertyDetail }) { return (
{data.documents.length === 0 ? (
No hay documentos registrados para esta propiedad.
) : ( <>
{data.documents.map((d, i) => (
{d.documentType || "Documento"}
{d.storageKey || "—"}
))}
Los archivos se almacenan en el object storage (storageKey); no se descargan desde esta vista.
)}
); } /* ------------------------------------------------------------ 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 (
); }