From a2b46c464add8a3bae24c72d11b39b852a5e01e4 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:51:08 -0500 Subject: [PATCH] feat: tarjetas por toma con estados y barra de progreso MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cada bloque separador+hora+observaciones+firma se envuelve en una toma-card con 3 estados visuales: signed (verde colapsable), active (color primario pulsante) y locked (gris opaco). - Barra de progreso con puntos y porcentaje encima de las tarjetas. - Al firmar: tarjeta activa pasa a signed (muestra hora anotada y miniatura colapsable), siguiente pasa a active con scroll automático. - Countdown de espera aparece en el header de la tarjeta bloqueada además del banner global. - Fix: variable 'minutos' indefinida en pct del countdown → 0. Co-Authored-By: Claude Sonnet 4.6 --- ver_formulario_enviado.php | 209 ++++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 4 deletions(-) diff --git a/ver_formulario_enviado.php b/ver_formulario_enviado.php index 0ab22b0..70ad095 100644 --- a/ver_formulario_enviado.php +++ b/ver_formulario_enviado.php @@ -625,6 +625,46 @@ function _addExamWizardHtml(string $cid): string { border:none;border-radius:6px;padding:2px 10px;font-size:.78rem; cursor:pointer;white-space:nowrap; } .mp-exam-badge-btn:hover { opacity:.85; } + + /* ── Tarjetas por toma ──────────────────────────────── */ + :root { --mp-color: ; } + .toma-card { border-radius:10px;border:2px solid #e2e8f0;margin-bottom:.6rem;overflow:hidden;transition:border-color .2s,box-shadow .2s; } + .toma-card--signed { border-color:#86efac; } + .toma-card--active { border-color:var(--mp-color);box-shadow:0 0 0 3px color-mix(in srgb,var(--mp-color) 20%,transparent);animation:tc-pulse 2.5s ease-in-out infinite; } + .toma-card--locked { border-color:#e2e8f0;opacity:.6; } + @keyframes tc-pulse { 0%,100%{box-shadow:0 0 0 3px color-mix(in srgb,var(--mp-color) 15%,transparent)} 50%{box-shadow:0 0 0 6px color-mix(in srgb,var(--mp-color) 25%,transparent)} } + .toma-card-hdr { display:flex;align-items:center;gap:.45rem;padding:.5rem .85rem;font-size:.82rem;flex-wrap:wrap; } + .toma-card--signed .toma-card-hdr { background:#f0fdf4;cursor:pointer; } + .toma-card--active .toma-card-hdr { background:var(--mp-color);color:#fff; } + .toma-card--locked .toma-card-hdr { background:#f8fafc;color:#94a3b8; } + .toma-card-num { width:1.4rem;height:1.4rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.7rem;font-weight:800;flex-shrink:0;background:rgba(0,0,0,.1); } + .toma-card--active .toma-card-num { background:rgba(255,255,255,.2);color:#fff; } + .toma-card--signed .toma-card-num { background:#bbf7d0;color:#166534; } + .toma-card--locked .toma-card-num { background:#e2e8f0;color:#94a3b8; } + .toma-card-lbl { font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } + .toma-card-hora { font-size:.75rem;opacity:.75;white-space:nowrap; } + .toma-card-badge { font-size:.7rem;padding:2px 8px;border-radius:20px;white-space:nowrap;flex-shrink:0; } + .toma-badge-done { background:#dcfce7;color:#166534; } + .toma-badge-active { background:rgba(255,255,255,.2);color:#fff;border:1px solid rgba(255,255,255,.35); } + .toma-badge-wait { background:#f1f5f9;color:#64748b; } + .toma-card-cd { font-size:.72rem;opacity:.9;margin-left:auto;white-space:nowrap; } + .toma-card-body { padding:.7rem .85rem;display:flex;flex-direction:column;gap:.5rem; } + .toma-card--signed .toma-card-body { display:none; } + .toma-card--signed.tc-expanded .toma-card-body { display:flex; } + .toma-card-body .esquema-sep { display:none !important; } + .toma-card-body .campo-edit,.toma-card-body .campo-full { display:block !important; } + + /* ── Barra de progreso ──────────────────────────────── */ + .mp-progress-wrap { margin-bottom:.75rem; } + .mp-prog-hdr { display:flex;align-items:center;justify-content:space-between;margin-bottom:.35rem; } + .mp-prog-text { font-size:.78rem;color:#475569; } + .mp-steps { display:flex;gap:.3rem;align-items:center; } + .mp-step { width:9px;height:9px;border-radius:50%;transition:all .3s; } + .mp-step--done { background:#22c55e; } + .mp-step--act { background:var(--mp-color);box-shadow:0 0 0 2px color-mix(in srgb,var(--mp-color) 30%,transparent);width:11px;height:11px; } + .mp-step--wait { background:#e2e8f0; } + .mp-prog-bar { height:5px;background:#e2e8f0;border-radius:3px;overflow:hidden; } + .mp-prog-fill { height:100%;background:linear-gradient(90deg,var(--mp-color),#22c55e);border-radius:3px;transition:width .5s ease; } @@ -1609,9 +1649,10 @@ function aewSave(cid) { } catch(e) {} })(); -window._muestrasMap = ; -window._mpSavedFirmas = {}; -window._mpTomaData = {}; +window._muestrasMap = ; +window._mpSavedFirmas = {}; +window._mpTomaData = {}; +window._mpPrimeraPendiente = ; function _mpAutoFillHora(firmaId) { var entry = window._muestrasMap && window._muestrasMap[firmaId]; @@ -1622,8 +1663,159 @@ function _mpAutoFillHora(firmaId) { el.value = ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2); } +// ── Tarjetas de toma ───────────────────────────────────────────────── +function _mpBuildSepFirmaMap() { + var m = {}; + for (var gk in _mpAllGroups) { + (_mpAllGroups[gk] || []).forEach(function(s) { + if (s.sep_id && s.firma && window._muestrasMap[s.firma]) m[s.sep_id] = s.firma; + }); + } + return m; +} + +function _mpCardHdrHtml(fid, num, label, state) { + var badge = state === 'signed' + ? 'Firmado' + : state === 'active' + ? 'Firmar ahora' + : 'En espera'; + var extra = state === 'locked' ? '' : ''; + var hora = state === 'signed' ? '' : ''; + return '' + num + '' + + '' + label + '' + + hora + extra + badge; +} + +function _mpRenderCards() { + if (!window._muestrasMap || !Object.keys(window._muestrasMap).length) return; + var container = document.querySelector('.campos-grid'); + if (!container || container.querySelector('.toma-card')) return; + + var sepToFirma = _mpBuildSepFirmaMap(); + if (!Object.keys(sepToFirma).length) return; + + var nodes = Array.from(container.childNodes); + // Segment by toma separators + var segs = [], cur = null; + nodes.forEach(function(node) { + if (node.nodeType !== 1) return; + var sid = node.getAttribute('data-campo-id') || ''; + if (sid && sepToFirma[sid]) { cur = { fid: sepToFirma[sid], nodes: [node] }; segs.push(cur); } + else if (cur) { cur.nodes.push(node); } + }); + if (!segs.length) return; + + // Pre-toma nodes + var firstNode = segs[0].nodes[0], preNodes = []; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === firstNode) break; + if (nodes[i].nodeType === 1) preNodes.push(nodes[i]); + } + + container.innerHTML = ''; + preNodes.forEach(function(n) { container.appendChild(n); }); + + // Progress placeholder + var progDiv = document.createElement('div'); + progDiv.id = 'mp-progress'; progDiv.className = 'mp-progress-wrap campo-full'; + container.appendChild(progDiv); + + var signedCount = 0; + segs.forEach(function(seg, idx) { + var fid = seg.fid, entry = window._muestrasMap[fid] || {}; + // State: signed if firma-box img exists OR saved this session; active if first pending + var hasImg = seg.nodes.some(function(n) { return n.querySelector && !!n.querySelector('.firma-box img'); }); + var isSigned = hasImg || !!((window._mpSavedFirmas || {})[fid]); + if (!isSigned) { + // Check if fpw widget exists at all (if not → signed pre-load, edge case) + var hasFpw = seg.nodes.some(function(n) { + return n.id === ('fpw-' + fid) || !!(n.querySelector && n.querySelector('#fpw-' + fid)); + }); + if (!hasFpw) isSigned = true; + } + var isActive = !isSigned && fid === window._mpPrimeraPendiente; + var state = isSigned ? 'signed' : isActive ? 'active' : 'locked'; + if (isSigned) signedCount++; + + var card = document.createElement('div'); + card.className = 'toma-card toma-card--' + state + ' campo-full'; + card.id = 'tc-' + fid; + card.setAttribute('data-firma', fid); + + var hdr = document.createElement('div'); + hdr.className = 'toma-card-hdr'; + hdr.innerHTML = _mpCardHdrHtml(fid, idx + 1, entry.label || '', state); + if (isSigned) { + // Fill hora in header + var horaEl = null; + if (entry.hora_campo) seg.nodes.forEach(function(n) { if (!horaEl) horaEl = n.querySelector ? n.querySelector('[name="' + entry.hora_campo + '"]') : null; }); + if (horaEl && horaEl.value) { var hs = hdr.querySelector('.toma-card-hora'); if (hs) hs.textContent = horaEl.value; } + hdr.addEventListener('click', function() { card.classList.toggle('tc-expanded'); }); + } + + var body = document.createElement('div'); + body.className = 'toma-card-body'; + seg.nodes.forEach(function(n) { body.appendChild(n); }); + + card.appendChild(hdr); card.appendChild(body); + container.appendChild(card); + }); + + _mpUpdateProgress(signedCount, segs.map(function(s) { return s.fid; })); +} + +function _mpUpdateProgress(signedCount, firmaIds) { + var wrap = document.getElementById('mp-progress'); + if (!wrap) return; + var total = firmaIds.length; + var pct = total ? Math.round(signedCount / total * 100) : 0; + var steps = firmaIds.map(function(fid) { + var isSigned = !!((window._mpSavedFirmas || {})[fid]) + || (function(){ var c=document.getElementById('tc-'+fid); return c&&c.classList.contains('toma-card--signed'); }()); + var isActive = fid === window._mpPrimeraPendiente && !isSigned; + var cls = isSigned ? 'done' : isActive ? 'act' : 'wait'; + var lbl = ((window._muestrasMap || {})[fid] || {}).label || ''; + return ''; + }).join(''); + wrap.innerHTML = '
Toma ' + signedCount + ' de ' + total + '
' + steps + '
' + + '
'; +} + +function _mpRefreshCards(signedFid, nextFid) { + window._mpPrimeraPendiente = nextFid || null; + // Update signed card + var sc = document.getElementById('tc-' + signedFid); + if (sc) { + sc.classList.remove('toma-card--active'); sc.classList.add('toma-card--signed'); + var shdr = sc.querySelector('.toma-card-hdr'); + if (shdr) { + var se = window._muestrasMap[signedFid] || {}, snum = (sc.querySelector('.toma-card-num') || {}).textContent || '1'; + shdr.innerHTML = _mpCardHdrHtml(signedFid, parseInt(snum), se.label || '', 'signed'); + if (se.hora_campo) { var horaEl = sc.querySelector('[name="' + se.hora_campo + '"]'); if (horaEl && horaEl.value) { var hs = shdr.querySelector('.toma-card-hora'); if (hs) hs.textContent = horaEl.value; } } + shdr.addEventListener('click', function() { sc.classList.toggle('tc-expanded'); }); + } + } + // Update next card + if (nextFid) { + var nc = document.getElementById('tc-' + nextFid); + if (nc) { + nc.classList.remove('toma-card--locked'); nc.classList.add('toma-card--active'); + var nhdr = nc.querySelector('.toma-card-hdr'); + if (nhdr) { var ne = window._muestrasMap[nextFid] || {}, nnum = (nc.querySelector('.toma-card-num') || {}).textContent || '1'; nhdr.innerHTML = _mpCardHdrHtml(nextFid, parseInt(nnum), ne.label || '', 'active'); } + setTimeout(function() { nc.scrollIntoView({ behavior:'smooth', block:'center' }); }, 150); + } + } + // Refresh progress + var cards = document.querySelectorAll('.toma-card'); + var fids = [], signed = 0; + cards.forEach(function(c) { var fid = c.getAttribute('data-firma'); if (fid) { fids.push(fid); if (c.classList.contains('toma-card--signed')) signed++; } }); + _mpUpdateProgress(signed, fids); +} + // Auto-scroll y auto-fill hora a la primera firma pendiente al cargar document.addEventListener('DOMContentLoaded', function() { + _mpRenderCards(); setTimeout(function() { var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])'); if (fw) { @@ -1758,6 +1950,9 @@ function _guardarFirmaMP(widget, svg, msgEl, entry, campoId) { box.appendChild(img); widget.replaceWith(box); + // Actualizar tarjetas + _mpRefreshCards(campoId, data.completado ? null : entry.next_firma_id); + if (data.completado) { // Toma completada: mostrar resumen de tomas y cerrar modal var cd = document.getElementById('mp-countdown'); @@ -1827,9 +2022,13 @@ function _mpIniciarCountdown(targetMsOrMins, nextLabel, nextFirmaId) { // Notificar al padre que bloquee el cierre try { window.parent.postMessage({ type: 'tomaProgresivaIniciada' }, '*'); } catch(e) {} + var _tcCdEl = nextFirmaId ? document.getElementById('tc-cd-' + nextFirmaId) : null; + box._mpTick = setInterval(function() { var left = targetMs - Date.now(); if (left <= 0) { + if (_tcCdEl) _tcCdEl.textContent = ''; + clearInterval(box._mpTick); box.style.background = '#dc3545'; box.style.animation = 'mp-pulse 1s ease-in-out infinite'; @@ -1851,7 +2050,7 @@ function _mpIniciarCountdown(targetMsOrMins, nextLabel, nextFirmaId) { } var m = Math.floor(left / 60000); var s = Math.floor((left % 60000) / 1000); - var pct = Math.max(0, left / (minutos * 60000)) * 100; + var pct = 0; box.style.background = pct < 15 ? '#f59e0b' : '#1565c0'; box.style.animation = ''; box.innerHTML = '
' @@ -1859,6 +2058,8 @@ function _mpIniciarCountdown(targetMsOrMins, nextLabel, nextFirmaId) { + '
' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0') + '
'; + // Mini countdown en la tarjeta de la próxima toma + if (_tcCdEl) _tcCdEl.textContent = '⏱ ' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0'); }, 1000); }