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');
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
return [
|
||||
'slug' => 'lab_examenes',
|
||||
'name' => 'Catálogo de Exámenes',
|
||||
'icon' => 'fas fa-flask',
|
||||
'category' => 'clinico',
|
||||
'route' => '/erp.php?m=lab_examenes&v=index',
|
||||
'is_active' => true,
|
||||
'sort_order' => 60,
|
||||
'oleada' => 2,
|
||||
'description' => 'Gestión del catálogo de exámenes, valores de referencia y tarifas.',
|
||||
'links' => [
|
||||
['name' => 'Exámenes', 'icon' => 'fas fa-flask', 'route' => '/erp.php?m=lab_examenes&v=index'],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,503 @@
|
||||
<?php
|
||||
require_once APP_ROOT . '/config/config.php';
|
||||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
|
||||
$exam = null;
|
||||
$items = [];
|
||||
if ($id) {
|
||||
$s = $pdo->prepare('SELECT * FROM exam_tipos WHERE id=?');
|
||||
$s->execute([$id]);
|
||||
$exam = $s->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$exam) { header('Location: ' . BASE_URL . 'erp.php?m=lab_examenes&v=index'); exit; }
|
||||
|
||||
$s = $pdo->prepare('SELECT * FROM lab_items_resultado WHERE cod_protocolo=? ORDER BY orden,id');
|
||||
$s->execute([$exam['cod_protocolo'] ?? $exam['codigo']]);
|
||||
$items = $s->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$protocolos = $pdo->query('SELECT codigo, nombre FROM lab_protocolos ORDER BY nombre')->fetchAll(PDO::FETCH_ASSOC);
|
||||
$muestras = $pdo->query('SELECT codigo, nombre FROM lab_tipos_muestra ORDER BY nombre')->fetchAll(PDO::FETCH_ASSOC);
|
||||
$formularios = $pdo->query('SELECT id, nombre FROM lab_formularios ORDER BY nombre')->fetchAll(PDO::FETCH_ASSOC);
|
||||
$formActual = null;
|
||||
if ($id) {
|
||||
$s = $pdo->prepare('SELECT formulario_id FROM exam_tipo_consentimientos WHERE exam_tipo_id=? LIMIT 1');
|
||||
$s->execute([$id]);
|
||||
$formActual = $s->fetchColumn() ?: null;
|
||||
}
|
||||
|
||||
$titulo = $exam ? 'Editar: ' . htmlspecialchars($exam['nombre']) : 'Nuevo Examen';
|
||||
Layout::open($titulo, 'fas fa-flask');
|
||||
$API = BASE_URL . 'modules/lab_examenes/api/';
|
||||
?>
|
||||
<style>
|
||||
body { background:#f1f5f9 }
|
||||
.page-header { background:#fff; border-bottom:1px solid #e2e8f0; padding:12px 24px;
|
||||
display:flex; align-items:center; gap:10px; flex-wrap:wrap }
|
||||
.breadcrumb-lab { font-size:.82rem; color:#64748b }
|
||||
.breadcrumb-lab a { color:#2563eb; text-decoration:none }
|
||||
.content-wrap { max-width:1100px; margin:20px auto; padding:0 16px 60px }
|
||||
.panel { background:#fff; border:1px solid #e2e8f0; border-radius:12px; margin-bottom:20px }
|
||||
.panel-hdr { padding:14px 20px; border-bottom:1px solid #e2e8f0; display:flex; align-items:center; gap:8px }
|
||||
.panel-hdr h2 { font-size:1rem; font-weight:700; color:#1e293b; margin:0; flex:1 }
|
||||
.panel-body { padding:20px }
|
||||
.form-label { font-size:.8rem; font-weight:600; color:#374151 }
|
||||
/* items table */
|
||||
.items-table { width:100%; border-collapse:collapse; font-size:.83rem }
|
||||
.items-table th { background:#f8fafc; padding:7px 10px; font-size:.7rem; text-transform:uppercase;
|
||||
letter-spacing:.06em; color:#64748b; border-bottom:1px solid #e2e8f0; white-space:nowrap }
|
||||
.items-table td { padding:6px 8px; border-bottom:1px solid #f1f5f9; vertical-align:middle }
|
||||
.items-table tr:last-child td { border-bottom:none }
|
||||
.items-table input, .items-table select { font-size:.8rem; padding:3px 6px; border:1px solid #d1d5db;
|
||||
border-radius:5px; width:100% }
|
||||
.items-table input:focus, .items-table select:focus { outline:none; border-color:#6366f1 }
|
||||
/* tarifas table */
|
||||
.tar-table { width:100%; border-collapse:collapse; font-size:.83rem }
|
||||
.tar-table th { background:#f8fafc; padding:7px 14px; font-size:.7rem; text-transform:uppercase;
|
||||
letter-spacing:.06em; color:#64748b; border-bottom:1px solid #e2e8f0 }
|
||||
.tar-table td { padding:7px 14px; border-bottom:1px solid #f1f5f9; vertical-align:middle }
|
||||
.tar-table tr:last-child td { border-bottom:none }
|
||||
.tar-table tr:hover td { background:#f8fafc }
|
||||
.tar-val { font-family:monospace; font-size:.88rem }
|
||||
.edit-inline { display:none; gap:4px; align-items:center }
|
||||
.tr-editing .edit-inline { display:flex }
|
||||
.tr-editing .val-display { display:none }
|
||||
.inp-val { width:90px; padding:3px 6px; font-size:.83rem; border:1px solid #d1d5db; border-radius:5px }
|
||||
.inp-val:focus { border-color:#6366f1; outline:none }
|
||||
</style>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb-lab">
|
||||
<a href="<?= BASE_URL ?>erp.php?m=lab_examenes&v=index"><i class="fas fa-flask me-1"></i>Exámenes</a>
|
||||
<i class="fas fa-chevron-right mx-1" style="font-size:.7rem"></i>
|
||||
<?= $exam ? htmlspecialchars($exam['nombre']) : 'Nuevo examen' ?>
|
||||
</div>
|
||||
<?php if ($exam): ?>
|
||||
<button class="btn btn-outline-danger btn-sm ms-auto" onclick="eliminar(<?=$exam['id']?>, '<?= addslashes(htmlspecialchars($exam['nombre'])) ?>')">
|
||||
<i class="fas fa-trash me-1"></i>Eliminar
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="content-wrap">
|
||||
<div id="alerta-global" class="alert d-none mb-3 py-2"></div>
|
||||
|
||||
<!-- ── Datos generales ── -->
|
||||
<div class="panel">
|
||||
<div class="panel-hdr">
|
||||
<i class="fas fa-info-circle text-primary"></i>
|
||||
<h2>Datos generales</h2>
|
||||
<button class="btn btn-primary btn-sm" onclick="guardarExamen()">
|
||||
<i class="fas fa-save me-1"></i>Guardar
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<input type="hidden" id="exam-id" value="<?= $exam['id'] ?? '' ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label">Código <span class="text-danger">*</span></label>
|
||||
<input type="text" id="f-codigo" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($exam['codigo'] ?? '') ?>" maxlength="20" style="text-transform:uppercase">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label">Código CUPS</label>
|
||||
<input type="text" id="f-cups" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($exam['cups'] ?? '') ?>" maxlength="20">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label">Abreviatura</label>
|
||||
<input type="text" id="f-abreviatura" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($exam['abreviatura'] ?? '') ?>" maxlength="30">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label">Nivel</label>
|
||||
<input type="number" id="f-nivel" class="form-control form-control-sm"
|
||||
value="<?= $exam['nivel'] ?? '' ?>" min="0" max="9">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" id="f-nombre" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($exam['nombre'] ?? '') ?>" maxlength="150">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">Protocolo</label>
|
||||
<select id="f-protocolo" class="form-select form-select-sm">
|
||||
<option value="">— ninguno —</option>
|
||||
<?php foreach ($protocolos as $p): ?>
|
||||
<option value="<?= htmlspecialchars($p['codigo']) ?>"
|
||||
<?= ($exam['cod_protocolo'] ?? '') === $p['codigo'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($p['codigo'] . ' – ' . $p['nombre']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">Tipo de muestra</label>
|
||||
<select id="f-muestra" class="form-select form-select-sm">
|
||||
<option value="">— ninguna —</option>
|
||||
<?php foreach ($muestras as $m): ?>
|
||||
<option value="<?= htmlspecialchars($m['codigo']) ?>"
|
||||
<?= ($exam['tipo_muestra'] ?? '') === $m['codigo'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($m['nombre']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">Formulario consentimiento</label>
|
||||
<select id="f-formulario" class="form-select form-select-sm">
|
||||
<option value="">— ninguno —</option>
|
||||
<?php foreach ($formularios as $f): ?>
|
||||
<option value="<?= $f['id'] ?>" <?= (int)$formActual === (int)$f['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($f['nombre']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Se recibe de (lab externo)</label>
|
||||
<input type="text" id="f-serecibe" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($exam['serecibe'] ?? '') ?>" maxlength="50">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Instrucciones para el paciente</label>
|
||||
<input type="text" id="f-instrucciones" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($exam['instrucciones'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex gap-4 flex-wrap">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="f-seremite"
|
||||
<?= ($exam['seremite'] ?? 0) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label small" for="f-seremite">Se remite a laboratorio externo</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="f-ayuno"
|
||||
<?= ($exam['requiere_ayuno'] ?? 0) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label small" for="f-ayuno">Requiere ayuno</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="f-activo"
|
||||
<?= ($exam['activo'] ?? 1) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label small" for="f-activo">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3" id="blq-ayuno" style="display:<?= ($exam['requiere_ayuno'] ?? 0) ? 'block' : 'none' ?>">
|
||||
<label class="form-label">Horas de ayuno</label>
|
||||
<input type="number" id="f-horas-ayuno" class="form-control form-control-sm"
|
||||
value="<?= $exam['horas_ayuno'] ?? '' ?>" min="1" max="72">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($exam): ?>
|
||||
<!-- ── Items de resultado ── -->
|
||||
<div class="panel">
|
||||
<div class="panel-hdr">
|
||||
<i class="fas fa-list text-success"></i>
|
||||
<h2>Items / Valores de referencia</h2>
|
||||
<button class="btn btn-outline-success btn-sm" onclick="addItem()">
|
||||
<i class="fas fa-plus me-1"></i>Agregar item
|
||||
</button>
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px">#</th>
|
||||
<th>Nombre item</th>
|
||||
<th style="width:70px">Sexo</th>
|
||||
<th style="width:70px">Tipo</th>
|
||||
<th style="width:90px">Medida</th>
|
||||
<th style="width:80px">V.Min</th>
|
||||
<th style="width:80px">V.Max</th>
|
||||
<th style="width:50px">Ord.</th>
|
||||
<th>Fórmula</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-body">
|
||||
<?php foreach ($items as $it): ?>
|
||||
<tr data-id="<?=$it['id']?>" data-cod="<?=htmlspecialchars($exam['cod_protocolo']??$exam['codigo'])?>">
|
||||
<?= _itemRow($it) ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Tarifas ── -->
|
||||
<div class="panel">
|
||||
<div class="panel-hdr">
|
||||
<i class="fas fa-tags text-warning"></i>
|
||||
<h2>Precios por tarifa</h2>
|
||||
<small class="text-muted">Clic en el valor para editar</small>
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="tar-table" id="tar-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px">#</th>
|
||||
<th>Tarifa</th>
|
||||
<th style="width:120px">Valor</th>
|
||||
<th style="width:100px">R. Urgencia</th>
|
||||
<th style="width:100px">R. Festivo</th>
|
||||
<th style="width:100px">R. Especial</th>
|
||||
<th style="width:80px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tar-body">
|
||||
<tr><td colspan="7" class="text-center text-muted py-3">Cargando tarifas…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
function _itemRow(array $it): string {
|
||||
$sexos = [''=>'Ambos','M'=>'M','F'=>'F'];
|
||||
$tipos = ['T'=>'Texto','N'=>'Numérico'];
|
||||
$sOpts = ''; foreach($sexos as $v=>$l) $sOpts .= "<option value='$v'".($it['tipo_sexo']===$v?' selected':'').">$l</option>";
|
||||
$tOpts = ''; foreach($tipos as $v=>$l) $tOpts .= "<option value='$v'".($it['tipo']===$v?' selected':'').">$l</option>";
|
||||
return "
|
||||
<td><input type='number' name='orden' value='{$it['orden']}' style='width:45px'></td>
|
||||
<td><input type='text' name='nombre' value='".htmlspecialchars($it['nombre'])."' style='min-width:160px'></td>
|
||||
<td><select name='tipo_sexo'>$sOpts</select></td>
|
||||
<td><select name='tipo'>$tOpts</select></td>
|
||||
<td><input type='text' name='medida' value='".htmlspecialchars($it['medida']??'')."'></td>
|
||||
<td><input type='number' name='vmin_ref' value='".($it['vmin_ref']??'')."' step='any'></td>
|
||||
<td><input type='number' name='vmax_ref' value='".($it['vmax_ref']??'')."' step='any'></td>
|
||||
<td><input type='number' name='orden2' value='{$it['orden']}' style='width:45px' disabled></td>
|
||||
<td><input type='text' name='formula' value='".htmlspecialchars($it['formula']??'')."'></td>
|
||||
<td><button type='button' class='btn btn-sm btn-outline-success py-0 px-1 me-1' onclick='saveItem(this)' title='Guardar'><i class='fas fa-check'></i></button>
|
||||
<button type='button' class='btn btn-sm btn-outline-danger py-0 px-1' onclick='delItem(this)' title='Eliminar'><i class='fas fa-trash'></i></button></td>
|
||||
";
|
||||
}
|
||||
?>
|
||||
|
||||
<script>
|
||||
const API = '<?= $API ?>';
|
||||
const EXAM_ID = <?= $id ?: 'null' ?>;
|
||||
const COD_PROT = '<?= addslashes($exam['cod_protocolo'] ?? ($exam['codigo'] ?? '')) ?>';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('f-ayuno')?.addEventListener('change', e => {
|
||||
document.getElementById('blq-ayuno').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
if (EXAM_ID) cargarTarifas();
|
||||
});
|
||||
|
||||
// ── Guardar examen ─────────────────────────────────
|
||||
async function guardarExamen() {
|
||||
const payload = {
|
||||
id: document.getElementById('exam-id').value || null,
|
||||
codigo: document.getElementById('f-codigo').value.trim().toUpperCase(),
|
||||
nombre: document.getElementById('f-nombre').value.trim(),
|
||||
cups: document.getElementById('f-cups').value.trim(),
|
||||
abreviatura: document.getElementById('f-abreviatura').value.trim(),
|
||||
nivel: document.getElementById('f-nivel').value,
|
||||
cod_protocolo: document.getElementById('f-protocolo').value,
|
||||
tipo_muestra: document.getElementById('f-muestra').value,
|
||||
seremite: document.getElementById('f-seremite').checked ? 1 : 0,
|
||||
requiere_ayuno: document.getElementById('f-ayuno').checked ? 1 : 0,
|
||||
horas_ayuno: document.getElementById('f-horas-ayuno')?.value || null,
|
||||
instrucciones: document.getElementById('f-instrucciones').value.trim(),
|
||||
serecibe: document.getElementById('f-serecibe').value.trim(),
|
||||
activo: document.getElementById('f-activo').checked ? 1 : 0,
|
||||
formulario_id: document.getElementById('f-formulario').value || null,
|
||||
};
|
||||
const r = await fetch(API+'save.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json());
|
||||
if (r.ok) {
|
||||
mostrarAlerta('success', r.message || 'Guardado');
|
||||
if (!payload.id && r.id) setTimeout(() => location.href = location.href.split('?')[0] + '?m=lab_examenes&v=examen&id='+r.id, 800);
|
||||
} else {
|
||||
mostrarAlerta('danger', r.error || 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
async function eliminar(id, nombre) {
|
||||
if (!confirm(`¿Eliminar "${nombre}"? No se puede deshacer.`)) return;
|
||||
const r = await fetch(API+'save.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,_delete:true})}).then(r=>r.json());
|
||||
if (r.ok) location.href = '<?= BASE_URL ?>erp.php?m=lab_examenes&v=index';
|
||||
else mostrarAlerta('danger', r.error);
|
||||
}
|
||||
|
||||
// ── Items ─────────────────────────────────────────
|
||||
function addItem() {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.id = '0';
|
||||
tr.dataset.cod = COD_PROT;
|
||||
tr.innerHTML = `
|
||||
<td><input type='number' name='orden' value='0' style='width:45px'></td>
|
||||
<td><input type='text' name='nombre' placeholder='Nombre del item' style='min-width:160px'></td>
|
||||
<td><select name='tipo_sexo'><option value=''>Ambos</option><option value='M'>M</option><option value='F'>F</option></select></td>
|
||||
<td><select name='tipo'><option value='T'>Texto</option><option value='N'>Numérico</option></select></td>
|
||||
<td><input type='text' name='medida'></td>
|
||||
<td><input type='number' name='vmin_ref' step='any'></td>
|
||||
<td><input type='number' name='vmax_ref' step='any'></td>
|
||||
<td><input type='number' name='orden2' style='width:45px' disabled></td>
|
||||
<td><input type='text' name='formula'></td>
|
||||
<td><button type='button' class='btn btn-sm btn-outline-success py-0 px-1 me-1' onclick='saveItem(this)'><i class='fas fa-check'></i></button>
|
||||
<button type='button' class='btn btn-sm btn-outline-danger py-0 px-1' onclick='this.closest("tr").remove()'><i class='fas fa-times'></i></button></td>`;
|
||||
document.getElementById('items-body').appendChild(tr);
|
||||
tr.querySelector('[name=nombre]').focus();
|
||||
}
|
||||
|
||||
async function saveItem(btn) {
|
||||
const tr = btn.closest('tr');
|
||||
const g = n => tr.querySelector(`[name=${n}]`)?.value ?? '';
|
||||
const payload = {
|
||||
id: +tr.dataset.id || null,
|
||||
cod_protocolo: tr.dataset.cod || COD_PROT,
|
||||
nombre: g('nombre').trim(),
|
||||
tipo_sexo: g('tipo_sexo') || null,
|
||||
tipo: g('tipo') || 'T',
|
||||
medida: g('medida'),
|
||||
vmin_ref: g('vmin_ref'),
|
||||
vmax_ref: g('vmax_ref'),
|
||||
orden: +g('orden') || 0,
|
||||
formula: g('formula'),
|
||||
};
|
||||
if (!payload.nombre) { alert('Nombre requerido'); return; }
|
||||
const r = await fetch(API+'save_item.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json());
|
||||
if (r.ok) { tr.dataset.id = r.id || tr.dataset.id; mostrarAlerta('success','Item guardado'); }
|
||||
else mostrarAlerta('danger', r.error);
|
||||
}
|
||||
|
||||
async function delItem(btn) {
|
||||
const tr = btn.closest('tr');
|
||||
const id = +tr.dataset.id;
|
||||
if (id && !confirm('¿Eliminar este item?')) return;
|
||||
if (id) {
|
||||
const r = await fetch(API+'save_item.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,_delete:true})}).then(r=>r.json());
|
||||
if (!r.ok) { mostrarAlerta('danger', r.error); return; }
|
||||
}
|
||||
tr.remove();
|
||||
}
|
||||
|
||||
// ── Tarifas ──────────────────────────────────────
|
||||
let _tarifas = [];
|
||||
async function cargarTarifas() {
|
||||
const r = await fetch(API+'get_tarifas.php?exam_id='+EXAM_ID).then(r=>r.json());
|
||||
if (!r.ok) return;
|
||||
_tarifas = r.data;
|
||||
renderTarifas();
|
||||
}
|
||||
|
||||
function renderTarifas() {
|
||||
const body = document.getElementById('tar-body');
|
||||
if (!_tarifas.length) { body.innerHTML='<tr><td colspan="7" class="text-center text-muted py-3">Sin tarifas.</td></tr>'; return; }
|
||||
body.innerHTML = _tarifas.map((t,i) => {
|
||||
const v = t.valor !== null ? parseFloat(t.valor).toLocaleString('es-CO') : '—';
|
||||
return `<tr data-tidx="${i}">
|
||||
<td class="text-muted" style="font-size:.75rem">${t.tarifa_id}</td>
|
||||
<td>${esc(t.tarifa_nombre)}</td>
|
||||
<td class="tar-val val-display" onclick="editTarifa(this)">${v}</td>
|
||||
<td class="text-muted tar-val" style="font-size:.8rem">${t.recargo_urg>0?parseFloat(t.recargo_urg).toLocaleString('es-CO'):'—'}</td>
|
||||
<td class="text-muted tar-val" style="font-size:.8rem">${t.recargo_fes>0?parseFloat(t.recargo_fes).toLocaleString('es-CO'):'—'}</td>
|
||||
<td class="text-muted tar-val" style="font-size:.8rem">${t.recargo_esp>0?parseFloat(t.recargo_esp).toLocaleString('es-CO'):'—'}</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-2" onclick="abrirEditorTarifa(${i})" title="Editar precios">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function abrirEditorTarifa(idx) {
|
||||
const t = _tarifas[idx];
|
||||
const modal = new bootstrap.Modal(document.getElementById('modalTarifa'));
|
||||
document.getElementById('mt-nombre').textContent = t.tarifa_nombre;
|
||||
document.getElementById('mt-idx').value = idx;
|
||||
document.getElementById('mt-valor').value = t.valor ?? '';
|
||||
document.getElementById('mt-urg').value = t.recargo_urg ?? 0;
|
||||
document.getElementById('mt-fes').value = t.recargo_fes ?? 0;
|
||||
document.getElementById('mt-esp').value = t.recargo_esp ?? 0;
|
||||
modal.show();
|
||||
document.getElementById('mt-valor').focus();
|
||||
}
|
||||
|
||||
async function guardarTarifa() {
|
||||
const idx = +document.getElementById('mt-idx').value;
|
||||
const t = _tarifas[idx];
|
||||
const payload = {
|
||||
exam_tipo_id: EXAM_ID,
|
||||
tarifa_id: t.tarifa_id,
|
||||
valor: parseFloat(document.getElementById('mt-valor').value) || 0,
|
||||
recargo_urg: parseFloat(document.getElementById('mt-urg').value) || 0,
|
||||
recargo_fes: parseFloat(document.getElementById('mt-fes').value) || 0,
|
||||
recargo_esp: parseFloat(document.getElementById('mt-esp').value) || 0,
|
||||
};
|
||||
const btn = document.getElementById('btn-guardar-tarifa');
|
||||
btn.disabled = true;
|
||||
const r = await fetch(API+'save_tarifa.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json());
|
||||
btn.disabled = false;
|
||||
if (r.ok) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('modalTarifa')).hide();
|
||||
cargarTarifas();
|
||||
mostrarAlerta('success','Precio guardado');
|
||||
} else {
|
||||
mostrarAlerta('danger', r.error);
|
||||
}
|
||||
}
|
||||
|
||||
function mostrarAlerta(tipo, msg) {
|
||||
const el = document.getElementById('alerta-global');
|
||||
el.className = `alert alert-${tipo} py-2`;
|
||||
el.textContent = msg;
|
||||
el.classList.remove('d-none');
|
||||
setTimeout(()=>el.classList.add('d-none'), 3500);
|
||||
el.scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
}
|
||||
|
||||
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
</script>
|
||||
|
||||
<!-- Modal editar tarifa -->
|
||||
<div class="modal fade" id="modalTarifa" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title"><i class="fas fa-tag me-1"></i><span id="mt-nombre"></span></h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="mt-idx">
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:600">Valor base</label>
|
||||
<input type="number" id="mt-valor" class="form-control form-control-sm" step="1" min="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:600">R. Urgencia</label>
|
||||
<input type="number" id="mt-urg" class="form-control form-control-sm" step="1" min="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:600">R. Festivo</label>
|
||||
<input type="number" id="mt-fes" class="form-control form-control-sm" step="1" min="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:600">R. Especial</label>
|
||||
<input type="number" id="mt-esp" class="form-control form-control-sm" step="1" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button class="btn btn-primary btn-sm" id="btn-guardar-tarifa" onclick="guardarTarifa()">
|
||||
<i class="fas fa-save me-1"></i>Guardar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php Layout::close(); ?>
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
require_once APP_ROOT . '/config/config.php';
|
||||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||||
Layout::open('Catálogo de Exámenes', 'fas fa-flask');
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$categorias = $pdo->query(
|
||||
"SELECT DISTINCT categoria FROM exam_tipos WHERE categoria IS NOT NULL ORDER BY categoria"
|
||||
)->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$API = BASE_URL . 'modules/lab_examenes/api/';
|
||||
$DETAIL = BASE_URL . 'erp.php?m=lab_examenes&v=examen&id=';
|
||||
?>
|
||||
<style>
|
||||
body { background:#f1f5f9 }
|
||||
.page-header { background:#fff; border-bottom:1px solid #e2e8f0; padding:14px 24px;
|
||||
display:flex; align-items:center; gap:12px; flex-wrap:wrap }
|
||||
.page-header h1 { font-size:1.18rem; font-weight:700; color:#1e293b; margin:0; flex:1 }
|
||||
.content-wrap { max-width:1200px; margin:24px auto; padding:0 16px 48px }
|
||||
.card-box { background:#fff; border:1px solid #e2e8f0; border-radius:12px; overflow:hidden }
|
||||
.toolbar { padding:12px 16px; display:flex; gap:10px; align-items:center;
|
||||
border-bottom:1px solid #e2e8f0; flex-wrap:wrap }
|
||||
.toolbar input, .toolbar select { font-size:.85rem }
|
||||
table { width:100%; border-collapse:collapse; font-size:.88rem }
|
||||
thead th { background:#f8fafc; padding:9px 14px; font-size:.7rem; text-transform:uppercase;
|
||||
letter-spacing:.07em; color:#64748b; border-bottom:1px solid #e2e8f0; white-space:nowrap }
|
||||
tbody td { padding:9px 14px; border-bottom:1px solid #f1f5f9; color:#1e293b; vertical-align:middle }
|
||||
tbody tr:last-child td { border-bottom:none }
|
||||
tbody tr:hover td { background:#f8fafc }
|
||||
.badge-cat { background:#eff6ff; color:#1d4ed8; border-radius:20px; padding:2px 9px;
|
||||
font-size:.72rem; font-weight:600; white-space:nowrap }
|
||||
.badge-remite { background:#dcfce7; color:#166534; border-radius:20px; padding:2px 9px; font-size:.72rem }
|
||||
.badge-off { background:#f1f5f9; color:#94a3b8; border-radius:20px; padding:2px 9px; font-size:.72rem }
|
||||
.pag-bar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.5rem;
|
||||
padding:10px 16px; border-top:1px solid #e2e8f0; background:#f8fafc }
|
||||
.pag-info { font-size:.8rem; color:#64748b }
|
||||
.pag-btn { min-width:32px; height:32px; padding:0 8px; border:1px solid #e2e8f0; border-radius:7px;
|
||||
background:#fff; font-size:.82rem; color:#374151; cursor:pointer;
|
||||
display:inline-flex; align-items:center; justify-content:center }
|
||||
.pag-btn:hover:not(:disabled) { background:#eff6ff; border-color:#bfdbfe; color:#1d4ed8 }
|
||||
.pag-btn.active { background:#2563eb; color:#fff; border-color:#2563eb }
|
||||
.pag-btn:disabled { opacity:.4; cursor:not-allowed }
|
||||
.toggle-activo { cursor:pointer; font-size:1.05rem }
|
||||
</style>
|
||||
|
||||
<div class="page-header">
|
||||
<i class="fas fa-flask" style="font-size:1.3rem;color:#6366f1"></i>
|
||||
<h1>Catálogo de Exámenes</h1>
|
||||
<a href="<?= BASE_URL ?>erp.php?m=lab_examenes&v=examen" class="btn btn-primary btn-sm ms-auto">
|
||||
<i class="fas fa-plus me-1"></i>Nuevo examen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="content-wrap">
|
||||
<div class="card-box">
|
||||
<div class="toolbar">
|
||||
<input type="search" id="inp-q" class="form-control form-control-sm"
|
||||
placeholder="Buscar por nombre, código o CUPS…" style="max-width:280px" oninput="buscar()">
|
||||
<select id="sel-cat" class="form-select form-select-sm" style="max-width:220px" onchange="cargar(1)">
|
||||
<option value="">Todas las categorías</option>
|
||||
<?php foreach ($categorias as $c): ?>
|
||||
<option value="<?= htmlspecialchars($c) ?>"><?= htmlspecialchars($c) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<span class="text-muted small ms-auto" id="lbl-total"></span>
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre</th>
|
||||
<th>Categoría</th>
|
||||
<th>CUPS</th>
|
||||
<th>Protocolo</th>
|
||||
<th>Se remite</th>
|
||||
<th>Activo</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbl-body">
|
||||
<tr><td colspan="8" class="text-center text-muted py-4">Cargando…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pag-bar" id="pag-bar" style="display:none">
|
||||
<span class="pag-info" id="pag-info"></span>
|
||||
<div id="pag-ctrl" style="display:flex;gap:4px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '<?= $API ?>';
|
||||
const DETAIL = '<?= $DETAIL ?>';
|
||||
let _page=1, _limit=50, _total=0, _q='', _timer;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => cargar(1));
|
||||
|
||||
function buscar() {
|
||||
clearTimeout(_timer);
|
||||
_timer = setTimeout(() => cargar(1), 260);
|
||||
}
|
||||
|
||||
async function cargar(page=_page) {
|
||||
_page = page;
|
||||
_q = document.getElementById('inp-q').value.trim();
|
||||
const cat = document.getElementById('sel-cat').value;
|
||||
const p = new URLSearchParams({page, limit:_limit});
|
||||
if (_q) p.set('q', _q);
|
||||
if (cat) p.set('categoria', cat);
|
||||
const r = await fetch(API+'list.php?'+p).then(r=>r.json());
|
||||
if (!r.ok) return;
|
||||
_total = r.total;
|
||||
renderTabla(r.data);
|
||||
renderPag(Math.ceil(_total/_limit));
|
||||
const d=(_page-1)*_limit+1, h=Math.min(_page*_limit,_total);
|
||||
document.getElementById('lbl-total').textContent = _total ? `${d}–${h} de ${_total}` : '0 exámenes';
|
||||
}
|
||||
|
||||
function renderTabla(rows) {
|
||||
const b = document.getElementById('tbl-body');
|
||||
if (!rows.length) { b.innerHTML='<tr><td colspan="8" class="text-center text-muted py-4">Sin resultados.</td></tr>'; return; }
|
||||
b.innerHTML = rows.map(r => `
|
||||
<tr>
|
||||
<td><code class="text-primary">${esc(r.codigo)}</code></td>
|
||||
<td><a href="${DETAIL+r.id}" class="text-decoration-none fw-semibold text-dark">${esc(r.nombre)}</a></td>
|
||||
<td>${r.categoria ? `<span class="badge-cat">${esc(r.categoria)}</span>` : '<span class="text-muted">—</span>'}</td>
|
||||
<td><small class="text-muted">${esc(r.cups||'—')}</small></td>
|
||||
<td><small class="text-muted">${esc(r.cod_protocolo||'—')}</small></td>
|
||||
<td>${+r.seremite ? '<span class="badge-remite">Sí</span>' : '<span class="badge-off">No</span>'}</td>
|
||||
<td><span class="toggle-activo" title="Clic para cambiar" onclick="toggleActivo(${r.id},${r.activo})">${+r.activo?'✅':'⬜'}</span></td>
|
||||
<td><a href="${DETAIL+r.id}" class="btn btn-outline-primary btn-sm py-0 px-2"><i class="fas fa-pencil-alt"></i></a></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function toggleActivo(id, actual) {
|
||||
const r = await fetch(API+'save.php', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({id, activo: actual=='1'||actual===1 ? 0 : 1})
|
||||
}).then(r=>r.json());
|
||||
if (r.ok) cargar(_page);
|
||||
}
|
||||
|
||||
function renderPag(total) {
|
||||
const bar = document.getElementById('pag-bar');
|
||||
const ctrl = document.getElementById('pag-ctrl');
|
||||
if (total<=1) { bar.style.display='none'; return; }
|
||||
bar.style.display='flex';
|
||||
document.getElementById('pag-info').textContent = `Página ${_page} de ${total}`;
|
||||
const pages = new Set([1,total,_page]);
|
||||
for(let i=_page-2;i<=_page+2;i++) if(i>0&&i<=total) pages.add(i);
|
||||
const sorted=[...pages].sort((a,b)=>a-b);
|
||||
let h=`<button class="pag-btn" onclick="cargar(${_page-1})" ${_page===1?'disabled':''}><i class="fas fa-chevron-left"></i></button>`;
|
||||
let prev=0;
|
||||
for(const p of sorted){
|
||||
if(prev&&p-prev>1) h+=`<span style="padding:0 4px;line-height:32px;font-size:.82rem;color:#94a3b8">…</span>`;
|
||||
h+=`<button class="pag-btn ${p===_page?'active':''}" onclick="cargar(${p})">${p}</button>`;
|
||||
prev=p;
|
||||
}
|
||||
h+=`<button class="pag-btn" onclick="cargar(${_page+1})" ${_page===total?'disabled':''}><i class="fas fa-chevron-right"></i></button>`;
|
||||
ctrl.innerHTML=h;
|
||||
}
|
||||
|
||||
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
</script>
|
||||
<?php Layout::close(); ?>
|
||||
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
/**
|
||||
* ETL: Firebird staging → MySQL producción
|
||||
*
|
||||
* PASO PREVIO (una sola vez):
|
||||
* php scripts/etl_examenes.php --load # carga staging y ejecuta ETL
|
||||
* php scripts/etl_examenes.php # solo ETL (staging ya cargado)
|
||||
* php scripts/etl_examenes.php --clean # elimina tablas staging
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
// MariaDB defaultea utf8mb4 a uca1400_ai_ci; forzamos unicode_ci para que
|
||||
// los literales de string en queries no colisionen con columnas de producción
|
||||
$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
|
||||
$doLoad = in_array('--load', $argv);
|
||||
$doClean = in_array('--clean', $argv);
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────
|
||||
function run(PDO $pdo, string $sql, string $label): void {
|
||||
$t = microtime(true);
|
||||
$rows = $pdo->exec($sql);
|
||||
printf(" %-45s %6d filas %.2fs\n", $label, $rows, microtime(true) - $t);
|
||||
}
|
||||
|
||||
function step(string $msg): void {
|
||||
echo "\n── $msg\n";
|
||||
}
|
||||
|
||||
// ── limpiar staging si se pide ────────────────────────────────
|
||||
if ($doClean) {
|
||||
$staging = ['SECCION','PROTOCOLO','MUESTRA','EXAMEN','ITEM',
|
||||
'PERFIL','PERFIL_EXA','TARIFAID','TARIFA','LAB_REFER',
|
||||
'stg_empresa','stg_examen_emp'];
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||
foreach ($staging as $t) {
|
||||
$pdo->exec("DROP TABLE IF EXISTS `$t`");
|
||||
echo " DROP $t\n";
|
||||
}
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||
echo "Staging eliminado.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ── cargar staging desde examenes_completo.sql ────────────────
|
||||
if ($doLoad) {
|
||||
$sqlFile = realpath(__DIR__ . '/../database/examenes_completo.sql');
|
||||
if (!$sqlFile) { echo "ERROR: No se encuentra examenes_completo.sql\n"; exit(1); }
|
||||
|
||||
$tmpFile = sys_get_temp_dir() . '/examenes_staging_' . getmypid() . '.sql';
|
||||
echo "Preparando staging SQL...\n";
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
$sql = preg_replace('/\blab_examenes_empresa\b/', 'stg_examen_emp', $sql);
|
||||
$sql = preg_replace('/\blab_empresas\b/', 'stg_empresa', $sql);
|
||||
$sql = preg_replace('/\bINSERT INTO\b/', 'INSERT IGNORE INTO', $sql);
|
||||
file_put_contents($tmpFile, $sql);
|
||||
unset($sql);
|
||||
|
||||
$host = DB_HOST; $port = DB_PORT; $user = DB_USER; $pass = DB_PASS; $db = DB_NAME;
|
||||
echo "Cargando staging vía mysql CLI (puede tardar 2-3 min)...\n";
|
||||
$cmd = "mysql -h " . escapeshellarg($host)
|
||||
. " -P " . escapeshellarg($port)
|
||||
. " -u " . escapeshellarg($user)
|
||||
. " -p" . escapeshellarg($pass)
|
||||
. " --default-character-set=utf8mb4"
|
||||
. " " . escapeshellarg($db)
|
||||
. " < " . escapeshellarg($tmpFile)
|
||||
. " 2>&1";
|
||||
passthru($cmd, $ret);
|
||||
unlink($tmpFile);
|
||||
if ($ret !== 0) { echo "ERROR cargando staging.\n"; exit(1); }
|
||||
echo "Staging cargado.\n";
|
||||
}
|
||||
|
||||
// ── verificar staging ─────────────────────────────────────────
|
||||
try {
|
||||
$pdo->query("SELECT 1 FROM EXAMEN LIMIT 1");
|
||||
} catch (\Throwable $e) {
|
||||
echo "ERROR: Tablas staging no encontradas. Ejecuta: php etl_examenes.php --load\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "ETL Firebird → MySQL\n";
|
||||
echo str_repeat('─', 60) . "\n";
|
||||
|
||||
// convertir staging a misma collation que producción (MariaDB usa uca1400 por defecto)
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
|
||||
foreach (['SECCION','PROTOCOLO','MUESTRA','EXAMEN','ITEM',
|
||||
'PERFIL','PERFIL_EXA','TARIFAID','TARIFA','LAB_REFER',
|
||||
'stg_empresa','stg_examen_emp'] as $_t) {
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE `$_t` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
} catch (\Throwable $_e) { /* tabla no existe aún, ok */ }
|
||||
}
|
||||
$pdo->exec('SET UNIQUE_CHECKS = 0');
|
||||
|
||||
// ── 1. SECCION → lab_secciones ───────────────────────────────
|
||||
step('1. SECCION → lab_secciones');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_secciones (codigo, nombre)
|
||||
SELECT CODIGO, NOMBRE FROM SECCION
|
||||
", 'lab_secciones');
|
||||
|
||||
// ── 2. PROTOCOLO → lab_protocolos ────────────────────────────
|
||||
step('2. PROTOCOLO → lab_protocolos');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_protocolos (codigo, nombre, cod_seccion)
|
||||
SELECT CODIGO, NOMBRE, COD_SECCION FROM PROTOCOLO
|
||||
", 'lab_protocolos');
|
||||
|
||||
// ── 3. MUESTRA → lab_tipos_muestra ───────────────────────────
|
||||
step('3. MUESTRA → lab_tipos_muestra');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_tipos_muestra (codigo, nombre)
|
||||
SELECT CODIGO, NOMBRE FROM MUESTRA
|
||||
", 'lab_tipos_muestra');
|
||||
|
||||
// ── 4. EXAMEN → exam_tipos ───────────────────────────────────
|
||||
step('4. EXAMEN → exam_tipos');
|
||||
// codigo = EXAMEN.CODIGO (unique en Firebird, usamos como código interno también)
|
||||
// categoria = nombre de la sección del protocolo
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO exam_tipos
|
||||
(codigo, nombre, categoria, activo,
|
||||
codigo_legacy, cups, cod_protocolo, tipo_muestra,
|
||||
nivel, abreviatura, seremite, serecibe)
|
||||
SELECT
|
||||
e.CODIGO,
|
||||
e.NOMBRE,
|
||||
s.NOMBRE,
|
||||
1,
|
||||
e.CODIGO,
|
||||
NULLIF(e.CUPS, ''),
|
||||
NULLIF(e.COD_PROTOCOLO, ''),
|
||||
NULLIF(e.TIPOMUESTRA, ''),
|
||||
e.NIVEL,
|
||||
NULLIF(e.ABREVIATURA, ''),
|
||||
IF(e.SEREMITE = 'T', 1, 0),
|
||||
NULLIF(e.SERECIBE, '')
|
||||
FROM EXAMEN e
|
||||
LEFT JOIN PROTOCOLO p ON p.CODIGO = e.COD_PROTOCOLO
|
||||
LEFT JOIN SECCION s ON s.CODIGO = p.COD_SECCION
|
||||
", 'exam_tipos');
|
||||
|
||||
// ── 5. ITEM → lab_items_resultado ────────────────────────────
|
||||
step('5. ITEM → lab_items_resultado');
|
||||
// ponytail: truncate antes de insertar — ITEM no tiene UK en MySQL
|
||||
$pdo->exec('DELETE FROM lab_items_resultado');
|
||||
run($pdo, "
|
||||
INSERT INTO lab_items_resultado
|
||||
(cod_protocolo, nombre, tipo_sexo, tipo,
|
||||
medida, abreviatura, vmin_ref, vmax_ref,
|
||||
orden, formula, cups_detalle)
|
||||
SELECT
|
||||
i.COD_PROTOCOLO,
|
||||
i.NOM_ITEM,
|
||||
CASE i.TIPO_SEXO WHEN 'M' THEN 'M' WHEN 'F' THEN 'F' ELSE NULL END,
|
||||
CASE WHEN i.TIPO = '2' THEN 'N' ELSE 'T' END,
|
||||
NULLIF(i.MEDIDA, ''),
|
||||
NULLIF(i.ABREVIATURA, ''),
|
||||
NULLIF(i.VMINREF, 0),
|
||||
NULLIF(i.VMAXREF, 0),
|
||||
COALESCE(i.ORDEN, 0),
|
||||
NULLIF(i.FORMULA, ''),
|
||||
NULLIF(i.CUPS_DETALLE, '')
|
||||
FROM ITEM i
|
||||
WHERE EXISTS (SELECT 1 FROM lab_protocolos p WHERE p.codigo = i.COD_PROTOCOLO)
|
||||
", 'lab_items_resultado');
|
||||
|
||||
// ── 6. PERFIL → lab_perfiles ─────────────────────────────────
|
||||
step('6. PERFIL → lab_perfiles');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_perfiles (id, nombre, activo)
|
||||
SELECT COD_PERFIL, NOM_PERFIL, 1 FROM PERFIL
|
||||
", 'lab_perfiles');
|
||||
|
||||
// ── 7. PERFIL_EXA → lab_perfil_examenes ──────────────────────
|
||||
step('7. PERFIL_EXA → lab_perfil_examenes');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_perfil_examenes (perfil_id, exam_tipo_id)
|
||||
SELECT pe.COD_PERFIL, et.id
|
||||
FROM PERFIL_EXA pe
|
||||
JOIN exam_tipos et ON et.codigo_legacy = pe.COD_EXA
|
||||
", 'lab_perfil_examenes');
|
||||
|
||||
// ── 8. TARIFAID → lab_tarifas_id ─────────────────────────────
|
||||
step('8. TARIFAID → lab_tarifas_id');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_tarifas_id (id, nombre, tarifa_origen, porcentaje)
|
||||
SELECT
|
||||
COD_TARIFA,
|
||||
NOMBRE,
|
||||
NULLIF(TARIFAORIGEN, 0),
|
||||
PORCENTAJE
|
||||
FROM TARIFAID
|
||||
", 'lab_tarifas_id');
|
||||
|
||||
// ── 9. TARIFA → lab_tarifas (104 788 filas) ──────────────────
|
||||
step('9. TARIFA → lab_tarifas [puede tardar ~30s]');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_tarifas
|
||||
(cod_examen_legacy, exam_tipo_id, tarifa_id,
|
||||
valor, recargo_urg, recargo_fes, recargo_esp)
|
||||
SELECT
|
||||
t.COD_EXAMEN,
|
||||
et.id,
|
||||
t.TARIFA,
|
||||
COALESCE(t.VALOR, 0),
|
||||
COALESCE(t.RECARGO_URG, 0),
|
||||
COALESCE(t.RECARGO_FES, 0),
|
||||
COALESCE(t.RECARGO_ESP, 0)
|
||||
FROM TARIFA t
|
||||
LEFT JOIN exam_tipos et ON et.codigo_legacy = t.COD_EXAMEN
|
||||
", 'lab_tarifas');
|
||||
|
||||
// ── 10. LAB_REFER → lab_laboratorios_externos ────────────────
|
||||
step('10. LAB_REFER → lab_laboratorios_externos');
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS lab_laboratorios_externos (
|
||||
codigo VARCHAR(20) NOT NULL,
|
||||
nombre VARCHAR(100),
|
||||
nit_lab VARCHAR(30),
|
||||
direccion VARCHAR(200),
|
||||
telefonos VARCHAR(50),
|
||||
email VARCHAR(100),
|
||||
activo TINYINT(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (codigo)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Laboratorios externos de referencia — replica LAB_REFER Firebird'
|
||||
");
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_laboratorios_externos
|
||||
(codigo, nombre, nit_lab, direccion, telefonos, email, activo)
|
||||
SELECT
|
||||
CODIGO, NOMBRE, NIT_LAB, DIRECCION, TELEFONOS, EMAIL,
|
||||
IF(ACTIVADO = 'T', 1, 0)
|
||||
FROM LAB_REFER
|
||||
", 'lab_laboratorios_externos');
|
||||
|
||||
// ── 11. EMPRESA (staging) → lab_empresas ─────────────────────
|
||||
step('11. EMPRESA → lab_empresas');
|
||||
// ponytail: staging usa columnas Firebird, producción tiene columnas en snake_case
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_empresas
|
||||
(nit, nombre, razon_social, tarifa_id, descuento_pct,
|
||||
codigo_eps, tipo_usuario, tipo_usuario_sispro,
|
||||
cod_contrato, cod_tercero, centro_costo,
|
||||
req_autoriza, activa)
|
||||
SELECT
|
||||
NIT,
|
||||
NOMBRE,
|
||||
NULLIF(RAZONSOCIAL, ''),
|
||||
NULLIF(TARIFA, 0),
|
||||
COALESCE(DSCTO, 0),
|
||||
NULLIF(CODIGOEPS, ''),
|
||||
NULLIF(TIPOUSUARIO, ''),
|
||||
NULLIF(TIPOUSUARIOSISPRO, ''),
|
||||
NULLIF(CODCONTRATO, ''),
|
||||
NULLIF(CODTERCERO, ''),
|
||||
NULLIF(CENTROCOSTO, ''),
|
||||
IF(REQAUTORIZA = 'T', 1, 0),
|
||||
IF(ACTIVADA = 'T', 1, 0)
|
||||
FROM stg_empresa
|
||||
", 'lab_empresas');
|
||||
|
||||
// ── 12. EXAMEN_EMP (staging) → lab_examenes_empresa ──────────
|
||||
step('12. EXAMEN_EMP → lab_examenes_empresa');
|
||||
run($pdo, "
|
||||
INSERT IGNORE INTO lab_examenes_empresa
|
||||
(nit_empresa, cod_examen_legacy, exam_tipo_id, codigo_empresa)
|
||||
SELECT
|
||||
ee.NIT_EMP,
|
||||
ee.COD_EXA,
|
||||
et.id,
|
||||
NULLIF(ee.EXA_EMP, '')
|
||||
FROM stg_examen_emp ee
|
||||
LEFT JOIN exam_tipos et ON et.codigo_legacy = ee.COD_EXA
|
||||
", 'lab_examenes_empresa');
|
||||
|
||||
// ── fin ───────────────────────────────────────────────────────
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
||||
$pdo->exec('SET UNIQUE_CHECKS = 1');
|
||||
|
||||
echo "\n" . str_repeat('─', 60) . "\n";
|
||||
echo "ETL completado.\n\n";
|
||||
|
||||
// resumen rápido
|
||||
$tablas = [
|
||||
'lab_secciones', 'lab_protocolos', 'lab_tipos_muestra', 'exam_tipos',
|
||||
'lab_items_resultado', 'lab_perfiles', 'lab_perfil_examenes',
|
||||
'lab_tarifas_id', 'lab_tarifas', 'lab_laboratorios_externos',
|
||||
'lab_empresas', 'lab_examenes_empresa',
|
||||
];
|
||||
echo "Totales en producción:\n";
|
||||
foreach ($tablas as $t) {
|
||||
$n = $pdo->query("SELECT COUNT(*) FROM `$t`")->fetchColumn();
|
||||
printf(" %-35s %7d\n", $t, $n);
|
||||
}
|
||||
echo "\nListo.\n";
|
||||
Reference in New Issue
Block a user