This commit is contained in:
Lizandro Guarnizo
2026-03-10 18:42:51 -05:00
parent b57fc16ab3
commit db469b2134
98 changed files with 16365 additions and 303 deletions
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* Helpers compartidos para los endpoints del módulo de laboratorio.
* Incluido automáticamente por cada endpoint de /api/lab/.
*/
// Capturar cualquier salida accidental (warnings, notices) antes del JSON
ob_start();
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../classes/lab/ActividadAdmin.php';
require_once __DIR__ . '/../../classes/lab/Paciente.php';
require_once __DIR__ . '/../../classes/lab/Enfermera.php';
require_once __DIR__ . '/../../classes/lab/OrdenMedica.php';
require_once __DIR__ . '/../../classes/lab/Domicilio.php';
require_once __DIR__ . '/../../classes/lab/Asignacion.php';
require_once __DIR__ . '/../../classes/lab/Formulario.php';
// Los endpoints API nunca deben mostrar errores PHP en HTML — enviar como JSON
error_reporting(E_ERROR | E_PARSE);
ini_set('display_errors', '0');
ini_set('html_errors', '0');
// Interceptar errores fatales y devolverlos como JSON en lugar de HTML
register_shutdown_function(function () {
$err = error_get_last();
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
ob_clean();
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => false,
'error' => 'Error interno del servidor: ' . $err['message'],
]);
}
});
// Cabeceras JSON estándar
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
requireAuthentication();
/**
* ID del admin autenticado actualmente.
*/
function adminId(): ?int {
return isset($_SESSION['admin_user']['id'])
? (int)$_SESSION['admin_user']['id']
: null;
}
/**
* Rol del usuario autenticado ('admin' | 'enfermero').
*/
function userRole(): string {
return $_SESSION['admin_user']['role'] ?? 'admin';
}
/**
* Detiene la ejecución con 403 JSON si el usuario no es admin (bloquea enfermeros).
*/
function requireAdmin(): void {
if (userRole() !== 'admin') {
jsonError('Acceso restringido a administradores', 403);
}
}
/**
* Detiene la ejecución si no se puede actuar como enfermero.
* Acepta tanto admins (que pueden actuar en nombre de cualquier enfermera)
* como el propio enfermero autenticado.
*/
function requireEnfermeroAccess(int $enfermeraIdSolicitado): void {
if (userRole() === 'admin') return;
$propio = enfermeraId();
if (!$propio || $propio !== $enfermeraIdSolicitado) {
jsonError('Solo puedes gestionar tus propios domicilios', 403);
}
}
/**
* Lee el body JSON del request.
*/
function inputJson(): array {
static $parsed = null;
if ($parsed === null) {
$raw = file_get_contents('php://input');
$parsed = json_decode($raw, true) ?? [];
}
return $parsed;
}
/**
* Devuelve una respuesta de éxito.
*/
function jsonOk(array $payload = [], string $mensaje = ''): void {
ob_clean();
$resp = ['ok' => true, 'success' => true];
if ($mensaje) {
$resp['message'] = $mensaje;
}
echo json_encode(array_merge($resp, $payload), JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Devuelve un error JSON con código HTTP.
*/
function jsonError(string $mensaje, int $code = 400): void {
ob_clean();
http_response_code($code);
echo json_encode(['ok' => false, 'success' => false, 'error' => $mensaje], JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Valida que el método HTTP sea el esperado.
*/
function requireMethod(string $method): void {
if ($_SERVER['REQUEST_METHOD'] !== strtoupper($method)) {
jsonError('Método no permitido', 405);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* POST /api/lab/autorizar_orden.php
* Cambia el estado de una orden médica (flujo de revisión).
* Body JSON:
* { id, accion: 'en_revision'|'autorizada'|'rechazada'|'completada', comentario? }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$om = new OrdenMedica();
$admin = adminId();
if (empty($datos['id'])) {
jsonError('El campo id es obligatorio');
}
if (empty($datos['accion'])) {
jsonError('El campo accion es obligatorio');
}
$id = (int)$datos['id'];
$accion = $datos['accion'];
$comentario = $datos['comentario'] ?? '';
if ($accion === 'rechazada' && empty($comentario)) {
jsonError('El motivo de rechazo es obligatorio');
}
$om->cambiarEstado($id, $accion, $admin, $comentario);
$mensajes = [
'en_revision' => 'Orden marcada en revisión',
'autorizada' => 'Orden autorizada correctamente',
'rechazada' => 'Orden rechazada',
'en_domicilio'=> 'Orden marcada como en domicilio',
'completada' => 'Orden completada',
];
jsonOk(['id' => $id], $mensajes[$accion] ?? 'Estado actualizado');
} catch (InvalidArgumentException $e) {
jsonError($e->getMessage());
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+110
View File
@@ -0,0 +1,110 @@
<?php
/**
* API: Crear orden médica desde conversación de WhatsApp
*
* GET ?solo_paciente=1&conversation_id=X → obtiene/crea paciente del contacto
* POST { paciente_id, conversation_id, local_file, media_message_id, ... }
* → crea lab_ordenes_medicas
*/
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../classes/Database.php';
require_once __DIR__ . '/../../classes/lab/ActividadAdmin.php';
require_once __DIR__ . '/../../classes/lab/Paciente.php';
require_once __DIR__ . '/../../classes/lab/OrdenMedica.php';
// Helpers inline (similar a _helpers.php pero sin incluir todas las clases de nuevo)
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
requireAuthentication();
$adminId = (int)($_SESSION['admin_user']['id'] ?? 0);
$db = Database::getInstance();
// ── GET: solo_paciente ─────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['solo_paciente'])) {
$convId = (int)($_GET['conversation_id'] ?? 0);
$userId = (int)($_GET['user_id'] ?? 0);
if (!$convId && !$userId) {
echo json_encode(['success' => false, 'error' => 'conversation_id o user_id requerido']);
exit;
}
// Obtener user_id y phone_number: por conversation_id o directamente por user_id
if ($convId) {
$conv = $db->fetch(
'SELECT c.id, c.user_id, u.phone_number, u.name AS whatsapp_name
FROM conversations c
JOIN users u ON u.id = c.user_id
WHERE c.id = ?',
[$convId]
);
} else {
$conv = $db->fetch(
'SELECT u.id AS user_id, u.phone_number, u.name AS whatsapp_name
FROM users u
WHERE u.id = ?',
[$userId]
);
}
if (!$conv) {
echo json_encode(['success' => false, 'error' => 'Conversación / usuario no encontrado']);
exit;
}
$pacienteRepo = new Paciente();
$pacienteId = $pacienteRepo->obtenerOCrearDesdeWhatsapp($conv['user_id']);
$paciente = $pacienteRepo->obtener($pacienteId);
echo json_encode(['success' => true, 'paciente' => $paciente]);
exit;
}
// ── POST: crear orden ──────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
echo json_encode(['success' => false, 'error' => 'Datos inválidos']);
exit;
}
$pacienteId = (int)($input['paciente_id'] ?? 0);
if (!$pacienteId) {
echo json_encode(['success' => false, 'error' => 'paciente_id requerido']);
exit;
}
// Construir datos de la orden
$datos = [
'paciente_id' => $pacienteId,
'conversation_id' => !empty($input['conversation_id']) ? (int)$input['conversation_id'] : null,
'local_file' => $input['local_file'] ?? null,
'media_message_id' => $input['media_message_id'] ?? null,
'medico_nombre' => $input['medico_nombre'] ?? null,
'fecha_orden' => !empty($input['fecha_orden']) ? $input['fecha_orden'] : null,
'examenes_solicitados'=> $input['examenes_solicitados']?? null,
'requiere_ayuno' => isset($input['requiere_ayuno']) ? (int)$input['requiere_ayuno'] : 0,
'horas_ayuno' => !empty($input['horas_ayuno']) ? (int)$input['horas_ayuno'] : null,
'notas_admin' => $input['notas_admin'] ?? null,
'estado' => 'pendiente',
];
try {
$orden = new OrdenMedica();
$ordenId = $orden->crear($datos, $adminId);
echo json_encode([
'success' => true,
'orden_id' => $ordenId,
'message' => "Orden médica #$ordenId creada correctamente",
]);
} catch (Exception $e) {
error_log('[lab/crear_desde_whatsapp] ' . $e->getMessage());
echo json_encode(['success' => false, 'error' => 'Error interno: ' . $e->getMessage()]);
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* POST /api/lab/create_enfermero_user.php
* Crea o actualiza el usuario de acceso al sistema para una enfermera.
* Solo administradores pueden llamar este endpoint.
*
* Body JSON:
* { enfermera_id, username, password?, full_name? }
* Si el usuario ya existe para esa enfermera_id, actualiza username/password.
* Si password viene vacío en actualización, no cambia la contraseña.
*
* Returns:
* { ok:true, message, user_id }
*/
require_once __DIR__ . '/_helpers.php';
requireAdmin();
requireMethod('POST');
try {
$datos = inputJson();
$enfermeraId = (int)($datos['enfermera_id'] ?? 0);
$username = trim($datos['username'] ?? '');
$password = $datos['password'] ?? '';
$fullName = trim($datos['full_name'] ?? '');
if (!$enfermeraId) jsonError('enfermera_id es obligatorio');
if (!$username) jsonError('username es obligatorio');
if (strlen($username) < 3) jsonError('El usuario debe tener al menos 3 caracteres');
$db = Database::getInstance();
// Verificar que la enfermera existe
$enf = $db->fetch('SELECT id, nombre_completo FROM lab_enfermeras WHERE id = ?', [$enfermeraId]);
if (!$enf) jsonError('Enfermera no encontrada', 404);
if (empty($fullName)) $fullName = $enf['nombre_completo'];
// ¿Ya existe un usuario vinculado a esta enfermera?
$existente = $db->fetch(
'SELECT id, username FROM admin_users WHERE enfermera_id = ? LIMIT 1',
[$enfermeraId]
);
if ($existente) {
// --- ACTUALIZAR ---
// Verificar que el nuevo username no esté en uso por OTRO usuario
$conflicto = $db->fetch(
'SELECT id FROM admin_users WHERE username = ? AND id != ?',
[$username, $existente['id']]
);
if ($conflicto) jsonError("El usuario '$username' ya está en uso por otra cuenta");
$sets = ['username = ?', 'full_name = ?', 'is_active = 1', 'updated_at = NOW()'];
$vals = [$username, $fullName];
if (!empty($password)) {
if (strlen($password) < 6) jsonError('La contraseña debe tener al menos 6 caracteres');
$sets[] = 'password_hash = ?';
$vals[] = password_hash($password, PASSWORD_DEFAULT);
}
$vals[] = $existente['id'];
$db->execute(
'UPDATE admin_users SET ' . implode(', ', $sets) . ' WHERE id = ?',
$vals
);
jsonOk(['user_id' => $existente['id']], 'Acceso actualizado correctamente');
} else {
// --- CREAR ---
if (empty($password)) jsonError('La contraseña es obligatoria para crear el acceso');
if (strlen($password) < 6) jsonError('La contraseña debe tener al menos 6 caracteres');
// Verificar username único
$conflicto = $db->fetch('SELECT id FROM admin_users WHERE username = ?', [$username]);
if ($conflicto) jsonError("El usuario '$username' ya está en uso");
$hash = password_hash($password, PASSWORD_DEFAULT);
$db->execute(
"INSERT INTO admin_users (username, password_hash, full_name, role, enfermera_id, is_active, created_at)
VALUES (?, ?, ?, 'enfermero', ?, 1, NOW())",
[$username, $hash, $fullName, $enfermeraId]
);
$userId = $db->lastInsertId();
jsonOk(['user_id' => $userId], 'Acceso creado correctamente');
}
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* POST /api/lab/delete_lab_user.php
* Elimina un usuario. No permite auto-eliminarse.
* Solo admins.
*
* Body JSON: { id }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin();
$body = inputJson();
$id = (int)($body['id'] ?? 0);
if (!$id) jsonError('ID de usuario requerido');
$selfId = adminId();
if ($id === $selfId) jsonError('No puedes eliminar tu propio usuario');
$db = Database::getInstance();
$user = $db->fetch("SELECT id, username FROM admin_users WHERE id = ?", [$id]);
if (!$user) jsonError('Usuario no encontrado', 404);
$db->query("DELETE FROM admin_users WHERE id = ?", [$id]);
jsonOk([], "Usuario '{$user['username']}' eliminado correctamente");
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* POST /api/lab/delete_role.php
* Elimina un rol no-sistema.
* Solo admins.
*
* Body JSON: { id }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin();
$body = inputJson();
$id = (int)($body['id'] ?? 0);
if (!$id) jsonError('ID de rol requerido');
$db = Database::getInstance();
$role = $db->fetch("SELECT id, is_system, slug FROM roles WHERE id = ?", [$id]);
if (!$role) jsonError('Rol no encontrado', 404);
if ($role['is_system']) jsonError('Los roles del sistema no se pueden eliminar');
// Reasignar usuarios que tengan este rol al rol admin (id=1) antes de borrar
$db->query("UPDATE admin_users SET role_id = 1, role = 'admin' WHERE role_id = ?", [$id]);
// La FK ON DELETE CASCADE borra role_modules automáticamente
$db->query("DELETE FROM roles WHERE id = ?", [$id]);
jsonOk([], 'Rol eliminado. Los usuarios afectados fueron reasignados al rol Administrador.');
+25
View File
@@ -0,0 +1,25 @@
<?php
/** GET /api/lab/get_actividad.php — Log de trazabilidad del módulo */
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$log = new ActividadAdmin();
$modulo = $_GET['modulo'] ?? '';
$adminFilt = (int)($_GET['admin_id'] ?? 0);
$entidad = (int)($_GET['entidad_id'] ?? 0);
$limit = max(1, min(200, (int)($_GET['limit'] ?? 50)));
if ($adminFilt) {
$data = $log->porAdmin($adminFilt, $limit);
} elseif ($entidad && $modulo) {
$data = $log->porEntidad($modulo, $entidad);
} else {
$data = $log->reciente($limit, $modulo);
}
jsonOk(['data' => $data, 'total' => count($data)]);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/** GET /api/lab/get_asignaciones.php — Asignaciones del día / por enfermera */
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$asig = new Asignacion();
$fecha = $_GET['fecha'] ?? date('Y-m-d');
$data = $asig->porFecha($fecha);
$carga = $asig->cargaHoy();
jsonOk([
'data' => $data,
'total' => count($data),
'fecha' => $fecha,
'carga_hoy' => $carga,
]);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* GET /api/lab/get_config.php — config global del lab
* GET /api/lab/get_config.php?forma=1 — config como objeto clave=>valor (para el builder)
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
$db = Database::getInstance();
$rows = $db->fetchAll('SELECT clave, valor FROM lab_config ORDER BY clave');
$cfg = [];
foreach ($rows as $r) {
$cfg[$r['clave']] = $r['valor'];
}
jsonOk(['config' => $cfg]);
+41
View File
@@ -0,0 +1,41 @@
<?php
/** GET /api/lab/get_domicilios.php — Lista de domicilios con filtros */
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$dom = new Domicilio();
// Vista de un solo domicilio
if (!empty($_GET['id'])) {
$d = $dom->obtener((int)$_GET['id']);
if (!$d) {
jsonError('Domicilio no encontrado', 404);
}
jsonOk(['domicilio' => $d]);
}
$filtros = array_filter([
'fecha' => $_GET['fecha'] ?? '',
'desde' => $_GET['desde'] ?? '',
'hasta' => $_GET['hasta'] ?? '',
'estado' => $_GET['estado'] ?? '',
'enfermera_id' => (int)($_GET['enfermera_id'] ?? 0) ?: null,
'paciente_id' => (int)($_GET['paciente_id'] ?? 0) ?: null,
], fn($v) => $v !== null && $v !== '');
$pagina = max(1, (int)($_GET['page'] ?? 1));
$por = max(1, min(200, (int)($_GET['limit'] ?? 30)));
$resultado = $dom->listar($filtros, $pagina, $por);
// Incluir resumen del día
if (empty($_GET['no_stats'])) {
$resultado['estadisticas_hoy'] = $dom->estadisticasHoy();
$resultado['sin_asignar_hoy'] = count($dom->sinAsignar());
}
jsonOk($resultado);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+47
View File
@@ -0,0 +1,47 @@
<?php
/** GET /api/lab/get_enfermeras.php — Lista de enfermeras */
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$enf = new Enfermera();
// Perfil + agenda de una sola enfermera
if (!empty($_GET['id'])) {
$id = (int)$_GET['id'];
$e = $enf->obtener($id);
if (!$e) {
jsonError('Enfermera no encontrada', 404);
}
$fecha = $_GET['fecha'] ?? date('Y-m-d');
$e['agenda'] = $enf->agenda($id, $fecha);
// Incluir info de acceso al sistema (si tiene usuario creado)
$db = Database::getInstance();
$u = $db->fetch(
'SELECT id, username FROM admin_users WHERE enfermera_id = ? AND role = "enfermero" LIMIT 1',
[$id]
);
$e['usuario_acceso'] = $u ?: null;
jsonOk(['enfermera' => $e]);
}
$soloActivas = !isset($_GET['todas']) || $_GET['todas'] !== '1';
$lista = $enf->listar($soloActivas);
// Enriquecer con username de acceso para cada enfermera
$db = Database::getInstance();
foreach ($lista as &$item) {
$u = $db->fetch(
'SELECT username FROM admin_users WHERE enfermera_id = ? AND is_active = 1 LIMIT 1',
[$item['id']]
);
$item['username_acceso'] = $u['username'] ?? null;
}
unset($item);
jsonOk(['data' => $lista, 'total' => count($lista)]);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* GET /api/lab/get_formularios.php — Lista de plantillas
* GET /api/lab/get_formularios.php?id=X — Plantilla específica
* GET /api/lab/get_formularios.php?envios=1&formulario_id=X — Envíos
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$form = new Formulario();
// Lista de envíos
if (!empty($_GET['envios'])) {
$filtros = [];
if (!empty($_GET['formulario_id'])) $filtros['formulario_id'] = (int)$_GET['formulario_id'];
if (!empty($_GET['paciente_id'])) $filtros['paciente_id'] = (int)$_GET['paciente_id'];
if (!empty($_GET['estado'])) $filtros['estado'] = $_GET['estado'];
// Enfermero solo ve los suyos
if (userRole() === 'enfermero') {
$filtros['enviado_por'] = adminId();
}
jsonOk(['data' => $form->listarEnvios($filtros)]);
}
// Plantilla específica
if (!empty($_GET['id'])) {
$f = $form->obtener((int)$_GET['id']);
if (!$f) jsonError('Formulario no encontrado', 404);
jsonOk(['formulario' => $f]);
}
// Lista de plantillas
$todas = isset($_GET['todas']) && userRole() === 'admin';
jsonOk(['data' => $form->listar(!$todas)]);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* GET /api/lab/get_lab_users.php
* Devuelve todos los usuarios del sistema con información de rol.
* Solo admins.
*/
require_once __DIR__ . '/_helpers.php';
requireAdmin();
$db = Database::getInstance();
$users = $db->fetchAll("
SELECT
u.id,
u.username,
u.full_name,
u.email,
u.is_active,
u.role,
u.role_id,
u.enfermera_id,
u.last_login,
u.created_at,
r.name AS role_name,
r.color AS role_color,
r.slug AS role_slug,
e.nombre_completo AS enfermera_nombre
FROM admin_users u
LEFT JOIN roles r ON r.id = u.role_id
LEFT JOIN lab_enfermeras e ON e.id = u.enfermera_id
ORDER BY u.id ASC
");
foreach ($users as &$u) {
$u['is_active'] = (bool)$u['is_active'];
$u['role_name'] = $u['role_name'] ?? ucfirst($u['role'] ?? 'admin');
$u['role_color'] = $u['role_color'] ?? '#0d6efd';
}
unset($u);
jsonOk(['users' => $users]);
+39
View File
@@ -0,0 +1,39 @@
<?php
/** GET /api/lab/get_ordenes.php — Lista paginada de órdenes médicas con filtros */
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$om = new OrdenMedica();
$filtros = array_filter([
'estado' => $_GET['estado'] ?? '',
'paciente_id' => (int)($_GET['paciente_id'] ?? 0) ?: null,
'desde' => $_GET['desde'] ?? '',
'hasta' => $_GET['hasta'] ?? '',
'busqueda' => trim($_GET['busqueda'] ?? $_GET['search'] ?? ''),
], fn($v) => $v !== null && $v !== '');
$pagina = max(1, (int)($_GET['page'] ?? 1));
$por = max(1, min(100, (int)($_GET['limit'] ?? 30)));
// Vista de una sola orden
if (!empty($_GET['id'])) {
$orden = $om->obtener((int)$_GET['id']);
if (!$orden) {
jsonError('Orden no encontrada', 404);
}
jsonOk(['orden' => $orden]);
}
$resultado = $om->listar($filtros, $pagina, $por);
// Incluir también el contador por estado para el dashboard
if (!isset($_GET['no_counts'])) {
$resultado['contadores'] = $om->contadorPorEstado();
}
jsonOk($resultado);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+16
View File
@@ -0,0 +1,16 @@
<?php
/** GET /api/lab/get_pacientes.php — Lista paginada de pacientes */
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$pac = new Paciente();
$busqueda = trim($_GET['busqueda'] ?? $_GET['search'] ?? '');
$pagina = max(1, (int)($_GET['page'] ?? $_GET['pagina'] ?? 1));
$por = max(1, min(100, (int)($_GET['limit'] ?? $_GET['por_pagina'] ?? 30)));
jsonOk($pac->listar($busqueda, $pagina, $por));
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* GET /api/lab/get_roles.php
* Devuelve todos los roles con sus módulos asignados.
* Solo admins.
*/
require_once __DIR__ . '/_helpers.php';
requireAdmin();
$db = Database::getInstance();
$roles = $db->fetchAll("SELECT id, name, slug, description, color, is_system, created_at FROM roles ORDER BY id ASC");
foreach ($roles as &$role) {
$mods = $db->fetchAll(
"SELECT module_slug FROM role_modules WHERE role_id = ? ORDER BY module_slug",
[$role['id']]
);
$role['modules'] = array_column($mods, 'module_slug');
$role['is_system'] = (bool)$role['is_system'];
$role['user_count'] = (int)$db->fetch(
"SELECT COUNT(*) AS c FROM admin_users WHERE role_id = ?",
[$role['id']]
)['c'];
}
unset($role);
jsonOk(['roles' => $roles]);
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* GET /api/lab/get_stats.php — Estadísticas del dashboard del módulo
* Devuelve contadores, pendientes urgentes, agenda hoy y carga de enfermeras.
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
$om = new OrdenMedica();
$dom = new Domicilio();
$asig = new Asignacion();
$pac = new Paciente();
$db = Database::getInstance();
// ── Órdenes ──────────────────────────────────────────────────────────────
$estadosOrdenes = $om->contadorPorEstado();
$pendientesViejos = $om->pendientesAntiguos(5);
// ── Domicilios hoy ───────────────────────────────────────────────────────
$estDomHoy = $dom->estadisticasHoy();
$sinAsignarHoy = $dom->sinAsignar();
// ── Carga de enfermeras hoy ──────────────────────────────────────────────
$cargaEnfermeras = $asig->cargaHoy();
// ── Totales generales ────────────────────────────────────────────────────
$totalPacientes = $db->fetch('SELECT COUNT(*) AS n FROM lab_pacientes WHERE is_active = 1')['n'] ?? 0;
$totalEnfermeras = $db->fetch('SELECT COUNT(*) AS n FROM lab_enfermeras WHERE is_active = 1')['n'] ?? 0;
// ── Órdenes de hoy ───────────────────────────────────────────────────────
$ordenesHoy = $db->fetch(
'SELECT COUNT(*) AS n FROM lab_ordenes_medicas WHERE DATE(created_at) = CURDATE()'
)['n'] ?? 0;
// ── Actividad reciente ───────────────────────────────────────────────────
$actividadReciente = (new ActividadAdmin())->reciente(10);
jsonOk([
'ordenes' => [
'por_estado' => $estadosOrdenes,
'pendientes_hoy' => (int)($estadosOrdenes['pendiente'] ?? 0),
'en_revision_hoy' => (int)($estadosOrdenes['en_revision'] ?? 0),
'pendientes_viejos'=> $pendientesViejos,
'total_hoy' => (int)$ordenesHoy,
],
'domicilios' => [
'por_estado' => $estDomHoy,
'total_hoy' => array_sum($estDomHoy),
'sin_asignar' => count($sinAsignarHoy),
'lista_sin_asignar' => $sinAsignarHoy,
],
'enfermeras' => [
'activas' => (int)$totalEnfermeras,
'carga_hoy' => $cargaEnfermeras,
],
'generales' => [
'total_pacientes' => (int)$totalPacientes,
],
'actividad_reciente' => $actividadReciente,
]);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* GET /api/lab/my_agenda.php
* Devuelve la agenda del enfermero autenticado (o de cualquier enfermera para admins).
*
* Query params:
* enfermera_id (int) — Requerido para admins. Enfermeros usan automáticamente el suyo.
* fecha (Y-m-d) — Por defecto: hoy.
* rango_inicio (Y-m-d) — Para vista semanal/mensual.
* rango_fin (Y-m-d)
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
try {
// Determinar enfermera_id
if (userRole() === 'enfermero') {
$eid = enfermeraId();
if (!$eid) {
jsonError('Tu usuario no tiene una enfermera vinculada. Contacta al administrador.');
}
} else {
$eid = (int)($_GET['enfermera_id'] ?? 0);
if (!$eid) {
jsonError('enfermera_id requerido');
}
}
$enf = new Enfermera();
$fecha = $_GET['fecha'] ?? date('Y-m-d');
// Agenda del día con pleno detalle
$agenda = $enf->agenda($eid, $fecha);
// Enriquecer con servicios extra y estado de asignación
$db = Database::getInstance();
foreach ($agenda as &$item) {
$item['servicios_extra'] = $db->fetchAll(
"SELECT * FROM lab_servicios_extra WHERE domicilio_id = ? ORDER BY created_at ASC",
[(int)$item['domicilio_id']]
);
// Info completa del domicilio (notas, indicaciones)
$extra = $db->fetch(
"SELECT notas_admin, indicaciones_dir, barrio, ciudad FROM lab_domicilios WHERE id = ?",
[(int)$item['domicilio_id']]
);
if ($extra) $item = array_merge($item, $extra);
}
unset($item);
// Resumen rápido
$totales = array_count_values(array_column($agenda, 'domicilio_estado'));
jsonOk([
'enfermera_id' => $eid,
'fecha' => $fecha,
'agenda' => $agenda,
'total' => count($agenda),
'totales' => $totales,
]);
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+49
View File
@@ -0,0 +1,49 @@
<?php
/**
* POST /api/lab/save_asignacion.php
* Asigna (o reasigna) una enfermera a un domicilio.
* Body JSON:
* { domicilio_id, enfermera_id, notas? }
* -- Para liberar/completar:
* { id, accion: 'liberar'|'completar', motivo? }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$asig = new Asignacion();
$admin = adminId();
// Liberar o completar
if (!empty($datos['id']) && !empty($datos['accion'])) {
$id = (int)$datos['id'];
if ($datos['accion'] === 'liberar') {
$asig->liberar($id, $admin, $datos['motivo'] ?? '');
jsonOk(['id' => $id], 'Asignación liberada');
}
if ($datos['accion'] === 'completar') {
$asig->completar($id, $admin);
jsonOk(['id' => $id], 'Asignación completada');
}
jsonError('Acción no reconocida');
}
// Asignar / reasignar
foreach (['domicilio_id', 'enfermera_id'] as $req) {
if (empty($datos[$req])) {
jsonError("El campo $req es obligatorio");
}
}
$id = $asig->asignar(
(int)$datos['domicilio_id'],
(int)$datos['enfermera_id'],
$admin,
$datos['notas'] ?? ''
);
jsonOk(['id' => $id], 'Enfermera asignada correctamente');
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* POST /api/lab/save_config.php — Guardar configuración global del lab
* Solo admin.
* Body JSON: { empresa_nombre, empresa_subtitulo, empresa_direccion, empresa_telefono,
* empresa_email, empresa_ciudad, doc_color, doc_logo_base64, doc_pie_pagina }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin();
$datos = inputJson();
$db = Database::getInstance();
$permitidas = [
'empresa_nombre', 'empresa_subtitulo', 'empresa_direccion',
'empresa_telefono', 'empresa_email', 'empresa_ciudad',
'doc_color', 'doc_logo_base64', 'doc_pie_pagina',
];
$guardadas = 0;
foreach ($permitidas as $clave) {
if (array_key_exists($clave, $datos)) {
$db->query(
'INSERT INTO lab_config(clave, valor) VALUES(?,?) ON DUPLICATE KEY UPDATE valor=?',
[$clave, $datos[$clave], $datos[$clave]]
);
$guardadas++;
}
}
jsonOk(['guardadas' => $guardadas], 'Configuración guardada');
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* POST /api/lab/save_domicilio.php
* Crea, actualiza o cambia estado de un domicilio.
* Body JSON:
* { id?, paciente_id, orden_id?, direccion, ciudad?, barrio?,
* indicaciones_dir?, fecha_programada, hora_programada?,
* tipo_servicio?, estado?,
* -- Para cambio de estado solamente:
* solo_estado?: true, nuevo_estado?, motivo_cancelacion?, ... }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$dom = new Domicilio();
$admin = adminId();
// Solo cambio de estado
if (!empty($datos['id']) && !empty($datos['solo_estado'])) {
$id = (int)$datos['id'];
$nuevoEstado = $datos['nuevo_estado'] ?? '';
$extras = array_filter([
'motivo_cancelacion' => $datos['motivo_cancelacion'] ?? null,
'fecha_reprogramada' => $datos['fecha_reprogramada'] ?? null,
'hora_llegada' => $datos['hora_llegada'] ?? null,
'hora_salida' => $datos['hora_salida'] ?? null,
'observaciones' => $datos['observaciones'] ?? null,
'muestras_tomadas' => $datos['muestras_tomadas'] ?? null,
]);
$dom->cambiarEstado($id, $nuevoEstado, $admin, $extras);
jsonOk(['id' => $id], 'Estado actualizado');
}
if (!empty($datos['id'])) {
// Actualización general
$id = (int)$datos['id'];
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)) {
$datos['orden_id'] = null;
}
$dom->actualizar($id, $datos, $admin);
jsonOk(['id' => $id], 'Domicilio actualizado correctamente');
} else {
// Creación
foreach (['paciente_id', 'direccion', 'fecha_programada'] as $req) {
if (empty($datos[$req])) {
jsonError("El campo $req es obligatorio");
}
}
// 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;
}
// Si el creador es un enfermero y no vino enfermera_id, auto-asignarlo desde la sesión
if (empty($datos['enfermera_id']) && userRole() === 'enfermero') {
$eid = enfermeraId();
if ($eid) $datos['enfermera_id'] = $eid;
}
$id = $dom->crear($datos, $admin);
// Crear asignación automática si se indicó enfermera_id
if (!empty($datos['enfermera_id'])) {
$asig = new Asignacion();
$asig->asignar($id, (int)$datos['enfermera_id'], $admin);
}
jsonOk(['id' => $id], 'Domicilio creado correctamente');
}
} catch (InvalidArgumentException $e) {
jsonError($e->getMessage());
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* POST /api/lab/save_enfermera.php
* Crea o actualiza una enfermera.
* Body JSON:
* { id?, nombre_completo, numero_documento, tipo_documento?,
* telefono, telefono_alt?, email?, zona?, notas?, is_active? }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$enf = new Enfermera();
$admin = adminId();
if (!empty($datos['id'])) {
$id = (int)$datos['id'];
unset($datos['id']);
// Desactivar
if (isset($datos['is_active']) && $datos['is_active'] == 0) {
$enf->desactivar($id, $admin);
jsonOk(['id' => $id], 'Enfermera desactivada');
}
$enf->actualizar($id, $datos, $admin);
jsonOk(['id' => $id], 'Enfermera actualizada correctamente');
} else {
foreach (['nombre_completo', 'numero_documento', 'telefono'] as $req) {
if (empty($datos[$req])) {
jsonError("El campo $req es obligatorio");
}
}
$id = $enf->crear($datos, $admin);
jsonOk(['id' => $id], 'Enfermera registrada correctamente');
}
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* POST /api/lab/save_formulario.php
* Solo admins pueden crear/editar plantillas.
*
* Body JSON para crear: { nombre, descripcion?, categoria?, esquema, permite_firma?, requiere_firma? }
* Body JSON para editar: { id, ...mismos campos... }
* Body JSON para borrar: { id, borrar: true }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin(); // enfermeros NO pueden diseñar formularios
try {
$datos = inputJson();
$form = new Formulario();
$admin = adminId();
// Borrar (soft delete)
if (!empty($datos['id']) && !empty($datos['borrar'])) {
$form->actualizar((int)$datos['id'], ['is_active' => 0], $admin);
jsonOk(['id' => (int)$datos['id']], 'Formulario eliminado');
}
// Editar
if (!empty($datos['id'])) {
$id = (int)$datos['id'];
unset($datos['id']);
$form->actualizar($id, $datos, $admin);
jsonOk(['id' => $id], 'Formulario actualizado');
}
// Crear
if (empty($datos['nombre'])) jsonError('El nombre es obligatorio');
if (empty($datos['esquema'])) jsonError('El esquema es obligatorio');
$id = $form->crear($datos, $admin);
jsonOk(['id' => $id], 'Formulario creado correctamente');
} catch (InvalidArgumentException $e) {
jsonError($e->getMessage());
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+87
View File
@@ -0,0 +1,87 @@
<?php
/**
* POST /api/lab/save_lab_user.php
* Crea o actualiza un usuario del sistema.
* Solo admins.
*
* Body JSON:
* { id?, username, full_name, email?, password?, role_id, is_active?, enfermera_id? }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin();
$body = inputJson();
$id = isset($body['id']) ? (int)$body['id'] : null;
$username = trim($body['username'] ?? '');
$fullName = trim($body['full_name'] ?? '');
$email = trim($body['email'] ?? '');
$password = $body['password'] ?? '';
$roleId = isset($body['role_id']) ? (int)$body['role_id'] : null;
$isActive = isset($body['is_active']) ? (int)(bool)$body['is_active'] : 1;
$enfermeraId = isset($body['enfermera_id']) && $body['enfermera_id'] !== '' && $body['enfermera_id'] !== null
? (int)$body['enfermera_id'] : null;
if (!$username) jsonError('El nombre de usuario es obligatorio');
if (!$fullName) jsonError('El nombre completo es obligatorio');
if (!$roleId) jsonError('Debes seleccionar un rol');
// Validar username (solo alfanumérico + guión bajo)
if (!preg_match('/^[a-zA-Z0-9_\.]{3,50}$/', $username)) {
jsonError('El username solo puede tener letras, números, puntos y guiones bajos (3-50 caracteres)');
}
$db = Database::getInstance();
// Obtener slug del rol
$role = $db->fetch("SELECT id, slug FROM roles WHERE id = ?", [$roleId]);
if (!$role) jsonError('Rol no encontrado', 404);
$roleSlug = $role['slug'];
// Si el rol es enfermero y no hay enfermera_id, buscar si tiene ya uno vinculado
// (se puede crear sin vincular y vincularlo después desde la pantalla de Enfermeras)
if ($id) {
// ── Actualizar ──────────────────────────────────────────
$existing = $db->fetch("SELECT id, username FROM admin_users WHERE id = ?", [$id]);
if (!$existing) jsonError('Usuario no encontrado', 404);
// Verificar duplicado de username (excluir el propio)
$dup = $db->fetch("SELECT id FROM admin_users WHERE username = ? AND id != ?", [$username, $id]);
if ($dup) jsonError('Ese nombre de usuario ya está en uso');
$params = [$username, $fullName, $email ?: null, $roleId, $roleSlug, $isActive, $enfermeraId, $id];
$sql = "UPDATE admin_users
SET username=?, full_name=?, email=?, role_id=?, role=?, is_active=?, enfermera_id=?, updated_at=NOW()
WHERE id=?";
if ($password) {
if (strlen($password) < 6) jsonError('La contraseña debe tener al menos 6 caracteres');
$hash = password_hash($password, PASSWORD_BCRYPT);
$sql = "UPDATE admin_users
SET username=?, full_name=?, email=?, role_id=?, role=?, is_active=?, enfermera_id=?, password_hash=?, updated_at=NOW()
WHERE id=?";
$params = [$username, $fullName, $email ?: null, $roleId, $roleSlug, $isActive, $enfermeraId, $hash, $id];
}
$db->query($sql, $params);
jsonOk(['id' => $id], 'Usuario actualizado correctamente');
} else {
// ── Crear ────────────────────────────────────────────────
if (!$password) jsonError('La contraseña es obligatoria al crear un usuario');
if (strlen($password) < 6) jsonError('La contraseña debe tener al menos 6 caracteres');
$dup = $db->fetch("SELECT id FROM admin_users WHERE username = ?", [$username]);
if ($dup) jsonError('Ese nombre de usuario ya está en uso');
$hash = password_hash($password, PASSWORD_BCRYPT);
$db->query(
"INSERT INTO admin_users (username, full_name, email, password_hash, role_id, role, is_active, enfermera_id)
VALUES (?,?,?,?,?,?,?,?)",
[$username, $fullName, $email ?: null, $hash, $roleId, $roleSlug, $isActive, $enfermeraId]
);
$newId = $db->lastInsertId();
jsonOk(['id' => $newId], 'Usuario creado correctamente');
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* POST /api/lab/save_orden.php
* Crea o actualiza datos de una orden médica (sin cambiar estado).
* Body JSON:
* { id?, paciente_id, conversation_id?, whatsapp_media_id?, local_file?,
* medico_nombre?, medico_registro?, fecha_orden?, diagnostico?,
* examenes_solicitados?, requiere_ayuno?, horas_ayuno?,
* indicaciones?, notas_admin? }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$om = new OrdenMedica();
$admin = adminId();
if (!empty($datos['id'])) {
$id = (int)$datos['id'];
unset($datos['id']);
$om->actualizar($id, $datos, $admin);
jsonOk(['id' => $id], 'Orden actualizada correctamente');
} else {
if (empty($datos['paciente_id'])) {
jsonError('El campo paciente_id es obligatorio');
}
$id = $om->crear($datos, $admin);
jsonOk(['id' => $id], 'Orden creada correctamente');
}
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* POST /api/lab/save_paciente.php
* Crea o actualiza un paciente.
* Body JSON:
* { id?, nombre_completo, numero_documento?, tipo_documento?, telefono?,
* email?, fecha_nacimiento?, genero?, direccion?, ciudad?, barrio?,
* eps?, notas_admin?, user_id? }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
$pac = new Paciente();
$admin = adminId();
if (!empty($datos['id'])) {
// Actualización
$id = (int)$datos['id'];
unset($datos['id']);
$pac->actualizar($id, $datos, $admin);
jsonOk(['id' => $id], 'Paciente actualizado correctamente');
} else {
// Creación — nombre obligatorio
if (empty($datos['nombre_completo'])) {
jsonError('El campo nombre_completo es obligatorio');
}
$id = $pac->crear($datos, $admin);
jsonOk(['id' => $id], 'Paciente creado correctamente');
}
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* POST /api/lab/save_role.php
* Crea o actualiza un rol y sus módulos.
* Solo admins.
*
* Body JSON:
* { id?, name, slug, description?, color?, modules: [slug, ...] }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin();
$body = inputJson();
$id = isset($body['id']) ? (int)$body['id'] : null;
$name = trim($body['name'] ?? '');
$slug = trim($body['slug'] ?? '');
$description = trim($body['description'] ?? '');
$color = trim($body['color'] ?? '#6c757d');
$modules = $body['modules'] ?? [];
if (!$name) jsonError('El nombre del rol es obligatorio');
if (!$slug) jsonError('El slug del rol es obligatorio');
if (!preg_match('/^[a-z0-9_\-]+$/', $slug)) jsonError('El slug solo puede contener letras minúsculas, números, guiones y guiones bajos');
if (!is_array($modules)) jsonError('modules debe ser un arreglo');
$db = Database::getInstance();
// Verificar duplicado de slug
$existing = $db->fetch("SELECT id, is_system FROM roles WHERE slug = ?", [$slug]);
if ($existing && (!$id || $existing['id'] !== $id)) {
jsonError('Ya existe un rol con ese slug');
}
if ($id) {
// Actualizar
$role = $db->fetch("SELECT id, is_system FROM roles WHERE id = ?", [$id]);
if (!$role) jsonError('Rol no encontrado', 404);
$db->query(
"UPDATE roles SET name=?, slug=?, description=?, color=?, updated_at=NOW() WHERE id=?",
[$name, $slug, $description, $color, $id]
);
// Reconstruir módulos solo si no es sistema, o siempre (admin puede editar módulos)
$db->query("DELETE FROM role_modules WHERE role_id = ?", [$id]);
} else {
// Crear
$db->query(
"INSERT INTO roles (name, slug, description, color) VALUES (?,?,?,?)",
[$name, $slug, $description, $color]
);
$id = $db->lastInsertId();
}
// Insertar módulos
foreach ($modules as $mod) {
$mod = trim((string)$mod);
if ($mod) {
$db->query(
"INSERT IGNORE INTO role_modules (role_id, module_slug) VALUES (?,?)",
[$id, $mod]
);
}
}
jsonOk(['id' => $id], $id ? 'Rol actualizado' : 'Rol creado');
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* POST /api/lab/save_servicio_extra.php
* El enfermero agrega (o actualiza/elimina) un servicio extra a un domicilio.
*
* Body JSON:
* { domicilio_id, descripcion, tipo, notas?, requiere_pago?, valor? } -- crear
* { id, descripcion?, tipo?, notas?, requiere_pago?, valor? } -- editar
* { id, eliminar: true } -- borrar
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
$TIPOS_VALIDOS = ['inyeccion','cura','nebulizacion','toma_muestra',
'tension_arterial','glucometria','otro'];
try {
$datos = inputJson();
$db = Database::getInstance();
// ── Eliminar ───────────────────────────────────────────────────────────
if (!empty($datos['id']) && !empty($datos['eliminar'])) {
$se = $db->fetch('SELECT * FROM lab_servicios_extra WHERE id = ?', [(int)$datos['id']]);
if (!$se) jsonError('Servicio no encontrado', 404);
if (userRole() === 'enfermero') {
requireEnfermeroAccess((int)$se['realizado_por']);
}
$db->delete('lab_servicios_extra', 'id = ?', [(int)$datos['id']]);
jsonOk(['id' => (int)$datos['id']], 'Servicio eliminado');
}
// ── Editar ─────────────────────────────────────────────────────────────
if (!empty($datos['id'])) {
$id = (int)$datos['id'];
$se = $db->fetch('SELECT * FROM lab_servicios_extra WHERE id = ?', [$id]);
if (!$se) jsonError('Servicio no encontrado', 404);
if (userRole() === 'enfermero') {
requireEnfermeroAccess((int)$se['realizado_por']);
}
$permitidos = ['descripcion','tipo','notas','requiere_pago','valor'];
$campos = array_intersect_key($datos, array_flip($permitidos));
if (isset($campos['tipo']) && !in_array($campos['tipo'], $TIPOS_VALIDOS)) {
jsonError('Tipo de servicio inválido');
}
$db->update('lab_servicios_extra', $campos, 'id = ?', [$id]);
jsonOk(['id' => $id], 'Servicio actualizado');
}
// ── Crear ──────────────────────────────────────────────────────────────
foreach (['domicilio_id', 'descripcion', 'tipo'] as $req) {
if (empty($datos[$req])) jsonError("El campo $req es obligatorio");
}
if (!in_array($datos['tipo'], $TIPOS_VALIDOS)) {
jsonError('Tipo de servicio inválido');
}
// Verificar que el domicilio existe y que el enfermero le pertenece
$domId = (int)$datos['domicilio_id'];
if (userRole() === 'enfermero') {
$eid = enfermeraId();
$asig = $db->fetch(
'SELECT a.id FROM lab_asignaciones a WHERE a.domicilio_id = ? AND a.enfermera_id = ?',
[$domId, $eid]
);
if (!$asig) jsonError('No tienes permiso para este domicilio', 403);
}
$id = $db->insert('lab_servicios_extra', [
'domicilio_id' => $domId,
'descripcion' => trim($datos['descripcion']),
'tipo' => $datos['tipo'],
'notas' => $datos['notas'] ?? null,
'requiere_pago' => !empty($datos['requiere_pago']) ? 1 : 0,
'valor' => !empty($datos['valor']) ? (float)$datos['valor'] : null,
'realizado_por' => enfermeraId() ?? (userRole() === 'admin' ? null : null),
]);
jsonOk(['id' => $id], 'Servicio extra registrado');
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+86
View File
@@ -0,0 +1,86 @@
<?php
/**
* POST /api/lab/send_formulario.php
* Crea un envío (instancia) con datos pre-llenados y devuelve el link para WhatsApp.
* Accesible por admin Y enfermero.
*
* Body JSON:
* { formulario_id, paciente_id?, domicilio_id?, datos_prefilled:{...}, enviado_via? }
*
* Returns:
* { id, token, url, whatsapp_url, mensaje_wa }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
try {
$datos = inputJson();
if (empty($datos['formulario_id'])) jsonError('formulario_id es obligatorio');
// Verificar que la plantilla existe
$form = new Formulario();
$plantilla = $form->obtener((int)$datos['formulario_id']);
if (!$plantilla) jsonError('Formulario no encontrado', 404);
// Pre-llenado: si viene paciente_id, auto-enriquecer con datos del paciente
$prefilled = $datos['datos_prefilled'] ?? [];
if (!empty($datos['paciente_id'])) {
$pac = (new Paciente())->obtener((int)$datos['paciente_id']);
if ($pac) {
// Mapeo automático de campos estándar
$prefilled['__paciente'] = [
'id' => $pac['id'],
'nombre_completo' => $pac['nombre_completo'],
'numero_documento'=> $pac['numero_documento'],
'tipo_documento' => $pac['tipo_documento'],
'telefono' => $pac['telefono'],
'email' => $pac['email'] ?? '',
'fecha_nacimiento'=> $pac['fecha_nacimiento'] ?? '',
'eps' => $pac['eps'] ?? '',
'direccion' => $pac['direccion'] ?? '',
'ciudad' => $pac['ciudad'] ?? '',
];
}
}
$datos['datos_prefilled'] = json_encode($prefilled, JSON_UNESCAPED_UNICODE);
$datos['enviado_via'] = $datos['enviado_via'] ?? 'whatsapp';
$envioId = $form->crearEnvio($datos, adminId());
// Construir URL pública
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$base = $protocol . '://' . $host;
// Subir un nivel desde /api/lab/
$basePath = rtrim(dirname(dirname(dirname($_SERVER['SCRIPT_NAME']))), '/');
// Obtener el token del envío recién creado
$db = Database::getInstance();
$envio = $db->fetch('SELECT token FROM lab_form_envios WHERE id = ?', [$envioId]);
$token = $envio['token'];
$url = $base . $basePath . '/form_cliente.php?t=' . $token;
// Mensaje de WhatsApp preformateado
$nomPac = $prefilled['__paciente']['nombre_completo'] ?? 'Estimado paciente';
$nomForm = $plantilla['nombre'];
$mensajeWa = "Hola $nomPac, le enviamos el formulario *\"$nomForm\"* para que lo complete y firme digitalmente.\n\n"
. "👉 Accede aquí:\n$url\n\n"
. "El enlace expira en 7 días. Si tiene dudas contáctenos.";
$telPac = $prefilled['__paciente']['telefono'] ?? '';
$waUrl = 'https://wa.me/' . preg_replace('/\D/', '', $telPac)
. '?text=' . rawurlencode($mensajeWa);
jsonOk([
'id' => $envioId,
'token' => $token,
'url' => $url,
'whatsapp_url'=> $waUrl,
'mensaje_wa' => $mensajeWa,
], 'Envío creado correctamente');
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* POST /api/lab/submit_formulario.php — Guardar respuesta del cliente (público, sin auth)
* GET /api/lab/submit_formulario.php?t=TOKEN — Verificar estado del token (público)
*
* Body JSON (POST): { token, datos_cliente:{...}, firma_svg? }
*/
// No usar _helpers.php porque este endpoint es PÚBLICO (no requiere sesión)
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../classes/lab/Formulario.php';
require_once __DIR__ . '/../../classes/Database.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
function pubOk(array $data = [], string $msg = ''): void {
$r = ['success' => true];
if ($msg) $r['message'] = $msg;
echo json_encode(array_merge($r, $data), JSON_UNESCAPED_UNICODE);
exit;
}
function pubErr(string $msg, int $code = 400): void {
http_response_code($code);
echo json_encode(['success' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$form = new Formulario();
// GET: cargar datos del formulario por token
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$token = $_GET['t'] ?? '';
if (!$token) pubErr('Token requerido');
$envio = $form->obtenerPorToken($token);
if (!$envio) pubErr('Link inválido o expirado', 404);
// Config de diseño: primero override del formulario, luego config global
$db2 = Database::getInstance();
$cfgRows = $db2->fetchAll('SELECT clave, valor FROM lab_config ORDER BY clave');
$gCfg = [];
foreach ($cfgRows as $r) { $gCfg[$r['clave']] = $r['valor']; }
$docConfig = [
'doc_color' => $envio['doc_color'] ?: ($gCfg['doc_color'] ?? '#1565c0'),
'doc_logo_base64' => $envio['doc_logo_base64'] ?: ($gCfg['doc_logo_base64'] ?? null),
'doc_encabezado' => $envio['doc_encabezado'] ?: ($gCfg['empresa_nombre'] ?? null),
'doc_subtitulo' => $envio['doc_subtitulo'] ?: ($gCfg['empresa_subtitulo'] ?? null),
'doc_pie_pagina' => $envio['doc_pie_pagina'] ?: ($gCfg['doc_pie_pagina'] ?? null),
];
// Estructura que espera form_cliente.php
pubOk([
'formulario' => [
'nombre' => $envio['form_nombre'],
'descripcion' => $envio['form_descripcion'] ?? '',
'esquema_decoded'=> $envio['esquema_decoded'],
'permite_firma' => (bool)($envio['permite_firma'] ?? false),
'requiere_firma' => (bool)($envio['requiere_firma'] ?? false),
],
'envio' => [
'id' => $envio['id'],
'estado' => $envio['estado'],
'firma_svg' => $envio['firma_svg'] ?? null,
'hash_verificacion'=> $envio['hash_verificacion'] ?? null,
'completado_en' => $envio['completado_en'] ?? null,
],
'prefilled' => $envio['prefilled_decoded'] ?? [],
'config' => $docConfig,
]);
}
// POST: guardar respuesta
$raw = file_get_contents('php://input');
$datos = json_decode($raw, true) ?? [];
$token = $datos['token'] ?? '';
if (!$token) pubErr('Token requerido');
$envio = $form->obtenerPorToken($token);
if (!$envio) pubErr('Link inválido o expirado', 404);
if (in_array($envio['estado'], ['completado', 'firmado'])) {
// El formulario ya fue guardado (posible reintento tras error de red).
// Devolvemos éxito con los datos existentes para que el cliente no quede bloqueado.
$db = Database::getInstance();
$saved = $db->fetch('SELECT id, hash_verificacion, firma_svg FROM lab_form_envios WHERE token = ?', [$token]);
pubOk([
'firmado' => !empty($saved['firma_svg']),
'hash' => $saved['hash_verificacion'] ?? null,
'envio_id' => $saved['id'] ?? null,
'ya_enviado'=> true,
], 'Formulario enviado correctamente. ¡Gracias!');
}
$datosCliente = $datos['datos_cliente'] ?? [];
$firmaSvg = $datos['firma_svg'] ?? null;
$firmaFoto = $datos['firma_foto'] ?? null;
// Guardar foto de firma dentro de datos_cliente para no requerir nueva columna
if ($firmaFoto) {
$datosCliente['__firma_foto'] = $firmaFoto;
}
// Validar que si requiere firma, venga
if ($envio['requiere_firma'] && !$firmaSvg) {
pubErr('La firma es obligatoria para este formulario');
}
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$ok = $form->guardarRespuesta($token, $datosCliente, $firmaSvg, $ip, $ua);
if (!$ok) pubErr('No se pudo guardar la respuesta', 500);
// Recuperar hash e id para la respuesta
$db = Database::getInstance();
$saved = $db->fetch('SELECT id, hash_verificacion FROM lab_form_envios WHERE token = ?', [$token]);
pubOk([
'firmado' => (bool)$firmaSvg,
'hash' => $saved['hash_verificacion'] ?? null,
'envio_id' => $saved['id'] ?? null,
], 'Formulario enviado correctamente. ¡Gracias!');
} catch (Exception $e) {
pubErr($e->getMessage(), 500);
}
+94
View File
@@ -0,0 +1,94 @@
<?php
/**
* POST /api/lab/update_domicilio_enfermero.php
* El enfermero actualiza el estado de un domicilio asignado.
*
* Body JSON:
* { domicilio_id, nuevo_estado, notas? }
*
* Transiciones permitidas al enfermero:
* programado → confirmado
* confirmado → en_camino
* en_camino → en_domicilio
* en_domicilio → completado
* cualquiera → cancelado (con notas obligatorias)
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
// Flujo permitido enfermero: estado_actual => [estados_siguientes_posibles]
const TRANSICIONES_ENFERMERO = [
'programado' => ['confirmado'],
'confirmado' => ['en_camino', 'cancelado'],
'en_camino' => ['en_domicilio', 'cancelado'],
'en_domicilio' => ['completado', 'cancelado'],
];
try {
$datos = inputJson();
$domId = (int)($datos['domicilio_id'] ?? 0);
$estado = trim($datos['nuevo_estado'] ?? '');
$notas = trim($datos['notas'] ?? '');
if (!$domId) jsonError('domicilio_id requerido');
if (!$estado) jsonError('nuevo_estado requerido');
$db = Database::getInstance();
// Verificar domicilio y obtener estado actual
$dom = $db->fetch(
'SELECT id, estado, paciente_id FROM lab_domicilios WHERE id = ?',
[$domId]
);
if (!$dom) jsonError('Domicilio no encontrado', 404);
// Verificar que el enfermero tenga asignación activa
if (userRole() === 'enfermero') {
$eid = enfermeraId();
$asig = $db->fetch(
"SELECT a.id FROM lab_asignaciones a
WHERE a.domicilio_id = ? AND a.enfermera_id = ?
AND a.estado NOT IN ('liberada','completada')",
[$domId, $eid]
);
if (!$asig) jsonError('No tienes asignación activa para este domicilio', 403);
// Validar transición
$estadoActual = $dom['estado'];
$permitidos = TRANSICIONES_ENFERMERO[$estadoActual] ?? [];
if (!in_array($estado, $permitidos)) {
jsonError("No puedes pasar de '$estadoActual' a '$estado'");
}
}
if ($estado === 'cancelado' && !$notas) {
jsonError('Las notas son obligatorias al cancelar');
}
// Actualizar domicilio
$campos = ['estado' => $estado];
if ($notas) $campos['notas_admin'] = $notas;
if ($estado === 'completado') {
$campos['hora_salida'] = date('H:i:s');
}
if ($estado === 'en_domicilio') {
$campos['hora_llegada'] = date('H:i:s');
}
$db->update('lab_domicilios', $campos, 'id = ?', [$domId]);
// Actualizar asignación si completado/cancelado
if (in_array($estado, ['completado', 'cancelado']) && userRole() === 'enfermero') {
$estAsig = $estado === 'completado' ? 'completada' : 'liberada';
$db->update(
'lab_asignaciones',
['estado' => $estAsig],
'domicilio_id = ? AND enfermera_id = ?',
[$domId, enfermeraId()]
);
}
jsonOk(['domicilio_id' => $domId, 'estado' => $estado], 'Estado actualizado correctamente');
} catch (Exception $e) {
jsonError($e->getMessage(), 500);
}