diff --git a/api/kpi_actividad.php b/api/kpi_actividad.php new file mode 100644 index 0000000..570d130 --- /dev/null +++ b/api/kpi_actividad.php @@ -0,0 +1,131 @@ + $hasta) [$desde, $hasta] = [$hasta, $desde]; + + // ── Mensajes entrantes por día (usuarios únicos) ───────── + $msgRows = $db->fetchAll(" + SELECT + DATE(created_at) AS fecha, + COUNT(DISTINCT user_id) AS usuarios, + COUNT(*) AS total_mensajes + FROM conversations + WHERE direction = 'incoming' + AND DATE(created_at) BETWEEN ? AND ? + GROUP BY DATE(created_at) + ORDER BY fecha ASC + ", [$desde, $hasta]); + + // ── Agendamientos (turnos creados) por día ─────────────── + $turnosRows = $db->fetchAll(" + SELECT + DATE(creado_at) AS fecha, + COUNT(*) AS agendamientos + FROM turnero_turnos + WHERE DATE(creado_at) BETWEEN ? AND ? + GROUP BY DATE(creado_at) + ORDER BY fecha ASC + ", [$desde, $hasta]); + + // ── Construir mapa fecha → datos combinados ────────────── + $dias = []; + + // Generar todos los días del rango para que no haya huecos + $cur = new DateTime($desde); + $fin = new DateTime($hasta); + while ($cur <= $fin) { + $f = $cur->format('Y-m-d'); + $dias[$f] = ['fecha' => $f, 'usuarios' => 0, 'total_mensajes' => 0, 'agendamientos' => 0]; + $cur->modify('+1 day'); + } + + foreach ($msgRows as $r) { + if (isset($dias[$r['fecha']])) { + $dias[$r['fecha']]['usuarios'] = (int)$r['usuarios']; + $dias[$r['fecha']]['total_mensajes'] = (int)$r['total_mensajes']; + } + } + foreach ($turnosRows as $r) { + if (isset($dias[$r['fecha']])) { + $dias[$r['fecha']]['agendamientos'] = (int)$r['agendamientos']; + } + } + + $porDia = array_values($dias); + + // ── Agrupado por día de la semana (0=Domingo … 6=Sábado) ─ + $semana = [ + 0 => ['dia' => 'Domingo', 'usuarios' => 0, 'agendamientos' => 0], + 1 => ['dia' => 'Lunes', 'usuarios' => 0, 'agendamientos' => 0], + 2 => ['dia' => 'Martes', 'usuarios' => 0, 'agendamientos' => 0], + 3 => ['dia' => 'Miércoles', 'usuarios' => 0, 'agendamientos' => 0], + 4 => ['dia' => 'Jueves', 'usuarios' => 0, 'agendamientos' => 0], + 5 => ['dia' => 'Viernes', 'usuarios' => 0, 'agendamientos' => 0], + 6 => ['dia' => 'Sábado', 'usuarios' => 0, 'agendamientos' => 0], + ]; + + foreach ($porDia as $d) { + $dow = (int)(new DateTime($d['fecha']))->format('w'); // 0=Dom + $semana[$dow]['usuarios'] += $d['usuarios']; + $semana[$dow]['agendamientos'] += $d['agendamientos']; + } + + // ── Totales del rango ──────────────────────────────────── + $totalUsuarios = array_sum(array_column($porDia, 'usuarios')); + $totalMensajes = array_sum(array_column($porDia, 'total_mensajes')); + $totalAgendamientos = array_sum(array_column($porDia, 'agendamientos')); + + // Día de semana pico (más usuarios) + $picoDow = array_keys($semana, max($semana, fn($a,$b) => $a['usuarios'] <=> $b['usuarios']))[0] ?? null; + $picoDow = array_reduce(array_keys($semana), fn($carry, $k) => ($semana[$k]['usuarios'] > $semana[$carry]['usuarios'] ? $k : $carry), 0); + + echo json_encode([ + 'ok' => true, + 'desde' => $desde, + 'hasta' => $hasta, + 'totales' => [ + 'usuarios' => $totalUsuarios, + 'mensajes' => $totalMensajes, + 'agendamientos' => $totalAgendamientos, + 'pico_semana' => $semana[$picoDow]['dia'] ?? '—', + ], + 'por_dia' => $porDia, + 'por_dia_semana' => array_values($semana), + ], JSON_UNESCAPED_UNICODE); + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => $e->getMessage()]); +} diff --git a/api/kpi_respuesta_asesor.php b/api/kpi_respuesta_asesor.php new file mode 100644 index 0000000..6b7eb30 --- /dev/null +++ b/api/kpi_respuesta_asesor.php @@ -0,0 +1,212 @@ + $hasta) [$desde, $hasta] = [$hasta, $desde]; + + // ── Sesiones de atención en el rango ───────────────────────────────── + // Para cada "attend" buscamos: + // a) El primer mensaje outgoing del mismo user_id DESPUÉS del attend + // y dentro de 4 horas (descartamos bots post-sesión). + // b) El primer "finish" del mismo user_id DESPUÉS del attend. + $sesiones = $db->fetchAll(" + SELECT + oa.id AS sesion_id, + oa.user_id, + oa.operator_id, + COALESCE(au.full_name, au.username, CONCAT('Asesor #', oa.operator_id)) AS operador, + oa.created_at AS attend_at, + + -- Primera respuesta outgoing del asesor + ( + SELECT MIN(c.created_at) + FROM conversations c + WHERE c.user_id = oa.user_id + AND c.direction = 'outgoing' + AND c.created_at > oa.created_at + AND c.created_at < oa.created_at + INTERVAL 4 HOUR + ) AS primera_respuesta_at, + + -- Fin de la sesión + ( + SELECT MIN(oa2.created_at) + FROM operator_activity oa2 + WHERE oa2.user_id = oa.user_id + AND oa2.action = 'finish' + AND oa2.created_at > oa.created_at + AND oa2.created_at < oa.created_at + INTERVAL 8 HOUR + ) AS finish_at + + FROM operator_activity oa + LEFT JOIN admin_users au ON au.id = oa.operator_id + WHERE oa.action = 'attend' + AND DATE(oa.created_at) BETWEEN ? AND ? + ORDER BY oa.created_at ASC + ", [$desde, $hasta]); + + // ── Calcular métricas por sesión ───────────────────────────────────── + $totalSesiones = 0; + $conRespuesta = 0; + $sumaMinRespuesta = 0; + $sumaMinDuracion = 0; + $conDuracion = 0; + $sinRespuesta = 0; + + // Por asesor + $asesores = []; + + // Por día (para tendencia) + $porDia = []; + + // Histograma: cubos < 1, 1-3, 3-5, 5-10, 10-20, >20 minutos + $hist = ['< 1 min' => 0, '1-3 min' => 0, '3-5 min' => 0, '5-10 min' => 0, '10-20 min' => 0, '> 20 min' => 0, 'Sin resp.' => 0]; + + foreach ($sesiones as $s) { + $totalSesiones++; + + $attendDt = new DateTime($s['attend_at']); + $fechaDia = $attendDt->format('Y-m-d'); + + if (!isset($porDia[$fechaDia])) { + $porDia[$fechaDia] = ['fecha' => $fechaDia, 'sesiones' => 0, 'suma_resp' => 0, 'con_resp' => 0]; + } + $porDia[$fechaDia]['sesiones']++; + + $opId = $s['operator_id'] ?? 0; + if (!isset($asesores[$opId])) { + $asesores[$opId] = [ + 'operador' => $s['operador'], + 'sesiones' => 0, + 'con_resp' => 0, + 'suma_resp' => 0, + 'suma_dur' => 0, + 'con_dur' => 0, + ]; + } + $asesores[$opId]['sesiones']++; + + // Tiempo de primera respuesta + if (!empty($s['primera_respuesta_at'])) { + $respDt = new DateTime($s['primera_respuesta_at']); + $minResp = round(($respDt->getTimestamp() - $attendDt->getTimestamp()) / 60, 1); + if ($minResp < 0) $minResp = 0; + + $conRespuesta++; + $sumaMinRespuesta += $minResp; + $asesores[$opId]['con_resp']++; + $asesores[$opId]['suma_resp'] += $minResp; + $porDia[$fechaDia]['suma_resp'] += $minResp; + $porDia[$fechaDia]['con_resp']++; + + // Histograma + if ($minResp < 1) $hist['< 1 min']++; + elseif ($minResp < 3) $hist['1-3 min']++; + elseif ($minResp < 5) $hist['3-5 min']++; + elseif ($minResp < 10) $hist['5-10 min']++; + elseif ($minResp < 20) $hist['10-20 min']++; + else $hist['> 20 min']++; + } else { + $sinRespuesta++; + $hist['Sin resp.']++; + } + + // Duración de la sesión + if (!empty($s['finish_at'])) { + $finDt = new DateTime($s['finish_at']); + $minDur = round(($finDt->getTimestamp() - $attendDt->getTimestamp()) / 60, 1); + if ($minDur >= 0) { + $conDuracion++; + $sumaMinDuracion += $minDur; + $asesores[$opId]['suma_dur'] += $minDur; + $asesores[$opId]['con_dur']++; + } + } + } + + $promedioRespuesta = $conRespuesta > 0 ? round($sumaMinRespuesta / $conRespuesta, 1) : null; + $promedioDuracion = $conDuracion > 0 ? round($sumaMinDuracion / $conDuracion, 1) : null; + $pctRapidas = $totalSesiones > 0 ? round(($hist['< 1 min'] + $hist['1-3 min']) / $totalSesiones * 100) : 0; + + // Formatear asesores + $listaAsesores = []; + foreach ($asesores as $a) { + $listaAsesores[] = [ + 'operador' => $a['operador'], + 'sesiones' => $a['sesiones'], + 'prom_respuesta' => $a['con_resp'] > 0 ? round($a['suma_resp'] / $a['con_resp'], 1) : null, + 'prom_duracion' => $a['con_dur'] > 0 ? round($a['suma_dur'] / $a['con_dur'], 1) : null, + ]; + } + usort($listaAsesores, fn($a, $b) => ($a['prom_respuesta'] ?? 9999) <=> ($b['prom_respuesta'] ?? 9999)); + + // Rellenar días sin actividad en el rango + $cur = new DateTime($desde); + $fin = new DateTime($hasta); + while ($cur <= $fin) { + $f = $cur->format('Y-m-d'); + if (!isset($porDia[$f])) $porDia[$f] = ['fecha' => $f, 'sesiones' => 0, 'suma_resp' => 0, 'con_resp' => 0]; + $cur->modify('+1 day'); + } + ksort($porDia); + + $serieDia = array_map(fn($d) => [ + 'fecha' => $d['fecha'], + 'sesiones' => $d['sesiones'], + 'prom_respuesta' => $d['con_resp'] > 0 ? round($d['suma_resp'] / $d['con_resp'], 1) : null, + ], array_values($porDia)); + + echo json_encode([ + 'ok' => true, + 'desde' => $desde, + 'hasta' => $hasta, + 'totales' => [ + 'sesiones' => $totalSesiones, + 'con_respuesta' => $conRespuesta, + 'sin_respuesta' => $sinRespuesta, + 'prom_respuesta' => $promedioRespuesta, + 'prom_duracion' => $promedioDuracion, + 'pct_rapidas' => $pctRapidas, + ], + 'asesores' => $listaAsesores, + 'histograma' => array_map(fn($k, $v) => ['rango' => $k, 'sesiones' => $v], array_keys($hist), $hist), + 'serie_dia' => $serieDia, + ], JSON_UNESCAPED_UNICODE); + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => $e->getMessage()]); +} diff --git a/cronograma_turnero.html b/cronograma_turnero.html new file mode 100644 index 0000000..b3765a2 --- /dev/null +++ b/cronograma_turnero.html @@ -0,0 +1,456 @@ + + + + + +Cronograma — Sistema Turnero Inteligente (60 días) + + + + + +
+

