>();
useEffect(() => {
getPropertyStats().then(setStats).catch(() => setStats(null));
getPropertyFacets().then(setFacets).catch(() => setFacets(null));
}, []);
const runSearch = useCallback(
(p: number) => {
setLoading(true);
setError(null);
listProperties({
query: query || undefined,
serviceKind: serviceKind || undefined,
municipality: municipality || undefined,
bank: bank || undefined,
sort,
days: EXPIRY_WINDOW_DAYS,
page: p,
pageSize: 25,
...focusQuery(focus),
})
.then((res) => {
setData(res);
setLoading(false);
})
.catch((e) => {
setError(e?.message ?? "No se pudieron cargar las propiedades.");
setLoading(false);
});
},
[query, focus, serviceKind, municipality, bank, sort],
);
// Debounced re-query whenever any filter changes; always back to page 1.
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => runSearch(1), 280);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [runSearch]);
/** Picking a renewal bucket also switches the sort to due-date order —
* a list of renewals sorted by customer name isn't a worklist. */
function pickFocus(f: Focus) {
setFocus(f);
if (f === "expiring" || f === "expired") setSort("trust_due_asc");
else if (sort === "trust_due_asc" || sort === "trust_due_desc")
setSort("customer");
}
function goToPage(p: number) {
runSearch(p);
if (typeof window !== "undefined")
window.scrollTo({ top: 0, behavior: "smooth" });
}
const filtered =
query !== "" ||
focus !== "all" ||
serviceKind !== "" ||
municipality !== "" ||
bank !== "";
return (
<>
Administración de servicios
Propiedades
{canCreate && (
+ Nueva propiedad
)}
setServiceKind(k === serviceKind ? "" : k)}
/>
{filtered && (
)}
{data && !loading && !error && (
{data.total === 0
? "Sin resultados"
: `${formatNumber(data.total)} ${
data.total === 1 ? "propiedad" : "propiedades"
}`}
{query ? ` para “${query}”` : ""}
{(sort === "trust_due_asc" || sort === "trust_due_desc") &&
" · solo propiedades con fideicomiso"}
)}
{error ? (
{error}
) : loading ? (
) : data && data.items.length === 0 ? (
) : (
<>
{data?.items.map((p) => (
))}
{data && data.pageCount > 1 && (
)}
>
)}
>
);
}
/** Counts double as filter shortcuts — clicking a cell applies that bucket. */
function StatStrip({
stats,
focus,
onPickFocus,
}: {
stats: PropertyStats | null;
focus: Focus;
onPickFocus: (f: Focus) => void;
}) {
if (!stats) {
return (
{Array.from({ length: 6 }).map((_, i) => (
))}
);
}
const cells: {
key: Focus;
value: number;
label: string;
accent?: boolean;
}[] = [
{ key: "all", value: stats.properties, label: "Propiedades", accent: true },
{
key: "expiring",
value: stats.trustExpiring,
label: `Fideicomisos en ${stats.days} días`,
accent: true,
},
{ key: "expired", value: stats.trustExpired, label: "Fideicomisos vencidos" },
{ key: "trust", value: stats.trusts, label: "Con fideicomiso" },
{ key: "no_services", value: stats.withoutServices, label: "Sin servicios" },
];
return (
{cells.map((c) => (
))}
{formatNumber(stats.services)}
Servicios · {formatNumber(stats.owners)} clientes
);
}
/** The monthly workload, per service type — also the fastest kind filter. */
function ServiceMixStrip({
stats,
serviceKind,
onPickKind,
}: {
stats: PropertyStats | null;
serviceKind: ServiceKind | "";
onPickKind: (k: ServiceKind) => void;
}) {
if (!stats || stats.byKind.length === 0) return null;
return (
Servicios administrados
{stats.byKind.map((k) => (
))}
);
}
function PropertyRow({ p }: { p: PropertyListItem }) {
const addr = [p.addressLine1, p.addressLine2].filter(Boolean).join(", ");
const location = [p.customerCity?.replace(/,\s*$/, ""), p.customerState]
.filter(Boolean)
.join(", ");
const phrase = p.trust ? expiryPhrase(p.trust.daysToDue) : null;
return (
{addr || "Propiedad sin dirección"}
{p.municipality && (
{p.municipality}
)}
{p.trust && (
Fideicomiso · {trustStatusLabel(p.trust.status)}
)}
{p.serviceCount === 0 && (
Sin servicios
)}
{p.customerName}
{location && (
<>
·
{location}
>
)}
{p.zone && (
<>
·
Zona {p.zone}
>
)}
{p.phones.length > 0 && (
<>
·
{p.phones[0]}
>
)}
{p.services.map((s) => (
{serviceKindGlyph(s.kind)}
{serviceKindLabel(s.kind)}
))}
{p.services.length === 0 && —}
{p.trust?.dueDate2 && (
<>
Vence {formatDate(p.trust.dueDate2)}
{phrase && (
{phrase}
)}
>
)}
);
}
function Pager({
page,
pageCount,
onChange,
}: {
page: number;
pageCount: number;
onChange: (p: number) => void;
}) {
return (
);
}
function ListSkeleton() {
return (
{Array.from({ length: 8 }).map((_, i) => (
))}
);
}
function EmptyState({ query }: { query: string }) {
return (
⌕
Sin resultados
{query
? `No encontramos propiedades para “${query}”.`
: "No hay propiedades que coincidan con los filtros."}
);
}