(function () {
  var CR = window.CR, R = window.React, h = R.createElement;
  var useState = R.useState, useEffect = R.useEffect, useRef = R.useRef;
  function field(label, props, options) {
    return h('label', { className: 'lm-field' }, h('span', null, label), options
      ? h('select', Object.assign({ className: 'input' }, props), options.map(function (o) {
        return h('option', Object.assign({ key: o[0], value: o[0] }, o[2] || {}), o[1]);
      }))
      : h('input', Object.assign({ className: 'input' }, props)));
  }
  function button(text, onClick, disabled) { return h('button', { className: 'btn', type: 'button', onClick: onClick, disabled: disabled }, text); }
  function today() { return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()); }
  function fresh(fecha) { return { id: window.crypto.randomUUID(), fecha: fecha, svc: 'SCF2', ciclo: 'AM1', tipoUnidad: 'CAMIONETA', cantidad: 1, citacion: '08:45' }; }









  function redesDe(catalogo) { return catalogo && Array.isArray(catalogo.redes) ? catalogo.redes : []; }
  function redDeCentro(catalogo, svc) {
    return redesDe(catalogo).filter(function (r) { return (r.centros || []).indexOf(svc) !== -1; })[0] || null;
  }
  // La fila trae su red (`red`, del servidor); si no la trae, se la busca por su centro en el catálogo.
  function redDeFila(catalogo, row) { if (row.red) return row.red; var r = redDeCentro(catalogo, row.svc); return r ? r.clave : null; }
  function nombreDeRed(catalogo, clave) { var r = redesDe(catalogo).filter(function (x) { return x.clave === clave; })[0]; return r ? r.nombre : null; }



  function familiaDe(catalogo, row) {
    return row.familiaUnidad || (catalogo && catalogo.familias && catalogo.familias[row.tipoUnidad]) || row.tipoUnidad;
  }
  // Los centros del filtro: los del catálogo, en su orden, más el de alguna fila que ya no esté en él.
  function centrosDe(catalogo, rows) {
    var lista = catalogo && catalogo.centros ? Object.keys(catalogo.centros) : [];
    rows.forEach(function (r) { if (r.svc && lista.indexOf(r.svc) === -1) lista.push(r.svc); });
    return lista;
  }
  /* El SVC de «Nueva citación», agrupado por red con los nombres del servidor. Un centro que ninguna red
     declara (servidor viejo) va suelto, al final: se puede citar igual. */
  function opcionesSvc(catalogo) {
    var centros = Object.keys(catalogo.centros), usados = {};
    var opcion = function (c) { return h('option', { key: c, value: c }, c); };
    var grupos = redesDe(catalogo).map(function (red) {
      var propios = (red.centros || []).filter(function (c) { return centros.indexOf(c) !== -1; });
      propios.forEach(function (c) { usados[c] = true; });
      return propios.length ? h('optgroup', { key: red.clave, label: red.nombre }, propios.map(opcion)) : null;
    });
    return grupos.concat(centros.filter(function (c) { return !usados[c]; }).map(opcion));
  }






  function validarKm(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;
  }
  function kmConComa(n) { return String(n).replace('.', ','); }




  var AYUDA_PNU = 'PNU = muleto que no salió: se paga el 50 %, sin km ni cumplimiento';
  function esPnu(v) { return /^\s*pnu\s*$/i.test(String(v == null ? '' : v)); }
  function claseEstado(estado) { return estado === 'Pagable' ? 'is-ok' : estado === 'No pagable' ? 'is-no' : estado === 'PNU' ? 'is-pnu' : ''; }
  function Modulos(props) {
    var onlyLast = props.session && props.session.modulos && !props.session.modulos.brains;
    function arrow() { return h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8,
      strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false' },
      h('path', { d: 'M5 12h14M13 6l6 6-6 6' })); }
    function features(items) { return h('ul', { className: 'module-features' }, items.map(function (item) {
      return h('li', { key: item }, item);
    })); }
    return h('main', { className: 'module-shell', 'aria-labelledby': 'module-heading-title' },
      h('header', { className: 'module-masthead' },
        h('div', { className: 'module-brand' },
          h('img', { src: 'assets/icons/app/craneo-rutas.svg', alt: '', width: 40, height: 40 }),
          h('div', null, h('strong', null, 'CRÁNEO RUTAS'), h('span', null, 'Plataforma de operación logística'))),
        h('span', { className: 'module-version' }, 'v' + ((window.CR_VERSION || {}).version || '1.0.1'))),
      h('div', { className: 'module-heading' },
        h('span', { className: 'module-overline' }, 'CENTRO DE OPERACIONES'),
        h('h1', { id: 'module-heading-title' }, 'Tu operación, en un lugar.'),
        h('p', null, 'Elegí el módulo para comenzar tu jornada.')),
      h('nav', { className: 'module-grid', 'aria-label': 'Módulos de operación' },
        !onlyLast ? h('a', { className: 'module-card', href: '#/brains', 'aria-labelledby': 'module-rutas-enter' },
          h('div', { className: 'module-card-top' },
            h('img', { className: 'module-icon', src: 'assets/icons/app/craneo-rutas.svg', alt: '', width: 64, height: 64 }),
            h('span', { className: 'module-number', 'aria-hidden': 'true' }, '01')),
          h('span', { className: 'module-category' }, 'PRIMERA MILLA'),
          h('h2', { id: 'module-rutas-title' }, 'Cráneo Rutas'),
          h('p', null, 'Planificá las rutas del día y coordiná tu flota desde un mismo lugar.'),
          features(['Planificación', 'Flota', 'Seguimiento']),
          h('div', { className: 'module-card-footer' },
            h('span', { className: 'module-enter', id: 'module-rutas-enter' }, 'Abrir Cráneo Rutas', arrow()))) : null,
        h('a', { className: 'module-card module-card--lm', href: '#/last-mile', 'aria-labelledby': 'module-lm-enter' },
          h('div', { className: 'module-card-top' },
            h('img', { className: 'module-icon', src: 'assets/icons/app/last-mile.svg', alt: '', width: 64, height: 64 }),
            h('span', { className: 'module-number', 'aria-hidden': 'true' }, '02')),
          h('span', { className: 'module-category' }, 'ÚLTIMA MILLA'),
          h('h2', { id: 'module-lm-title' }, 'Last Mile'),
          h('p', null, 'Organizá las citaciones, asigná unidades y registrá el resultado de cada ruta.'),
          features(['Citaciones', 'Asignaciones', 'Resultados']),
          h('div', { className: 'module-card-footer' },
            h('span', { className: 'module-enter', id: 'module-lm-enter' }, 'Abrir Last Mile', arrow())))),
      h('footer', { className: 'module-foot' },
        h('span', null, h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6,
          strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false' },
          h('rect', { x: 5, y: 10, width: 14, height: 11, rx: 3 }), h('path', { d: 'M8 10V7a4 4 0 0 1 8 0v3M12 14v3' })),
          'Acceso con tu cuenta individual'),
        h('span', null, 'BRAINS · Operación logística')));
  }
  function Usuarios(props) {
    var s = useState([]), users = s[0], setUsers = s[1];
    var e = useState(''), error = e[0], setError = e[1];
    var b = useState(false), busy = b[0], setBusy = b[1];
    var n = useState({ nombre: '', email: '', password: '', rol: 'operador' }), nuevo = n[0], setNuevo = n[1];
    var p = useState({}), pending = p[0], setPending = p[1];
    function reload() { return CR.backend.lastMileGet('usuarios').then(function (j) { setUsers(j.usuarios); setPending({}); }); }
    useEffect(function () { reload().catch(function (err) { setError(err.message); }); }, []);
    function change(key, value) { setNuevo(Object.assign({}, nuevo, { [key]: value })); }
    function save(ruta, data) { if (busy) return; setBusy(true); setError('');
      CR.backend.lastMilePost(ruta, data).then(function () {
        if (ruta === 'usuario') setNuevo({ nombre: '', email: '', password: '', rol: 'operador' });
        return reload();
      }).catch(function (err) { setError(err.message); }).finally(function () { setBusy(false); });
    }
    var roles = [['lector','Solo lectura'],['operador','Carga y edición'],['administrador','Administración']];
    return h('section', { className: 'card lm-section' }, h('h2', null, 'Usuarios de Last Mile'),
      h('p', { className: 'muted' }, 'Los usuarios nuevos acceden solo a este módulo. Su contraseña inicial es provisoria.'),
      error ? h('p', { className: 'alerta alerta--alta', role: 'alert' }, error) : null,
      h('form', { className: 'lm-form', onSubmit: function (ev) { ev.preventDefault(); save('usuario', nuevo); } },
        field('Nombre', { required: true, maxLength: 120, value: nuevo.nombre, disabled: busy, onChange: function (ev) { change('nombre',ev.target.value); } }),
        field('Email', { required: true, type: 'email', autoComplete: 'off', value: nuevo.email, disabled: busy, onChange: function (ev) { change('email',ev.target.value); } }),
        field('Contraseña provisoria', { required: true, type: 'password', minLength: 12, maxLength: 200, autoComplete: 'new-password', value: nuevo.password, disabled: busy, onChange: function (ev) { change('password',ev.target.value); } }),
        field('Permiso inicial', { value: nuevo.rol, disabled: busy, onChange: function (ev) { change('rol',ev.target.value); } }, roles),
        h('button', { className: 'btn btn--primary', disabled: busy }, busy ? 'Guardando…' : 'Crear usuario')),
      h('div', { className: 'lm-table-wrap' }, h('table', { className: 'lm-table' }, h('thead', null, h('tr', null,
        ['Usuario','Email','Acceso Last Mile',''].map(function (t) { return h('th',{key:t},t); }))),
        h('tbody', null, users.map(function (u) { return h('tr', { key:u.id }, h('td',null,u.nombre),h('td',null,u.email),
          h('td',null,field('Permiso de '+u.nombre,{ 'aria-label':'Permiso de '+u.nombre, value: pending[u.id] === undefined ? (u.rol || '') : pending[u.id],
            disabled: busy || !u.activo, onChange:function(ev) { setPending(Object.assign({},pending,{[u.id]:ev.target.value})); }
          }, [['','Sin acceso']].concat(roles))),
          h('td',null,button('Aplicar',function(){save('acceso',{usuarioId:u.id,rol:pending[u.id] || null});},busy || !u.activo || pending[u.id] === undefined)));
        })))));
  }









  function CentrosHabilitados() {
    var d = useState(null), datos = d[0], setDatos = d[1];
    var p = useState({}), pendientes = p[0], setPendientes = p[1];
    var e = useState(''), error = e[0], setError = e[1];
    var m = useState(''), mensaje = m[0], setMensaje = m[1];
    var b = useState(false), busy = b[0], setBusy = b[1];
    function cargar() { return CR.backend.lastMileGet('unidades').then(function (j) { setDatos(j || {}); setPendientes({}); }); }
    useEffect(function () { cargar().catch(function (err) { setError(err.message); setDatos({}); }); }, []);
    var unidades = datos && Array.isArray(datos.unidades) ? datos.unidades : [];
    var centros = datos && Array.isArray(datos.centros) ? datos.centros : [];
    var puedeEditar = !!(datos && datos.permisos && datos.permisos.administrar);
    function redDe(svc) { var r = (datos && datos.redes || []).filter(function (x) { return (x.centros || []).indexOf(svc) !== -1; })[0]; return r ? r.nombre : undefined; }
    // Los centros seguidos de la misma red, juntos bajo su nombre; un centro que ninguna red declara va solo y sin nombre.
    function gruposDeRed() {
      return centros.reduce(function (grupos, svc) {
        var nombre = redDe(svc) || '', ultimo = grupos[grupos.length - 1];
        if (ultimo && ultimo.nombre === nombre && nombre) ultimo.centros.push(svc); else grupos.push({ nombre: nombre, centros: [svc] });
        return grupos;
      }, []);
    }
    function elegidos(u) { return pendientes[u.id] || u.centros || []; }
    // Lo tildado se guarda en el orden de los centros del servidor: «SCF2, SBU10», no en el orden de los clics.
    function alternar(u, svc) {
      var actual = elegidos(u), sig = actual.indexOf(svc) === -1 ? actual.concat([svc]) : actual.filter(function (x) { return x !== svc; });
      setPendientes(Object.assign({}, pendientes, { [u.id]: centros.filter(function (c) { return sig.indexOf(c) !== -1; }) }));
    }
    function cambio(u) { return !!pendientes[u.id] && pendientes[u.id].join() !== (u.centros || []).join(); }
    function guardar(u) {
      if (busy) return; setBusy(true); setError(''); setMensaje('');
      CR.backend.lastMilePost('unidad-centros', { vehiculoId: u.id, centros: elegidos(u) }).then(function (r) {
        var lista = r && Array.isArray(r.centros) ? r.centros : [];
        setMensaje(u.patente + (lista.length ? ': habilitada para ' + lista.join(', ') + '.' : ': sin centros habilitados. No entra en la asignación automática.'));
        return cargar();
      }).catch(function (err) { setError(err.message); }).finally(function () { setBusy(false); });
    }
    return h('section', { className: 'card lm-section lm-centros', 'aria-label': 'Centros habilitados', 'data-novedad': 'lm-centros-habilitados' },
      h('h2', null, 'Centros habilitados'),
      h('p', { className: 'muted' }, 'Para qué centros (SVC) trabaja cada unidad de Última Milla. La «Asignación automática» de Operación sólo propone una unidad en los centros que tiene habilitados. Una unidad sin centros no entra, pero se sigue pudiendo asignar a mano.'),
      datos && !puedeEditar ? h('p', { className: 'muted' }, 'Los centros los cambia un administrador de Last Mile.') : null,
      error ? h('p', { className: 'alerta alerta--alta', role: 'alert' }, error) : null,
      mensaje ? h('p', { className: 'lm-success', role: 'status' }, mensaje) : null,
      !datos ? h('p', { role: 'status' }, 'Cargando unidades…') : !unidades.length ? h('p', { className: 'lm-empty' }, 'No hay unidades de Última Milla en Flota. Para sumar una, creala o editá su estado a ÚLTIMA MILLA en el padrón de abajo.') :
        h('div', { className: 'lm-table-wrap' }, h('table', { className: 'lm-table lm-centros-tabla' },
          // Dos filas de encabezado: arriba la red (agrupa sus centros, como los publica el servidor), abajo cada centro.
          h('thead', null,
            h('tr', null, ['Patente', 'Tipo', 'Chofer'].map(function (t) { return h('th', { key: t, rowSpan: 2 }, t); })
              .concat(gruposDeRed().map(function (g, i) { return h('th', { key: 'red' + i, colSpan: g.centros.length, className: 'lm-centros-red' }, g.nombre); }))
              .concat([h('th', { key: 'acciones', rowSpan: 2 }, '')])),
            h('tr', null, centros.map(function (svc) { return h('th', { key: svc, title: redDe(svc), className: 'lm-centro-col' }, svc); }))),
          h('tbody', null, unidades.map(function (u) {
            var sel = elegidos(u);
            return h('tr', { key: u.id },
              h('td', null, u.patente, !sel.length ? h('small', { className: 'lm-sin-centros' }, 'sin centros habilitados') : null),
              h('td', null, u.familia),
              // «Sin chofer» de Flota no es un chofer: la asignación automática no propone esa unidad.
              h('td', null, u.chofer || h('span', { className: 'muted' }, 'Sin chofer en Flota')),
              centros.map(function (svc) {
                var posible = (u.centrosPosibles || []).indexOf(svc) !== -1, marcado = sel.indexOf(svc) !== -1;
                return h('td', { key: svc, className: 'lm-centro-col' }, h('input', { type: 'checkbox', checked: marcado, 'aria-label': svc + ' para ' + u.patente,
                  title: posible ? undefined : svc + ' no cita unidades de tipo ' + u.familia,
                  // Uno que no corresponde a su tipo no se puede tildar; si quedó tildado de antes, se puede destildar.
                  disabled: busy || !puedeEditar || (!posible && !marcado), onChange: function () { alternar(u, svc); } }));
              }),
              h('td', null, puedeEditar ? button('Guardar', function () { guardar(u); }, busy || !cambio(u)) : null));
          })))));
  }
  function LastMile(props) {
    var permissions = props.session.modulos && props.session.modulos.lastMile;
    var vistaFlota = props.vista === 'flota';
    var vistaTarifas = props.vista === 'tarifas';

    var vistaLiquidacion = props.vista === 'liquidacion';
    var vistaOperacion = !vistaFlota && !vistaTarifas && !vistaLiquidacion;
    // No se concede acceso al padrón de Primera Milla por tener un rol Last Mile.
    var puedeFlota = !!(permissions && props.session.modulos.brains &&
      (props.session.secciones || []).indexOf('flota-real') !== -1);
    var d = useState(today()), dia = d[0], setDia = d[1];
    var c = useState(null), catalogo = c[0], setCatalogo = c[1];
    var editarOperacion = !!(permissions && permissions.editar && catalogo && catalogo.permisos && catalogo.permisos.editar);
    var rowsState = useState([]), rows = rowsState[0], setRows = rowsState[1];
    var retiradosState = useState([]), retirados = retiradosState[0], setRetirados = retiradosState[1];
    var retiroState = useState(null), retiro = retiroState[0], setRetiro = retiroState[1];
    var retiroEnCurso = useRef(false);
    var loadingState = useState(true), loading = loadingState[0], setLoading = loadingState[1];
    var errorState = useState(''), error = errorState[0], setError = errorState[1];
    var msgState = useState(''), mensaje = msgState[0], setMensaje = msgState[1];
    var busyState = useState(false), busy = busyState[0], setBusy = busyState[1];
    var counter = useState(0), revision = counter[0], setRevision = counter[1];
    var citState = useState(function(){return fresh(dia);}), cit = citState[0], setCit = citState[1];
    var editState = useState(null), edit = editState[0], setEdit = editState[1];
    var adminState = useState(false), admin = adminState[0], setAdmin = adminState[1];
    var filterState = useState(''), filtro = filterState[0], setFiltro = filterState[1];
    var redFilterState = useState(''), filtroRed = redFilterState[0], setFiltroRed = redFilterState[1];
    // v1.0.18: la asignación automática (la propuesta que se está mirando, el ciclo elegido y lo que contestó el «Confirmar»).
    var propuestaState = useState(null), propuesta = propuestaState[0], setPropuesta = propuestaState[1];
    var cicloAutoState = useState(''), cicloAuto = cicloAutoState[0], setCicloAuto = cicloAutoState[1];
    var resultadoAutoState = useState(null), resultadoAuto = resultadoAutoState[0], setResultadoAuto = resultadoAutoState[1];
    var intercambioState = useState(null), intercambio = intercambioState[0], setIntercambio = intercambioState[1];
    var autoSeq = useRef(0), intercambioSeq = useRef(0), editOriginal = useRef(null);
    var busyRef = useRef(false); busyRef.current = busy;
    useEffect(function () {
      if (!edit && !intercambio && !retiro) return;
      var before = document.activeElement, dialog = document.querySelector('.lm-editor');
      if (!dialog) return;
      var first = dialog.querySelector('a[href]:not([aria-disabled="true"]),input:not(:disabled),select:not(:disabled),button:not(:disabled)'); if (first) first.focus();
      function keys(ev) {
        if (ev.key === 'Escape' && !busyRef.current) { setEdit(null); setIntercambio(null); setRetiro(null); setError(''); return; }
        if (ev.key !== 'Tab') return;
        var items = Array.from(dialog.querySelectorAll('a[href],input:not(:disabled),select:not(:disabled),button:not(:disabled)'));
        if (!items.length) { ev.preventDefault(); return; }
        if (ev.shiftKey && document.activeElement === items[0]) { ev.preventDefault(); items[items.length-1].focus(); }
        if (!ev.shiftKey && document.activeElement === items[items.length-1]) { ev.preventDefault(); items[0].focus(); }
      }
      dialog.addEventListener('keydown',keys);
      return function(){dialog.removeEventListener('keydown',keys);if(before && before.isConnected)before.focus();};
    },[!!edit,!!intercambio,!!retiro]);
    useEffect(function () {
      if (!edit && !intercambio && !retiro) return;
      var dialog=document.querySelector('.lm-editor');if(!dialog)return;
      var first=dialog.querySelector('a[href]:not([aria-disabled="true"]),input:not(:disabled),select:not(:disabled),button:not(:disabled)');
      var active=document.activeElement;
      if (busy && (!dialog.contains(active) || active.disabled)) (first || dialog).focus();
      if (!busy && active === dialog && first) first.focus();
    },[busy,!!edit,!!intercambio,!!retiro]);
    useEffect(function () {
      if (!permissions || vistaFlota || vistaTarifas || vistaLiquidacion) { setLoading(false); return; }
      var active = true; setLoading(true); setError('');
      Promise.all([CR.backend.lastMileGet('catalogo', dia), CR.backend.lastMileGet('servicios', dia)]).then(function (results) {
        if (!active) return; setCatalogo(results[0]); setRows(results[1].servicios); setRetirados(results[1].retirados || []);
      }).catch(function (err) { if(active) { setError(err.message); setRows([]); setRetirados([]); } }).finally(function(){if(active)setLoading(false);});
      return function(){active=false;};
    },[dia,revision,permissions,vistaFlota,vistaTarifas,vistaLiquidacion]);
    function refresh() { autoSeq.current++; intercambioSeq.current++; setRevision(function(v){return v+1;}); }
    function abrirRetiro(row) {
      if (busy || loading || !editarOperacion || row.puedeRetirar !== true) return;
      setEdit(null); setIntercambio(null); setError(''); setMensaje('');
      setRetiro({ row: row, motivo: '', operacionId: window.crypto.randomUUID(), pedido: null });
    }
    function confirmarRetiro() {
      if (busy || retiroEnCurso.current || !retiro || !retiro.motivo.trim() || !editarOperacion) return;


      var pedido = retiro.pedido || { id: retiro.row.id, version: retiro.row.version,
        motivo: retiro.motivo.trim(), operacionId: retiro.operacionId };
      setRetiro(Object.assign({},retiro,{pedido:pedido}));
      retiroEnCurso.current = true; setBusy(true); setError(''); setMensaje('');
      CR.backend.lastMilePost('cupo/retirar',pedido).then(function(r){
        if (!r || r.id !== pedido.id || r.operacionId !== pedido.operacionId || r.version !== pedido.version + 1
            || r.retirado !== true || typeof r.yaAplicado !== 'boolean')
          throw new Error('No se pudo comprobar el retiro. Reintentá el mismo pedido o recargá para consultar el historial.');
        setRetiro(null); setPropuesta(null); setResultadoAuto(null);
        setMensaje('Cupo retirado. El motivo, la fecha y la persona quedaron en «Cupos retirados».'); refresh();
      }).catch(function(err){setError(err.message);}).finally(function(){retiroEnCurso.current=false;setBusy(false);});
    }
    function changeCit(key, value) {
      var next = Object.assign({},cit,{[key]:value});
      if(key === 'svc') next.tipoUnidad = (catalogo.centros[value] || [])[0];
      setCit(next);
    }
    function save(ruta, data) {
      if (busy) return; setBusy(true); setError(''); setMensaje('');
      CR.backend.lastMilePost(ruta,data).then(function(){
        if(ruta === 'citaciones') setCit(fresh(dia)); else { setEdit(null); setPropuesta(null); setResultadoAuto(null); }
        setMensaje('Guardado.'); refresh();
      }).catch(function(err){setError(err.message);}).finally(function(){setBusy(false);});
    }
    function openEdit(row) { intercambioSeq.current++; editOriginal.current={vehiculoId:row.vehiculoId || '',choferId:row.choferId || ''}; setIntercambio(null); setError(''); setEdit(Object.assign({},row,{vehiculoId:row.vehiculoId || '',choferId:row.choferId || '',
      llegada:row.llegada || '',salida:row.salida || '',idRuta:row.idRuta || '',km:row.km == null ? '' : kmConComa(row.km),
      cumplimiento:row.cumplimiento === null ? '' : row.cumplimiento})); }
    function editChange(key,value) { var next = Object.assign({},edit,{[key]:value});
      if(key === 'vehiculoId') {
        var v = catalogo.vehiculos.find(function(x){return x.id === value;}), vinculado = v && v.choferId;
        var ocupado = vinculado && vinculado !== (editOriginal.current && editOriginal.current.choferId) && rows.some(function(r){
          return r.id !== edit.id && r.fecha === edit.fecha && r.ciclo === edit.ciclo && r.choferId === vinculado;
        });
        next.choferId = ocupado ? '' : vinculado || '';
      }
      setEdit(next);
    }



    var redesCat = redesDe(catalogo);
    var centrosFiltro = centrosDe(catalogo,rows).filter(function(c){var r = redDeCentro(catalogo,c);return !filtroRed || (r && r.clave) === filtroRed;});
    function cambiarRed(clave) { setFiltroRed(clave); if (clave && filtro && (redDeCentro(catalogo,filtro) || {}).clave !== clave) setFiltro(''); }
    var shown = rows.filter(function(row){return (!filtro || row.svc === filtro) && (!filtroRed || redDeFila(catalogo,row) === filtroRed);});
    var retiradosVisibles = retirados.filter(function(row){return (!filtro || row.svc === filtro) && (!filtroRed || redDeFila(catalogo,row) === filtroRed);});
    var redCit = catalogo ? redDeCentro(catalogo,cit.svc) : null;
    var familiaEdit = edit && catalogo ? familiaDe(catalogo,edit) : null;
    var requierePala = edit && edit.tipoUnidad === 'CHASIS MEDIANO CON PALA';
    function ocupanteDel(campo,id) {
      return edit && id ? rows.find(function(r){
        return r.id !== edit.id && r.fecha === edit.fecha && r.ciclo === edit.ciclo && r[campo] === id;
      }) : null;
    }
    function etiquetaOcupada(r) { return 'ocupado en '+r.svc+' / '+r.ciclo+' · Cupo '+r.cupo+' · Usá Intercambiar'; }
    var elegibles = edit && catalogo ? catalogo.vehiculos.filter(function(v){
      return v.tipoUnidad === familiaEdit && (!requierePala || v.conPala === true);
    }) : [];
    var opcionesVehiculo = elegibles.map(function(v){
      var actual = editOriginal.current && v.id === editOriginal.current.vehiculoId, ocupante = ocupanteDel('vehiculoId',v.id);
      return [v.id,v.patente+' · '+(actual ? 'asignación actual' : ocupante ? etiquetaOcupada(ocupante) : 'disponible'),
        { disabled:!!ocupante && !actual }];
    });
    var opcionesChofer = edit && catalogo ? catalogo.choferes.map(function(c){
      var actual = editOriginal.current && c.id === editOriginal.current.choferId, ocupante = ocupanteDel('choferId',c.id);
      return [c.id,c.nombre+' · '+(c.agencia || 'Particular')+' · '+(actual ? 'asignación actual' : ocupante ? etiquetaOcupada(ocupante) : 'disponible'),
        { disabled:!!ocupante && !actual }];
    }) : [];
    var asignadaFueraCatalogo = edit && edit.vehiculoId && !elegibles.some(function(v){return v.id === edit.vehiculoId;});



    var pnu = !!edit && esPnu(edit.idRuta);
    var kmProblema = edit && !pnu && String(edit.km == null ? '' : edit.km).trim() ? validarKm(edit.km) : null;
    var pnuSinUnidad = pnu && (!edit.vehiculoId || !edit.choferId);







    var ciclosDia = rows.map(function (r) { return r.ciclo; }).filter(function (c, i, a) { return c && a.indexOf(c) === i; }).sort();
    function describirCupo(x) { return x.svc + ' / ' + x.ciclo + ' · Cupo ' + x.cupo + ' · ' + x.tipoUnidad; }
    function describirAsignacion(x) { var a=(x&&x.asignacion)||{};return (a.patente||'Sin patente')+' / '+(a.chofer||'Sin chofer')+' · Agencia histórica: '+(a.agencia||'Particular'); }
    function diferenciasPadron(x) {
      var a=(x&&x.asignacion)||{}, diferencias=[];
      var vehiculo=catalogo && catalogo.vehiculos.find(function(v){return v.id === (x&&x.vehiculoId);});
      var chofer=catalogo && catalogo.choferes.find(function(c){return c.id === (x&&x.choferId);});
      if (!vehiculo) diferencias.push('la unidad no está disponible en el padrón actual');
      else if (vehiculo.patente !== a.patente) diferencias.push('patente actual: '+vehiculo.patente);
      if (!chofer) diferencias.push('el chofer no está disponible en el padrón actual');
      else {
        if (chofer.nombre !== a.chofer) diferencias.push('chofer actual: '+chofer.nombre);
        var agenciaActual=chofer.agencia||'Particular', agenciaHistorica=a.agencia||'Particular';
        if (agenciaActual !== agenciaHistorica) diferencias.push('agencia actual: '+agenciaActual);
      }
      return diferencias;
    }
    function mismoJson(a,b) {
      if (a === b) return true;
      if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) return false;
      var ka=Object.keys(a).sort(), kb=Object.keys(b).sort();
      return ka.length === kb.length && ka.every(function(k,i){return k === kb[i] && mismoJson(a[k],b[k]);});
    }
    function snapshotAsignacionValido(a) {
      return !!a && typeof a === 'object' && !Array.isArray(a)
        && typeof a.patente === 'string' && !!a.patente
        && typeof a.chofer === 'string' && !!a.chofer
        && typeof a.agencia === 'string' && !!a.agencia;
    }
    function pedirPropuesta() {
      if (busy) return; setBusy(true); setError(''); setMensaje(''); setResultadoAuto(null);
      var pedido = ++autoSeq.current;
      CR.backend.lastMilePost('asignacion-automatica/propuesta', { fecha: dia, ciclo: cicloAuto || null })
        .then(function (r) { if (pedido === autoSeq.current) setPropuesta(r); })
        .catch(function (err) { if (pedido === autoSeq.current) { setError(err.message); setPropuesta(null); } })
        .finally(function () { setBusy(false); });
    }
    function confirmarPropuesta() {
      if (busy || !propuesta || !propuesta.propuesta.length) return; setBusy(true); setError(''); setMensaje('');
      // Lo que viaja es lo que se vio, con la versión de cada cupo: si alguien lo cambió en el medio, el servidor no lo pisa.
      var vista = propuesta, cupos = {};
      vista.propuesta.forEach(function (x) { cupos[x.servicioId] = describirCupo(x) + ' → ' + x.patente; });
      CR.backend.lastMilePost('asignacion-automatica', { fecha: vista.fecha, ciclo: vista.ciclo, propuesta: vista.propuesta.map(function (x) {
        return { servicioId: x.servicioId, version: x.version, vehiculoId: x.vehiculoId, choferId: x.choferId }; }) })
        .then(function (r) { setPropuesta(null); setResultadoAuto(Object.assign({}, r, { cupos: cupos })); refresh(); })
        .catch(function (err) { setError(err.message); }).finally(function () { setBusy(false); });
    }
    function textoAplicados(n) { return n === 0 ? 'No se asignó ningún cupo.' : n === 1 ? 'Se asignó 1 cupo.' : 'Se asignaron ' + n + ' cupos.'; }
    function guardarServicio() {
      if (kmProblema || pnuSinUnidad) return;
      var km = String(edit.km == null ? '' : edit.km).trim();
      // El km viaja como se escribió («120,5»): el servidor lo vuelve a validar y lo normaliza, como en Estado de Negocio.
      save('servicio',Object.assign({},edit,{idRuta:pnu ? 'PNU' : edit.idRuta,
        km:pnu || !km ? null : km,
        cumplimiento:pnu || edit.cumplimiento === '' ? null : Number(edit.cumplimiento)}));
    }


    var origenIntercambio = intercambio && rows.find(function (r) { return r.id === intercambio.origenId; });
    var candidatosIntercambio = origenIntercambio ? rows.filter(function (r) {
      return r.id !== origenIntercambio.id && r.fecha === origenIntercambio.fecha && r.ciclo === origenIntercambio.ciclo
        && r.vehiculoId && r.choferId;
    }) : [];
    var destinoIntercambio = intercambio && rows.find(function (r) { return r.id === intercambio.destinoId; });
    function abrirIntercambio(row) {
      if (!row.vehiculoId || !row.choferId) return;
      intercambioSeq.current++; setEdit(null); setError(''); setMensaje('');
      setIntercambio({ origenId: row.id, destinoId: '', propuesta: null, operacionId: window.crypto.randomUUID(), error:'' });
    }
    function elegirIntercambio(id) {
      intercambioSeq.current++; setError('');
      setIntercambio(Object.assign({},intercambio,{ destinoId:id, propuesta:null, operacionId:window.crypto.randomUUID(), error:'' }));
    }
    function cuerpoIntercambio() {
      if (!origenIntercambio || !destinoIntercambio) return null;
      return { servicios: [{ id: origenIntercambio.id, version: origenIntercambio.version },
        { id: destinoIntercambio.id, version: destinoIntercambio.version }] };
    }
    function reciboIntercambioValido(recibo,cuerpo,propuestaConfirmada) {
      if (!recibo || typeof recibo !== 'object' || recibo.operacionId !== cuerpo.operacionId
          || typeof recibo.yaAplicado !== 'boolean' || !Array.isArray(recibo.servicios)
          || recibo.servicios.length !== cuerpo.servicios.length || !propuestaConfirmada
          || recibo.fecha !== propuestaConfirmada.fecha || recibo.ciclo !== propuestaConfirmada.ciclo) return false;
      return cuerpo.servicios.every(function (esperado) {
        var recibido = recibo.servicios.find(function (s) { return s && s.id === esperado.id; });
        var servicio = propuestaConfirmada.servicios.find(function (s) { return s && s.id === esperado.id; });
        var despues = servicio && servicio.despues;
        return !!recibido && recibido.version === esperado.version + 1
          && !!despues && recibido.vehiculoId === despues.vehiculoId
          && recibido.choferId === despues.choferId
          && snapshotAsignacionValido(despues.asignacion) && snapshotAsignacionValido(recibido.asignacion)
          && mismoJson(recibido.asignacion,despues.asignacion);
      });
    }
    function pedirIntercambio() {
      var cuerpo = cuerpoIntercambio(); if (busy || !cuerpo) return;
      var pedido = ++intercambioSeq.current; setBusy(true); setError('');
      CR.backend.lastMilePost('intercambio/propuesta', cuerpo).then(function (r) {
        if (pedido === intercambioSeq.current) setIntercambio(Object.assign({},intercambio,{propuesta:r,error:''}));
      }).catch(function (err) { if (pedido === intercambioSeq.current) { setError(err.message); setIntercambio(Object.assign({},intercambio,{error:err.message})); } })
        .finally(function () { setBusy(false); });
    }
    function confirmarIntercambio() {
      if (busy || !intercambio || !intercambio.propuesta) return;
      var propuestaConfirmada = intercambio.propuesta;
      var cuerpo = { operacionId:intercambio.operacionId, servicios:propuestaConfirmada.servicios.map(function (s) {
        return { id:s.id, version:s.version };
      }) };
      var pedido = ++intercambioSeq.current; setBusy(true); setError(''); setMensaje('');
      CR.backend.lastMilePost('intercambio', cuerpo).then(function (recibo) {
        if (pedido !== intercambioSeq.current) return;
        if (!reciboIntercambioValido(recibo,cuerpo,propuestaConfirmada))
          throw new Error('No pudimos confirmar la respuesta del servidor. Reintentá «Confirmar intercambio»: se usará la misma operación y no se duplicará.');
        setBusy(false);
        setIntercambio(null); setPropuesta(null); setResultadoAuto(null);
        setMensaje('Intercambio aplicado: ambos cupos se actualizaron juntos.'); refresh();
      }).catch(function (err) {
        if (pedido !== intercambioSeq.current) return;
        setError(err.message);
        if (err.status === 409) {
          setBusy(false);
          setPropuesta(null); setResultadoAuto(null);
          setIntercambio(Object.assign({},intercambio,{propuesta:null,operacionId:window.crypto.randomUUID(),error:err.message}));
          refresh();
        }
      }).finally(function () { setBusy(false); });
    }
    return h('div', { className:'app app--last-mile' },
      h(CR.ModuleSidebar, { open: props.menuOpen, onClose: props.onCloseMenu,
        icon: 'last-mile', name: 'LAST MILE', subtitle: 'ÚLTIMA MILLA · BRAINS',
        theme: props.theme, onTheme: props.onTheme, onPassword: props.onPassword,
        // «🆕 Novedades» (v1.0.13) sólo con acceso al módulo: sin acceso no hay nada que anunciar
        session: props.session, novedades: permissions ? 'lastMile' : null,
        onLogout: function(){props.onLogout().catch(function(err){setError(err.message);});} },
        h('nav',{className:'sidebar-nav','aria-label':'Secciones de Last Mile'},
          permissions ? h(R.Fragment,null,
            h('a',{className:'navlink'+(vistaOperacion && !admin ? ' active' : ''),href:'#/last-mile',
              onClick:function(){setAdmin(false);if(props.onCloseMenu)props.onCloseMenu();},'aria-current':vistaOperacion && !admin ? 'page' : undefined},
              h(CR.Ico,{name:'asignacion'}),h('span',null,'Operación')),
            puedeFlota ? h('a',{className:'navlink'+(vistaFlota ? ' active' : ''),href:'#/last-mile/flota','aria-current':vistaFlota ? 'page' : undefined},
              h(CR.Ico,{name:'flota'}),h('span',null,'Flota (MELI)')) : null,
            h('a',{className:'navlink'+(vistaTarifas ? ' active' : ''),href:'#/last-mile/tarifas','aria-current':vistaTarifas ? 'page' : undefined,onClick:function(){if(props.onCloseMenu)props.onCloseMenu();}},h(CR.Ico,{name:'kpi'}),h('span',null,'Tarifas')),

            h('a',{className:'navlink'+(vistaLiquidacion ? ' active' : ''),href:'#/last-mile/liquidacion','aria-current':vistaLiquidacion ? 'page' : undefined,
              onClick:function(){if(props.onCloseMenu)props.onCloseMenu();}},h(CR.Ico,{name:'timeline'}),h('span',null,'Liquidación')),
            permissions.administrar ? h('button',{type:'button',className:'navlink'+(vistaOperacion && admin ? ' active' : ''),
              'aria-current':vistaOperacion && admin ? 'page' : undefined,onClick:function(){setAdmin(true);window.location.hash='#/last-mile';if(props.onCloseMenu)props.onCloseMenu();}},
              h(CR.Ico,{name:'usuarios'}),h('span',null,'Usuarios y permisos')) : null) : null)),
      h('div',{className:'main'},
        h('header',{className:'topbar'},
          h('button',{type:'button',className:'btn btn--ghost btn--sm hamb','aria-label':'Abrir menú',
            'aria-expanded':!!props.menuOpen,'aria-controls':'module-sidebar',onClick:props.onMenu},'☰'),
          h('h1',null,!permissions ? 'Last Mile' : vistaFlota ? 'Flota (MELI)' : vistaTarifas ? 'Tarifas de Última Milla' : vistaLiquidacion ? 'Liquidación de Última Milla' : admin ? 'Usuarios y permisos' : 'Operación Last Mile'),
          h('div',{className:'spacer'}),
          h('span',{className:'badge '+(props.backendOn ? 'badge--ok' : 'badge--neutral')},props.backendOn ? 'Sistema conectado' : 'Sin conexión')),
      h('main', { className:'content lm-shell' },
      h('div',{className:'page-head lm-page-head'},h('div',{className:'eyebrow'},'ÚLTIMA MILLA'),
        h('h2',null,vistaFlota ? 'Tu flota, conectada a la operación' : vistaTarifas ? 'Tarifarios de Última Milla' : vistaLiquidacion ? 'Liquidación por quincena' : admin ? 'Administración del módulo' : 'Organizá la última milla'),

        h('p',null,vistaFlota ? 'Gestioná las unidades del padrón compartido de Cráneo Rutas.' : vistaLiquidacion ? (permissions && permissions.administrar ? 'Lo que se le cobra a Mercado Libre y lo que se le paga a cada proveedor, servicio por servicio, con sus ajustes.' : 'Lo que se le paga a cada proveedor, servicio por servicio, con sus ajustes.') : vistaTarifas ? (permissions && permissions.administrar ? 'Lo que paga Mercado Libre por red, tipo de unidad, rango de km y ciclo, y lo que cobra cada patente por ciclo.' : 'Lo que cobra cada patente de Última Milla por ciclo.') : admin ? 'Gestioná los accesos de tu equipo a Last Mile.' : 'Citaciones, asignaciones y resultados de ruta en un mismo lugar.')),
      !permissions ? h('section',{className:'card lm-section'},h('h2',null,'Acceso pendiente'),
        h('p',null,'La administración de Last Mile gestiona los accesos. Tu cuenta todavía no está habilitada.')) :
      h(R.Fragment,null,
        // v1.0.18: arriba del padrón, los centros habilitados de cada unidad de Última Milla (los edita sólo el administrador de Last Mile).
        vistaFlota ? (puedeFlota ? h(R.Fragment,null,h(CentrosHabilitados,null),h('section',{className:'lm-flota','aria-label':'Flota compartida'},
          h('p',{className:'lm-flota-note'},'Es el mismo padrón de Cráneo Rutas. Para asignar una patente en Last Mile, creala o editá su estado a ÚLTIMA MILLA. Al volver a Operación se actualizará el catálogo.'),
          h(CR.screens.FlotaReal,{readOnly:!(props.session.caps && props.session.caps.mutate)}))) :
          h('section',{className:'card lm-section'},h('h2',null,'Flota sin acceso'),
            h('p',null,'Tu cuenta no tiene permiso para consultar el padrón compartido de Flota (MELI). Solicitá la gestión de las unidades a la administración de Last Mile.'))) : vistaTarifas ? h(CR.screens.LastMileTarifas,{permissions:permissions}) :
        vistaLiquidacion ? h(CR.screens.LastMileLiquidacion,{permissions:permissions,irAOperacion:function(s){
          autoSeq.current++; intercambioSeq.current++; setDia(s.fecha); setCit(fresh(s.fecha)); setFiltro(s.svc); setFiltroRed('');
          setAdmin(false); setEdit(null); setIntercambio(null); setMensaje(''); setPropuesta(null); setResultadoAuto(null); setCicloAuto('');
        }}) : h(R.Fragment,null,
        !admin ? h('div',{className:'lm-toolbar card'},field('Fecha de operación',{type:'date',value:dia,disabled:busy,required:true,onChange:function(ev){if(!ev.target.value)return;autoSeq.current++;intercambioSeq.current++;setDia(ev.target.value);setCit(fresh(ev.target.value));setEdit(null);setIntercambio(null);setMensaje('');setPropuesta(null);setResultadoAuto(null);setCicloAuto('');}}),
          redesCat.length ? field('Red',{value:filtroRed,onChange:function(ev){cambiarRed(ev.target.value);}},[['','Todas las redes']].concat(redesCat.map(function(r){return [r.clave,r.nombre];}))) : null,
          field('Centro',{value:filtro,onChange:function(ev){setFiltro(ev.target.value);}},[['','Todos los centros']].concat(centrosFiltro.map(function(k){return [k,k];}))),
          button('Actualizar',function(){setEdit(null);setIntercambio(null);setPropuesta(null);setResultadoAuto(null);refresh();},busy)) : null,
        error ? h('p',{className:'alerta alerta--alta',role:'alert'},error) : null,
        mensaje ? h('p',{className:'lm-success',role:'status'},mensaje) : null,
        admin ? h(Usuarios,{session:props.session}) : h(R.Fragment,null,
          h('div',{className:'lm-summary'},[['Requeridos',rows.length],['Asignados',rows.filter(function(r){return r.vehiculoId;}).length],
            // Un PNU no tiene cumplimiento y no lo va a tener: no es un pendiente.
            ['Cumplimiento pendiente',rows.filter(function(r){return r.cumplimiento === null && !esPnu(r.idRuta);}).length]].map(function(item){return h('div',{className:'card',key:item[0]},h('span',{className:'muted'},item[0]),h('strong',null,loading ? '—' : item[1]));})),

          catalogo && catalogo.cierre && catalogo.cierre.cerrado ? h('p',{className:'lm-flota-note',role:'status'},'Esta quincena está cerrada. Podés consultarla; para corregirla, abrí Liquidación y pedí su reapertura con motivo a Gerencia o Administración.') : null,
          editarOperacion && catalogo ? h('section',{className:'card lm-section','data-novedad': 'lm-nueva-citacion-hb'},h('h2',null,'Nueva citación'),
            h('p',{className:'muted'},'Centros, redes y tipos según la fecha de operación seleccionada. Los cupos de chasis con pala requieren una patente identificada Con pala en Flota (MELI).'),
            h('form',{className:'lm-form',onSubmit:function(ev){ev.preventDefault();save('citaciones',cit);}},
              h('label',{className:'lm-field'},h('span',null,'SVC'),
                h('select',{className:'input',value:cit.svc,disabled:busy,onChange:function(ev){changeCit('svc',ev.target.value);}},opcionesSvc(catalogo))),
              // La red del centro elegido, con el nombre que publica el servidor: la red sale del SVC, no de la patente.
              redCit ? h('div',{className:'lm-field lm-red'},h('span',null,'Red del centro'),h('output',{className:'lm-red-nombre','aria-live':'polite'},redCit.nombre)) : null,
              field('Ciclo',{required:true,value:cit.ciclo,pattern:'(AM|PM)([1-9][0-9]?)?',title:'AM, PM, AM1 o PM1',maxLength:4,disabled:busy,onChange:function(ev){changeCit('ciclo',ev.target.value.toUpperCase());}}),
              // Más ancho que los demás: «EXTRA LARGE VAN H&B» no entra en un select de 150 px.
              h('label',{className:'lm-field lm-field--tipo'},h('span',null,'Tipo de unidad'),
                h('select',{className:'input',value:cit.tipoUnidad,disabled:busy,onChange:function(ev){changeCit('tipoUnidad',ev.target.value);}},
                  (catalogo.centros[cit.svc] || []).map(function(k){return h('option',{key:k,value:k},k);}))),
              field('Cantidad',{type:'number',required:true,min:1,max:200,step:1,value:cit.cantidad,disabled:busy,onChange:function(ev){changeCit('cantidad',Number(ev.target.value));}}),
              field('Horario de citación',{type:'time',required:true,value:cit.citacion,disabled:busy,onChange:function(ev){changeCit('citacion',ev.target.value);}}),
              h('button',{className:'btn btn--primary',disabled:busy || loading},busy ? 'Guardando…' : 'Crear cupos'))) : null,
          // v1.0.18: «Asignación automática» (ver pedirPropuesta). Sólo para quien edita la operación; el servidor lo vuelve a exigir.
          editarOperacion && catalogo ? h('section',{className:'card lm-section lm-auto','data-novedad': 'lm-asignacion-automatica','aria-label':'Asignación automática'},
            h('h2',null,'Asignación automática'),
            h('p',{className:'muted'},'El sistema propone una unidad de Última Milla para cada cupo sin unidad de la fecha: del tipo citado, habilitada para el centro, libre en ese ciclo y con chofer en Flota. Revisás la propuesta y confirmás: nada se asigna antes, y un cupo que ya tiene unidad no se toca.'),
            h('div',{className:'lm-form'},
              field('Ciclo a asignar',{value:cicloAuto,disabled:busy,onChange:function(ev){autoSeq.current++;setCicloAuto(ev.target.value);setPropuesta(null);}},[['','Todos los ciclos']].concat(ciclosDia.map(function(c){return [c,c];}))),
              h('button',{className:'btn btn--primary',type:'button',disabled:busy || loading,onClick:pedirPropuesta},'Asignación automática')),
            propuesta ? h('div',{className:'lm-propuesta','data-propuesta':''},
              h('h3',null,'Propuesta para el '+propuesta.fecha+(propuesta.ciclo ? ' · '+propuesta.ciclo : ' · todos los ciclos')),
              h('p',{className:'muted lm-criterio'},propuesta.criterio),
              !propuesta.propuesta.length && !propuesta.sinCubrir.length ? h('p',{className:'lm-empty'},'No hay cupos sin unidad para esta fecha'+(propuesta.ciclo ? ' y ciclo' : '')+'.') : null,
              propuesta.propuesta.length ? h('div',{className:'lm-table-wrap'},h('table',{className:'lm-table lm-propuesta-tabla'},
                h('thead',null,h('tr',null,['SVC / ciclo','Unidad / citación','Patente / chofer','Agencia','Cupos que lleva'].map(function(t){return h('th',{key:t},t);}))),
                h('tbody',null,propuesta.propuesta.map(function(x){return h('tr',{key:x.servicioId},
                  h('td',null,x.svc+' / '+x.ciclo,h('small',null,'Cupo '+x.cupo)),h('td',null,x.tipoUnidad,h('small',null,x.citacion)),
                  h('td',null,x.patente,h('small',null,x.chofer)),h('td',null,x.agencia),
                  // La carga es lo que explica por qué va esta unidad (el criterio de arriba): los 7 días anteriores y lo de ese día.
                  h('td',null,x.cuposPrevios+' en 7 días',x.cuposDelDia ? h('small',null,x.cuposDelDia+' ese día') : null));})))) : null,
              propuesta.sinCubrir.length ? h('div',{className:'lm-sin-cubrir'},h('h4',null,'Sin cubrir ('+propuesta.sinCubrir.length+')'),
                h('ul',null,propuesta.sinCubrir.map(function(x){return h('li',{key:x.servicioId},h('strong',null,describirCupo(x)),': '+x.mensaje);}))) : null,
              (propuesta.unidadesSinCentros || []).length ? h('p',{className:'lm-aviso lm-sin-centros-aviso'},'Sin centros habilitados (no entran en la asignación automática): '
                +propuesta.unidadesSinCentros.map(function(u){return u.patente;}).join(', ')+'. Se habilitan en Flota (MELI) → «Centros habilitados».') : null,
              h('div',{className:'lm-actions'},button('Descartar',function(){setPropuesta(null);},busy),
                h('button',{className:'btn btn--primary',type:'button',disabled:busy || !propuesta.propuesta.length,onClick:confirmarPropuesta},busy ? 'Confirmando…' : 'Confirmar'))) : null,
            resultadoAuto ? h('div',{className:'lm-resultado-auto',role:'status'},
              h('p',{className:'lm-success'},textoAplicados(resultadoAuto.aplicados.length)),
              resultadoAuto.omitidos.length ? h('div',{className:'lm-omitidos'},h('p',null,'No se aplicaron '+resultadoAuto.omitidos.length+' (quedaron como estaban):'),
                h('ul',null,resultadoAuto.omitidos.map(function(o){return h('li',{key:o.servicioId},h('strong',null,resultadoAuto.cupos[o.servicioId] || 'Cupo'),': '+o.mensaje);}))) : null) : null) : null,

          h('section',{className:'card lm-section','data-novedad': 'lm-km-pnu'},h('h2',null,'Asignación y resultados'),
            h('p',{className:'muted'},'Completá el ID de Mercado Libre, los horarios y los km cuando termine la operación. El cumplimiento se carga como porcentaje entero. Si el muleto no salió, en ID Ruta va PNU.'),
            loading ? h('p',{role:'status'},'Cargando operación…') : !shown.length ? h('p',{className:'lm-empty'},'Todavía no hay citaciones para esta fecha y centro.') :
              h('div',{className:'lm-table-wrap'},h('table',{className:'lm-table'},h('thead',null,h('tr',null,['SVC / ciclo','Unidad / citación','Patente / chofer','Agencia','Llegada / salida','ID Ruta','Km','Cumplimiento','Estado pago',''].map(function(t){return h('th',{key:t},t);}))),
                h('tbody',null,shown.map(function(row){var filaPnu = esPnu(row.idRuta);
                  return h('tr',{key:row.id},h('td',null,row.svc+' / '+row.ciclo,h('small',null,'Cupo '+row.cupo)),
                  h('td',null,row.tipoUnidad,h('small',null,row.citacion),row.tipoUnidadOriginal ? h('small',null,'Citado como '+row.tipoUnidadOriginal) : null),h('td',null,row.asignacion.patente || 'Sin asignar',h('small',null,row.asignacion.chofer || '—')),
                  h('td',null,row.asignacion.agencia || '—'),h('td',null,(row.llegada || '—')+' / '+(row.salida || '—')),h('td',null,row.idRuta || 'Pendiente'),
                  // Un PNU no lleva km ni cumplimiento: «—», no «Pendiente» (no falta nada).
                  h('td',null,row.km != null ? kmConComa(row.km)+' km' : filaPnu ? '—' : 'Pendiente'),
                  h('td',null,filaPnu ? '—' : row.cumplimiento === null ? 'Pendiente' : row.cumplimiento+'%'),
                  h('td',null,h('span',{className:'lm-status '+claseEstado(row.estadoPago),title:row.estadoPago === 'PNU' ? AYUDA_PNU : undefined},row.estadoPago)),
                   h('td',null,editarOperacion ? h('div',{className:'lm-row-actions'},
                     button('Editar',function(){openEdit(row);},busy),
                     row.vehiculoId && row.choferId ? button('Intercambiar',function(){abrirIntercambio(row);},busy) : null,
                     row.puedeRetirar === true ? button('Retirar cupo',function(){abrirRetiro(row);},busy) : null) : null));}))))),
          h('section',{className:'card lm-section','data-novedad': 'lm-cupos-retirados','aria-label':'Cupos retirados'},
            h('h2',null,'Cupos retirados'),
            h('p',{className:'muted'},'Para corregir una citación creada de más, usá «Retirar cupo» en la fila sin unidad ni resultados. El cupo queda en este historial y deja de contar en la operación y la liquidación.'),
            loading ? h('p',{role:'status'},'Cargando historial…') : !retiradosVisibles.length ? h('p',{className:'lm-empty'},'No hay cupos retirados para esta fecha y estos filtros.') :
              h('div',{className:'lm-table-wrap'},h('table',{className:'lm-table'},
                h('thead',null,h('tr',null,['Cupo','Citación','Motivo','Retirado por','Fecha del retiro'].map(function(t){return h('th',{key:t},t);}))),
                h('tbody',null,retiradosVisibles.map(function(row){return h('tr',{key:row.id},
                  h('td',null,describirCupo(row)),h('td',null,row.fecha+' · '+row.citacion),h('td',null,row.retiro.motivo),
                  h('td',null,row.retiro.por || 'Usuario registrado'),
                  h('td',null,new Date(row.retiro.fecha).toLocaleString('es-AR',{timeZone:'America/Argentina/Buenos_Aires'})));}))))),

          h('p',{className:'muted'},'El estado pagable describe el cumplimiento (90 a 100 % se paga entero; 0 a 89 %, nada) y PNU marca el muleto que no salió (se paga el 50 %). '
            + (permissions.administrar ? 'Lo que se cobra y se paga por cada servicio está en «Liquidación». ' : 'Lo que se paga a cada proveedor está en «Liquidación». ')
            + (catalogo && catalogo.cierre && catalogo.cierre.cerrado
              ? 'La quincena está cerrada. Para corregirla, Gerencia o Administración debe reabrirla con motivo desde «Liquidación».'
              : 'El cierre de esta quincena se gestiona desde «Liquidación». Mientras siga abierta, sus datos e importes pueden cambiar.'))),
        retiro ? h('div',{className:'lm-modal'},h('section',{className:'card lm-editor',role:'dialog',tabIndex:-1,'aria-modal':'true','aria-labelledby':'lm-retiro-title'},
          h('h2',{id:'lm-retiro-title'},'Retirar cupo creado de más'),
          h('p',null,describirCupo(retiro.row)),
          h('p',{className:'muted'},retiro.row.fecha+' · Citación '+retiro.row.citacion+' · Sin unidad ni resultados'),
          h('p',{className:'lm-aviso'},'Se quitará de los pendientes y la asignación. Conservaremos el cupo y su motivo en el historial. No se borra la citación ni se cambian los otros cupos.'),
          error ? h('p',{role:'alert',className:'alerta alerta--alta'},error) : null,
          h('form',{className:'lm-form',onSubmit:function(ev){ev.preventDefault();confirmarRetiro();}},
            field('Motivo del retiro',{required:true,maxLength:300,value:retiro.motivo,disabled:busy || !!retiro.pedido,
              onChange:function(ev){setRetiro(Object.assign({},retiro,{motivo:ev.target.value}));}}),
            retiro.pedido ? h('p',{className:'muted'},'El pedido conserva su motivo para poder reintentarlo sin duplicar el retiro. Si ya se confirmó, lo verás al actualizar el historial.') : null,
            h('div',{className:'lm-actions'},button('Volver',function(){setRetiro(null);setError('');},busy),
              h('button',{className:'btn btn--primary',disabled:busy || !retiro.motivo.trim()},busy ? 'Retirando…' : retiro.pedido ? 'Reintentar retiro' : 'Confirmar retiro'))))) : null,
        edit ? h('div',{className:'lm-modal'},h('section',{className:'card lm-editor',role:'dialog',tabIndex:-1,'aria-modal':'true','aria-labelledby':'lm-editor-title'},
          h('h2',{id:'lm-editor-title'},edit.svc+' · '+edit.ciclo+' · Cupo '+edit.cupo),
          h('p',{className:'muted'},edit.fecha+' · '+edit.tipoUnidad+(nombreDeRed(catalogo,redDeFila(catalogo,edit)) ? ' · '+nombreDeRed(catalogo,redDeFila(catalogo,edit)) : '')),
          h('p',{className:'lm-aviso'},'Para reemplazar la asignación, elegí una unidad y un chofer libres en este ciclo. Si querés usar los de otro cupo asignado, cerrá esta edición y elegí «Intercambiar».'),
          requierePala ? h('p',{className:'lm-aviso'},'Este cupo requiere una unidad identificada Con pala en Flota (MELI). Sin confirmar o Sin pala no habilita una nueva asignación.') : null,
          !elegibles.length ? h('p',{className:'lm-empty',role:'status'},'No hay patentes habilitadas de tipo '+familiaEdit+(familiaEdit !== edit.tipoUnidad ? ', que es lo que se asigna a una citación '+edit.tipoUnidad : '')+'. En Flota (MELI), editá o creá la unidad con estado ÚLTIMA MILLA y luego actualizá Last Mile. Las unidades de Primera Milla, inactivas o de baja no se ofrecen aquí.') : null,
          !elegibles.length && puedeFlota ? h('a',{className:'btn',href:'#/last-mile/flota',onClick:function(){setEdit(null);}},'Ir a Flota (MELI)') : null,
          asignadaFueraCatalogo ? h('p',{className:'muted'},'La patente asignada ya no está disponible en el catálogo actual. Podés completar sus resultados históricos o elegir una unidad habilitada para reasignar.') : null,
          error ? h('p',{role:'alert',className:'alerta alerta--alta'},error) : null,
          h('form',{className:'lm-form',onSubmit:function(ev){ev.preventDefault();guardarServicio();}},
            field('Patente',{value:edit.vehiculoId,disabled:busy,onChange:function(ev){editChange('vehiculoId',ev.target.value);}},[['','Sin asignar']].concat(asignadaFueraCatalogo ? [[edit.vehiculoId,(edit.asignacion.patente || edit.vehiculoId)+' · asignación histórica']] : []).concat(opcionesVehiculo)),
            field('Chofer',{value:edit.choferId,disabled:busy || !edit.vehiculoId,onChange:function(ev){editChange('choferId',ev.target.value);}},[['','Elegir chofer']].concat(opcionesChofer)),
            field('Llegada al SVC',{type:'time',value:edit.llegada,disabled:busy,onChange:function(ev){editChange('llegada',ev.target.value);}}),
            field('Salida del SVC',{type:'time',value:edit.salida,disabled:busy,onChange:function(ev){editChange('salida',ev.target.value);}}),



            h('div',{className:'lm-resultado',role:'group','aria-labelledby':'lm-resultado-titulo'},
              h('span',{id:'lm-resultado-titulo',className:'lm-resultado-titulo'},'Resultado de la ruta'),
              h('label',{className:'lm-field'},h('span',null,'ID Ruta de Mercado Libre'),
                h('input',{className:'input',value:edit.idRuta,pattern:'[0-9]{1,40}|[Pp][Nn][Uu]',title:'Sólo dígitos, o PNU si el muleto no salió',maxLength:40,autoComplete:'off',
                  'aria-describedby':'lm-ayuda-pnu',disabled:busy,onChange:function(ev){editChange('idRuta',ev.target.value);}})),
              h('label',{className:'lm-field'},h('span',null,'Km'),
                h('input',{className:'input',value:pnu ? '' : edit.km,inputMode:'decimal',maxLength:10,placeholder:pnu ? 'no lleva' : 'ej. 120,5',autoComplete:'off',
                  'aria-invalid':kmProblema ? 'true' : undefined,'aria-describedby':kmProblema ? 'lm-km-invalido' : undefined,
                  disabled:busy || pnu,onChange:function(ev){editChange('km',ev.target.value);}}),
                kmProblema ? h('small',{id:'lm-km-invalido',className:'lm-km-invalido',role:'status','data-km-invalido':''},kmProblema) : null),
              field('Cumplimiento (%)',{type:'number',min:0,max:100,step:1,value:pnu ? '' : edit.cumplimiento,placeholder:pnu ? 'no lleva' : undefined,
                disabled:busy || pnu,onChange:function(ev){editChange('cumplimiento',ev.target.value);}}),
              h('small',{id:'lm-ayuda-pnu',className:'lm-ayuda'},AYUDA_PNU)),
            // El servidor rechaza un PNU sin patente o sin chofer (se le paga a esa patente): se dice acá, antes de guardar.
            pnuSinUnidad ? h('p',{className:'lm-aviso',role:'status'},'Un PNU lleva patente y chofer: se le paga a esa patente. Asignalos antes de guardar.') : null,
            h('div',{className:'lm-actions'},button('Cancelar',function(){setEdit(null);setError('');},busy),h('button',{className:'btn btn--primary',disabled:busy || !!kmProblema || pnuSinUnidad},busy ? 'Guardando…' : 'Guardar'))))) : null,
        intercambio && origenIntercambio ? h('div',{className:'lm-modal'},h('section',{className:'card lm-editor lm-intercambio',role:'dialog',tabIndex:-1,'aria-modal':'true','aria-labelledby':'lm-intercambio-title'},
          h('h2',{id:'lm-intercambio-title'},'Intercambiar dos cupos'),
          h('p',{className:'muted'},'Origen: '+describirCupo(origenIntercambio)+' · '+describirAsignacion(origenIntercambio)),
          !candidatosIntercambio.length ? h('p',{className:'lm-empty',role:'status'},'No hay otro cupo asignado en '+origenIntercambio.ciclo+' para intercambiar.') :
            h('div',{className:'lm-form'},
              field('Cupo a intercambiar',{value:intercambio.destinoId,disabled:busy,onChange:function(ev){elegirIntercambio(ev.target.value);}},
                [['','Elegir otro cupo']].concat(candidatosIntercambio.map(function(r){return [r.id,describirCupo(r)+' · '+describirAsignacion(r)];}))),
              h('button',{className:'btn',type:'button',disabled:busy || !destinoIntercambio,onClick:pedirIntercambio},busy ? 'Validando…' : 'Ver cambios')),
          intercambio.error || error ? h('p',{role:'alert',className:'alerta alerta--alta'},intercambio.error || error) : null,
          intercambio.propuesta ? h('div',{className:'lm-intercambio-previa','data-intercambio-previa':''},
            h('h3',null,'Vista previa · '+intercambio.propuesta.fecha+' · '+intercambio.propuesta.ciclo),
            h('div',{className:'lm-intercambio-grid'},intercambio.propuesta.servicios.map(function(s){
              var diferenciasAntes=diferenciasPadron(s.antes), diferenciasDespues=diferenciasPadron(s.despues);
              return h('article',{className:'lm-intercambio-cupo',key:s.id},
                h('strong',null,s.svc+' · '+s.ciclo+' · Cupo '+s.cupo),
                h('span',null,'Antes: '+describirAsignacion(s.antes)),
                diferenciasAntes.length ? h('small',{className:'lm-intercambio-padron'},'Diferencias con padrón actual: '+diferenciasAntes.join(' · ')) : null,
                h('span',{className:'lm-intercambio-despues'},'Después: '+describirAsignacion(s.despues)),
                diferenciasDespues.length ? h('small',{className:'lm-intercambio-padron'},'Diferencias con padrón actual: '+diferenciasDespues.join(' · ')) : null);
            })),
            h('ul',{className:'lm-intercambio-advertencias'},(intercambio.propuesta.advertencias || []).map(function(a){return h('li',{key:a},a);}))) : null,
          h('div',{className:'lm-actions'},
            button('Cancelar',function(){intercambioSeq.current++;setIntercambio(null);setError('');},busy),
            intercambio.propuesta ? h('button',{className:'btn btn--primary',type:'button',disabled:busy,onClick:confirmarIntercambio},busy ? 'Intercambiando…' : 'Confirmar intercambio') : null))) : null)))),
      /* POPUP DE NOVEDADES (v1.0.13): en todas las vistas del módulo, pero se abre SOLO en la principal
         («Operación Last Mile», con acceso y fuera de «Usuarios y permisos») y pasada la carga. */
      CR.Novedades && CR.novedades ? h(CR.Novedades.Host, { modulo: 'lastMile', session: props.session,
        ruta: props.vista + (admin ? ':usuarios' : ''), principal: !!permissions && vistaOperacion && !admin,
        cargando: loading }) : null);
  }
  CR.screens.Modulos = Modulos; CR.screens.LastMile = LastMile;
  CR.screens.LastMileCentros = CentrosHabilitados;   // v1.0.18: la monta Flota (MELI); expuesta para los arneses
})();
