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:
+66
-14
@@ -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) {
|
||||
({ '<':'<','>':'>','&':'&','"':'"',"'":''' }[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();
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user