📅 Cronograma de Implementación — Sistema Turnero Inteligente

+
Proyecto: ERP Multi-Módulo  ·  Laboratorio Clínico  ·  14 Abril – 20 Junio 2026
+
+
67
Días calendario
+
10
Semanas
+
9
Entregables
+
1/10
Completado
+
20 Jun
Go-live
+
+
+ + +
+ Referencias: + Completado + Desarrollo (BD + APIs) + Desarrollo (Vistas) + Desarrollo (Admin) + Pruebas / QA + Capacitación + Go-Live / Producción +
+ + +
+ + + 67 días calendario · 14 Abr – 20 Jun 2026 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FaseActividadEntregables principalesDíasInicioFinEstadoS114-20 AbrS221-27 AbrS328 Abr-4 MayS45-11 MayS512-18 MayS619-25 MayS726 May-1 JunS82-8 JunS99-15 JunS1016-20 Jun
⚙️ BLOQUE 1 — ADECUACIÓN ERP BASE    (Semana 1 — COMPLETADO)
1ERPAdecuar Software como ERPEstructura core/ · modules/ · shared/ · Router centralizado · Sidebar dinámico · SYSTEM_MODULES en BD · 11 módulos migrados a nueva estructura714 Abr20 Abr✅ Completado·········
🗄️ BLOQUE 2 — BASE DE DATOS DEL TURNERO — FASE 3.1    (Semana 2)
2F 3.1Migración 003 — Tablas Turneroexam_tipos · exam_tipo_consentimientos · turnero_lugares · turnero_prioridades · turnero_sesiones · turnero_turnos · turnero_solicitudes · turnero_examen_items · turnero_consentimientos · ALTER lab_formularios521 Abr25 Abr🔵 Próximo·········
3F 3.1Datos iniciales + module.phpPrioridades A-F por defecto · Registro en system_modules · module.php descriptor · Asignación roles recepcionista / auxiliar226 Abr27 Abr⬜ Pendiente·········
⚡ BLOQUE 3 — MOTOR DE PRIORIDADES + APIs CORE — FASE 3.2    (Semana 3)
4F 3.2APIs de turno y solicitudcreate_turno.php (sesión diaria + correlativo) · create_solicitud.php (exámenes + pago + lugar + consentimientos auto) · Actualización estado turno428 Abr01 May⬜ Pendiente·········
5F 3.2Motor de cola + SSEllamar_turno.php (motor prioridad + FIFO) · cambiar_estado.php (validaciones bloqueo) · get_cola.php · sse_turno.php (Server-Sent Events)302 May04 May⬜ Pendiente·········
📺 BLOQUE 4 — KIOSKO + PANTALLAS TV — FASE 3.3    (Semana 4)
6F 3.3Kiosko táctilviews/kiosko.php fullscreen · Botones A-F (icono + descripción) · Campo nombre/celular (opcional) · Código generado (ej. "E-042") · Sin exposición de datos305 May07 May⬜ Pendiente·········
7F 3.3Pantallas TV / Displayviews/display.php · ?display=recepcion (cola general) · ?display=lugar&lugar_id=X (cola del lugar) · SSE en tiempo real · Colores prioridad + animación408 May11 May⬜ Pendiente·········
🖥️ BLOQUE 5 — PUESTO DE RECEPCIÓN — FASE 3.4    (Semana 5)
8F 3.4aVista Recepción — Atención al pacienteviews/recepcion.php · Cola general con prioridad visual · Llamar siguiente · Buscador/creación paciente (reutiliza lab_pacientes) · Catálogo exam_tipos + selector412 May15 May⬜ Pendiente·········
9F 3.4bSolicitudes + Pago + Lugar destinoRegistro pago (monto + método) · Selector Lugar destino · Botón Crear Solicitud · create_solicitud.php · Cálculo auto consentimientos requeridos316 May18 May⬜ Pendiente·········
10F 3.4cEnvío Consentimientos por WhatsAppsend_consentimiento.php · Deduplicación de formularios · Tokens UUID únicos · WhatsAppService + plantilla Meta "consentimiento_turno" · Estados en tiempo real719 May25 May⬜ Pendiente·········
🔬 BLOQUE 6 — PUESTO LUGAR / ESTACIÓN DE SERVICIO — FASE 3.5    (Semana 7)
11F 3.5Vista Lugar / Estación genéricaviews/lugar.php (?lugar_id=X) · Cola priorizada del lugar · Llamar siguiente · Ficha: datos + exámenes + estado consentimientos · Bloqueo si pendientes426 May29 May⬜ Pendiente·········
12F 3.5Flujo de servicio: Iniciar / FinalizarBotón "Iniciar atención" (habilitado si consentimientos OK) · Botón "Finalizar" → estado finalizado · Botón "Ausente" / Regresar a cola · polling 5s330 May01 Jun⬜ Pendiente·········
✍️ BLOQUE 7 — FIRMA DIGITAL DE CONSENTIMIENTOS — FASE 3.6    (Semana 8)
13F 3.6Integración firma en flujo turneroCampo tipo en lab_formularios · ver_formulario_enviado.php detecta token de turno · Actualiza turnero_consentimientos · Registro evidencia: IP, UA, fecha, versión402 Jun05 Jun⬜ Pendiente·········
14F 3.6"Firmar aquí" en tablet del puestoBotón "Firmar aquí" en lugar.php → modal/iframe con ver_formulario_enviado.php · Firma física en pantalla del auxiliar · Detección en tiempo real (polling 5s)306 Jun08 Jun⬜ Pendiente·········
⚙️ BLOQUE 8 — PANEL ADMIN + CONFIGURACIÓN — FASE 3.7    (Semana 9)
15F 3.7Configuración: Lugares + ExámenesTab Lugares: CRUD + URL TV automática · Tab Exámenes y Consentimientos: M:N (exam_tipos ↔ formularios) · Vista inversa por consentimiento409 Jun12 Jun⬜ Pendiente·········
16F 3.7Dashboard Admin + Prioridades + SesiónDashboard: KPIs del día + tiempo promedio + exportar CSV · Tab Prioridades (CRUD) · Tab Sesiones (abrir/cerrar/historial) · Tab WhatsApp (plantilla Meta)313 Jun15 Jun⬜ Pendiente·········
🧪 BLOQUE 9 — QA · CAPACITACIÓN · GO-LIVE    (Semana 10 · 16–20 Jun)
17QATesting end-to-end y corrección de bugsFlujo completo paciente · Múltiples lugares simultáneos · SSE stress test · Consentimientos en tiempo real · Validación OWASP Top 10216 Jun17 Jun🟡 Pendiente·········
18CAPCapacitación del equipoManual recepcionista · Manual auxiliar/lugar · Manual administrador · Capacitación presencial o remota · Guías rápidas por rol118 Jun18 Jun🟣 Pendiente·········
19DEPLOYGo-Live y soporte post-lanzamientoDeploy en producción · Configuración inicial: lugares, exámenes, prioridades, plantillas Meta · Prueba en producción · Monitoreo · Ajustes finales219 Jun20 Jun🟢 Pendiente·········
+
+ + +
+

