feat: tarjetas por toma con estados y barra de progreso

- 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 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-16 11:51:08 -05:00
co-authored by Claude Sonnet 4.6
parent 86ec692028
commit a2b46c464a
+205 -4
View File
@@ -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: <?= htmlspecialchars($docColor) ?>; }
.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; }
</style>
</head>
<body>
@@ -1609,9 +1649,10 @@ function aewSave(cid) {
} catch(e) {}
})();
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
window._mpSavedFirmas = {};
window._mpTomaData = {};
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
window._mpSavedFirmas = {};
window._mpTomaData = {};
window._mpPrimeraPendiente = <?= json_encode($_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'
? '<span class="toma-card-badge toma-badge-done"><i class="fas fa-check-circle me-1"></i>Firmado</span>'
: state === 'active'
? '<span class="toma-card-badge toma-badge-active"><i class="fas fa-pen me-1"></i>Firmar ahora</span>'
: '<span class="toma-card-badge toma-badge-wait"><i class="fas fa-hourglass-half me-1"></i>En espera</span>';
var extra = state === 'locked' ? '<span class="toma-card-cd" id="tc-cd-' + fid + '"></span>' : '';
var hora = state === 'signed' ? '<span class="toma-card-hora"></span>' : '';
return '<span class="toma-card-num">' + num + '</span>'
+ '<span class="toma-card-lbl">' + label + '</span>'
+ 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 '<span class="mp-step mp-step--' + cls + '" title="' + lbl.replace(/"/g,'&quot;') + '"></span>';
}).join('');
wrap.innerHTML = '<div class="mp-prog-hdr"><span class="mp-prog-text">Toma <strong>' + signedCount + '</strong> de <strong>' + total + '</strong></span><div class="mp-steps">' + steps + '</div></div>'
+ '<div class="mp-prog-bar"><div class="mp-prog-fill" style="width:' + pct + '%"></div></div>';
}
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 = '<div style="font-size:.75rem;opacity:.85;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">'
@@ -1859,6 +2058,8 @@ function _mpIniciarCountdown(targetMsOrMins, nextLabel, nextFirmaId) {
+ '<div style="font-size:2.4rem;font-weight:900;font-family:monospace;line-height:1;letter-spacing:.04em">'
+ String(m).padStart(2,'0') + '<span style="opacity:.6;animation:mp-pulse .8s infinite">:</span>' + String(s).padStart(2,'0')
+ '</div>';
// 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);
}