<style>
  .emp-card { border:1px solid #e9ecef; border-radius: .75rem; padding: 12px; }
  .emp-meta .badge { background: #f8f9fa; color:#212529; border:1px solid #e9ecef; }
  .spark-wrap { width:100%; height:80px; }
  .spark { width:100%; height:100%; }
  .spark text { font-size:10px; fill:#6c757d; }
  .spark rect:hover { filter: brightness(0.9); }
</style>

<!-- services/manso/src/views/usuarios.ejs -->
<div class="d-flex align-items-center justify-content-between mb-3">
  <h1 class="h4 m-0">Usuarios · Carga de horarios</h1>
  <div class="d-flex align-items-center gap-2">
    <input id="fileInput" type="file" accept=".txt" class="form-control form-control-sm" />
    <div id="errorBox" class="alert alert-danger d-none" role="alert"></div>
    <button id="btnGuardarBD" class="btn btn-success btn-sm" disabled>Guardar asistencia</button>
    <button id="btnProcesar" class="btn btn-primary btn-sm">Procesar</button>
    <button id="btnExport" class="btn btn-outline-secondary btn-sm" disabled>Exportar CSV</button>
  </div>
</div>




<div class="row g-3">
    <!-- Resumen diario (últimos 30 días) -->
    <div class="col-12">
        <div class="card shadow-sm mt-3">
            <div class="card-header d-flex align-items-center">
                <strong>Resumen diario (últimos 30 días)</strong>
                <div class="ms-auto small text-muted" id="resumeCount">—</div>
            </div>
            <div class="card-body">
                <div id="resumenCards" class="row g-3"></div>
            </div>
        </div>
    </div>

    <!-- Carga manual de horario -->
    <div class="col-12">
        <div class="card shadow-sm mb-3">
            <div class="card-header"><strong>Carga manual de horario</strong></div>
            <div class="card-body">
                <form id="frmManual" class="row g-3">
                <div class="col-12 col-md-3">
                    <label class="form-label">Documento</label>
                    <input id="m_doc" type="text" class="form-control" placeholder="ej: 052809684">
                    <div class="form-text" id="m_doc_help">Ingresá el documento (se quitan ceros a la izquierda).</div>
                </div>

                <div class="col-12 col-md-4">
                    <label class="form-label">Entrada</label>
                    <div class="d-flex gap-2">
                    <input id="m_in_date" type="date" class="form-control">
                    <input id="m_in_time" type="time" step="1" class="form-control" placeholder="HH:MM:SS">
                    </div>
                </div>

                <div class="col-12 col-md-4">
                    <label class="form-label">Salida</label>
                    <div class="d-flex gap-2">
                    <input id="m_out_date" type="date" class="form-control">
                    <input id="m_out_time" type="time" step="1" class="form-control" placeholder="HH:MM:SS">
                    </div>
                </div>

                <div class="col-12 col-md-1 d-flex align-items-end">
                    <button id="btnManualSave" type="submit" class="btn btn-success w-100">Guardar</button>
                </div>

                <div class="col-12">
                    <div id="m_feedback" class="small text-muted"></div>
                </div>
                </form>
            </div>
        </div>
    </div>

  <!-- Resumen de intervalos -->
  <div class="col-12">
    <div class="card shadow-sm">
      <div class="card-header d-flex align-items-center">
        <strong>Resumen por usuario y día (pares de fichadas)</strong>
        <div class="ms-auto small text-muted" id="status">Sin datos</div>
      </div>
      <div class="card-body p-0">
        <div class="table-responsive">
          <table class="table table-sm align-middle mb-0">
            <thead class="table-light">
              <tr>
                <th>Documento</th>
                <th>Nombre</th>
                <th>Fecha</th>
                <th class="text-end">Desde</th>
                <th class="text-end">Hasta</th>
                <th class="text-end">Duración</th>
                <th>Obs.</th>
              </tr>
            </thead>
            <tbody id="tbResumen">
              <tr><td colspan="7" class="p-3 text-muted">Cargá un archivo .txt y presioná “Procesar”.</td></tr>
            </tbody>
          </table>
        </div>
      </div>
      <div class="card-footer small text-muted">
        Los intervalos se calculan por pares del mismo día: (1→2), (3→4), … Si queda una fichada impar se marca como “incompleto”.
      </div>
    </div>
  </div>

  <!-- Registros crudos -->
  <div class="col-12">
    <div class="card shadow-sm">
      <div class="card-header"><strong>Registros crudos</strong></div>
      <div class="card-body p-0">
        <div class="table-responsive" style="max-height:50vh;overflow:auto;">
          <table class="table table-sm align-middle mb-0">
            <thead class="table-light">
              <tr>
                <th>#</th>
                <th>Documento</th>
                <th>Nombre</th>
                <th>Modo</th>
                <th>Fecha</th>
                <th>Hora</th>
              </tr>
            </thead>
            <tbody id="tbCrudo">
              <tr><td colspan="6" class="p-3 text-muted">—</td></tr>
            </tbody>
          </table>
        </div>
      </div>
      <div class="card-footer small text-muted">
        Origen: archivo .txt (campos separados por tabulación). Documento es el **3er campo** (sin ceros a la izquierda), fecha el **6º** (YY/MM/DD o YYYY/MM/DD) y hora el **7º** (HH:MM:SS 24h).
      </div>
    </div>
  </div>
    <!-- Administrar asistencia -->
    <div class="col-12">
        <div class="card shadow-sm mb-3">
            <div class="card-header"><strong>Administrar asistencia</strong></div>
            <div class="card-body">
                <div class="row g-2 align-items-end">
                <div class="col-12 col-md-3">
                    <label class="form-label">Documento</label>
                    <input id="adm_doc" type="text" class="form-control" placeholder="ej: 052809684">
                    <div class="form-text" id="adm_doc_help">Ingrese documento (se limpian ceros a la izquierda).</div>
                </div>
                <div class="col-6 col-md-2">
                    <label class="form-label">Desde</label>
                    <input id="adm_from" type="date" class="form-control">
                </div>
                <div class="col-6 col-md-2">
                    <label class="form-label">Hasta</label>
                    <input id="adm_to" type="date" class="form-control">
                </div>
                <div class="col-12 col-md-2">
                    <button id="btnAdmLoad" class="btn btn-primary w-100">Cargar</button>
                </div>
                <div class="col-12 col-md-3 text-end">
                    <span id="adm_status" class="small text-muted">—</span>
                </div>
                </div>

                <div class="row g-3 mt-3">
                <div class="col-12 col-lg-6">
                    <div class="card h-100">
                    <div class="card-header"><strong>Marcas (crudo)</strong></div>
                    <div class="card-body p-0">
                        <div class="table-responsive" style="max-height:50vh;overflow:auto;">
                        <table class="table table-sm align-middle mb-0">
                            <thead class="table-light">
                            <tr>
                                <th>#</th>
                                <th>Fecha</th>
                                <th>Hora</th>
                                <th>Modo</th>
                                <th>Origen</th>
                                <th></th>
                            </tr>
                            </thead>
                            <tbody id="adm_tb_raw">
                            <tr><td colspan="6" class="p-3 text-muted">Sin datos</td></tr>
                            </tbody>
                        </table>
                        </div>
                    </div>
                    <div class="card-footer small text-muted">Editar cambia la marca y recalcula pares del entorno.</div>
                    </div>
                </div>

                <div class="col-12 col-lg-6">
                    <div class="card h-100">
                    <div class="card-header"><strong>Intervalos (pares)</strong></div>
                    <div class="card-body p-0">
                        <div class="table-responsive" style="max-height:50vh;overflow:auto;">
                        <table class="table table-sm align-middle mb-0">
                            <thead class="table-light">
                            <tr>
                                <th>Fecha (jornada)</th>
                                <th class="text-end">Desde</th>
                                <th class="text-end">Hasta</th>
                                <th class="text-end">Duración</th>
                            </tr>
                            </thead>
                            <tbody id="adm_tb_pairs">
                            <tr><td colspan="4" class="p-3 text-muted">Sin datos</td></tr>
                            </tbody>
                        </table>
                        </div>
                    </div>
                    <div class="card-footer small text-muted">Fecha del par = día de la entrada (jornada).</div>
                    </div>
                </div>
                </div>
        </div>
        </div>
    </div>
</div>

<script>
  // ===== Helpers UI =====
    (() => {
        const $ = s => document.querySelector(s);
        const z2 = n => String(n).padStart(2,'0');
        const normDoc = s => String(s||'').replace(/\D/g,'').replace(/^0+/, '') || '0';

        // RPC helpers
        async function rpcGet(doc, desde, hasta){
            const r = await fetch('/api/rpc/asistencia_get', {
            method:'POST', headers:{'Content-Type':'application/json'},
            body: JSON.stringify({ doc, desde, hasta })
            });
            if(!r.ok) throw new Error('asistencia_get failed');
            return await r.json();
        }
        async function rpcUpdateRaw(id_raw, fecha, hora, modo){
            const r = await fetch('/api/rpc/asistencia_update_raw', {
            method:'POST', headers:{'Content-Type':'application/json'},
            body: JSON.stringify({ id_raw, fecha, hora, modo })
            });
            if(!r.ok) throw new Error('asistencia_update_raw failed');
            return await r.json();
        }
        async function rpcDeleteRaw(id_raw){
            const r = await fetch('/api/rpc/asistencia_delete_raw', {
            method:'POST', headers:{'Content-Type':'application/json'},
            body: JSON.stringify({ id_raw })
            });
            if(!r.ok) throw new Error('asistencia_delete_raw failed');
            return await r.json();
        }

        // Render tablas
        function renderRaw(rows){
            const tb = $('#adm_tb_raw');
            if (!rows?.length){ tb.innerHTML = '<tr><td colspan="6" class="p-3 text-muted">Sin datos</td></tr>'; return; }
            tb.innerHTML = '';
            rows.forEach((r, idx) => {
            const tr = document.createElement('tr');
            tr.dataset.id = r.id_raw;
            tr.innerHTML = `
                <td>${idx+1}</td>
                <td><input class="form-control form-control-sm" type="date" value="${r.fecha}" disabled></td>
                <td><input class="form-control form-control-sm" type="time" step="1" value="${r.hora}" disabled></td>
                <td><input class="form-control form-control-sm" type="text" value="${r.modo || ''}" disabled></td>
                <td class="small text-muted">${r.origen || ''}</td>
                <td class="text-nowrap">
                <button class="btn btn-sm btn-outline-secondary btn-raw-edit">Editar</button>
                <button class="btn btn-sm btn-success d-none btn-raw-save">Guardar</button>
                <button class="btn btn-sm btn-outline-danger ms-1 btn-raw-del">Eliminar</button>
                </td>`;
            tb.appendChild(tr);
            });
        }

        function fmtHM(mins){ const h = Math.floor(mins/60); const m = Math.round(mins%60); return `${z2(h)}:${z2(m)}`; }
        function renderPairs(rows){
            const tb = $('#adm_tb_pairs');
            if (!rows?.length){ tb.innerHTML = '<tr><td colspan="4" class="p-3 text-muted">Sin datos</td></tr>'; return; }
            tb.innerHTML = '';
            rows.forEach(p => {
            const tr = document.createElement('tr');
            tr.innerHTML = `
                <td>${p.fecha}</td>
                <td class="text-end">${p.desde_hora}</td>
                <td class="text-end">${p.hasta_hora}</td>
                <td class="text-end">${fmtHM(Number(p.dur_min||0))}</td>`;
            tb.appendChild(tr);
            });
        }

        // Estado y carga
        async function loadAdm(){
            const doc = normDoc($('#adm_doc').value);
            const desde = $('#adm_from').value;
            const hasta = $('#adm_to').value;
            if (!doc || doc==='0'){ showError?.('Documento inválido'); return; }
            if (!desde || !hasta){ showError?.('Indicá rango de fechas'); return; }
            clearError?.();
            $('#adm_status').textContent = 'Cargando…';
            try{
            // Valida que exista el usuario
            const map = await fetchNamesForDocs([doc]);
            const hit = map?.[doc];
            if (!hit || (!hit.nombre && !hit.apellido)){
                showError?.('El documento no existe en la base.'); $('#adm_status').textContent = 'Error';
                return;
            }
            $('#adm_doc_help').textContent = `✓ ${hit.nombre||''} ${hit.apellido||''}`.trim();
            $('#adm_doc_help').classList.add('text-success');

            const data = await rpcGet(doc, desde, hasta);
            renderRaw(data.raw || []);
            renderPairs(data.intervalos || []);
            $('#adm_status').textContent = `${(data.raw||[]).length} marcas · ${(data.intervalos||[]).length} pares`;
            }catch(e){
            showError?.(e.message || 'Error al cargar asistencia'); $('#adm_status').textContent = 'Error';
            }
        }

        // Eventos
        document.getElementById('btnAdmLoad').addEventListener('click', loadAdm);

        // Delegado para editar/guardar/eliminar
        document.getElementById('adm_tb_raw').addEventListener('click', async (ev)=>{
            const btn = ev.target.closest('button'); if (!btn) return;
            const tr = ev.target.closest('tr'); if (!tr) return;
            const id = Number(tr.dataset.id);
            const [inpF, inpH, inpM] = tr.querySelectorAll('input');

            if (btn.classList.contains('btn-raw-edit')) {
            inpF.disabled = inpH.disabled = inpM.disabled = false;
            tr.querySelector('.btn-raw-edit').classList.add('d-none');
            tr.querySelector('.btn-raw-save').classList.remove('d-none');
            return;
            }

            if (btn.classList.contains('btn-raw-save')) {
            const fecha = inpF.value.trim();
            const hora  = inpH.value.trim();
            const modo  = inpM.value.trim() || null;
            if (!fecha || !hora){ showError?.('Completá fecha y hora'); return; }
            try{
                const res = await rpcUpdateRaw(id, fecha, hora, modo);
                inpF.disabled = inpH.disabled = inpM.disabled = true;
                tr.querySelector('.btn-raw-edit').classList.remove('d-none');
                tr.querySelector('.btn-raw-save').classList.add('d-none');
                // refrescar panel con los mismos filtros
                await loadAdm();
                // refrescar cards del resumen
                if (typeof loadResumenDiario30d === 'function') await loadResumenDiario30d();
            }catch(e){
                showError?.(e.message || 'Error al guardar cambio');
            }
            return;
            }

            if (btn.classList.contains('btn-raw-del')) {
            if (!confirm('¿Eliminar esta marca? Se recalcularán los pares del entorno.')) return;
            try{
                const res = await rpcDeleteRaw(id);
                await loadAdm();
                if (typeof loadResumenDiario30d === 'function') await loadResumenDiario30d();
            }catch(e){
                showError?.(e.message || 'Error al eliminar marca');
            }
            return;
            }
        });

        // Defaults de fecha (últimos 7 días)
        const today = new Date(); const d7 = new Date(today); d7.setDate(today.getDate()-7);
        $('#adm_from').value = today.toISOString().slice(0,10).replace(/\d{2}$/,(m)=> z2(Number(m)-7) ); // fallback simple
        $('#adm_from').value = d7.toISOString().slice(0,10);
        $('#adm_to').value   = today.toISOString().slice(0,10);
    })();

  const $ = (s, r=document)=>r.querySelector(s);
  const setStatus = t => $('#status').textContent = t || '';
  const pad2 = n => String(n).padStart(2,'0');
  const fmtHMS = d => `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
    const normDoc = s => {
        // solo dígitos y sin ceros a la izquierda
        const v = String(s||'').replace(/\D/g,'').replace(/^0+/,'');
        return v || '0';
    };
    const normTime = s => {
        if (!s) return '';
        // permite HH:MM o HH:MM:SS -> normaliza a HH:MM:SS
        const m = String(s).trim().match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
        if (!m) return '';
        return `${z2(+m[1])}:${z2(+m[2])}:${z2(+m[3]||0)}`;
    };

  function toUTCms(isoDate, time) {
    const [Y,M,D] = isoDate.split('-').map(n=>parseInt(n,10));
    const [h,m,s] = time.split(':').map(n=>parseInt(n,10));
    return Date.UTC(Y, (M||1)-1, D||1, h||0, m||0, s||0); // UTC fijo
  }
  function fmtHMSUTC(ms){
    const d = new Date(ms);
    const z = n => String(n).padStart(2,'0');
    return `${z(d.getUTCHours())}:${z(d.getUTCMinutes())}:${z(d.getUTCSeconds())}`;
  }

  async function jget(url){
    const r = await fetch(url);
    const j = await r.json().catch(()=>null);
    if (!r.ok) throw new Error(j?.error || `${r.status} ${r.statusText}`);
    return j;
  }

  async function importAsistencia(registros, origen){
    const r = await fetch('/api/rpc/import_asistencia', {
        method: 'POST',
        headers: {'Content-Type':'application/json'},
        body: JSON.stringify({ registros, origen })
    });
    if (!r.ok) throw new Error('Error al importar');
    return await r.json(); // {inserted_raw, inserted_pairs, missing_docs:[...]}
  }
    function showError(msg){ const el = document.getElementById('errorBox'); el.textContent = msg; el.classList.remove('d-none'); }
    function clearError(){ const el = document.getElementById('errorBox'); el.textContent = ''; el.classList.add('d-none'); }

    const z2 = n => String(n).padStart(2,'0');
    const fmtHM = mins => { const h = Math.floor(mins/60); const m = Math.round(mins%60); return `${z2(h)}:${z2(m)}`; };
    const ymd = s => String(s||'').slice(0,10);  // '2025-08-29T..' -> '2025-08-29'

    // Construye un SVG de barras simple (sin libs)
    function barsSVG(series /* [{x:'YYYY-MM-DD', h:Number}] */, maxH = null) {
    const W = 520, H = 80, PAD = 6, GAP = 3;
    const n = series.length || 1;
    const max = maxH ?? Math.max(1, ...series.map(d => d.h || 0));
    const bw = Math.max(2, Math.floor((W - PAD*2 - GAP*(n-1)) / n));
    let x = PAD;

    let bars = '';
    series.forEach(d => {
        const vh = max ? Math.round((d.h / max) * (H - PAD*2)) : 0;
        const y = H - PAD - vh;
        const label = `${d.x} · ${fmtHM((d.h || 0) * 60)}`;  // HH:MM
        bars += `
        <rect x="${x}" y="${y}" width="${bw}" height="${vh}" rx="2" ry="2"
                data-x="${d.x}" data-h="${d.h ?? 0}">
            <title>${label}</title>
        </rect>`;
        x += bw + GAP;
    });

    const axis = `<line x1="${PAD}" y1="${H-PAD}" x2="${W-PAD}" y2="${H-PAD}" stroke="#adb5bd" stroke-width="1"/>`;
    const css  = `rect{ fill:#0d6efd; }`;

    return `
        <svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" class="spark">
        <style>${css}</style>
        ${axis}
        ${bars}
        </svg>`;
    }
    
    // ---------- render de tarjetas ----------
    function renderResumenCards(grouped) {
    const container = document.getElementById('resumenCards');
    container.innerHTML = '';
    let cardCount = 0;

    for (const [key, data] of grouped.entries()) {
        cardCount++;
        const { doc, nombre, apellido, rows } = data;

        // serie (orden cronológico asc) con horas por día
        rows.sort((a,b)=> a.fecha.localeCompare(b.fecha));
        const series = rows.map(r => ({ x: r.fecha, h: Number(r.horas) || 0 }));
        const totalH = series.reduce((s,d)=> s + d.h, 0);
        const dias   = series.length;
        const avgH   = dias ? totalH / dias : 0;
        const pairs  = rows.reduce((s,r)=> s + (Number(r.pares)||0), 0);
        const last   = series.at(-1) || {x:'',h:0};

        const svg = barsSVG(series);

        const col = document.createElement('div');
        col.className = 'col-12 col-md-6 col-lg-4';
        col.innerHTML = `
        <div class="emp-card h-100">
            <div class="d-flex justify-content-between align-items-start mb-2">
            <div>
                <div class="fw-semibold">${nombre || ''} ${apellido || ''}</div>
                <div class="text-muted small">${doc}</div>
            </div>
            <div class="text-end">
                <div class="small text-muted">Total</div>
                <div class="fs-5 fw-semibold">${fmtHM(totalH*60)}</div>
            </div>
            </div>

            <div class="spark-wrap mb-1">${svg}</div>
            <div class="small text-muted legend">Pasá el mouse por una barra…</div>

            <div class="d-flex flex-wrap gap-1 emp-meta mt-2">
            <span class="badge">Días: ${dias}</span>
            <span class="badge">Prom: ${fmtHM(avgH*60)}</span>
            <span class="badge">Pares: ${pairs}</span>
            <span class="badge">Último: ${fmtHM((last.h||0)*60)} ${last.x ? `(${last.x})` : ''}</span>
            </div>
        </div>`;
        container.appendChild(col);
    }

    document.getElementById('resumeCount').textContent =
        `${grouped.size} empleado(s)`;
    }

    // Actualiza la leyenda al sobrevolar barras
    document.getElementById('resumenCards').addEventListener('mouseover', (e) => {
        const r = e.target;
        if (!(r instanceof SVGRectElement)) return;
        const card = r.closest('.emp-card');
        const legend = card?.querySelector('.legend');
        if (!legend) return;
        const x = r.getAttribute('data-x') || '';
        const h = Number(r.getAttribute('data-h') || 0);
        legend.textContent = `${x} · ${fmtHM(h*60)}`;
    });

    // manejar click “Guardar asistencia”
    document.getElementById('btnGuardarBD').addEventListener('click', async (ev) => {
        const btn = ev.currentTarget;
        // evita doble click si por alguna razón el disable no alcanza
        if (btn.dataset.busy === '1') return;
        if (!payloadDB?.length) {
            showError('Primero procesá un archivo para generar los registros.');
            return;
        }
        clearError();
        const f = document.getElementById('fileInput').files?.[0];
        const origen = f ? f.name : 'carga_manual';

        // bloquear UI mientras importa
        btn.dataset.busy = '1';
        const oldHtml = btn.innerHTML;
        btn.innerHTML = 'Guardando…';
        btn.disabled = true;
        setStatus('Guardando asistencia…');

        try {
            const res = await importAsistencia(payloadDB, origen); // llama a tu RPC
            if (res?.missing_docs?.length) {
                showError(`Faltan usuarios para documentos: ${res.missing_docs.join(', ')}`);
                return;
            }
                alert(`Importación OK.\nRaw insertados: ${res.inserted_raw}\nPares insertados: ${res.inserted_pairs}`);
                await (typeof loadResumenDiario30d === 'function' ? loadResumenDiario30d() : Promise.resolve());
                setStatus('Listo');
        } catch (e) {
            showError(e.message || 'Error al guardar asistencia');
            setStatus('Error al guardar');
        } finally {
            // restaurar UI
            btn.disabled = false;
            btn.dataset.busy = '';
            btn.innerHTML = oldHtml;
        }
    });

    function showError(msg){
    const el = document.getElementById('errorBox');
    el.textContent = msg;
    el.classList.remove('d-none');
    }
    function clearError(){
    const el = document.getElementById('errorBox');
    el.textContent = '';
    el.classList.add('d-none');
    }

    async function fetchNamesForDocs(docs){
        try {
            const r = await fetch('/api/rpc/find_usuarios_por_documentos', {
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify({ docs })
            });
            return await r.json(); // { "12345678": {nombre, apellido}, ... }
        } catch {
            return {};
        }
    }

    // ---------- carga de datos desde la vista SQL ----------
    async function loadResumenDiario30d(){
    // trae todo y filtra los últimos 30 días
    const rows = await jget('/api/table/asistencia_resumen_diario?limit=5000').catch(()=>[]);
    const today = new Date(); const cut = new Date(today); cut.setDate(today.getDate()-30);

    // agrupar por empleado
    const byKey = new Map();
    for (const r of (rows||[])) {
        // normalizar fecha (servidor puede mandar '...Z')
        const fStr = ymd(r.fecha);
        const fDt  = new Date(fStr);
        if (!(fDt >= cut)) continue;

        const key = `${r.documento}::${r.nombre||''}::${r.apellido||''}`;
        if (!byKey.has(key)) byKey.set(key, {
        doc: r.documento, nombre: r.nombre||'', apellido: r.apellido||'', rows: []
        });

        byKey.get(key).rows.push({
        fecha: fStr,
        horas: Number(r.horas_dia ?? r.horas ?? (r.minutos_dia||0)/60),
        pares: Number(r.pares_dia ?? r.pares ?? 0)
        });
    }

    renderResumenCards(byKey);
    }

    document.addEventListener('DOMContentLoaded', async () => {
        const badge = document.getElementById('resumeCount');
        try {
        if (badge) badge.textContent = 'Cargando…';
            await loadResumenDiario30d();
        if (badge) badge.textContent = 'Listo';
        } catch (e) {
        if (badge) badge.textContent = 'Error';
        if (typeof showError === 'function') {
            showError('No se pudo cargar el resumen diario: ' + (e?.message || e));
        }
        }
    });

  // Normaliza fecha "YY/MM/DD" o "YYYY/MM/DD" a "YYYY-MM-DD"
  function normDateStr(s) {
    const m = String(s || '').trim().match(/^(\d{2,4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/);
    if (!m) return null;
    let [_, y, mo, d] = m;
    let yy = parseInt(y, 10);
    if (y.length === 2) yy = 2000 + yy; // 20YY
    const mm = parseInt(mo, 10), dd = parseInt(d, 10);
    if (!(mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31)) return null;
    return `${yy}-${String(mm).padStart(2,'0')}-${String(dd).padStart(2,'0')}`;
  }
  // Normaliza documento quitando ceros a la izquierda
  const cleanDoc = s => {
    const v = String(s||'').trim().replace(/^0+/, '');
    return v === '' ? '0' : v;
  };


    (function attachManualForm(){
        const $ = s => document.querySelector(s);
        const frm = $('#frmManual');
        if (!frm) return;

        const $doc = $('#m_doc'), $inD=$('#m_in_date'), $inT=$('#m_in_time'),
                $outD=$('#m_out_date'), $outT=$('#m_out_time'),
                $fb = $('#m_feedback'), $btn = $('#btnManualSave'), $docHelp = $('#m_doc_help');

        let lastDocFound = null;

        async function validateDoc(){
            const d = normDoc($doc.value);
            if (!d || d==='0') {
            lastDocFound = null;
            $docHelp.textContent = 'Documento inválido.';
            $docHelp.classList.remove('text-success');
            $docHelp.classList.add('text-danger');
            return false;
            }
            // consulta a la API
            try{
            const map = await fetchNamesForDocs([d]);
            const hit = map?.[d];
            if (hit && (hit.nombre || hit.apellido)){
                lastDocFound = d;
                $docHelp.textContent = `✓ ${hit.nombre||''} ${hit.apellido||''}`.trim();
                $docHelp.classList.remove('text-danger');
                $docHelp.classList.add('text-success');
                return true;
            }
            lastDocFound = null;
            $docHelp.textContent = '✗ Documento no encontrado en la base. Creá el usuario antes de continuar.';
            $docHelp.classList.remove('text-success');
            $docHelp.classList.add('text-danger');
            return false;
            }catch(e){
            lastDocFound = null;
            $docHelp.textContent = 'Error validando documento.';
            $docHelp.classList.remove('text-success');
            $docHelp.classList.add('text-danger');
            return false;
            }
        }
        $doc.addEventListener('blur', validateDoc);

        frm.addEventListener('submit', async (ev)=>{
            ev.preventDefault();
            $fb.textContent = '';
            if ($btn.dataset.busy === '1') return;

            const doc = normDoc($doc.value);
            const inDate = ($inD.value||'').trim();
            const inTime = normTime($inT.value);
            const outDate = ($outD.value||'').trim();
            const outTime = normTime($outT.value);

            // validaciones básicas
            if (!(await validateDoc())) { showError?.('Documento inexistente.'); return; }
            if (!inDate || !inTime || !outDate || !outTime) { showError?.('Completá entrada y salida.'); return; }

            // entrada <= salida (permitimos cruzar medianoche)
            const tIn  = toUTCms(inDate,  inTime);
            const tOut = toUTCms(outDate, outTime);
            if (!(tOut > tIn)) { showError?.('La salida debe ser posterior a la entrada.'); return; }

            // armar payload para la función import_asistencia
            const registros = [
            { doc, isoDate: inDate,  time: inTime,  mode: 'IN'  },
            { doc, isoDate: outDate, time: outTime, mode: 'OUT' }
            ];
            const origen = 'manual_form';

            // bloquear botón mientras guardamos
            $btn.dataset.busy = '1'; const old = $btn.innerHTML; $btn.innerHTML = 'Guardando…'; $btn.disabled = true;
            clearError?.();
            try{
            const res = await importAsistencia(registros, origen);
            if (res?.missing_docs?.length) {
                showError?.(`Faltan usuarios para documentos: ${res.missing_docs.join(', ')}`);
                return;
            }
            // feedback
            $fb.textContent = `OK: Raw +${res.inserted_raw||0}, Pares +${res.inserted_pairs||0}`;
            // limpiar formulario
            // (dejamos documento como está para cargar más rápido varios días)
            $inD.value = ''; $inT.value = ''; $outD.value = ''; $outT.value = '';
            // refrescar tarjetas del resumen si existe
            if (typeof loadResumenDiario30d === 'function') await loadResumenDiario30d();
            } catch(e){
            showError?.(e?.message || 'Error al guardar el registro manual');
            } finally {
            $btn.disabled = false; $btn.dataset.busy = ''; $btn.innerHTML = old;
            }
        });
    })();





  // Parsea una línea con prioridad por TAB; si no hay, cae a espacios; también separa fecha/hora si vienen juntas.
  function parseLine(line) {
    const raw = String(line || '').replace(/\r/g, '').trim();
    if (!raw) return null;

    // omitir encabezado
    if (/^no[\t ]|^mchn[\t ]|^enno[\t ]|^name[\t ]|^datetime[\t ]/i.test(raw)) return null;

    let parts = raw.split(/\t+/);

    // Si no alcanzan 7 campos, intentar fallback con dos o más espacios
    if (parts.length < 7) {
      const dtMatch = raw.match(/(\d{2,4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})$/);
      if (dtMatch) {
        const head = raw.slice(0, dtMatch.index).trim();
        const headParts = head.split(/\t+|\s{2,}/).filter(Boolean);
        parts = [...headParts, dtMatch[1], dtMatch[2]];
      } else {
        parts = raw.split(/\s{2,}/).filter(Boolean);
      }
    }
    if (parts.length < 7) return null;

    // Indices base en archivos "normales"
    // 0:No, 1:Mchn, 2:EnNo(doc), 3:Name, 4:Mode, 5:Fecha, 6:Hora
    const DOC_IDX  = 2;
    const NAME_IDX = 3;
    const MODE_IDX = 4;

    const doc  = cleanDoc(parts[DOC_IDX]);
    const name = String(parts[NAME_IDX] || '').trim();
    const mode = String(parts[MODE_IDX] || '').trim();

    let dateStr = String(parts[5] || '').trim();
    let timeStr = String(parts[6] || '').trim();

    // ✅ Caso TU ARCHIVO: 7 columnas y la última es DateTime (fecha y hora juntas)
    // Detectamos un DateTime en la última columna y lo partimos.
    const last = parts[parts.length - 1];
    const dtBoth = /(\d{2,4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})/.exec(last);
    if (dtBoth) {
      dateStr = dtBoth[1];
      timeStr = dtBoth[2];
    } else {
      // Si la "fecha" ya trae hh:mm:ss pegado, separar ahí
      if (!timeStr && /\d{1,2}:\d{2}:\d{2}/.test(dateStr)) {
        const m = dateStr.match(/^(.+?)\s+(\d{1,2}:\d{2}:\d{2})$/);
        if (m) { dateStr = m[1]; timeStr = m[2]; }
      }
    }

    const iso = normDateStr(dateStr);               // YY/MM/DD o YYYY/MM/DD -> YYYY-MM-DD
    if (!iso || !/^\d{1,2}:\d{2}:\d{2}$/.test(timeStr)) return null;

    const [H, M, S] = timeStr.split(':').map(n => parseInt(n, 10));
    const dt = new Date(`${iso}T${String(H).padStart(2,'0')}:${String(M).padStart(2,'0')}:${String(S).padStart(2,'0')}`);

    return {
        doc, name,
        isoDate: iso,
        time: timeStr,
        dt_ms: toUTCms(iso, timeStr),   // ⬅️ clave
        mode
    };
  }

    function buildIntervalsCrossDay(rows){
        const byDoc = new Map();
        rows.forEach(r => {
            (byDoc.get(r.doc) || byDoc.set(r.doc, []).get(r.doc))
            .push({ ms: r.dt_ms, date: r.isoDate, name: r.name });
        });
        const out = [];
        for (const [doc, arr] of byDoc.entries()){
            arr.sort((a,b)=>a.ms-b.ms);
            for (let i=0;i<arr.length;i+=2){
            const a = arr[i], b = arr[i+1];
            if (!b){ out.push({doc, name:a.name, fecha:a.date, desde_ms:a.ms, hasta_ms:null, durMins:null, obs:'incompleto'}); break; }
            const dur = Math.max(0,(b.ms-a.ms)/60000);
            out.push({doc, name:a.name, fecha:a.date, desde_ms:a.ms, hasta_ms:b.ms, durMins:dur, obs:''});
            }
        }
        // ordenar por doc, fecha (inicio), desde
        out.sort((x,y)=> x.doc.localeCompare(y.doc) || x.fecha.localeCompare(y.fecha) || (x.desde_ms - y.desde_ms));
        return out;
    }

  // Agrupa por (doc, fecha) y arma pares (1-2, 3-4, ...)
    function buildIntervals(rows) {
        const nameByDoc = new Map();
        const byKey = new Map(); // doc|isoDate -> [ms]

        for (const r of rows) {
            nameByDoc.set(r.doc, r.name);
            const key = `${r.doc}|${r.isoDate}`;
            (byKey.get(key) || byKey.set(key, []).get(key)).push(r.dt_ms);
        }

        const result = [];
        for (const [key, arr] of byKey.entries()) {
            arr.sort((a,b)=>a-b);
            const [doc, isoDate] = key.split('|');
            const name = nameByDoc.get(doc) || '';
            for (let i=0; i<arr.length; i+=2) {
            const desde = arr[i];
            const hasta = arr[i+1] ?? null;
            let durMins = null, obs = '';
            if (hasta != null) durMins = Math.max(0, (hasta - desde)/60000);
            else obs = 'incompleto';
            result.push({ doc, name, isoDate, desde_ms: desde, hasta_ms: hasta, durMins, obs });
            }
        }

        result.sort((a,b)=>{
            if (a.doc !== b.doc) return a.doc.localeCompare(b.doc);
            if (a.isoDate !== b.isoDate) return a.isoDate.localeCompare(b.isoDate);
            return (a.desde_ms||0) - (b.desde_ms||0);
        });
        return result;
    }

  // Render tablas
  function renderCrudo(rows, badDocs = []) {
    const tb = $('#tbCrudo');
    if (!rows.length) {
        tb.innerHTML = '<tr><td colspan="6" class="p-3 text-muted">—</td></tr>';
        return;
    }
    const bad = new Set(badDocs);
    tb.innerHTML = '';
    rows.forEach((r, i)=>{
        const tr = document.createElement('tr');
        tr.innerHTML = `
        <td>${i+1}</td>
        <td>${r.doc}</td>
        <td>${r.name || '—'}</td>
        <td>${r.mode || '—'}</td>
        <td>${r.isoDate}</td>
        <td class="text-end">${r.time}</td>
        `;
        if (bad.has(r.doc)) tr.classList.add('table-danger');
        tb.appendChild(tr);
    });
  }


    function renderResumen(pairs){
        const tb = $('#tbResumen');
        if (!pairs.length){ tb.innerHTML='<tr><td colspan="7" class="p-3 text-muted">Sin datos</td></tr>'; return; }
        tb.innerHTML='';
        pairs.forEach(p=>{
            const tr=document.createElement('tr');
            tr.innerHTML = `
            <td>${p.doc}</td>
            <td>${p.name||'—'}</td>
            <td>${p.fecha}</td>
            <td class="text-end">${p.desde_ms!=null?fmtHMSUTC(p.desde_ms):'—'}</td>
            <td class="text-end">${p.hasta_ms!=null?fmtHMSUTC(p.hasta_ms):'—'}</td>
            <td class="text-end">${p.durMins!=null ? (m=>`${String(Math.floor(m/60)).padStart(2,'0')}:${String(Math.round(m%60)).padStart(2,'0')}`)(p.durMins) : '—'}</td>
            <td>${p.obs||''}</td>`;
            if (p.obs) tr.classList.add('table-warning');
            tb.appendChild(tr);
        });
    }



  function exportCSV(pairs) {
    if (!pairs.length) return;
    const head = ['documento','nombre','fecha','desde','hasta','duracion_hhmm','duracion_min','obs'];
    const rows = pairs.map(p => ([
      p.doc,
      p.name || '',
      p.isoDate,
      p.desde ? fmtHMS(p.desde) : '',
      p.hasta ? fmtHMS(p.hasta) : '',
      p.durMins!=null ? fmtHM(p.durMins) : '',
      p.durMins!=null ? Math.round(p.durMins) : '',
      p.obs || ''
    ].map(v => `"${String(v).replaceAll('"','""')}"`).join(',')));
    const csv = head.join(',') + '\n' + rows.join('\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = Object.assign(document.createElement('a'), { href:url, download:`usuarios_intervalos_${new Date().toISOString().slice(0,10)}.csv` });
    document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
  }

  // ===== Flujo principal =====
  let parsedRows = [];
  let payloadDB  = [];
  let pairs = [];

  async function processFile() {
    const f = $('#fileInput').files?.[0];
    if (!f) { setStatus('Elegí un .txt'); return; }
    setStatus('Leyendo archivo…');
    const text = await new Promise((res,rej)=>{
      const fr = new FileReader();
      fr.onload = () => res(String(fr.result||''));
      fr.onerror = () => rej(fr.error||new Error('No se pudo leer'));
      fr.readAsText(f, 'utf-8');
    });

    const lines = text.split(/\n/);
    const rows = [];
    for (const line of lines) {
      const r = parseLine(line);
      if (r) rows.push(r);
    }

    parsedRows = rows;

    const uniqueDocs = [...new Set(parsedRows.map(r => r.doc))];
    clearError();

    // pedir a la API los nombres/ apellidos
    setStatus(`Leyendo archivo… | consultando ${uniqueDocs.length} documentos…`);
    const map = await fetchNamesForDocs(uniqueDocs); // {"doc": {nombre, apellido}}

    // Documentos faltantes (usa 'found' si existe; si no, cae a heuristic de nombre/apellido vacíos)
    const missingDocs = uniqueDocs.filter(d => {
    const hit = map?.[d];
    if (!hit) return true;
    if (typeof hit.found === 'boolean') return !hit.found;
    // fallback (si no actualizaste la función SQL aún):
    return !(hit.nombre || '').trim() && !(hit.apellido || '').trim();
    });

    if (missingDocs.length) {
        // Pintar error, resaltar filas, y DETENER el proceso
        const msg = `No se encontraron ${missingDocs.length} documento(s) en la base: ${missingDocs.join(', ')}. ` +
                    `Por favor, creá/actualizá estos usuarios antes de continuar.`;
        showError(msg);
        renderCrudo(parsedRows, missingDocs);
        renderResumen([]);               // vacío
        $('#btnExport').disabled = true; // bloquear exportación
        setStatus('Hay documentos sin usuario. Corrigí y volvé a procesar.');
        return; // ← no seguimos hasta que se solucione
    }

    // sobreescribir name cuando la DB tenga datos
    parsedRows.forEach(r => {
        const hit = map?.[r.doc];
        if (hit && (hit.nombre || hit.apellido)) {
            r.name = `${hit.nombre || ''} ${hit.apellido || ''}`.trim();
        }
    });


    pairs = buildIntervalsCrossDay(parsedRows);

    renderCrudo(parsedRows);
    renderResumen(pairs);

    document.getElementById('btnExport').disabled   = pairs.length === 0;
    document.getElementById('btnGuardarBD').disabled = parsedRows.length === 0;

    payloadDB = parsedRows.map(r => ({
        doc: r.doc,             // sin ceros
        isoDate: r.isoDate,     // YYYY-MM-DD
        time: r.time,           // HH:MM:SS
        mode: r.mode || null
    }));

    $('#btnExport').disabled = pairs.length === 0;
    setStatus(`${parsedRows.length} registros · ${pairs.length} intervalos`);
  }

  // ===== Eventos =====
  $('#btnProcesar').addEventListener('click', processFile);
  $('#btnExport').addEventListener('click', ()=> exportCSV(pairs));

  // Hooks con sidebar (opcional)
  window.scRefreshList = () => { /* sin backend */ };
  window.scExportCsv = () => exportCSV(pairs);
</script>
