383 lines
20 KiB
PHP
383 lines
20 KiB
PHP
<?php
|
||
/**
|
||
* Gestión de Pacientes — Módulo Administrativo de Laboratorio
|
||
*/
|
||
require_once 'config/config.php';
|
||
|
||
if (!isUserLoggedIn()) {
|
||
header('Location: login.php');
|
||
exit;
|
||
}
|
||
if (isEnfermero()) { header('Location: enfermero_portal.php'); exit; }
|
||
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Admin';
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Pacientes — Módulo Lab</title>
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
|
||
<style>
|
||
.table-pacientes th { font-size:.8rem; text-transform:uppercase; letter-spacing:.04em; white-space:nowrap; }
|
||
.hist-badge { font-size:.72rem; }
|
||
#detail-panel { display:none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="sidebar">
|
||
<div class="sidebar-header"><h4><i class="fas fa-flask"></i> Módulo Lab</h4></div>
|
||
<ul class="sidebar-menu">
|
||
<li><a href="lab_dashboard.php" class="nav-link"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
|
||
<li><a href="lab_ordenes.php" class="nav-link"><i class="fas fa-file-medical"></i> Órdenes</a></li>
|
||
<li><a href="lab_pacientes.php" class="nav-link active"><i class="fas fa-users"></i> Pacientes</a></li>
|
||
<li><a href="lab_domicilios.php" class="nav-link"><i class="fas fa-house-medical"></i> Domicilios</a></li>
|
||
<li><a href="lab_enfermeras.php" class="nav-link"><i class="fas fa-user-nurse"></i> Enfermeras</a></li>
|
||
<li><a href="lab_formularios.php" class="nav-link"><i class="fas fa-wpforms"></i> Formularios</a></li>
|
||
<li><a href="lab_reportes.php" class="nav-link"><i class="fas fa-chart-bar"></i> Reportes</a></li>
|
||
<li><a href="lab_configuracion.php" class="nav-link"><i class="fas fa-sliders-h"></i> Configuración</a></li>
|
||
<li><a href="lab_usuarios.php" class="nav-link"><i class="fas fa-users-cog"></i> Usuarios</a></li>
|
||
<li class="sidebar-divider"></li>
|
||
<li><a href="index.php" class="nav-link"><i class="fas fa-arrow-left"></i> Volver al Bot</a></li>
|
||
<li><a href="logout.php" class="nav-link logout-link" onclick="return confirm('¿Cerrar sesión?')"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</a></li>
|
||
</ul>
|
||
</nav>
|
||
|
||
<main class="main-content">
|
||
<header class="content-header d-flex align-items-center justify-content-between">
|
||
<div>
|
||
<h1><i class="fas fa-users text-primary"></i> Pacientes</h1>
|
||
<small class="text-muted"><?= htmlspecialchars($adminNombre) ?></small>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="abrirFormulario()">
|
||
<i class="fas fa-plus me-1"></i> Nuevo Paciente
|
||
</button>
|
||
</header>
|
||
|
||
<div class="container-fluid py-3">
|
||
<!-- Buscador -->
|
||
<div class="row mb-3">
|
||
<div class="col-md-5">
|
||
<div class="input-group">
|
||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||
<input type="text" id="buscador" class="form-control" placeholder="Nombre, documento o teléfono…" oninput="debounce(cargarLista, 400)()">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-3">
|
||
<!-- Lista -->
|
||
<div class="col-lg-7" id="lista-col">
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover table-pacientes mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Paciente</th>
|
||
<th>Documento</th>
|
||
<th>Teléfono</th>
|
||
<th>EPS</th>
|
||
<th>Órdenes</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tabla-body">
|
||
<tr><td colspan="6" class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin me-2"></i>Cargando...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<!-- Paginación -->
|
||
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-top" id="paginacion"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel de detalle -->
|
||
<div class="col-lg-5" id="detail-panel">
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-white border-0 d-flex align-items-center justify-content-between">
|
||
<h6 class="mb-0 fw-semibold" id="detail-nombre">Paciente</h6>
|
||
<button class="btn-close" onclick="cerrarDetalle()"></button>
|
||
</div>
|
||
<div class="card-body" id="detail-body"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- /container -->
|
||
</main>
|
||
|
||
<!-- Modal Formulario Paciente -->
|
||
<div class="modal fade" id="modalPaciente" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="modal-titulo"><i class="fas fa-user me-2"></i>Nuevo Paciente</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="form-paciente">
|
||
<input type="hidden" name="id" id="pac-id">
|
||
<div class="row g-3">
|
||
<div class="col-md-8">
|
||
<label class="form-label">Nombre completo <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" name="nombre_completo" id="pac-nombre" required>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Tipo doc.</label>
|
||
<select class="form-select" name="tipo_documento" id="pac-tipo-doc">
|
||
<option value="CC">CC</option><option value="CE">CE</option>
|
||
<option value="TI">TI</option><option value="PA">PA</option>
|
||
<option value="NIT">NIT</option><option value="RC">RC</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Número de documento</label>
|
||
<input type="text" class="form-control" name="numero_documento" id="pac-doc">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Teléfono</label>
|
||
<input type="tel" class="form-control" name="telefono" id="pac-tel">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Email</label>
|
||
<input type="email" class="form-control" name="email" id="pac-email">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Fecha nacimiento</label>
|
||
<input type="date" class="form-control" name="fecha_nacimiento" id="pac-fnac">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Género</label>
|
||
<select class="form-select" name="genero" id="pac-genero">
|
||
<option value="">—</option>
|
||
<option value="M">Masculino</option>
|
||
<option value="F">Femenino</option>
|
||
<option value="O">Otro</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">EPS</label>
|
||
<input type="text" class="form-control" name="eps" id="pac-eps">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Ciudad</label>
|
||
<input type="text" class="form-control" name="ciudad" id="pac-ciudad">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Barrio</label>
|
||
<input type="text" class="form-control" name="barrio" id="pac-barrio">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Dirección</label>
|
||
<input type="text" class="form-control" name="direccion" id="pac-dir">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Notas internas</label>
|
||
<textarea class="form-control" name="notas_admin" id="pac-notas" rows="2"></textarea>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||
<button type="button" class="btn btn-primary" onclick="guardarPaciente()">
|
||
<i class="fas fa-save me-1"></i> Guardar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
let paginaActual = 1;
|
||
const modal = new bootstrap.Modal('#modalPaciente');
|
||
|
||
// ── Lista ──────────────────────────────────────────────────────────────────
|
||
async function cargarLista(pag = 1) {
|
||
paginaActual = pag;
|
||
const busq = document.getElementById('buscador').value.trim();
|
||
const r = await fetch(`api/lab/get_pacientes.php?busqueda=${encodeURIComponent(busq)}&page=${pag}&limit=25`);
|
||
const d = await r.json();
|
||
|
||
const tbody = document.getElementById('tabla-body');
|
||
if (!d.data?.length) {
|
||
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted">Sin resultados</td></tr>';
|
||
document.getElementById('paginacion').innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = d.data.map(p => `
|
||
<tr style="cursor:pointer" onclick="verDetalle(${p.id})">
|
||
<td>
|
||
<div class="fw-semibold">${esc(p.nombre_completo)}</div>
|
||
${p.phone_number ? `<small class="text-muted"><i class="fab fa-whatsapp text-success"></i> ${esc(p.phone_number)}</small>` : ''}
|
||
</td>
|
||
<td>${esc(p.tipo_documento)} ${esc(p.numero_documento||'—')}</td>
|
||
<td>${esc(p.telefono||'—')}</td>
|
||
<td>${esc(p.eps||'—')}</td>
|
||
<td><span class="badge bg-primary hist-badge">${p.total_ordenes||0}</span></td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation();editarPaciente(${p.id})" title="Editar">
|
||
<i class="fas fa-pen"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
// Paginación
|
||
const pag_html = [];
|
||
if (d.paginas > 1) {
|
||
pag_html.push(`<small class="text-muted">Mostrando ${((pag-1)*25)+1}–${Math.min(pag*25,d.total)} de ${d.total}</small>`);
|
||
pag_html.push(`<div class="btn-group btn-group-sm">`);
|
||
if (pag > 1) pag_html.push(`<button class="btn btn-outline-secondary" onclick="cargarLista(${pag-1})"><i class="fas fa-chevron-left"></i></button>`);
|
||
pag_html.push(`<button class="btn btn-secondary disabled">${pag}/${d.paginas}</button>`);
|
||
if (pag < d.paginas) pag_html.push(`<button class="btn btn-outline-secondary" onclick="cargarLista(${pag+1})"><i class="fas fa-chevron-right"></i></button>`);
|
||
pag_html.push(`</div>`);
|
||
} else {
|
||
pag_html.push(`<small class="text-muted">${d.total} paciente(s)</small>`);
|
||
}
|
||
document.getElementById('paginacion').innerHTML = pag_html.join('');
|
||
}
|
||
|
||
// ── Detalle ────────────────────────────────────────────────────────────────
|
||
async function verDetalle(id) {
|
||
document.getElementById('detail-panel').style.display = 'block';
|
||
document.getElementById('detail-body').innerHTML = '<div class="text-center py-3"><i class="fas fa-spinner fa-spin"></i></div>';
|
||
|
||
const r = await fetch(`api/lab/get_pacientes.php?id=${id}`);
|
||
const d = await r.json();
|
||
const p = d.data?.[0];
|
||
if (!p) return;
|
||
|
||
document.getElementById('detail-nombre').textContent = p.nombre_completo;
|
||
|
||
const r2 = await fetch(`api/lab/get_ordenes.php?paciente_id=${id}&limit=10&no_counts=1`);
|
||
const od = await r2.json();
|
||
const ords = od.data || [];
|
||
|
||
document.getElementById('detail-body').innerHTML = `
|
||
<dl class="row small mb-3">
|
||
<dt class="col-5 text-muted">Documento</dt><dd class="col-7">${esc(p.tipo_documento)} ${esc(p.numero_documento||'—')}</dd>
|
||
<dt class="col-5 text-muted">Teléfono</dt><dd class="col-7">${esc(p.telefono||'—')}</dd>
|
||
<dt class="col-5 text-muted">WhatsApp</dt><dd class="col-7">${p.phone_number ? `<i class="fab fa-whatsapp text-success"></i> ${esc(p.phone_number)}` : '—'}</dd>
|
||
${p.email ? `<dt class="col-5 text-muted">Email</dt><dd class="col-7">${esc(p.email)}</dd>` : ''}
|
||
${p.fecha_nacimiento ? `<dt class="col-5 text-muted">Nacimiento</dt><dd class="col-7">${esc(p.fecha_nacimiento.slice(0,10))}</dd>` : ''}
|
||
${p.genero ? `<dt class="col-5 text-muted">Género</dt><dd class="col-7">${p.genero==='M'?'Masculino':p.genero==='F'?'Femenino':'Otro'}</dd>` : ''}
|
||
<dt class="col-5 text-muted">EPS</dt><dd class="col-7">${esc(p.eps||'—')}</dd>
|
||
<dt class="col-5 text-muted">Ciudad</dt><dd class="col-7">${esc(p.ciudad||'—')}</dd>
|
||
${p.barrio ? `<dt class="col-5 text-muted">Barrio</dt><dd class="col-7">${esc(p.barrio)}</dd>` : ''}
|
||
<dt class="col-5 text-muted">Dirección</dt><dd class="col-7">${esc(p.direccion||'—')}</dd>
|
||
${p.notas_admin ? `<dt class="col-5 text-muted">Notas</dt><dd class="col-7 text-muted fst-italic">${esc(p.notas_admin)}</dd>` : ''}
|
||
</dl>
|
||
<hr>
|
||
<h6 class="fw-semibold mb-2"><i class="fas fa-file-medical me-2 text-primary"></i>Órdenes médicas (${ords.length})</h6>
|
||
${ords.length ? `
|
||
<table class="table table-sm table-hover">
|
||
<thead class="table-light"><tr><th>#</th><th>Fecha</th><th>Estado</th></tr></thead>
|
||
<tbody>
|
||
${ords.map(o => `<tr onclick="location.href='lab_ordenes.php?id=${o.id}'" style="cursor:pointer">
|
||
<td>#${o.id}</td>
|
||
<td>${formatFecha(o.created_at)}</td>
|
||
<td><span class="badge bg-${colorEstado(o.estado)}">${esc(o.estado)}</span></td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table>` : '<p class="text-muted small">Sin órdenes</p>'}
|
||
<button class="btn btn-sm btn-outline-primary w-100 mt-2" onclick="editarPaciente(${p.id})">
|
||
<i class="fas fa-pen me-1"></i> Editar datos
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
function cerrarDetalle() {
|
||
document.getElementById('detail-panel').style.display = 'none';
|
||
}
|
||
|
||
// ── Formulario ─────────────────────────────────────────────────────────────
|
||
function abrirFormulario(p = null) {
|
||
const form = document.getElementById('form-paciente');
|
||
form.reset();
|
||
document.getElementById('pac-id').value = p ? p.id : '';
|
||
document.getElementById('modal-titulo').innerHTML = p
|
||
? `<i class="fas fa-pen me-2"></i>Editar Paciente`
|
||
: `<i class="fas fa-plus me-2"></i>Nuevo Paciente`;
|
||
|
||
if (p) {
|
||
document.getElementById('pac-nombre').value = p.nombre_completo || '';
|
||
document.getElementById('pac-doc').value = p.numero_documento || '';
|
||
document.getElementById('pac-tipo-doc').value= p.tipo_documento || 'CC';
|
||
document.getElementById('pac-tel').value = p.telefono || '';
|
||
document.getElementById('pac-email').value = p.email || '';
|
||
document.getElementById('pac-fnac').value = p.fecha_nacimiento || '';
|
||
document.getElementById('pac-genero').value = p.genero || '';
|
||
document.getElementById('pac-eps').value = p.eps || '';
|
||
document.getElementById('pac-ciudad').value = p.ciudad || '';
|
||
document.getElementById('pac-barrio').value = p.barrio || '';
|
||
document.getElementById('pac-dir').value = p.direccion || '';
|
||
document.getElementById('pac-notas').value = p.notas_admin || '';
|
||
}
|
||
modal.show();
|
||
}
|
||
|
||
async function editarPaciente(id) {
|
||
const r = await fetch(`api/lab/get_pacientes.php?id=${id}`);
|
||
const d = await r.json();
|
||
if (d.data?.[0]) abrirFormulario(d.data[0]);
|
||
}
|
||
|
||
async function guardarPaciente() {
|
||
const form = document.getElementById('form-paciente');
|
||
if (!form.checkValidity()) { form.reportValidity(); return; }
|
||
|
||
const datos = Object.fromEntries(new FormData(form).entries());
|
||
if (!datos.id) delete datos.id;
|
||
|
||
const r = await fetch('api/lab/save_paciente.php', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(datos),
|
||
});
|
||
const d = await r.json();
|
||
|
||
if (d.success) {
|
||
modal.hide();
|
||
mostrarToast(d.message, 'success');
|
||
cargarLista(paginaActual);
|
||
} else {
|
||
mostrarToast(d.error || 'Error al guardar', 'danger');
|
||
}
|
||
}
|
||
|
||
// ── Utils ──────────────────────────────────────────────────────────────────
|
||
const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c]));
|
||
const formatFecha = s => s ? new Date(s).toLocaleDateString('es-CO') : '—';
|
||
const colorEstado = e => ({
|
||
pendiente:'warning', en_revision:'info', autorizada:'success',
|
||
rechazada:'danger', en_domicilio:'primary', completada:'secondary',
|
||
}[e] || 'secondary');
|
||
|
||
function debounce(fn, ms) {
|
||
let t;
|
||
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
||
}
|
||
|
||
function mostrarToast(msg, tipo = 'success') {
|
||
const div = document.createElement('div');
|
||
div.className = `alert alert-${tipo} alert-dismissible position-fixed bottom-0 end-0 m-3`;
|
||
div.style.zIndex = 9999;
|
||
div.innerHTML = `${esc(msg)}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
|
||
document.body.appendChild(div);
|
||
setTimeout(() => div.remove(), 4000);
|
||
}
|
||
|
||
// Init
|
||
cargarLista();
|
||
</script>
|
||
<script src="assets/js/lab-sidebar.js"></script>
|
||
</body>
|
||
</html>
|