fix: lab_pacientes.php — corregir comentarios Blade y mejorar diseño modal

- Eliminar comentarios {{-- --}} de Blade que se renderizaban como texto visible
- Inyectar --brand/--brand-dark desde doc_color de BD (igual que Layout.php)
- Modal header con gradiente brand igual que login/sidebar
- Secciones de formulario con separadores visuales (Datos, Contacto, Ubicación)
- Panel detalle con header brand en lugar de card-header blanco
- Actualizar version CSS a v=13

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-23 20:18:07 -05:00
co-authored by Claude Sonnet 4.6
parent bec4629a3c
commit a5d42a54be
+162 -93
View File
@@ -11,6 +11,19 @@ if (!isUserLoggedIn()) {
}
if (isEnfermero()) { header('Location: enfermero_portal.php'); exit; }
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Admin';
// Color de marca
$brandColor = '#1565c0';
try {
$row = Database::getInstance()->fetch("SELECT valor FROM lab_config WHERE clave = 'doc_color'");
if ($row && preg_match('/^#[0-9a-fA-F]{3,8}$/', $row['valor'])) $brandColor = $row['valor'];
} catch (\Throwable $_) {}
$hex = ltrim($brandColor, '#');
$brandDark = sprintf('#%02x%02x%02x',
max(0, hexdec(substr($hex,0,2)) - 40),
max(0, hexdec(substr($hex,2,2)) - 40),
max(0, hexdec(substr($hex,4,2)) - 40)
);
?>
<!DOCTYPE html>
<html lang="es">
@@ -20,11 +33,70 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
<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">
<link href="./assets/css/styles.css?v=13" 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; }
:root {
--brand: <?= htmlspecialchars($brandColor, ENT_QUOTES, 'UTF-8') ?>;
--brand-dark: <?= htmlspecialchars($brandDark, ENT_QUOTES, 'UTF-8') ?>;
--sidebar-width: 270px;
}
body { overflow-x: hidden; }
.main-content {
margin-left: var(--sidebar-width);
min-height: 100vh;
background: #f8f9fa;
}
@media (max-width: 991.98px) { .main-content { margin-left: 0; } }
.table-pacientes th {
font-size: .78rem;
text-transform: uppercase;
letter-spacing: .05em;
white-space: nowrap;
color: #64748b;
}
.hist-badge { font-size: .72rem; }
#detail-panel { display: none; }
/* Modal header con gradiente brand */
.modal-header-brand {
background: linear-gradient(135deg, var(--brand-dark) 0%, var(--brand) 100%);
color: #fff;
border-radius: .5rem .5rem 0 0;
padding: 1rem 1.25rem;
}
.modal-header-brand .modal-title { color: #fff; font-size: .95rem; font-weight: 700; }
.modal-header-brand .btn-close { filter: invert(1) brightness(2); }
/* Secciones del formulario */
.form-section-label {
font-size: .68rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .1em;
color: var(--brand);
margin: .75rem 0 .4rem;
padding-bottom: .25rem;
border-bottom: 1px solid color-mix(in srgb, var(--brand) 20%, transparent);
display: flex;
align-items: center;
gap: .4rem;
}
/* Panel detalle */
.detail-header-band {
background: linear-gradient(135deg, var(--brand-dark) 0%, var(--brand) 100%);
color: #fff;
border-radius: .5rem .5rem 0 0;
padding: .75rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-header-band .btn-close { filter: invert(1) brightness(2); }
/* Row hover en tabla */
.table-hover tbody tr:hover { background: color-mix(in srgb, var(--brand) 6%, #fff) !important; }
</style>
</head>
<body>
@@ -37,12 +109,12 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<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 class="d-flex align-items-center gap-2">
<h1 class="mb-0"><i class="fas fa-users" style="color:var(--brand)"></i> Pacientes</h1>
<small class="text-muted d-none d-sm-inline"><?= htmlspecialchars($adminNombre) ?></small>
</div>
<button class="btn btn-primary btn-sm" onclick="abrirFormulario()">
<i class="fas fa-plus me-1"></i> Nuevo Paciente
<button class="btn btn-sm" style="background:var(--brand);color:#fff" onclick="abrirFormulario()">
<i class="fas fa-plus me-1"></i> Nuevo
</button>
</header>
@@ -51,8 +123,10 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<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)()">
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
<input type="text" id="buscador" class="form-control border-start-0 ps-0"
placeholder="Nombre, documento o teléfono…"
oninput="debounce(cargarLista, 380)()">
</div>
</div>
</div>
@@ -60,26 +134,27 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<div class="row g-3">
<!-- Lista -->
<div class="col-lg-7" id="lista-col">
<div class="card border-0 shadow-sm">
<div class="card border-0 shadow-sm rounded-3">
<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 class="ps-3">Paciente</th>
<th>Documento</th>
<th>Teléfono</th>
<th>EPS</th>
<th>Órdenes</th>
<th>Órd.</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>
<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>
@@ -87,39 +162,38 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<!-- 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>
<div class="card border-0 shadow-sm rounded-3 overflow-hidden">
<div class="detail-header-band">
<span class="fw-semibold" id="detail-nombre">Paciente</span>
<button class="btn-close" onclick="cerrarDetalle()"></button>
</div>
<div class="card-body" id="detail-body"></div>
</div>
</div>
</div>
</div><!-- /container -->
</div>
</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 py-2">
<h6 class="modal-title" id="modal-titulo"><i class="fas fa-user me-2 text-primary"></i>Nuevo Paciente</h6>
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content border-0 shadow rounded-3 overflow-hidden">
<div class="modal-header-brand">
<h6 class="modal-title" id="modal-titulo"><i class="fas fa-user me-2"></i>Nuevo Paciente</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-3">
<form id="form-paciente">
<div class="modal-body py-3 px-4">
<form id="form-paciente" autocomplete="off">
<input type="hidden" name="id" id="pac-id">
<div class="row g-2">
{{-- Nombre completo --}}
<!-- Datos personales -->
<div class="form-section-label"><i class="fas fa-id-card"></i> Datos personales</div>
<div class="row g-2">
<div class="col-12">
<label class="form-label small fw-semibold mb-1">Nombre completo <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-sm" name="nombre_completo" id="pac-nombre" required placeholder="Ej: Juan Pérez">
<input type="text" class="form-control form-control-sm" name="nombre_completo" id="pac-nombre" required placeholder="Ej: María García López">
</div>
{{-- Documento --}}
<div class="col-4">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">Tipo doc.</label>
<select class="form-select form-select-sm" name="tipo_documento" id="pac-tipo-doc">
<option value="CC">CC — Cédula</option>
@@ -131,20 +205,23 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<option value="RC">RC — Reg. Civil</option>
</select>
</div>
<div class="col-4">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">Número documento</label>
<input type="text" class="form-control form-control-sm" name="numero_documento" id="pac-doc" placeholder="123456789">
</div>
<div class="col-4">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">Fecha nacimiento</label>
<input type="date" class="form-control form-control-sm" name="fecha_nacimiento" id="pac-fnac">
</div>
</div>
{{-- Contacto --}}
<div class="col-5">
<!-- Contacto -->
<div class="form-section-label mt-3"><i class="fas fa-phone"></i> Contacto</div>
<div class="row g-2">
<div class="col-sm-5">
<label class="form-label small fw-semibold mb-1">Teléfono</label>
<div class="input-group input-group-sm">
<select id="pac-tel-prefijo" class="form-select form-select-sm" style="max-width:72px">
<select id="pac-tel-prefijo" class="form-select form-select-sm" style="max-width:76px">
<option value="57" selected>🇨🇴 57</option>
<option value="58">🇻🇪 58</option>
<option value="1">🇺🇸 1</option>
@@ -152,11 +229,11 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<input type="tel" class="form-control form-control-sm" name="telefono" id="pac-tel" placeholder="3001234567">
</div>
</div>
<div class="col-4">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">Email</label>
<input type="email" class="form-control form-control-sm" name="email" id="pac-email" placeholder="correo@ejemplo.com">
</div>
<div class="col-3">
<div class="col-sm-3">
<label class="form-label small fw-semibold mb-1">Género</label>
<select class="form-select form-select-sm" name="genero" id="pac-genero">
<option value="">—</option>
@@ -165,17 +242,20 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<option value="O">Otro</option>
</select>
</div>
</div>
{{-- Ubicación --}}
<div class="col-4">
<!-- Ubicación -->
<div class="form-section-label mt-3"><i class="fas fa-map-marker-alt"></i> Ubicación y EPS</div>
<div class="row g-2">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">EPS</label>
<input type="text" class="form-control form-control-sm" name="eps" id="pac-eps" placeholder="EPS del paciente">
</div>
<div class="col-4">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">Ciudad</label>
<input type="text" class="form-control form-control-sm" name="ciudad" id="pac-ciudad">
</div>
<div class="col-4">
<div class="col-sm-4">
<label class="form-label small fw-semibold mb-1">Barrio</label>
<input type="text" class="form-control form-control-sm" name="barrio" id="pac-barrio">
</div>
@@ -187,13 +267,13 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<label class="form-label small fw-semibold mb-1">Notas internas</label>
<textarea class="form-control form-control-sm" name="notas_admin" id="pac-notas" rows="2" placeholder="Observaciones o notas adicionales…"></textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-sm btn-primary" onclick="guardarPaciente()">
<div class="modal-footer py-2 border-top">
<button type="button" class="btn btn-sm btn-light" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-sm" style="background:var(--brand);color:#fff" onclick="guardarPaciente()">
<i class="fas fa-save me-1"></i> Guardar
</button>
</div>
@@ -222,23 +302,23 @@ async function cargarLista(pag = 1) {
tbody.innerHTML = d.data.map(p => `
<tr style="cursor:pointer" onclick="verDetalle(${p.id})">
<td>
<td class="ps-3">
<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>${tipoDocLabel(p.tipo_documento)} ${esc(p.numero_documento||'—')}</td>
<td>${esc(p.telefono||'—')}</td>
<td>${esc(p.eps||'—')}</td>
<td class="small">${tipoDocLabel(p.tipo_documento)} ${esc(p.numero_documento||'—')}</td>
<td class="small">${esc(p.telefono||'—')}</td>
<td class="small">${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 class="btn btn-sm btn-outline-secondary py-1 px-2"
onclick="event.stopPropagation();editarPaciente(${p.id})" title="Editar">
<i class="fas fa-pen fa-xs"></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>`);
@@ -255,8 +335,9 @@ async function cargarLista(pag = 1) {
// ── 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 panel = document.getElementById('detail-panel');
panel.style.display = 'block';
document.getElementById('detail-body').innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin fa-lg text-muted"></i></div>';
const r = await fetch(`api/lab/get_pacientes.php?id=${id}`);
const d = await r.json();
@@ -283,8 +364,8 @@ async function verDetalle(id) {
<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>
<hr class="my-2">
<h6 class="fw-semibold mb-2 small"><i class="fas fa-file-medical me-2" style="color:var(--brand)"></i>Órdenes (${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>
@@ -295,8 +376,8 @@ async function verDetalle(id) {
<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})">
</table>` : '<p class="text-muted small mb-2">Sin órdenes</p>'}
<button class="btn btn-sm w-100 mt-1" style="border:1px solid var(--brand);color:var(--brand)" onclick="editarPaciente(${p.id})">
<i class="fas fa-pen me-1"></i> Editar datos
</button>
`;
@@ -308,17 +389,17 @@ function cerrarDetalle() {
// ── Formulario ─────────────────────────────────────────────────────────────
function abrirFormulario(p = null) {
const form = document.getElementById('form-paciente');
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`;
: `<i class="fas fa-user-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-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-prefijo').value = '57';
let telRaw = p.telefono || '';
if (/^1\d{10}$/.test(telRaw)) {
@@ -328,15 +409,15 @@ function abrirFormulario(p = null) {
document.getElementById('pac-tel-prefijo').value = telRaw.slice(0, 2);
telRaw = telRaw.slice(2);
}
document.getElementById('pac-tel').value = telRaw;
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 || '';
document.getElementById('pac-tel').value = telRaw;
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();
}
@@ -354,7 +435,6 @@ async function guardarPaciente() {
const datos = Object.fromEntries(new FormData(form).entries());
if (!datos.id) delete datos.id;
// ── Validaciones de formato ───────────────────────────────────────────
const nombre = (datos.nombre_completo || '').trim();
if (!/^[\p{L}\s'\-\.]+$/u.test(nombre)) {
mostrarToast('El nombre solo debe contener letras, tildes y espacios.', 'danger'); return;
@@ -363,7 +443,7 @@ async function guardarPaciente() {
mostrarToast('Ingresa nombre y apellido completos (mínimo 2 palabras).', 'danger'); return;
}
const docVal = (datos.numero_documento || '').trim();
const tipoDoc = (document.getElementById('pac-tipo-doc')?.value || datos.tipo_documento || 'CC');
const tipoDoc = document.getElementById('pac-tipo-doc')?.value || datos.tipo_documento || 'CC';
if (docVal && ['CC','TI','RC','CE'].includes(tipoDoc)) {
const docDigits = docVal.replace(/[^0-9]/g, '');
if (docDigits.length < 4 || docDigits.length > 12) {
@@ -374,19 +454,15 @@ async function guardarPaciente() {
if (emailVal && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) {
mostrarToast('El correo electrónico no es válido. Ej: nombre@dominio.com.', 'danger'); return;
}
// ─────────────────────────────────────────────────────────────────────
// Normalizar teléfono: anteponer prefijo seleccionado
const prefijo = document.getElementById('pac-tel-prefijo').value || '57';
let tel = (datos.telefono || '').replace(/[^0-9+]/g, '');
if (tel.startsWith('+')) tel = tel.slice(1);
if (tel.length === 10) tel = prefijo + tel;
datos.telefono = tel || null;
// Validar formato: colombiano (57) exige iniciar en 3 con 10 dígitos; otros países solo longitud
if (datos.telefono) {
const digits = datos.telefono.replace(/[^0-9]/g, '');
const esColombia = prefijo === '57';
if (esColombia) {
if (prefijo === '57') {
const sinPref = digits.replace(/^57/, '');
if (!/^3\d{9}$/.test(sinPref)) {
mostrarToast('El celular colombiano debe comenzar por 3 y tener 10 dígitos. Ej: 3001234567.', 'danger'); return;
@@ -415,17 +491,11 @@ async function guardarPaciente() {
// ── Utils ──────────────────────────────────────────────────────────────────
const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
const TIPO_DOC = {
CC: 'CC — Cédula de Ciudadanía',
CE: 'CE — Cédula de Extranjería',
TI: 'TI — Tarjeta de Identidad',
PA: 'PA — Pasaporte',
DE: 'DE — Documento Extranjero',
NIT: 'NIT',
RC: 'RC — Registro Civil',
CC:'CC', CE:'CE', TI:'TI', PA:'PA', DE:'DE', NIT:'NIT', RC:'RC',
};
const tipoDocLabel = t => TIPO_DOC[t] || esc(t||'—');
const formatFecha = s => s ? new Date(s).toLocaleDateString('es-CO') : '—';
const colorEstado = e => ({
const tipoDocLabel = t => TIPO_DOC[t] ? `<span class="badge bg-secondary" style="font-size:.7rem">${TIPO_DOC[t]}</span>` : esc(t||'—');
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');
@@ -437,14 +507,13 @@ function debounce(fn, 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.className = `alert alert-${tipo} alert-dismissible position-fixed bottom-0 end-0 m-3 shadow`;
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>