feat: accordion por hora, activo/finalizado, notas en detalle e informe domicilio

- enfermero_portal: cards colapsables con toggle en cabecera, ordenadas por hora_programada
- enfermero_portal: sección 'Finalizados' colapsada abajo para completado/cancelado
- enfermero_portal: acudiente en ficha clínica solo si fecha nacimiento confirma menor de edad
- lab_domicilios: notas del enfermero read-only en panel de detalle (cargarNotasDetalle)
- lab_domicilios: botón 'Informe' abre modal con datos del paciente + ficha clínica + notas libres
This commit is contained in:
Lizandro Guarnizo
2026-03-25 14:06:42 -05:00
parent 5f695b5604
commit da640680d3
2 changed files with 230 additions and 14 deletions
+66 -14
View File
@@ -166,6 +166,24 @@ $hoy = date('Y-m-d');
border-radius:10px; padding:10px; }
.nota-btn-guardar { width:100%; border:none; border-radius:10px; padding:13px;
font-weight:800; font-size:.92rem; color:#fff; cursor:pointer; }
/* ── Acordeón domicilios ── */
.domcard-header { cursor:pointer; user-select:none; display:flex; align-items:center;
justify-content:space-between; padding:8px 0 6px;
border-bottom:1px solid #f0f2f5; }
.domcard-header .dom-chev { transition:transform .2s; font-size:.68rem; color:#9ca3af;
flex-shrink:0; margin-left:6px; }
.domcard-header .dom-chev.open { transform:rotate(180deg); }
.domcard-body { display:none; padding-top:10px; }
.domcard-body.open { display:block; }
.fin-section-toggle { width:100%; background:#f3f4f6; border:1px solid #e5e7eb;
border-radius:10px; padding:9px 14px; font-size:.8rem;
font-weight:700; color:#6b7280; cursor:pointer; text-align:left;
display:flex; align-items:center; gap:8px; margin-top:6px; }
.fin-section-toggle .fin-chev { margin-left:auto; font-size:.68rem;
transition:transform .2s; }
.fin-section-toggle.open .fin-chev { transform:rotate(180deg); }
#seccion-fin { display:none; }
</style>
</head>
<body>
@@ -451,10 +469,29 @@ const portal = {
</div>`;
return;
}
lista.innerHTML = this._agenda.map(item => this._cardHTML(item)).join('');
const ACTIVOS = ['programado','confirmado','en_camino','en_domicilio'];
const CERRADOS = ['completado','cancelado'];
const sorted = [...this._agenda].sort((a,b) =>
(a.hora_programada||'99:99').localeCompare(b.hora_programada||'99:99'));
const activos = sorted.filter(i => ACTIVOS.includes(i.domicilio_estado||'programado'));
const cerrados = sorted.filter(i => CERRADOS.includes(i.domicilio_estado||''));
let html = activos.map((item, idx) => this._cardHTML(item, idx === 0)).join('');
if (cerrados.length) {
html += `<button class="fin-section-toggle" onclick="toggleFinalizados(this)">
<i class="fas fa-check-circle text-success"></i>
Finalizados (${cerrados.length})
<i class="fas fa-chevron-down fin-chev"></i>
</button>
<div id="seccion-fin">
${cerrados.map(item => this._cardHTML(item, false)).join('')}
</div>`;
}
lista.innerHTML = html;
},
_cardHTML(item) {
_cardHTML(item, expandido = false) {
const est = item.domicilio_estado || 'programado';
const info = ESTADOS_LABEL[est] || { label: est, icon:'📌', cls:'bg-secondary' };
const hora = item.hora_programada ? item.hora_programada.slice(0,5) : '—';
@@ -521,15 +558,16 @@ const portal = {
: '';
return `<div class="domcard estado-${est} mb-3 p-3" id="card-${item.domicilio_id}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<div class="domcard-header" onclick="toggleDomCard(this)">
<div class="d-flex align-items-center gap-2 flex-wrap" style="max-width:calc(100% - 22px)">
<span class="badge ${info.cls} text-white">${info.icon} ${info.label}</span>
<span class="ms-1 text-muted small">🕐 ${hora}</span>
<span class="text-muted small">🕐 ${hora}</span>
<span class="fw-semibold small" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(item.paciente_nombre || '—')}</span>
</div>
<small class="text-muted">#${item.domicilio_id}</small>
<i class="fas fa-chevron-down dom-chev ${expandido ? 'open' : ''}"></i>
</div>
<div class="fw-bold mb-1">
<div class="domcard-body ${expandido ? 'open' : ''}">
<div class="fw-bold mb-1 mt-2">
<i class="fas fa-user me-1 text-primary"></i>${esc(item.paciente_nombre || '—')}
</div>
<!-- Documento y fecha de nacimiento -->
@@ -642,6 +680,7 @@ const portal = {
<i class="fas fa-folder-open me-1"></i>Ver llenados
</button>` : ''}
</div>
</div>
</div>`;
},
@@ -756,6 +795,23 @@ function esc(s) {
({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;' }[c]));
}
// ── Acordeón domicilios ────────────────────────────────────────────────────
function toggleDomCard(headerEl) {
const body = headerEl.nextElementSibling;
const chev = headerEl.querySelector('.dom-chev');
const open = body.classList.toggle('open');
if (chev) chev.classList.toggle('open', open);
}
function toggleFinalizados(btnEl) {
const sec = document.getElementById('seccion-fin');
const chev = btnEl.querySelector('.fin-chev');
const open = btnEl.classList.toggle('open');
if (sec) sec.style.display = open ? '' : 'none';
if (chev) chev.classList.toggle('open', open);
}
// ──────────────────────────────────────────────────────────────────────────
// Resolver ruta de imagen: evita duplicar "uploads/media/"
function resImg(f) {
if (!f) return '';
@@ -2109,18 +2165,14 @@ const notasManager = {
<input type="text" id="ns-acud-doc" class="nota-input"
placeholder="Número de documento" value="${esc(fc.acudiente_documento||'')}">
</div>
${!esMenor ? `<label class="d-flex align-items-center gap-2 small text-muted mb-3" style="cursor:pointer">
<input type="checkbox" id="ns-menor-toggle" onchange="notasManager._toggleAcudiente(this.checked)"
${fc.acudiente_nombre ? 'checked' : ''}>
<span>¿Paciente menor de edad?</span>
</label>` : ''}
<button class="nota-btn-guardar" style="background:#0d6efd"
onclick="notasManager._guardarFicha()">
<i class="fas fa-save me-2"></i>Guardar ficha clínica
</button>`;
if (!esMenor && fc.acudiente_nombre) {
document.getElementById('ns-acudiente-bloque').classList.remove('d-none');
// paciente no reconocido como menor por fecha de nacimiento
// pero tenía datos del acudiente guardados → se mantienen ocultos
}
this._abrirSheet();
},