📌 Notas del Cronograma

+
+
+ 🔄 Metodología incremental +

Desarrollo iterativo. El sistema sigue funcionando en producción durante toda la implementación. Los módulos legacy (lab_*.php) permanecen activos hasta que su equivalente nuevo esté validado.

+
+
+ 🔗 Reutilización de componentes +

Los consentimientos reutilizan el módulo de Formularios existente. El buscador de pacientes reutiliza lab_pacientes. WhatsApp reutiliza el servicio ya activo. La firma digital ya está implementada.

+
+
+ ⚠️ Dependencias críticas +

El Bloque 3 (API Core) debe completarse antes de iniciar las vistas (Bloques 4–7). La plantilla Meta "consentimiento_turno" debe aprobarse en WhatsApp Business Manager preferiblemente en la semana 3. La última semana (S10) es compacta: QA + capacitación + go-live en 5 días — cualquier retraso acumulado en semanas anteriores impacta directamente la fecha de lanzamiento.

+
+
+ 📱 Compatibilidad móvil +

Kiosko: táctil fullscreen (tablet Android/iPad). Display TV: cualquier pantalla con navegador. Puesto Recepción y Lugar: PC o tablet. Firma paciente: cualquier celular vía WhatsApp.

+
+
+ 🛡️ Seguridad OWASP +

