


















(function () {
  var React = window.React;
  var h = React.createElement;
  var useState = React.useState;
  var useEffect = React.useEffect;

  var money = function (n) {
    if (n == null) return '—';
    return '$' + Number(n).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  };
  var pct = function (n) {
    if (n == null) return '—';
    return String(Math.round(n * 1000) / 10).replace('.', ',') + '%';
  };
  var mult = function (n) {
    if (n == null) return '—';
    return String(Math.round(n * 100) / 100).replace('.', ',') + '×';
  };
  /* Postgres devuelve `numeric` como texto con punto. Para que Excel es-AR pueda sumar las columnas
     de plata, el CSV usa coma decimal. No se calcula nada: solo cambia la representacion. */
  var numeroCsv = function (n) {
    if (n === null || n === undefined || n === '') return '';
    var s = String(n);
    return /^-?\d+(\.\d+)?$/.test(s) ? s.replace('.', ',') : s;
  };

  var MESES = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
    'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];

  /* LAS FAMILIAS SON TRES Y EL RÓTULO TENÍA DOS RAMAS. Un `familia === 'meli' ? A : B` mandaba a
     `impuesto` por la rama de `indirecto`: IIBB e IMP DEB - CRED se listaban como «Gasto
     indirecto». La CUENTA estaba bien —el servidor los aplica como alícuota sobre el Sub Total y
     los muestra aparte en la cascada—; mentía la etiqueta. Y hoy las únicas filas cargadas en la
     base son impuestos, así que el gerente ve las dos mal rotuladas apenas abre el módulo.
     El default devuelve la familia CRUDA y no una de las tres: si mañana entra una cuarta, un
     rótulo elegido a dedo se lee como un dato y nadie lo revisa. Un `familia` desconocido en la
     tabla se ve raro, que es exactamente lo que tiene que pasar. */
  var ROTULO_FAMILIA = { meli: 'Descuento MELI', impuesto: 'Impuesto', indirecto: 'Gasto indirecto' };
  var rotuloFamilia = function (f) { return ROTULO_FAMILIA[f] || f; };

  /* Los motivos por los que una ruta no se puede facturar, en castellano y con DÓNDE se arregla.
     Un contador sin acción es un número que se mira y no se toca. */
  var MOTIVOS = {
    sin_tipo_de_unidad: { texto: 'sin tipo de unidad cargado', como: 'Se completa en la ficha de la unidad, en Flota.' },
    sin_km: { texto: 'sin kilómetros', como: 'Llegan con el archivo diario de MELI. Si la ruta ya cerró sin km, gerencia se los carga acá abajo.' },
    /* Revisión de v1.0.14, I1: una doble se factura por la SUMA de sus dos mitades; si a una le falta el
       km, la doble no factura (antes facturaba el tramo de la mitad que tenía, más bajo y sin aviso). */
    doble_sin_km: { texto: 'dobles con una mitad sin kilómetros', como: 'La doble se factura con los km de sus dos mitades: cargale el km a la mitad que falta, acá abajo (la columna Ciclo dice cuál).' },
    sin_tarifa_cargada: { texto: 'sin tarifa de MELI para esa combinación', como: 'Se carga en Tarifas de Mercado Libre.' }
  };






  var validarKm = function (v) {
    var t = String(v == null ? '' : v).trim();
    if (/^\d{1,3}(\.\d{3})+(,\d{1,2})?$/.test(t))
      return 'Escribí los km sin separador de miles (1000, no 1.000); los decimales van con coma: 120,5.';
    var m = /^(\d+)(?:[.,](\d{1,2}))?$/.exec(t);
    if (!m) return 'Los km van con números: hasta dos decimales, con coma o punto (por ejemplo 120,5).';
    var km = Number(m[1] + (m[2] ? '.' + m[2] : ''));
    if (!(km > 0) || km > 1000) return 'Los km recorridos van de 0,01 a 1.000.';
    return null;
  };
  var kmConComa = function (n) { return n == null ? '—' : String(n).replace('.', ','); };
  var ESTILO_KM_INVALIDO = { color: 'var(--danger)', fontSize: 12, flexBasis: '100%' };








  var TRAMO_TEXTO = { '0_50': '0–50 km', '51_100': '51–100 km', '101_150': '101–150 km', MAS_150: 'más de 150 km' };
  var JORNADA_TEXTO = { media: 'media jornada', completa: 'jornada completa' };
  var textoBackupNoSalio = function (r) {
    if (r.factorBackup == null) return 'BACKUP que no salió';
    var base = r.rangoKm ? (TRAMO_TEXTO[r.rangoKm] || r.rangoKm)
      : r.jornada ? (JORNADA_TEXTO[r.jornada] || r.jornada) + ' (Rosario)' : '';
    return 'BACKUP que no salió · ' + String(Math.round(r.factorBackup * 1000) / 10).replace('.', ',')
      + ' % de ' + base;
  };

  /* Es la antigua Hoja de rutas convertida en archivo: mismos datos resueltos por el servidor, sin
     volver a meter una tabla larga en la pantalla. `margenPct` es facturacion/costo (multiplicador)
     y `rentabilidad` es margen/facturacion; los rotulos dicen la formula para que no se confundan. */
  var CSV_HOJA_RUTAS = [
    { key: 'fecha', label: 'Fecha' },
    { key: 'quincena', label: 'Quincena' },
    { key: 'cabecera', label: 'Ruta' },
    { key: 'chofer', label: 'Chofer' },
    { key: 'patente', label: 'Patente' },
    { key: 'tipoUnidad', label: 'Tipo de unidad' },
    { key: 'servicio', label: 'Servicio' },
    { key: 'ambito', label: 'Ámbito' },


    { key: 'plaza', label: 'Plaza', csv: function (r) { return r.plaza || 'Sin plaza'; } },
    { key: 'km', label: 'Km', csv: function (r) { return numeroCsv(r.km); } },


    { key: 'kmCargado', label: 'Km cargado a mano', csv: function (r) {
      return r.kmCargado ? 'Sí: ' + (r.kmCargado.motivo || 'sin motivo') : '';
    } },
    { key: 'kmFacturable', label: 'Km facturables', csv: function (r) { return numeroCsv(r.kmFacturable); } },


    { key: 'rangoKm', label: 'Tramo tarifario', csv: function (r) {
      if (r.backupNoSalio === true) return textoBackupNoSalio(r);
      return r.jornada ? 'Rosario · ' + r.jornada : (r.rangoKm || '');
    } },
    { key: 'doble', label: 'Doble', csv: function (r) { return r.doble ? 'Sí' : 'No'; } },
    { key: 'costo', label: 'Costo base', 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: 'costoTotal', label: 'Costo total', csv: function (r) { return numeroCsv(r.costoTotal); } },
    { key: 'facturacion', label: 'Facturación', csv: function (r) { return r.sinFacturar ? '' : numeroCsv(r.facturacion); } },
    { key: 'margen', label: 'Margen $', csv: function (r) { return r.sinFacturar ? '' : numeroCsv(r.margen); } },
    { key: 'margenPct', label: 'Margen % (facturación ÷ costo)', csv: function (r) { return r.sinFacturar ? '' : mult(r.margenPct); } },
    { key: 'rentabilidad', label: 'Rentabilidad (margen ÷ facturación)', csv: function (r) { return r.sinFacturar ? '' : pct(r.rentabilidad); } },
    { key: 'sinFacturar', label: 'Sin facturar', csv: function (r) {
      var motivo = MOTIVOS[r.sinFacturar];
      return motivo ? motivo.texto : (r.sinFacturar || '');
    } },




    { key: 'mediaVuelta', label: 'Media vuelta', csv: function (r) {
      var mv = r.mediaVuelta;
      if (!mv || !mv.rol) return '';
      var pct = String(Math.round(Number(mv.factor) * 100)) + ' %';
      if (mv.rol === 'titular') return 'Titular ' + pct + ': la otra vuelta (' + (mv.parIdRuta || '?') + ') la cubrió otra unidad';
      if (mv.rol === 'mitad_sola') return 'Sola ' + pct + ': la otra vuelta (' + (mv.parIdRuta || '?') + ') se desestimó';
      return 'Cubre ' + pct;
    } }
  ];














  var pctDe = function (parte, base) {
    var b = Number(base || 0);
    if (!b) return null;
    return Number(parte || 0) / b;
  };
  var bloque = function (titulo, filas, opciones) {
    var o = opciones || {};
    return h('div', { key: titulo, className: 'stat' + (o.tone ? ' stat--' + o.tone : ''),
      'data-bloque': titulo },
      h('div', { className: 'lbl' }, titulo),
      filas.map(function (f, i) {
        return h('div', { key: f[0] || i, className: 'val tnum', 'data-dato': f[0],
          style: { fontSize: 19, marginTop: i === 0 ? 7 : 2, display: 'flex',
            justifyContent: 'space-between', gap: 10, alignItems: 'baseline' } },
          h('span', { className: 'lbl', style: { textTransform: 'none', letterSpacing: 0 } }, f[0]),
          h('span', null, f[1]));
      }),
      o.sub ? h('div', { className: 'sub' }, o.sub) : null);
  };
  /* Las dos bandas que abren y cierran el croquis. Mismo criterio: el valor viaja hecho. */
  var banda = function (clave, titulo, valor, opciones) {
    var o = opciones || {};
    return h('div', { className: 'stat' + (o.tone ? ' stat--' + o.tone : ''), 'data-banda': clave,
      style: { textAlign: 'center' } },
      h('div', { className: 'lbl' }, titulo),
      h('div', { className: 'val tnum', 'data-dato': titulo }, valor),
      /* La banda de abajo lleva DOS renglones en el croquis: el total y la rentabilidad. */
      o.extra ? h('div', { className: 'val tnum', 'data-dato': o.extra[0],
        style: { fontSize: 20, marginTop: 2 } },
        h('span', { className: 'lbl', style: { textTransform: 'none', letterSpacing: 0, marginRight: 6 } }, o.extra[0]),
        o.extra[1]) : null,
      o.sub ? h('div', { className: 'sub' }, o.sub) : null);
  };
  var FILA_CROQUIS = { display: 'grid', gap: 14, marginTop: 14,
    gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))' };

  function EstadoNegocio(props) {
    var ui = window.CR.ui;



    var permisos = window.CR.permisos;
    var puedeCargarKm = !!(permisos && permisos.puedeSeccion(props && props.session, 'cierre-manual'));
    var st = useState(null); var data = st[0], setData = st[1];
    /* La respuesta queda firmada con los filtros que la produjeron. Mantener `data` durante una
       recarga evita que la pantalla salte, pero NUNCA autoriza a exportarla con el nombre del filtro
       nuevo: si la recarga falla, sigue siendo el snapshot anterior. */
    var dk = useState(null); var dataClave = dk[0], setDataClave = dk[1];
    var ec = useState(null); var errorCarga = ec[0], setErrorCarga = ec[1];
    var ea = useState(null); var errorAccion = ea[0], setErrorAccion = ea[1];
    var okS = useState(null); var ok = okS[0], setOk = okS[1];
    var lo = useState(true); var cargando = lo[0], setCargando = lo[1];



    var hoy = new Date();
    var pe = useState({ anio: String(hoy.getFullYear()), mes: String(hoy.getMonth() + 1) });
    var periodo = pe[0], setPeriodo = pe[1];
    /* '' = ambas quincenas (el default que pidio), '1' o '2' para una sola. */
    var qs = useState(''); var quincenaSel = qs[0], setQuincenaSel = qs[1];
    var claveActual = [periodo.anio || '', periodo.mes || '', quincenaSel || ''].join('|');
    var nu = useState(0); var recargar = nu[0], setRecargar = nu[1];
    var fo = useState({ familia: 'meli', concepto: '', otro: '', quincena: '1', monto: '', porcentaje: '', detalle: '' });
    var form = fo[0], setForm = fo[1];
    var bu = useState(false); var busy = bu[0], setBusy = bu[1];

    var ck = useState(null); var cargaKm = ck[0], setCargaKm = ck[1];

    var cc = useState(null); var corrKm = cc[0], setCorrKm = cc[1];

    useEffect(function () {
      var vivo = true;
      var claveSolicitada = [periodo.anio || '', periodo.mes || '', quincenaSel || ''].join('|');


      setCargando(true); setDataClave(null); setErrorCarga(null);
      window.CR.backend.estadoNegocio(Object.assign({}, periodo, { quincena: quincenaSel })).then(function (j) {
        if (!vivo) return; setData(j); setDataClave(claveSolicitada); setCargando(false);
      }).catch(function (e) {
        if (!vivo) return; setErrorCarga(e.message || 'No pude traer el estado de negocio.'); setCargando(false);
      });
      return function () { vivo = false; };
    }, [periodo.anio, periodo.mes, quincenaSel, recargar]);

    var cambiarPeriodo = function (campo, v) {
      setPeriodo(function (p) { var n = Object.assign({}, p); n[campo] = v; return n; });
    };
    var cambiarForm = function (campo, v) {
      setForm(function (f) { var n = Object.assign({}, f); n[campo] = v; return n; });
    };
    var catalogoDe = function (fam) {
      var c = (data && data.catalogoConceptos) || {};
      /* Los impuestos vienen como objetos, con su alícuota sugerida; los otros, como texto. */
      return (c[fam] || []).map(function (x) { return typeof x === 'string' ? x : x.nombre; });
    };
    /* Cambiar de familia LIMPIA el concepto: el catálogo es otro, y dejar el anterior elegido
       cargaría un "Sueldos" como descuento de Mercado Libre. */
    var cambiarFamilia = function (v) {
      setForm(function (f) {
        return Object.assign({}, f, { familia: v, concepto: '', otro: '',
          porcentaje: v === 'impuesto' ? f.porcentaje : '' });
      });
    };


    var elegirConcepto = function (v) {
      var c = (data && data.catalogoConceptos) || {};
      var hit = (c.impuesto || []).filter(function (x) { return x && x.nombre === v; })[0];
      setForm(function (f) {
        return Object.assign({}, f, { concepto: v, porcentaje: hit ? String(hit.porcentaje) : f.porcentaje });
      });
    };

    /* La coma decimal se respeta: el operador escribe 4.299.193,76 como lo ve en su planilla.
       Borrar todos los puntos a ciegas ya costó un error de CIEN VECES sobre plata en el módulo de
       tarifas — acá el campo arranca vacío, pero la regla es la misma y no se repite el atajo. */
    var normalizarMonto = function (v) {
      var s = String(v == null ? '' : v).trim();
      if (s.indexOf(',') >= 0) return s.replace(/\./g, '').replace(',', '.');
      return s;
    };





    var guardarKm = function () {
      if (!cargaKm) return;
      var km = String(cargaKm.km == null ? '' : cargaKm.km).trim();
      var motivo = String(cargaKm.motivo == null ? '' : cargaKm.motivo).trim();
      if (!km || !motivo) { setErrorAccion('Escribí los km recorridos y de dónde sale el dato.'); return; }
      if (!window.confirm('¿Cargar ' + km + ' km a la ruta ' + cargaKm.cabecera + ' del ' + cargaKm.fecha + '?\n'
        + 'Estado de Negocio la va a facturar con el tramo de esos km. El costo al proveedor no cambia. '
        + 'Si quedara mal, gerencia lo corrige con motivo y queda auditado.')) return;
      /* El error de CARGA (el período visible) no se toca: es otro canal, igual que en `guardar`. La
         línea no repite la de `guardar` a propósito: es el ancla de una mutación de ese formulario, y
         una segunda copia idéntica la dejaría ambigua (ancla rota, no se mide nada). */
      setErrorAccion(null); setOk(null); setBusy(true);
      window.CR.backend.cargarKmRuta(cargaKm.id, km, motivo).then(function (r) {
        setOk((r && r.mensaje) || 'Km cargados.');
        setCargaKm(null); setBusy(false); setDataClave(null); setRecargar(function (n) { return n + 1; });
      }).catch(function (e) { setErrorAccion(e.message || 'No pude cargar los km de esa ruta.'); setBusy(false); });
    };







    var guardarCorreccion = function () {
      if (!corrKm) return;
      var km = String(corrKm.km == null ? '' : corrKm.km).trim();
      var motivo = String(corrKm.motivo == null ? '' : corrKm.motivo).trim();
      if (!window.confirm('¿Corregir la ruta ' + corrKm.cabecera + ' del ' + corrKm.fecha + ' de '
        + kmConComa(corrKm.kmAnterior) + ' a ' + km + ' km?\n'
        + 'Estado de Negocio la va a facturar con el km corregido. Queda auditado con el valor anterior.')) return;
      setOk(null); setErrorAccion(null); setBusy(true);
      window.CR.backend.corregirKmRuta(corrKm.id, km, motivo, corrKm.kmAnterior).then(function (r) {
        setOk((r && r.mensaje) || 'Km corregidos.');
        setCorrKm(null); setBusy(false); setDataClave(null); setRecargar(function (n) { return n + 1; });
      }).catch(function (e) {
        setErrorAccion(e.message || 'No pude corregir los km de esa ruta.'); setBusy(false);
        if (e && (e.clave === 'km_cambio' || e.clave === 'km_no_cargado_aca')) {
          setCorrKm(null); setDataClave(null); setRecargar(function (n) { return n + 1; });
        }
      });
    };

    var guardar = function () {
      if (!periodo.anio || !periodo.mes) {
        setErrorAccion('Elegí el año y el mes antes de cargar un concepto: un concepto sin período no se puede imputar a ningún cierre.');
        return;
      }
      setBusy(true); setErrorAccion(null); setOk(null);
      window.CR.backend.cargarConceptoNegocio({
        familia: form.familia,
        concepto: form.concepto === '__otro' ? form.otro : form.concepto,
        anio: Number(periodo.anio), mes: Number(periodo.mes),
        quincena: form.familia === 'meli' ? Number(form.quincena) : null,



        monto: form.familia === 'impuesto' ? 0 : Number(normalizarMonto(form.monto)),
        porcentaje: form.porcentaje === '' ? null : Number(normalizarMonto(form.porcentaje)),
        detalle: form.detalle || null
      }).then(function () {
        setOk('Concepto cargado.');
        setForm({ familia: form.familia, concepto: '', quincena: form.quincena, monto: '', porcentaje: '', detalle: '' });
        setBusy(false); setDataClave(null); setRecargar(function (n) { return n + 1; });
      }).catch(function (e) { setErrorAccion(e.message || 'No pude cargar el concepto.'); setBusy(false); });
    };

    if (errorCarga && !data) {
      return h(ui.Page, { title: 'Estado de Negocio' }, h(ui.EmptyState, { title: 'No pude traer el estado' }, errorCarga));
    }
    if (cargando && !data) {
      return h(ui.Page, { title: 'Estado de Negocio' }, h('p', { className: 'muted' }, 'Cargando…'));
    }
    if (!data) return null;

    var m = data.totalMes || {};
    var ctrl = data.controles || {};
    var motivos = ctrl.motivosSinFacturar || {};
    var motivoKeys = Object.keys(motivos);
    /* Una doble cobra una sola vez. La segunda pieza sigue en `data.rutas` para auditoria, pero no
       es otra ruta economica y exportarla duplicaria el conteo. */
    var rutasHoja = (data.rutas || []).filter(function (r) { return r.cobra; });




    var rutasSinKm = (data.rutas || []).filter(function (r) { return r.kmFaltante === true; });
    /* Revisión M5: las rutas con km cargado por gerencia (el servidor manda la marca sólo si dice el km
       que la ruta tiene), para corregirlo si quedó mal. */
    var rutasKmCargado = (data.rutas || []).filter(function (r) { return !!r.kmCargado && r.km != null; });








    var rotuloSalio = function (r) {
      if (String(r.servicio || '').toUpperCase() !== 'BACKUP') return '—';
      return r.salio === 'si' ? 'Sí' : r.salio === 'no' ? 'No' : 'sin marcar';
    };
    /* Los Back Up que no salieron y se facturan (cobran y encontraron tarifa). Es UNA fila por ruta, sin
       sumar nada: los importes son los de cada fila del servidor. */
    var rutasBackupNoSalio = (data.rutas || []).filter(function (r) {
      return r.backupNoSalio === true && r.cobra === true && !r.sinFacturar;
    });
    var hayFiltroRutas = !!(periodo.anio || periodo.mes || quincenaSel);
    var datosDelPeriodoVisible = dataClave === claveActual;
    var motivoHoja = errorCarga ? 'No se descarga: el período visible no pudo actualizarse.'
      : !datosDelPeriodoVisible ? (cargando ? 'Esperá a que termine de actualizar el período.'
        : 'No se descarga: los datos visibles pertenecen a otro período.')
      : !rutasHoja.length ? 'No hay rutas facturables para descargar con este filtro.'
      : '';
    /* UN PERÍODO SIN CONCEPTOS SE VE COMPLETO Y DA MÁS ALTO QUE EL REAL.
       La cascada dibuja todos sus niveles aunque no haya ni un impuesto ni un gasto indirecto
       cargado: los importes salen en cero, el resultado parece cerrado, y nada en la pantalla
       distingue «este mes no tuvo IIBB» de «todavía no lo cargaron».
       Medido el 30/08 contra los datos del cliente: JULIO muestra $32.812.703,18 de Total
       Facturación y, con las mismas alícuotas que agosto (IIBB 5% e IMP DEB - CRED 1,2%), lo
       correcto serían $30.778.315,58 — dos millones de diferencia en la línea contra la que se
       mide el % de cada gasto.
       NO se estima acá cuánto falta: esta pantalla no calcula plata. Se dice QUÉ falta y DÓNDE se
       carga, que es lo accionable. Es el mismo criterio que el aviso de Última Milla: un total sin
       la línea parece completo.
       Los indirectos se miran por las DOS listas: con una quincena elegida el servidor los saca de
       `detalleIndirectos` a propósito (van al mes entero) y los informa en `detalleIndirectosDelMes`.
       Mirando sólo la primera, cualquier quincena diría que faltan y el aviso se volvería ruido. */
    var sinImpuestos = !(m.detalleImpuestos || []).length;
    var sinIndirectos = !(m.detalleIndirectos || []).length && !(m.detalleIndirectosDelMes || []).length;
    /* Sin facturación no hay nada que se vea "más alto": el período está vacío y el aviso sería un
       cartel para un número que no existe. */
    var faltanConceptos = (m.facturacionOperativa == null ? m.facturacionPrimeraMilla || 0 : m.facturacionOperativa) > 0
      && (sinImpuestos || sinIndirectos);
    var faltantes = [sinImpuestos ? 'impuestos' : null, sinIndirectos ? 'gastos indirectos' : null]
      .filter(function (x) { return x; }).join(' ni ');














    var EN_DESARROLLO = 'en desarrollo';
    var NO_CARGADO = 'no cargado';
    var pm = data.primeraMilla || {};
    // Compatibilidad de lectura durante el recambio: nunca sumar importes en el navegador.
    var subtotalPm = m.subTotalPrimeraMilla == null ? m.subTotalMeli : m.subTotalPrimeraMilla;
    var margenOperativo = m.margenOperativo == null ? pm.margen : m.margenOperativo;
    var margenOperativoPct = m.margenOperativoSobreFacturacion === undefined
      ? pm.margenSobreFacturacion : m.margenOperativoSobreFacturacion;
    var umMotivo = (data.ultimaMilla && data.ultimaMilla.motivo)
      || 'El módulo de Última Milla todavía no existe en CRÁNEO.';
    var conceptosDescuentos = (m.detalleDescuentos || [])
      .map(function (x) { return x.concepto; }).join(' · ');
    var lineasImpuestos = sinImpuestos
      ? [['Impuestos del período', NO_CARGADO]]
      : (m.detalleImpuestos || []).map(function (x) {
        return [x.concepto + (x.porcentaje != null
          ? ' · ' + String(x.porcentaje).replace('.', ',') + '%' : ''), money(x.monto)];
      });
    /* El overhead del período y el del mes NO son el mismo número cuando hay una quincena elegida:
       el servidor saca los indirectos del cierre quincenal a propósito y manda el total del mes
       aparte. Se muestra el que corresponde y se dice cuál es. */
    var overheadDelMes = !!m.indirectosExcluidos;
    var overheadMonto = overheadDelMes ? m.indirectosDelMesTotal : m.indirectos;
    var lineasOverhead = sinIndirectos
      ? [['Gasto indirecto', NO_CARGADO], ['% vs Total Facturación', '—']]
      : [['$', money(overheadMonto)],
        ['% vs Total Facturación', pct(pctDe(overheadMonto, m.totalFacturacion))]];
    var conceptosOverhead = (overheadDelMes ? (m.detalleIndirectosDelMes || []) : (m.detalleIndirectos || []))
      .map(function (x) { return x.concepto; }).join(' · ');
    var subOverhead = sinIndirectos ? 'total de gasto indirecto — todavía no se cargó'
      : (overheadDelMes ? 'del MES completo: no se reparte por quincena' : 'total de gasto indirecto')
        + (conceptosOverhead ? ' · ' + conceptosOverhead : '');












    var mp = data.porMillaYPlaza || null;
    var um = mp && mp.ultimaMilla && mp.ultimaMilla.disponible ? mp.ultimaMilla : null;
    var ROTULO_RED = { ESTANDAR: 'Red Estándar', HEAVY_AND_BULKY: 'Red Heavy and Bulky' };
    /* Lo que Última Milla no pudo facturar a MELI son SERVICIOS (sin km, sin tarifa…), no rutas: se cuentan así. */
    var umSinFacturar = function (x) {
      var n = x && x.sinFacturar ? x.sinFacturar.servicios : 0;
      return n ? n + (n === 1 ? ' servicio' : ' servicios') + ' sin facturar' : '—';
    };
    var filaUm = function (x, clave, tipo, linea) {
      return { clave: clave, tipo: tipo, linea: linea, rutas: x.servicios + (x.pendientes ? ' (' + x.pendientes + ' pend.)' : ''),
        facturacion: money(x.facturacion), costo: money(x.costo), margen: money(x.margen), margenPct: pct(x.margenSobreFacturacion),
        sinFacturar: umSinFacturar(x) };
    };
    var ROTULO_PLAZA = { AMBA: 'AMBA', ROSARIO: 'Rosario', SIN_PLAZA: 'Sin plaza' };
    var conSigno = function (n) {
      if (n == null) return '—';
      var v = Number(n);
      return v < 0 ? '− ' + money(-v) : (v > 0 ? '+ ' + money(v) : money(0));
    };
    var sinFacturarTexto = function (sf) {
      if (!sf || !sf.rutas) return '—';
      return sf.rutas + (sf.rutas === 1 ? ' ruta · ' : ' rutas · ') + money(sf.costo) + ' de costo';
    };
    var sinFacturarDetalle = function (sf) {
      var pm2 = (sf && sf.porMotivo) || {};
      return Object.keys(pm2).map(function (k) {
        var d = MOTIVOS[k] || { texto: k };
        return pm2[k].rutas + ' ' + d.texto + ' (' + money(pm2[k].costo) + ')';
      }).join(' · ');
    };
    var filasMillaPlaza = !mp ? [] : (mp.plazas || []).map(function (p) {
      return { clave: 'primera-' + p.plaza, tipo: 'plaza',
        linea: 'Primera Milla · ' + (ROTULO_PLAZA[p.plaza] || p.plaza),
        rutas: p.rutas, facturacion: money(p.facturacion), costo: money(p.costo), margen: money(p.margen),
        margenPct: pct(p.margenSobreFacturacion), sinFacturar: sinFacturarTexto(p.sinFacturar),
        sinFacturarDetalle: sinFacturarDetalle(p.sinFacturar) };
    }).concat(!mp ? [] : [
      { clave: 'subtotal', tipo: 'subtotal', linea: 'Subtotal Primera Milla',
        rutas: (mp.subtotalPrimeraMilla || {}).rutas, facturacion: money((mp.subtotalPrimeraMilla || {}).facturacion),
        costo: money((mp.subtotalPrimeraMilla || {}).costo), margen: money((mp.subtotalPrimeraMilla || {}).margen),
        margenPct: pct((mp.subtotalPrimeraMilla || {}).margenSobreFacturacion),
        sinFacturar: sinFacturarTexto((mp.subtotalPrimeraMilla || {}).sinFacturar),
        sinFacturarDetalle: sinFacturarDetalle((mp.subtotalPrimeraMilla || {}).sinFacturar) },
      /* Los ajustes de MELI no tienen plaza: se cargan por quincena o por mes. Van en SU línea, con el
         neto firmado en facturación y en margen (bajan los dos), y sin costo. */
      { clave: 'ajustes', tipo: 'ajustes', linea: 'Ajustes de Mercado Libre (sin plaza)',
        rutas: '—', facturacion: conSigno((mp.ajustesSinPlaza || {}).neto), costo: '—',
        margen: conSigno((mp.ajustesSinPlaza || {}).neto), margenPct: '—', sinFacturar: '—',
        detalle: 'Descuentos ' + conSigno(-Number((mp.ajustesSinPlaza || {}).descuentos || 0))
          + ' · a favor ' + conSigno((mp.ajustesSinPlaza || {}).aFavor)
          + ' · de otros períodos ' + conSigno((mp.ajustesSinPlaza || {}).deOtrosPeriodos) },
      { clave: 'total', tipo: 'total', linea: 'Total Primera Milla',
        rutas: '', facturacion: money((mp.totalPrimeraMilla || {}).facturacion),
        costo: money((mp.totalPrimeraMilla || {}).costo), margen: money((mp.totalPrimeraMilla || {}).margen),
        margenPct: pct((mp.totalPrimeraMilla || {}).margenSobreFacturacion), sinFacturar: '' },
    ]).concat(!mp ? [] : um
      ? (um.redes || []).map(function (r) { return filaUm(r, 'ultima-' + r.red, 'plaza', 'Última Milla · ' + (ROTULO_RED[r.red] || r.nombre || r.red)); })
        .concat([Object.assign(filaUm(um, 'ultima-subtotal', 'subtotal', 'Subtotal Última Milla'), { detalle: um.nota })])
      : [{ clave: 'ultima', tipo: 'ausente', linea: 'Última Milla',
        rutas: EN_DESARROLLO, facturacion: EN_DESARROLLO, costo: EN_DESARROLLO, margen: EN_DESARROLLO,
        margenPct: '—', sinFacturar: '—', detalle: (mp.ultimaMilla && mp.ultimaMilla.motivo) || umMotivo }]);
    var celdaMP = function (campo) {
      return function (r) {
        var v = r[campo];
        if (r.tipo === 'subtotal' || r.tipo === 'total') return h('b', null, v);
        if (r.tipo === 'ausente') return h('span', { className: 'muted', title: r.detalle }, v);
        if (campo === 'sinFacturar' && r.sinFacturarDetalle) return h('span', { title: r.sinFacturarDetalle }, v);
        if (r.tipo === 'ajustes' && (campo === 'facturacion' || campo === 'margen')) return h('span', { title: r.detalle }, v);
        return v;
      };
    };

    var refMes = String(hoy.getFullYear()) + '-' + String(hoy.getMonth() + 1).padStart(2, '0');
    var periodoSel = (periodo.anio && periodo.mes)
      ? String(periodo.anio) + '-' + String(periodo.mes).padStart(2, '0') : null;
    var mesAbierto = periodoSel != null && periodoSel >= refMes;

    var descargarHojaRutas = function () {
      if (motivoHoja) return;
      var partes = [];
      if (periodo.anio) partes.push(String(periodo.anio));
      if (periodo.mes) partes.push(String(periodo.mes).padStart(2, '0'));
      if (quincenaSel) partes.push('q' + quincenaSel);
      var marca = partes.length ? partes.join('-') : new Date().toISOString().slice(0, 10);
      var nombre = 'craneo-hoja-rutas-' + marca + (hayFiltroRutas ? '-filtrada' : '') + '.csv';
      window.CR.csv.descargar(nombre, CSV_HOJA_RUTAS, rutasHoja);
    };

    return h('div', null,
      h(ui.Page, {
        title: 'Estado de Negocio',
        subtitle: 'Facturación de Mercado Libre contra el costo de cada ruta. Margen y rentabilidad por ruta, por quincena y del mes.'
      }),

      h('div', { className: 'alerta alerta--baja', role: 'note' },
        h('b', null, 'Sólo gerencia.'), ' Es la información más sensible del sistema: el equipo operativo no la ve.'),
      errorCarga ? h('div', { className: 'alerta alerta--alta', role: 'alert' }, errorCarga) : null,
      errorAccion ? h('div', { className: 'alerta alerta--alta', role: 'alert' }, errorAccion) : null,
      ok ? h('div', { className: 'alerta alerta--baja', role: 'status' }, ok) : null,

      h(ui.Toolbar, null,
        h(ui.Field, { label: 'Año' },
          h(ui.Input, { type: 'number', value: periodo.anio, placeholder: 'todos', 'aria-label': 'Año',
            onChange: function (v) { cambiarPeriodo('anio', v); } })),
        h(ui.Field, { label: 'Mes' },
          h(ui.Select, { value: periodo.mes, 'aria-label': 'Mes',
            options: [{ value: '', label: 'Todos' }].concat(MESES.map(function (n, i) {
              return { value: String(i + 1), label: n };
            })),
            onChange: function (v) { cambiarPeriodo('mes', v); } })),
        h(ui.Button, { variant: 'ghost', onClick: function () {
          setPeriodo({ anio: '', mes: '' }); setQuincenaSel('');
        } }, 'Todo'),
        /* La quincena vive DENTRO de un mes. Sin periodo no se ofrece: "Q1" sobre varios meses junta
           la primera quincena de julio con la de agosto y devuelve un cierre que no es de ninguno.
           El servidor tambien lo rechaza — esto evita que el operador llegue al 400. */
        periodo.anio && periodo.mes ? h('div', { className: 'prov-chips', role: 'group', 'aria-label': 'Filtrar por quincena' },
          [{ v: '', l: 'Ambas quincenas' }, { v: '1', l: '1ra quincena' }, { v: '2', l: '2da quincena' }]
            .map(function (x) {
              var activo = quincenaSel === x.v;
              return h('button', { key: x.v || 'ambas', type: 'button',
                className: 'rep-chip' + (activo ? ' rep-chip--on' : ''), 'aria-pressed': activo,
                onClick: function () { setQuincenaSel(x.v); } }, x.l);
            })) : null,
        h(ui.Button, {
          variant: 'ghost', onClick: descargarHojaRutas,
          disabled: !!motivoHoja,
          'aria-describedby': 'estado-descarga-hoja',
          title: motivoHoja || 'Baja el costo, la facturación y el margen de cada ruta del período visible.'
        }, cargando ? 'Actualizando hoja…' : 'Descargar hoja de rutas (' + rutasHoja.length + ')'),
        h('span', { id: 'estado-descarga-hoja', className: 'muted', role: 'status' },
          motivoHoja || 'La hoja corresponde al período visible.')),






      h(ui.Card, { title: 'Total del período' },





        banda('total-facturacion', 'TOTAL FACTURACIÓN', money(m.totalFacturacion), { tone: 'ok',
          sub: (m.ultimaMillaIncluida ? 'Sub Total MELI ' : 'Sub Total Primera Milla ')
            + money(m.subTotalMeli) + ' menos ' + money(m.impuestos) + ' de impuestos' }),

        h('div', { style: FILA_CROQUIS },
          bloque('PRIMERA MILLA', [
            ['Sub Total Primera Milla', money(subtotalPm)],
            ['Costo Primera Milla', money(m.costoPrimeraMilla)]
          ], { tone: 'ok',
            sub: money(m.facturacionPrimeraMilla) + ' menos ' + money(m.descuentosMeli)
              + ' de descuentos · el costo es costo + adicional − penalidad' }),
          /* LM conserva su desglose y su aviso de provisionalidad aunque integre el total. */


          um ? bloque('ÚLTIMA MILLA', [
            ['Sub Total Última Milla', money(um.facturacion)],
            ['Costo Última Milla', money(um.costo)]
          ], { sub: um.nota }) : bloque('ÚLTIMA MILLA', [
            ['Sub Total Última Milla', EN_DESARROLLO],
            ['Costo Última Milla', EN_DESARROLLO]
          ], { sub: umMotivo }),
          /* MARGEN OPERATIVO: PM + LM (facturación − costo ajustado), antes de conceptos MELI
             e impuestos. Ambos números salen de totalMes, con fallback para servidor antiguo.
             La banda inferior es el resultado con impuestos e indirectos, no el mismo margen. */
          /* El rótulo lleva la FÓRMULA pegada, como el del cierre por quincena. En esta pantalla
             "Margen %" ya significó dos cosas distintas (facturación ÷ costo y margen ÷
             facturación) y un «Margen %» pelado es el bug original. */
          bloque('MARGEN OPERATIVO', [
            ['Margen % (margen ÷ facturación)', pct(margenOperativoPct)],
            ['Margen $', money(margenOperativo)]
          ], { tone: 'ok', sub: m.ultimaMillaIncluida
            ? 'Primera y Última Milla: facturación − costo con ajustes del proveedor, antes de ajustes MELI e impuestos'
            : 'de las rutas: facturación − costo, antes de descuentos e impuestos' })),

        h('div', { style: FILA_CROQUIS },
          bloque('DESCUENTOS', [
            ['$', money(m.descuentosMeli)],
            ['% vs facturación', pct(pctDe(m.descuentosMeli,
              m.facturacionOperativa == null ? m.facturacionPrimeraMilla : m.facturacionOperativa))]
          ], { tone: 'danger',
            sub: conceptosDescuentos || 'penalidades MELI + paquetes descontados' }),
          /* IMPUESTOS: una línea por alícuota, como el croquis (IIBB 5% · DEB/CRED 1,2%). Si el
             período no tiene ninguno cargado dice «no cargado» y NO $0,00: cero impuestos es una
             afirmación falsa sobre el período, y el aviso de abajo dice dónde se cargan. */
          bloque('IMPUESTOS', lineasImpuestos, { tone: 'danger',
            sub: sinImpuestos ? 'todavía no se cargaron para este período'
              : m.ultimaMillaIncluida ? 'sobre el Sub Total MELI de ambas millas, en el mes correspondiente'
                : 'se calculan sobre el Sub Total Primera Milla' }),
          /* OVERHEAD = el total de gasto indirecto (sueldos, oficina, contador). Con una quincena
             elegida el servidor los deja afuera del cierre y los informa aparte: se muestran los
             del MES y se dice, en vez de un cero que se leería como "este período no tuvo". */
          bloque('OVERHEAD', lineasOverhead, { tone: 'danger', sub: subOverhead })),

        banda('total-periodo', 'TOTAL PERÍODO', money(m.resultado), {
          tone: (m.resultado || 0) < 0 ? 'danger' : 'ok',
          extra: ['RENTABILIDAD %', pct(m.rentabilidad)],
          sub: 'facturación − costo' }),

        /* Con una quincena elegida, el cierre NO lleva los gastos indirectos: son montos del mes
           (sueldos, oficina, contador) y partirlos por quincena es una decision que nadie tomo.
           Se dice, porque un "Total del periodo" sin ellos y sin aviso se lee como el cierre. */
        m.indirectosExcluidos
          ? h('div', { className: 'alerta alerta--media', role: 'status' },
              h('b', null, 'Este total no incluye los gastos indirectos.'),
              ' Son ' + money(m.indirectosDelMesTotal) + ' del mes completo (' +
              (m.detalleIndirectosDelMes || []).map(function (x) { return x.concepto; }).join(', ') +
              '). No se reparten por quincena: mirá "Ambas quincenas" para el cierre con todo.')
          : null,







        h('details', { className: 'card', 'data-detalle-conceptos': '1', style: { marginTop: 14 } },
          h('summary', { style: { cursor: 'pointer', fontWeight: 600 } },
            'Ver el detalle — cascada concepto por concepto'),
          h(ui.DataTable, {
            rows: [
              { linea: 'Facturación de las rutas (primera milla)', monto: money(m.facturacionPrimeraMilla), pc: '' },
              { linea: 'menos Descuentos de Mercado Libre', monto: money(m.descuentosMeli), pc: '' },
              { linea: 'más Ajustes a favor de Mercado Libre', monto: money(m.ajustesAFavorMeli), pc: '' },
              { linea: 'Ajustes de otros períodos (neto)', monto: money(m.ajustesDeOtrosPeriodos), pc: '' },
              { linea: '= Sub Total Primera Milla', monto: money(subtotalPm), pc: '' }
            ].concat(!m.ultimaMillaIncluida ? [] : [
              { linea: 'más Facturación Última Milla', monto: money(m.facturacionUltimaMilla), pc: '' },
              { linea: '= Sub Total MELI (ambas millas)', monto: money(m.subTotalMeli), pc: '' }
            ]).concat((m.detalleImpuestos || []).map(function (x) {
              return { linea: 'menos ' + x.concepto + (x.porcentaje != null
                  ? ' (' + String(x.porcentaje).replace('.', ',') + '% de ' + money(x.base) + ')' : ''),
                monto: money(x.monto), pc: '' };
            })).concat([
              { linea: '= Total Facturación', monto: money(m.totalFacturacion), pc: '' },
              { linea: 'menos Costo de las rutas (primera milla)', monto: money(m.costoPrimeraMilla), pc: '' }
            ]).concat(!m.ultimaMillaIncluida ? [] : [
              { linea: 'menos Costo Última Milla (con ajustes)', monto: money(m.costoUltimaMilla), pc: '' }
            ]).concat((m.detalleIndirectos || []).map(function (x) {
              return { linea: 'menos ' + x.concepto, monto: money(x.monto), pc: pct(x.pctSobreFacturacion) };
            /* «Sub Total Costos» es el nivel que agrupa el costo de las rutas con los gastos
               indirectos, igual que «Sub Total Primera Milla» agrupa del lado de la facturación.
               SE DIBUJA SÓLO SI EL SERVIDOR LO MANDA. Hoy `totalMes` trae `costoPrimeraMilla` e
               `indirectos` por separado y NO su suma bajo ningún nombre (verificado el 30/08 sobre
               `local/repo-pg.cjs`), así que la línea no aparece.
               No se suma acá a propósito: esta pantalla no calcula plata. Si la sumara y mañana el
               servidor cambiara cómo arma `resultado` —por ejemplo prorrateando indirectos—, la
               cascada mostraría un subtotal que no reconcilia con su propio total, y el gerente le
               creería a la pantalla. El día que `subTotalCostos` viaje, esta línea aparece sola. */
            })).concat(m.subTotalCostos == null ? [] : [
              { linea: '= Sub Total Costos', monto: money(m.subTotalCostos),
                pc: pct(m.subTotalCostosPctSobreFacturacion) }
            ]).concat([
              { linea: '= TOTAL DEL PERÍODO', monto: money(m.resultado), pc: pct(m.rentabilidad) }
            ]),
            columns: [
              { key: 'linea', label: 'Concepto' },
              { key: 'monto', label: 'Monto' },
              { key: 'pc', label: '% sobre Total Facturación' }
            ]
          })),
        /* El `id` no es decorativo: es lo que deja anclar una asercion a ESTE aviso y no a
           cualquier `.alerta` de la pantalla — hay tres, y dos dicen cosas parecidas. */
        faltanConceptos
          ? h('div', { id: 'estado-faltan-conceptos', className: 'alerta alerta--media', role: 'status' },
              h('b', null, 'Este período no tiene ' + faltantes + ' cargados.'),
              ' La cascada los muestra en cero, así que el resultado que ves es MÁS ALTO que el real.',
              sinImpuestos
                ? ' Los impuestos se cargan acá abajo, en «Cargar un concepto»: va la alícuota (IIBB 5%, IMP DEB - CRED 1,2%) y el importe lo saca el sistema.'
                : '',
              sinIndirectos
                ? (mesAbierto
                  ? ' Los gastos indirectos (sueldos, oficina, contador) van a MES CERRADO: los de un mes en curso recién se pueden cargar a fin de mes, así que hasta entonces este resultado es PROVISORIO.'
                  : ' Los gastos indirectos se cargan acá abajo, en «Cargar un concepto».')
                : '')
          : null,

        m.ultimaMillaIncluida && data.ultimaMilla && data.ultimaMilla.provisoria
          ? h('div', { id: 'estado-ultima-milla-provisoria', className: 'alerta alerta--media', role: 'status' },
              h('b', null, 'Última Milla incluida · liquidación provisoria. '),
              'El cierre independiente todavía no está definido. ',
              'Pendientes de datos: ' + (data.ultimaMilla.pendientes || 0) + '. ',
              'Sin importe MELI completo: ' + ((data.ultimaMilla.sinFacturar || {}).servicios || 0) + '. ',
              'Sin tarifa de proveedor: ' + (data.ultimaMilla.sinTarifaProveedor || 0) + '. ',
              'Revisalos en Last Mile → Liquidación; no son importes definitivos en cero.')
          : null,
        /* Conserva el aviso con un backend anterior durante el recambio. */
        m.ultimaMillaIncluida === false
          ? h('div', { className: 'alerta alerta--media', role: 'status' },
              h('b', null, 'Este total no incluye Última Milla.'),
              um ? ' Su liquidación se ve en «Por milla y por plaza», pero todavía no entra en este total (impuestos y resultado): falta que BRAINS defina cómo se suma.'
                : ' Ese módulo todavía no existe en CRÁNEO, así que el resultado es sólo de primera milla.')
          : null),






      mp ? h('div', { className: 'card', 'data-novedad': 'negocio-milla-plaza', 'data-milla-plaza': '' },
          h('div', { className: 'card-h' }, h('h3', null, 'Por milla y por plaza')),
          h('p', { className: 'muted', style: { marginTop: 0 } },
            'Facturación de Mercado Libre, costo operativo y margen operativo de cada plaza. ',
            'La plaza es la misma de la liquidación al proveedor (Rosario o AMBA). ',
            'El margen % es margen ÷ facturación, y queda vacío si la plaza no facturó.'),
          h(ui.DataTable, {
            rows: filasMillaPlaza, rowKey: 'clave',
            columns: [
              { key: 'linea', label: 'Milla y plaza', render: celdaMP('linea') },
              { key: 'rutas', label: 'Rutas', render: celdaMP('rutas') },
              { key: 'facturacion', label: 'Facturación', render: celdaMP('facturacion') },
              { key: 'costo', label: 'Costo operativo', render: celdaMP('costo') },
              { key: 'margen', label: 'Margen $', render: celdaMP('margen') },
              { key: 'margenPct', label: 'Margen % (margen ÷ facturación)', render: celdaMP('margenPct') },
              { key: 'sinFacturar', label: 'Sin facturar', render: celdaMP('sinFacturar') }
            ]
          }),
          h('p', { className: 'muted', style: { fontSize: 12 } },
            'Los ajustes de Mercado Libre (descuentos, ajustes a favor y los de otros períodos) no son de una plaza: ',
            'van en su propia línea, sin repartir. Con ellos, el total es el Sub Total Primera Milla de arriba. ',
            'Lo «sin facturar» cuesta y no factura: el margen de esa plaza es menor que el real.'),

          um ? h('p', { className: 'muted', style: { fontSize: 12 }, 'data-ultima-milla-nota': '' },
            'Última Milla va por red: «Rutas» son sus servicios (los pendientes, entre paréntesis), la facturación es lo que se le cobra a Mercado Libre ',
            'y el costo, lo que se le paga a cada proveedor con sus ajustes. Es la misma cuenta que Last Mile → Liquidación. ', um.nota) : null) : null,



      h(ui.Card, { title: 'Cierre por quincena' },
        /* RUTAS Y VUELTAS NO SON LO MISMO, y la misma palabra en dos pantallas daba dos números:
           Historial dice «Rutas 349» y acá decía «Rutas 302» para la misma quincena. Las dos están
           bien y cuentan cosas distintas — una ruta DOBLE es una sola ruta cobrable y dos vueltas,
           porque cobra una vez. Sin la columna, el gerente ve dos verdades incompatibles y no tiene
           con qué reconciliarlas.
           `piezas` lo manda el servidor (`resumen()` en repo-pg.cjs cuenta las filas; `rutas` cuenta
           las que cobran). No se deriva acá. */
        h('p', { className: 'muted', style: { marginTop: 0 } },
          h('b', null, 'Sólo Primera Milla. '),
          h('b', null, 'Rutas'), ' son las que se facturan; ', h('b', null, 'Vueltas'),
          ' son los tramos que se hicieron. Una ruta doble es 1 ruta y 2 vueltas: cobra una sola vez. ',
          'Historial cuenta vueltas, así que su número es el más alto de los dos.'),
        h(ui.DataTable, {
          rows: (data.quincenas || []).map(function (q) {
            return {
              periodo: q.periodo, rutas: q.rutas, vueltas: q.piezas,
              facturacion: money(q.facturacion),
              descuentos: q.descuentosMeli ? '− ' + money(q.descuentosMeli) : '—',
              neta: money(q.facturacionNeta),
              costo: money(q.costo),
              margen: money(q.margenNeto),
              margenPct: pct(q.margenSobreFacturacionNeta),
              sinFacturar: q.sinFacturar || 0
            };
          /* EL TOTAL DE LA TABLA. Hasta hoy había que sumar las quincenas a ojo, que es como se
             cuela un error de un renglón en el número que después se le pasa al contador.
             NINGUNA celda se calcula acá. Cada una es un campo que el servidor ya resolvió sobre
             las MISMAS filas que arma la tabla: `primeraMilla` es `resumen(filas)` (las rutas y
             piezas del período visible) y `subTotalPrimeraMilla` conserva sus conceptos MELI.
             `subTotalMeli` ya incluye LM: no pertenece a esta tabla de rutas PM.
             MARGEN $ Y MARGEN % QUEDAN EN «—» A PROPÓSITO: la columna muestra `margenNeto`
             (neta − costo) y su equivalente mensual no viaja. Poner ahí el margen bruto sería la
             misma columna con dos fórmulas distintas según la fila — que es exactamente el bug que
             esta pantalla arrastra desde que "Margen %" significaba dos cosas. El margen del mes
             está arriba, en «Total del período», y lleva impuestos e indirectos. */
          }).concat(!data.primeraMilla || !(data.quincenas || []).length ? [] : [{
            esTotal: true,
            periodo: 'TOTAL',
            rutas: data.primeraMilla.rutas, vueltas: data.primeraMilla.piezas,
            facturacion: money(data.primeraMilla.facturacion),
            descuentos: m.descuentosMeli ? '− ' + money(m.descuentosMeli) : '—',
            neta: money(subtotalPm),
            costo: money(data.primeraMilla.costo),
            margen: '—', margenPct: '—',
            sinFacturar: data.primeraMilla.sinFacturar || 0
          }]),
          columns: [
            { key: 'periodo', label: 'Período', render: function (r) {
              return r.esTotal ? h('b', null, r.periodo) : r.periodo; } },
            { key: 'rutas', label: 'Rutas', render: function (r) {
              return r.esTotal ? h('b', null, r.rutas) : r.rutas; } },
            /* La columna que desambigua: mismo dato que Historial llama "Rutas". */
            { key: 'vueltas', label: 'Vueltas', render: function (r) {
              return r.esTotal ? h('b', null, r.vueltas) : r.vueltas; } },
            { key: 'facturacion', label: 'Facturación' },
            { key: 'descuentos', label: 'Descuentos MELI' },
            { key: 'neta', label: 'Facturación neta' },
            { key: 'costo', label: 'Costo' },
            /* El `render` SÓLO estiliza: el valor sale de la fila, que es la única fuente. Cuando
               tenía el «—» escrito acá TAMBIÉN, cambiar el de la fila no movía nada en pantalla y
               la mutación que ponía el margen bruto sobrevivía en verde — dos copias del mismo
               dato y una de ellas invisible. */
            { key: 'margen', label: 'Margen $', render: function (r) {
              return r.esTotal
                ? h('span', { className: 'muted',
                  title: 'El margen del período está arriba, en «Total del período»: lleva impuestos '
                    + 'y gastos indirectos, que no se reparten por quincena.' }, r.margen)
                : r.margen; } },
            { key: 'margenPct', label: 'Margen % (margen ÷ facturación)', render: function (r) {
              return r.esTotal ? h('span', { className: 'muted' }, r.margenPct) : r.margenPct; } },
            { key: 'sinFacturar', label: 'Sin facturar' }
          ]
        })),

      /* LO QUE NO SE PUDO FACTURAR. Un 0 entre miles de pesos no se distingue de "esta ruta no
         cobra": el motivo es lo que convierte un número en algo que alguien puede ir a arreglar. */
      motivoKeys.length
        ? h(ui.Card, { title: 'Rutas que todavía no se pueden facturar' },
            h('p', { className: 'muted' },
              'Estas rutas tienen costo pero facturan 0, así que el margen que ves es MENOR que el real.'),
            h('ul', null, motivoKeys.map(function (k) {
              var d = MOTIVOS[k] || { texto: k, como: '' };
              return h('li', { key: k },
                h('b', null, motivos[k] + ' ruta(s) '), d.texto, ' — ', d.como);
            })),




            rutasSinKm.length ? h('div', { 'data-rutas-sin-km': rutasSinKm.length },
              h('p', { className: 'muted', style: { marginBottom: 6 } },
                puedeCargarKm
                  ? 'Las rutas sin kilómetros cerraron sin el dato. Si lo tenés, cargalo y la ruta se factura con el tramo que le corresponde (el costo al proveedor no cambia):'
                  : 'Las rutas sin kilómetros cerraron sin el dato:'),
              h(ui.DataTable, {
                rows: rutasSinKm, rowKey: 'id',
                columns: [
                  { key: 'fecha', label: 'Fecha' },
                  { key: 'cabecera', label: 'Ruta' },
                  { key: 'doble', label: 'Doble', render: function (r) { return r.doble ? 'Sí' : '—'; } },
                  { key: 'ciclo', label: 'Ciclo', render: function (r) { return r.ciclo || '—'; } },
                  { key: 'servicio', label: 'Servicio' },
                  { key: 'salio', label: 'Salió', render: rotuloSalio },
                  { key: 'patente', label: 'Patente' },
                  { key: 'costoTotal', label: 'Costo', render: function (r) { return money(r.costoTotal); } },
                  { key: 'accion', label: puedeCargarKm ? 'Km recorridos' : '', render: function (r) {
                    if (!puedeCargarKm) return '—';
                    if (cargaKm && cargaKm.id === r.id) {
                      var problemaKm = String(cargaKm.km).trim() ? validarKm(cargaKm.km) : null;
                      return h('div', { 'data-form-km': r.id, style: { display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' } },
                        h(ui.Input, { value: cargaKm.km, placeholder: 'km, ej. 120,5', disabled: busy,
                          'aria-label': 'Km recorridos de la ruta ' + r.cabecera,
                          onChange: function (v) { setCargaKm(Object.assign({}, cargaKm, { km: v })); } }),
                        h(ui.Input, { value: cargaKm.motivo, placeholder: 'de dónde sale el dato', disabled: busy,
                          'aria-label': 'Motivo de los km de la ruta ' + r.cabecera,
                          onChange: function (v) { setCargaKm(Object.assign({}, cargaKm, { motivo: v })); } }),
                        h(ui.Button, { size: 'sm', onClick: guardarKm,
                          disabled: busy || !!validarKm(cargaKm.km) || String(cargaKm.motivo).trim().length < 5,
                          title: 'El motivo va de 5 a 300 caracteres: queda en la auditoría.' },
                          busy ? 'Guardando…' : 'Guardar km'),
                        h(ui.Button, { size: 'sm', variant: 'ghost', disabled: busy,
                          onClick: function () { setCargaKm(null); } }, 'Cancelar'),
                        problemaKm ? h('span', { role: 'status', 'data-km-invalido': r.id, style: ESTILO_KM_INVALIDO }, problemaKm) : null);
                    }
                    return h('button', { type: 'button', className: 'btn btn--sm', disabled: busy,
                      'data-novedad': 'cargar-km-ruta',
                      'aria-label': 'Cargar los km recorridos de la ruta ' + r.cabecera,
                      title: 'Cargá los km recorridos: la ruta se factura con su tramo. No pisa un km existente.',
                      onClick: function () {
                        setErrorAccion(null); setOk(null); setCorrKm(null);   // un formulario de km abierto por vez
                        setCargaKm({ id: r.id, cabecera: r.cabecera, fecha: r.fecha, km: '', motivo: '' });
                      } }, 'Cargar km');
                  } }
                ]
              })) : null)
        : null,









      rutasBackupNoSalio.length
        ? h(ui.Card, { title: 'Back Up que no salieron', 'data-novedad': 'backup-no-salio-factura' },   // el «Mostrame dónde» de la novedad de v1.0.15
            h('div', { 'data-backup-no-salio': rutasBackupNoSalio.length },
              h('p', { className: 'muted', style: { marginTop: 0 } },
                'Mercado Libre paga el Back Up que no salió al 50 %, sin km: en AMBA, de la tarifa de 0 a 50 km de su tipo de unidad; ',
                'en Rosario, de la media jornada. No hace falta cargarles nada: se facturan solos.'),
              h(ui.DataTable, {
                rows: rutasBackupNoSalio, rowKey: 'id',
                columns: [
                  { key: 'fecha', label: 'Fecha' },
                  { key: 'cabecera', label: 'Ruta' },
                  { key: 'tipoUnidad', label: 'Tipo de unidad' },
                  { key: 'tarifaSimple', label: 'Tarifa entera', render: function (r) { return money(r.tarifaSimple); } },
                  { key: 'facturacion', label: 'Se factura', render: function (r) {
                    return h('span', { 'data-backup-no-salio-fila': r.id }, money(r.facturacion));
                  } },
                  { key: 'comoSeFactura', label: 'Cómo se factura', render: function (r) { return textoBackupNoSalio(r); } }
                ]
              })))
        : null,





      (puedeCargarKm && rutasKmCargado.length)
        ? h(ui.Card, { title: 'Km cargados a mano' },
            h('div', { 'data-km-cargados': rutasKmCargado.length },
              h('p', { className: 'muted', style: { marginTop: 0 } },
                'Los km que gerencia les cargó a rutas que cerraron sin el dato. Si alguno quedó mal, se corrige acá: ',
                'con motivo, y queda auditado con el valor anterior. Un km que vino con la ruta no se corrige por acá.'),
              h(ui.DataTable, {
                rows: rutasKmCargado, rowKey: 'id',
                columns: [
                  { key: 'fecha', label: 'Fecha' },
                  { key: 'cabecera', label: 'Ruta', render: function (r) { return r.cabecera + (r.doble && r.ciclo ? ' · ' + r.ciclo : ''); } },
                  { key: 'km', label: 'Km', render: function (r) { return kmConComa(r.km); } },
                  { key: 'motivoKm', label: 'De dónde salió', render: function (r) { return (r.kmCargado && r.kmCargado.motivo) || '—'; } },
                  { key: 'corregir', label: 'Corregir', render: function (r) {
                    if (corrKm && corrKm.id === r.id) {
                      var problemaCorr = String(corrKm.km).trim() ? validarKm(corrKm.km) : null;
                      return h('div', { 'data-form-corregir-km': r.id, style: { display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' } },
                        h(ui.Input, { value: corrKm.km, placeholder: 'km correctos', disabled: busy,
                          'aria-label': 'Km correctos de la ruta ' + r.cabecera,
                          onChange: function (v) { setCorrKm(Object.assign({}, corrKm, { km: v })); } }),
                        h(ui.Input, { value: corrKm.motivo, placeholder: 'por qué se corrige', disabled: busy,
                          'aria-label': 'Motivo de la corrección de la ruta ' + r.cabecera,
                          onChange: function (v) { setCorrKm(Object.assign({}, corrKm, { motivo: v })); } }),
                        h(ui.Button, { size: 'sm', onClick: guardarCorreccion,
                          disabled: busy || !!validarKm(corrKm.km) || String(corrKm.motivo).trim().length < 5,
                          title: 'El motivo va de 5 a 300 caracteres: queda en la auditoría.' },
                          busy ? 'Guardando…' : 'Guardar corrección'),
                        h(ui.Button, { size: 'sm', variant: 'ghost', disabled: busy,
                          onClick: function () { setCorrKm(null); } }, 'Cancelar'),
                        problemaCorr ? h('span', { role: 'status', 'data-km-invalido': r.id, style: ESTILO_KM_INVALIDO }, problemaCorr) : null);
                    }
                    return h('button', { type: 'button', className: 'btn btn--sm', disabled: busy,
                      'aria-label': 'Corregir los km de la ruta ' + r.cabecera,
                      title: 'Corregí un km que cargó gerencia: con motivo, queda auditado.',
                      onClick: function () {
                        setErrorAccion(null); setOk(null); setCargaKm(null);
                        setCorrKm({ id: r.id, cabecera: r.cabecera, fecha: r.fecha, kmAnterior: r.km, km: '', motivo: '' });
                      } }, 'Corregir km');
                  } }
                ]
              })))
        : null,




      (data.primeraMilla && data.primeraMilla.desestimadas)
        ? h('p', { className: 'muted', 'data-desestimadas-negocio': data.primeraMilla.desestimadas },
            h('b', null, data.primeraMilla.desestimadas + (data.primeraMilla.desestimadas === 1
              ? ' ruta desestimada no cuenta' : ' rutas desestimadas no cuentan')),
            ': no salieron (Mercado Libre las cubrió con otra empresa), así que no facturan ni suman rutas.',
            ctrl.doblesConMitadDesestimada
              ? ' ' + ctrl.doblesConMitadDesestimada + ' era(n) la mitad de una ruta doble: la otra mitad factura sola, por sus propios km.'
              : '')
        : null,

      ctrl.doblesConPiezasDistintasDeDos
        ? h('div', { className: 'alerta alerta--alta', role: 'alert' },
            h('b', null, ctrl.doblesConPiezasDistintasDeDos + ' pieza(s) de ruta doble no vienen de a dos'),
            ' — la facturación de esas rutas puede estar tomando un rango de kilómetros equivocado. Avisale al equipo.')
        : null,



      h(ui.Card, { title: 'Cargar un concepto' },
        h('p', { className: 'muted' },
          'Los descuentos de Mercado Libre (penalidades, paquetes perdidos) bajan la facturación de ',
          'una quincena. Los gastos indirectos (IIBB, sueldos, oficina) bajan el resultado del mes.'),
        h(ui.Toolbar, null,
          h(ui.Field, { label: 'Tipo' },
            h(ui.Select, { value: form.familia, 'aria-label': 'Tipo de concepto',
              options: [
                { value: 'meli', label: 'Descuento de Mercado Libre' },
                { value: 'impuesto', label: 'Impuesto (% sobre la facturación)' },
                { value: 'indirecto', label: 'Gasto indirecto' }
              ],
              onChange: function (v) { cambiarFamilia(v); } })),
          form.familia === 'meli'
            ? h(ui.Field, { label: 'Quincena' },
                h(ui.Select, { value: form.quincena, 'aria-label': 'Quincena',
                  options: [{ value: '1', label: '1ª quincena' }, { value: '2', label: '2ª quincena' }],
                  onChange: function (v) { cambiarForm('quincena', v); } }))
            : null,
          /* El nombre sale de un DESPLEGABLE con los conceptos de la planilla, no de un campo
             libre: "Sueldos" un mes y "sueldos" al siguiente parten el mismo gasto en dos líneas y
             la comparación mes contra mes deja de servir. Queda "Otro…" para lo que no esté. */
          h(ui.Field, { label: 'Concepto' },
            h(ui.Select, { value: form.concepto, 'aria-label': 'Nombre del concepto',
              options: [{ value: '', label: 'Elegí uno…' }]
                .concat(catalogoDe(form.familia).map(function (n) { return { value: n, label: n }; }))
                .concat([{ value: '__otro', label: 'Otro…' }]),
              onChange: function (v) { elegirConcepto(v); } })),
          form.concepto === '__otro'
            ? h(ui.Field, { label: 'Nombre' },
                h(ui.Input, { value: form.otro, placeholder: 'nombre del concepto',
                  'aria-label': 'Nombre del concepto nuevo',
                  onChange: function (v) { cambiarForm('otro', v); } }))
            : null,
          h(ui.Field, { label: 'Monto' },
            h(ui.Input, { value: form.monto, placeholder: '4.299.193,76', 'aria-label': 'Monto',
              onChange: function (v) { cambiarForm('monto', v); } })),
          h(ui.Field, { label: '% (opcional)' },
            h(ui.Input, { value: form.porcentaje, placeholder: '5', 'aria-label': 'Alícuota en porcentaje',
              onChange: function (v) { cambiarForm('porcentaje', v); } })),
          h(ui.Button, { onClick: guardar,
            disabled: busy || !form.concepto || (form.concepto === '__otro' && !form.otro)
              || (form.familia === 'impuesto' ? form.porcentaje === '' : form.monto === '') },
            busy ? 'Guardando…' : 'Cargar')),
        h('p', { className: 'muted', style: { fontSize: 12 } },
          'El porcentaje se guarda junto al monto (IIBB 5%, Imp. Déb-Créd 1,2%). Así, si mañana ',
          'cambia la alícuota, un mes ya cerrado sigue dando el mismo número.'),
        (data.conceptos || []).length
          ? h(ui.DataTable, {
              rows: data.conceptos.map(function (c) {
                return {
                  periodo: c.anio + '-' + String(c.mes).padStart(2, '0') + (c.quincena ? ' Q' + c.quincena : ' (mes)'),
                  tipo: rotuloFamilia(c.familia),
                  concepto: c.concepto, monto: money(c.monto),
                  porcentaje: c.porcentaje == null ? '—' : String(c.porcentaje).replace('.', ',') + '%'
                };
              }),
              columns: [
                { key: 'periodo', label: 'Período' }, { key: 'tipo', label: 'Tipo' },
                { key: 'concepto', label: 'Concepto' }, { key: 'monto', label: 'Monto' },
                { key: 'porcentaje', label: 'Alícuota' }
              ]
            })
          : h('p', { className: 'muted' }, 'Todavía no hay conceptos cargados en este período.')),




      null);
  }

  window.CR.screens = window.CR.screens || {};
  window.CR.screens.EstadoNegocio = EstadoNegocio;
})();
