/* historial.jsx — el día cerrado, ruta por ruta, con sus costos.
   Es la base de la liquidación: `rutas_realizadas` guarda la foto congelada al cerrar la jornada,
   con el costo que la ruta tenía ESE día. `rutas` no se vacía al cerrar, pero sí se reconcilia
   —un re-guardado de la jornada borra lo que ya no está en el estado— y las tarifas cambian.
   Dos cortes: la QUINCENA (lo que se le manda a cada proveedor) y el DÍA (el detalle).
   Sólo Gerencia — la pantalla es toda costos. */
(function () {
  window.CR = window.CR || {};
  window.CR.screens = window.CR.screens || {};
  var React = window.React;
  var h = React.createElement;
  var useState = React.useState, useEffect = React.useEffect;

  function money(n) { return '$' + Number(n || 0).toLocaleString('es-AR', { maximumFractionDigits: 0 }); }
  function num(n) { return Number(n || 0).toLocaleString('es-AR'); }
  function diaMes(f) { var p = String(f || '').split('-'); return p.length === 3 ? p[2] + '/' + p[1] : (f || '—'); }

  var MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
    'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
  function nombrePeriodo(p) {
    return (p.quincena === 1 ? '1ª quincena' : '2ª quincena') + ' de ' + MESES[p.mes - 1] + ' ' + p.anio;
  }

  /* Postgres devuelve `numeric` como string con punto decimal ("139000.00"). Excel en es-AR usa COMA:
     con el punto lee la columna como texto y el TOTAL de la planilla no suma. Se convierte al
     escribir el CSV, no antes — en pantalla el formateo lo hace `toLocaleString`. */
  function numeroCsv(v) {
    if (v === null || v === undefined || v === '') return '';
    var s = String(v);
    return /^-?\d+(\.\d+)?$/.test(s) ? s.replace('.', ',') : s;
  }

  /* CSV para pegar en la planilla de liquidación. La implementación se mudó a `lib/csv.js` cuando
     Flota MELI necesitó exportar también: una sola copia del escapador, del separador `;` y del BOM.

     Se resuelve AL LLAMAR y no al cargar el módulo. Capturarlo con `var x = window.CR.csv.descargar`
     hace que esta pantalla muera entera si `lib/csv.js` todavía no corrió — y `index.html` hoy lo
     carga antes, pero eso es una garantía de orden, no del código. Con la llamada tardía, un orden
     equivocado rompe SÓLO la descarga, y esta pantalla es la de liquidaciones. */
  function descargarCsv(nombre, columnas, filas) {
    return window.CR.csv.descargar(nombre, columnas, filas);
  }

  var COLS_PROVEEDOR = [
    { key: 'transporte', label: 'Proveedor', render: function (r) { return r.transporte || 'Sin proveedor'; },
      csv: function (r) { return r.transporte || 'Sin proveedor'; } },

    { key: 'plaza', label: 'Plaza', render: function (r) { return r.plaza || 'AMBA'; }, csv: function (r) { return r.plaza || 'AMBA'; } },
    { key: 'patente', label: 'Patente' },
    { key: 'rutas', label: 'Rutas', align: 'right' },
    { key: 'costo', label: 'Costo', align: 'right', render: function (r) { return money(r.costo); }, csv: function (r) { return numeroCsv(r.costo); } },
    { key: 'adicional', label: 'Adicionales', align: 'right', render: function (r) { return money(r.adicional); }, csv: function (r) { return numeroCsv(r.adicional); } },
    { key: 'penalidad', label: 'Penalidades', align: 'right', render: function (r) { return money(r.penalidad); }, csv: function (r) { return numeroCsv(r.penalidad); } },
    { key: 'total', label: 'Total', align: 'right', render: function (r) { return h('b', null, money(r.total)); },
      csv: function (r) { return numeroCsv(r.total); } },
    { key: 'desde', label: 'Período', render: function (r) { return diaMes(r.desde) + ' al ' + diaMes(r.hasta); },
      csv: function (r) { return r.desde + ' a ' + r.hasta; } }
  ];










  var PLAZAS = ['AMBA', 'ROSARIO'];
  var CABECERA_PROVEEDOR = ['Fecha', 'Vehículo', 'Costo', 'Adicional', 'Descuentos/Penalidades', 'Total Costo'];
  function fechaAr(f) { var p = String(f || '').split('-'); return p.length === 3 ? p[2] + '-' + p[1] + '-' + p[0] : (f || ''); }
  function importeAr(n) { return Number(n || 0).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
  function cero() { return { costo: 0, adicional: 0, penalidad: 0, total: 0 }; }
  function acumular(acc, x) {
    acc.costo += Number(x.costo || 0); acc.adicional += Number(x.adicional || 0);
    acc.penalidad += Number(x.penalidad || 0); acc.total += Number(x.total || 0);
    return acc;
  }
  // las penalidades viajan POSITIVAS desde la base (total = costo + adicional − penalidad) y en la
  // planilla del proveedor van en NEGATIVO: es la columna "Descuentos/Penalidades"
  function celdasImporte(a) { return [importeAr(a.costo), importeAr(a.adicional), importeAr(a.penalidad > 0 ? -a.penalidad : 0), importeAr(a.total)]; }





  function notaMediaVuelta(r) {
    var pct = r && r.media_vuelta_pct ? ' · ' + r.media_vuelta_pct + ' %' : '';
    if (!r || !r.media_vuelta) return '';
    if (r.media_vuelta === 'cubre') return 'cubrió una vuelta de doble' + (r.media_vuelta_par ? ' (la otra: ' + r.media_vuelta_par + ')' : '') + pct;
    if (r.media_vuelta === 'titular') return 'una vuelta de doble; la otra la cubrió otra unidad' + pct;
    if (r.media_vuelta === 'mitad_sola') return 'una vuelta de doble; la otra se desestimó' + pct;
    return '';
  }
  function textoMediaVuelta(r) {
    if (!r || !r.media_vuelta) return '';
    var pct = r.media_vuelta_pct ? ' ' + r.media_vuelta_pct + ' %' : '';
    if (r.media_vuelta === 'cubre') return 'Cubre' + pct + (r.cobertura_motivo ? ' (' + r.cobertura_motivo + ')' : '');
    if (r.media_vuelta === 'titular') return 'Titular' + pct + (r.media_vuelta_par ? ' (la otra: ' + r.media_vuelta_par + ')' : '');
    if (r.media_vuelta === 'mitad_sola') return 'Sola' + pct + (r.media_vuelta_par ? ' (' + r.media_vuelta_par + ' desestimada)' : '');
    return '';
  }
  function csvProveedor(proveedor, rutas) {
    var fila = window.CR.csv.filaCsv;
    var nombre = proveedor || 'Sin proveedor';
    var lineas = [fila(['PROVEEDOR', nombre])];
    var porPlaza = {};
    (rutas || []).forEach(function (r) {
      if (r.desestimada) return;
      var p = PLAZAS.indexOf(r.plaza) !== -1 ? r.plaza : 'AMBA';
      (porPlaza[p] = porPlaza[p] || []).push(r);
    });
    var plazas = PLAZAS.filter(function (p) { return porPlaza[p] && porPlaza[p].length; });
    var totalProveedor = cero();
    plazas.forEach(function (plaza, i) {
      if (i > 0) lineas.push('');
      lineas.push(fila(['DIVISIÓN DE PROVINCIA DE SERVICIO: ' + plaza]));
      lineas.push(fila(CABECERA_PROVEEDOR));
      // fecha × patente: dos rutas de la misma unidad el mismo día son UNA fila, sumada
      var dias = {}, fechas = [];
      porPlaza[plaza].forEach(function (r) {
        var f = String(r.fecha || '');
        if (!dias[f]) { dias[f] = { patentes: {}, orden: [], notas: {} }; fechas.push(f); }
        var pat = String(r.patente || '').trim() || 'Sin patente';
        if (!dias[f].patentes[pat]) { dias[f].patentes[pat] = cero(); dias[f].orden.push(pat); }
        if (notaMediaVuelta(r)) dias[f].notas[pat] = notaMediaVuelta(r);
        acumular(dias[f].patentes[pat], { costo: r.costo, adicional: r.adicional, penalidad: r.penalidad, total: r.total_costo });
      });
      var totalPlaza = cero();
      fechas.sort().forEach(function (f) {
        var dia = dias[f], totalDia = cero();
        dia.orden.slice().sort(function (a, b) { return a.localeCompare(b, 'es'); }).forEach(function (pat) {
          lineas.push(fila([fechaAr(f), pat + (dia.notas[pat] ? ' (' + dia.notas[pat] + ')' : '')].concat(celdasImporte(dia.patentes[pat]))));
          acumular(totalDia, dia.patentes[pat]);
        });
        lineas.push(fila(['Total ' + fechaAr(f), ''].concat(celdasImporte(totalDia))));
        acumular(totalPlaza, totalDia);
      });
      if (plazas.length > 1) lineas.push(fila(['Total ' + plaza, ''].concat(celdasImporte(totalPlaza))));
      acumular(totalProveedor, totalPlaza);
    });
    lineas.push(fila(['TOTAL ' + nombre, ''].concat(celdasImporte(totalProveedor))));
    return lineas.join('\n');
  }
  function nombreCsvProveedor(proveedor, periodo) {
    return 'craneo-liquidacion-' + String(proveedor || 'sin-proveedor').replace(/[^A-Za-z0-9-]+/g, '_')
      + '-' + periodo.anio + '-' + periodo.mes + '-Q' + periodo.quincena + '.csv';
  }
  window.CR.liquidacion = { csvProveedor: csvProveedor, fechaAr: fechaAr, importeAr: importeAr, nombreCsvProveedor: nombreCsvProveedor,
    textoMediaVuelta: textoMediaVuelta };






  var CSV_QUINCENA_RUTA = [
    { key: 'fecha', label: 'Fecha' },
    { key: 'agencia', label: 'Agencia' },
    { key: 'ruta', label: 'Ruta' },
    { key: 'chofer', label: 'Chofer' },
    { key: 'patente', label: 'Patente' },
    { key: 'unidad', label: 'Tipo de unidad' },
    { key: 'costo', label: 'Costo', csv: function (r) { return numeroCsv(r.costo); } },
    { key: 'adicional', label: 'Adicional', csv: function (r) { return numeroCsv(r.adicional); } },
    { key: 'penalidad', label: 'Penalidad', csv: function (r) { return numeroCsv(r.penalidad); } },
    { key: 'total_costo', label: 'Total', csv: function (r) { return numeroCsv(r.total_costo); } },
    { key: 'servicio', label: 'Servicio' },
    { key: 'km', label: 'Km', csv: function (r) { return numeroCsv(r.km); } },
    { key: 'doble', label: 'Doble', csv: function (r) { return r.doble ? 'Si' : 'No'; } },
    { key: 'salio', label: 'Salio (BackUp)', csv: function (r) { return r.salio === 'si' ? 'Si' : r.salio === 'no' ? 'No (50%)' : ''; } },
    { key: 'desestimada', label: 'Desestimada', csv: function (r) { return r.desestimada ? ('Si' + (r.desestimada_motivo ? ' (' + r.desestimada_motivo + ')' : '')) : 'No'; } },

    { key: 'media_vuelta', label: 'Media vuelta', csv: textoMediaVuelta }
  ];

  /* La tabla en pantalla y el CSV NO llevan las mismas columnas, a propósito. En pantalla entran
     ~12 antes de que el `overflow-x` empuje el TOTAL fuera de la vista — y en una pantalla de
     liquidación la columna que no se ve es justo la que importa. El CSV sí lleva todo, que es lo
     que se pega en la planilla. */
  var COLS_RUTA = [


    { key: 'cabecera', label: 'Cabecera', render: function (r) {

      if (!r.desestimada && r.media_vuelta) return h('span', null, r.cabecera, ' ', h('span', { className: 'badge badge--info', 'data-media-vuelta': r.cabecera,
        title: textoMediaVuelta(r) }, r.media_vuelta === 'cubre' ? '⇄ Cubrió · ' + (r.media_vuelta_pct || 75) + ' %' : '½ vuelta · ' + (r.media_vuelta_pct || 50) + ' %'));
      if (!r.desestimada) return r.cabecera;
      return h('span', null, r.cabecera, ' ', h('span', { className: 'badge badge--neutral', 'data-desestimada': r.cabecera,
        title: 'No la hicimos nosotros (MELI la cubrió con otra empresa). Motivo: ' + (r.desestimada_motivo || 'sin motivo') }, 'Desestimada'));
    } },
    { key: 'chofer_nombre', label: 'Chofer' },
    { key: 'id_driver', label: 'ID Driver', render: function (r) { return r.id_driver || vacio(); } },
    { key: 'patente', label: 'Patente' },


    { key: 'salio', label: 'Sali\u00f3', render: function (r) {
      if (String(r.servicio || '').toUpperCase() !== 'BACKUP') return vacio();
      if (r.salio === 'no') return h('span', { className: 'badge badge--warn', title: 'No sali\u00f3: pag\u00f3 el 50% de la tarifa' }, 'NO \u00b7 50%');
      if (r.salio === 'si') return 'S\u00ed';
      return h('span', { className: 'badge badge--danger', title: 'Back Up sin marcar \u00bfsali\u00f3?: pag\u00f3 completo. Marcalo en Asignar rutas antes de cerrar.' }, '\u26a0 sin marcar');
    } },
    { key: 'costo', label: 'Costo', align: 'right', render: function (r) { return money(r.costo); }, csv: function (r) { return numeroCsv(r.costo); } },
    { key: 'adicional', label: 'Adic.', align: 'right', render: function (r) { return money(r.adicional); } },
    { key: 'penalidad', label: 'Pen.', align: 'right', render: function (r) { return money(r.penalidad); } },
    // Se abre desde acá, ruta por ruta: el adicional es de UNA ruta, no del día. El contador dice
    // cuántos ítems tiene, así que una ruta sin ajustes y una con uno anulado no se ven igual.
    { key: '_ajustes', label: 'Ajustes', align: 'right', render: function (r) {
      return h('button', { className: 'btn btn--ghost btn--sm', 'data-ajustes': r.id,
        title: 'Cargar o ver adicionales y penalidades de esta ruta',
        onClick: function () { window.CR._abrirAjustes && window.CR._abrirAjustes(r); } },
        r.ajustes ? String(r.ajustes) + (r.ajustes === 1 ? ' ítem' : ' ítems') : 'Cargar');
    } },
    { key: 'total_costo', label: 'Total', align: 'right', className: 'col-fija-der', render: function (r) { return h('b', null, money(r.total_costo)); } },
    { key: 'con_ok', label: 'OK', render: function (r) { return r.con_ok ? 'Sí' : 'No'; } }
  ];

  var CSV_RUTA = [
    { key: 'fecha', label: 'Fecha' },
    { key: 'cabecera', label: 'Cabecera' },
    { key: 'id_ruta_meli', label: 'ID Ruta MELI', csv: function (r) { return r.id_ruta_meli || ''; } },
    { key: 'chofer_nombre', label: 'Chofer' },
    { key: 'id_driver', label: 'ID Driver', csv: function (r) { return r.id_driver || ''; } },
    { key: 'patente', label: 'Patente' },
    { key: 'id_vehiculo_meli', label: 'ID Vehiculo MELI', csv: function (r) { return r.id_vehiculo_meli || ''; } },
    { key: 'transporte', label: 'Proveedor', csv: function (r) { return r.transporte || ''; } },
    { key: 'tipo_vehiculo', label: 'Tipo' },
    { key: 'servicio', label: 'Servicio' },
    { key: 'salio', label: 'Salio (BackUp)', csv: function (r) { return r.salio === 'si' ? 'Si' : r.salio === 'no' ? 'No (50%)' : ''; } },
    { key: 'doble', label: 'Doble', csv: function (r) { return r.doble ? 'Si' : 'No'; } },
    { key: 'ciclo', label: 'Ciclo' },
    { key: 'macro_zona', label: 'Zona' },
    { key: 'hora', label: 'Hora' },
    { key: 'km', label: 'Km', csv: function (r) { return numeroCsv(r.km); } },
    { key: 'costo', label: 'Costo', csv: function (r) { return numeroCsv(r.costo); } },
    { key: 'adicional', label: 'Adicional', csv: function (r) { return numeroCsv(r.adicional); } },
    { key: 'penalidad', label: 'Penalidad', csv: function (r) { return numeroCsv(r.penalidad); } },
    { key: 'total_costo', label: 'Total', csv: function (r) { return numeroCsv(r.total_costo); } },
    { key: 'forzada', label: 'Forzada', csv: function (r) { return r.forzada ? 'Si' : 'No'; } },
    { key: 'ajustada', label: 'Ajustada', csv: function (r) { return r.ajustada ? 'Si' : 'No'; } },
    { key: 'notificada', label: 'Notificada', csv: function (r) { return r.notificada ? 'Si' : 'No'; } },
    { key: 'con_ok', label: 'OK', csv: function (r) { return r.con_ok ? 'Si' : 'No'; } },

    { key: 'desestimada', label: 'Desestimada', csv: function (r) { return r.desestimada ? ('Si' + (r.desestimada_motivo ? ' (' + r.desestimada_motivo + ')' : '')) : 'No'; } },

    { key: 'media_vuelta', label: 'Media vuelta', csv: textoMediaVuelta }
  ];

  function vacio() { return h('span', { className: 'muted', title: 'Todavía no lo estamos capturando' }, '—'); }








  function sinDato(motivo) { return h('span', { className: 'muted', title: motivo }, '—'); }
  var COLS_SIN_ID = [
    { key: 'patente', label: 'Patente', render: function (r) { return r.patente || sinDato('La ruta quedó congelada sin patente'); } },
    { key: 'chofer', label: 'Chofer', render: function (r) { return r.chofer || sinDato('La ruta quedó congelada sin chofer'); } },
    { key: 'transporte', label: 'Proveedor', render: function (r) { return r.transporte || 'Sin proveedor'; } },
    { key: 'rutas', label: 'Rutas', align: 'right' }
  ];

  /* Filtros del detalle del día. Con 75 rutas, encontrar las de una agencia a ojo es el trabajo que
     el operador NO tiene que hacer. Las opciones salen de las rutas que hay ese día, no de un
     catálogo: ofrecer un proveedor que ese día no trabajó manda a mirar una tabla vacía. */
  var SIN_FILTRO = { transporte: '', patente: '', chofer: '' };
  var ETIQUETA = { transporte: 'Proveedor', patente: 'Patente', chofer: 'Chofer' };
  function valoresDe(filas, campo) {
    var vistos = {}, out = [];
    (filas || []).forEach(function (r) {
      var v = String(r[campo] || '').trim();
      if (v && !vistos[v]) { vistos[v] = true; out.push(v); }
    });
    return out.sort(function (a, b) { return a.localeCompare(b, 'es'); });
  }





  function signo(n) { var v = Number(n || 0); return (v > 0 ? '+' : v < 0 ? '−' : '') + money(Math.abs(v)); }
  var ORIGEN = { patente: 'por patente', tipo_unidad: 'genérica (tipo de unidad)', estructura: 'estructura (ciclo 2 = $0)' };
  function nombreOrigen(o) { return o ? (ORIGEN[o] || o) : 'sin registrar'; }
  var COLS_PREVIA = [
    { key: 'cabecera', label: 'Ruta' },
    { key: 'patente', label: 'Patente', render: function (r) { return r.patente || vacio(); } },
    { key: 'chofer', label: 'Chofer', render: function (r) { return r.chofer || vacio(); } },
    { key: 'origen', label: 'Origen actual → nuevo', render: function (r) { return nombreOrigen(r.origenActual) + ' → ' + nombreOrigen(r.origenNuevo); } },
    { key: 'costoActual', label: 'Costo actual', align: 'right', render: function (r) { return money(r.costoActual); } },
    { key: 'costoNuevo', label: 'Costo nuevo', align: 'right', render: function (r) { return r.estado === 'cambia' ? h('b', null, money(r.costoNuevo)) : money(r.costoNuevo); } },
    { key: 'diferencia', label: 'Diferencia', align: 'right', render: function (r) { return r.diferencia ? h('b', null, signo(r.diferencia)) : h('span', { className: 'muted' }, '—'); } },
    { key: 'adicional', label: 'Adicional', align: 'right', render: function (r) { return money(r.adicional); } },
    { key: 'penalidad', label: 'Penalidad', align: 'right', render: function (r) { return money(r.penalidad); } },
    // Las marcas son lo que hay que MIRAR: el ajuste que se conserva, la patente a la que le falta
    // tarifa (se queda con la genérica) y la ruta que ya no existe en el día (no se toca).
    { key: '_marcas', label: 'Marcas', render: function (r) {
      var m = [];
      if (r.tieneAjuste) m.push(h('span', { key: 'aj', className: 'badge badge--warn', title: 'Tiene adicional o penalidad cargados: el costo se recalcula y el ajuste queda tal cual' }, 'con ajuste'));
      if (r.sinTarifaPatente) m.push(h('span', { key: 'st', className: 'badge badge--danger', title: 'La patente no tiene tarifa de proveedor vigente ese día: se queda con la genérica por tipo de unidad' }, 'sin tarifa por patente'));
      if (r.estado === 'sin_ruta_viva') m.push(h('span', { key: 'rv', className: 'badge badge--neutral', title: 'La ruta ya no existe en la jornada: no hay de dónde recalcular, no se toca' }, 'sin ruta viva'));
      return m.length ? h('span', { style: { display: 'inline-flex', gap: 4, flexWrap: 'wrap' } }, m) : null;
    } }
  ];

  function Historial(props) {
    var ui = window.CR.ui;
    var diasState = useState([]); var dias = diasState[0], setDias = diasState[1];
    var vistaState = useState('quincena'); var vista = vistaState[0], setVista = vistaState[1];
    var periodoState = useState(null); var periodo = periodoState[0], setPeriodo = periodoState[1];
    var resumenState = useState(null); var resumen = resumenState[0], setResumen = resumenState[1];
    var diaState = useState(''); var dia = diaState[0], setDia = diaState[1];
    var rutasState = useState([]); var rutas = rutasState[0], setRutas = rutasState[1];
    var loadState = useState(true); var loading = loadState[0], setLoading = loadState[1];
    var busyState = useState(false); var busy = busyState[0], setBusy = busyState[1];
    var recargaState = useState(0); var recarga = recargaState[0], setRecarga = recargaState[1];
    var errState = useState(''); var error = errState[0], setError = errState[1];
    var filtroState = useState(SIN_FILTRO); var filtro = filtroState[0], setFiltro = filtroState[1];
    var ajusteState = useState(null); var rutaAjuste = ajusteState[0], setRutaAjuste = ajusteState[1];
    // Estado propio del detalle quincenal: no reusa `busy`, que ya lo usa la carga del día. Con uno
    // solo, bajar el CSV deshabilitaría media pantalla y al revés.
    var bajaState = useState(false); var bajando = bajaState[0], setBajando = bajaState[1];


    var provCsvState = useState(null); var provCsv = provCsvState[0], setProvCsv = provCsvState[1];


    var sinIdState = useState(null); var sinId = sinIdState[0], setSinId = sinIdState[1];
    var sinIdBusyState = useState(false); var sinIdBusy = sinIdBusyState[0], setSinIdBusy = sinIdBusyState[1];



    var previaState = useState(null); var previaRecalc = previaState[0], setPreviaRecalc = previaState[1];
    var motivoState = useState(''); var motivoRecalc = motivoState[0], setMotivoRecalc = motivoState[1];
    var recalcBusyState = useState(false); var recalcBusy = recalcBusyState[0], setRecalcBusy = recalcBusyState[1];
    var recalcAvisoState = useState(null); var recalcAviso = recalcAvisoState[0], setRecalcAviso = recalcAvisoState[1];
    /* El permiso lo decide el servidor (`session.secciones`, vía window.CR.permisos): sin
       `cierre-manual` no hay botón — y aunque lo hubiera, el endpoint contesta 403. Misma sección
       que reabrir y recongelar: reescribe el costo de una liquidación ya congelada. */
    var permisos = window.CR.permisos;
    var puedeRecalcular = !!(permisos && permisos.puedeSeccion(props && props.session, 'cierre-manual'));

    var hayFiltro = !!(filtro.transporte || filtro.patente || filtro.chofer);
    var visibles = rutas.filter(function (r) {
      return (!filtro.transporte || String(r.transporte || '') === filtro.transporte)
        && (!filtro.patente || String(r.patente || '') === filtro.patente)
        && (!filtro.chofer || String(r.chofer_nombre || '') === filtro.chofer);
    });



    var hecha = function (r) { return !r.desestimada; };
    /* Las opciones de cada filtro salen de lo que queda tras aplicar LOS OTROS. Si ya filtraste por
       COSTANZO, el desplegable de choferes ofrece sólo los de COSTANZO: si ofreciera todos, elegir
       uno daría una tabla vacía y parecería que el sistema perdió las rutas. */
    function opcionesDe(campo) {
      var resto = rutas.filter(function (r) {
        return (campo === 'transporte' || !filtro.transporte || String(r.transporte || '') === filtro.transporte)
          && (campo === 'patente' || !filtro.patente || String(r.patente || '') === filtro.patente)
          && (campo === 'chofer' || !filtro.chofer || String(r.chofer_nombre || '') === filtro.chofer);
      });
      return valoresDe(resto, campo === 'chofer' ? 'chofer_nombre' : campo);
    }

    // el archivo se llama como lo que trae: si bajó el corte de COSTANZO, que no diga "todo el día"
    function nombreCsv() {
      var suf = [filtro.transporte, filtro.patente, filtro.chofer].filter(Boolean)
        .join('-').replace(/[^A-Za-z0-9-]+/g, '_');
      return 'craneo-rutas-' + dia + (suf ? '-' + suf : '') + '.csv';
    }



    function bajarCsvProveedor(transporte) {
      setBajando(true); setError('');
      window.CR.backend.historialQuincena(periodo.anio, periodo.mes, periodo.quincena, true)
        .then(function (j) {
          var rutas = ((j && j.rutas) || []).filter(function (r) { return (r.agencia || '') === (transporte || ''); });
          if (!rutas.length) { setError('Ese proveedor no tiene rutas congeladas en la quincena.'); return; }
          window.CR.csv.descargarTexto(nombreCsvProveedor(transporte, periodo), csvProveedor(transporte, rutas));
        })
        .catch(function () { setError('No pudimos armar el CSV del proveedor.'); })
        .finally(function () { setBajando(false); });
    }
    // un botón por agencia de la quincena, en el orden en que salen del resumen
    var proveedores = [];
    ((resumen && resumen.porProveedor) || []).forEach(function (r) {
      var k = r.transporte || '';
      if (proveedores.indexOf(k) === -1) proveedores.push(k);
    });













    var provCsvValido = provCsv !== null && proveedores.indexOf(provCsv) !== -1;

    // Los períodos salen de los días QUE EXISTEN: ofrecer un combo de meses completo mandaría al
    // operador a elegir una quincena vacía y a pensar que se perdieron los datos.
    var periodos = [];
    var vistos = {};
    dias.forEach(function (d) {
      var k = d.anio + '-' + d.mes + '-' + d.quincena;
      if (!vistos[k]) { vistos[k] = true; periodos.push({ anio: d.anio, mes: d.mes, quincena: d.quincena, key: k }); }
    });

    useEffect(function () {
      window.CR.backend.historialDias(120).then(function (rows) {
        setDias(rows);
        if (rows.length) setPeriodo({ anio: rows[0].anio, mes: rows[0].mes, quincena: rows[0].quincena });
        setDia(rows.length ? rows[0].fecha : '');
      }).catch(function () { setError('No pudimos leer los días cerrados. Reintentá.'); })
        .finally(function () { setLoading(false); });
    }, []);

    useEffect(function () {
      if (!periodo) return;
      setBusy(true); setError('');
      // la lista de sin ID Driver es de UNA quincena: al cambiar de período se cierra, si no quedaría
      // mostrando patentes de la quincena anterior debajo del aviso de la nueva
      setSinId(null);
      window.CR.backend.historialQuincena(periodo.anio, periodo.mes, periodo.quincena)
        .then(setResumen)
        .catch(function () { setError('No pudimos calcular la quincena. Reintentá.'); })
        .finally(function () { setBusy(false); });
    }, [periodo && periodo.anio, periodo && periodo.mes, periodo && periodo.quincena]);

    // al cambiar de día se limpian los filtros: quedarían apuntando a un proveedor o una patente que
    // ese otro día no trabajó, y la tabla saldría vacía sin que se entienda por qué.
    useEffect(function () { setFiltro(SIN_FILTRO); }, [dia]);
    // la previa del recálculo es de UN día: al cambiar de día o de pestaña se cierra. Si quedara
    // abierta, debajo del detalle del día nuevo habría una propuesta de plata de otra fecha.
    useEffect(function () { setPreviaRecalc(null); setMotivoRecalc(''); setRecalcAviso(null); }, [dia, vista]);

    /* La previa se PIDE al apretar, con el día elegido, y no cambia nada: es lo que el servidor
       calcularía hoy con la misma fórmula del cierre. Un fallo se dice, no se traga. */
    function abrirPreviaRecalculo() {
      setRecalcBusy(true); setRecalcAviso(null); setError('');
      window.CR.backend.previaRecalculoDia(dia)
        .then(function (p) { setPreviaRecalc(p); setMotivoRecalc(''); })
        .catch(function (e) { setRecalcAviso({ tono: 'alta', texto: (e && e.message) || 'No pudimos armar la previa del recálculo. Reintentá.' }); })
        .finally(function () { setRecalcBusy(false); });
    }
    /* Confirmar manda la DIFERENCIA QUE SE VIO (`totales.diferencia` de la previa): si la base
       cambió en el medio —una tarifa, un ajuste—, el servidor contesta 409 previa_desactualizada
       y no toca nada. Aplicar un número que nadie miró es la familia de bugs de este sistema. */
    function confirmarRecalculo() {
      if (!previaRecalc) return;
      var motivo = String(motivoRecalc || '').trim();
      if (motivo.length < 3) { setRecalcAviso({ tono: 'alta', texto: 'Escribí por qué se recalcula el día (queda en el historial).' }); return; }
      var t = previaRecalc.totales;
      if (!window.confirm('Vas a recalcular el ' + dia + ' con la tarifa por patente.\n\n'
        + t.conCambio + ' ruta(s) cambian de costo: de ' + money(t.costoActual) + ' a ' + money(t.costoNuevo) + ' (' + signo(t.diferencia) + ').\n'
        + (t.conAjuste ? t.conAjuste + ' con ajuste manual: el costo se recalcula y el ajuste queda como está.\n' : '')
        + '\nMotivo: ' + motivo + '\n\n¿Confirmás?')) return;
      setRecalcBusy(true); setRecalcAviso(null);
      window.CR.backend.recalcularDia(dia, motivo, t.diferencia)
        .then(function (r) {
          setPreviaRecalc(null); setMotivoRecalc('');
          setRecalcAviso({ tono: 'ok', texto: (r && r.mensaje) || 'Día recalculado.' });
          // el detalle y la lista de días se releen: el total del día cambió EN LA BASE, y la
          // pantalla no puede seguir mostrando el número viejo
          setRecarga(function (n) { return n + 1; });
          return window.CR.backend.historialDias(120).then(setDias).catch(function () {});
        })
        .catch(function (e) {
          if (e && e.clave === 'previa_desactualizada') {
            setPreviaRecalc(null);
            setRecalcAviso({ tono: 'alta', texto: 'Cambió algo desde que abriste la previa (una tarifa, un ajuste o el propio día): no se modificó nada. Volvé a abrirla y confirmá de nuevo.' });
          } else {
            setRecalcAviso({ tono: 'alta', texto: (e && e.message) || 'No pudimos recalcular el día.' });
          }
        })
        .finally(function () { setRecalcBusy(false); });
    }

    useEffect(function () {
      if (!dia || vista !== 'dia') return;
      setBusy(true); setError('');
      window.CR.backend.historialRutas(dia, dia)
        .then(setRutas)
        .catch(function () { setError('No pudimos leer el detalle del día. Reintentá.'); })
        .finally(function () { setBusy(false); });
    }, [dia, vista, recarga]);

    /* El botón de cada fila vive dentro de una columna de DataTable, que no recibe props del
       componente. El puente es este handler global — se registra mientras la pantalla está montada y
       se saca al desmontarla, para no dejar apuntando a un setState muerto. */
    useEffect(function () {
      window.CR._abrirAjustes = setRutaAjuste;
      return function () { delete window.CR._abrirAjustes; };
    }, []);

    /* Aca vivia un CUARTO gate por rol —`props.rol !== 'gerente_logistica'`— que el 21/08 le
       mostraba «Acceso restringido» a Coordinador, Planificador y Liquidaciones aunque el servidor
       ya les habilitaba la seccion. El router de `app.jsx` decide por `puedeSeccion` con las
       secciones que publica el servidor: una copia de la regla acá solo puede desincronizarse. */
    if (loading) return h('div', { className: 'empty', role: 'status' }, h('p', null, 'Cargando el historial…'));
    if (!dias.length) {
      return h(ui.Page, { title: 'Módulo de Liquidaciones', subtitle: 'Base de la liquidación' },
        h(ui.EmptyState, { title: 'Todavía no hay días cerrados' },
          'Cada vez que se cierra una jornada, su detalle queda congelado acá con los costos de esa fecha.'));
    }

    var t = resumen && resumen.totales;




    var faltantes = [];
    if (t && Number(t.sin_id_driver) > 0) faltantes.push(Number(t.sin_id_driver) + ' sin ID Driver');


    return h(ui.Page, { title: 'Módulo de Liquidaciones', subtitle: 'El día cerrado con sus costos — base de la liquidación' },
      h(ui.Tabs, {
        value: vista, onChange: setVista,
        tabs: [{ value: 'quincena', label: 'Por quincena' }, { value: 'dia', label: 'Detalle del día' }]
      }),

      error ? h('div', { className: 'alerta alerta--alta', role: 'alert' }, error) : null,

      vista === 'quincena' ? h('div', null,
        h(ui.Toolbar, null,
          h(ui.Field, { label: 'Quincena' },
            h(ui.Select, {
              value: periodo ? periodo.anio + '-' + periodo.mes + '-' + periodo.quincena : '',
              onChange: function (k) {
                var p = k.split('-');
                setPeriodo({ anio: Number(p[0]), mes: Number(p[1]), quincena: Number(p[2]) });
              },
              options: periodos.map(function (p) { return { value: p.key, label: nombrePeriodo(p) }; })
            })),



          proveedores.length ? h(ui.Field, { label: 'Proveedor' },
            h(ui.Select, {
              value: provCsvValido ? String(proveedores.indexOf(provCsv)) : '',
              placeholder: 'Elegí la agencia', 'aria-label': 'Proveedor del CSV',
              onChange: function (v) { setProvCsv(v === '' ? null : proveedores[Number(v)]); },
              options: proveedores.map(function (p, i) { return { value: String(i), label: p || 'Sin proveedor' }; })
            })) : null,
          proveedores.length ? h(ui.Button, {
            variant: 'primary', disabled: bajando || !provCsvValido,
            'data-novedad': 'csv-proveedor',
            title: provCsvValido
              ? 'Baja el CSV de ' + (provCsv || 'Sin proveedor') + ' con el formato que se le manda a la agencia: fecha, vehículo, costo, adicional, penalidades y total, un bloque por plaza'
              : 'Elegí la agencia: baja su CSV con el formato que se le manda (fecha, vehículo, costo, adicional, penalidades y total, un bloque por plaza)',
            onClick: function () { if (provCsvValido) bajarCsvProveedor(provCsv); }
          }, 'Descargar CSV del proveedor') : null,
          /* NO es el archivo de la agencia: una fila por unidad con la cantidad de rutas y el total de la
             quincena, TODAS las agencias juntas. Se llamaba «Descargar CSV (por proveedor)» y era lo
             primero que se veía: todo indica que es lo que el cliente bajó el 22/09 (ver arriba). */
          resumen && resumen.porProveedor.length ? h(ui.Button, {
            variant: 'ghost',
            title: 'Una fila por agencia, plaza y unidad, con la cantidad de rutas y el total de la quincena. No es el formato que se le manda a la agencia: ese es «Descargar CSV del proveedor».',
            onClick: function () {
              descargarCsv('craneo-quincena-' + periodo.anio + '-' + periodo.mes + '-Q' + periodo.quincena + '.csv',
                COLS_PROVEEDOR, resumen.porProveedor);
            }
          }, 'Descargar resumen por unidad (todas las agencias)') : null,


          resumen && resumen.porProveedor.length ? h(ui.Button, {
            variant: 'ghost', disabled: bajando,
            title: 'Una fila por ruta de la quincena, de TODAS las agencias. No es el formato que se le manda a la agencia: ese es «Descargar CSV del proveedor».',
            onClick: function () {
              setBajando(true); setError('');
              window.CR.backend.historialQuincena(periodo.anio, periodo.mes, periodo.quincena, true)
                .then(function (j) {
                  var filas = (j && j.rutas) || [];
                  if (!filas.length) { setError('Esa quincena no tiene rutas congeladas para detallar.'); return; }
                  descargarCsv('craneo-quincena-detalle-' + periodo.anio + '-' + periodo.mes
                    + '-Q' + periodo.quincena + '.csv', CSV_QUINCENA_RUTA, filas);
                })
                .catch(function () { setError('No pudimos traer el detalle de la quincena.'); })
                .finally(function () { setBajando(false); });
            }
          }, bajando ? 'Armando…' : 'Descargar detalle por ruta (todas las agencias)') : null),

        busy ? h('div', { className: 'empty', role: 'status' }, h('p', null, 'Calculando…')) : null,

        t ? h(ui.Card, null, h('div', { className: 'resumen-row' },
          h(ui.StatCard, { label: 'Rutas', value: t.rutas }),
          h(ui.StatCard, { label: 'Proveedores', value: t.proveedores }),
          h(ui.StatCard, { label: 'Unidades', value: t.unidades }),

          Number(t.desestimadas) > 0 ? h(ui.StatCard, { label: 'Desestimadas', value: Number(t.desestimadas), sub: 'no se hicieron: a $0' }) : null,
          window.CR.canSeeCosts ? h(ui.StatCard, { label: 'Total a liquidar', value: money(t.total), tone: 'ok' }) : null)) : null,

        faltantes.length ? h('div', { className: 'alerta alerta--media' },
          'Faltan datos de MELI en esta quincena: ' + faltantes.join(' · ') + '. ' +
          'No los estamos capturando todavía — se completan cuando BRAINS defina de dónde salen.',


          Number(t.sin_id_driver) > 0 ? h(ui.Button, {
            variant: 'ghost', size: 'sm', disabled: sinIdBusy, style: { marginLeft: 8 },
            'aria-label': 'Ver quiénes no tienen ID Driver en esta quincena',
            title: 'Lista patente y chofer de las rutas sin ID Driver, para cargarlo en Flota',
            onClick: function () {
              setSinIdBusy(true); setError('');
              window.CR.backend.historialSinIdDriver(periodo.anio, periodo.mes, periodo.quincena)
                .then(function (j) { setSinId(j && j.filas ? j : { filas: [], sin_id_driver: 0 }); })
                .catch(function () { setError('No pudimos traer quiénes no tienen ID Driver. Reintentá.'); })
                .finally(function () { setSinIdBusy(false); });
            }
          }, sinIdBusy ? 'Buscando…' : 'Ver quiénes') : null) : null,

        sinId ? h('div', { className: 'card', 'data-sin-id-driver': '1' },
          h('h3', null, 'Sin ID Driver en esta quincena'),
          h('p', { className: 'muted' },
            'Una fila por patente y chofer; la suma de rutas es la del aviso. ' +
            'El ID Driver se carga al chofer en Flota (MELI): las jornadas que se cierren desde entonces salen completas. ',
            h('a', { href: '#/flota-real' }, 'Cargar el ID en Flota')),
          h(ui.DataTable, {
            columns: COLS_SIN_ID, rows: sinId.filas, rowKey: null,
            empty: 'Esta quincena ya tiene todas sus rutas con ID Driver'
          })) : null,

        resumen ? h('div', { className: 'card' },
          h('h3', null, 'Por proveedor y unidad'),



          h('p', { className: 'muted' }, 'Totales de la quincena por agencia, plaza (AMBA o ROSARIO) y unidad. El archivo para mandarle a cada agencia es «Descargar CSV del proveedor», arriba.'),
          h(ui.DataTable, {
            columns: COLS_PROVEEDOR, rows: resumen.porProveedor, rowKey: null,
            empty: 'La quincena no tiene rutas cerradas'
          }),


          proveedores.length ? h('div', { className: 'toolbar', 'data-csv-proveedores': '1', style: { marginTop: 12, flexWrap: 'wrap' } },
            h('span', { className: 'muted' }, 'El mismo CSV del proveedor, uno por agencia (para bajar varias seguidas):'),
            proveedores.map(function (p) {
              return h(ui.Button, { key: p || '_', variant: 'ghost', size: 'sm', disabled: bajando,
                'aria-label': 'CSV proveedor ' + (p || 'Sin proveedor'),
                title: 'Baja el CSV de ' + (p || 'Sin proveedor') + ': fecha, vehículo, costo, adicional, penalidades y total, por plaza',
                onClick: function () { bajarCsvProveedor(p); } }, 'CSV proveedor: ' + (p || 'Sin proveedor'));
            })) : null) : null) : null,

      vista === 'dia' ? h('div', null,
        h(ui.Toolbar, null,
          h(ui.Field, { label: 'Día cerrado' },
            h(ui.Select, {
              value: dia, onChange: setDia,
              options: dias.map(function (d) {


                var des = Number(d.desestimadas) || 0;
                return { value: d.fecha, label: d.fecha + ' · ' + d.rutas + ' rutas' +
                  (des ? ' · ' + des + (des === 1 ? ' desestimada' : ' desestimadas') : '') +
                  (window.CR.canSeeCosts ? ' · ' + money(d.total) : '') };
              })
            })),
          rutas.length ? h(ui.Field, { label: 'Proveedor' },
            h(ui.Select, { value: filtro.transporte, placeholder: 'Todos', 'aria-label': 'Filtrar por proveedor',
              onChange: function (v) { setFiltro(Object.assign({}, filtro, { transporte: v })); },
              options: opcionesDe('transporte') })) : null,
          rutas.length ? h(ui.Field, { label: 'Patente' },
            h(ui.Select, { value: filtro.patente, placeholder: 'Todas', 'aria-label': 'Filtrar por patente',
              onChange: function (v) { setFiltro(Object.assign({}, filtro, { patente: v })); },
              options: opcionesDe('patente') })) : null,
          rutas.length ? h(ui.Field, { label: 'Chofer' },
            h(ui.Select, { value: filtro.chofer, placeholder: 'Todos', 'aria-label': 'Filtrar por chofer',
              onChange: function (v) { setFiltro(Object.assign({}, filtro, { chofer: v })); },
              options: opcionesDe('chofer') })) : null,
          visibles.length ? h(ui.Button, {
            variant: 'ghost',
            // baja LO QUE ESTÁ VIENDO: filtrar por agencia y descargar es justo el corte que se le
            // manda a cada proveedor. Si bajara el día entero, el filtro sería decorativo.
            onClick: function () { descargarCsv(nombreCsv(), CSV_RUTA, visibles); }
          }, 'Descargar CSV (' + CSV_RUTA.length + ' columnas)') : null,


          puedeRecalcular && dia && !previaRecalc ? h(ui.Button, {
            variant: 'ghost', disabled: recalcBusy || busy,
            'aria-label': 'Recalcular con tarifa por patente',
            title: 'Muestra, ruta por ruta, cuánto cambiaría el costo de este día con la tarifa por patente vigente esa fecha. No cambia nada hasta que confirmes.',
            onClick: abrirPreviaRecalculo
          }, recalcBusy ? 'Armando la previa…' : 'Recalcular con tarifa por patente') : null),

        // los filtros puestos, visibles y sacables de a uno
        hayFiltro ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', margin: '4px 0 12px' } },
          ['transporte', 'patente', 'chofer'].filter(function (k) { return filtro[k]; }).map(function (k) {
            return h('span', { className: 'chip-filtro', key: k },
              ETIQUETA[k] + ': ' + filtro[k],
              h('button', { type: 'button', title: 'Quitar este filtro', 'aria-label': 'Quitar el filtro de ' + ETIQUETA[k],
                onClick: function () { var n = Object.assign({}, filtro); n[k] = ''; setFiltro(n); } }, '✕'));
          }).concat([
            h('button', { key: '_todos', className: 'btn btn--ghost btn--sm',
              onClick: function () { setFiltro(SIN_FILTRO); } }, 'Ver todas')
          ])) : null,

        // el resultado (o el rechazo) del recálculo se DICE, con su tono: un 409 por previa vieja no
        // es un error del sistema, es una instrucción — volvé a abrir la previa
        recalcAviso ? h('div', {
          className: 'alerta ' + (recalcAviso.tono === 'ok' ? 'alerta--baja' : 'alerta--alta'),
          role: recalcAviso.tono === 'ok' ? 'status' : 'alert', 'data-recalculo-aviso': recalcAviso.tono
        }, recalcAviso.texto) : null,

        previaRecalc ? h('div', { className: 'card', 'data-recalculo-dia': '1' },
          h('h3', null, 'Recalcular el ' + previaRecalc.fecha + ' con la tarifa por patente'),
          h('p', { className: 'muted' },
            'Es una previa: no cambia nada hasta que confirmes. Cada fila usa la tarifa de proveedor vigente ESE día, ' +
            'con la misma fórmula del cierre. Las rutas con ajuste se recalculan igual y el ajuste queda como está; ' +
            'una ruta sin tarifa por patente se queda con la genérica, y una sin ruta viva no se toca.'),
          h('div', { className: 'resumen-row', 'data-recalculo-totales': '1' },
            h(ui.StatCard, { label: 'Rutas que cambian', value: previaRecalc.totales.conCambio, sub: 'de ' + previaRecalc.totales.filas }),
            h(ui.StatCard, { label: 'Con ajuste', value: previaRecalc.totales.conAjuste }),
            h(ui.StatCard, { label: 'Costo actual', value: money(previaRecalc.totales.costoActual) }),
            h(ui.StatCard, { label: 'Costo nuevo', value: money(previaRecalc.totales.costoNuevo) }),
            h(ui.StatCard, { label: 'Diferencia', value: signo(previaRecalc.totales.diferencia), tone: previaRecalc.totales.diferencia ? 'accent' : 'ok' })),
          (previaRecalc.totales.sinTarifaPatente || previaRecalc.totales.sinRutaViva) ? h('p', { className: 'muted' },
            [previaRecalc.totales.sinTarifaPatente
              ? previaRecalc.totales.sinTarifaPatente + ' sin tarifa por patente: se quedan con la genérica (cargala en Tarifa de proveedores y volvé a abrir la previa)' : null,
             previaRecalc.totales.sinRutaViva ? previaRecalc.totales.sinRutaViva + ' sin ruta viva: no se tocan' : null]
              .filter(Boolean).join(' · ')) : null,
          h(ui.DataTable, { columns: COLS_PREVIA, rows: previaRecalc.filas, rowKey: 'id', empty: 'El día no tiene filas congeladas' }),
          h('div', { className: 'toolbar', style: { marginTop: 12, flexWrap: 'wrap' } },
            h(ui.Field, { label: 'Motivo (queda en el historial)' },
              h(ui.Input, { value: motivoRecalc, onChange: setMotivoRecalc, placeholder: 'Por qué se recalcula este día',
                'aria-label': 'Motivo del recálculo', disabled: recalcBusy })),
            h(ui.Button, {
              variant: 'primary',
              disabled: recalcBusy || previaRecalc.totales.conCambio === 0 || String(motivoRecalc || '').trim().length < 3,
              'aria-label': 'Confirmar recálculo',
              title: previaRecalc.totales.conCambio === 0
                ? 'Este día ya está con la tarifa por patente: no hay nada que recalcular'
                : 'Aplica los costos nuevos de la tabla. Te pide una confirmación más.',
              onClick: confirmarRecalculo
            }, recalcBusy ? 'Recalculando…' : 'Confirmar recálculo'),
            h(ui.Button, { variant: 'ghost', disabled: recalcBusy, 'aria-label': 'Cerrar la previa',
              onClick: function () { setPreviaRecalc(null); setMotivoRecalc(''); } }, 'Cancelar'))) : null,

        busy ? h('div', { className: 'empty', role: 'status' }, h('p', null, 'Cargando el día…')) : null,

        !busy && rutas.length ? h(ui.Card, null, h('div', { className: 'resumen-row' },
          h(ui.StatCard, { label: hayFiltro ? 'Rutas filtradas' : 'Rutas del día',
            value: visibles.filter(hecha).length, sub: hayFiltro ? 'de ' + rutas.filter(hecha).length : null }),

          h(ui.StatCard, { label: 'Con OK del dueño', value: visibles.filter(function (r) { return hecha(r) && r.con_ok; }).length }),
          visibles.some(function (r) { return r.desestimada; }) ? h(ui.StatCard, { label: 'Desestimadas', value: visibles.filter(function (r) { return r.desestimada; }).length, sub: 'no se hicieron: a $0' }) : null,
          window.CR.canSeeCosts ? h(ui.StatCard, {
            label: hayFiltro ? 'Total filtrado' : 'Total del día',
            value: money(visibles.reduce(function (a, r) { return a + Number(r.total_costo || 0); }, 0))
          }) : null)) : null,

        !busy ? h('div', { className: 'card' },
          h(ui.DataTable, {
            columns: COLS_RUTA, rows: visibles, rowKey: 'cabecera',
            empty: hayFiltro ? 'Ninguna ruta de ese día coincide con el filtro' : 'Ese día no tiene rutas congeladas'
          }),
          rutaAjuste ? h(window.CR.screens.AjustesRuta, {
            ruta: rutaAjuste,
            onClose: function () { setRutaAjuste(null); },
            // tras cargar o anular, el día se relee: si no, el total de arriba sigue mostrando el
            // número viejo y la pantalla queda diciendo una cosa mientras la base dice otra.
            onCambio: function () { setRecarga(function (n) { return n + 1; }); }
          }) : null,
          rutas.length ? h('p', { className: 'muted', style: { marginTop: 8 } },
            'En pantalla van las columnas que entran sin perder de vista el total. La descarga trae ' +
            'las 25: ID de ruta MELI, proveedor, zona, km, ciclo, hora, tipo de vehículo, doble, forzada, ajustada, ' +
            'notificada y desestimada — y baja sólo lo que estás viendo.') : null) : null) : null);
  }

  window.CR.screens.Historial = Historial;
})();
