feat(lis): migración completa Firebird→MySQL + tracking de muestras en turnero
- Migraciones LIS 01-08: schema completo del nuevo LIS (secciones, protocolos, ítems de resultado, perfiles, tarifas, empresas, histórico transaccional) - ETL Firebird→MySQL: script CLI con conversión WIN1252→UTF-8, batches de 500, resolución de FKs y deduplicación de pacientes - turnero_muestras: tracking pendiente/recibida/rechazada por tipo de tubo - lugar.php: widget de recepción de muestras (solo tipo=muestras) - update_muestra_estado.php: API para marcar estado de muestra - create_solicitud.php: auto-crea muestras al guardar solicitud - get_consentimientos.php: incluye muestras[] en el response - 6 vistas SQL: v_muestras_hoy, v_recepcion_completa, v_examen_precio, etc. - numero_orden en encabezado del formulario firmado (D-/F- color diferenciado) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
86ed70db34
commit
5fedd233f4
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
config.local.php
|
||||
etl_*.log
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Configuración de conexiones para el ETL Firebird → MySQL
|
||||
*
|
||||
* Antes de ejecutar:
|
||||
* 1. Copiar este archivo como config.local.php (está en .gitignore)
|
||||
* 2. Llenar los valores reales
|
||||
* 3. Ejecutar: php run_etl.php
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// ── Firebird (origen) ────────────────────────────────────────────────
|
||||
'firebird' => [
|
||||
// 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',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Funciones auxiliares compartidas por el ETL
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convierte un string de WIN1252 a UTF-8.
|
||||
* Si el valor ya es UTF-8 válido, lo devuelve sin cambios.
|
||||
*/
|
||||
function toUtf8(?string $val, string $srcEncoding = 'WIN1252'): ?string {
|
||||
if ($val === null) return null;
|
||||
if (mb_check_encoding($val, 'UTF-8')) return $val;
|
||||
return iconv($srcEncoding, 'UTF-8//TRANSLIT//IGNORE', $val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte un array completo de strings (resultado de Firebird) a UTF-8.
|
||||
*/
|
||||
function rowToUtf8(array $row, string $srcEncoding = 'WIN1252'): array {
|
||||
foreach ($row as $k => $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();
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* ETL Firebird 2.5 → MySQL (utf8mb4)
|
||||
* Sistema: DBLAB_XIMENA_FB25 → nuevo sistema WhatsApp-Lab
|
||||
*
|
||||
* Uso:
|
||||
* php run_etl.php [--dry-run] [--skip-historico] [--only=PASO]
|
||||
*
|
||||
* Pasos disponibles (--only):
|
||||
* secciones | especialidades | tipos_muestra | protocolos | items |
|
||||
* perfiles | exam_tipos | tarifas | empresas |
|
||||
* medicos | pacientes | recepciones | relaciones | pagos
|
||||
*
|
||||
* Requerimientos:
|
||||
* - PHP 8.0+ con extensión PDO_Firebird (php-firebird) instalada
|
||||
* - Archivo config.local.php con credenciales reales
|
||||
* - Las migraciones LIS 01-07 ya ejecutadas en MySQL
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
set_time_limit(0);
|
||||
ini_set('memory_limit', '512M');
|
||||
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
|
||||
// ── Configuración ────────────────────────────────────────────────────────────
|
||||
$cfgFile = file_exists(__DIR__ . '/config.local.php')
|
||||
? __DIR__ . '/config.local.php'
|
||||
: __DIR__ . '/config.php';
|
||||
|
||||
$cfg = require $cfgFile;
|
||||
|
||||
// Argumentos CLI
|
||||
$args = array_slice($argv ?? [], 1);
|
||||
$dryRun = in_array('--dry-run', $args, true) || $cfg['options']['dry_run'];
|
||||
$skipHist = in_array('--skip-historico', $args, true) || $cfg['options']['skip_historico'];
|
||||
$onlyPaso = null;
|
||||
foreach ($args as $arg) {
|
||||
if (str_starts_with($arg, '--only=')) {
|
||||
$onlyPaso = strtolower(substr($arg, 7));
|
||||
}
|
||||
}
|
||||
|
||||
$logFp = fopen($cfg['options']['log_file'], 'w');
|
||||
|
||||
etlLog('=== ETL Firebird → MySQL ===', $logFp);
|
||||
etlLog("DRY_RUN: " . ($dryRun ? 'SÍ' : 'NO'), $logFp);
|
||||
if ($onlyPaso) etlLog("Solo paso: $onlyPaso", $logFp);
|
||||
|
||||
// ── Conexiones ───────────────────────────────────────────────────────────────
|
||||
try {
|
||||
$fb = new PDO(
|
||||
$cfg['firebird']['dsn'],
|
||||
$cfg['firebird']['user'],
|
||||
$cfg['firebird']['password'],
|
||||
[PDO::ATTR_ERRMODE => 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";
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /modules/turnero/api/update_muestra_estado.php
|
||||
* Marca una muestra como recibida o rechazada.
|
||||
* Solo accesible desde estaciones tipo "muestras".
|
||||
*
|
||||
* Body JSON:
|
||||
* muestra_id int requerido
|
||||
* estado string requerido 'recibida' | 'rechazada' | 'pendiente'
|
||||
* motivo_rechazo string opcional requerido si estado = 'rechazada'
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireMethod('POST');
|
||||
requireTurnero();
|
||||
|
||||
$datos = inputJson();
|
||||
$muestraId = isset($datos['muestra_id']) ? (int)$datos['muestra_id'] : 0;
|
||||
$estado = trim($datos['estado'] ?? '');
|
||||
$motivo = isset($datos['motivo_rechazo']) ? trim($datos['motivo_rechazo']) : null;
|
||||
|
||||
if ($muestraId <= 0) jsonError('muestra_id inválido.');
|
||||
if (!in_array($estado, ['recibida', 'rechazada', 'pendiente'], true)) {
|
||||
jsonError('estado debe ser recibida, rechazada o pendiente.');
|
||||
}
|
||||
if ($motivo === '') $motivo = null;
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Verificar que la muestra existe
|
||||
$stmt = $pdo->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');
|
||||
@@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -540,6 +574,16 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Muestras (solo estaciones tipo muestras) ── -->
|
||||
<div class="ficha-sec d-none" id="sec-muestras">
|
||||
<div class="ficha-sec-hdr">
|
||||
<i class="fas fa-flask" style="color:#ea580c"></i>
|
||||
Recepción de muestras
|
||||
</div>
|
||||
<div id="muestras-summary" class="muestras-summary"></div>
|
||||
<div id="lista-muestras"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Formulario embebido (iframe, modo link) ── -->
|
||||
<div class="ficha-sec d-none" id="sec-form-embebido">
|
||||
<div class="ficha-sec-hdr"><i class="fas fa-file-alt"></i>Formulario de consentimiento</div>
|
||||
@@ -697,11 +741,13 @@ let pollingConsentId = null;
|
||||
let _consentTokenCache = null;
|
||||
let _pacienteActivo = null;
|
||||
let _solicitudActiva = null;
|
||||
let _muestrasActivas = [];
|
||||
|
||||
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||
const BASE_WA = '<?= BASE_URL ?>';
|
||||
const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>';
|
||||
const LUGAR_FORM_MODO = '<?= $lugarFormModo ?>';
|
||||
const LUGAR_TIPO = '<?= $lugarTipo ?>';
|
||||
|
||||
// ── Arranque ──────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -1011,6 +1057,7 @@ async function cargarFichaSolicitud(turnoId) {
|
||||
}
|
||||
|
||||
renderConsentimientos(consts);
|
||||
renderMuestras(json.muestras || []);
|
||||
cargarComentarios(turnoId);
|
||||
|
||||
} catch (_) {}
|
||||
@@ -1265,6 +1312,9 @@ function resetFicha() {
|
||||
_consentTokenCache = null;
|
||||
_pacienteActivo = null;
|
||||
_solicitudActiva = null;
|
||||
_muestrasActivas = [];
|
||||
const secMuestras = document.getElementById('sec-muestras');
|
||||
if (secMuestras) secMuestras.classList.add('d-none');
|
||||
document.getElementById('ficha-orden').classList.add('d-none');
|
||||
cerrarModalPaciente();
|
||||
document.getElementById('btn-ver-paciente').classList.add('d-none');
|
||||
@@ -1518,6 +1568,117 @@ function renderHistorialTimeline(turnos, total) {
|
||||
return `<div class="mpac-timeline">${items}</div>${totalLabel}`;
|
||||
}
|
||||
|
||||
// ── Muestras ──────────────────────────────────────────────────
|
||||
function renderMuestras(lista) {
|
||||
_muestrasActivas = lista || [];
|
||||
const sec = document.getElementById('sec-muestras');
|
||||
const cont = document.getElementById('lista-muestras');
|
||||
const summary = document.getElementById('muestras-summary');
|
||||
if (!sec) return;
|
||||
|
||||
// Solo mostrar en estaciones tipo "muestras"
|
||||
if (LUGAR_TIPO !== 'muestras' || !lista.length) {
|
||||
sec.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
sec.classList.remove('d-none');
|
||||
|
||||
const nPend = lista.filter(m => m.estado === 'pendiente').length;
|
||||
const nRec = lista.filter(m => m.estado === 'recibida').length;
|
||||
const nRech = lista.filter(m => m.estado === 'rechazada').length;
|
||||
|
||||
let chips = '';
|
||||
if (nPend) chips += `<span class="mc pend">${nPend} pendiente${nPend > 1 ? 's' : ''}</span>`;
|
||||
if (nRec) chips += `<span class="mc rec">${nRec} recibida${nRec > 1 ? 's' : ''}</span>`;
|
||||
if (nRech) chips += `<span class="mc rech">${nRech} rechazada${nRech > 1 ? 's' : ''}</span>`;
|
||||
summary.innerHTML = chips;
|
||||
|
||||
cont.innerHTML = lista.map(m => {
|
||||
const esPend = m.estado === 'pendiente';
|
||||
const label = escHtml(m.label || m.tipo_muestra || 'MUESTRA');
|
||||
const tsHtml = m.recibida_at
|
||||
? `<span class="muestra-ts">${formatHora(m.recibida_at)}</span>`
|
||||
: '';
|
||||
const motivoHtml = m.motivo_rechazo
|
||||
? `<div class="muestra-motivo">${escHtml(m.motivo_rechazo)}</div>` : '';
|
||||
|
||||
const acciones = esPend
|
||||
? `<div style="display:flex;gap:5px;flex-shrink:0">
|
||||
<button class="btn-muestra-accion recibir"
|
||||
onclick="marcarMuestra(${m.id},'recibida')">
|
||||
<i class="fas fa-check"></i> Recibida
|
||||
</button>
|
||||
<button class="btn-muestra-accion rechazar"
|
||||
onclick="pedirRechazo(${m.id},'${label.replace(/'/g,'\\\'')}')" >
|
||||
<i class="fas fa-times"></i> Rechazar
|
||||
</button>
|
||||
</div>`
|
||||
: `${tsHtml}
|
||||
<button class="btn-muestra-accion"
|
||||
style="background:#f1f5f9;color:#64748b;border:1px solid #e2e8f0"
|
||||
onclick="marcarMuestra(${m.id},'pendiente')" title="Revertir a pendiente">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>`;
|
||||
|
||||
const ico = m.estado === 'recibida'
|
||||
? '<i class="fas fa-check-circle" style="color:#16a34a;flex-shrink:0"></i>'
|
||||
: m.estado === 'rechazada'
|
||||
? '<i class="fas fa-times-circle" style="color:#dc2626;flex-shrink:0"></i>'
|
||||
: '<i class="fas fa-clock" style="color:#d97706;flex-shrink:0"></i>';
|
||||
|
||||
return `<div class="muestra-row ${m.estado}" data-muestra-id="${m.id}">
|
||||
${ico}
|
||||
<div class="muestra-info">
|
||||
<span class="muestra-label">${label}</span>
|
||||
${motivoHtml}
|
||||
</div>
|
||||
${acciones}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function marcarMuestra(muestraId, estado, motivo = null) {
|
||||
try {
|
||||
const body = { muestra_id: muestraId, estado };
|
||||
if (motivo) body.motivo_rechazo = motivo;
|
||||
const res = await fetch(API + 'update_muestra_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
// Actualizar estado local sin re-fetch
|
||||
const m = _muestrasActivas.find(x => x.id == muestraId);
|
||||
if (m) {
|
||||
m.estado = estado;
|
||||
m.motivo_rechazo = motivo;
|
||||
m.recibida_at = estado !== 'pendiente' ? new Date().toISOString() : null;
|
||||
}
|
||||
renderMuestras(_muestrasActivas);
|
||||
const msg = estado === 'recibida'
|
||||
? 'Muestra recibida ✓'
|
||||
: estado === 'rechazada' ? 'Muestra rechazada' : 'Revertida a pendiente';
|
||||
mostrarToast(msg, estado === 'recibida' ? 'success' : estado === 'rechazada' ? 'warn' : 'info', 2000);
|
||||
} catch (err) {
|
||||
mostrarError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function pedirRechazo(muestraId, tipoMuestra) {
|
||||
const motivo = prompt(`Motivo de rechazo para "${tipoMuestra}":\n(hemólisis, coagulado, volumen insuficiente…)`);
|
||||
if (motivo === null) return;
|
||||
marcarMuestra(muestraId, 'rechazada', motivo.trim() || null);
|
||||
}
|
||||
|
||||
function formatHora(isoStr) {
|
||||
if (!isoStr) return '';
|
||||
try {
|
||||
return new Date(isoStr).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
|
||||
} catch (_) { return ''; }
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Aplica las migraciones LIS 01-07 en orden de dependencias.
|
||||
* Ejecutar UNA VEZ antes del ETL.
|
||||
*
|
||||
* Uso:
|
||||
* php run_lis_migrations.php
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/config/database.php'; // $pdo o Database::getInstance()
|
||||
require_once __DIR__ . '/classes/Database.php';
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
|
||||
$migrations = [
|
||||
'20260704_lis_01_catalogos_base.sql',
|
||||
'20260704_lis_02_protocolos.sql',
|
||||
'20260704_lis_03_perfiles.sql',
|
||||
'20260704_lis_04_exam_ampliar.sql',
|
||||
'20260704_lis_05_tarifas.sql',
|
||||
'20260704_lis_06_historico.sql',
|
||||
'20260704_lis_07_turnero_muestras.sql',
|
||||
'20260704_lis_08_vistas.sql',
|
||||
];
|
||||
|
||||
$dir = __DIR__ . '/migrations/';
|
||||
|
||||
foreach ($migrations as $file) {
|
||||
$path = $dir . $file;
|
||||
echo "Ejecutando $file... ";
|
||||
try {
|
||||
$sql = file_get_contents($path);
|
||||
// Separar por ; para ejecutar sentencia a sentencia
|
||||
foreach (array_filter(array_map('trim', explode(';', $sql))) as $stmt) {
|
||||
if ($stmt !== '') $pdo->exec($stmt);
|
||||
}
|
||||
echo "OK\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "ERROR: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nMigraciones LIS completadas. Ahora ejecuta el ETL:\n";
|
||||
echo " cd migrations/etl && cp config.php config.local.php\n";
|
||||
echo " # editar config.local.php con credenciales Firebird\n";
|
||||
echo " php run_etl.php --dry-run # prueba sin insertar\n";
|
||||
echo " php run_etl.php # migración real\n";
|
||||
@@ -36,7 +36,8 @@ if ($modoTurnero) {
|
||||
p.nombre_completo AS paciente_nombre,
|
||||
p.numero_documento, p.tipo_documento,
|
||||
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
|
||||
t.sesion_id
|
||||
t.sesion_id,
|
||||
ts.numero_orden
|
||||
FROM turnero_consentimientos tc
|
||||
JOIN lab_formularios f ON f.id = tc.formulario_id
|
||||
JOIN turnero_turnos t ON t.id = tc.turno_id
|
||||
@@ -128,11 +129,13 @@ if ($modoTurnero) {
|
||||
f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina,
|
||||
p.nombre_completo AS paciente_nombre, p.numero_documento, p.tipo_documento,
|
||||
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
|
||||
u.full_name AS enviado_por_nombre, u.email AS enviado_por_email
|
||||
u.full_name AS enviado_por_nombre, u.email AS enviado_por_email,
|
||||
ld.numero_orden
|
||||
FROM lab_form_envios e
|
||||
JOIN lab_formularios f ON f.id = e.formulario_id
|
||||
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
|
||||
LEFT JOIN admin_users u ON u.id = e.enviado_por
|
||||
LEFT JOIN lab_domicilios ld ON ld.id = e.domicilio_id
|
||||
WHERE e.token = ?",
|
||||
[$tokenPublico]
|
||||
);
|
||||
@@ -150,11 +153,13 @@ if ($modoTurnero) {
|
||||
f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina,
|
||||
p.nombre_completo AS paciente_nombre, p.numero_documento, p.tipo_documento,
|
||||
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
|
||||
u.full_name AS enviado_por_nombre, u.email AS enviado_por_email
|
||||
u.full_name AS enviado_por_nombre, u.email AS enviado_por_email,
|
||||
ld.numero_orden
|
||||
FROM lab_form_envios e
|
||||
JOIN lab_formularios f ON f.id = e.formulario_id
|
||||
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
|
||||
LEFT JOIN admin_users u ON u.id = e.enviado_por
|
||||
LEFT JOIN lab_domicilios ld ON ld.id = e.domicilio_id
|
||||
WHERE e.id = ?",
|
||||
[$id]
|
||||
);
|
||||
@@ -167,6 +172,11 @@ if ($modoTurnero) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Consecutivo de orden (D-... domicilio | F-... turnero) ────────────
|
||||
$numeroOrden = $modoTurnero
|
||||
? ($tcRow['numero_orden'] ?? null)
|
||||
: ($envio['numero_orden'] ?? null);
|
||||
|
||||
// ── Config global del lab ─────────────────────────────────────────────
|
||||
$cfgRows = $db->fetchAll('SELECT clave, valor FROM lab_config WHERE valor != ""');
|
||||
$cfg = [];
|
||||
@@ -442,6 +452,20 @@ function esc2(mixed $v): string {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="doc-header-badge" style="text-align:right">
|
||||
<?php if ($numeroOrden): ?>
|
||||
<?php
|
||||
$esDomicilio = str_starts_with($numeroOrden, 'D-');
|
||||
$ordenColor = $esDomicilio ? '#bbf7d0' : '#bfdbfe';
|
||||
$ordenTxt = $esDomicilio ? '#14532d' : '#1e3a8a';
|
||||
$ordenLabel = $esDomicilio ? 'Domicilio' : 'Turnero';
|
||||
?>
|
||||
<div style="background:<?= $ordenColor ?>;color:<?= $ordenTxt ?>;border-radius:8px;
|
||||
padding:4px 10px;font-size:12px;font-weight:700;letter-spacing:.03em;
|
||||
margin-bottom:6px;font-family:monospace">
|
||||
<?= esc2($numeroOrden) ?>
|
||||
</div>
|
||||
<div style="font-size:10px;opacity:.75;margin-bottom:4px"><?= $ordenLabel ?></div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$eBadge = match($envio['estado']) {
|
||||
'firmado' => 'estado-firmado',
|
||||
@@ -877,6 +901,9 @@ function esc2(mixed $v): string {
|
||||
<?= esc2($docPiePagina) ?> •
|
||||
<?php endif; ?>
|
||||
<i class="fas fa-shield-alt me-1"></i>Generado el <?= date('d/m/Y H:i') ?> • ID #<?= $envio['id'] ?>
|
||||
<?php if ($numeroOrden): ?>
|
||||
• Orden: <strong><?= esc2($numeroOrden) ?></strong>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php if ($envio['hash_verificacion']): ?>
|
||||
<span class="hash-short" title="Hash SHA-256"><?= substr($envio['hash_verificacion'],0,16) ?>...</span>
|
||||
|
||||
Reference in New Issue
Block a user