Todas las APIs implementan: requireMethod() + verificar sesión + PDO preparado + sanitización de parámetros. Pantallas sin login (kiosko, TV) nunca exponen datos sensibles.

+
+
+ 📊 Soporte post-lanzamiento +

30 días de soporte incluidos tras el go-live (20 Jun – 20 Jul 2026) para ajustes basados en uso real, reportes de KPIs, y optimizaciones de UX identificadas en producción.

+
+
+
+ + + + + + diff --git a/enfermero_portal.php b/enfermero_portal.php index 54b39a2..cc465d3 100644 --- a/enfermero_portal.php +++ b/enfermero_portal.php @@ -1238,8 +1238,9 @@ const formVer = {
diff --git a/index.php b/index.php index 34995ae..ef76387 100644 --- a/index.php +++ b/index.php @@ -202,6 +202,194 @@ try {
+ +
+
+
+
+
Actividad de Pacientes — KPI
+
+ +
+ + + +
+ + + + + +
+
+
+ +
+
+
+
+
Usuarios únicos
+
+
+
+
+
+
Mensajes recibidos
+
+
+
+
+
+
Agendamientos
+
+
+
+
+
+
Día más activo
+
+
+
+ + +
+
+
+
+
Por día
+
+ +
+
+
+
+
+
+
+
Por día de la semana
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
Tiempo de Respuesta de Asesores
+
+
+ + + +
+ + + + +
+
+
+ +
+
+
+
+
Atenciones
+
+
+
+
+
+
Prom. 1ª respuesta
+
+
+
+
+
+
Prom. duración
+
+
+
+
+
+
Resp. < 3 min
+
+
+
+
+
+
Sin respuesta
+
+
+
+ + +
+
+
+
+
Promedio de respuesta por día (minutos)
+
+ +
+
+
+
+
+
+
+
Distribución de tiempos
+
+ +
+
+
+
+
+ + +
+
+
Detalle por asesor
+
+ + + + + + + + + + + + + +
AsesorAtencionesProm. 1ª respuestaProm. duraciónVelocidad
+
+
+
+ +
+
+
+
+ +
@@ -2312,7 +2500,320 @@ try { }); })(); - + + + + + \ No newline at end of file diff --git a/lab_domicilios.php b/lab_domicilios.php index 6090d39..7abb4f2 100644 --- a/lab_domicilios.php +++ b/lab_domicilios.php @@ -963,7 +963,7 @@ async function cargarNotasDetalle(domId) { html += `
${esc(n.titulo||'Sin título')}
${n.cuerpo||''}
- ${n.imagen_path ? `` : ''} + ${_renderArchivosAdmin(n)}
${esc(n.created_at||'')}
`; }); @@ -1054,7 +1054,8 @@ async function verInformeDom() { `
${esc(n.titulo||'Sin título')}
${n.cuerpo||''}
- ${n.imagen_path ? `` : ''} + ${_renderArchivosAdmin(n)} +
${esc(n.enfermera_nombre||'')}${n.created_at ? ' · ' + esc(n.created_at) : ''}
` ).join(''); } else { @@ -1332,6 +1333,37 @@ function exportarDomicilios() { // ── Utils ────────────────────────────────────────────────────────────────── const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c])); + +// Renderiza los archivos adjuntos de una nota (soporta nuevo campo `archivos` JSON +// y el campo legacy `imagen_path`). +function _renderArchivosAdmin(n) { + let lista = []; + if (n.archivos) { + try { lista = typeof n.archivos === 'string' ? JSON.parse(n.archivos) : n.archivos; } catch(e) {} + } + if (!lista.length && n.imagen_path) { + lista = [{ nombre: n.imagen_path, path: n.imagen_path, mime: 'image/jpeg', size: 0 }]; + } + if (!lista.length) return ''; + + return '
' + lista.map(a => { + const src = `uploads/media/${esc(a.path)}`; + const esImg = (a.mime || '').startsWith('image/'); + if (esImg) { + return ``; + } + const icon = (a.mime||'').includes('pdf') ? 'fa-file-pdf text-danger' + : (a.mime||'').includes('word') ? 'fa-file-word text-primary' + : (a.mime||'').includes('excel') || (a.mime||'').includes('sheet') ? 'fa-file-excel text-success' + : 'fa-file text-secondary'; + return ` + +
${esc(a.nombre)}
+
`; + }).join('') + '
'; +} function formatFechaNac(fnac) { if (!fnac) return '—'; const d = new Date(fnac + 'T00:00:00'); diff --git a/lab_pacientes.php b/lab_pacientes.php index 3b4919c..1f4e796 100644 --- a/lab_pacientes.php +++ b/lab_pacientes.php @@ -118,9 +118,13 @@ require_once __DIR__ . '/shared/components/sidebar.php';
@@ -131,8 +135,9 @@ require_once __DIR__ . '/shared/components/sidebar.php';
@@ -212,7 +217,7 @@ async function cargarLista(pag = 1) {
${esc(p.nombre_completo)}
${p.phone_number ? ` ${esc(p.phone_number)}` : ''} - ${esc(p.tipo_documento)} ${esc(p.numero_documento||'—')} + ${tipoDocLabel(p.tipo_documento)} ${esc(p.numero_documento||'—')} ${esc(p.telefono||'—')} ${esc(p.eps||'—')} ${p.total_ordenes||0} @@ -257,7 +262,7 @@ async function verDetalle(id) { document.getElementById('detail-body').innerHTML = `
-
Documento
${esc(p.tipo_documento)} ${esc(p.numero_documento||'—')}
+
Documento
${tipoDocLabel(p.tipo_documento)} ${esc(p.numero_documento||'—')}
Teléfono
${esc(p.telefono||'—')}
WhatsApp
${p.phone_number ? ` ${esc(p.phone_number)}` : '—'}
${p.email ? `
Email
${esc(p.email)}
` : ''} @@ -307,7 +312,10 @@ function abrirFormulario(p = null) { document.getElementById('pac-tipo-doc').value= p.tipo_documento || 'CC'; document.getElementById('pac-tel-prefijo').value = '57'; let telRaw = p.telefono || ''; - if (/^(57|58)\d{10}$/.test(telRaw)) { + 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); } @@ -362,6 +370,16 @@ async function guardarPaciente() { // ── Utils ────────────────────────────────────────────────────────────────── const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c])); +const TIPO_DOC = { + CC: 'CC — Cédula de Ciudadanía', + CE: 'CE — Cédula de Extranjería', + TI: 'TI — Tarjeta de Identidad', + PA: 'PA — Pasaporte', + DE: 'DE — Documento Extranjero', + NIT: 'NIT', + RC: 'RC — Registro Civil', +}; +const tipoDocLabel = t => TIPO_DOC[t] || esc(t||'—'); const formatFecha = s => s ? new Date(s).toLocaleDateString('es-CO') : '—'; const colorEstado = e => ({ pendiente:'warning', en_revision:'info', autorizada:'success',