feat: módulo lab_examenes — CRUD catálogo de exámenes, items y tarifas
- Nuevo módulo lab_examenes con lista paginada, buscador y filtro por categoría - Vista de detalle/edición con campos completos de exam_tipos (CUPS, protocolo, tipo muestra, nivel, abreviatura, seremite, ayuno, instrucciones) - Sub-tabla inline de items de resultado (lab_items_resultado) con edición por fila - Sub-tabla de precios por tarifa (lab_tarifas) con modal de edición - APIs: list, get, save, save_item, save_tarifa, get_tarifas - Script ETL Firebird→MySQL (scripts/etl_examenes.php) para la migración inicial Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
229977785b
commit
fb0e77523c
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
error_reporting(E_ERROR | E_PARSE);
|
||||
ini_set('display_errors', '0');
|
||||
ini_set('html_errors', '0');
|
||||
|
||||
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: ' . $err['message']]);
|
||||
}
|
||||
});
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
||||
|
||||
function jsonOk(array $data = [], string $msg = ''): never {
|
||||
ob_clean();
|
||||
$r = ['ok' => true];
|
||||
if ($msg) $r['message'] = $msg;
|
||||
if ($data) $r = array_merge($r, $data);
|
||||
echo json_encode($r);
|
||||
exit;
|
||||
}
|
||||
|
||||
function jsonError(string $msg, int $code = 400): never {
|
||||
ob_clean();
|
||||
http_response_code($code);
|
||||
echo json_encode(['ok' => false, 'error' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
function requireAdmin(): void {
|
||||
if (!isUserLoggedIn()) jsonError('No autorizado', 401);
|
||||
$role = $_SESSION['admin_user']['role'] ?? '';
|
||||
if (!in_array($role, ['superadmin', 'admin', 'supervisor'], true)) jsonError('Sin permiso', 403);
|
||||
}
|
||||
|
||||
function requireLogin(): void {
|
||||
if (!isUserLoggedIn()) jsonError('No autorizado', 401);
|
||||
}
|
||||
|
||||
function inputJson(): array {
|
||||
return json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
}
|
||||
|
||||
function db(): PDO {
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
return $pdo;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireLogin();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) jsonError('ID requerido');
|
||||
|
||||
$pdo = db();
|
||||
|
||||
$s = $pdo->prepare('SELECT * FROM exam_tipos WHERE id = ?');
|
||||
$s->execute([$id]);
|
||||
$exam = $s->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$exam) jsonError('Examen no encontrado', 404);
|
||||
|
||||
$items = $pdo->prepare(
|
||||
'SELECT * FROM lab_items_resultado WHERE cod_protocolo = ? ORDER BY orden, id'
|
||||
);
|
||||
$items->execute([$exam['cod_protocolo'] ?? $exam['codigo']]);
|
||||
|
||||
jsonOk(['data' => $exam, 'items' => $items->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireLogin();
|
||||
|
||||
$examId = (int)($_GET['exam_id'] ?? 0);
|
||||
if (!$examId) jsonError('exam_id requerido');
|
||||
|
||||
$pdo = db();
|
||||
|
||||
$s = $pdo->prepare('SELECT 1 FROM exam_tipos WHERE id = ?');
|
||||
$s->execute([$examId]);
|
||||
if (!$s->fetchColumn()) jsonError('Examen no encontrado', 404);
|
||||
|
||||
$s = $pdo->prepare(
|
||||
"SELECT ti.id AS tarifa_id, ti.nombre AS tarifa_nombre,
|
||||
lt.id AS precio_id, lt.valor, lt.recargo_urg, lt.recargo_fes, lt.recargo_esp
|
||||
FROM lab_tarifas_id ti
|
||||
LEFT JOIN lab_tarifas lt ON lt.tarifa_id = ti.id AND lt.exam_tipo_id = ?
|
||||
ORDER BY ti.id"
|
||||
);
|
||||
$s->execute([$examId]);
|
||||
|
||||
jsonOk(['data' => $s->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireLogin();
|
||||
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
$cat = trim($_GET['categoria'] ?? '');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = max(1, min(200, (int)($_GET['limit'] ?? 50)));
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($q !== '') {
|
||||
$like = '%' . $q . '%';
|
||||
$where[] = '(nombre LIKE ? OR codigo LIKE ? OR cups LIKE ?)';
|
||||
$params[] = $like; $params[] = $like; $params[] = $like;
|
||||
}
|
||||
if ($cat !== '') {
|
||||
$where[] = 'categoria = ?';
|
||||
$params[] = $cat;
|
||||
}
|
||||
$wSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
$s = $pdo->prepare("SELECT COUNT(*) FROM exam_tipos $wSql");
|
||||
$s->execute($params);
|
||||
$total = (int)$s->fetchColumn();
|
||||
|
||||
$s = $pdo->prepare(
|
||||
"SELECT id, codigo, nombre, categoria, activo, cups, seremite, cod_protocolo, tipo_muestra
|
||||
FROM exam_tipos $wSql ORDER BY categoria, nombre LIMIT $limit OFFSET $offset"
|
||||
);
|
||||
$s->execute($params);
|
||||
|
||||
jsonOk(['data' => $s->fetchAll(PDO::FETCH_ASSOC), 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireAdmin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('Método no permitido', 405);
|
||||
|
||||
$d = inputJson();
|
||||
$id = (int)($d['id'] ?? 0);
|
||||
|
||||
if (!empty($d['_delete'])) {
|
||||
if (!$id) jsonError('ID requerido');
|
||||
$pdo = db();
|
||||
$s = $pdo->prepare('SELECT COUNT(*) FROM turnero_examen_items WHERE exam_tipo_id = ?');
|
||||
$s->execute([$id]);
|
||||
if ($s->fetchColumn() > 0) jsonError('Hay solicitudes con este examen. Desactívelo en su lugar.');
|
||||
$pdo->prepare('DELETE FROM exam_tipo_consentimientos WHERE exam_tipo_id = ?')->execute([$id]);
|
||||
$pdo->prepare('DELETE FROM exam_tipos WHERE id = ?')->execute([$id]);
|
||||
jsonOk([], 'Examen eliminado');
|
||||
}
|
||||
|
||||
$codigo = strtoupper(trim($d['codigo'] ?? ''));
|
||||
$nombre = trim($d['nombre'] ?? '');
|
||||
$categoria = trim($d['categoria'] ?? '') ?: null;
|
||||
$cups = trim($d['cups'] ?? '') ?: null;
|
||||
$cod_prot = trim($d['cod_protocolo'] ?? '') ?: null;
|
||||
$tipo_m = trim($d['tipo_muestra'] ?? '') ?: null;
|
||||
$nivel = isset($d['nivel']) && $d['nivel'] !== '' ? (int)$d['nivel'] : null;
|
||||
$abrev = trim($d['abreviatura'] ?? '') ?: null;
|
||||
$seremite = (int)($d['seremite'] ?? 0);
|
||||
$serecibe = trim($d['serecibe'] ?? '') ?: null;
|
||||
$activo = (int)($d['activo'] ?? 1);
|
||||
$req_ayuno = (int)($d['requiere_ayuno'] ?? 0);
|
||||
$horas_ayuno = isset($d['horas_ayuno']) && $d['horas_ayuno'] !== '' ? (int)$d['horas_ayuno'] : null;
|
||||
$instruc = trim($d['instrucciones'] ?? '') ?: null;
|
||||
$formId = (int)($d['formulario_id'] ?? 0) ?: null;
|
||||
|
||||
if ($codigo === '') jsonError('El código es requerido');
|
||||
if ($nombre === '') jsonError('El nombre es requerido');
|
||||
if (strlen($codigo) > 20) jsonError('Código máximo 20 caracteres');
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
if ($id) {
|
||||
$pdo->prepare(
|
||||
'UPDATE exam_tipos SET codigo=?,nombre=?,categoria=?,cups=?,cod_protocolo=?,tipo_muestra=?,
|
||||
nivel=?,abreviatura=?,seremite=?,serecibe=?,activo=?,requiere_ayuno=?,horas_ayuno=?,instrucciones=?
|
||||
WHERE id=?'
|
||||
)->execute([$codigo,$nombre,$categoria,$cups,$cod_prot,$tipo_m,
|
||||
$nivel,$abrev,$seremite,$serecibe,$activo,$req_ayuno,$horas_ayuno,$instruc,$id]);
|
||||
$examId = $id;
|
||||
} else {
|
||||
$pdo->prepare(
|
||||
'INSERT INTO exam_tipos (codigo,nombre,categoria,cups,cod_protocolo,tipo_muestra,
|
||||
nivel,abreviatura,seremite,serecibe,activo,codigo_legacy,requiere_ayuno,horas_ayuno,instrucciones)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
|
||||
)->execute([$codigo,$nombre,$categoria,$cups,$cod_prot,$tipo_m,
|
||||
$nivel,$abrev,$seremite,$serecibe,$activo,$codigo,$req_ayuno,$horas_ayuno,$instruc]);
|
||||
$examId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
$pdo->prepare('DELETE FROM exam_tipo_consentimientos WHERE exam_tipo_id = ?')->execute([$examId]);
|
||||
if ($formId) {
|
||||
$pdo->prepare('INSERT INTO exam_tipo_consentimientos (exam_tipo_id, formulario_id) VALUES (?,?)')
|
||||
->execute([$examId, $formId]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
jsonOk(['id' => $examId], $id ? 'Examen actualizado' : 'Examen creado');
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
if ($e->getCode() === '23000') jsonError('Ya existe un examen con ese código');
|
||||
jsonError('Error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireAdmin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('Método no permitido', 405);
|
||||
|
||||
$d = inputJson();
|
||||
$id = (int)($d['id'] ?? 0);
|
||||
|
||||
if (!empty($d['_delete'])) {
|
||||
if (!$id) jsonError('ID requerido');
|
||||
db()->prepare('DELETE FROM lab_items_resultado WHERE id = ?')->execute([$id]);
|
||||
jsonOk([], 'Item eliminado');
|
||||
}
|
||||
|
||||
$codProt = trim($d['cod_protocolo'] ?? '');
|
||||
$nombre = trim($d['nombre'] ?? '');
|
||||
if (!$codProt) jsonError('cod_protocolo requerido');
|
||||
if (!$nombre) jsonError('Nombre requerido');
|
||||
|
||||
$tipoSexo = in_array($d['tipo_sexo'] ?? '', ['M','F']) ? $d['tipo_sexo'] : null;
|
||||
$tipo = in_array($d['tipo'] ?? '', ['N','T']) ? $d['tipo'] : 'T';
|
||||
$medida = trim($d['medida'] ?? '') ?: null;
|
||||
$abrev = trim($d['abreviatura'] ?? '') ?: null;
|
||||
$vmin = $d['vmin_ref'] !== '' && $d['vmin_ref'] !== null ? (float)$d['vmin_ref'] : null;
|
||||
$vmax = $d['vmax_ref'] !== '' && $d['vmax_ref'] !== null ? (float)$d['vmax_ref'] : null;
|
||||
$orden = (int)($d['orden'] ?? 0);
|
||||
$formula = trim($d['formula'] ?? '') ?: null;
|
||||
$cups_d = trim($d['cups_detalle'] ?? '') ?: null;
|
||||
|
||||
$pdo = db();
|
||||
if ($id) {
|
||||
$pdo->prepare(
|
||||
'UPDATE lab_items_resultado SET cod_protocolo=?,nombre=?,tipo_sexo=?,tipo=?,medida=?,
|
||||
abreviatura=?,vmin_ref=?,vmax_ref=?,orden=?,formula=?,cups_detalle=? WHERE id=?'
|
||||
)->execute([$codProt,$nombre,$tipoSexo,$tipo,$medida,$abrev,$vmin,$vmax,$orden,$formula,$cups_d,$id]);
|
||||
jsonOk(['id' => $id], 'Item actualizado');
|
||||
} else {
|
||||
$pdo->prepare(
|
||||
'INSERT INTO lab_items_resultado
|
||||
(cod_protocolo,nombre,tipo_sexo,tipo,medida,abreviatura,vmin_ref,vmax_ref,orden,formula,cups_detalle)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)'
|
||||
)->execute([$codProt,$nombre,$tipoSexo,$tipo,$medida,$abrev,$vmin,$vmax,$orden,$formula,$cups_d]);
|
||||
jsonOk(['id' => (int)$pdo->lastInsertId()], 'Item creado');
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireAdmin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('Método no permitido', 405);
|
||||
|
||||
$d = inputJson();
|
||||
$examId = (int)($d['exam_tipo_id'] ?? 0);
|
||||
$tarifaId = (int)($d['tarifa_id'] ?? 0);
|
||||
if (!$examId || !$tarifaId) jsonError('exam_tipo_id y tarifa_id son requeridos');
|
||||
|
||||
$pdo = db();
|
||||
|
||||
if (!empty($d['_delete'])) {
|
||||
$pdo->prepare('DELETE FROM lab_tarifas WHERE exam_tipo_id = ? AND tarifa_id = ?')
|
||||
->execute([$examId, $tarifaId]);
|
||||
jsonOk([], 'Precio eliminado');
|
||||
}
|
||||
|
||||
$valor = (float)($d['valor'] ?? 0);
|
||||
$rec_urg = (float)($d['recargo_urg'] ?? 0);
|
||||
$rec_fes = (float)($d['recargo_fes'] ?? 0);
|
||||
$rec_esp = (float)($d['recargo_esp'] ?? 0);
|
||||
|
||||
$s = $pdo->prepare('SELECT COALESCE(codigo_legacy, codigo) FROM exam_tipos WHERE id = ?');
|
||||
$s->execute([$examId]);
|
||||
$leg = $s->fetchColumn();
|
||||
if (!$leg) jsonError('Examen no encontrado', 404);
|
||||
|
||||
// Check if price row exists (no unique key in schema, so do it manually)
|
||||
$s = $pdo->prepare('SELECT id FROM lab_tarifas WHERE exam_tipo_id = ? AND tarifa_id = ?');
|
||||
$s->execute([$examId, $tarifaId]);
|
||||
$precioId = $s->fetchColumn();
|
||||
|
||||
if ($precioId) {
|
||||
$pdo->prepare(
|
||||
'UPDATE lab_tarifas SET valor=?,recargo_urg=?,recargo_fes=?,recargo_esp=?,cod_examen_legacy=? WHERE id=?'
|
||||
)->execute([$valor, $rec_urg, $rec_fes, $rec_esp, $leg, $precioId]);
|
||||
} else {
|
||||
$pdo->prepare(
|
||||
'INSERT INTO lab_tarifas (cod_examen_legacy,exam_tipo_id,tarifa_id,valor,recargo_urg,recargo_fes,recargo_esp)
|
||||
VALUES (?,?,?,?,?,?,?)'
|
||||
)->execute([$leg, $examId, $tarifaId, $valor, $rec_urg, $rec_fes, $rec_esp]);
|
||||
}
|
||||
|
||||
jsonOk([], 'Precio guardado');
|
||||
Reference in New Issue
Block a user