From 2f72d87c3b59bbd628b0ed21d10001120d2c747d Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 27 May 2026 20:40:17 -0500 Subject: [PATCH] cambios --- api/lab/my_agenda.php | 7 +++++- api/lab/save_domicilio.php | 23 +++++++++++++++++- api/lab/save_paciente.php | 32 +++++++++++++++++++++++++ classes/lab/Domicilio.php | 36 ++++++++++++++++++++++++++-- classes/lab/Enfermera.php | 3 ++- conversations.php | 27 ++++++++++++++++++++- enfermero_portal.php | 48 ++++++++++++++++++++++++++++++++++++-- lab_domicilios.php | 15 +++++++++++- lab_pacientes.php | 26 +++++++++++++++++++++ 9 files changed, 208 insertions(+), 9 deletions(-) diff --git a/api/lab/my_agenda.php b/api/lab/my_agenda.php index 9a9de5d..90ecf78 100644 --- a/api/lab/my_agenda.php +++ b/api/lab/my_agenda.php @@ -48,7 +48,12 @@ try { } unset($item); - // Resumen rápido + // Marcar si el domicilio fue creado por el propio usuario (para control de edición en portal enfermero) + $adminUserId = adminId(); + foreach ($agenda as &$item) { + $item['es_propio'] = (!empty($item['creado_por']) && (int)$item['creado_por'] === $adminUserId); + } + unset($item); $totales = array_count_values(array_column($agenda, 'domicilio_estado')); jsonOk([ diff --git a/api/lab/save_domicilio.php b/api/lab/save_domicilio.php index 59aefe4..9bb55ee 100644 --- a/api/lab/save_domicilio.php +++ b/api/lab/save_domicilio.php @@ -19,7 +19,15 @@ try { // ── Reprogramar ── if (!empty($datos['id']) && !empty($datos['reprogramar'])) { - $id = (int)$datos['id']; + $id = (int)$datos['id']; + // Enfermero solo puede reprogramar domicilios que él mismo creó + if (userRole() === 'enfermero') { + $dbCheck = Database::getInstance(); + $chk = $dbCheck->fetchOne("SELECT creado_por FROM lab_domicilios WHERE id = ?", [$id]); + if (!$chk || (int)($chk['creado_por'] ?? 0) !== adminId()) { + jsonError('Solo puedes reprogramar domicilios que tú mismo agendaste.'); + } + } $nuevaFecha = trim($datos['nueva_fecha'] ?? ''); $nuevaHora = trim($datos['nueva_hora'] ?? ''); $motivo = trim($datos['motivo'] ?? ''); @@ -47,6 +55,14 @@ try { if (!empty($datos['id'])) { // Actualización general $id = (int)$datos['id']; + // Enfermero solo puede editar domicilios que él mismo creó + if (userRole() === 'enfermero') { + $dbCheck = Database::getInstance(); + $chk = $dbCheck->fetchOne("SELECT creado_por FROM lab_domicilios WHERE id = ?", [$id]); + if (!$chk || (int)($chk['creado_por'] ?? 0) !== adminId()) { + jsonError('Solo puedes editar domicilios que tú mismo agendaste.'); + } + } unset($datos['id'], $datos['solo_estado']); // Normalizar orden_id: vacío o 0 → null if (isset($datos['orden_id']) && ($datos['orden_id'] === '' || (int)$datos['orden_id'] === 0)) { @@ -61,6 +77,11 @@ try { jsonError("El campo $req es obligatorio"); } } + // Validar formato mínimo de dirección + $dirVal = trim($datos['direccion'] ?? ''); + if (strlen($dirVal) < 5) { + jsonError('La dirección es muy corta. Ingresa una dirección completa. Ej: Cra 15 # 32-10.'); + } // Normalizar orden_id: vacío o 0 → null (evita FK constraint) if (isset($datos['orden_id']) && ($datos['orden_id'] === '' || (int)$datos['orden_id'] === 0)) { $datos['orden_id'] = null; diff --git a/api/lab/save_paciente.php b/api/lab/save_paciente.php index e3d7012..7b3cd4a 100644 --- a/api/lab/save_paciente.php +++ b/api/lab/save_paciente.php @@ -12,6 +12,38 @@ requireMethod('POST'); try { $datos = inputJson(); + + // ── Validación de formato de campos del paciente ──────────────────────── + $nombreVal = trim($datos['nombre_completo'] ?? ''); + if ($nombreVal !== '') { + if (!preg_match('/^[\p{L}\s\'\-\.]+$/u', $nombreVal)) { + jsonError('El nombre solo debe contener letras, tildes y espacios (sin números ni símbolos).'); + } + if (count(preg_split('/\s+/', $nombreVal, -1, PREG_SPLIT_NO_EMPTY)) < 2) { + jsonError('Ingresa el nombre completo: nombre y apellido (mínimo 2 palabras).'); + } + } + $telVal = trim($datos['telefono'] ?? ''); + if ($telVal !== '') { + $telDigits = preg_replace('/[^0-9]/', '', $telVal); + if (!preg_match('/^(57)?3\d{9}$/', $telDigits)) { + jsonError('El celular debe comenzar por 3 y tener 10 dígitos. Ej: 3001234567.'); + } + } + $docVal = trim($datos['numero_documento'] ?? ''); + $tipoDoc = trim($datos['tipo_documento'] ?? 'CC'); + if ($docVal !== '' && in_array($tipoDoc, ['CC', 'TI', 'RC', 'CE'], true)) { + $docDigits = preg_replace('/[^0-9]/', '', $docVal); + if (strlen($docDigits) < 4 || strlen($docDigits) > 12) { + jsonError('La cédula/TI/RC/CE debe tener entre 4 y 12 dígitos numéricos.'); + } + } + $emailVal = trim($datos['email'] ?? ''); + if ($emailVal !== '' && !filter_var($emailVal, FILTER_VALIDATE_EMAIL)) { + jsonError('El correo electrónico no es válido. Ej: nombre@dominio.com.'); + } + // ──────────────────────────────────────────────────────────────────────── + $pac = new Paciente(); $admin = adminId(); diff --git a/classes/lab/Domicilio.php b/classes/lab/Domicilio.php index d132273..9f8b683 100644 --- a/classes/lab/Domicilio.php +++ b/classes/lab/Domicilio.php @@ -88,7 +88,23 @@ class Domicilio { e.telefono AS enfermera_telefono, a.enfermera_id AS enfermera_lab_id, a.id AS asignacion_id, - a.estado AS asignacion_estado + a.estado AS asignacion_estado, + CASE + WHEN a.enfermera_id IS NULL OR d.hora_llegada IS NULL THEN NULL + ELSE TIMESTAMPDIFF( + MINUTE, + ( + SELECT MAX(CONCAT(d2.fecha_programada, ' ', d2.hora_salida)) + FROM lab_domicilios d2 + JOIN lab_asignaciones a2 ON a2.domicilio_id = d2.id + WHERE a2.enfermera_id = a.enfermera_id + AND d2.hora_salida IS NOT NULL + AND d2.id <> d.id + AND CONCAT(d2.fecha_programada, ' ', d2.hora_salida) < CONCAT(d.fecha_programada, ' ', d.hora_llegada) + ), + CONCAT(d.fecha_programada, ' ', d.hora_llegada) + ) + END AS tiempo_recorrido_min FROM lab_domicilios d JOIN lab_pacientes p ON p.id = d.paciente_id LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id @@ -129,7 +145,23 @@ class Domicilio { a.enfermera_id AS enfermera_lab_id, a.id AS asignacion_id, a.estado AS asignacion_estado, - adm.full_name AS creado_por_nombre + adm.full_name AS creado_por_nombre, + CASE + WHEN a.enfermera_id IS NULL OR d.hora_llegada IS NULL THEN NULL + ELSE TIMESTAMPDIFF( + MINUTE, + ( + SELECT MAX(CONCAT(d2.fecha_programada, ' ', d2.hora_salida)) + FROM lab_domicilios d2 + JOIN lab_asignaciones a2 ON a2.domicilio_id = d2.id + WHERE a2.enfermera_id = a.enfermera_id + AND d2.hora_salida IS NOT NULL + AND d2.id <> d.id + AND CONCAT(d2.fecha_programada, ' ', d2.hora_salida) < CONCAT(d.fecha_programada, ' ', d.hora_llegada) + ), + CONCAT(d.fecha_programada, ' ', d.hora_llegada) + ) + END AS tiempo_recorrido_min FROM lab_domicilios d JOIN lab_pacientes p ON p.id = d.paciente_id LEFT JOIN lab_ordenes_medicas o ON o.id = d.orden_id diff --git a/classes/lab/Enfermera.php b/classes/lab/Enfermera.php index 7487b11..122cd14 100644 --- a/classes/lab/Enfermera.php +++ b/classes/lab/Enfermera.php @@ -146,7 +146,8 @@ class Enfermera { LIMIT 1) ) AS orden_id_display, a.estado AS asignacion_estado, - a.id AS asignacion_id + a.id AS asignacion_id, + d.creado_por FROM lab_asignaciones a JOIN lab_domicilios d ON d.id = a.domicilio_id JOIN lab_pacientes p ON p.id = d.paciente_id diff --git a/conversations.php b/conversations.php index 5941ec3..b812ed4 100644 --- a/conversations.php +++ b/conversations.php @@ -6989,7 +6989,9 @@ const labDomicilio = (() => { } function limpiarValidacion() { - ['labdom-fecha','labdom-direccion'].forEach(id => $(id).classList.remove('is-invalid')); + ['labdom-fecha','labdom-direccion','labdom-pac-nombre','labdom-pac-cedula','labdom-pac-correo'].forEach(id => { + const el = $(id); if (el) el.classList.remove('is-invalid'); + }); } function validar() { @@ -6997,11 +6999,34 @@ const labDomicilio = (() => { limpiarValidacion(); if (!fv('labdom-fecha')) { $('labdom-fecha').classList.add('is-invalid'); ok = false; } if (!fv('labdom-direccion')){ $('labdom-direccion').classList.add('is-invalid'); ok = false; } + // Validar formato de dirección + const dirVal = fv('labdom-direccion') || ''; + if (dirVal && dirVal.trim().length < 5) { + $('labdom-direccion').classList.add('is-invalid'); ok = false; + } const esSeguro = document.getElementById('tc-seguro')?.checked; if (esSeguro && !fv('labdom-seguro-nombre')) { $('labdom-seguro-nombre').classList.add('is-invalid'); ok = false; } + // Validaciones de paciente nuevo + if (_isNewPaciente) { + const nom = fv('labdom-pac-nombre') || ''; + if (!nom) { $('labdom-pac-nombre').classList.add('is-invalid'); ok = false; } + else if (!/^[\p{L}\s'\-\.]+$/u.test(nom)) { $('labdom-pac-nombre').classList.add('is-invalid'); ok = false; } + else if (nom.trim().split(/\s+/).length < 2) { $('labdom-pac-nombre').classList.add('is-invalid'); ok = false; } + + const ced = fv('labdom-pac-cedula') || ''; + if (ced) { + const tipodoc = $('labdom-pac-tipodoc')?.value || 'CC'; + if (['CC','TI','RC','CE'].includes(tipodoc)) { + const digits = ced.replace(/[^0-9]/g, ''); + if (digits.length < 4 || digits.length > 12) { $('labdom-pac-cedula').classList.add('is-invalid'); ok = false; } + } + } + const cor = fv('labdom-pac-correo') || ''; + if (cor && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cor)) { $('labdom-pac-correo').classList.add('is-invalid'); ok = false; } + } return ok; } diff --git a/enfermero_portal.php b/enfermero_portal.php index cc465d3..2dada98 100644 --- a/enfermero_portal.php +++ b/enfermero_portal.php @@ -633,14 +633,14 @@ const portal = { ` ).join(''); - const btnCancelar = !['completado','cancelado','reprogramado'].includes(est) + const btnCancelar = !['completado','cancelado','reprogramado'].includes(est) && item.es_propio ? ` ❌ Cancelar ` : ''; - const btnReprogramar = !['completado','reprogramado'].includes(est) + const btnReprogramar = !['completado','reprogramado'].includes(est) && item.es_propio ? ` Reprogramar @@ -1664,6 +1664,7 @@ const agendaNueva = { return; } if (!direccion) { this._mostrarError('La dirección es obligatoria.'); return; } + if (direccion.length < 5) { this._mostrarError('La dirección es muy corta. Ej: Cra 15 # 32-10.'); return; } if (!fecha) { this._mostrarError('La fecha es obligatoria.'); return; } const btn = document.getElementById('na-btn-guardar'); @@ -1681,6 +1682,19 @@ const agendaNueva = { this._mostrarError('El nombre del nuevo paciente es obligatorio.'); return; } + // Validar formato del nombre + if (!/^[\p{L}\s'\-\.]+$/u.test(nombre)) { + btn.disabled = false; + btn.innerHTML = 'Agendar'; + this._mostrarError('El nombre solo debe contener letras y espacios (sin números ni símbolos).'); + return; + } + if (nombre.trim().split(/\s+/).length < 2) { + btn.disabled = false; + btn.innerHTML = 'Agendar'; + this._mostrarError('Ingresa nombre y apellido completos (mínimo 2 palabras).'); + return; + } try { const documento = document.getElementById('na-np-documento').value.trim(); if (!documento) { @@ -1689,6 +1703,36 @@ const agendaNueva = { this._mostrarError('El número de documento del paciente es obligatorio.'); return; } + // Validar formato del documento + const tipoDoc = document.getElementById('na-np-tipo-doc').value; + if (['CC','TI','RC','CE'].includes(tipoDoc)) { + const docDigits = documento.replace(/[^0-9]/g, ''); + if (docDigits.length < 4 || docDigits.length > 12) { + btn.disabled = false; + btn.innerHTML = 'Agendar'; + this._mostrarError('La cédula/TI debe tener entre 4 y 12 dígitos.'); + return; + } + } + // Validar teléfono si fue ingresado + const telRaw = document.getElementById('na-np-telefono').value.trim(); + if (telRaw) { + const telDigits = telRaw.replace(/[^0-9]/g, ''); + if (!/^(57)?3\d{9}$/.test(telDigits)) { + btn.disabled = false; + btn.innerHTML = 'Agendar'; + this._mostrarError('El celular debe comenzar por 3 y tener 10 dígitos. Ej: 3001234567.'); + return; + } + } + // Validar correo si fue ingresado + const emailVal = document.getElementById('na-np-email').value.trim(); + if (emailVal && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) { + btn.disabled = false; + btn.innerHTML = 'Agendar'; + this._mostrarError('El correo electrónico no es válido. Ej: nombre@dominio.com.'); + return; + } const rPac = await fetch('api/lab/save_paciente.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/lab_domicilios.php b/lab_domicilios.php index 7abb4f2..b87fcb3 100644 --- a/lab_domicilios.php +++ b/lab_domicilios.php @@ -682,7 +682,10 @@ async function cargarLista(pag = 1) { ${esc(dom.paciente_nombre)} ${esc(dom.barrio||'')} - ${dom.enfermera_nombre ? `${esc(dom.enfermera_nombre)}` : 'Sin asignar'} + + ${dom.enfermera_nombre ? `${esc(dom.enfermera_nombre)}` : 'Sin asignar'} + ${dom.tiempo_recorrido_min != null ? `Recorrido: ${formatearMinutos(dom.tiempo_recorrido_min)}` : ''} + ${esc(dom.estado)} ${(dom.pago_estado === 'pagado') @@ -782,6 +785,7 @@ async function verDomicilio(id) { Hora llegada${dom.hora_llegada ? dom.hora_llegada.slice(0,5) : '—'} Hora salida${dom.hora_salida ? dom.hora_salida.slice(0,5) : '—'} ${dom.hora_llegada && dom.hora_salida ? `Duración${calcDuracion(dom.hora_llegada, dom.hora_salida)}` : ''}` : ''} + ${dom.tiempo_recorrido_min != null ? `Tiempo recorrido${formatearMinutos(dom.tiempo_recorrido_min)} (desde fin turno anterior)` : ''} ${dom.temperatura_inicio != null ? `Temp. inicio${dom.temperatura_inicio}°C` : ''} ${dom.temperatura_salida != null ? `Temp. salida${dom.temperatura_salida}°C` : ''} Servicio${esc(dom.tipo_servicio||'—')} @@ -1383,6 +1387,15 @@ function calcDuracion(hl, hs) { return mins >= 60 ? `${Math.floor(mins/60)}h ${mins%60}min` : `${mins} min`; } +function formatearMinutos(mins) { + const n = Number(mins); + if (!Number.isFinite(n) || n < 0) return '—'; + if (n < 60) return `${n} min`; + const h = Math.floor(n / 60); + const m = n % 60; + return m ? `${h}h ${m}min` : `${h}h`; +} + /** * Renderiza el bloque de órdenes médicas (multi-archivo) en el detalle de domicilio. */ diff --git a/lab_pacientes.php b/lab_pacientes.php index 1f4e796..aac70eb 100644 --- a/lab_pacientes.php +++ b/lab_pacientes.php @@ -345,12 +345,38 @@ 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; + } + 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; + } + // ───────────────────────────────────────────────────────────────────── + // 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 de celular + if (datos.telefono && !/^(57)?3\d{9}$/.test(datos.telefono)) { + mostrarToast('El celular debe comenzar por 3 y tener 10 dígitos. Ej: 3001234567.', 'danger'); return; + } const r = await fetch('api/lab/save_paciente.php', { method: 'POST',