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();
},
+164
View File
@@ -140,6 +140,10 @@ $domIdParam = (int)($_GET['id'] ?? 0);
<div class="card-header bg-white border-0 d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-semibold">Detalle <span id="detail-id" class="text-muted"></span></h6>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-success d-none" id="btn-informe-dom"
onclick="verInformeDom()">
<i class="fas fa-file-medical me-1"></i>Informe
</button>
<button class="btn btn-sm btn-outline-primary d-none" id="btn-editar-dom"
onclick="editarDomicilio(domSeleccionado)">
<i class="fas fa-edit me-1"></i>Editar
@@ -412,6 +416,52 @@ $domIdParam = (int)($_GET['id'] ?? 0);
</div>
</div>
<!-- Modal Informe Domicilio -->
<div class="modal fade" id="modalInformeDom" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-file-medical me-2 text-success"></i>Informe de domicilio</h5>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" onclick="window.print()">
<i class="fas fa-print me-1"></i>Imprimir
</button>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="modal-body" id="informe-body">
<!-- Datos del paciente -->
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-user me-1"></i>Datos del paciente</h6>
<dl class="row small mb-3">
<dt class="col-4">Nombre</dt> <dd class="col-8 fw-semibold" id="inf-nombre"></dd>
<dt class="col-4">Tipo doc.</dt> <dd class="col-8" id="inf-tipodoc"></dd>
<dt class="col-4">Nº documento</dt> <dd class="col-8" id="inf-numdoc"></dd>
<dt class="col-4">Teléfono</dt> <dd class="col-8" id="inf-telefono"></dd>
<dt class="col-4">Correo</dt> <dd class="col-8" id="inf-correo"></dd>
<dt class="col-4">Dirección</dt> <dd class="col-8" id="inf-direccion"></dd>
<dt class="col-4">Seguro/tipo</dt> <dd class="col-8" id="inf-seguro"></dd>
<dt class="col-4">Fecha servicio</dt><dd class="col-8" id="inf-fecha"></dd>
<dt class="col-4">Exámenes</dt> <dd class="col-8" id="inf-examenes"></dd>
</dl>
<hr>
<!-- Ficha clínica -->
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-clipboard-list me-1"></i>Ficha clínica</h6>
<dl class="row small mb-3">
<dt class="col-4">Antecedentes</dt> <dd class="col-8" id="inf-antecedentes"></dd>
<dt class="col-4">Medicamentos</dt> <dd class="col-8" id="inf-medicamentos"></dd>
<dt class="col-4 d-none" id="inf-fila-acudiente">Acudiente</dt>
<dd class="col-8 d-none" id="inf-acudiente"></dd>
</dl>
<hr>
<!-- Notas libres -->
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-sticky-note me-1"></i>Notas del enfermero</h6>
<div id="inf-notas-libres"></div>
</div>
</div>
</div>
</div>
<!-- Modal Asignar / Reasignar Enfermero -->
<div class="modal fade" id="modalAsignarEnf" tabindex="-1">
<div class="modal-dialog">
@@ -534,6 +584,7 @@ function domQuitarOrden() {
}
const COLOR_DOM = { programado:'secondary', confirmado:'info', en_camino:'primary', en_domicilio:'warning', completado:'success', cancelado:'danger', reprogramado:'dark' };
let domSeleccionado = <?= $domIdParam ?: 'null' ?>;
let _domActual = null; // objeto completo del domicilio activo en el panel de detalle
let paginaActual = 1;
let _listaToken = 0; // anti race-condition
@@ -643,11 +694,13 @@ async function verDomicilio(id) {
document.getElementById('detail-id').textContent = `#${id}`;
document.getElementById('detail-body').innerHTML = '<div class="text-center py-3"><i class="fas fa-spinner fa-spin"></i></div>';
document.getElementById('btn-editar-dom').classList.remove('d-none');
document.getElementById('btn-informe-dom').classList.remove('d-none');
const r = await fetch(`api/lab/get_domicilios.php?id=${id}&no_stats=1`);
const d = await r.json();
const dom = d.domicilio;
if (!dom) return;
_domActual = dom;
// Botones de estado
const SIGUIENTES = {
@@ -758,16 +811,127 @@ async function verDomicilio(id) {
</div>
</div>` : ''}
`;
cargarNotasDetalle(id);
}
function cerrarDetalle() {
domSeleccionado = null;
_domActual = null;
document.querySelectorAll('.dom-row').forEach(r => r.classList.remove('table-active'));
document.getElementById('detail-id').textContent = '';
document.getElementById('btn-editar-dom').classList.add('d-none');
document.getElementById('btn-informe-dom').classList.add('d-none');
document.getElementById('detail-body').innerHTML = '<div class="text-center py-5 text-muted"><i class="fas fa-arrow-left me-2"></i>Selecciona un domicilio</div>';
}
// ── Notas del enfermero (read-only en detalle) ────────────────────────────
async function cargarNotasDetalle(domId) {
try {
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${domId}`);
const d = await r.json();
if (!d.success) return;
const c = d.clinica || {};
const libres = d.libres || [];
const hayFicha = c.antecedentes || c.medicamentos || c.acudiente_nombre;
const hayLibres = libres.length > 0;
if (!hayFicha && !hayLibres) return;
let html = `<div class="row mt-3" id="notas-enfermero-bloque">
<div class="col-12">
<hr class="my-2">
<label class="form-label small text-muted text-uppercase">
<i class="fas fa-notes-medical me-1"></i>Notas del enfermero
</label>`;
if (hayFicha) {
html += `<div class="p-2 rounded mb-2" style="background:#fff7ed;border:1px solid #fdba74">
<div class="fw-semibold small mb-1" style="color:#9a3412">
<i class="fas fa-clipboard-list me-1"></i>Ficha clínica
</div>`;
if (c.antecedentes) html += `<div class="small mb-1"><strong>Antecedentes:</strong> ${esc(c.antecedentes)}</div>`;
if (c.medicamentos) html += `<div class="small mb-1"><strong>Medicamentos:</strong> ${esc(c.medicamentos)}</div>`;
if (c.acudiente_nombre) html += `<div class="small mb-1"><strong>Acudiente:</strong> ${esc(c.acudiente_nombre)}${c.acudiente_documento ? ' · ' + esc(c.acudiente_documento) : ''}</div>`;
html += `</div>`;
}
libres.forEach(n => {
html += `<div class="p-2 rounded mb-2" style="background:#f8fafc;border:1px solid #e2e8f0">
<div class="fw-semibold small">${esc(n.titulo||'Sin título')}</div>
<div class="small mt-1" style="white-space:pre-line">${n.cuerpo||''}</div>
${n.imagen_path ? `<img src="${esc(n.imagen_path)}" class="img-fluid rounded mt-1" style="max-height:160px">` : ''}
<div class="text-muted mt-1" style="font-size:.7rem">${esc(n.created_at||'')}</div>
</div>`;
});
html += `</div></div>`;
document.getElementById('detail-body').insertAdjacentHTML('beforeend', html);
} catch(e) { /* silencioso */ }
}
// ── Informe del domicilio ─────────────────────────────────────────────────
async function verInformeDom() {
if (!_domActual) return;
const dom = _domActual;
// Llenar datos básicos del paciente
document.getElementById('inf-nombre').textContent = dom.paciente_nombre || '—';
document.getElementById('inf-tipodoc').textContent = dom.paciente_tipo_documento || '—';
document.getElementById('inf-numdoc').textContent = dom.paciente_numero_documento || '—';
document.getElementById('inf-telefono').textContent= dom.paciente_telefono || '—';
document.getElementById('inf-correo').textContent = dom.paciente_email || '—';
document.getElementById('inf-direccion').textContent = (dom.direccion||'') + (dom.barrio ? ', ' + dom.barrio : '');
document.getElementById('inf-seguro').textContent = dom.tipo_cliente === 'seguro'
? ('Seguro' + (dom.seguro_nombre ? ' — ' + dom.seguro_nombre : ''))
: 'Particular';
document.getElementById('inf-examenes').textContent = dom.examenes_solicitados || '—';
document.getElementById('inf-fecha').textContent = (dom.fecha_programada||'') + ' ' + (dom.hora_programada||'');
// Limpiar campos clínicos mientras cargamos
['inf-antecedentes','inf-medicamentos','inf-acudiente','inf-notas-libres'].forEach(id => {
const el = document.getElementById(id);
if (el) el.innerHTML = '<em class="text-muted">Cargando...</em>';
});
const _modalInforme = bootstrap.Modal.getOrCreateInstance('#modalInformeDom');
_modalInforme.show();
try {
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${dom.id}`);
const d = await r.json();
const c = d.success ? (d.clinica||{}) : {};
const libres = d.success ? (d.libres||[]) : [];
document.getElementById('inf-antecedentes').textContent = c.antecedentes || '—';
document.getElementById('inf-medicamentos').textContent = c.medicamentos || '—';
if (c.acudiente_nombre) {
document.getElementById('inf-acudiente').innerHTML =
`<strong>${esc(c.acudiente_nombre)}</strong> · ${esc(c.acudiente_documento||'')}`;
document.getElementById('inf-fila-acudiente').classList.remove('d-none');
} else {
document.getElementById('inf-fila-acudiente').classList.add('d-none');
document.getElementById('inf-acudiente').textContent = '';
}
if (libres.length) {
document.getElementById('inf-notas-libres').innerHTML = libres.map(n =>
`<div class="mb-2 p-2 rounded" style="background:#f8fafc;border:1px solid #e2e8f0">
<div class="fw-semibold small">${esc(n.titulo||'Sin título')}</div>
<div class="small mt-1" style="white-space:pre-line">${n.cuerpo||''}</div>
${n.imagen_path ? `<img src="${esc(n.imagen_path)}" class="img-fluid rounded mt-1" style="max-height:120px">` : ''}
</div>`
).join('');
} else {
document.getElementById('inf-notas-libres').textContent = '—';
}
} catch(e) {
document.getElementById('inf-antecedentes').textContent = '—';
document.getElementById('inf-medicamentos').textContent = '—';
document.getElementById('inf-notas-libres').textContent = '—';
}
}
// ──────────────────────────────────────────────────────────────────────────
// ── Asignación rápida ──────────────────────────────────────────────────────
async function mostrarAsignacion(domId) {
document.getElementById('asig-dom-id').value = domId;