Files
whatsapp/lab_pacientes.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 828e323eba feat(lab_pacientes): columna origen + vista mejorada con stats y filtros
- Migration: columna 'origen' (manual|rips|whatsapp) en lab_pacientes
- ingest_paciente.php: guarda origen='rips' en cada paciente importado
- Paciente::listar(): filtro por origen + orden por created_at DESC
- Paciente::stats(): conteo total/importados/con_wa/manuales
- get_pacientes.php: expone ?origen= y ?stats=1
- lab_pacientes.php: cards de stats clicables, badge origen por fila,
  columna ciudad, columna fecha registro, dropdown filtro por origen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 11:16:52 -05:00

719 lines
36 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Gestión de Pacientes — Módulo Administrativo de Laboratorio
*/
require_once 'config/config.php';
if (!defined('APP_ROOT')) { define('APP_ROOT', __DIR__); }
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';
// 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">
<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=13" rel="stylesheet">
<style>
: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 ──────────────────────────────── */
.modal-header-brand {
background: linear-gradient(135deg, var(--brand-dark) 0%, var(--brand) 100%);
color: #fff;
padding: 1.4rem 1.5rem 1rem;
position: relative;
}
.modal-header-brand .btn-close { filter: invert(1) brightness(2); }
.pac-avatar {
width: 52px; height: 52px; border-radius: 50%;
background: rgba(255,255,255,.18);
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; font-weight: 800; color: #fff;
border: 2px solid rgba(255,255,255,.35);
flex-shrink: 0;
}
.modal-header-brand .modal-title {
color: #fff; font-size: 1rem; font-weight: 700; line-height: 1.2;
}
.modal-header-brand .modal-subtitle {
font-size: .75rem; opacity: .75; margin-top: 1px;
}
/* ── Secciones ─────────────────────────────────── */
.form-section {
background: #f8faff;
border: 1px solid color-mix(in srgb, var(--brand) 12%, #e2e8f0);
border-radius: .6rem;
padding: .9rem 1rem .6rem;
margin-bottom: .8rem;
}
.form-section-label {
font-size: .65rem; font-weight: 800; text-transform: uppercase;
letter-spacing: .1em; color: var(--brand);
margin: 0 0 .65rem;
display: flex; align-items: center; gap: .4rem;
}
.form-section-label i { opacity: .7; }
/* Floating label overrides */
.form-floating > label { font-size: .8rem; color: #64748b; }
.form-floating > .form-control,
.form-floating > .form-select { font-size: .875rem; border-color: #dde3ed; border-radius: .45rem; }
.form-floating > .form-control:focus,
.form-floating > .form-select:focus {
border-color: var(--brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 15%, transparent);
}
/* ── Modal footer ──────────────────────────────── */
.modal-footer-brand {
background: #f8faff;
border-top: 1px solid #e8edf5;
padding: .8rem 1.25rem;
display: flex; gap: .5rem; justify-content: flex-end;
}
.btn-brand-solid {
background: linear-gradient(135deg, var(--brand-dark), var(--brand));
color: #fff; border: none; border-radius: .45rem;
padding: .45rem 1.2rem; font-size: .85rem; font-weight: 600;
transition: opacity .15s;
}
.btn-brand-solid:hover { opacity: .88; color: #fff; }
/* ── 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); }
/* ── Tabla ─────────────────────────────────────── */
.table-hover tbody tr:hover { background: color-mix(in srgb, var(--brand) 6%, #fff) !important; }
</style>
</head>
<body>
<?php
$SIDEBAR_TITLE = 'Panel Lab';
$SIDEBAR_ICON = 'fas fa-flask';
require_once __DIR__ . '/shared/components/sidebar.php';
?>
<main class="main-content">
<header class="content-header d-flex align-items-center justify-content-between">
<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-sm" style="background:var(--brand);color:#fff" onclick="abrirFormulario()">
<i class="fas fa-plus me-1"></i> Nuevo
</button>
</header>
<div class="container-fluid py-3">
<!-- Stats -->
<div class="row g-2 mb-3" id="stats-row">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm rounded-3 text-center py-2">
<div class="fw-bold fs-4" id="stat-total">—</div>
<div class="text-muted small">Total</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm rounded-3 text-center py-2" style="cursor:pointer" onclick="filtrarOrigen('rips')">
<div class="fw-bold fs-4 text-primary" id="stat-rips">—</div>
<div class="text-muted small"><i class="fas fa-database me-1 text-primary"></i>Importados RIPS</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm rounded-3 text-center py-2" style="cursor:pointer" onclick="filtrarOrigen('whatsapp')">
<div class="fw-bold fs-4 text-success" id="stat-wa">—</div>
<div class="text-muted small"><i class="fab fa-whatsapp me-1 text-success"></i>Vinculados WA</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm rounded-3 text-center py-2" style="cursor:pointer" onclick="filtrarOrigen('manual')">
<div class="fw-bold fs-4 text-secondary" id="stat-manual">—</div>
<div class="text-muted small"><i class="fas fa-pen me-1"></i>Manuales</div>
</div>
</div>
</div>
<!-- Filtros -->
<div class="row mb-3 g-2">
<div class="col-md-5">
<div class="input-group">
<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 class="col-md-3">
<select id="filtro-origen" class="form-select" onchange="cargarLista(1)">
<option value="">Todos los orígenes</option>
<option value="rips">Importados RIPS</option>
<option value="manual">Manuales</option>
<option value="whatsapp">Desde WhatsApp</option>
</select>
</div>
<div class="col-md-auto ms-auto">
<button class="btn btn-sm btn-outline-secondary" onclick="cargarStats()" title="Actualizar stats">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="row g-3">
<!-- Lista -->
<div class="col-lg-7" id="lista-col">
<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 class="ps-3">Paciente</th>
<th>Documento</th>
<th>Ciudad</th>
<th>Origen</th>
<th>Registro</th>
<th>Órd.</th>
<th></th>
</tr>
</thead>
<tbody id="tabla-body">
<tr><td colspan="7" class="text-center py-4 text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Cargando...
</td></tr>
</tbody>
</table>
</div>
<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 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>
</main>
<!-- Modal Formulario Paciente -->
<div class="modal fade" id="modalPaciente" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content border-0 shadow-lg rounded-4 overflow-hidden">
<!-- Header -->
<div class="modal-header-brand d-flex align-items-center gap-3">
<div class="pac-avatar" id="modal-avatar">?</div>
<div class="flex-1">
<div class="modal-title" id="modal-titulo">Nuevo Paciente</div>
<div class="modal-subtitle" id="modal-subtitulo">Completa los datos del paciente</div>
</div>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-3 px-4">
<form id="form-paciente" autocomplete="off">
<input type="hidden" name="id" id="pac-id">
<!-- Datos personales -->
<div class="form-section">
<div class="form-section-label"><i class="fas fa-id-card"></i> Datos personales</div>
<div class="row g-2">
<div class="col-12">
<div class="form-floating">
<input type="text" class="form-control" name="nombre_completo" id="pac-nombre"
required placeholder="Nombre completo" tabindex="1"
oninput="actualizarAvatar(this.value)">
<label for="pac-nombre">Nombre completo <span class="text-danger">*</span></label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<select class="form-select" name="tipo_documento" id="pac-tipo-doc" tabindex="2">
<option value="CC">CC — Cédula</option>
<option value="CE">CE — Extranjería</option>
<option value="TI">TI — Tarjeta Identidad</option>
<option value="PA">PA — Pasaporte</option>
<option value="DE">DE — Doc. Extranjero</option>
<option value="NIT">NIT</option>
<option value="RC">RC — Reg. Civil</option>
</select>
<label for="pac-tipo-doc">Tipo documento</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control" name="numero_documento" id="pac-doc" placeholder="Número documento" tabindex="3">
<label for="pac-doc">Número documento</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="date" class="form-control" name="fecha_nacimiento" id="pac-fnac" placeholder="Fecha nacimiento" tabindex="4">
<label for="pac-fnac">Fecha nacimiento</label>
</div>
</div>
</div>
</div>
<!-- Contacto -->
<div class="form-section">
<div class="form-section-label"><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" style="color:#64748b">Teléfono</label>
<div class="input-group">
<select id="pac-tel-prefijo" class="form-select" tabindex="-1" style="max-width:82px;border-color:#dde3ed;border-radius:.45rem 0 0 .45rem;font-size:.875rem">
<option value="57" selected>🇨🇴 57</option>
<option value="58">🇻🇪 58</option>
<option value="1">🇺🇸 1</option>
</select>
<input type="tel" class="form-control" name="telefono" id="pac-tel"
placeholder="3001234567" tabindex="5" style="border-radius:0 .45rem .45rem 0"
onblur="verificarWhatsapp()">
</div>
<div id="wa-badge" class="mt-1" style="font-size:.78rem;min-height:18px"></div>
<input type="hidden" id="pac-user-id" name="user_id">
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="email" class="form-control" name="email" id="pac-email" placeholder="Email" tabindex="6">
<label for="pac-email">Email</label>
</div>
</div>
<div class="col-sm-3">
<div class="form-floating">
<select class="form-select" name="genero" id="pac-genero" tabindex="7">
<option value="">—</option>
<option value="M">Masculino</option>
<option value="F">Femenino</option>
<option value="O">Otro</option>
</select>
<label for="pac-genero">Género</label>
</div>
</div>
</div>
</div>
<!-- Ubicación y EPS -->
<div class="form-section">
<div class="form-section-label"><i class="fas fa-map-marker-alt"></i> Ubicación y EPS</div>
<div class="row g-2">
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control" name="eps" id="pac-eps" placeholder="EPS" tabindex="8">
<label for="pac-eps">EPS</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control" name="ciudad" id="pac-ciudad" placeholder="Ciudad" tabindex="9" value="Cúcuta">
<label for="pac-ciudad">Ciudad</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control" name="barrio" id="pac-barrio" placeholder="Barrio" tabindex="10">
<label for="pac-barrio">Barrio</label>
</div>
</div>
<div class="col-12">
<div class="form-floating">
<input type="text" class="form-control" name="direccion" id="pac-dir" placeholder="Dirección" tabindex="11">
<label for="pac-dir">Dirección</label>
</div>
</div>
<div class="col-12">
<div class="form-floating">
<textarea class="form-control" name="notas_admin" id="pac-notas"
placeholder="Notas internas" tabindex="12" style="height:72px"></textarea>
<label for="pac-notas">Notas internas</label>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer-brand">
<button type="button" class="btn btn-sm btn-light px-3" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancelar
</button>
<button type="button" class="btn-brand-solid" onclick="guardarPaciente()">
<i class="fas fa-save me-1"></i> Guardar paciente
</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');
// ── Stats ──────────────────────────────────────────────────────────────────
async function cargarStats() {
try {
const r = await fetch('api/lab/get_pacientes.php?stats=1');
const d = await r.json();
const s = d.stats || {};
document.getElementById('stat-total').textContent = s.total ?? '—';
document.getElementById('stat-rips').textContent = s.importados ?? '—';
document.getElementById('stat-wa').textContent = s.con_wa ?? '—';
document.getElementById('stat-manual').textContent = s.manuales ?? '—';
} catch(_) {}
}
function filtrarOrigen(origen) {
const sel = document.getElementById('filtro-origen');
sel.value = sel.value === origen ? '' : origen;
cargarLista(1);
}
// ── Lista ──────────────────────────────────────────────────────────────────
const _origenBadge = {
rips: '<span class="badge" style="background:#e3f2fd;color:#1565c0;font-size:.7rem"><i class="fas fa-database me-1"></i>RIPS</span>',
whatsapp: '<span class="badge" style="background:#e8f5e9;color:#2e7d32;font-size:.7rem"><i class="fab fa-whatsapp me-1"></i>WA</span>',
manual: '<span class="badge bg-secondary bg-opacity-10 text-secondary" style="font-size:.7rem"><i class="fas fa-pen me-1"></i>Manual</span>',
};
function origenBadge(origen) {
return _origenBadge[origen] || _origenBadge.manual;
}
function fmtFecha(str) {
if (!str) return '—';
return str.slice(0, 10).split('-').reverse().join('/');
}
async function cargarLista(pag = 1) {
paginaActual = pag;
const busq = document.getElementById('buscador').value.trim();
const origen = document.getElementById('filtro-origen').value;
const r = await fetch(
`api/lab/get_pacientes.php?busqueda=${encodeURIComponent(busq)}&page=${pag}&limit=25&origen=${encodeURIComponent(origen)}`
);
const d = await r.json();
const tbody = document.getElementById('tabla-body');
if (!d.data?.length) {
tbody.innerHTML = '<tr><td colspan="7" 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 class="ps-3">
<div class="fw-semibold">${esc(p.nombre_completo)}</div>
${p.telefono ? `<small class="text-muted">${esc(p.telefono)}</small>` : ''}
</td>
<td class="small">${tipoDocLabel(p.tipo_documento)} ${esc(p.numero_documento||'—')}</td>
<td class="small text-muted">${esc(p.ciudad||'—')}</td>
<td>${origenBadge(p.origen)}</td>
<td class="small text-muted">${fmtFecha(p.created_at)}</td>
<td><span class="badge bg-primary hist-badge">${p.total_ordenes||0}</span></td>
<td>
<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('');
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) {
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();
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">${tipoDocLabel(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>
<dt class="col-5 text-muted">Origen</dt><dd class="col-7">${origenBadge(p.origen)}</dd>
<dt class="col-5 text-muted">Registro</dt><dd class="col-7 text-muted small">${fmtFecha(p.created_at)}</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 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>
<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 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>
`;
}
function cerrarDetalle() {
document.getElementById('detail-panel').style.display = 'none';
}
// ── Formulario ─────────────────────────────────────────────────────────────
function actualizarAvatar(nombre) {
const av = document.getElementById('modal-avatar');
if (!av) return;
const partes = nombre.trim().split(/\s+/).filter(Boolean);
av.textContent = partes.length >= 2
? (partes[0][0] + partes[1][0]).toUpperCase()
: (partes[0]?.[0] || '?').toUpperCase();
}
function abrirFormulario(p = null) {
const form = document.getElementById('form-paciente');
form.reset();
document.getElementById('pac-id').value = p ? p.id : '';
document.getElementById('modal-titulo').textContent = p ? 'Editar Paciente' : 'Nuevo Paciente';
document.getElementById('modal-subtitulo').textContent = p
? (p.nombre_completo || 'Actualiza los datos')
: 'Completa los datos del paciente';
actualizarAvatar(p?.nombre_completo || '');
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-prefijo').value = '57';
let telRaw = p.telefono || '';
if (/^1\d{10}$/.test(telRaw)) {
document.getElementById('pac-tel-prefijo').value = '1';
telRaw = telRaw.slice(1);
} else if (/^(57|58)\d{10}$/.test(telRaw)) {
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 || '';
}
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 verificarWhatsapp() {
const prefijo = document.getElementById('pac-tel-prefijo').value || '57';
let tel = (document.getElementById('pac-tel').value || '').replace(/[^0-9]/g, '');
if (tel.length === 10) tel = prefijo + tel;
const badge = document.getElementById('wa-badge');
const hiddenUid = document.getElementById('pac-user-id');
if (!tel || tel.length < 10) { badge.innerHTML = ''; hiddenUid.value = ''; return; }
badge.innerHTML = '<span class="text-muted"><i class="fas fa-spinner fa-spin me-1"></i>Verificando WhatsApp…</span>';
try {
const r = await fetch(`api/lab/check_whatsapp.php?phone=${encodeURIComponent(tel)}`);
const d = await r.json();
if (d.ok && d.found) {
badge.innerHTML = '<span class="text-success"><i class="fab fa-whatsapp me-1"></i>Registrado en WhatsApp</span>';
hiddenUid.value = d.user_id;
} else {
badge.innerHTML = '<span class="text-muted"><i class="fas fa-times-circle me-1"></i>Sin WhatsApp registrado</span>';
hiddenUid.value = '';
}
} catch(_) { badge.innerHTML = ''; hiddenUid.value = ''; }
}
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 nombre = (datos.nombre_completo || '').trim();
if (!/^[\p{L}\s'\-\.]+$/u.test(nombre)) {
mostrarToast('El nombre solo debe contener letras, tildes y espacios.', 'danger'); return;
}
if (nombre.split(/\s+/).filter(Boolean).length < 2) {
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';
if (docVal && ['CC','TI','RC','CE'].includes(tipoDoc)) {
const docDigits = docVal.replace(/[^0-9]/g, '');
if (docDigits.length < 4 || docDigits.length > 12) {
mostrarToast('La cédula/TI debe tener entre 4 y 12 dígitos.', 'danger'); return;
}
}
const emailVal = (datos.email || '').trim();
if (emailVal && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) {
mostrarToast('El correo electrónico no es válido. Ej: nombre@dominio.com.', 'danger'); return;
}
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;
if (datos.telefono) {
const digits = datos.telefono.replace(/[^0-9]/g, '');
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;
}
} else if (digits.length < 6 || digits.length > 15) {
mostrarToast('El número telefónico no es válido (entre 6 y 15 dígitos).', 'danger'); return;
}
}
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 => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
const TIPO_DOC = {
CC:'CC', CE:'CE', TI:'TI', PA:'PA', DE:'DE', NIT:'NIT', RC:'RC',
};
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');
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 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);
}
cargarLista();
cargarStats();
</script>
<script src="assets/js/lab-sidebar.js"></script>
</body>
</html>