feat(turnero): modal completo de paciente en recepción
- Reemplaza window.open a lab_pacientes.php por modal Bootstrap inline - Modal incluye campos completos: datos personales, contacto (con verificador WA), ubicación y EPS - Botón Ver/Editar en paciente vinculado carga datos desde API y abre modal en modo edición - Botón Cambiar desvincula y enfoca el buscador - Al crear: seleccionarPaciente() auto-vincula al turno y cierra modal - Al editar: actualiza labels en pantalla sin perder el turno activo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
062f565ab5
commit
6adbcc3059
@@ -419,9 +419,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="text-muted small" id="lbl-pac-doc">—</div>
|
||||
<div class="text-muted small" id="lbl-pac-cel"></div>
|
||||
</div>
|
||||
<button class="btn btn-link btn-sm text-secondary p-0" onclick="desvincularPaciente()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="abrirModalPaciente(pacienteActivo?.id)" title="Ver / Editar paciente">
|
||||
<i class="fas fa-pen fa-xs"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="cambiarPaciente()" title="Buscar otro paciente">
|
||||
<i class="fas fa-exchange-alt fa-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historial colapsable del paciente -->
|
||||
@@ -566,6 +571,165 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Modal Paciente (crear / ver / editar) ════════════════════════════ -->
|
||||
<div class="modal fade" id="modalPacienteRec" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content border-0 shadow-lg overflow-hidden" style="border-radius:1rem">
|
||||
|
||||
<!-- Header con avatar -->
|
||||
<div id="mpac-header"
|
||||
style="background:linear-gradient(135deg,#1043a0 0%,#1565c0 100%);
|
||||
color:#fff;padding:1.3rem 1.5rem 1rem;
|
||||
display:flex;align-items:center;gap:.9rem">
|
||||
<div id="mpac-avatar"
|
||||
style="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">?</div>
|
||||
<div class="flex-grow-1">
|
||||
<div style="font-size:1rem;font-weight:700" id="mpac-titulo">Nuevo Paciente</div>
|
||||
<div style="font-size:.75rem;opacity:.75" id="mpac-subtitulo">Completa los datos del paciente</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white ms-auto" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body py-3 px-4">
|
||||
<form id="form-paciente-rec" autocomplete="off">
|
||||
<input type="hidden" id="mpac-id">
|
||||
|
||||
<!-- Datos personales -->
|
||||
<div class="ficha-section mb-3">
|
||||
<h6><i class="fas fa-id-card me-1"></i>Datos personales</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-nombre"
|
||||
required placeholder="Nombre completo"
|
||||
oninput="mpacAvatar(this.value)">
|
||||
<label>Nombre completo <span class="text-danger">*</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-select form-select-sm" id="mpac-tipo-doc">
|
||||
<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>Tipo documento</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-doc" placeholder="Número documento">
|
||||
<label>Número documento</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="date" class="form-control form-control-sm" id="mpac-fnac" placeholder="Fecha nacimiento">
|
||||
<label>Fecha nacimiento</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contacto -->
|
||||
<div class="ficha-section mb-3">
|
||||
<h6><i class="fas fa-phone me-1"></i>Contacto</h6>
|
||||
<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 input-group-sm">
|
||||
<select id="mpac-tel-prefijo" class="form-select" style="max-width:82px">
|
||||
<option value="57" selected>🇨🇴 57</option>
|
||||
<option value="58">🇻🇪 58</option>
|
||||
<option value="1">🇺🇸 1</option>
|
||||
</select>
|
||||
<input type="tel" class="form-control" id="mpac-tel"
|
||||
placeholder="3001234567" onblur="mpacVerificarWA()">
|
||||
</div>
|
||||
<div id="mpac-wa-badge" style="font-size:.78rem;min-height:18px;margin-top:3px"></div>
|
||||
<input type="hidden" id="mpac-user-id">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control form-control-sm" id="mpac-email" placeholder="Email">
|
||||
<label>Email</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-floating">
|
||||
<select class="form-select form-select-sm" id="mpac-genero">
|
||||
<option value="">—</option>
|
||||
<option value="M">Masculino</option>
|
||||
<option value="F">Femenino</option>
|
||||
<option value="O">Otro</option>
|
||||
</select>
|
||||
<label>Género</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ubicación y EPS -->
|
||||
<div class="ficha-section mb-2">
|
||||
<h6><i class="fas fa-map-marker-alt me-1"></i>Ubicación y EPS</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-eps" placeholder="EPS">
|
||||
<label>EPS</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-ciudad" placeholder="Ciudad" value="Cúcuta">
|
||||
<label>Ciudad</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-barrio" placeholder="Barrio">
|
||||
<label>Barrio</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-dir" placeholder="Dirección">
|
||||
<label>Dirección</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<textarea class="form-control form-control-sm" id="mpac-notas"
|
||||
placeholder="Notas internas" style="height:68px"></textarea>
|
||||
<label>Notas internas</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="background:#f8faff;border-top:1px solid #e8edf5;padding:.8rem 1.25rem">
|
||||
<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 btn-sm btn-primary px-4" id="mpac-btn-guardar" onclick="mpacGuardar()">
|
||||
<i class="fas fa-save me-1"></i>Guardar paciente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rec-toast" id="rec-toast">
|
||||
<span class="ico" id="toast-ico"><i class="fas fa-check-circle"></i></span>
|
||||
<span id="toast-msg"></span>
|
||||
@@ -902,8 +1066,8 @@ async function buscarPaciente() {
|
||||
const lista = document.getElementById('lista-pacientes-res');
|
||||
const datos = json.data || json.registros || [];
|
||||
if (!datos.length) {
|
||||
lista.innerHTML = `<div class="text-muted small mt-1">Sin resultados.
|
||||
<a href="#" onclick="abrirNuevoPaciente('${escHtml(q)}');return false">Crear nuevo</a></div>`;
|
||||
lista.innerHTML = `<div class="text-muted small mt-1">Sin resultados.
|
||||
<a href="#" onclick="abrirModalPaciente(null,'${escHtml(q).replace(/'/g,"\\'")}');return false">Crear nuevo</a></div>`;
|
||||
return;
|
||||
}
|
||||
lista.innerHTML = datos.map(p => `
|
||||
@@ -959,15 +1123,9 @@ function desvincularPaciente() {
|
||||
document.getElementById('inp-buscar-pac').value = '';
|
||||
}
|
||||
|
||||
function abrirNuevoPaciente(nombre) {
|
||||
const ventana = window.open('<?= BASE_URL ?>lab_pacientes.php?nuevo=1&nombre=' + encodeURIComponent(nombre), '_blank');
|
||||
// Detectar cuando se cierra la ventana y refrescar la búsqueda
|
||||
const chequeo = setInterval(() => {
|
||||
if (ventana.closed) {
|
||||
clearInterval(chequeo);
|
||||
setTimeout(() => buscarPaciente(), 500); // Buscar nuevamente después de cerrar
|
||||
}
|
||||
}, 500);
|
||||
function cambiarPaciente() {
|
||||
desvincularPaciente();
|
||||
setTimeout(() => document.getElementById('inp-buscar-pac').focus(), 80);
|
||||
}
|
||||
|
||||
// ── Historial del paciente ──────────────────────────────────────
|
||||
@@ -1648,6 +1806,204 @@ function mostrarLlamando(codigo) {
|
||||
_iniciar();
|
||||
})(DESK_ID, DESK_NOMBRE, 'recepcion', 'desk_id');
|
||||
|
||||
// ── Modal paciente (crear / ver / editar) ────────────────────
|
||||
const _mpacModal = () => bootstrap.Modal.getOrCreateInstance(document.getElementById('modalPacienteRec'));
|
||||
|
||||
function mpacAvatar(nombre) {
|
||||
const parts = nombre.trim().split(/\s+/).filter(Boolean);
|
||||
document.getElementById('mpac-avatar').textContent = parts.length >= 2
|
||||
? (parts[0][0] + parts[1][0]).toUpperCase()
|
||||
: (parts[0]?.[0] || '?').toUpperCase();
|
||||
}
|
||||
|
||||
async function abrirModalPaciente(id, nombrePrefill) {
|
||||
// Reset form
|
||||
document.getElementById('form-paciente-rec').reset();
|
||||
document.getElementById('mpac-id').value = '';
|
||||
document.getElementById('mpac-avatar').textContent = '?';
|
||||
document.getElementById('mpac-wa-badge').innerHTML = '';
|
||||
document.getElementById('mpac-user-id').value = '';
|
||||
document.getElementById('mpac-ciudad').value = 'Cúcuta';
|
||||
|
||||
if (id) {
|
||||
document.getElementById('mpac-titulo').textContent = 'Cargando…';
|
||||
document.getElementById('mpac-subtitulo').textContent = '';
|
||||
_mpacModal().show();
|
||||
try {
|
||||
const r = await fetch(`${API_PAC}?id=${id}`);
|
||||
const d = await r.json();
|
||||
const p = (d.data || d.registros || [])[0];
|
||||
if (!p) { mostrarError('No se pudo cargar el paciente.'); return; }
|
||||
mpacRellenar(p);
|
||||
} catch (_) { mostrarError('Error al cargar paciente.'); }
|
||||
} else {
|
||||
document.getElementById('mpac-titulo').textContent = 'Nuevo Paciente';
|
||||
document.getElementById('mpac-subtitulo').textContent = 'Completa los datos del paciente';
|
||||
if (nombrePrefill) {
|
||||
document.getElementById('mpac-nombre').value = nombrePrefill;
|
||||
mpacAvatar(nombrePrefill);
|
||||
}
|
||||
_mpacModal().show();
|
||||
setTimeout(() => document.getElementById('mpac-nombre').focus(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
function mpacRellenar(p) {
|
||||
document.getElementById('mpac-id').value = p.id;
|
||||
document.getElementById('mpac-nombre').value = p.nombre_completo || '';
|
||||
document.getElementById('mpac-doc').value = p.numero_documento || '';
|
||||
document.getElementById('mpac-tipo-doc').value = p.tipo_documento || 'CC';
|
||||
document.getElementById('mpac-fnac').value = (p.fecha_nacimiento || '').slice(0, 10);
|
||||
document.getElementById('mpac-email').value = p.email || '';
|
||||
document.getElementById('mpac-genero').value = p.genero || '';
|
||||
document.getElementById('mpac-eps').value = p.eps || '';
|
||||
document.getElementById('mpac-ciudad').value = p.ciudad || 'Cúcuta';
|
||||
document.getElementById('mpac-barrio').value = p.barrio || '';
|
||||
document.getElementById('mpac-dir').value = p.direccion || '';
|
||||
document.getElementById('mpac-notas').value = p.notas_admin || '';
|
||||
|
||||
// Teléfono con prefijo
|
||||
let tel = p.telefono || '';
|
||||
const prefijos = ['57','58','1'];
|
||||
let prefijo = '57';
|
||||
for (const pf of prefijos) {
|
||||
if (tel.startsWith(pf) && tel.length > pf.length) {
|
||||
prefijo = pf; tel = tel.slice(pf.length); break;
|
||||
}
|
||||
}
|
||||
document.getElementById('mpac-tel-prefijo').value = prefijo;
|
||||
document.getElementById('mpac-tel').value = tel;
|
||||
|
||||
mpacAvatar(p.nombre_completo || '');
|
||||
document.getElementById('mpac-titulo').textContent = 'Editar Paciente';
|
||||
document.getElementById('mpac-subtitulo').textContent = p.nombre_completo || '';
|
||||
}
|
||||
|
||||
async function mpacVerificarWA() {
|
||||
const prefijo = document.getElementById('mpac-tel-prefijo').value || '57';
|
||||
let tel = (document.getElementById('mpac-tel').value || '').replace(/[^0-9]/g, '');
|
||||
if (tel.length === 10) tel = prefijo + tel;
|
||||
const badge = document.getElementById('mpac-wa-badge');
|
||||
if (!tel || tel.length < 10) { badge.innerHTML = ''; return; }
|
||||
badge.innerHTML = '<span class="text-muted"><i class="fas fa-spinner fa-spin me-1"></i>Verificando…</span>';
|
||||
try {
|
||||
const r = await fetch(`<?= BASE_URL ?>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>';
|
||||
document.getElementById('mpac-user-id').value = d.user_id;
|
||||
} else {
|
||||
badge.innerHTML = '<span class="text-muted"><i class="fas fa-times-circle me-1"></i>Sin WhatsApp registrado</span>';
|
||||
document.getElementById('mpac-user-id').value = '';
|
||||
}
|
||||
} catch(_) { badge.innerHTML = ''; }
|
||||
}
|
||||
|
||||
async function mpacGuardar() {
|
||||
const nombre = document.getElementById('mpac-nombre').value.trim();
|
||||
if (!nombre) { mostrarError('El nombre es obligatorio.'); return; }
|
||||
if (!/^[\p{L}\s'\-\.]+$/u.test(nombre)) {
|
||||
mostrarError('El nombre solo debe contener letras, tildes y espacios.'); return;
|
||||
}
|
||||
if (nombre.split(/\s+/).filter(Boolean).length < 2) {
|
||||
mostrarError('Ingresa nombre y apellido (mínimo 2 palabras).'); return;
|
||||
}
|
||||
|
||||
const tipoDoc = document.getElementById('mpac-tipo-doc').value;
|
||||
const docVal = document.getElementById('mpac-doc').value.trim();
|
||||
if (docVal && ['CC','TI','RC','CE'].includes(tipoDoc)) {
|
||||
const digits = docVal.replace(/[^0-9]/g, '');
|
||||
if (digits.length < 4 || digits.length > 12) {
|
||||
mostrarError('El documento debe tener entre 4 y 12 dígitos.'); return;
|
||||
}
|
||||
}
|
||||
|
||||
const emailVal = document.getElementById('mpac-email').value.trim();
|
||||
if (emailVal && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) {
|
||||
mostrarError('El correo electrónico no es válido.'); return;
|
||||
}
|
||||
|
||||
const prefijo = document.getElementById('mpac-tel-prefijo').value || '57';
|
||||
let tel = document.getElementById('mpac-tel').value.replace(/[^0-9]/g, '');
|
||||
if (tel.length === 10) tel = prefijo + tel;
|
||||
if (tel && prefijo === '57') {
|
||||
const sinPref = tel.replace(/^57/, '');
|
||||
if (!/^3\d{9}$/.test(sinPref)) {
|
||||
mostrarError('El celular colombiano debe comenzar por 3 y tener 10 dígitos.'); return;
|
||||
}
|
||||
}
|
||||
|
||||
const id = document.getElementById('mpac-id').value;
|
||||
const datos = {
|
||||
nombre_completo: nombre,
|
||||
tipo_documento: tipoDoc,
|
||||
numero_documento: docVal || null,
|
||||
fecha_nacimiento: document.getElementById('mpac-fnac').value || null,
|
||||
telefono: tel || null,
|
||||
email: emailVal || null,
|
||||
genero: document.getElementById('mpac-genero').value || null,
|
||||
eps: document.getElementById('mpac-eps').value.trim() || null,
|
||||
ciudad: document.getElementById('mpac-ciudad').value.trim() || null,
|
||||
barrio: document.getElementById('mpac-barrio').value.trim() || null,
|
||||
direccion: document.getElementById('mpac-dir').value.trim() || null,
|
||||
notas_admin: document.getElementById('mpac-notas').value.trim() || null,
|
||||
user_id: document.getElementById('mpac-user-id').value || null,
|
||||
};
|
||||
if (id) datos.id = parseInt(id);
|
||||
|
||||
const btn = document.getElementById('mpac-btn-guardar');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
||||
|
||||
try {
|
||||
const r = await fetch('<?= BASE_URL ?>api/lab/save_paciente.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(datos),
|
||||
});
|
||||
const d = await r.json();
|
||||
|
||||
if (!d.success && !d.ok) {
|
||||
mostrarError(d.error || 'Error al guardar el paciente.');
|
||||
return;
|
||||
}
|
||||
|
||||
_mpacModal().hide();
|
||||
mostrarToast(d.message || 'Paciente guardado.', 'success');
|
||||
|
||||
if (id) {
|
||||
// Edición: actualizar datos en pantalla si es el paciente activo
|
||||
if (pacienteActivo && pacienteActivo.id == id) {
|
||||
pacienteActivo.nombre_completo = nombre;
|
||||
pacienteActivo.numero_documento = docVal;
|
||||
pacienteActivo.tipo_documento = tipoDoc;
|
||||
pacienteActivo.telefono = tel;
|
||||
document.getElementById('lbl-pac-nombre').textContent =
|
||||
(pacienteActivo.full_name || nombre).trim();
|
||||
document.getElementById('lbl-pac-doc').textContent =
|
||||
tipoDoc + ' ' + (docVal || '');
|
||||
document.getElementById('lbl-pac-cel').textContent = tel || '';
|
||||
}
|
||||
} else {
|
||||
// Creación: vincular al turno automáticamente
|
||||
const nuevoPac = {
|
||||
id: d.id || d.data?.id,
|
||||
nombre_completo: nombre,
|
||||
full_name: nombre,
|
||||
tipo_documento: tipoDoc,
|
||||
numero_documento: docVal,
|
||||
telefono: tel,
|
||||
};
|
||||
seleccionarPaciente(nuevoPac);
|
||||
}
|
||||
} catch (err) {
|
||||
mostrarError('Error de red: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar paciente';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Toast ────────────────────────────────────────────────────
|
||||
let toastTimer = null;
|
||||
function mostrarToast(msg, type = 'info', duration = 2500) {
|
||||
|
||||
Reference in New Issue
Block a user