// ─── Finanzas · lista (Cobros/Pagos × tipo de documento) ──────────────────
// FinanzasScreen: motor de lista vivo (paginación server-side + KPIs vía
// /stats + chips + búsqueda + rango de fechas + export), reusado tal cual del
// P3 (mapa-comprension.md §Reutilización: "ya alineado con el patrón de
// EntityListScreen"). Extraído en P8 de screens-finanzas.jsx (1127 líneas),
// separando el dataset (screens/finanzas/lista-dataset.jsx, mismo tema pero
// distinta capa) y MATANDO el bloque muerto de ~400 líneas que ya no era
// alcanzable desde la UI real: `FinStepper` (único consumidor CobroDetail-
// Screen), `CobroExpedienteDocs`/`CobroDetailScreen`/`PagoDocs`/`PagoDetail-
// Screen`/`FinDetailHeader`/`FinDataCard` (FinanzasScreen destructura
// onOpenCobro/onOpenPago en su firma pero nunca los invoca — solo onOpenDoc;
// las rutas finanzas-cobro-detail/finanzas-pago-detail y sus 6 sub-rutas
// mueren junto con esto en core/router.jsx + core/app.jsx). Con esto onOpen-
// Cobro/onOpenPago también salen de la firma del componente (dead props).
// Expone a window: FinanzasScreen. Consume window.buildFinDataset/
// FIN_DOCTABS/FIN_PAGINABLE/NEW_DOC_LABELS/finPagConfig (lista-dataset.jsx),
// window.MicosStore (fetchPage/SCHEMA), window.useEntityStats/TableDateRange/
// ExportModal/IdDateCell/TablePill (ui/entity-list.jsx).
//
// EXCEPCIÓN AL TECHO DURO DE 400 LÍNEAS (regla de la compuerta, P7 §3 — mismo
// criterio que doc-kit-edit.jsx/screens/docs/remito.jsx): un solo tema (la
// pantalla de lista de Finanzas, con paginación server-side + KPIs + chips +
// búsqueda + rango de fechas + export, calcada del patrón EntityListScreen)
// que ya se partió por tema todo lo posible — el dataset (proyección fila-
// cruda→columnas, otro tema real) vive en lista-dataset.jsx; lo que queda acá
// es SOLO el render+estado de la pantalla, sin otro corte natural sin
// fragmentar un único componente en pedazos sin nombre propio.
const { useState: _useStateFin } = React;

