Files
whatsapp/lab_pacientes.php
T

635 lines
32 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">
<!-- Buscador -->
<div class="row mb-3">
<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>
<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>Teléfono</th>
<th>EPS</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>
</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');
// ── 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 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 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 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>
${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();
</script>
<script src="assets/js/lab-sidebar.js"></script>
</body>
</html>