cambios
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -20,6 +20,14 @@ try {
|
||||
// ── Reprogramar ──
|
||||
if (!empty($datos['id']) && !empty($datos['reprogramar'])) {
|
||||
$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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+46
-2
@@ -633,14 +633,14 @@ const portal = {
|
||||
</button>`
|
||||
).join('');
|
||||
|
||||
const btnCancelar = !['completado','cancelado','reprogramado'].includes(est)
|
||||
const btnCancelar = !['completado','cancelado','reprogramado'].includes(est) && item.es_propio
|
||||
? `<button class="btn btn-outline-danger btn-accion"
|
||||
onclick="portal.abrirCancelar(${item.domicilio_id})">
|
||||
❌ Cancelar
|
||||
</button>`
|
||||
: '';
|
||||
|
||||
const btnReprogramar = !['completado','reprogramado'].includes(est)
|
||||
const btnReprogramar = !['completado','reprogramado'].includes(est) && item.es_propio
|
||||
? `<button class="btn btn-accion text-white" style="background:#6f42c1"
|
||||
onclick="portal.abrirReprogramar(${item.domicilio_id}, '${(item.hora_programada||'').slice(0,5)}')">
|
||||
<i class="fas fa-calendar-alt me-1"></i>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 = '<i class="fas fa-calendar-check me-1"></i>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 = '<i class="fas fa-calendar-check me-1"></i>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 = '<i class="fas fa-calendar-check me-1"></i>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 = '<i class="fas fa-calendar-check me-1"></i>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 = '<i class="fas fa-calendar-check me-1"></i>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' },
|
||||
|
||||
+14
-1
@@ -682,7 +682,10 @@ async function cargarLista(pag = 1) {
|
||||
<div class="fw-semibold">${esc(dom.paciente_nombre)}</div>
|
||||
<small class="text-muted">${esc(dom.barrio||'')}</small>
|
||||
</td>
|
||||
<td>${dom.enfermera_nombre ? `<small>${esc(dom.enfermera_nombre)}</small>` : '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>Sin asignar</span>'}</td>
|
||||
<td>
|
||||
${dom.enfermera_nombre ? `<small>${esc(dom.enfermera_nombre)}</small>` : '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>Sin asignar</span>'}
|
||||
${dom.tiempo_recorrido_min != null ? `<div><span class="badge rounded-pill mt-1" style="background:#e0f2fe;color:#075985;border:1px solid #7dd3fc">Recorrido: ${formatearMinutos(dom.tiempo_recorrido_min)}</span></div>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-${COLOR_DOM[dom.estado]||'secondary'} estado-badge">${esc(dom.estado)}</span>
|
||||
${(dom.pago_estado === 'pagado')
|
||||
@@ -782,6 +785,7 @@ async function verDomicilio(id) {
|
||||
<dt class="col-5 text-muted">Hora llegada</dt><dd class="col-7 text-success fw-semibold">${dom.hora_llegada ? dom.hora_llegada.slice(0,5) : '—'}</dd>
|
||||
<dt class="col-5 text-muted">Hora salida</dt><dd class="col-7 text-danger fw-semibold">${dom.hora_salida ? dom.hora_salida.slice(0,5) : '—'}</dd>
|
||||
${dom.hora_llegada && dom.hora_salida ? `<dt class="col-5 text-muted">Duración</dt><dd class="col-7 fw-semibold text-info">${calcDuracion(dom.hora_llegada, dom.hora_salida)}</dd>` : ''}` : ''}
|
||||
${dom.tiempo_recorrido_min != null ? `<dt class="col-5 text-muted">Tiempo recorrido</dt><dd class="col-7 fw-semibold" style="color:#0369a1">${formatearMinutos(dom.tiempo_recorrido_min)} <span class="text-muted">(desde fin turno anterior)</span></dd>` : ''}
|
||||
${dom.temperatura_inicio != null ? `<dt class="col-5 text-muted"><i class="fas fa-thermometer-half me-1 text-primary"></i>Temp. inicio</dt><dd class="col-7 fw-semibold text-primary">${dom.temperatura_inicio}°C</dd>` : ''}
|
||||
${dom.temperatura_salida != null ? `<dt class="col-5 text-muted"><i class="fas fa-thermometer-full me-1 text-warning"></i>Temp. salida</dt><dd class="col-7 fw-semibold text-warning">${dom.temperatura_salida}°C</dd>` : ''}
|
||||
<dt class="col-5 text-muted">Servicio</dt><dd class="col-7">${esc(dom.tipo_servicio||'—')}</dd>
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user