Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4674f087d | ||
|
|
29cbebfeb5 |
+1
-6
@@ -36,12 +36,7 @@ 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)
|
||||||
# Se descarga por HTTPS de forma explícita: el filtro de red perimetral
|
RUN pecl install redis-6.0.2 && docker-php-ext-enable redis
|
||||||
# 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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* - 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';
|
||||||
@@ -41,10 +42,12 @@ if (!$sesion) {
|
|||||||
'tiempo_espera_promedio_min' => null,
|
'tiempo_espera_promedio_min' => null,
|
||||||
'tiempo_servicio_promedio_min' => null,
|
'tiempo_servicio_promedio_min' => null,
|
||||||
],
|
],
|
||||||
'por_prioridad' => [],
|
'por_prioridad' => [],
|
||||||
'por_lugar' => [],
|
'por_lugar' => [],
|
||||||
'consent_stats' => [],
|
'por_recepcionista' => [],
|
||||||
'turnos' => [],
|
'por_bacteriologo' => [],
|
||||||
|
'consent_stats' => [],
|
||||||
|
'turnos' => [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +128,55 @@ $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,
|
||||||
@@ -157,22 +209,47 @@ $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
|
// Enriquecer con exámenes y comentarios — en lote.
|
||||||
foreach ($turnos as &$turno) {
|
// Antes eran 2 consultas por turno dentro del foreach (N+1): con 80 turnos en
|
||||||
|
// 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 et.nombre FROM turnero_examen_items tei
|
"SELECT DISTINCT ts.turno_id, et.nombre
|
||||||
JOIN exam_tipos et ON et.id = tei.exam_tipo_id
|
FROM turnero_solicitudes ts
|
||||||
WHERE tei.solicitud_id = (SELECT id FROM turnero_solicitudes WHERE turno_id = ? LIMIT 1)"
|
JOIN turnero_examen_items tei ON tei.solicitud_id = ts.id
|
||||||
|
JOIN exam_tipos et ON et.id = tei.exam_tipo_id
|
||||||
|
WHERE ts.turno_id IN ($ph)
|
||||||
|
ORDER BY et.nombre ASC"
|
||||||
);
|
);
|
||||||
$stmtEx->execute([$turno['id']]);
|
$stmtEx->execute($turnoIds);
|
||||||
$turno['examenes'] = $stmtEx->fetchAll(PDO::FETCH_ASSOC);
|
foreach ($stmtEx->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$examenesPorTurno[(int)$row['turno_id']][] = ['nombre' => $row['nombre']];
|
||||||
|
}
|
||||||
|
|
||||||
$stmtCom = $pdo->prepare(
|
$stmtCom = $pdo->prepare(
|
||||||
"SELECT usuario_nombre, comentario, tipo, creado_at
|
"SELECT turno_id, usuario_nombre, comentario, tipo, creado_at
|
||||||
FROM turnero_comentarios WHERE turno_id = ? ORDER BY creado_at ASC"
|
FROM turnero_comentarios
|
||||||
|
WHERE turno_id IN ($ph)
|
||||||
|
ORDER BY turno_id, creado_at ASC"
|
||||||
);
|
);
|
||||||
$stmtCom->execute([$turno['id']]);
|
$stmtCom->execute($turnoIds);
|
||||||
$turno['comentarios'] = $stmtCom->fetchAll(PDO::FETCH_ASSOC);
|
foreach ($stmtCom->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$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);
|
||||||
|
|
||||||
@@ -201,6 +278,8 @@ 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: hace 7 días)
|
* fecha_desde YYYY-MM-DD (default: hoy; hace 12 meses si se pasa paciente_id)
|
||||||
* 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,16 +23,29 @@ $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)));
|
||||||
|
|||||||
@@ -287,6 +287,18 @@ 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">
|
||||||
@@ -445,7 +457,8 @@ function mostrarError(msg) {
|
|||||||
|
|
||||||
// ── Render ───────────────────────────────────────────────────
|
// ── Render ───────────────────────────────────────────────────
|
||||||
function renderDashboard(json) {
|
function renderDashboard(json) {
|
||||||
const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos } = json;
|
const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos,
|
||||||
|
por_recepcionista = [], por_bacteriologo = [] } = json;
|
||||||
|
|
||||||
// ── Sesión info ────────────────────────────────────────
|
// ── Sesión info ────────────────────────────────────────
|
||||||
const si = document.getElementById('sesionInfo');
|
const si = document.getElementById('sesionInfo');
|
||||||
@@ -536,11 +549,41 @@ 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user