(null);
async function upload() {
if (!file) return;
setBusy(true);
setError(null);
try {
await uploadPropertyDocument(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 (
Documentos
{data.documents.length > 0 && (
Tipo Clave Acción
{data.documents.map((d) => (
{d.documentType ?? "—"}
{d.storageKey ?? "—"}
{d.id && (
Descargar
)}
{
if (!d.id) return;
if (!window.confirm("¿Eliminar este documento?")) return;
try {
await removePropertyDocument(data.id, d.id);
onChange();
} catch (e) {
window.alert((e as Error)?.message ?? "No se pudo eliminar.");
}
}}
>
Eliminar
))}
)}
{error &&
{error}
}
setType(e.target.value)}
/>
setFile(e.target.files?.[0] ?? null)}
/>
{busy ? "Subiendo…" : "Subir"}
);
}
/** Trust is 1:1 — a small inline form that upserts or clears it. */
function TrustEditor({
data,
onChange,
}: {
data: PropertyDetail;
onChange: () => void;
}) {
const t = data.trustAccount;
const [bankName, setBankName] = useState(t?.bankName ?? "");
const [trustNumber, setTrustNumber] = useState(t?.trustNumber ?? "");
const [bankFee, setBankFee] = useState(t?.bankFee != null ? String(t.bankFee) : "");
const [dueDate1, setDueDate1] = useState(toDateInput(t?.dueDate1));
const [dueDate2, setDueDate2] = useState(toDateInput(t?.dueDate2));
const [busy, setBusy] = useState(false);
async function save() {
setBusy(true);
const input: TrustInput = {
bankName: bankName.trim() || undefined,
trustNumber: trustNumber.trim() || undefined,
bankFee: bankFee.trim() === "" ? undefined : Number(bankFee),
dueDate1: dueDate1 || undefined,
dueDate2: dueDate2 || undefined,
};
try {
await upsertTrust(data.id, input);
onChange();
} catch (e) {
window.alert((e as Error)?.message ?? "No se pudo guardar el fideicomiso.");
} finally {
setBusy(false);
}
}
async function clear() {
if (!window.confirm("¿Eliminar el fideicomiso de esta propiedad?")) return;
try {
await removeTrust(data.id);
onChange();
} catch (e) {
window.alert((e as Error)?.message ?? "No se pudo eliminar.");
}
}
return (
);
}
function toDateInput(v: string | null | undefined): string {
if (!v) return "";
const d = new Date(v);
return isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
}
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
)}
);
}
/* -------------------------------------------------------------- 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.
) : (
Fecha
Tipo
Periodo
Referencia
Monto
{data.customerTransactions.map((t) => (
))}
)}
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 || "—"}
{d.id && (
Descargar
)}
))}
>
)}
);
}
/* ------------------------------------------------------------ helpers */
function KV({
label,
value,
}: {
label: string;
value: string | null | undefined;
}) {
return (
);
}
function SectionHead({
rule,
title,
count,
countSuffix,
}: {
rule: string;
title: string;
count?: number;
countSuffix?: string;
}) {
return (
{title}
{count != null && (
{count} {countSuffix ?? ""}
)}
);
}
function DetailSkeleton() {
return (
);
}