Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa0f4ac68a |
+6
-1
@@ -36,7 +36,12 @@ RUN apk add --no-cache curl-dev \
|
|||||||
opcache
|
opcache
|
||||||
|
|
||||||
# Instalar Redis extension (versión fija para cache reproducible)
|
# Instalar Redis extension (versión fija para cache reproducible)
|
||||||
RUN pecl install redis-6.0.2 && docker-php-ext-enable redis
|
# Se descarga por HTTPS de forma explícita: el filtro de red perimetral
|
||||||
|
# responde 403 a los .tgz servidos por HTTP, lo que rompía "pecl install".
|
||||||
|
RUN apk add --no-cache curl \
|
||||||
|
&& curl -fsSL https://pecl.php.net/get/redis-6.0.2.tgz -o /tmp/redis-6.0.2.tgz \
|
||||||
|
&& pecl install /tmp/redis-6.0.2.tgz \
|
||||||
|
&& docker-php-ext-enable redis
|
||||||
|
|
||||||
# Instalar Composer
|
# Instalar Composer
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php';
|
|
||||||
requireMethod('POST');
|
requireMethod('POST');
|
||||||
requireTurnero();
|
requireTurnero();
|
||||||
|
|
||||||
@@ -234,27 +233,6 @@ try {
|
|||||||
|
|
||||||
notificarSSE((int) $turnoActualizado['sesion_id']);
|
notificarSSE((int) $turnoActualizado['sesion_id']);
|
||||||
|
|
||||||
// Queda constancia de quién cambió el estado a mano. Sin esto, en el
|
|
||||||
// historial aparecía un turno cancelado o ausente sin ninguna huella
|
|
||||||
// de quién lo hizo ni desde qué estado.
|
|
||||||
try {
|
|
||||||
(new ActividadAdmin())->registrar(
|
|
||||||
adminId(),
|
|
||||||
'turnero',
|
|
||||||
'cambiar_estado',
|
|
||||||
$turnoId,
|
|
||||||
[
|
|
||||||
'turno_id' => $turnoId,
|
|
||||||
'turno_codigo' => $turnoActualizado['codigo'] ?? null,
|
|
||||||
'estado_previo' => $estadoActual,
|
|
||||||
'estado_nuevo' => $nuevoEstado,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// La auditoría nunca debe tumbar la operación
|
|
||||||
error_log('[cambiar_estado] auditoría: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonOk(['turno' => $turnoActualizado], "Estado actualizado a '{$nuevoEstado}'");
|
jsonOk(['turno' => $turnoActualizado], "Estado actualizado a '{$nuevoEstado}'");
|
||||||
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
* Body: { consentimiento_id }
|
* Body: { consentimiento_id }
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php';
|
|
||||||
requireMethod('POST');
|
requireMethod('POST');
|
||||||
requireTurnero();
|
requireTurnero();
|
||||||
|
|
||||||
@@ -31,35 +30,4 @@ $stmt->execute([$turnoId, $consentId]);
|
|||||||
|
|
||||||
if ($stmt->rowCount() === 0) jsonError('Toma no encontrada o el paciente no coincide.', 404);
|
if ($stmt->rowCount() === 0) jsonError('Toma no encontrada o el paciente no coincide.', 404);
|
||||||
|
|
||||||
// Cancelar una toma pendiente cierra un protocolo que quedó a medias en otra
|
|
||||||
// visita: conviene saber quién lo dio por terminado.
|
|
||||||
try {
|
|
||||||
$inf = $pdo->prepare(
|
|
||||||
"SELECT tc.turno_id, t.codigo, f.nombre AS formulario
|
|
||||||
FROM turnero_consentimientos tc
|
|
||||||
JOIN turnero_turnos t ON t.id = tc.turno_id
|
|
||||||
JOIN lab_formularios f ON f.id = tc.formulario_id
|
|
||||||
WHERE tc.id = ? LIMIT 1"
|
|
||||||
);
|
|
||||||
$inf->execute([$consentId]);
|
|
||||||
$d = $inf->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
||||||
|
|
||||||
(new ActividadAdmin())->registrar(
|
|
||||||
adminId(),
|
|
||||||
'turnero',
|
|
||||||
'cancelar_toma_pendiente',
|
|
||||||
$consentId,
|
|
||||||
[
|
|
||||||
// Se registra bajo el turno donde se hizo la cancelación, que es
|
|
||||||
// donde alguien la va a buscar en el historial.
|
|
||||||
'turno_id' => $turnoId,
|
|
||||||
'turno_origen_id' => isset($d['turno_id']) ? (int)$d['turno_id'] : null,
|
|
||||||
'turno_origen' => $d['codigo'] ?? null,
|
|
||||||
'formulario_nombre' => $d['formulario'] ?? null,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('[cancelar_toma_pendiente] auditoría: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonOk(['cancelado' => true]);
|
jsonOk(['cancelado' => true]);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
* - resumen global (total, atendidos, pendientes, ausentes, tiempo promedio)
|
* - resumen global (total, atendidos, pendientes, ausentes, tiempo promedio)
|
||||||
* - desglose por prioridad
|
* - desglose por prioridad
|
||||||
* - desglose por lugar
|
* - desglose por lugar
|
||||||
* - desglose por recepcionista y por bacteriólogo (quién atendió a cuántos)
|
|
||||||
* - lista de turnos del día (para tabla detalle)
|
* - lista de turnos del día (para tabla detalle)
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
@@ -44,8 +43,6 @@ if (!$sesion) {
|
|||||||
],
|
],
|
||||||
'por_prioridad' => [],
|
'por_prioridad' => [],
|
||||||
'por_lugar' => [],
|
'por_lugar' => [],
|
||||||
'por_recepcionista' => [],
|
|
||||||
'por_bacteriologo' => [],
|
|
||||||
'consent_stats' => [],
|
'consent_stats' => [],
|
||||||
'turnos' => [],
|
'turnos' => [],
|
||||||
]);
|
]);
|
||||||
@@ -128,55 +125,6 @@ $stmtLug = $pdo->prepare(
|
|||||||
$stmtLug->execute([$sesionId]);
|
$stmtLug->execute([$sesionId]);
|
||||||
$porLugar = $stmtLug->fetchAll(PDO::FETCH_ASSOC);
|
$porLugar = $stmtLug->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// ── 4b. Desglose por recepcionista ────────────────────────────
|
|
||||||
// Mismo molde que el ranking de enfermeras de lab_domicilios
|
|
||||||
// (api/lab/get_metricas.php). atendido_recepcion_por guarda al PRIMER
|
|
||||||
// recepcionista que tomó el turno (COALESCE en cambiar_estado.php).
|
|
||||||
$stmtRec = $pdo->prepare(
|
|
||||||
"SELECT u.id,
|
|
||||||
COALESCE(NULLIF(u.full_name, ''), u.username) AS nombre,
|
|
||||||
COUNT(t.id) AS total,
|
|
||||||
SUM(t.estado IN ('finalizado','en_servicio','en_espera_lugar')) AS atendidos,
|
|
||||||
SUM(t.estado = 'ausente') AS ausentes,
|
|
||||||
SUM(t.estado = 'cancelado') AS cancelados,
|
|
||||||
ROUND(AVG(
|
|
||||||
CASE
|
|
||||||
WHEN t.fin_recepcion_at IS NOT NULL AND t.inicio_recepcion_at IS NOT NULL
|
|
||||||
THEN TIMESTAMPDIFF(SECOND, t.inicio_recepcion_at, t.fin_recepcion_at) / 60.0
|
|
||||||
END
|
|
||||||
), 1) AS tiempo_promedio_min
|
|
||||||
FROM turnero_turnos t
|
|
||||||
JOIN admin_users u ON u.id = t.atendido_recepcion_por
|
|
||||||
WHERE t.sesion_id = ?
|
|
||||||
GROUP BY u.id, u.full_name, u.username
|
|
||||||
ORDER BY atendidos DESC, total DESC"
|
|
||||||
);
|
|
||||||
$stmtRec->execute([$sesionId]);
|
|
||||||
$porRecepcionista = $stmtRec->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// ── 4c. Desglose por bacteriólogo (quien atendió en el lugar) ──
|
|
||||||
$stmtBac = $pdo->prepare(
|
|
||||||
"SELECT u.id,
|
|
||||||
COALESCE(NULLIF(u.full_name, ''), u.username) AS nombre,
|
|
||||||
COUNT(t.id) AS total,
|
|
||||||
SUM(t.estado = 'finalizado') AS finalizados,
|
|
||||||
SUM(t.estado = 'en_servicio') AS en_servicio,
|
|
||||||
SUM(t.estado = 'ausente') AS ausentes,
|
|
||||||
ROUND(AVG(
|
|
||||||
CASE
|
|
||||||
WHEN t.fin_lugar_at IS NOT NULL AND t.inicio_lugar_at IS NOT NULL
|
|
||||||
THEN TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, t.fin_lugar_at) / 60.0
|
|
||||||
END
|
|
||||||
), 1) AS tiempo_promedio_min
|
|
||||||
FROM turnero_turnos t
|
|
||||||
JOIN admin_users u ON u.id = t.atendido_lugar_por
|
|
||||||
WHERE t.sesion_id = ?
|
|
||||||
GROUP BY u.id, u.full_name, u.username
|
|
||||||
ORDER BY finalizados DESC, total DESC"
|
|
||||||
);
|
|
||||||
$stmtBac->execute([$sesionId]);
|
|
||||||
$porBacteriologo = $stmtBac->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// ── 5. Lista de turnos del día ────────────────────────────────
|
// ── 5. Lista de turnos del día ────────────────────────────────
|
||||||
$stmtTurnos = $pdo->prepare(
|
$stmtTurnos = $pdo->prepare(
|
||||||
"SELECT t.id, t.codigo, t.numero, t.estado,
|
"SELECT t.id, t.codigo, t.numero, t.estado,
|
||||||
@@ -209,47 +157,22 @@ $stmtTurnos = $pdo->prepare(
|
|||||||
$stmtTurnos->execute([$sesionId]);
|
$stmtTurnos->execute([$sesionId]);
|
||||||
$turnos = $stmtTurnos->fetchAll(PDO::FETCH_ASSOC);
|
$turnos = $stmtTurnos->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Enriquecer con exámenes y comentarios — en lote.
|
// Enriquecer con exámenes y comentarios
|
||||||
// Antes eran 2 consultas por turno dentro del foreach (N+1): con 80 turnos en
|
foreach ($turnos as &$turno) {
|
||||||
// el día eran 160 consultas evitables. Mismo patrón que get_historial.php.
|
|
||||||
$turnoIds = array_column($turnos, 'id');
|
|
||||||
$examenesPorTurno = [];
|
|
||||||
$comentariosPorTurno = [];
|
|
||||||
|
|
||||||
if ($turnoIds) {
|
|
||||||
$ph = implode(',', array_fill(0, count($turnoIds), '?'));
|
|
||||||
|
|
||||||
$stmtEx = $pdo->prepare(
|
$stmtEx = $pdo->prepare(
|
||||||
"SELECT DISTINCT ts.turno_id, et.nombre
|
"SELECT DISTINCT et.nombre FROM turnero_examen_items tei
|
||||||
FROM turnero_solicitudes ts
|
|
||||||
JOIN turnero_examen_items tei ON tei.solicitud_id = ts.id
|
|
||||||
JOIN exam_tipos et ON et.id = tei.exam_tipo_id
|
JOIN exam_tipos et ON et.id = tei.exam_tipo_id
|
||||||
WHERE ts.turno_id IN ($ph)
|
WHERE tei.solicitud_id = (SELECT id FROM turnero_solicitudes WHERE turno_id = ? LIMIT 1)"
|
||||||
ORDER BY et.nombre ASC"
|
|
||||||
);
|
);
|
||||||
$stmtEx->execute($turnoIds);
|
$stmtEx->execute([$turno['id']]);
|
||||||
foreach ($stmtEx->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
$turno['examenes'] = $stmtEx->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$examenesPorTurno[(int)$row['turno_id']][] = ['nombre' => $row['nombre']];
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtCom = $pdo->prepare(
|
$stmtCom = $pdo->prepare(
|
||||||
"SELECT turno_id, usuario_nombre, comentario, tipo, creado_at
|
"SELECT usuario_nombre, comentario, tipo, creado_at
|
||||||
FROM turnero_comentarios
|
FROM turnero_comentarios WHERE turno_id = ? ORDER BY creado_at ASC"
|
||||||
WHERE turno_id IN ($ph)
|
|
||||||
ORDER BY turno_id, creado_at ASC"
|
|
||||||
);
|
);
|
||||||
$stmtCom->execute($turnoIds);
|
$stmtCom->execute([$turno['id']]);
|
||||||
foreach ($stmtCom->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
$turno['comentarios'] = $stmtCom->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$tid = (int)$row['turno_id'];
|
|
||||||
unset($row['turno_id']);
|
|
||||||
$comentariosPorTurno[$tid][] = $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($turnos as &$turno) {
|
|
||||||
$tid = (int)$turno['id'];
|
|
||||||
$turno['examenes'] = $examenesPorTurno[$tid] ?? [];
|
|
||||||
$turno['comentarios'] = $comentariosPorTurno[$tid] ?? [];
|
|
||||||
}
|
}
|
||||||
unset($turno);
|
unset($turno);
|
||||||
|
|
||||||
@@ -278,8 +201,6 @@ jsonOk([
|
|||||||
'resumen' => $resumen,
|
'resumen' => $resumen,
|
||||||
'por_prioridad' => $porPrioridad,
|
'por_prioridad' => $porPrioridad,
|
||||||
'por_lugar' => $porLugar,
|
'por_lugar' => $porLugar,
|
||||||
'por_recepcionista' => $porRecepcionista,
|
|
||||||
'por_bacteriologo' => $porBacteriologo,
|
|
||||||
'consent_stats' => $consentStats,
|
'consent_stats' => $consentStats,
|
||||||
'turnos' => $turnos,
|
'turnos' => $turnos,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* GET Historial paginado de turnos con filtros multi-día.
|
* GET Historial paginado de turnos con filtros multi-día.
|
||||||
*
|
*
|
||||||
* Parámetros (todos opcionales):
|
* Parámetros (todos opcionales):
|
||||||
* fecha_desde YYYY-MM-DD (default: hoy; hace 12 meses si se pasa paciente_id)
|
* fecha_desde YYYY-MM-DD (default: hace 7 días)
|
||||||
* fecha_hasta YYYY-MM-DD (default: hoy)
|
* fecha_hasta YYYY-MM-DD (default: hoy)
|
||||||
* estado string ("finalizado","ausente","cancelado","en_servicio",... o vacío = todos)
|
* estado string ("finalizado","ausente","cancelado","en_servicio",... o vacío = todos)
|
||||||
* lugar_id int (0 = todos)
|
* lugar_id int (0 = todos)
|
||||||
@@ -23,29 +23,16 @@ $pdo = db();
|
|||||||
// ── Parámetros ────────────────────────────────────────────────
|
// ── Parámetros ────────────────────────────────────────────────
|
||||||
$hoy = date('Y-m-d');
|
$hoy = date('Y-m-d');
|
||||||
|
|
||||||
// paciente_id se lee primero: determina el rango de fechas por defecto.
|
|
||||||
$pacienteId = (int)($_GET['paciente_id'] ?? 0);
|
|
||||||
|
|
||||||
$fechaDesde = trim($_GET['fecha_desde'] ?? '');
|
$fechaDesde = trim($_GET['fecha_desde'] ?? '');
|
||||||
$fechaHasta = trim($_GET['fecha_hasta'] ?? '');
|
$fechaHasta = trim($_GET['fecha_hasta'] ?? '');
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $fechaDesde)) $fechaDesde = $hoy;
|
||||||
// Rango por defecto cuando el cliente no manda fechas:
|
|
||||||
// - Ficha de paciente (paciente_id): ultimos 12 meses.
|
|
||||||
// Antes caia en "hoy", asi que el historial del paciente solo podia mostrar
|
|
||||||
// visitas del mismo dia y siempre respondia "sin visitas anteriores".
|
|
||||||
// - Listado general del historial: hoy (comportamiento original).
|
|
||||||
// Para ver el historial completo, el cliente pasa fecha_desde explicita.
|
|
||||||
$desdePorDefecto = $pacienteId > 0
|
|
||||||
? date('Y-m-d', strtotime('-12 months'))
|
|
||||||
: $hoy;
|
|
||||||
|
|
||||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $fechaDesde)) $fechaDesde = $desdePorDefecto;
|
|
||||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $fechaHasta)) $fechaHasta = $hoy;
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $fechaHasta)) $fechaHasta = $hoy;
|
||||||
if ($fechaDesde > $fechaHasta) $fechaDesde = $fechaHasta;
|
if ($fechaDesde > $fechaHasta) $fechaDesde = $fechaHasta;
|
||||||
|
|
||||||
$estadoFiltro = trim($_GET['estado'] ?? '');
|
$estadoFiltro = trim($_GET['estado'] ?? '');
|
||||||
$lugarId = (int)($_GET['lugar_id'] ?? 0);
|
$lugarId = (int)($_GET['lugar_id'] ?? 0);
|
||||||
$prioridadId = (int)($_GET['prioridad_id'] ?? 0);
|
$prioridadId = (int)($_GET['prioridad_id'] ?? 0);
|
||||||
|
$pacienteId = (int)($_GET['paciente_id'] ?? 0);
|
||||||
$q = trim($_GET['q'] ?? '');
|
$q = trim($_GET['q'] ?? '');
|
||||||
|
|
||||||
$perPage = min(200, max(10, (int)($_GET['per_page'] ?? 50)));
|
$perPage = min(200, max(10, (int)($_GET['per_page'] ?? 50)));
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* GET /modules/turnero/api/get_traza_turno.php?turno_id=N
|
|
||||||
*
|
|
||||||
* Bitácora de un turno: qué pasó, cuándo y quién lo hizo.
|
|
||||||
*
|
|
||||||
* La información ya existía, pero repartida en cinco tablas y sin ninguna
|
|
||||||
* pantalla que la juntara: para saber quién retiró un formulario había que
|
|
||||||
* consultar la base a mano. Aquí se unifica en una sola línea de tiempo.
|
|
||||||
*
|
|
||||||
* No inventa nada: cada evento sale de un dato guardado. Lo que el sistema no
|
|
||||||
* registró —ver `sinRastro` más abajo— simplemente no aparece.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/_helpers.php';
|
|
||||||
requireMethod('GET');
|
|
||||||
requireTurnero();
|
|
||||||
|
|
||||||
$turnoId = (int)($_GET['turno_id'] ?? 0);
|
|
||||||
if (!$turnoId) jsonError('turno_id requerido');
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
$t = $pdo->prepare(
|
|
||||||
"SELECT t.*, l.nombre AS lugar_nombre, d.nombre AS desk_nombre,
|
|
||||||
ur.full_name AS recepcionista, ul.full_name AS profesional
|
|
||||||
FROM turnero_turnos t
|
|
||||||
LEFT JOIN turnero_lugares l ON l.id = t.lugar_destino_id
|
|
||||||
LEFT JOIN turnero_lugares d ON d.id = t.recepcion_desk_id
|
|
||||||
LEFT JOIN admin_users ur ON ur.id = t.atendido_recepcion_por
|
|
||||||
LEFT JOIN admin_users ul ON ul.id = t.atendido_lugar_por
|
|
||||||
WHERE t.id = ? LIMIT 1"
|
|
||||||
);
|
|
||||||
$t->execute([$turnoId]);
|
|
||||||
$turno = $t->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if (!$turno) jsonError('Turno no encontrado', 404);
|
|
||||||
|
|
||||||
$ev = [];
|
|
||||||
$add = function ($cuando, $tipo, $texto, $quien = null, $extra = null) use (&$ev) {
|
|
||||||
if (!$cuando) return;
|
|
||||||
$ev[] = [
|
|
||||||
'cuando' => $cuando,
|
|
||||||
'tipo' => $tipo, // la vista lo usa para el ícono y el color
|
|
||||||
'texto' => $texto,
|
|
||||||
'quien' => $quien ?: null,
|
|
||||||
'extra' => $extra ?: null,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── 1. Recorrido del turno ────────────────────────────────────────────────
|
|
||||||
$add($turno['creado_at'], 'turno', 'Turno creado');
|
|
||||||
$add($turno['llamado_recepcion_at'], 'llamado', 'Llamado a ' . ($turno['desk_nombre'] ?: 'recepción'));
|
|
||||||
$add($turno['inicio_recepcion_at'], 'atencion', 'Inicia atención en recepción', $turno['recepcionista']);
|
|
||||||
$add($turno['fin_recepcion_at'], 'atencion', 'Termina recepción');
|
|
||||||
$add($turno['llamado_lugar_at'], 'llamado', 'Llamado a ' . ($turno['lugar_nombre'] ?: 'toma de muestras'));
|
|
||||||
$add($turno['inicio_lugar_at'], 'atencion', 'Inicia toma de muestras', $turno['profesional']);
|
|
||||||
$add($turno['muestra_espera_at'], 'muestra', 'Pasa a espera de muestra');
|
|
||||||
$add($turno['fin_lugar_at'], 'fin', 'Finaliza la atención');
|
|
||||||
|
|
||||||
// ── 2. Documentos ─────────────────────────────────────────────────────────
|
|
||||||
$q = $pdo->prepare(
|
|
||||||
"SELECT tc.enviado_at, tc.firmado_at, tc.firmado_profesional_at, tc.estado,
|
|
||||||
f.nombre AS formulario
|
|
||||||
FROM turnero_consentimientos tc
|
|
||||||
JOIN lab_formularios f ON f.id = tc.formulario_id
|
|
||||||
WHERE tc.turno_id = ?"
|
|
||||||
);
|
|
||||||
$q->execute([$turnoId]);
|
|
||||||
foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $c) {
|
|
||||||
$add($c['enviado_at'], 'doc', 'Enviado: ' . $c['formulario']);
|
|
||||||
$add($c['firmado_at'], 'doc_ok', 'Firmado por el paciente: ' . $c['formulario']);
|
|
||||||
$add($c['firmado_profesional_at'], 'doc_ok', 'Firmado por el profesional: ' . $c['formulario']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 3. Comentarios del personal ───────────────────────────────────────────
|
|
||||||
// Aquí caen también las ausencias, que se guardan como comentario con su motivo.
|
|
||||||
$q = $pdo->prepare(
|
|
||||||
"SELECT creado_at, usuario_nombre, comentario, tipo
|
|
||||||
FROM turnero_comentarios WHERE turno_id = ? ORDER BY creado_at"
|
|
||||||
);
|
|
||||||
$q->execute([$turnoId]);
|
|
||||||
foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $c) {
|
|
||||||
$add($c['creado_at'], 'comentario', $c['comentario'], $c['usuario_nombre'], $c['tipo']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 4. Muestras ───────────────────────────────────────────────────────────
|
|
||||||
$q = $pdo->prepare(
|
|
||||||
"SELECT m.recibida_at, m.tipo_muestra, m.estado, m.motivo_rechazo,
|
|
||||||
u.full_name AS quien
|
|
||||||
FROM turnero_muestras m
|
|
||||||
LEFT JOIN admin_users u ON u.id = m.recibida_por
|
|
||||||
WHERE m.recibida_en_turno_id = ?"
|
|
||||||
);
|
|
||||||
$q->execute([$turnoId]);
|
|
||||||
foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $m) {
|
|
||||||
$texto = $m['estado'] === 'rechazada'
|
|
||||||
? 'Muestra RECHAZADA (' . $m['tipo_muestra'] . '): ' . ($m['motivo_rechazo'] ?: 'sin motivo')
|
|
||||||
: 'Muestra recibida (' . $m['tipo_muestra'] . ')';
|
|
||||||
$add($m['recibida_at'], $m['estado'] === 'rechazada' ? 'alerta' : 'muestra', $texto, $m['quien']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 5. Acciones administrativas auditadas ─────────────────────────────────
|
|
||||||
// El detalle es JSON; se filtra por turno en PHP para no depender de LIKE.
|
|
||||||
$q = $pdo->prepare(
|
|
||||||
"SELECT created_at, admin_nombre, accion, detalle
|
|
||||||
FROM lab_actividad_admin
|
|
||||||
WHERE modulo IN ('turnero','turnero_config') AND detalle LIKE ?"
|
|
||||||
);
|
|
||||||
$q->execute(['%"turno_id":' . $turnoId . '%']);
|
|
||||||
foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $a) {
|
|
||||||
$d = json_decode($a['detalle'], true) ?: [];
|
|
||||||
if ((int)($d['turno_id'] ?? 0) !== $turnoId) continue; // evita coincidencias por prefijo
|
|
||||||
|
|
||||||
// Cada acción se traduce a algo que se entienda leyéndolo, no al nombre
|
|
||||||
// técnico del endpoint.
|
|
||||||
switch ($a['accion']) {
|
|
||||||
case 'quitar_consentimiento':
|
|
||||||
$add($a['created_at'], 'alerta',
|
|
||||||
'Formulario RETIRADO: ' . ($d['formulario_nombre'] ?? '?'),
|
|
||||||
$a['admin_nombre'],
|
|
||||||
'Motivo: ' . ($d['motivo'] ?? 'sin motivo')
|
|
||||||
. ' · estaba en «' . ($d['estado_previo'] ?? '?') . '»');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'cambiar_estado':
|
|
||||||
$add($a['created_at'], 'admin',
|
|
||||||
'Estado cambiado a mano: ' . ($d['estado_previo'] ?? '?')
|
|
||||||
. ' → ' . ($d['estado_nuevo'] ?? '?'),
|
|
||||||
$a['admin_nombre']);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'resetear_consentimiento':
|
|
||||||
$add($a['created_at'], 'alerta',
|
|
||||||
'Formulario REINICIADO: ' . ($d['formulario_nombre'] ?? '?'),
|
|
||||||
$a['admin_nombre'],
|
|
||||||
!empty($d['tenia_firma'])
|
|
||||||
? 'Se descartó una firma ya hecha'
|
|
||||||
: 'No tenía firma todavía');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'resetear_toma':
|
|
||||||
$n = is_array($d['campos_olvidados'] ?? null) ? count($d['campos_olvidados']) : 0;
|
|
||||||
$add($a['created_at'], 'alerta',
|
|
||||||
'Toma olvidada en ' . ($d['formulario_nombre'] ?? 'protocolo'),
|
|
||||||
$a['admin_nombre'],
|
|
||||||
$n ? "Se descartaron $n dato(s) de la toma" : null);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'cancelar_toma_pendiente':
|
|
||||||
$add($a['created_at'], 'alerta',
|
|
||||||
'Cancelada la toma pendiente de ' . ($d['turno_origen'] ?? 'otra visita'),
|
|
||||||
$a['admin_nombre'],
|
|
||||||
$d['formulario_nombre'] ?? null);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vincular_paciente':
|
|
||||||
$add($a['created_at'], 'admin',
|
|
||||||
'Paciente vinculado: ' . ($d['paciente_nombre'] ?? '#' . ($d['paciente_id'] ?? '?')),
|
|
||||||
$a['admin_nombre']);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
$add($a['created_at'], 'admin', $a['accion'], $a['admin_nombre']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($ev, fn($a, $b) => strcmp($a['cuando'], $b['cuando']));
|
|
||||||
|
|
||||||
// ── 6. Conversación de WhatsApp ───────────────────────────────────────────
|
|
||||||
// Solo el conteo del día y el enlace al chat: volcar los mensajes aquí sería
|
|
||||||
// esparcir datos personales del paciente por una pantalla de consulta.
|
|
||||||
$chat = null;
|
|
||||||
if (!empty($turno['paciente_id'])) {
|
|
||||||
$q = $pdo->prepare(
|
|
||||||
"SELECT u.id AS user_id,
|
|
||||||
(SELECT COUNT(*) FROM conversations c
|
|
||||||
WHERE c.user_id = u.id AND DATE(c.created_at) = DATE(?)) AS mensajes
|
|
||||||
FROM lab_pacientes p JOIN users u ON u.id = p.user_id
|
|
||||||
WHERE p.id = ? LIMIT 1"
|
|
||||||
);
|
|
||||||
$q->execute([$turno['creado_at'], (int)$turno['paciente_id']]);
|
|
||||||
$r = $q->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($r && (int)$r['mensajes'] > 0) {
|
|
||||||
$chat = ['user_id' => (int)$r['user_id'], 'mensajes' => (int)$r['mensajes']];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonOk([
|
|
||||||
'turno' => [
|
|
||||||
'id' => (int)$turno['id'],
|
|
||||||
'codigo' => $turno['codigo'],
|
|
||||||
'estado' => $turno['estado'],
|
|
||||||
'paciente' => $turno['paciente_nombre'],
|
|
||||||
],
|
|
||||||
'eventos' => $ev,
|
|
||||||
'chat' => $chat,
|
|
||||||
// Se declara lo que el sistema NO registra, para que nadie lea la ausencia
|
|
||||||
// de un evento como prueba de que no ocurrió.
|
|
||||||
// Lo que el sistema sigue sin registrar. Se declara para que la ausencia de
|
|
||||||
// un evento no se lea como prueba de que no ocurrió.
|
|
||||||
'sinRastro' => [
|
|
||||||
'rellamados',
|
|
||||||
'quién guardó cada toma de un protocolo',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
* Body JSON: { token: string }
|
* Body JSON: { token: string }
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php';
|
|
||||||
requireMethod('POST');
|
requireMethod('POST');
|
||||||
requireTurnero();
|
requireTurnero();
|
||||||
|
|
||||||
@@ -15,11 +14,9 @@ if (!$token) jsonError('token requerido.');
|
|||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT tc.id, tc.turno_id, tc.estado, tc.firmado_at, t.sesion_id, t.codigo,
|
"SELECT tc.id, t.sesion_id
|
||||||
f.nombre AS formulario
|
|
||||||
FROM turnero_consentimientos tc
|
FROM turnero_consentimientos tc
|
||||||
JOIN turnero_turnos t ON t.id = tc.turno_id
|
JOIN turnero_turnos t ON t.id = tc.turno_id
|
||||||
JOIN lab_formularios f ON f.id = tc.formulario_id
|
|
||||||
WHERE tc.token = ? LIMIT 1"
|
WHERE tc.token = ? LIMIT 1"
|
||||||
);
|
);
|
||||||
$stmt->execute([$token]);
|
$stmt->execute([$token]);
|
||||||
@@ -37,25 +34,4 @@ $pdo->prepare(
|
|||||||
)->execute([$token]);
|
)->execute([$token]);
|
||||||
|
|
||||||
notificarSSE((int)$tc['sesion_id']);
|
notificarSSE((int)$tc['sesion_id']);
|
||||||
|
|
||||||
// Esta acción descarta una firma ya hecha, así que tiene que dejar constancia:
|
|
||||||
// se anota qué documento era, en qué estado estaba y si tenía firma.
|
|
||||||
try {
|
|
||||||
(new ActividadAdmin())->registrar(
|
|
||||||
adminId(),
|
|
||||||
'turnero',
|
|
||||||
'resetear_consentimiento',
|
|
||||||
(int)$tc['id'],
|
|
||||||
[
|
|
||||||
'turno_id' => (int)$tc['turno_id'],
|
|
||||||
'turno_codigo' => $tc['codigo'],
|
|
||||||
'formulario_nombre' => $tc['formulario'],
|
|
||||||
'estado_previo' => $tc['estado'],
|
|
||||||
'tenia_firma' => !empty($tc['firmado_at']),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('[resetear_consentimiento] auditoría: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonOk([], 'Consentimiento reiniciado correctamente.');
|
jsonOk([], 'Consentimiento reiniciado correctamente.');
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
* Body JSON: { token: string, campos: string[] }
|
* Body JSON: { token: string, campos: string[] }
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php';
|
|
||||||
requireMethod('POST');
|
requireMethod('POST');
|
||||||
requireTurnero();
|
requireTurnero();
|
||||||
|
|
||||||
@@ -18,11 +17,9 @@ if (!is_array($campos) || empty($campos)) jsonError('campos requerido.');
|
|||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT tc.id, tc.turno_id, tc.datos_respuestas, tc.estado, t.sesion_id, t.codigo,
|
"SELECT tc.id, tc.datos_respuestas, tc.estado, t.sesion_id
|
||||||
f.nombre AS formulario
|
|
||||||
FROM turnero_consentimientos tc
|
FROM turnero_consentimientos tc
|
||||||
JOIN turnero_turnos t ON t.id = tc.turno_id
|
JOIN turnero_turnos t ON t.id = tc.turno_id
|
||||||
JOIN lab_formularios f ON f.id = tc.formulario_id
|
|
||||||
WHERE tc.token = ? LIMIT 1"
|
WHERE tc.token = ? LIMIT 1"
|
||||||
);
|
);
|
||||||
$stmt->execute([$token]);
|
$stmt->execute([$token]);
|
||||||
@@ -43,24 +40,4 @@ $pdo->prepare(
|
|||||||
)->execute([json_encode($dr, JSON_UNESCAPED_UNICODE), $token]);
|
)->execute([json_encode($dr, JSON_UNESCAPED_UNICODE), $token]);
|
||||||
|
|
||||||
notificarSSE((int)$tc['sesion_id']);
|
notificarSSE((int)$tc['sesion_id']);
|
||||||
|
|
||||||
// Olvidar una toma borra datos de un protocolo prolongado en curso: se registra
|
|
||||||
// qué campos se descartaron, porque después no hay forma de saberlo.
|
|
||||||
try {
|
|
||||||
(new ActividadAdmin())->registrar(
|
|
||||||
adminId(),
|
|
||||||
'turnero',
|
|
||||||
'resetear_toma',
|
|
||||||
(int)$tc['id'],
|
|
||||||
[
|
|
||||||
'turno_id' => (int)$tc['turno_id'],
|
|
||||||
'turno_codigo' => $tc['codigo'],
|
|
||||||
'formulario_nombre' => $tc['formulario'],
|
|
||||||
'campos_olvidados' => $campos,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('[resetear_toma] auditoría: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonOk([], 'Toma olvidada.');
|
jsonOk([], 'Toma olvidada.');
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
* Vincula un paciente al turno para que los consentimientos puedan crearse.
|
* Vincula un paciente al turno para que los consentimientos puedan crearse.
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php';
|
|
||||||
requireMethod('POST');
|
requireMethod('POST');
|
||||||
requireTurnero();
|
requireTurnero();
|
||||||
|
|
||||||
@@ -28,22 +27,4 @@ $stmt->execute([$pacienteId, $nombreCompleto, $turnoId]);
|
|||||||
|
|
||||||
if (!$stmt->rowCount()) jsonError('Turno no encontrado.', 404);
|
if (!$stmt->rowCount()) jsonError('Turno no encontrado.', 404);
|
||||||
|
|
||||||
// Vincular a la persona equivocada manda la atención a otra historia clínica,
|
|
||||||
// así que queda registrado quién hizo la vinculación.
|
|
||||||
try {
|
|
||||||
(new ActividadAdmin())->registrar(
|
|
||||||
adminId(),
|
|
||||||
'turnero',
|
|
||||||
'vincular_paciente',
|
|
||||||
$turnoId,
|
|
||||||
[
|
|
||||||
'turno_id' => $turnoId,
|
|
||||||
'paciente_id' => $pacienteId,
|
|
||||||
'paciente_nombre' => $nombreCompleto,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('[vincular_paciente] auditoría: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonOk(['turno_id' => $turnoId, 'paciente_id' => $pacienteId]);
|
jsonOk(['turno_id' => $turnoId, 'paciente_id' => $pacienteId]);
|
||||||
|
|||||||
@@ -251,9 +251,6 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
|
|||||||
.bnd-com-user { font-weight: 600; color: #1e293b; }
|
.bnd-com-user { font-weight: 600; color: #1e293b; }
|
||||||
.bnd-com-date { font-size: 0.7rem; color: #94a3b8; margin-left: auto; }
|
.bnd-com-date { font-size: 0.7rem; color: #94a3b8; margin-left: auto; }
|
||||||
.bnd-com-text { color: #334155; }
|
.bnd-com-text { color: #334155; }
|
||||||
.bnd-traza { border-left:2px solid #e2e8f0; margin-left:4px; padding-left:10px; }
|
|
||||||
.bnd-traza-fila { display:flex; align-items:flex-start; gap:8px; padding:3px 0; font-size:.8rem; color:#334155; }
|
|
||||||
.bnd-traza-hora { color:#94a3b8; font-variant-numeric:tabular-nums; min-width:42px; }
|
|
||||||
/* Agregar comentario */
|
/* Agregar comentario */
|
||||||
.bnd-add-com { display: flex; gap: 8px; align-items: flex-end; }
|
.bnd-add-com { display: flex; gap: 8px; align-items: flex-end; }
|
||||||
.bnd-add-com textarea {
|
.bnd-add-com textarea {
|
||||||
@@ -705,50 +702,7 @@ function _renderDetalle(d) {
|
|||||||
<button onclick="_bndEnviarCom(${t.id})"><i class="fas fa-paper-plane"></i> Enviar</button>
|
<button onclick="_bndEnviarCom(${t.id})"><i class="fas fa-paper-plane"></i> Enviar</button>
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
|
|
||||||
const secTraza = `<div class="bnd-sec">
|
document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secRel + secC + secCom;
|
||||||
<div class="bnd-sec-title"><i class="fas fa-clock-rotate-left"></i> Qué pasó con este turno</div>
|
|
||||||
<div id="bnd-traza"><span style="color:#94a3b8;font-size:.82rem">
|
|
||||||
<i class="fas fa-spinner fa-spin"></i> Cargando…</span></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secRel + secC + secCom + secTraza;
|
|
||||||
_bndCargarTraza(t.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Bitácora del turno ────────────────────────────────────────
|
|
||||||
// Los comentarios ya salen arriba; aquí va el recorrido completo, incluidas las
|
|
||||||
// acciones administrativas (por ejemplo el retiro de un formulario, con motivo).
|
|
||||||
const BND_TRAZA_ICO = {
|
|
||||||
turno:'fa-plus-circle', llamado:'fa-bullhorn', atencion:'fa-user-check',
|
|
||||||
muestra:'fa-vial', fin:'fa-flag-checkered', doc:'fa-paper-plane',
|
|
||||||
doc_ok:'fa-file-signature', comentario:'fa-comment',
|
|
||||||
alerta:'fa-triangle-exclamation', admin:'fa-user-shield',
|
|
||||||
};
|
|
||||||
const BND_TRAZA_COLOR = { alerta:'#dc2626', doc_ok:'#16a34a', comentario:'#2563eb', fin:'#0f766e', admin:'#7c3aed' };
|
|
||||||
|
|
||||||
async function _bndCargarTraza(turnoId) {
|
|
||||||
const cont = document.getElementById('bnd-traza');
|
|
||||||
if (!cont) return;
|
|
||||||
const esc = x => String(x ?? '').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c]));
|
|
||||||
try {
|
|
||||||
const j = await fetch(`modules/turnero/api/get_traza_turno.php?turno_id=${turnoId}`).then(r=>r.json());
|
|
||||||
if (!j.ok || !j.eventos?.length) {
|
|
||||||
cont.innerHTML = '<span style="color:#94a3b8;font-size:.82rem">Sin registros</span>'; return;
|
|
||||||
}
|
|
||||||
const hora = c => c ? new Date(c.replace(' ','T')).toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'}) : '';
|
|
||||||
cont.innerHTML = '<div class="bnd-traza">' + j.eventos.map(e => `
|
|
||||||
<div class="bnd-traza-fila">
|
|
||||||
<span class="bnd-traza-hora">${hora(e.cuando)}</span>
|
|
||||||
<i class="fas ${BND_TRAZA_ICO[e.tipo]||'fa-circle'}"
|
|
||||||
style="color:${BND_TRAZA_COLOR[e.tipo]||'#94a3b8'};width:14px;font-size:.72rem;margin-top:2px"></i>
|
|
||||||
<div style="flex:1">${esc(e.texto)}
|
|
||||||
${e.quien ? `<span style="color:#64748b;font-style:italic"> ${esc(e.quien)}</span>` : ''}
|
|
||||||
${e.extra ? `<div style="color:#b45309;font-size:.74rem">${esc(e.extra)}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>`).join('') + '</div>';
|
|
||||||
} catch (_) {
|
|
||||||
cont.innerHTML = '<span style="color:#94a3b8;font-size:.82rem">Error al cargar</span>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Marcar visto ──────────────────────────────────────────────
|
// ── Marcar visto ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -287,18 +287,6 @@ try {
|
|||||||
<div class="lugar-grid" id="lugarGrid"></div>
|
<div class="lugar-grid" id="lugarGrid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Desglose por recepcionista -->
|
|
||||||
<div class="section-card" id="sectionRecepcionista" style="display:none">
|
|
||||||
<div class="section-title"><i class="fas fa-user-check me-1"></i>Por recepcionista</div>
|
|
||||||
<div class="lugar-grid" id="recepcionistaGrid"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desglose por bacteriólogo -->
|
|
||||||
<div class="section-card" id="sectionBacteriologo" style="display:none">
|
|
||||||
<div class="section-title"><i class="fas fa-user-nurse me-1"></i>Por bacteriólogo</div>
|
|
||||||
<div class="lugar-grid" id="bacteriologoGrid"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabla detalle de turnos -->
|
<!-- Tabla detalle de turnos -->
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||||
@@ -457,8 +445,7 @@ function mostrarError(msg) {
|
|||||||
|
|
||||||
// ── Render ───────────────────────────────────────────────────
|
// ── Render ───────────────────────────────────────────────────
|
||||||
function renderDashboard(json) {
|
function renderDashboard(json) {
|
||||||
const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos,
|
const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos } = json;
|
||||||
por_recepcionista = [], por_bacteriologo = [] } = json;
|
|
||||||
|
|
||||||
// ── Sesión info ────────────────────────────────────────
|
// ── Sesión info ────────────────────────────────────────
|
||||||
const si = document.getElementById('sesionInfo');
|
const si = document.getElementById('sesionInfo');
|
||||||
@@ -549,41 +536,11 @@ function renderDashboard(json) {
|
|||||||
secLug.style.display = 'none';
|
secLug.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tarjetas por persona ───────────────────────────────
|
|
||||||
renderPersonas('sectionRecepcionista', 'recepcionistaGrid', por_recepcionista, [
|
|
||||||
['Turnos', p => p.total],
|
|
||||||
['Atendidos', p => p.atendidos],
|
|
||||||
['Ausentes', p => p.ausentes],
|
|
||||||
['T. recepción prom.', p => p.tiempo_promedio_min !== null ? p.tiempo_promedio_min + ' min' : '—'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
renderPersonas('sectionBacteriologo', 'bacteriologoGrid', por_bacteriologo, [
|
|
||||||
['Turnos', p => p.total],
|
|
||||||
['Finalizados', p => p.finalizados],
|
|
||||||
['En servicio', p => p.en_servicio],
|
|
||||||
['T. servicio prom.', p => p.tiempo_promedio_min !== null ? p.tiempo_promedio_min + ' min' : '—'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ── Tabla de turnos ────────────────────────────────────
|
// ── Tabla de turnos ────────────────────────────────────
|
||||||
_turnos = turnos;
|
_turnos = turnos;
|
||||||
renderTabla(turnos);
|
renderTabla(turnos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tarjetas "quién atendió a cuántos". Reutiliza el grid de lugares para que
|
|
||||||
// las tres secciones se vean iguales.
|
|
||||||
function renderPersonas(sectionId, gridId, filas, columnas) {
|
|
||||||
const sec = document.getElementById(sectionId);
|
|
||||||
if (!filas || !filas.length) { sec.style.display = 'none'; return; }
|
|
||||||
sec.style.display = '';
|
|
||||||
document.getElementById(gridId).innerHTML = filas.map(p => `
|
|
||||||
<div class="lugar-card">
|
|
||||||
<div class="lc-name"><i class="fas fa-user text-primary me-1"></i>${escHtml(p.nombre || '—')}</div>
|
|
||||||
${columnas.map(([lbl, val]) =>
|
|
||||||
`<div class="lc-row"><span>${lbl}</span><span>${escHtml(String(val(p) ?? '—'))}</span></div>`
|
|
||||||
).join('')}
|
|
||||||
</div>`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function kpiCard(val, lbl, sub) {
|
function kpiCard(val, lbl, sub) {
|
||||||
return `<div class="kpi-card">
|
return `<div class="kpi-card">
|
||||||
<div class="kpi-val">${val !== null && val !== undefined ? escHtml(String(val)) : '—'}</div>
|
<div class="kpi-val">${val !== null && val !== undefined ? escHtml(String(val)) : '—'}</div>
|
||||||
|
|||||||
@@ -110,14 +110,6 @@ Layout::open('Historial de Turnos', 'fas fa-history');
|
|||||||
|
|
||||||
/* ── Documentos firmados ── */
|
/* ── Documentos firmados ── */
|
||||||
.docs-section { margin-top:10px; padding-top:10px; border-top:1px solid #e2e8f0; }
|
.docs-section { margin-top:10px; padding-top:10px; border-top:1px solid #e2e8f0; }
|
||||||
.traza { border-left:2px solid #e2e8f0; margin:2px 0 0 6px; padding-left:10px; }
|
|
||||||
.traza-fila { display:flex; align-items:flex-start; gap:8px; padding:3px 0; font-size:.78rem; }
|
|
||||||
.traza-hora { color:#94a3b8; font-variant-numeric:tabular-nums; min-width:42px; }
|
|
||||||
.traza-ico { width:14px; text-align:center; margin-top:2px; font-size:.72rem; }
|
|
||||||
.traza-txt { flex:1; color:#334155; }
|
|
||||||
.traza-quien { color:#64748b; font-style:italic; }
|
|
||||||
.traza-extra { color:#b45309; font-size:.74rem; margin-top:1px; }
|
|
||||||
.traza-nota { color:#94a3b8; font-size:.7rem; margin-top:6px; font-style:italic; }
|
|
||||||
.docs-lbl { font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:#94a3b8; margin-bottom:5px; }
|
.docs-lbl { font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:#94a3b8; margin-bottom:5px; }
|
||||||
.doc-pill { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:99px;
|
.doc-pill { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:99px;
|
||||||
font-size:.73rem; font-weight:600; margin:2px 2px; text-decoration:none; transition:opacity .15s; }
|
font-size:.73rem; font-weight:600; margin:2px 2px; text-decoration:none; transition:opacity .15s; }
|
||||||
@@ -602,10 +594,6 @@ function renderDetalle(t) {
|
|||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
${relHtml}${examHtml}${comHtml}
|
${relHtml}${examHtml}${comHtml}
|
||||||
<div class="docs-section" id="traza-${t.id}">
|
|
||||||
<div class="docs-lbl"><i class="fas fa-clock-rotate-left me-1"></i>Qué pasó con este turno</div>
|
|
||||||
<span class="text-muted small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando…</span>
|
|
||||||
</div>
|
|
||||||
<div class="docs-section" id="docs-${t.id}">
|
<div class="docs-section" id="docs-${t.id}">
|
||||||
<div class="docs-lbl"><i class="fas fa-file-signature me-1"></i>Documentos</div>
|
<div class="docs-lbl"><i class="fas fa-file-signature me-1"></i>Documentos</div>
|
||||||
<span class="text-muted small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando…</span>
|
<span class="text-muted small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando…</span>
|
||||||
@@ -626,79 +614,11 @@ function toggleDetalle(rowId, btn, turnoId) {
|
|||||||
docsEl.dataset.loaded = '1';
|
docsEl.dataset.loaded = '1';
|
||||||
cargarDocumentosTurno(turnoId, docsEl);
|
cargarDocumentosTurno(turnoId, docsEl);
|
||||||
}
|
}
|
||||||
const trazaEl = document.getElementById(`traza-${turnoId}`);
|
|
||||||
if (trazaEl && !trazaEl.dataset.loaded) {
|
|
||||||
trazaEl.dataset.loaded = '1';
|
|
||||||
cargarTrazaTurno(turnoId, trazaEl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_URL_ROOT = '<?= BASE_URL ?>';
|
const BASE_URL_ROOT = '<?= BASE_URL ?>';
|
||||||
|
|
||||||
// ── Bitácora del turno ────────────────────────────────────────
|
|
||||||
// Junta en una sola línea de tiempo lo que antes estaba repartido: el recorrido
|
|
||||||
// del turno, los documentos, los comentarios del personal, las muestras y las
|
|
||||||
// acciones administrativas auditadas.
|
|
||||||
const TRAZA_ICONO = {
|
|
||||||
turno:'fa-plus-circle', llamado:'fa-bullhorn', atencion:'fa-user-check',
|
|
||||||
muestra:'fa-vial', fin:'fa-flag-checkered', doc:'fa-paper-plane',
|
|
||||||
doc_ok:'fa-file-signature', comentario:'fa-comment', alerta:'fa-triangle-exclamation',
|
|
||||||
admin:'fa-user-shield',
|
|
||||||
};
|
|
||||||
const TRAZA_COLOR = {
|
|
||||||
alerta:'#dc2626', doc_ok:'#16a34a', comentario:'#2563eb',
|
|
||||||
fin:'#0f766e', admin:'#7c3aed',
|
|
||||||
};
|
|
||||||
|
|
||||||
async function cargarTrazaTurno(turnoId, container) {
|
|
||||||
const titulo = '<div class="docs-lbl"><i class="fas fa-clock-rotate-left me-1"></i>Qué pasó con este turno</div>';
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API}get_traza_turno.php?turno_id=${turnoId}`);
|
|
||||||
const json = await res.json();
|
|
||||||
if (!json.ok) { container.innerHTML = titulo + '<span class="docs-empty">No se pudo cargar</span>'; return; }
|
|
||||||
|
|
||||||
const ev = json.eventos || [];
|
|
||||||
if (!ev.length) { container.innerHTML = titulo + '<span class="docs-empty">Sin registros</span>'; return; }
|
|
||||||
|
|
||||||
const hora = c => c ? new Date(c.replace(' ','T')).toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'}) : '';
|
|
||||||
const filas = ev.map(e => {
|
|
||||||
const color = TRAZA_COLOR[e.tipo] || '#94a3b8';
|
|
||||||
const ico = TRAZA_ICONO[e.tipo] || 'fa-circle';
|
|
||||||
const quien = e.quien ? `<span class="traza-quien">${esc(e.quien)}</span>` : '';
|
|
||||||
const extra = e.extra ? `<div class="traza-extra">${esc(e.extra)}</div>` : '';
|
|
||||||
return `<div class="traza-fila">
|
|
||||||
<span class="traza-hora">${hora(e.cuando)}</span>
|
|
||||||
<i class="fas ${ico} traza-ico" style="color:${color}"></i>
|
|
||||||
<div class="traza-txt"><span>${esc(e.texto)}</span> ${quien}${extra}</div>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// El chat no se vuelca aquí: son datos personales del paciente y esta es
|
|
||||||
// una pantalla de consulta. Se indica que existe y se enlaza.
|
|
||||||
const chat = json.chat
|
|
||||||
? `<div class="traza-fila">
|
|
||||||
<span class="traza-hora"></span>
|
|
||||||
<i class="fab fa-whatsapp traza-ico" style="color:#25d366"></i>
|
|
||||||
<div class="traza-txt">
|
|
||||||
<a href="erp.php?m=turnero&v=chat&user_id=${json.chat.user_id}" target="_blank">
|
|
||||||
${json.chat.mensajes} mensaje(s) de WhatsApp ese día — ver conversación
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>` : '';
|
|
||||||
|
|
||||||
// Se dice qué NO quedó registrado, para que la ausencia de un evento no
|
|
||||||
// se lea como prueba de que no ocurrió.
|
|
||||||
const nota = (json.sinRastro && json.sinRastro.length)
|
|
||||||
? `<div class="traza-nota">El sistema aún no deja rastro de: ${json.sinRastro.map(esc).join(' · ')}.</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
container.innerHTML = titulo + `<div class="traza">${filas}${chat}</div>` + nota;
|
|
||||||
} catch (_) {
|
|
||||||
container.innerHTML = titulo + '<span class="docs-empty">Error al cargar</span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cargarDocumentosTurno(turnoId, container) {
|
async function cargarDocumentosTurno(turnoId, container) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
|
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user