"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; import { EXPIRY_WINDOW_DAYS, getPropertyFacets, getPropertyStats, listProperties, } from "@/lib/api"; import { expiryPhrase, formatDate, formatNumber, serviceKindGlyph, serviceKindLabel, SIN_NOMBRE, trustStatusLabel, } from "@/lib/labels"; import type { PropertyFacets, PropertyListItem, PropertyListResponse, PropertySort, PropertyStats, ServiceKind, } from "@/lib/types"; /** * The buckets staff actually work from. Trust (fideicomiso) renewals are the * recurring deadline in this line of business, so they get first-class filters * next to the "nothing enrolled yet" bucket that flags incomplete records. */ type Focus = "all" | "trust" | "expiring" | "expired" | "no_services"; const FOCUS_FILTERS: { key: Focus; label: string }[] = [ { key: "all", label: "Todas" }, { key: "expiring", label: "Fideicomiso por vencer" }, { key: "expired", label: "Fideicomiso vencido" }, { key: "trust", label: "Con fideicomiso" }, { key: "no_services", label: "Sin servicios" }, ]; const SORTS: { key: PropertySort; label: string }[] = [ { key: "customer", label: "Cliente (A–Z)" }, { key: "address", label: "Dirección (A–Z)" }, { key: "services_desc", label: "Más servicios" }, { key: "trust_due_asc", label: "Vencimiento de fideicomiso (solo con fideicomiso)", }, { key: "trust_due_desc", label: "Vencimiento más lejano (solo con fideicomiso)", }, ]; /** Focus bucket → the query the API understands. */ function focusQuery(focus: Focus) { switch (focus) { case "trust": return { trust: "with" as const }; case "expiring": return { trust: "expiring" as const }; case "expired": return { trust: "expired" as const }; case "no_services": return { hasServices: false }; default: return {}; } } export default function ServiciosPage() { return ( ); } function ServiciosBrowser() { const [stats, setStats] = useState(null); const [facets, setFacets] = useState(null); const [query, setQuery] = useState(""); const [focus, setFocus] = useState("all"); const [serviceKind, setServiceKind] = useState(""); const [municipality, setMunicipality] = useState(""); const [bank, setBank] = useState(""); const [sort, setSort] = useState("customer"); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const debounceRef = useRef>(); 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

setServiceKind(k === serviceKind ? "" : k)} />
setQuery(e.target.value)} placeholder="Buscar por dirección, cliente, cuenta, medidor, fideicomiso…" aria-label="Buscar propiedades" />
{FOCUS_FILTERS.map((f) => ( ))}
{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."}

); }