// ─── FinanzasScreen (cobros/pagos × tipo de documento) ────────────
function FinanzasScreen({ onOpenDoc, onNew, prefilter }) {
  // Lado (Cobros/Pagos) que pide abrir el prefilter cross-módulo del Directorio: "Pagos a …"
  // (tarjeta del Proveedor) → Pagos, "Cobros de …" (tarjeta de la Entidad) → Cobros. La tab
  // arranca en ESE lado (antes SIEMPRE 'cobros', así que "Pagos a X" abría en Cobros sin filtrar
  // — la tarjeta Pagos del proveedor era un drill-down muerto). FinanzasScreen se remonta en cada
  // navegación (render condicional por ruta en core/app.jsx), así que el initializer alcanza.
  const prefilterLado = prefilter ? (/^\s*Pagos\b/i.test(prefilter.label || '') ? 'pagos' : 'cobros') : null;
  const [activeTab, setActiveTab] = _useStateFin(prefilterLado || 'cobros');
  const [docType, setDocType] = _useStateFin('facturas');
  const [chipKey, setChipKey] = _useStateFin('todos');
  const [search, setSearch] = _useStateFin('');
  const [dateFrom, setDateFrom] = _useStateFin('');
  const [dateTo, setDateTo] = _useStateFin('');
  const [exportOpen, setExportOpen] = _useStateFin(false);

  const isCobros = activeTab === 'cobros';
  const lado = isCobros ? 'cobros' : 'pagos';

  // Título: en la 1ra carga entra de abajo (como el resto); al cambiar de
  // Cobros↔Pagos entra de costado (ltr/rtl).
  const _titleSeen = React.useRef(null);
  const _titleChanged = _titleSeen.current !== null && _titleSeen.current !== lado;
  React.useEffect(() => { _titleSeen.current = lado; }, [lado]);

  const switchTab = (tab) => { setActiveTab(tab); setDocType('facturas'); setChipKey('todos'); setSearch(''); setDateFrom(''); setDateTo(''); };
  const switchDocType = (dt) => { setDocType(dt); setChipKey('todos'); setSearch(''); setDateFrom(''); setDateTo(''); };

  // ── Paginación server-side (patrón Expedientes/screens-list.jsx) — SOLO
  // para la selección ACTIVA cuando mapea a una colección limpia (ver
  // FIN_PAGINABLE en lista-dataset.jsx). Estado PROPIO de esta pantalla: no
  // toca hydrate/BIG_LIMIT ni los arrays globales de window (salvo el merge
  // puntual del drill-down en `open`, más abajo).
  const pagCfg = finPagConfig(lado, docType);
  const _pagSchema = (pagCfg && window.MicosStore) ? window.MicosStore.SCHEMA[pagCfg.collection] : null;
  const [pgRows, setPgRows] = _useStateFin([]);
  const [pgTotal, setPgTotal] = _useStateFin(0);
  const [pgLoading, setPgLoading] = _useStateFin(false);
  const pgOffsetRef = React.useRef(0);
  const pgLoadingRef = React.useRef(false);
  const pgSentinelRef = React.useRef(null);
  const PG_BATCH = (window.LIST_CONFIG && window.LIST_CONFIG.batchSize) || 60;

  // Carga inicial (y RESET) de la tanda al cambiar de lado/docType. Sin
  // pagCfg (docType en modo memoria) limpia el estado paginado y no pide nada.
  React.useEffect(() => {
    if (!pagCfg) { setPgRows([]); setPgTotal(0); pgOffsetRef.current = 0; return undefined; }
    let cancelled = false;
    setPgRows([]);
    setPgTotal(0);
    pgOffsetRef.current = 0;
    setPgLoading(true);
    window.MicosStore.fetchPage(pagCfg.collection, { offset: 0, limit: PG_BATCH })
      .then((r) => {
        if (cancelled) return;
        const items = r.items || [];
        setPgRows(items);
        setPgTotal(r.total || 0);
        pgOffsetRef.current = items.length;
      })
      .catch((e) => {
        console.warn('[FinanzasScreen] fetchPage', pagCfg.collection, e.message);
        if (cancelled) return;
        // Fallback: lo que ya haya hidratado en memoria de esa colección
        // (semilla / capado a BIG_LIMIT) — no deja la pantalla vacía.
        const mem = Array.isArray(window[_pagSchema ? _pagSchema.global : '']) ? window[_pagSchema.global] : [];
        setPgRows(mem.slice());
        setPgTotal(mem.length);
        pgOffsetRef.current = mem.length;
      })
      .finally(() => { if (!cancelled) setPgLoading(false); });
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lado, docType]);

  // Sentinel + IntersectionObserver (patrón EXACTO de screens-list.jsx): al
  // entrar en viewport pide la próxima tanda y appendea. Guard sincrónico por
  // ref para no disparar un fetch duplicado mientras el anterior sigue en vuelo.
  const pgHayMas = pagCfg ? pgRows.length < pgTotal : false;
  React.useEffect(() => {
    if (!pagCfg || !pgHayMas) return undefined;
    const el = pgSentinelRef.current;
    if (!el || typeof IntersectionObserver === 'undefined') return undefined;
    const obs = new IntersectionObserver((entries) => {
      if (!(entries[0] && entries[0].isIntersecting) || pgLoadingRef.current) return;
      pgLoadingRef.current = true;
      setPgLoading(true);
      window.MicosStore.fetchPage(pagCfg.collection, { offset: pgOffsetRef.current, limit: PG_BATCH })
        .then((r) => {
          const items = r.items || [];
          setPgRows((prev) => prev.concat(items));
          pgOffsetRef.current += items.length;
          if (typeof r.total === 'number') setPgTotal(r.total);
        })
        .catch((e) => console.warn('[FinanzasScreen] fetchPage (más)', pagCfg.collection, e.message))
        .finally(() => { pgLoadingRef.current = false; setPgLoading(false); });
    }, { rootMargin: (window.LIST_CONFIG && window.LIST_CONFIG.observerRootMargin) || '400px' });
    obs.observe(el);
    return () => obs.disconnect();
  }, [pagCfg && pagCfg.collection, pgHayMas]);

  // KPIs server-side (GET /api/<coleccion>/stats) para la colección activa:
  // total/sumTotal reales de TODO el universo (agregados en Postgres).
  // Cobros→Facturas además trae `byEstado` REAL (v_facturas_stats, PENDIENTE/
  // PARCIAL/COBRADO por SUM(imputacion.monto_imputado) — BUGS-caza-01-07-2026.md
  // #1/A1): con eso los chips "A Cobrar"/"Cobrados" también salen del universo
  // completo, no solo de lo cargado. Para el resto de las selecciones (Recibos/
  // Pagos, y NC/ND que quedan en modo memoria) no hay `byEstado` agregado en
  // Postgres todavía, así que sus chips de estado/imputación siguen contando
  // sobre lo YA CARGADO (effRows/pgRows) como fallback. `useEntityStats`
  // (entity-list.jsx) cachea por colección y no rompe si la key es null.
  const srvStats = window.useEntityStats(pagCfg ? pagCfg.collection : null);
  const byEstadoSrv = (pagCfg && pagCfg.collection === 'facturas' && srvStats && srvStats.byEstado)
    ? srvStats.byEstado
    : null;

  const dataset = buildFinDataset(lado, docType);
  const docTabs = FIN_DOCTABS[lado];
  const chips = dataset.chips;
  const chipPred = (chips.find(c => c.key === chipKey) || chips[0]).pred;

  // Filas efectivas: paginadas server-side (mapeadas con el MISMO mapper que
  // buildFinDataset usa en memoria) si la selección activa es paginable; si
  // no, el dataset en memoria de siempre. LIMITACIÓN (igual que Expedientes/
  // EntityListScreen): con pagCfg activo, chips/búsqueda/export operan sobre
  // lo YA CARGADO (effRows/pgRows), no sobre el universo completo — si el
  // registro buscado todavía no se cargó, no aparece hasta scrollear más. El
  // pie "N resultados" y el chip "Todos" sí muestran el total REAL (pgTotal).
  const effRows = pagCfg ? pgRows.map(pagCfg.map) : dataset.rows;

  // Filtro: chip de estado/imputación + prefilter (entidad, solo cobros) + búsqueda
  const filtered = (() => {
    let r = effRows.filter(chipPred);
    // Prefilter cross-módulo, solo cuando la tab activa coincide con el lado que pidió el
    // prefilter (el auto-switch de arriba ya lo garantiza en la carga inicial).
    if (prefilter && prefilterLado === lado) {
      if (isCobros && prefilter.label) {
        // Cobros de <entidad>: match por nombre de cliente (heredado; robusto entre docTypes,
        // las filas mapeadas llevan `cliente` pero no `entidadId`).
        const mm = /Cobros de (.+)/.exec(prefilter.label);
        const nm = mm ? mm[1].trim().toLowerCase() : null;
        if (nm) r = r.filter(row => String(row.cliente || '').toLowerCase().includes(nm));
      } else if (!isCobros && typeof prefilter.predicate === 'function') {
        // Pagos a <proveedor>: se LEE el predicate real (row.proveedorId === proveedorId) sobre
        // la fila CRUDA `row._src` — la fila mapeada de la tabla no lleva proveedorId, pero el
        // registro original (que embebe /api/pagos) sí. Simétrico a Cobros: la tarjeta Pagos del
        // proveedor filtra de verdad.
        r = r.filter(row => { try { return prefilter.predicate(row._src); } catch (e) { return false; } });
      }
    }
    if (search) {
      const q = search.toLowerCase();
      const keys = dataset.search || ['publicId', 'cliente'];
      r = r.filter(row => keys.some(k => String(row[k] || '').toLowerCase().includes(q)));
    }
    if (dateFrom || dateTo) {
      r = r.filter(row => {
        const d = String(row.fecha || '').slice(0, 10);
        if (!d) return false;
        if (dateFrom && d < dateFrom) return false;
        if (dateTo && d > dateTo) return false;
        return true;
      });
    }
    // Orden por defecto: más nuevo → más viejo por fecha de negocio (P15 — mismo
    // molde que screens/expedientes/lista.jsx). Cubre las 5 combinaciones que
    // pasan por ESTE render (`row.fecha`/`row.publicId` ya vienen unificados por
    // los mappers de lista-dataset.jsx): facturas y recibos (lado cobro,
    // paginados server-side — el server YA ordena igual, esto es red de
    // seguridad si effRows viniera de memoria/fallback), NC/ND (lado cobro o
    // pago, modo memoria) y pagos (lado pago, paginado). "Facturas" del lado
    // pago (facturaRecibida) no es una de las 11 colecciones (es PAGOS con
    // filtro) pero comparte el mismo `row.fecha` — el mismo criterio le cabe sin
    // caso especial.
    r = r.slice().sort((a, b) =>
      (b.fecha || '').localeCompare(a.fecha || '') ||
      (b.publicId || '').localeCompare(a.publicId || '')
    );
    return r;
  })();

  // Columnas para exportar (segun el tipo de documento del dataset actual).
  const finExportConfig = (() => {
    const cfg = [
      { label: 'ID', value: (r) => r.publicId },
      { label: 'Fecha', value: (r) => r.fecha },
      { label: isCobros ? 'Cliente' : 'Proveedor', value: (r) => r.cliente },
    ];
    if (effRows.some(r => r.tipo && r.tipo !== '·')) cfg.push({ label: 'Tipo', value: (r) => r.tipo });
    if (effRows.some(r => r.ref)) cfg.push({ label: 'Referencia', value: (r) => r.ref });
    cfg.push({ label: 'Total', value: (r) => r.total });
    if (effRows.some(r => r.estado)) cfg.push({ label: 'Estado', value: (r) => r.estado });
    if (effRows.some(r => r.imput)) cfg.push({ label: 'Imputación', value: (r) => (r.imput === 'IMPUTADO' ? 'Imputado' : 'A cuenta') });
    return cfg;
  })();
  const finDocLabel = (FIN_DOCTABS[lado].find(d => d.key === docType) || {}).label || '';
  const finExportName = `Finanzas · ${isCobros ? 'Cobros' : 'Pagos'} · ${finDocLabel}`;

  // KPIs derivados del conjunto actual. Total/Monto salen de /stats cuando hay
  // paginación activa (srvStats, universo completo real vía Postgres); si
  // todavía no llegó o no aplica, caen al cálculo en memoria sobre effRows
  // (fallback, patrón B2). chips[1]/chips[2] (A Cobrar/Cobrados o Imputados/A
  // cuenta): en Cobros→Facturas salen de `byEstadoSrv` (universo real); el
  // resto sigue contando sobre lo cargado (fallback, ver comentario de
  // srvStats más arriba).
  const kpis = (() => {
    const rws = effRows;
    const total = pagCfg ? pgTotal : rws.length;
    const c1 = byEstadoSrv
      ? (Number(byEstadoSrv.PENDIENTE || 0) + Number(byEstadoSrv.PARCIAL || 0))
      : rws.filter(chips[1].pred).length;
    const c2 = byEstadoSrv ? Number(byEstadoSrv.COBRADO || 0) : rws.filter(chips[2].pred).length;
    const montoServer = srvStats && typeof srvStats.sumTotal === 'number' ? srvStats.sumTotal : null;
    const monto = montoServer != null ? montoServer : rws.reduce((s, r) => s + (r.total || 0), 0);
    const spark = (v) => [v * 0.4, v * 0.55, v * 0.5, v * 0.72, v * 0.62, v * 0.85, v].map(x => Math.max(0.1, x));
    const short = (docTabs.find(d => d.key === docType) || {}).short || '';
    return [
      { label: `Total ${short}`, value: String(total), icon: IconFileText, trend: spark(total || 1) },
      { label: chips[1].label, value: String(c1), deltaTone: 'warn', icon: IconClock, trend: spark(c1 || 1) },
      { label: 'Monto total', value: formatMoneyShort(monto), icon: IconDollar, trend: spark((monto / 1000000) || 1) },
      { label: chips[2].label, value: String(c2), deltaTone: 'pos', icon: IconCheckCircle, trend: spark(c2 || 1) },
    ];
  })();

  const columns   = dataset.columns;
  const gridTpl   = columns.map(c => c.width || '1fr').join(' ');
  const minTableW = columns.reduce((s, c) => {
    const m = /(\d+)px/.exec(c.width || '');
    return s + (m ? parseInt(m[1], 10) : 150);
  }, 0);

  // Abrir el documento de una fila en el visor unificado (todos los campos +
  // Imprimir/Editar). Drill-down (mismo patrón que EntityListScreen._openRow /
  // openExpediente de Expedientes): si la selección activa pagina server-side
  // y la fila viene de una página que window[coll] nunca hidrató (offset más
  // allá de lo que trajo hydrate()), la trae por publicId/id y la mergea
  // ANTES de abrir — así el detalle (que lee window[coll] síncrono) nunca cae
  // al documento de EJEMPLO ni se queda en blanco.
  const open = (row) => {
    if (!onOpenDoc) return;
    if (!pagCfg || !_pagSchema) { onOpenDoc(row._kind, row._src); return; }
    const globalArr = window[_pagSchema.global];
    if (Array.isArray(globalArr) && globalArr.some(x => x.id === row.id)) {
      onOpenDoc(row._kind, row._src);
      return;
    }
    const ident = row.publicId || row.id;
    fetch(`${_pagSchema.endpoint}/${encodeURIComponent(ident)}`, { credentials: 'same-origin' })
      .then(r => (r.ok ? r.json() : Promise.reject(r.status)))
      .then(server => {
        if (!server || !server.id) throw new Error('sin id');
        const coll = window[_pagSchema.global] || (window[_pagSchema.global] = []);
        if (!coll.some(x => x.id === server.id)) coll.push(server);
        onOpenDoc(row._kind, server);
      })
      .catch(err => {
        console.warn('[FinanzasScreen] drill-down fetch', pagCfg.collection, err);
        onOpenDoc(row._kind, row._src); // best-effort: abre igual con lo que ya tenía
      });
  };

  return (
    <div className="space-y-7">
      {/* ── Page header ─────────────────────────── */}
      <div className="flex flex-wrap items-end justify-between gap-4 animate-fade-in-up pt-2">
        <div className="space-y-3">
          <p className="text-[11px] font-bold uppercase tracking-[0.18em]"
            style={{ color: 'var(--accent)' }}>
            Operaciones · Finanzas
          </p>
          <h1 className="text-[clamp(28px,4.2vw,40px)] leading-[1.05] font-extrabold tracking-[-0.02em]"
            style={{ color: 'var(--foreground)' }}>
            <span key={isCobros ? 'cobros' : 'pagos'}
              className={`inline-block ${_titleChanged ? (isCobros ? 'animate-name-pop-ltr' : 'animate-name-pop-rtl') : ''}`}>
              {isCobros ? 'Cobros' : 'Pagos'}
            </span>
          </h1>
          <p className="text-[13.5px] font-medium max-w-2xl" style={{ color: 'var(--muted-foreground)' }}>
            {isCobros
              ? 'Cobros a entidades y obras sociales. Seguí el estado de cada factura.'
              : 'Pagos a proveedores. Gestioná comprobantes y transferencias.'}
          </p>
        </div>
        <div className="flex items-center gap-2.5 flex-shrink-0">
          <button className="btn btn-outline" onClick={() => setExportOpen(true)}>
            <IconDownload size={15} />
            <span className="hidden sm:inline">Exportar</span>
          </button>
          <button className="btn btn-primary" onClick={() => onNew && onNew(docType, lado)}>
            <IconPlus size={15} />
            <span className="hidden sm:inline">{NEW_DOC_LABELS[docType] || (isCobros ? 'Nuevo cobro' : 'Nuevo pago')}</span>
          </button>
        </div>
      </div>

      {/* ── Nivel 1: Cobros / Pagos (segmentado primario, estilo Consumibles/Alquilables de Inventario) ── */}
      <div className="flex items-center gap-2 animate-fade-in-up" style={{ animationDelay: '90ms' }}>
        {[
          { key: 'cobros', label: 'Cobros' },
          { key: 'pagos',  label: 'Pagos'  },
        ].map(tab => {
          const active = activeTab === tab.key;
          return (
            <button key={tab.key} onClick={() => switchTab(tab.key)}
              className="chip flex-shrink-0 text-[13px] font-bold"
              style={active
                ? { background: 'var(--accent)', color: 'var(--accent-foreground)', boxShadow: '0 6px 18px -8px rgba(15,187,182,0.55)' }
                : { background: 'color-mix(in oklab, var(--muted-foreground) 8%, transparent)', color: 'var(--muted-foreground)' }}>
              {tab.label}
            </button>
          );
        })}
      </div>

      {/* ── Nivel 2: tipo de documento (Facturas / NC / ND / Recibos) ── */}
      <div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-none animate-fade-in-up" style={{ animationDelay: '120ms' }}>
        {docTabs.map(dt => {
          const active = docType === dt.key;
          return (
            <button key={dt.key} onClick={() => switchDocType(dt.key)}
              className="chip flex-shrink-0"
              style={active
                ? { background: 'var(--accent)', color: 'var(--accent-foreground)', boxShadow: '0 6px 18px -8px rgba(15,187,182,0.55)' }
                : { background: 'color-mix(in oklab, var(--muted-foreground) 8%, transparent)', color: 'var(--muted-foreground)' }}>
              {dt.label}
            </button>
          );
        })}
      </div>

      {/* ── KPIs ────────────────────────────────── */}
      <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 stagger">
        {kpis.map((k, i) => (
          <KpiCard key={i} label={k.label} value={k.value}
            deltaTone={k.deltaTone} icon={k.icon} trend={k.trend} />
        ))}
      </div>

      {/* ── Chips de estado / imputación ────────── */}
      <div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-none animate-fade-in-up"
        style={{ animationDelay: '180ms' }}>
        {chips.map(chip => {
          const active = chipKey === chip.key;
          // "Todos" con paginación activa = total REAL del universo (pgTotal, vía
          // X-Total-Count). En Cobros→Facturas, "pend"/"ok" (A Cobrar/Cobrados)
          // también salen del universo real (byEstadoSrv, ver comentario de
          // srvStats); el resto de los chips sigue contando sobre lo cargado
          // (effRows) por no tener contraparte agregada en Postgres todavía.
          let count;
          if (chip.key === 'todos' && pagCfg) count = pgTotal;
          else if (byEstadoSrv && chip.key === 'pend') count = Number(byEstadoSrv.PENDIENTE || 0) + Number(byEstadoSrv.PARCIAL || 0);
          else if (byEstadoSrv && chip.key === 'ok') count = Number(byEstadoSrv.COBRADO || 0);
          else count = effRows.filter(chip.pred).length;
          return (
            <button key={chip.key} onClick={() => setChipKey(chip.key)}
              className="chip"
              style={active
                ? { background: 'var(--accent)', color: 'var(--accent-foreground)', boxShadow: '0 6px 18px -8px rgba(15,187,182,0.55)' }
                : { background: 'color-mix(in oklab, var(--muted-foreground) 8%, transparent)', color: 'var(--muted-foreground)' }}>
              {chip.label}
              <span className="opacity-70 tabular-nums">{count}</span>
            </button>
          );
        })}
      </div>

      {/* ── Prefilter banner (en la tab que pidió el prefilter: Cobros o Pagos) ── */}
      {prefilter && prefilterLado === lado && (
        <div
          className="flex items-center gap-3 rounded-xl px-4 sm:px-5 py-3 animate-fade-in-up"
          style={{
            background: 'color-mix(in oklab, var(--accent) 8%, var(--card))',
            border: '1px solid color-mix(in oklab, var(--accent) 40%, transparent)',
            animationDelay: '160ms',
          }}
        >
          <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
            style={{
              width: 30, height: 30,
              background: 'color-mix(in oklab, var(--accent) 16%, transparent)',
              color: 'var(--accent)',
            }}>
            <IconFilter size={14} />
          </span>
          <div className="min-w-0 flex-1">
            <p className="text-[10.5px] font-bold uppercase tracking-[0.16em]" style={{ color: 'var(--accent)' }}>
              Filtrado desde otro módulo
            </p>
            <p className="text-[13px] font-bold truncate mt-0.5" style={{ color: 'var(--foreground)' }}>
              {prefilter.label}
              {prefilter.sublabel && (
                <span className="ml-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>· {prefilter.sublabel}</span>
              )}
            </p>
          </div>
          {prefilter.onClear && (
            <button
              onClick={prefilter.onClear}
              type="button"
              className="inline-flex items-center gap-1.5 text-[11.5px] font-bold uppercase tracking-[0.1em] px-3 py-1.5 rounded-md transition-colors cursor-pointer"
              style={{ color: 'var(--accent)', background: 'transparent', border: '1px solid color-mix(in oklab, var(--accent) 40%, transparent)' }}
              onMouseEnter={(e) => e.currentTarget.style.background = 'color-mix(in oklab, var(--accent) 12%, transparent)'}
              onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
            >
              Quitar filtro
              <IconX size={12} />
            </button>
          )}
        </div>
      )}

      {/* ── Table card ──────────────────────────── */}
      <div className="glow-track animate-fade-in-up rounded-2xl" style={{
        animationDelay: '220ms',
        background: 'var(--card)',
        border: '1px solid color-mix(in oklab, var(--border) 55%, transparent)',
        boxShadow: 'var(--shadow-card)',
        overflow: 'hidden',
      }}>
        {/* Search row */}
        <div className="flex items-center justify-between gap-4 px-5 py-4 flex-wrap"
          style={{ borderBottom: '1px solid color-mix(in oklab, var(--border) 55%, transparent)' }}>
          <div className="relative flex-1 min-w-[200px] max-w-2xl">
            <IconSearch size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2"
              style={{ color: 'var(--muted-foreground)' }} />
            <input value={search} onChange={e => setSearch(e.target.value)}
              placeholder="Buscar en esta lista…"
              className="w-full h-10 pl-10 pr-3 text-[13px] font-medium rounded-lg outline-none"
              style={{
                background: 'color-mix(in oklab, var(--surface) 50%, transparent)',
                border: '1px solid color-mix(in oklab, var(--border) 45%, transparent)',
                color: 'var(--foreground)',
                transition: 'border-color 0.2s',
              }} />
          </div>
          <div className="flex items-center gap-3 flex-wrap">
            <TableDateRange from={dateFrom} to={dateTo} onFrom={setDateFrom} onTo={setDateTo} />
            {/* Con paginación activa: total REAL del universo (X-Total-Count vía
                fetchPage), no lo cargado — así el pie nunca dice "60" habiendo
                855. Los filtros de arriba siguen operando solo sobre lo cargado
                (ver comentario de `effRows`). */}
            <span className="text-[12px] font-semibold tabular-nums whitespace-nowrap"
              style={{ color: 'var(--muted-foreground)' }}>
              {pagCfg ? pgTotal : filtered.length} resultados
            </span>
          </div>
        </div>

        {/* Table */}
        <div style={{ overflowX: 'auto' }}>
          <div style={{ minWidth: `${minTableW}px` }}>
            {/* Head */}
            <div className="grid gap-4 px-6 py-3.5 text-[10.5px] font-bold uppercase tracking-[0.08em]"
              style={{
                gridTemplateColumns: gridTpl,
                color: 'var(--muted-foreground)',
                borderBottom: '1px solid color-mix(in oklab, var(--border) 60%, transparent)',
              }}>
              {columns.map((c, ci) => (
                <div key={c.key} className={`${c.align === 'right' ? 'text-right' : ''} ${ci === columns.length - 1 ? 'pr-[23px]' : ''}`}>{c.label}</div>
              ))}
            </div>
            {/* Body */}
            <div className="stagger-rows">
              {filtered.map((row, idx) => (
                <button key={row.id}
                  onClick={() => open(row)}
                  className="exp-row w-full grid gap-4 px-6 py-4 items-center text-left cursor-pointer"
                  style={{
                    gridTemplateColumns: gridTpl,
                    borderBottom: idx < filtered.length - 1
                      ? '1px solid color-mix(in oklab, var(--border) 45%, transparent)'
                      : 'none',
                  }}>
                  {columns.map((c, ci) => {
                    const cellInner = c.render ? c.render(row) : (
                      <span className="text-[13px] font-medium truncate block"
                        style={{ color: 'var(--foreground)' }}>{row[c.key]}</span>
                    );
                    return (
                    <div key={c.key} className={`min-w-0 ${c.align === 'right' ? 'text-right' : ''}`}>
                      {ci === columns.length - 1 ? (
                        <div className="flex items-center justify-between gap-2">
                          {cellInner}
                          <span className="exp-chev" style={{ color: 'var(--muted-foreground)' }}><IconChevronRight size={15} /></span>
                        </div>
                      ) : cellInner}
                    </div>
                    );
                  })}
                </button>
              ))}
              {/* Sentinel: al entrar en viewport (cerca del fondo) carga la próxima tanda */}
              {pagCfg && pgHayMas && (
                <div ref={pgSentinelRef} className="px-6 py-6 text-center text-[12px] font-medium" style={{ color: 'var(--muted-foreground)' }}>
                  Cargando más… <span className="tabular-nums opacity-70">({pgRows.length} de {pgTotal})</span>
                </div>
              )}
              {/* Cierre: llegaste al final */}
              {pagCfg && !pgHayMas && pgRows.length > 0 && (
                <div className="px-6 py-8 text-center text-[11px] font-bold uppercase tracking-[0.14em]" style={{ color: 'var(--muted-foreground)' }}>
                  — No hay más resultados —
                </div>
              )}
              {filtered.length === 0 && (
                <div className="px-6 py-20 text-center">
                  <p className="text-[14px] font-semibold"
                    style={{ color: 'var(--muted-foreground)' }}>
                    {pagCfg && pgLoading && pgRows.length === 0 ? 'Cargando…' : 'Sin resultados.'}
                  </p>
                </div>
              )}
            </div>
          </div>
        </div>
      </div>

      {/* ── Modal exportar a Excel ── */}
      <ExportModal
        open={exportOpen}
        onClose={() => setExportOpen(false)}
        exportConfig={finExportConfig}
        rows={filtered}
        filename={finExportName}
      />
    </div>
  );
}

Object.assign(window, { FinanzasScreen });
