diff --git a/migrations/20260704_lis_01_catalogos_base.sql b/migrations/20260704_lis_01_catalogos_base.sql new file mode 100644 index 0000000..a845138 --- /dev/null +++ b/migrations/20260704_lis_01_catalogos_base.sql @@ -0,0 +1,32 @@ +-- ============================================================= +-- LIS 01 — Catálogos base +-- Crea: lab_secciones, lab_especialidades, lab_tipos_muestra +-- ============================================================= + +CREATE TABLE IF NOT EXISTS lab_secciones ( + codigo VARCHAR(10) NOT NULL, + nombre VARCHAR(100) NOT NULL, + PRIMARY KEY (codigo) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Secciones del laboratorio (Hematología, Química, etc.)'; + +-- ----------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS lab_especialidades ( + codigo VARCHAR(10) NOT NULL, + nombre VARCHAR(100) NOT NULL, + PRIMARY KEY (codigo) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Especialidades médicas — replica ESPECIALIDAD Firebird'; + +-- ----------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS lab_tipos_muestra ( + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(100) NOT NULL, + color_hex VARCHAR(7) DEFAULT NULL COMMENT 'Color del tubo para UI (#RRGGBB)', + requiere_ayuno TINYINT(1) NOT NULL DEFAULT 0, + activo TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (codigo) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Tipos de muestra derivados de EXAMEN.TIPOMUESTRA Firebird'; diff --git a/migrations/20260704_lis_02_protocolos.sql b/migrations/20260704_lis_02_protocolos.sql new file mode 100644 index 0000000..6a4052b --- /dev/null +++ b/migrations/20260704_lis_02_protocolos.sql @@ -0,0 +1,43 @@ +-- ============================================================= +-- LIS 02 — Protocolos e ítems de resultado (catálogos) +-- Crea: lab_protocolos, lab_items_resultado +-- Depende de: lab_secciones (LIS 01) +-- ============================================================= + +CREATE TABLE IF NOT EXISTS lab_protocolos ( + codigo VARCHAR(20) NOT NULL, + nombre VARCHAR(150) NOT NULL, + cod_seccion VARCHAR(10) DEFAULT NULL, + id_planilla VARCHAR(30) DEFAULT NULL COMMENT 'Identificador de plantilla de impresión', + only_show_items TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (codigo), + KEY idx_seccion (cod_seccion), + CONSTRAINT fk_proto_seccion + FOREIGN KEY (cod_seccion) REFERENCES lab_secciones (codigo) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Plantillas de resultado — replica PROTOCOLO Firebird'; + +-- ----------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS lab_items_resultado ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + cod_protocolo VARCHAR(20) NOT NULL, + nombre VARCHAR(150) NOT NULL, + tipo_sexo ENUM('M','F') DEFAULT NULL COMMENT 'NULL = aplica a ambos sexos', + tipo ENUM('N','T') DEFAULT NULL COMMENT 'N=numérico T=texto', + medida VARCHAR(30) DEFAULT NULL, + abreviatura VARCHAR(30) DEFAULT NULL, + vmin_ref DECIMAL(12,4) DEFAULT NULL, + vmax_ref DECIMAL(12,4) DEFAULT NULL, + orden SMALLINT NOT NULL DEFAULT 0, + formula VARCHAR(500) DEFAULT NULL COMMENT 'Fórmula de cálculo automático', + cups_detalle VARCHAR(20) DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_protocolo (cod_protocolo), + KEY idx_orden (cod_protocolo, orden), + CONSTRAINT fk_item_proto + FOREIGN KEY (cod_protocolo) REFERENCES lab_protocolos (codigo) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Analitos por protocolo — replica ITEM Firebird'; diff --git a/migrations/20260704_lis_03_perfiles.sql b/migrations/20260704_lis_03_perfiles.sql new file mode 100644 index 0000000..4de86d3 --- /dev/null +++ b/migrations/20260704_lis_03_perfiles.sql @@ -0,0 +1,26 @@ +-- ============================================================= +-- LIS 03 — Perfiles de examen (paquetes) +-- Crea: lab_perfiles, lab_perfil_examenes +-- Depende de: exam_tipos (migración 003) +-- ============================================================= + +CREATE TABLE IF NOT EXISTS lab_perfiles ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + nombre VARCHAR(150) NOT NULL, + activo TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Paquetes de exámenes — replica PERFIL Firebird'; + +-- ----------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS lab_perfil_examenes ( + perfil_id INT UNSIGNED NOT NULL, + exam_tipo_id INT UNSIGNED NOT NULL, + PRIMARY KEY (perfil_id, exam_tipo_id), + CONSTRAINT fk_pe_perfil FOREIGN KEY (perfil_id) + REFERENCES lab_perfiles (id) ON DELETE CASCADE, + CONSTRAINT fk_pe_exam FOREIGN KEY (exam_tipo_id) + REFERENCES exam_tipos (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Exámenes por perfil — replica PERFIL_EXA Firebird'; diff --git a/migrations/20260704_lis_04_exam_ampliar.sql b/migrations/20260704_lis_04_exam_ampliar.sql new file mode 100644 index 0000000..e0edcf9 --- /dev/null +++ b/migrations/20260704_lis_04_exam_ampliar.sql @@ -0,0 +1,50 @@ +-- ============================================================= +-- LIS 04 — Ampliar exam_tipos y lab_pacientes con campos legacy +-- Depende de: lab_tipos_muestra (LIS 01), lab_protocolos (LIS 02) +-- ============================================================= + +-- ----------------------------------------------------------- +-- exam_tipos: agregar campos del EXAMEN Firebird +-- ----------------------------------------------------------- +ALTER TABLE exam_tipos + ADD COLUMN IF NOT EXISTS codigo_legacy VARCHAR(20) DEFAULT NULL + COMMENT 'Código original Firebird (CODIGO). Usado para ETL y mapeos.', + ADD COLUMN IF NOT EXISTS cups VARCHAR(20) DEFAULT NULL + COMMENT 'Código CUPS colombiano', + ADD COLUMN IF NOT EXISTS cod_protocolo VARCHAR(20) DEFAULT NULL + COMMENT 'FK lab_protocolos.codigo', + ADD COLUMN IF NOT EXISTS tipo_muestra VARCHAR(20) DEFAULT NULL + COMMENT 'FK lab_tipos_muestra.codigo', + ADD COLUMN IF NOT EXISTS nivel TINYINT DEFAULT NULL + COMMENT 'Nivel de complejidad (1/2/3)', + ADD COLUMN IF NOT EXISTS abreviatura VARCHAR(30) DEFAULT NULL, + ADD COLUMN IF NOT EXISTS seremite TINYINT(1) NOT NULL DEFAULT 0 + COMMENT '1 = se remite a laboratorio externo', + ADD COLUMN IF NOT EXISTS serecibe VARCHAR(50) DEFAULT NULL + COMMENT 'Nombre del laboratorio donde se recibe si seremite=1'; + +-- Índice único para buscar por código legacy durante el ETL +ALTER TABLE exam_tipos + ADD UNIQUE KEY IF NOT EXISTS uq_codigo_legacy (codigo_legacy); + +-- FK suave (no FK real para facilitar carga masiva del ETL) +ALTER TABLE exam_tipos + ADD KEY IF NOT EXISTS idx_cod_protocolo (cod_protocolo), + ADD KEY IF NOT EXISTS idx_tipo_muestra (tipo_muestra); + +-- ----------------------------------------------------------- +-- lab_pacientes: agregar campos del PACIENTE Firebird +-- ----------------------------------------------------------- +ALTER TABLE lab_pacientes + ADD COLUMN IF NOT EXISTS codigo_legacy VARCHAR(20) DEFAULT NULL + COMMENT 'CODPAC Firebird', + ADD COLUMN IF NOT EXISTS codetnia VARCHAR(10) DEFAULT NULL + COMMENT 'Código de etnia (RIPS/SISPRO)', + ADD COLUMN IF NOT EXISTS tipores VARCHAR(10) DEFAULT NULL + COMMENT 'Tipo de residencia (RIPS/SISPRO)', + ADD COLUMN IF NOT EXISTS ocupacion VARCHAR(100) DEFAULT NULL, + ADD COLUMN IF NOT EXISTS es_historico TINYINT(1) NOT NULL DEFAULT 0 + COMMENT '1 = migrado solo de Firebird, sin cuenta en nuevo sistema'; + +ALTER TABLE lab_pacientes + ADD KEY IF NOT EXISTS idx_codigo_legacy (codigo_legacy); diff --git a/migrations/20260704_lis_05_tarifas.sql b/migrations/20260704_lis_05_tarifas.sql new file mode 100644 index 0000000..283f438 --- /dev/null +++ b/migrations/20260704_lis_05_tarifas.sql @@ -0,0 +1,97 @@ +-- ============================================================= +-- LIS 05 — Motor de precios y convenios +-- Crea: lab_tarifas_id, lab_tarifas, lab_empresas, +-- lab_empresa_subgrupos, lab_examenes_empresa +-- ============================================================= + +-- ----------------------------------------------------------- +-- Catálogo de tarifas +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_tarifas_id ( + id INT NOT NULL, + nombre VARCHAR(150) NOT NULL, + tarifa_origen INT DEFAULT NULL + COMMENT 'Si != NULL, esta tarifa = tarifa_origen * (1 + porcentaje/100)', + porcentaje DECIMAL(8,4) NOT NULL DEFAULT 0 + COMMENT '0 = precios fijos, >0 = derivada porcentualmente', + PRIMARY KEY (id), + KEY idx_origen (tarifa_origen) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Catálogo de tarifas — replica TARIFAID Firebird'; + +-- ----------------------------------------------------------- +-- Precios por examen y tarifa +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_tarifas ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + cod_examen_legacy VARCHAR(20) NOT NULL COMMENT 'CODIGO original Firebird para trazabilidad', + exam_tipo_id INT UNSIGNED DEFAULT NULL, + tarifa_id INT NOT NULL, + valor DECIMAL(12,2) NOT NULL, + recargo_urg DECIMAL(12,2) NOT NULL DEFAULT 0, + recargo_fes DECIMAL(12,2) NOT NULL DEFAULT 0, + recargo_esp DECIMAL(12,2) NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uq_examen_tarifa (exam_tipo_id, tarifa_id), + KEY idx_legacy (cod_examen_legacy), + KEY idx_tarifa (tarifa_id), + CONSTRAINT fk_tar_tarifa FOREIGN KEY (tarifa_id) + REFERENCES lab_tarifas_id (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Precios por examen y tarifa — replica TARIFA Firebird'; + +-- ----------------------------------------------------------- +-- Empresas / convenios +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_empresas ( + nit VARCHAR(20) NOT NULL, + nombre VARCHAR(200) NOT NULL, + razon_social VARCHAR(200) DEFAULT NULL, + tarifa_id INT DEFAULT NULL, + descuento_pct DECIMAL(8,4) NOT NULL DEFAULT 0, + codigo_eps VARCHAR(20) DEFAULT NULL, + tipo_usuario VARCHAR(10) DEFAULT NULL COMMENT 'Tipo usuario para facturación', + tipo_usuario_sispro VARCHAR(10) DEFAULT NULL, + cod_contrato VARCHAR(50) DEFAULT NULL, + cod_tercero VARCHAR(50) DEFAULT NULL, + centro_costo VARCHAR(50) DEFAULT NULL, + req_autoriza TINYINT(1) NOT NULL DEFAULT 0 + COMMENT '1 = exige número de autorización en recepción', + activa TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (nit), + KEY idx_tarifa (tarifa_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Empresas y convenios — replica EMPRESA Firebird'; + +-- ----------------------------------------------------------- +-- Subgrupos de empresa +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_empresa_subgrupos ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + nit_empresa VARCHAR(20) NOT NULL, + subgrupo VARCHAR(100) NOT NULL, + tarifa_id INT DEFAULT NULL, + ref_subgrupo VARCHAR(50) DEFAULT NULL, + cod_contrato VARCHAR(50) DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uq_empresa_sub (nit_empresa, subgrupo), + KEY idx_tarifa (tarifa_id), + CONSTRAINT fk_esub_empresa FOREIGN KEY (nit_empresa) + REFERENCES lab_empresas (nit) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Subgrupos por empresa — replica EMPRESA_SUB Firebird'; + +-- ----------------------------------------------------------- +-- Códigos de examen alternos por empresa (para interfaces) +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_examenes_empresa ( + nit_empresa VARCHAR(20) NOT NULL, + cod_examen_legacy VARCHAR(20) NOT NULL, + exam_tipo_id INT UNSIGNED DEFAULT NULL, + codigo_empresa VARCHAR(50) NOT NULL, + PRIMARY KEY (nit_empresa, cod_examen_legacy), + KEY idx_exam (exam_tipo_id), + CONSTRAINT fk_ee_empresa FOREIGN KEY (nit_empresa) + REFERENCES lab_empresas (nit) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Códigos alternos por empresa — replica EXAMEN_EMP Firebird'; diff --git a/migrations/20260704_lis_06_historico.sql b/migrations/20260704_lis_06_historico.sql new file mode 100644 index 0000000..22f2e43 --- /dev/null +++ b/migrations/20260704_lis_06_historico.sql @@ -0,0 +1,90 @@ +-- ============================================================= +-- LIS 06 — Histórico transaccional (solo lectura post-migración) +-- Crea: lab_recepciones, lab_relaciones, lab_pagos, lab_pagos_det +-- Depende de: lab_pacientes, medicos, lab_empresas +-- ============================================================= + +-- ----------------------------------------------------------- +-- Recepciones (equivalente a facturas del legacy) +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_recepciones ( + id INT NOT NULL COMMENT 'IDRECEPCION original Firebird', + cod_paciente_legacy VARCHAR(20) DEFAULT NULL, + paciente_id INT DEFAULT NULL, + cod_medico_legacy VARCHAR(20) DEFAULT NULL, + medico_id INT UNSIGNED DEFAULT NULL, + nit_empresa VARCHAR(20) DEFAULT NULL, + subgrupo VARCHAR(100) DEFAULT NULL, + fecha_recepcion DATE NOT NULL, + hora_inicio TIME DEFAULT NULL, + prefijo VARCHAR(5) DEFAULT NULL, + num_factura INT DEFAULT NULL, + valor_total DECIMAL(12,2) NOT NULL DEFAULT 0, + valor_desc DECIMAL(12,2) NOT NULL DEFAULT 0, + diag_ppal VARCHAR(10) DEFAULT NULL COMMENT 'Código diagnóstico CIE-10', + tipo_usuario VARCHAR(10) DEFAULT NULL, + autorizacion VARCHAR(50) DEFAULT NULL, + usuario VARCHAR(50) DEFAULT NULL COMMENT 'Login del operador en Firebird', + es_historico TINYINT(1) NOT NULL DEFAULT 1, + migrado_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_paciente (paciente_id), + KEY idx_fecha (fecha_recepcion), + KEY idx_empresa (nit_empresa), + KEY idx_factura (prefijo, num_factura) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Recepciones históricas — solo lectura, migrado de Firebird'; + +-- ----------------------------------------------------------- +-- Exámenes por recepción +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_relaciones ( + recepcion_id INT NOT NULL, + cod_examen_legacy VARCHAR(20) NOT NULL, + exam_tipo_id INT UNSIGNED DEFAULT NULL, + precio DECIMAL(12,2) NOT NULL DEFAULT 0, + fecha_reportado DATE DEFAULT NULL, + reportado TINYINT(1) NOT NULL DEFAULT 0, + reportado_por VARCHAR(50) DEFAULT NULL, + validado TINYINT(1) NOT NULL DEFAULT 0, + usuario_valida VARCHAR(50) DEFAULT NULL, + fecha_valida DATE DEFAULT NULL, + PRIMARY KEY (recepcion_id, cod_examen_legacy), + KEY idx_exam_tipo (exam_tipo_id), + CONSTRAINT fk_rel_recepcion FOREIGN KEY (recepcion_id) + REFERENCES lab_recepciones (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Exámenes por recepción histórica — replica RELACION Firebird'; + +-- ----------------------------------------------------------- +-- Pagos +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_pagos ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + numcaja_legacy INT NOT NULL UNIQUE COMMENT 'NUMCAJA original Firebird', + recepcion_id INT NOT NULL, + valor DECIMAL(12,2) NOT NULL, + fecha DATE NOT NULL, + usuario VARCHAR(50) DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_recepcion (recepcion_id), + CONSTRAINT fk_pago_recep FOREIGN KEY (recepcion_id) + REFERENCES lab_recepciones (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Pagos históricos — replica PAGOS Firebird'; + +-- ----------------------------------------------------------- +-- Detalle de formas de pago por transacción +-- ----------------------------------------------------------- +CREATE TABLE IF NOT EXISTS lab_pagos_det ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + pago_id INT UNSIGNED NOT NULL, + tipo_pago VARCHAR(30) NOT NULL COMMENT 'efectivo, cheque, tarjeta, etc.', + valor DECIMAL(12,2) NOT NULL, + num_doc VARCHAR(50) DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_pago (pago_id), + CONSTRAINT fk_pagdet_pago FOREIGN KEY (pago_id) + REFERENCES lab_pagos (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Detalle de formas de pago — replica PAGOS_DET Firebird'; diff --git a/migrations/20260704_lis_07_turnero_muestras.sql b/migrations/20260704_lis_07_turnero_muestras.sql new file mode 100644 index 0000000..058db49 --- /dev/null +++ b/migrations/20260704_lis_07_turnero_muestras.sql @@ -0,0 +1,28 @@ +-- ============================================================= +-- LIS 07 — Tracking de muestras por turnero (solicitud) +-- Crea: turnero_muestras +-- Depende de: turnero_solicitudes (003), lab_tipos_muestra (LIS 01) +-- ============================================================= + +CREATE TABLE IF NOT EXISTS turnero_muestras ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + solicitud_id INT UNSIGNED NOT NULL + COMMENT 'FK turnero_solicitudes.id', + tipo_muestra VARCHAR(20) NOT NULL + COMMENT 'FK lab_tipos_muestra.codigo (ej: SANGRE_VENOSA, ORINA)', + estado ENUM('pendiente','recibida','rechazada') + NOT NULL DEFAULT 'pendiente', + motivo_rechazo VARCHAR(200) DEFAULT NULL + COMMENT 'Razón de rechazo (hemólisis, coagulado, volumen insuficiente…)', + recibida_por INT DEFAULT NULL + COMMENT 'FK admin_users.id — quién marcó recibida', + recibida_at DATETIME DEFAULT NULL, + creado_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_solicitud_tipo (solicitud_id, tipo_muestra), + KEY idx_estado (estado), + KEY idx_solicitud (solicitud_id), + CONSTRAINT fk_tm_solicitud FOREIGN KEY (solicitud_id) + REFERENCES turnero_solicitudes (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Estado de muestras físicas por turno — solo lugar toma de muestras'; diff --git a/migrations/20260704_lis_08_vistas.sql b/migrations/20260704_lis_08_vistas.sql new file mode 100644 index 0000000..c7e6b64 --- /dev/null +++ b/migrations/20260704_lis_08_vistas.sql @@ -0,0 +1,186 @@ +-- ============================================================= +-- LIS 08 — Vistas del sistema +-- Todas usan CREATE OR REPLACE para poder re-ejecutar sin error. +-- ============================================================= + +-- ----------------------------------------------------------- +-- v_muestras_hoy +-- Muestras del día en el turnero (estado en tiempo real). +-- Usada por: dashboard, widget lugar.php +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_muestras_hoy AS +SELECT + tm.id, + tm.solicitud_id, + tm.tipo_muestra, + COALESCE(lt.nombre, tm.tipo_muestra) AS tipo_muestra_label, + lt.color_hex AS tipo_muestra_color, + tm.estado, + tm.motivo_rechazo, + tm.recibida_at, + ts.turno_id, + tt.codigo AS turno_codigo, + ts.lugar_id, + tl.nombre AS lugar_nombre, + ts.paciente_id, + lp.nombre_completo AS paciente_nombre, + lp.numero_documento AS paciente_documento, + DATE(tt.creado_at) AS fecha +FROM turnero_muestras tm +JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id +JOIN turnero_turnos tt ON tt.id = ts.turno_id +JOIN turnero_lugares tl ON tl.id = ts.lugar_id +LEFT JOIN lab_pacientes lp ON lp.id = ts.paciente_id +LEFT JOIN lab_tipos_muestra lt ON lt.codigo = tm.tipo_muestra; + +-- ----------------------------------------------------------- +-- v_muestras_pendientes_hoy +-- Solo las que faltan recibir hoy. +-- Usada por: contador en dashboard, alerta visual +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_muestras_pendientes_hoy AS +SELECT * +FROM v_muestras_hoy +WHERE estado = 'pendiente' + AND fecha = CURDATE(); + +-- ----------------------------------------------------------- +-- v_recepcion_completa +-- Histórico Firebird con todas las FK resueltas. +-- Usada por: módulo de consulta histórica (solo lectura) +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_recepcion_completa AS +SELECT + r.id, + r.fecha_recepcion, + r.hora_inicio, + r.prefijo, + r.num_factura, + CONCAT(COALESCE(r.prefijo,''), '-', + LPAD(COALESCE(r.num_factura, 0), 6, '0')) AS factura, + r.valor_total, + r.valor_desc, + (r.valor_total - r.valor_desc) AS valor_neto, + r.paciente_id, + r.cod_paciente_legacy, + COALESCE(lp.nombre_completo, r.cod_paciente_legacy) AS paciente_nombre, + lp.numero_documento AS paciente_documento, + lp.tipo_documento AS paciente_tipo_doc, + r.medico_id, + r.cod_medico_legacy, + CONCAT(COALESCE(m.nombres,''), ' ', COALESCE(m.apellidos,'')) AS medico_nombre, + m.cod_especialidad AS medico_especialidad, + r.nit_empresa, + COALESCE(e.nombre, r.nit_empresa) AS empresa_nombre, + r.subgrupo, + r.diag_ppal, + r.tipo_usuario, + r.autorizacion, + r.usuario +FROM lab_recepciones r +LEFT JOIN lab_pacientes lp ON lp.id = r.paciente_id +LEFT JOIN medicos m ON m.id = r.medico_id +LEFT JOIN lab_empresas e ON e.nit = r.nit_empresa; + +-- ----------------------------------------------------------- +-- v_relacion_completa +-- Exámenes por recepción con nombre resuelto. +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_relacion_completa AS +SELECT + lr.recepcion_id, + r.fecha_recepcion, + r.prefijo, + r.num_factura, + lr.cod_examen_legacy, + lr.exam_tipo_id, + et.nombre AS examen_nombre, + et.categoria AS examen_categoria, + et.tipo_muestra AS tipo_muestra, + lr.precio, + lr.reportado, + lr.fecha_reportado, + lr.reportado_por, + lr.validado, + lr.usuario_valida, + lr.fecha_valida +FROM lab_relaciones lr +JOIN lab_recepciones r ON r.id = lr.recepcion_id +LEFT JOIN exam_tipos et ON et.id = lr.exam_tipo_id; + +-- ----------------------------------------------------------- +-- v_examen_precio +-- Precio efectivo de cada examen en cada tarifa. +-- Resuelve tarifas derivadas por porcentaje. +-- Usada por: motor de precios en nueva recepción +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_examen_precio AS +SELECT + et.id AS exam_tipo_id, + et.codigo, + et.codigo_legacy, + et.nombre AS examen_nombre, + et.categoria, + et.tipo_muestra, + ti.id AS tarifa_id, + ti.nombre AS tarifa_nombre, + ti.porcentaje, + ti.tarifa_origen, + lt.valor AS valor_almacenado, + CASE + WHEN ti.porcentaje > 0 AND ti.tarifa_origen IS NOT NULL + AND lt_base.valor IS NOT NULL + THEN ROUND(lt_base.valor * (1 + ti.porcentaje / 100), 0) + ELSE lt.valor + END AS valor_efectivo, + lt.recargo_urg, + lt.recargo_fes, + lt.recargo_esp +FROM exam_tipos et +JOIN lab_tarifas lt ON lt.exam_tipo_id = et.id +JOIN lab_tarifas_id ti ON ti.id = lt.tarifa_id +LEFT JOIN lab_tarifas lt_base ON lt_base.exam_tipo_id = et.id + AND lt_base.tarifa_id = ti.tarifa_origen; + +-- ----------------------------------------------------------- +-- v_paciente_resumen +-- Vista unificada: pacientes del nuevo sistema + migrados. +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_paciente_resumen AS +SELECT + id, + nombre_completo, + tipo_documento, + numero_documento, + telefono, + email, + fecha_nacimiento, + genero, + ciudad, + eps, + es_historico, + codigo_legacy, + created_at +FROM lab_pacientes +WHERE is_active = 1; + +-- ----------------------------------------------------------- +-- v_turno_muestras_estado +-- Estado agregado de muestras por turno (para cola y dashboard). +-- ----------------------------------------------------------- +CREATE OR REPLACE VIEW v_turno_muestras_estado AS +SELECT + ts.turno_id, + COUNT(*) AS total_muestras, + SUM(tm.estado = 'pendiente') AS pendientes, + SUM(tm.estado = 'recibida') AS recibidas, + SUM(tm.estado = 'rechazada') AS rechazadas, + CASE + WHEN SUM(tm.estado = 'pendiente') = 0 THEN 'completo' + WHEN SUM(tm.estado = 'recibida') = 0 AND SUM(tm.estado = 'rechazada') = 0 + THEN 'sin_recibir' + ELSE 'parcial' + END AS estado_global +FROM turnero_muestras tm +JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id +GROUP BY ts.turno_id; diff --git a/migrations/etl/.gitignore b/migrations/etl/.gitignore new file mode 100644 index 0000000..0aa5ce4 --- /dev/null +++ b/migrations/etl/.gitignore @@ -0,0 +1,2 @@ +config.local.php +etl_*.log diff --git a/migrations/etl/config.php b/migrations/etl/config.php new file mode 100644 index 0000000..52dd2c6 --- /dev/null +++ b/migrations/etl/config.php @@ -0,0 +1,42 @@ + [ + // DSN para PDO: "firebird:dbname=HOST:RUTA_AL_FDB;charset=WIN1252" + // Si el FDB está en la misma máquina: "firebird:dbname=localhost:/opt/firebird/data/DBLAB.FDB" + // Si es una ruta Windows remota: "firebird:dbname=192.168.1.10:C:/datos/DBLAB_XIMENA_FB25.FDB" + 'dsn' => 'firebird:dbname=localhost:/ruta/DBLAB_XIMENA_FB25.FDB;charset=WIN1252', + 'user' => 'SYSDBA', + 'password' => 'masterkey', + // Encoding declarado en el FDB (para convertir a UTF-8 durante la extracción) + 'charset' => 'WIN1252', + ], + + // ── MySQL (destino) ────────────────────────────────────────────────── + 'mysql' => [ + 'host' => '127.0.0.1', + 'port' => 3306, + 'dbname' => 'whatsapp', // nombre de la BD del nuevo sistema + 'user' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + ], + + // ── Opciones de migración ──────────────────────────────────────────── + 'options' => [ + 'batch_size' => 500, // registros por INSERT batch + 'dry_run' => false, // true = solo leer, no insertar + 'skip_historico' => false, // true = saltar RECEPCION/RELACION/PAGOS + 'log_file' => __DIR__ . '/etl_' . date('Ymd_His') . '.log', + ], +]; diff --git a/migrations/etl/helpers.php b/migrations/etl/helpers.php new file mode 100644 index 0000000..96d0385 --- /dev/null +++ b/migrations/etl/helpers.php @@ -0,0 +1,90 @@ + $v) { + if (is_string($v)) { + $row[$k] = toUtf8($v, $srcEncoding); + } + } + return $row; +} + +/** + * Limpia un VARCHAR de Firebird: recorta espacios y convierte encoding. + */ +function cleanStr(?string $val, string $srcEncoding = 'WIN1252'): ?string { + if ($val === null) return null; + $val = trim(toUtf8($val, $srcEncoding) ?? ''); + return $val === '' ? null : $val; +} + +/** + * Convierte fecha Firebird (puede ser DATE o string 'YYYY-MM-DD') a MySQL DATE. + * Devuelve null si la fecha es inválida o vacía. + */ +function fbDate($val): ?string { + if ($val === null) return null; + if ($val instanceof DateTime) return $val->format('Y-m-d'); + $str = trim((string)$val); + if ($str === '' || $str === '0000-00-00') return null; + try { + return (new DateTime($str))->format('Y-m-d'); + } catch (\Exception $e) { + return null; + } +} + +/** + * INSERT en batch. + * $pdo → conexión MySQL + * $table → nombre de la tabla + * $rows → array de arrays asociativos con los mismos keys + * $ignore → usa INSERT IGNORE para saltar duplicados + */ +function batchInsert(PDO $pdo, string $table, array $rows, bool $ignore = false): int { + if (empty($rows)) return 0; + + $cols = array_keys($rows[0]); + $colList = implode(', ', array_map(fn($c) => "`$c`", $cols)); + $phRow = '(' . implode(', ', array_fill(0, count($cols), '?')) . ')'; + + $inserted = 0; + foreach (array_chunk($rows, 500) as $chunk) { + $placeholders = implode(', ', array_fill(0, count($chunk), $phRow)); + $keyword = $ignore ? 'INSERT IGNORE' : 'INSERT'; + $sql = "$keyword INTO `$table` ($colList) VALUES $placeholders"; + $flat = array_merge(...array_map('array_values', $chunk)); + $stmt = $pdo->prepare($sql); + $stmt->execute($flat); + $inserted += $stmt->rowCount(); + } + return $inserted; +} + +/** Escribe al log y a stdout simultáneamente. */ +function etlLog(string $msg, $logFp = null): void { + $line = '[' . date('H:i:s') . '] ' . $msg . PHP_EOL; + echo $line; + if ($logFp) fwrite($logFp, $line); +} + +/** Devuelve conteo de filas de una tabla Firebird. */ +function fbCount(PDO $fb, string $table): int { + return (int) $fb->query("SELECT COUNT(*) FROM $table")->fetchColumn(); +} diff --git a/migrations/etl/run_etl.php b/migrations/etl/run_etl.php new file mode 100644 index 0000000..7aa94dc --- /dev/null +++ b/migrations/etl/run_etl.php @@ -0,0 +1,715 @@ +#!/usr/bin/env php + PDO::ERRMODE_EXCEPTION] + ); + etlLog('Conexión Firebird OK', $logFp); +} catch (\Throwable $e) { + etlLog('ERROR conectando Firebird: ' . $e->getMessage(), $logFp); + exit(1); +} + +try { + $my = new PDO( + "mysql:host={$cfg['mysql']['host']};port={$cfg['mysql']['port']};dbname={$cfg['mysql']['dbname']};charset=utf8mb4", + $cfg['mysql']['user'], + $cfg['mysql']['password'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => true] + ); + $my->exec("SET NAMES utf8mb4"); + $my->exec("SET foreign_key_checks = 0"); + etlLog('Conexión MySQL OK', $logFp); +} catch (\Throwable $e) { + etlLog('ERROR conectando MySQL: ' . $e->getMessage(), $logFp); + exit(1); +} + +$charset = $cfg['firebird']['charset']; + +// ── Función auxiliar: ¿ejecutar este paso? ────────────────────────────────── +function shouldRun(string $paso, ?string $only): bool { + return $only === null || $only === $paso; +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 1 — SECCION → lab_secciones +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('secciones', $onlyPaso)) { + etlLog('--- SECCION → lab_secciones ---', $logFp); + $total = fbCount($fb, 'SECCION'); + etlLog(" Origen: $total registros", $logFp); + + $rows = []; + foreach ($fb->query('SELECT CODSECCION, NOMBSECCION FROM SECCION') as $r) { + $rows[] = [ + 'codigo' => cleanStr($r['CODSECCION'], $charset), + 'nombre' => cleanStr($r['NOMBSECCION'], $charset), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_secciones', $rows, true); + etlLog(" Insertados: $n", $logFp); + } else { + etlLog(" [DRY] Se insertarían " . count($rows), $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 2 — ESPECIALIDAD → lab_especialidades +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('especialidades', $onlyPaso)) { + etlLog('--- ESPECIALIDAD → lab_especialidades ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT CODESPECIA, NOMBESPECIA FROM ESPECIALIDAD') as $r) { + $rows[] = [ + 'codigo' => cleanStr($r['CODESPECIA'], $charset), + 'nombre' => cleanStr($r['NOMBESPECIA'], $charset), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_especialidades', $rows, true); + etlLog(" Insertados: $n / " . count($rows), $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 3 — EXAMEN.TIPOMUESTRA → lab_tipos_muestra +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('tipos_muestra', $onlyPaso)) { + etlLog('--- EXAMEN.TIPOMUESTRA → lab_tipos_muestra ---', $logFp); + $rows = []; + $seen = []; + foreach ($fb->query('SELECT DISTINCT TIPOMUESTRA FROM EXAMEN WHERE TIPOMUESTRA IS NOT NULL') as $r) { + $cod = cleanStr($r['TIPOMUESTRA'], $charset); + if (!$cod || isset($seen[$cod])) continue; + $seen[$cod] = true; + $rows[] = [ + 'codigo' => $cod, + 'nombre' => ucwords(strtolower($cod)), // nombre provisional, editar luego + ]; + } + etlLog(" Tipos únicos encontrados: " . count($rows), $logFp); + if (!$dryRun) { + $n = batchInsert($my, 'lab_tipos_muestra', $rows, true); + etlLog(" Insertados: $n", $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 4 — TARIFAID → lab_tarifas_id +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('tarifas', $onlyPaso)) { + etlLog('--- TARIFAID → lab_tarifas_id ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT IDTARIFA, NOMTARIFA, IDTARIFABASE, PORCENTAJE FROM TARIFAID') as $r) { + $rows[] = [ + 'id' => (int)$r['IDTARIFA'], + 'nombre' => cleanStr($r['NOMTARIFA'], $charset), + 'tarifa_origen' => $r['IDTARIFABASE'] ? (int)$r['IDTARIFABASE'] : null, + 'porcentaje' => (float)($r['PORCENTAJE'] ?? 0), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_tarifas_id', $rows, true); + etlLog(" Insertados: $n / " . count($rows), $logFp); + } + + // ── TARIFA → lab_tarifas (104k registros, se hace en batches) ──────── + etlLog('--- TARIFA → lab_tarifas ---', $logFp); + $total = fbCount($fb, 'TARIFA'); + etlLog(" Origen: $total registros", $logFp); + + $stmt = $fb->query('SELECT CODIGO, IDTARIFA, VALOR, RECARGO_URG, RECARGO_FES, RECARGO_ESP FROM TARIFA'); + $batch = []; + $count = 0; + + while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { + $batch[] = [ + 'cod_examen_legacy' => cleanStr($r['CODIGO'], $charset), + 'exam_tipo_id' => null, // se resuelve en paso 6 (exam_tipos) + 'tarifa_id' => (int)$r['IDTARIFA'], + 'valor' => (float)($r['VALOR'] ?? 0), + 'recargo_urg' => (float)($r['RECARGO_URG'] ?? 0), + 'recargo_fes' => (float)($r['RECARGO_FES'] ?? 0), + 'recargo_esp' => (float)($r['RECARGO_ESP'] ?? 0), + ]; + if (count($batch) >= 500) { + if (!$dryRun) $count += batchInsert($my, 'lab_tarifas', $batch, true); + $batch = []; + } + } + if ($batch && !$dryRun) $count += batchInsert($my, 'lab_tarifas', $batch, true); + etlLog(" Insertados: $count", $logFp); +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 5 — PROTOCOLO + ITEM → lab_protocolos + lab_items_resultado +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('protocolos', $onlyPaso)) { + etlLog('--- PROTOCOLO → lab_protocolos ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT CODPROTOCOLO, NOMPROTOCOLO, CODSECCION, IDPLANILLA, ONLYITEMS FROM PROTOCOLO') as $r) { + $rows[] = [ + 'codigo' => cleanStr($r['CODPROTOCOLO'], $charset), + 'nombre' => cleanStr($r['NOMPROTOCOLO'], $charset), + 'cod_seccion' => cleanStr($r['CODSECCION'], $charset), + 'id_planilla' => cleanStr($r['IDPLANILLA'], $charset), + 'only_show_items' => $r['ONLYITEMS'] ? 1 : 0, + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_protocolos', $rows, true); + etlLog(" Protocolos insertados: $n / " . count($rows), $logFp); + } +} + +if (shouldRun('items', $onlyPaso)) { + etlLog('--- ITEM → lab_items_resultado ---', $logFp); + $total = fbCount($fb, 'ITEM'); + etlLog(" Origen: $total registros", $logFp); + + $stmt = $fb->query('SELECT CODPROTOCOLO,NOMITEM,TIPOSEXO,TIPO,MEDIDA,ABREVITEM,VMINREF,VMAXREF,ORDEN,FORMULA,CUPS FROM ITEM ORDER BY CODPROTOCOLO, ORDEN'); + $batch = []; + $count = 0; + + while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { + $batch[] = [ + 'cod_protocolo' => cleanStr($r['CODPROTOCOLO'], $charset), + 'nombre' => cleanStr($r['NOMITEM'], $charset), + 'tipo_sexo' => cleanStr($r['TIPOSEXO'], $charset), + 'tipo' => cleanStr($r['TIPO'], $charset), + 'medida' => cleanStr($r['MEDIDA'], $charset), + 'abreviatura' => cleanStr($r['ABREVITEM'], $charset), + 'vmin_ref' => is_numeric($r['VMINREF']) ? (float)$r['VMINREF'] : null, + 'vmax_ref' => is_numeric($r['VMAXREF']) ? (float)$r['VMAXREF'] : null, + 'orden' => (int)($r['ORDEN'] ?? 0), + 'formula' => cleanStr($r['FORMULA'], $charset), + 'cups_detalle' => cleanStr($r['CUPS'], $charset), + ]; + if (count($batch) >= 500) { + if (!$dryRun) $count += batchInsert($my, 'lab_items_resultado', $batch, true); + $batch = []; + } + } + if ($batch && !$dryRun) $count += batchInsert($my, 'lab_items_resultado', $batch, true); + etlLog(" Insertados: $count", $logFp); +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 6 — PERFIL + PERFIL_EXA → lab_perfiles + lab_perfil_examenes +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('perfiles', $onlyPaso)) { + etlLog('--- PERFIL → lab_perfiles ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT CODPERFIL, NOMPERFIL FROM PERFIL') as $r) { + $rows[] = [ + 'nombre' => cleanStr($r['NOMPERFIL'], $charset), + ]; + } + // Firebird usa VARCHAR código; MySQL usa INT AUTO_INCREMENT + // Guardamos el código viejo → id nuevo en memoria para PERFIL_EXA + if (!$dryRun) { + // Insertar uno a uno para mapear código → id nuevo + $mapaPerfiles = []; + foreach ($fb->query('SELECT CODPERFIL, NOMPERFIL FROM PERFIL') as $r) { + $stmt2 = $my->prepare('INSERT IGNORE INTO lab_perfiles (nombre) VALUES (?)'); + $stmt2->execute([cleanStr($r['NOMPERFIL'], $charset)]); + $newId = (int)$my->lastInsertId(); + if ($newId) $mapaPerfiles[cleanStr($r['CODPERFIL'], $charset)] = $newId; + } + etlLog(" Perfiles insertados: " . count($mapaPerfiles), $logFp); + + // PERFIL_EXA — mapear cod_examen → exam_tipo_id + etlLog('--- PERFIL_EXA → lab_perfil_examenes ---', $logFp); + $examMap = []; + foreach ($my->query('SELECT id, codigo_legacy FROM exam_tipos WHERE codigo_legacy IS NOT NULL') as $r) { + $examMap[$r['codigo_legacy']] = (int)$r['id']; + } + + $peBatch = []; + foreach ($fb->query('SELECT CODPERFIL, CODEXAMEN FROM PERFIL_EXA') as $r) { + $pId = $mapaPerfiles[cleanStr($r['CODPERFIL'], $charset)] ?? null; + $eId = $examMap[cleanStr($r['CODEXAMEN'], $charset)] ?? null; + if ($pId && $eId) { + $peBatch[] = ['perfil_id' => $pId, 'exam_tipo_id' => $eId]; + } + } + $n = batchInsert($my, 'lab_perfil_examenes', $peBatch, true); + etlLog(" Relaciones perfil-examen: $n", $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 7 — EXAMEN → exam_tipos (ampliar con campos legacy) +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('exam_tipos', $onlyPaso)) { + etlLog('--- EXAMEN → exam_tipos ---', $logFp); + $total = fbCount($fb, 'EXAMEN'); + etlLog(" Origen: $total registros", $logFp); + + $insertados = 0; + $actualizados = 0; + + $stmt = $fb->query( + 'SELECT CODIGO, NOMEXAMEN, CODPROT, TIPOMUESTRA, NIVEL, CUPS, + ABREVEXAMEN, SEREMITE, SERECIBE, CODSECCION + FROM EXAMEN' + ); + + $checkStmt = $my->prepare('SELECT id FROM exam_tipos WHERE codigo_legacy = ?'); + $updStmt = $my->prepare( + 'UPDATE exam_tipos SET + cod_protocolo = ?, tipo_muestra = ?, nivel = ?, cups = ?, + abreviatura = ?, seremite = ?, serecibe = ? + WHERE codigo_legacy = ?' + ); + $insStmt = $my->prepare( + 'INSERT IGNORE INTO exam_tipos + (codigo, nombre, categoria, codigo_legacy, cod_protocolo, + tipo_muestra, nivel, cups, abreviatura, seremite, serecibe) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ); + + while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { + $codLeg = cleanStr($r['CODIGO'], $charset); + $nombre = cleanStr($r['NOMEXAMEN'], $charset); + $proto = cleanStr($r['CODPROT'], $charset); + $tMuest = cleanStr($r['TIPOMUESTRA'], $charset); + $nivel = $r['NIVEL'] ? (int)$r['NIVEL'] : null; + $cups = cleanStr($r['CUPS'], $charset); + $abrev = cleanStr($r['ABREVEXAMEN'], $charset); + $serem = $r['SEREMITE'] ? 1 : 0; + $serec = cleanStr($r['SERECIBE'], $charset); + $seccion = cleanStr($r['CODSECCION'], $charset); + + if (!$codLeg || !$nombre) continue; + + $checkStmt->execute([$codLeg]); + $existing = $checkStmt->fetchColumn(); + + if ($dryRun) continue; + + if ($existing) { + $updStmt->execute([$proto, $tMuest, $nivel, $cups, $abrev, $serem, $serec, $codLeg]); + $actualizados++; + } else { + // Usar el código Firebird como código del nuevo sistema (si no hay conflicto) + $insStmt->execute([$codLeg, $nombre, $seccion, $codLeg, $proto, $tMuest, $nivel, $cups, $abrev, $serem, $serec]); + $insertados++; + } + } + + etlLog(" Nuevos: $insertados | Actualizados: $actualizados", $logFp); + + // Resolver exam_tipo_id en lab_tarifas (ahora que los exámenes ya están) + if (!$dryRun) { + etlLog(' Resolviendo exam_tipo_id en lab_tarifas...', $logFp); + $updated = $my->exec( + 'UPDATE lab_tarifas t + JOIN exam_tipos e ON e.codigo_legacy = t.cod_examen_legacy + SET t.exam_tipo_id = e.id + WHERE t.exam_tipo_id IS NULL' + ); + etlLog(" lab_tarifas actualizadas: $updated filas", $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 8 — EMPRESA + EMPRESA_SUB + EXAMEN_EMP +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('empresas', $onlyPaso)) { + etlLog('--- EMPRESA → lab_empresas ---', $logFp); + $rows = []; + foreach ($fb->query( + 'SELECT NIT,NOMBRE,RAZSOCIAL,IDTARIFA,DESCUENTO,CODEEPS, + TIPOUSUARIO,TIPOUSUARIOSISPRO,CODCONTRATO,CODTERCERO, + CENTROCOSTO,REQAUTORIZA,ACTIVA + FROM EMPRESA' + ) as $r) { + $rows[] = [ + 'nit' => cleanStr($r['NIT'], $charset), + 'nombre' => cleanStr($r['NOMBRE'], $charset), + 'razon_social' => cleanStr($r['RAZSOCIAL'], $charset), + 'tarifa_id' => $r['IDTARIFA'] ? (int)$r['IDTARIFA'] : null, + 'descuento_pct' => (float)($r['DESCUENTO'] ?? 0), + 'codigo_eps' => cleanStr($r['CODEEPS'], $charset), + 'tipo_usuario' => cleanStr($r['TIPOUSUARIO'], $charset), + 'tipo_usuario_sispro' => cleanStr($r['TIPOUSUARIOSISPRO'],$charset), + 'cod_contrato' => cleanStr($r['CODCONTRATO'], $charset), + 'cod_tercero' => cleanStr($r['CODTERCERO'], $charset), + 'centro_costo' => cleanStr($r['CENTROCOSTO'], $charset), + 'req_autoriza' => $r['REQAUTORIZA'] ? 1 : 0, + 'activa' => $r['ACTIVA'] ? 1 : 0, + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_empresas', $rows, true); + etlLog(" Empresas: $n / " . count($rows), $logFp); + } + + etlLog('--- EMPRESA_SUB → lab_empresa_subgrupos ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT NIT,SUBGRUPO,IDTARIFA,REF_SUBGRUPO,CODCONTRATO FROM EMPRESA_SUB') as $r) { + $rows[] = [ + 'nit_empresa' => cleanStr($r['NIT'], $charset), + 'subgrupo' => cleanStr($r['SUBGRUPO'], $charset), + 'tarifa_id' => $r['IDTARIFA'] ? (int)$r['IDTARIFA'] : null, + 'ref_subgrupo' => cleanStr($r['REF_SUBGRUPO'],$charset), + 'cod_contrato' => cleanStr($r['CODCONTRATO'], $charset), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_empresa_subgrupos', $rows, true); + etlLog(" Subgrupos: $n", $logFp); + } + + etlLog('--- EXAMEN_EMP → lab_examenes_empresa ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT NIT,CODIGO,CODIGOEMP FROM EXAMEN_EMP') as $r) { + $rows[] = [ + 'nit_empresa' => cleanStr($r['NIT'], $charset), + 'cod_examen_legacy' => cleanStr($r['CODIGO'], $charset), + 'exam_tipo_id' => null, + 'codigo_empresa' => cleanStr($r['CODIGOEMP'],$charset), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_examenes_empresa', $rows, true); + // Resolver exam_tipo_id + $my->exec( + 'UPDATE lab_examenes_empresa ee + JOIN exam_tipos e ON e.codigo_legacy = ee.cod_examen_legacy + SET ee.exam_tipo_id = e.id + WHERE ee.exam_tipo_id IS NULL' + ); + etlLog(" Examenes-empresa: $n", $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 9 — MEDICO → medicos +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('medicos', $onlyPaso)) { + etlLog('--- MEDICO → medicos ---', $logFp); + $rows = []; + foreach ($fb->query( + 'SELECT CODMEDICO,NOMBRES,APELLIDOS,CODESPECIA,TELEFONO1,EMAIL,DOCIDMEDICO,ACTIVO + FROM MEDICO' + ) as $r) { + $rows[] = [ + 'codigo' => cleanStr($r['CODMEDICO'], $charset), + 'nombres' => cleanStr($r['NOMBRES'], $charset), + 'apellidos' => cleanStr($r['APELLIDOS'], $charset), + 'cod_especialidad'=> cleanStr($r['CODESPECIA'], $charset), + 'telefonos' => cleanStr($r['TELEFONO1'], $charset), + 'email' => cleanStr($r['EMAIL'], $charset), + 'docidmedico' => cleanStr($r['DOCIDMEDICO'], $charset), + 'activo' => $r['ACTIVO'] ? 1 : 0, + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'medicos', $rows, true); + etlLog(" Médicos: $n / " . count($rows), $logFp); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 10 — PACIENTE → lab_pacientes +// ════════════════════════════════════════════════════════════════════════════ +if (shouldRun('pacientes', $onlyPaso)) { + etlLog('--- PACIENTE → lab_pacientes ---', $logFp); + $total = fbCount($fb, 'PACIENTE'); + etlLog(" Origen: $total registros", $logFp); + + $stmt = $fb->query( + 'SELECT CODPAC,DOCIDENT,TIPOIDENT,NOMBRES,APELLIDOS, + TELEFONO1,EMAIL,FECHANAC,SEXO,CIUDAD, + OCUPACION,CODETNIA,TIPORES + FROM PACIENTE + ORDER BY CODPAC' + ); + + $checkStmt = $my->prepare('SELECT id FROM lab_pacientes WHERE numero_documento = ?'); + $insStmt = $my->prepare( + 'INSERT IGNORE INTO lab_pacientes + (numero_documento, tipo_documento, nombre_completo, telefono, + email, fecha_nacimiento, genero, ciudad, + ocupacion, codetnia, tipores, codigo_legacy, es_historico) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1)' + ); + $updLegacy = $my->prepare( + 'UPDATE lab_pacientes SET codigo_legacy = ? WHERE numero_documento = ? AND codigo_legacy IS NULL' + ); + + $nuevos = $coincidentes = 0; + + while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { + $doc = cleanStr($r['DOCIDENT'], $charset); + if (!$doc) continue; + + $nombres = cleanStr($r['NOMBRES'], $charset); + $apellidos = cleanStr($r['APELLIDOS'], $charset); + $nombre = trim("$nombres $apellidos"); + + if ($dryRun) continue; + + $checkStmt->execute([$doc]); + $existeId = $checkStmt->fetchColumn(); + + if ($existeId) { + // Ya existe → solo actualizar codigo_legacy si falta + $updLegacy->execute([cleanStr($r['CODPAC'], $charset), $doc]); + $coincidentes++; + } else { + $tipoDoc = match(strtoupper(cleanStr($r['TIPOIDENT'], $charset) ?? '')) { + 'CC' => 'CC', + 'CE' => 'CE', + 'TI' => 'TI', + 'PA' => 'PA', + 'NIT' => 'NIT', + 'RC' => 'RC', + 'MS' => 'MS', + default => 'CC', + }; + $genero = match(strtoupper(cleanStr($r['SEXO'], $charset) ?? '')) { + 'M' => 'M', 'F' => 'F', default => null + }; + $insStmt->execute([ + $doc, + $tipoDoc, + $nombre ?: 'Sin nombre', + cleanStr($r['TELEFONO1'], $charset), + cleanStr($r['EMAIL'], $charset), + fbDate($r['FECHANAC']), + $genero, + cleanStr($r['CIUDAD'], $charset), + cleanStr($r['OCUPACION'], $charset), + cleanStr($r['CODETNIA'], $charset), + cleanStr($r['TIPORES'], $charset), + cleanStr($r['CODPAC'], $charset), + ]); + $nuevos++; + } + } + + etlLog(" Nuevos: $nuevos | Coincidentes (codigo_legacy actualizado): $coincidentes", $logFp); +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASO 11-13 — Histórico transaccional (RECEPCION / RELACION / PAGOS) +// ════════════════════════════════════════════════════════════════════════════ +if (!$skipHist) { + + // ── 11. RECEPCION → lab_recepciones ────────────────────────────────── + if (shouldRun('recepciones', $onlyPaso)) { + etlLog('--- RECEPCION → lab_recepciones ---', $logFp); + $total = fbCount($fb, 'RECEPCION'); + etlLog(" Origen: $total registros", $logFp); + + // Mapa paciente legacy → id nuevo + $pacMap = []; + foreach ($my->query('SELECT id, codigo_legacy FROM lab_pacientes WHERE codigo_legacy IS NOT NULL') as $r) { + $pacMap[$r['codigo_legacy']] = (int)$r['id']; + } + // Mapa médico legacy → id nuevo + $medMap = []; + foreach ($my->query('SELECT id, codigo FROM medicos WHERE codigo IS NOT NULL') as $r) { + $medMap[$r['codigo']] = (int)$r['id']; + } + + $stmt = $fb->query( + 'SELECT IDRECEPCION,CODPAC,CODMEDICO,NIT,SUBGRUPO, + FECHA,HORAINICIO,PREFIJO,NUMFACTURA, + VALORTOTAL,VALORDESC,DIAGPPAL,TIPOUSUARIO,AUTORIZACION,USUARIO + FROM RECEPCION + ORDER BY IDRECEPCION' + ); + $batch = []; + $count = 0; + + while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { + $codPac = cleanStr($r['CODPAC'], $charset); + $codMed = cleanStr($r['CODMEDICO'],$charset); + + $batch[] = [ + 'id' => (int)$r['IDRECEPCION'], + 'cod_paciente_legacy' => $codPac, + 'paciente_id' => $pacMap[$codPac] ?? null, + 'cod_medico_legacy' => $codMed, + 'medico_id' => $medMap[$codMed] ?? null, + 'nit_empresa' => cleanStr($r['NIT'], $charset), + 'subgrupo' => cleanStr($r['SUBGRUPO'], $charset), + 'fecha_recepcion' => fbDate($r['FECHA']), + 'hora_inicio' => cleanStr($r['HORAINICIO'], $charset), + 'prefijo' => cleanStr($r['PREFIJO'], $charset), + 'num_factura' => $r['NUMFACTURA'] ? (int)$r['NUMFACTURA'] : null, + 'valor_total' => (float)($r['VALORTOTAL'] ?? 0), + 'valor_desc' => (float)($r['VALORDESC'] ?? 0), + 'diag_ppal' => cleanStr($r['DIAGPPAL'], $charset), + 'tipo_usuario' => cleanStr($r['TIPOUSUARIO'], $charset), + 'autorizacion' => cleanStr($r['AUTORIZACION'], $charset), + 'usuario' => cleanStr($r['USUARIO'], $charset), + ]; + + if (count($batch) >= 500) { + if (!$dryRun) $count += batchInsert($my, 'lab_recepciones', $batch, true); + $batch = []; + if ($count % 5000 === 0) etlLog(" ... $count procesadas", $logFp); + } + } + if ($batch && !$dryRun) $count += batchInsert($my, 'lab_recepciones', $batch, true); + etlLog(" Insertadas: $count", $logFp); + } + + // ── 12. RELACION → lab_relaciones ───────────────────────────────────── + if (shouldRun('relaciones', $onlyPaso)) { + etlLog('--- RELACION → lab_relaciones ---', $logFp); + $total = fbCount($fb, 'RELACION'); + etlLog(" Origen: $total registros", $logFp); + + // Mapa cod_examen_legacy → exam_tipo_id + $examMap = []; + foreach ($my->query('SELECT id, codigo_legacy FROM exam_tipos WHERE codigo_legacy IS NOT NULL') as $r) { + $examMap[$r['codigo_legacy']] = (int)$r['id']; + } + + $stmt = $fb->query( + 'SELECT IDRECEPCION,CODIGO,PRECIO,FECHAREPORT,REPORTADO,REPORPOR,VALIDADO,USRVALIDA,FECHAVALIDA + FROM RELACION' + ); + $batch = []; + $count = 0; + + while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { + $codExam = cleanStr($r['CODIGO'], $charset); + $batch[] = [ + 'recepcion_id' => (int)$r['IDRECEPCION'], + 'cod_examen_legacy' => $codExam, + 'exam_tipo_id' => $examMap[$codExam] ?? null, + 'precio' => (float)($r['PRECIO'] ?? 0), + 'fecha_reportado' => fbDate($r['FECHAREPORT']), + 'reportado' => $r['REPORTADO'] ? 1 : 0, + 'reportado_por' => cleanStr($r['REPORPOR'], $charset), + 'validado' => $r['VALIDADO'] ? 1 : 0, + 'usuario_valida' => cleanStr($r['USRVALIDA'], $charset), + 'fecha_valida' => fbDate($r['FECHAVALIDA']), + ]; + + if (count($batch) >= 500) { + if (!$dryRun) $count += batchInsert($my, 'lab_relaciones', $batch, true); + $batch = []; + if ($count % 10000 === 0) etlLog(" ... $count", $logFp); + } + } + if ($batch && !$dryRun) $count += batchInsert($my, 'lab_relaciones', $batch, true); + etlLog(" Insertadas: $count", $logFp); + } + + // ── 13. PAGOS + PAGOS_DET ───────────────────────────────────────────── + if (shouldRun('pagos', $onlyPaso)) { + etlLog('--- PAGOS → lab_pagos ---', $logFp); + $rows = []; + foreach ($fb->query('SELECT NUMCAJA,IDRECEPCION,VALOR,FECHAPAGO,USUARIO FROM PAGOS') as $r) { + $rows[] = [ + 'numcaja_legacy' => (int)$r['NUMCAJA'], + 'recepcion_id' => (int)$r['IDRECEPCION'], + 'valor' => (float)($r['VALOR'] ?? 0), + 'fecha' => fbDate($r['FECHAPAGO']), + 'usuario' => cleanStr($r['USUARIO'], $charset), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_pagos', $rows, true); + etlLog(" Pagos: $n / " . count($rows), $logFp); + } + + etlLog('--- PAGOS_DET → lab_pagos_det ---', $logFp); + // Mapa numcaja → id nuevo + $pagoMap = []; + if (!$dryRun) { + foreach ($my->query('SELECT id, numcaja_legacy FROM lab_pagos') as $r) { + $pagoMap[$r['numcaja_legacy']] = (int)$r['id']; + } + } + $rows = []; + foreach ($fb->query('SELECT NUMCAJA,TIPOPAGO,VALOR,NUMDOC FROM PAGOS_DET') as $r) { + $pagoId = $pagoMap[(int)$r['NUMCAJA']] ?? null; + if (!$pagoId) continue; + $rows[] = [ + 'pago_id' => $pagoId, + 'tipo_pago' => cleanStr($r['TIPOPAGO'], $charset), + 'valor' => (float)($r['VALOR'] ?? 0), + 'num_doc' => cleanStr($r['NUMDOC'], $charset), + ]; + } + if (!$dryRun) { + $n = batchInsert($my, 'lab_pagos_det', $rows, true); + etlLog(" Detalles de pago: $n", $logFp); + } + } + +} else { + etlLog('--- Histórico omitido (--skip-historico) ---', $logFp); +} + +// ── Restaurar FK checks ────────────────────────────────────────────────────── +if (!$dryRun) { + $my->exec("SET foreign_key_checks = 1"); +} + +etlLog('=== ETL COMPLETADO ===', $logFp); +fclose($logFp); +echo "Log guardado en: {$cfg['options']['log_file']}\n"; diff --git a/modules/turnero/api/create_solicitud.php b/modules/turnero/api/create_solicitud.php index acc008d..1226ab8 100644 --- a/modules/turnero/api/create_solicitud.php +++ b/modules/turnero/api/create_solicitud.php @@ -166,6 +166,30 @@ try { } unset($c); + // ── Auto-crear muestras por tipo_muestra de los exámenes ── + try { + if (!$soloMuestras && !empty($examIds)) { + $ph = implode(',', array_fill(0, count($examIds), '?')); + $stmtTm = $pdo->prepare( + "SELECT DISTINCT COALESCE(NULLIF(TRIM(tipo_muestra),''), codigo) AS tipo_key + FROM exam_tipos WHERE id IN ($ph) AND activo = 1" + ); + $stmtTm->execute($examIds); + $stmtIm = $pdo->prepare( + "INSERT IGNORE INTO turnero_muestras (solicitud_id, tipo_muestra) VALUES (?, ?)" + ); + foreach ($stmtTm->fetchAll(PDO::FETCH_COLUMN) as $tipoKey) { + $stmtIm->execute([$solicitudId, $tipoKey]); + } + } elseif ($soloMuestras) { + $pdo->prepare( + "INSERT IGNORE INTO turnero_muestras (solicitud_id, tipo_muestra) VALUES (?, 'MUESTRA')" + )->execute([$solicitudId]); + } + } catch (\Throwable $_) { + // Tabla aún no existe (migración pendiente) — continuar sin muestras + } + $pdo->commit(); notificarSSE(obtenerOCrearSesionHoy()); diff --git a/modules/turnero/api/get_consentimientos.php b/modules/turnero/api/get_consentimientos.php index 86950a3..ef86664 100644 --- a/modules/turnero/api/get_consentimientos.php +++ b/modules/turnero/api/get_consentimientos.php @@ -217,9 +217,29 @@ if ($incluirSolicitud) { } } + // Muestras pendientes / recibidas del turno + $muestras = []; + if ($solicitud) { + try { + $stmtM = $pdo->prepare( + "SELECT tm.id, tm.tipo_muestra, tm.estado, tm.motivo_rechazo, tm.recibida_at, + COALESCE(lt.nombre, tm.tipo_muestra) AS label + FROM turnero_muestras tm + LEFT JOIN lab_tipos_muestra lt ON lt.codigo = tm.tipo_muestra + WHERE tm.solicitud_id = ? + ORDER BY tm.id ASC" + ); + $stmtM->execute([$solicitud['id']]); + $muestras = $stmtM->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable $_) { + // Tabla aún no existe + } + } + $respuesta['solicitud'] = $solicitud; $respuesta['paciente'] = $paciente; $respuesta['examenes'] = $examenes; + $respuesta['muestras'] = $muestras; } jsonOk($respuesta); diff --git a/modules/turnero/api/update_muestra_estado.php b/modules/turnero/api/update_muestra_estado.php new file mode 100644 index 0000000..6d4509d --- /dev/null +++ b/modules/turnero/api/update_muestra_estado.php @@ -0,0 +1,69 @@ +prepare( + "SELECT tm.id, tm.solicitud_id, tm.estado + FROM turnero_muestras tm + WHERE tm.id = ?" +); +$stmt->execute([$muestraId]); +$muestra = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$muestra) jsonError('Muestra no encontrada.', 404); + +// Construir campos a actualizar +$ahora = date('Y-m-d H:i:s'); +$adminI = adminId(); + +if ($estado === 'recibida') { + $pdo->prepare( + "UPDATE turnero_muestras + SET estado = 'recibida', recibida_por = ?, recibida_at = ?, + motivo_rechazo = NULL + WHERE id = ?" + )->execute([$adminI, $ahora, $muestraId]); +} elseif ($estado === 'rechazada') { + $pdo->prepare( + "UPDATE turnero_muestras + SET estado = 'rechazada', recibida_por = ?, recibida_at = ?, + motivo_rechazo = ? + WHERE id = ?" + )->execute([$adminI, $ahora, $motivo, $muestraId]); +} else { + // Revertir a pendiente + $pdo->prepare( + "UPDATE turnero_muestras + SET estado = 'pendiente', recibida_por = NULL, + recibida_at = NULL, motivo_rechazo = NULL + WHERE id = ?" + )->execute([$muestraId]); +} + +jsonOk(['muestra_id' => $muestraId, 'estado' => $estado], 'Muestra actualizada'); diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index 6173c4a..0c0758f 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -12,7 +12,7 @@ if (!isUserLoggedIn()) { try { $pdo = Database::getInstance()->getConnection(); $lugares = $pdo->query( - "SELECT id, nombre, descripcion, formulario_modo FROM turnero_lugares WHERE activo = 1 ORDER BY sort_order ASC" + "SELECT id, nombre, descripcion, formulario_modo, tipo FROM turnero_lugares WHERE activo = 1 ORDER BY sort_order ASC" )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable) { $lugares = []; @@ -22,10 +22,12 @@ $lugarIdParam = isset($_GET['lugar_id']) ? (int)$_GET['lugar_id'] : 0; $lugarNombre = 'Estación de Servicio'; $lugarFormModo = 'link'; +$lugarTipo = 'muestras'; foreach ($lugares as $l) { if ((int)$l['id'] === $lugarIdParam) { $lugarNombre = $l['nombre']; $lugarFormModo = $l['formulario_modo'] ?? 'link'; + $lugarTipo = $l['tipo'] ?? 'muestras'; break; } } @@ -387,6 +389,38 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user'][' .rec-toast.info .ico { color: #3b82f6; } .rec-toast.warn .ico { color: #f59e0b; } .rec-toast.error .ico { color: #ef4444; } + + /* ── Widget muestras ────────────────────────────────────── */ + .muestra-row { + display: flex; align-items: center; gap: .6rem; + padding: .5rem .7rem; border-radius: 10px; margin-bottom: .35rem; + font-size: .84rem; border: 1px solid transparent; + } + .muestra-row.pendiente { background: #fffbeb; border-color: #fde68a; } + .muestra-row.recibida { background: #f0fdf4; border-color: #bbf7d0; color: #166534; } + .muestra-row.rechazada { background: #fef2f2; border-color: #fca5a5; color: #991b1b; } + .muestra-info { flex: 1; min-width: 0; } + .muestra-label { font-weight: 600; font-size: .82rem; font-family: monospace; } + .muestra-motivo { font-size: .7rem; margin-top: 2px; opacity: .85; } + .muestra-ts { font-size: .68rem; color: #94a3b8; flex-shrink: 0; } + .btn-muestra-accion { + border: none; border-radius: 8px; padding: 3px 11px; + font-size: .75rem; font-weight: 700; cursor: pointer; + display: flex; align-items: center; gap: .25rem; + transition: opacity .12s; + } + .btn-muestra-accion:active { opacity: .75; } + .btn-muestra-accion.recibir { background: #16a34a; color: #fff; } + .btn-muestra-accion.rechazar { background: #fff; color: #991b1b; border: 1.5px solid #fca5a5; } + .muestras-summary { + font-size: .72rem; color: #64748b; margin-bottom: .5rem; + display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; + } + .muestras-summary .mc { font-weight: 700; border-radius: 99px; padding: 1px 8px; + font-size: .68rem; } + .muestras-summary .mc.pend { background: #fef9c3; color: #854d0e; } + .muestras-summary .mc.rec { background: #dcfce7; color: #166534; } + .muestras-summary .mc.rech { background: #fee2e2; color: #991b1b; }
@@ -540,6 +574,16 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php'; + +