diff --git a/backend/src/establecimiento.sql b/backend/src/establecimiento.sql
index c4e9b2b..4a86f0d 100644
--- a/backend/src/establecimiento.sql
+++ b/backend/src/establecimiento.sql
@@ -1,47 +1,47 @@
--- INSERTS TABLA ESTABLECIMIENTO
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('148', 'CPMS ACACIAS', 'ACACIAS', 'META', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('113', 'COMPLEJO CARCELARIO Y PENITENCIARIO BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('156', 'PMS LA ESPERANZA DE GUADUAS', 'GUADUAS', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('129', 'CPAMSM BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('130', 'CPOMS ACACIAS', 'ACACIAS', 'META', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('114', 'CPMS BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('145', 'CPMS ESPINAL', 'ESPINAL', 'TOLIMA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('150', 'CPAMS EL BARNE', 'COMBITA', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('112', 'EPMSC SOGAMOSO', 'SOGAMOSO', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('153', 'CPMS YOPAL', 'YOPAL', 'CASANARE', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('107', 'EPMSC GUATEQUE', 'GUATEQUE', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('157', 'PMS LAS HELICONIAS DE FLORENCIA', 'FLORENCIA', 'CAQUETA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('131', 'CPMS VILLAVICENCIO', 'VILLAVICENCIO', 'META', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('124', 'CPMS LA MESA', 'LA MESA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('133', 'EPMSC GRANADA', 'GRANADA', 'META', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('138', 'CPMS GIRARDOT', 'GIRARDOT', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('142', 'EPMSC PITALITO', 'PITALITO', 'HUILA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('116', 'EPMSC CAQUEZA', 'CAQUEZA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('105', 'EPMSC DUITAMA', 'DUITAMA', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('139', 'CPMS NEIVA', 'NEIVA', 'HUILA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('143', 'CPMS FLORENCIA', 'FLORENCIA', 'CAQUETA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('158', 'CPMS GUAMO', 'GUAMO', 'TOLIMA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('127', 'CPMS VILLETA', 'VILLETA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('103', 'EPMSC SANTA ROSA DE VITERBO (JYP-MUJERES)', 'SANTA ROSA DE VITERBO', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('104', 'CPMS CHIQUINQUIRA', 'CHIQUINQUIRA', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('141', 'CPMS LA PLATA', 'LA PLATA', 'HUILA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('140', 'CPMS GARZON', 'GARZON', 'HUILA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('144', 'EPMSC CHAPARRAL', 'CHAPARRAL', 'TOLIMA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('120', 'CPMS GACHETA', 'GACHETA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('126', 'CPMS UBATE', 'UBATE', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('119', 'CPMS FUSAGASUGA', 'FUSAGASUGA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('117', 'CPMS CHOCONTA', 'CHOCONTA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('110', 'CPMS RAMIRIQUI', 'RAMIRIQUI', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('109', 'CPMS MONIQUIRA', 'MONIQUIRA', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('149', 'CPMS TUNJA', 'TUNJA', 'BOYACA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('101', 'EPMSC LETICIA', 'LETICIA', 'AMAZONAS', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9020', 'CPAMSEJAPI', 'META', 'REPUBLICA DE COLOMBIA', 'EJERCITO', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('152', 'CPMS PAZ DE ARIPORO', 'PAZ DE ARIPORO', 'CASANARE', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9001', 'CPMMSF FACATATIVA', 'FACATATIVA', 'CUNDINAMARCA', 'POLICIA', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('136', 'CPMS MELGAR', 'MELGAR', 'TOLIMA', 'CENTRAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9011', 'CPAMSEJART', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'EJERCITO', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9016', 'CPAMSEJECO', 'FACATATIVA', 'CUNDINAMARCA', 'EJERCITO', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9018', 'CPAMSEJEYO', 'YOPAL', 'CASANARE', 'EJERCITO', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9006', 'CPAMSEJEPO', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'EJERCITO', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9022', 'ARBOG BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'ARMADA NACIONAL', 'CENTRAL');
-INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9033', 'CPMMS FUERZA AEREA', 'VILLAVICENCIO', 'META', 'FUERZA AEREA', 'CENTRAL');
+-- UPSERTS TABLA ESTABLECIMIENTO
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('148', 'CPMS ACACIAS', 'ACACIAS', 'META', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('113', 'COMPLEJO CARCELARIO Y PENITENCIARIO BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('156', 'PMS LA ESPERANZA DE GUADUAS', 'GUADUAS', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('129', 'CPAMSM BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('130', 'CPOMS ACACIAS', 'ACACIAS', 'META', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('114', 'CPMS BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('145', 'CPMS ESPINAL', 'ESPINAL', 'TOLIMA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('150', 'CPAMS EL BARNE', 'COMBITA', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('112', 'EPMSC SOGAMOSO', 'SOGAMOSO', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('153', 'CPMS YOPAL', 'YOPAL', 'CASANARE', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('107', 'EPMSC GUATEQUE', 'GUATEQUE', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('157', 'PMS LAS HELICONIAS DE FLORENCIA', 'FLORENCIA', 'CAQUETA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('131', 'CPMS VILLAVICENCIO', 'VILLAVICENCIO', 'META', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('124', 'CPMS LA MESA', 'LA MESA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('133', 'EPMSC GRANADA', 'GRANADA', 'META', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('138', 'CPMS GIRARDOT', 'GIRARDOT', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('142', 'EPMSC PITALITO', 'PITALITO', 'HUILA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('116', 'EPMSC CAQUEZA', 'CAQUEZA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('105', 'EPMSC DUITAMA', 'DUITAMA', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('139', 'CPMS NEIVA', 'NEIVA', 'HUILA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('143', 'CPMS FLORENCIA', 'FLORENCIA', 'CAQUETA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('158', 'CPMS GUAMO', 'GUAMO', 'TOLIMA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('127', 'CPMS VILLETA', 'VILLETA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('103', 'EPMSC SANTA ROSA DE VITERBO (JYP-MUJERES)', 'SANTA ROSA DE VITERBO', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('104', 'CPMS CHIQUINQUIRA', 'CHIQUINQUIRA', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('141', 'CPMS LA PLATA', 'LA PLATA', 'HUILA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('140', 'CPMS GARZON', 'GARZON', 'HUILA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('144', 'EPMSC CHAPARRAL', 'CHAPARRAL', 'TOLIMA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('120', 'CPMS GACHETA', 'GACHETA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('126', 'CPMS UBATE', 'UBATE', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('119', 'CPMS FUSAGASUGA', 'FUSAGASUGA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('117', 'CPMS CHOCONTA', 'CHOCONTA', 'CUNDINAMARCA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('110', 'CPMS RAMIRIQUI', 'RAMIRIQUI', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('109', 'CPMS MONIQUIRA', 'MONIQUIRA', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('149', 'CPMS TUNJA', 'TUNJA', 'BOYACA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('101', 'EPMSC LETICIA', 'LETICIA', 'AMAZONAS', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9020', 'CPAMSEJAPI', 'META', 'REPUBLICA DE COLOMBIA', 'EJERCITO', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('152', 'CPMS PAZ DE ARIPORO', 'PAZ DE ARIPORO', 'CASANARE', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9001', 'CPMMSF FACATATIVA', 'FACATATIVA', 'CUNDINAMARCA', 'POLICIA', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('136', 'CPMS MELGAR', 'MELGAR', 'TOLIMA', 'CENTRAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9011', 'CPAMSEJART', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'EJERCITO', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9016', 'CPAMSEJECO', 'FACATATIVA', 'CUNDINAMARCA', 'EJERCITO', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9018', 'CPAMSEJEYO', 'YOPAL', 'CASANARE', 'EJERCITO', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9006', 'CPAMSEJEPO', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'EJERCITO', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9022', 'ARBOG BOGOTA', 'BOGOTA D.C.', 'BOGOTA DISTRITO CAPITAL', 'ARMADA NACIONAL', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
+INSERT INTO establecimiento (codigo_establecimiento, nombre_establecimiento, epc_ciudad, epc_departamento, regional, regional_normalizada) VALUES ('9033', 'CPMMS FUERZA AEREA', 'VILLAVICENCIO', 'META', 'FUERZA AEREA', 'CENTRAL') ON CONFLICT (codigo_establecimiento) DO UPDATE SET nombre_establecimiento = EXCLUDED.nombre_establecimiento, epc_ciudad = EXCLUDED.epc_ciudad, epc_departamento = EXCLUDED.epc_departamento, regional = EXCLUDED.regional, regional_normalizada = EXCLUDED.regional_normalizada;
diff --git a/backend/src/ingreso.sql b/backend/src/ingreso.sql
index d1d64be..21e66db 100644
--- a/backend/src/ingreso.sql
+++ b/backend/src/ingreso.sql
@@ -1,6 +1,5 @@
-- UPSERTS TABLA INGRESO
INSERT INTO ingreso (interno, codigo_establecimiento, estado, fecha_ingreso, nacionalidad) VALUES ('372', '148', 'INTRAMURAL', '2021-09-17', 'REPUBLICA DE COLOMBIA') ON CONFLICT (interno) DO UPDATE SET codigo_establecimiento = EXCLUDED.codigo_establecimiento, estado = EXCLUDED.estado, fecha_ingreso = EXCLUDED.fecha_ingreso, nacionalidad = EXCLUDED.nacionalidad;
-INSERT INTO ingreso (interno, codigo_establecimiento, estado, fecha_ingreso, nacionalidad) VALUES ('385', '148', 'INTRAMURAL', '2022-09-17', 'REPUBLICA DE COLOMBIA') ON CONFLICT (interno) DO UPDATE SET codigo_establecimiento = EXCLUDED.codigo_establecimiento, estado = EXCLUDED.estado, fecha_ingreso = EXCLUDED.fecha_ingreso, nacionalidad = EXCLUDED.nacionalidad;
INSERT INTO ingreso (interno, codigo_establecimiento, estado, fecha_ingreso, nacionalidad) VALUES ('394', '113', 'INTRAMURAL', '2019-12-06', 'REPUBLICA DE COLOMBIA') ON CONFLICT (interno) DO UPDATE SET codigo_establecimiento = EXCLUDED.codigo_establecimiento, estado = EXCLUDED.estado, fecha_ingreso = EXCLUDED.fecha_ingreso, nacionalidad = EXCLUDED.nacionalidad;
INSERT INTO ingreso (interno, codigo_establecimiento, estado, fecha_ingreso, nacionalidad) VALUES ('557', '113', 'INTRAMURAL', '2025-08-20', 'REPUBLICA DE COLOMBIA') ON CONFLICT (interno) DO UPDATE SET codigo_establecimiento = EXCLUDED.codigo_establecimiento, estado = EXCLUDED.estado, fecha_ingreso = EXCLUDED.fecha_ingreso, nacionalidad = EXCLUDED.nacionalidad;
INSERT INTO ingreso (interno, codigo_establecimiento, estado, fecha_ingreso, nacionalidad) VALUES ('833', '113', 'INTRAMURAL', '2019-12-30', 'NO DATO SISIPEC') ON CONFLICT (interno) DO UPDATE SET codigo_establecimiento = EXCLUDED.codigo_establecimiento, estado = EXCLUDED.estado, fecha_ingreso = EXCLUDED.fecha_ingreso, nacionalidad = EXCLUDED.nacionalidad;
diff --git a/backend/src/ips.xlsx b/backend/src/ips.xlsx
new file mode 100644
index 0000000..cb0d6cc
Binary files /dev/null and b/backend/src/ips.xlsx differ
diff --git a/backend/src/paciente.sql b/backend/src/paciente.sql
index ded6557..4fe887b 100644
--- a/backend/src/paciente.sql
+++ b/backend/src/paciente.sql
@@ -1,7 +1,6 @@
-- UPSERTS TABLA PACIENTE
UPDATE paciente SET activo = false;
INSERT INTO paciente (interno, tipo_documento, numero_documento, primer_apellido, segundo_apellido, primer_nombre, segundo_nombre, fecha_nacimiento, edad, sexo, activo) VALUES ('372', 'CC', '79427056', 'MATEUS', 'RODRIGUEZ', 'ALBEIRO', NULL, '1967-09-01', NULL, 'MASCULINO', true) ON CONFLICT (interno) DO UPDATE SET tipo_documento = EXCLUDED.tipo_documento, numero_documento = EXCLUDED.numero_documento, primer_apellido = EXCLUDED.primer_apellido, segundo_apellido = EXCLUDED.segundo_apellido, primer_nombre = EXCLUDED.primer_nombre, segundo_nombre = EXCLUDED.segundo_nombre, fecha_nacimiento = EXCLUDED.fecha_nacimiento, sexo = EXCLUDED.sexo, activo = true;
-INSERT INTO paciente (interno, tipo_documento, numero_documento, primer_apellido, segundo_apellido, primer_nombre, segundo_nombre, fecha_nacimiento, edad, sexo, activo) VALUES ('385', 'CC', '1013105461', 'GUEVARA', 'RAMIREZ', 'JHONATHAN', 'DAVID', '1983-09-05', NULL, 'MASCULINO', true) ON CONFLICT (interno) DO UPDATE SET tipo_documento = EXCLUDED.tipo_documento, numero_documento = EXCLUDED.numero_documento, primer_apellido = EXCLUDED.primer_apellido, segundo_apellido = EXCLUDED.segundo_apellido, primer_nombre = EXCLUDED.primer_nombre, segundo_nombre = EXCLUDED.segundo_nombre, fecha_nacimiento = EXCLUDED.fecha_nacimiento, sexo = EXCLUDED.sexo, activo = true;
INSERT INTO paciente (interno, tipo_documento, numero_documento, primer_apellido, segundo_apellido, primer_nombre, segundo_nombre, fecha_nacimiento, edad, sexo, activo) VALUES ('394', 'CC', '15371570', 'TUBERQUIA', 'GONZALEZ', 'JORGE', 'IVAN', '1984-09-05', NULL, 'MASCULINO', true) ON CONFLICT (interno) DO UPDATE SET tipo_documento = EXCLUDED.tipo_documento, numero_documento = EXCLUDED.numero_documento, primer_apellido = EXCLUDED.primer_apellido, segundo_apellido = EXCLUDED.segundo_apellido, primer_nombre = EXCLUDED.primer_nombre, segundo_nombre = EXCLUDED.segundo_nombre, fecha_nacimiento = EXCLUDED.fecha_nacimiento, sexo = EXCLUDED.sexo, activo = true;
INSERT INTO paciente (interno, tipo_documento, numero_documento, primer_apellido, segundo_apellido, primer_nombre, segundo_nombre, fecha_nacimiento, edad, sexo, activo) VALUES ('557', 'CC', '80253043', 'MEJIA', 'HUERTAS', 'ANDRES', 'FRANCISCO', '1983-03-16', NULL, 'MASCULINO', true) ON CONFLICT (interno) DO UPDATE SET tipo_documento = EXCLUDED.tipo_documento, numero_documento = EXCLUDED.numero_documento, primer_apellido = EXCLUDED.primer_apellido, segundo_apellido = EXCLUDED.segundo_apellido, primer_nombre = EXCLUDED.primer_nombre, segundo_nombre = EXCLUDED.segundo_nombre, fecha_nacimiento = EXCLUDED.fecha_nacimiento, sexo = EXCLUDED.sexo, activo = true;
INSERT INTO paciente (interno, tipo_documento, numero_documento, primer_apellido, segundo_apellido, primer_nombre, segundo_nombre, fecha_nacimiento, edad, sexo, activo) VALUES ('833', 'CC', '75076842', 'GIRALDO', 'CASTAÑO', 'FRANCISCO', 'IVAN', '1975-02-25', NULL, 'MASCULINO', true) ON CONFLICT (interno) DO UPDATE SET tipo_documento = EXCLUDED.tipo_documento, numero_documento = EXCLUDED.numero_documento, primer_apellido = EXCLUDED.primer_apellido, segundo_apellido = EXCLUDED.segundo_apellido, primer_nombre = EXCLUDED.primer_nombre, segundo_nombre = EXCLUDED.segundo_nombre, fecha_nacimiento = EXCLUDED.fecha_nacimiento, sexo = EXCLUDED.sexo, activo = true;
diff --git a/backend/src/pacientes.xlsx b/backend/src/pacientes.xlsx
index d7b3bcb..b008b0f 100644
Binary files a/backend/src/pacientes.xlsx and b/backend/src/pacientes.xlsx differ
diff --git a/backend/src/plantilla.xlsx b/backend/src/plantilla.xlsx
new file mode 100644
index 0000000..bc41157
Binary files /dev/null and b/backend/src/plantilla.xlsx differ
diff --git a/backend/src/reps.xlsx b/backend/src/reps.xlsx
new file mode 100644
index 0000000..91cfac5
Binary files /dev/null and b/backend/src/reps.xlsx differ
diff --git a/backend/src/schema.sql b/backend/src/schema.sql
index b0ca0e8..f084e69 100644
--- a/backend/src/schema.sql
+++ b/backend/src/schema.sql
@@ -49,7 +49,8 @@ CREATE TABLE IF NOT EXISTS ips (
direccion text,
telefono text,
departamento text,
- municipio text
+ municipio text,
+ tiene_convenio boolean DEFAULT true
);
-- ===== Ingreso =====
@@ -91,7 +92,8 @@ CREATE TABLE IF NOT EXISTS usuario (
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ultimo_login TIMESTAMP,
intentos_fallidos INTEGER DEFAULT 0,
- bloqueado_hasta TIMESTAMP
+ bloqueado_hasta TIMESTAMP,
+ token_version INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS usuario_sede (
@@ -163,9 +165,23 @@ CREATE TABLE IF NOT EXISTS autorizacion (
cup_codigo varchar(20),
tipo_autorizacion varchar(50) NOT NULL DEFAULT 'consultas_externas',
tipo_servicio varchar(50),
+ estado_autorizacion varchar(20) NOT NULL DEFAULT 'pendiente',
version integer NOT NULL DEFAULT 1
);
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_constraint
+ WHERE conname = 'autorizacion_estado_autorizacion_chk'
+ ) THEN
+ ALTER TABLE autorizacion
+ ADD CONSTRAINT autorizacion_estado_autorizacion_chk
+ CHECK (estado_autorizacion IN ('pendiente', 'autorizado', 'no_autorizado'));
+ END IF;
+END $$;
+
INSERT INTO consecutivo_autorizacion (id, codigo)
VALUES (1, COALESCE((SELECT MAX(numero_autorizacion) FROM autorizacion), 'UTUSCPGB00'))
ON CONFLICT (id) DO NOTHING;
@@ -201,6 +217,19 @@ CREATE TABLE IF NOT EXISTS autorizacion_version (
fecha_version TIMESTAMP NOT NULL DEFAULT NOW()
);
+-- ===== Profesionales REPS =====
+CREATE TABLE IF NOT EXISTS profesional_reps (
+ id SERIAL PRIMARY KEY,
+ nit TEXT,
+ nombre_profesional TEXT,
+ codigo_habilitacion TEXT,
+ direccion TEXT,
+ telefono TEXT,
+ departamento TEXT,
+ municipio TEXT,
+ activo boolean DEFAULT true
+);
+
CREATE UNIQUE INDEX IF NOT EXISTS autorizacion_version_unique
ON autorizacion_version (numero_autorizacion, version);
diff --git a/backend/src/server.js b/backend/src/server.js
index c8a991b..84cfa07 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -226,19 +226,8 @@ const ensureCupsTables = async () => {
`);
await pool.query(`
- DO $$
- BEGIN
- IF NOT EXISTS (
- SELECT 1
- FROM pg_constraint
- WHERE conname = 'autorizacion_cup_codigo_fk'
- ) THEN
- ALTER TABLE autorizacion
- ADD CONSTRAINT autorizacion_cup_codigo_fk
- FOREIGN KEY (cup_codigo)
- REFERENCES cups_cubiertos(codigo);
- END IF;
- END $$;
+ ALTER TABLE autorizacion
+ DROP CONSTRAINT IF EXISTS autorizacion_cup_codigo_fk;
`);
};
@@ -265,6 +254,87 @@ const ensureAutorizacionVersionTables = async () => {
`);
};
+const ensureIpsConvenio = async () => {
+ await pool.query(`
+ ALTER TABLE ips
+ ADD COLUMN IF NOT EXISTS tiene_convenio BOOLEAN NOT NULL DEFAULT true;
+ `);
+
+ await pool.query(`
+ UPDATE ips
+ SET tiene_convenio = true
+ WHERE tiene_convenio IS NULL;
+ `);
+};
+
+const ensureProfesionalRepsTable = async () => {
+ await pool.query(`
+ CREATE TABLE IF NOT EXISTS profesional_reps (
+ id SERIAL PRIMARY KEY,
+ nit TEXT,
+ nombre_profesional TEXT,
+ codigo_habilitacion TEXT,
+ direccion TEXT,
+ telefono TEXT,
+ departamento TEXT,
+ municipio TEXT,
+ activo BOOLEAN NOT NULL DEFAULT true
+ );
+ `);
+
+ await pool.query(`
+ ALTER TABLE profesional_reps
+ ADD COLUMN IF NOT EXISTS activo BOOLEAN NOT NULL DEFAULT true;
+ `);
+
+ await pool.query(`
+ UPDATE profesional_reps
+ SET activo = true
+ WHERE activo IS NULL;
+ `);
+};
+
+const ensureAutorizacionEstado = async () => {
+ await pool.query(`
+ ALTER TABLE autorizacion
+ ADD COLUMN IF NOT EXISTS estado_autorizacion VARCHAR(20) NOT NULL DEFAULT 'pendiente';
+ `);
+
+ await pool.query(`
+ UPDATE autorizacion
+ SET estado_autorizacion = 'pendiente'
+ WHERE estado_autorizacion IS NULL;
+ `);
+
+ await pool.query(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_constraint
+ WHERE conname = 'autorizacion_estado_autorizacion_chk'
+ ) THEN
+ ALTER TABLE autorizacion
+ ADD CONSTRAINT autorizacion_estado_autorizacion_chk
+ CHECK (estado_autorizacion IN ('pendiente', 'autorizado', 'no_autorizado'));
+ END IF;
+ END $$;
+ `);
+};
+
+const ensureUsuarioTokenVersion = async () => {
+ await pool.query(`
+ ALTER TABLE usuario
+ ADD COLUMN IF NOT EXISTS token_version INTEGER NOT NULL DEFAULT 1;
+ `);
+
+ await pool.query(`
+ UPDATE usuario
+ SET token_version = 1
+ WHERE token_version IS NULL;
+ `);
+};
+
const ensureAdminFromEnv = async () => {
const adminUser = process.env.ADMIN_USER;
const adminPass = process.env.ADMIN_PASS;
@@ -328,7 +398,7 @@ const ensureAdminFromEnv = async () => {
// Esto resuelve el error: "Cannot access 'verificarToken' before initialization".
// Middleware para verificar token JWT
-const verificarToken = (req, res, next) => {
+const verificarToken = async (req, res, next) => {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) {
@@ -337,10 +407,29 @@ const verificarToken = (req, res, next) => {
try {
const decoded = jwt.verify(token, JWT_SECRET);
+ const { rows } = await pool.query(
+ 'SELECT id_usuario, activo, token_version FROM usuario WHERE id_usuario = $1',
+ [decoded.id_usuario]
+ );
+
+ if (rows.length === 0) {
+ return res.status(401).json({ error: 'Usuario no encontrado' });
+ }
+
+ const usuarioDb = rows[0];
+ if (!usuarioDb.activo) {
+ return res.status(401).json({ error: 'Usuario inactivo' });
+ }
+
+ if (Number(usuarioDb.token_version) !== Number(decoded.token_version)) {
+ return res.status(401).json({ error: 'Sesion expirada' });
+ }
+
req.usuario = decoded;
- next();
+ return next();
} catch (error) {
- return res.status(401).json({ error: 'Token inválido o expirado' });
+ console.error('Error validando token:', error.message);
+ return res.status(401).json({ error: 'Token invalido o expirado' });
}
};
@@ -365,6 +454,45 @@ const puedeGenerarAutorizaciones = (req, res, next) => {
next();
};
+const puedeDescargarPdfAutorizacion = async (req, res, next) => {
+ if (req.usuario.nombre_rol === 'administrador') {
+ return next();
+ }
+
+ const numeroAutorizacion =
+ req.body?.numero_autorizacion || req.query?.numero_autorizacion;
+
+ if (!numeroAutorizacion) {
+ return res.status(400).json({ error: 'Falta parametro numero_autorizacion' });
+ }
+
+ try {
+ const { rows } = await pool.query(
+ `
+ SELECT COALESCE(estado_autorizacion, 'pendiente') AS estado_autorizacion
+ FROM autorizacion
+ WHERE numero_autorizacion = $1
+ `,
+ [numeroAutorizacion]
+ );
+
+ if (rows.length === 0) {
+ return res.status(404).json({ error: 'Autorizacion no encontrada' });
+ }
+
+ if (rows[0].estado_autorizacion !== 'autorizado') {
+ return res.status(403).json({
+ error: 'Autorizacion pendiente o no autorizada',
+ });
+ }
+
+ return next();
+ } catch (error) {
+ console.error('Error validando estado de autorizacion:', error.message);
+ return res.status(500).json({ error: 'Error validando autorizacion' });
+ }
+};
+
// Middleware para verificar acceso por sede
const verificarAccesoSede = async (req, res, next) => {
// Si es administrador, tiene acceso a todo
@@ -506,6 +634,143 @@ app.post(
}
);
+app.post(
+ '/api/cargar-ips',
+ verificarToken,
+ esAdministrador,
+ upload.single('archivo'),
+ async (req, res) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({ error: 'No se recibio archivo Excel' });
+ }
+
+ const jobId = createJobId();
+ const job = {
+ id: jobId,
+ type: 'ips-carga',
+ status: 'queued',
+ createdAt: new Date().toISOString(),
+ ownerId: req.usuario.id_usuario,
+ };
+
+ jobs.set(jobId, job);
+
+ const jobTmpDir = path.join(os.tmpdir(), 'saludut_jobs', jobId);
+ await fsPromises.mkdir(jobTmpDir, { recursive: true });
+ const inputPath = path.join(jobTmpDir, `ips_${jobId}.xlsx`);
+ await fsPromises.writeFile(inputPath, req.file.buffer);
+
+ enqueueJob(job, async () => {
+ try {
+ return await procesarExcelIps(inputPath);
+ } finally {
+ await safeUnlink(inputPath);
+ await safeRemoveDir(jobTmpDir);
+ }
+ });
+
+ return res.status(202).json(sanitizeJob(job));
+ } catch (error) {
+ console.error('Error creando job de IPS:', error);
+ return res.status(500).json({
+ error: 'Error creando el job para procesar el Excel de IPS',
+ });
+ }
+ }
+);
+
+app.post(
+ '/api/cargar-reps',
+ verificarToken,
+ esAdministrador,
+ upload.single('archivo'),
+ async (req, res) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({ error: 'No se recibio archivo Excel' });
+ }
+
+ const jobId = createJobId();
+ const job = {
+ id: jobId,
+ type: 'reps-carga',
+ status: 'queued',
+ createdAt: new Date().toISOString(),
+ ownerId: req.usuario.id_usuario,
+ };
+
+ jobs.set(jobId, job);
+
+ const jobTmpDir = path.join(os.tmpdir(), 'saludut_jobs', jobId);
+ await fsPromises.mkdir(jobTmpDir, { recursive: true });
+ const inputPath = path.join(jobTmpDir, `reps_${jobId}.xlsx`);
+ await fsPromises.writeFile(inputPath, req.file.buffer);
+
+ enqueueJob(job, async () => {
+ try {
+ return await procesarExcelReps(inputPath);
+ } finally {
+ await safeUnlink(inputPath);
+ await safeRemoveDir(jobTmpDir);
+ }
+ });
+
+ return res.status(202).json(sanitizeJob(job));
+ } catch (error) {
+ console.error('Error creando job de REPS:', error);
+ return res.status(500).json({
+ error: 'Error creando el job para procesar el Excel de REPS',
+ });
+ }
+ }
+);
+
+app.post(
+ '/api/cargar-autorizaciones-masivas',
+ verificarToken,
+ upload.single('archivo'),
+ async (req, res) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({ error: 'No se recibio archivo Excel' });
+ }
+
+ const jobId = createJobId();
+ const job = {
+ id: jobId,
+ type: 'autorizaciones-masivas',
+ status: 'queued',
+ createdAt: new Date().toISOString(),
+ ownerId: req.usuario.id_usuario,
+ };
+
+ jobs.set(jobId, job);
+
+ const jobTmpDir = path.join(os.tmpdir(), 'saludut_jobs', jobId);
+ await fsPromises.mkdir(jobTmpDir, { recursive: true });
+ const inputPath = path.join(jobTmpDir, `autorizaciones_${jobId}.xlsx`);
+ await fsPromises.writeFile(inputPath, req.file.buffer);
+
+ enqueueJob(job, async () => {
+ try {
+ return await procesarExcelAutorizacionesMasivas(inputPath, req.usuario);
+ } finally {
+ await safeUnlink(inputPath);
+ await safeRemoveDir(jobTmpDir);
+ }
+ });
+
+ return res.status(202).json(sanitizeJob(job));
+ } catch (error) {
+ console.error('Error creando job de autorizaciones masivas:', error);
+ return res.status(500).json({
+ error: 'Error creando el job para procesar autorizaciones masivas',
+ });
+ }
+ }
+);
+
// MIDDLEWARES GLOBALES (para el resto de las rutas que sí usan JSON)
app.use(
express.json({
@@ -665,6 +930,852 @@ async function procesarExcelCups(notaPath, referenciaPath) {
};
}
+
+
+const getExcelCellText = (value) => {
+ if (value === null || value === undefined) return '';
+ if (typeof value === 'string') return value.trim();
+ if (typeof value === 'number') return String(value);
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
+ if (typeof value === 'object') {
+ if (typeof value.text === 'string') return value.text.trim();
+ if (Array.isArray(value.richText)) {
+ return value.richText.map((rt) => rt.text || '').join('').trim();
+ }
+ if (value.result !== undefined) return String(value.result).trim();
+ if (value.formula && value.result !== undefined) return String(value.result).trim();
+ if (typeof value.hyperlink === 'string' && typeof value.text === 'string') {
+ return value.text.trim();
+ }
+ }
+ return String(value).trim();
+};
+
+const normalizeHeader = (value) =>
+ normalizeSearch(value).replace(/[^A-Z0-9]/g, '');
+
+const normalizeDigits = (value) => String(value || '').replace(/\D/g, '');
+
+const normalizeNameKey = (value) =>
+ normalizeSearch(value).replace(/[^A-Z0-9]/g, '');
+
+const extractCupCodigo = (value) => {
+ const text = String(value || '').trim();
+ if (!text) return '';
+ const match = text.match(/\d{4,10}/);
+ if (match) return match[0];
+ const digits = normalizeDigits(text);
+ return digits || '';
+};
+
+const parseServicio = (value) => {
+ const raw = normalizeSearch(value);
+ if (raw.includes('AMBULANCIA')) {
+ return { tipo_autorizacion: 'brigadas_ambulancias_hospitalarios', tipo_servicio: 'ambulancias' };
+ }
+ if (raw.includes('HOSPITAL')) {
+ return { tipo_autorizacion: 'brigadas_ambulancias_hospitalarios', tipo_servicio: 'hospitalarios' };
+ }
+ if (raw.includes('BRIGADA')) {
+ return { tipo_autorizacion: 'brigadas_ambulancias_hospitalarios', tipo_servicio: 'brigadas' };
+ }
+ return { tipo_autorizacion: 'consultas_externas', tipo_servicio: null };
+};
+
+const splitNombrePaciente = (nombreCompleto) => {
+ const limpio = String(nombreCompleto || '').trim();
+ if (!limpio) {
+ return {
+ primer_nombre: null,
+ segundo_nombre: null,
+ primer_apellido: null,
+ segundo_apellido: null,
+ };
+ }
+
+ const partes = limpio.split(/\s+/);
+ if (partes.length === 1) {
+ return {
+ primer_nombre: partes[0],
+ segundo_nombre: null,
+ primer_apellido: null,
+ segundo_apellido: null,
+ };
+ }
+
+ if (partes.length === 2) {
+ return {
+ primer_nombre: partes[0],
+ segundo_nombre: null,
+ primer_apellido: partes[1],
+ segundo_apellido: null,
+ };
+ }
+
+ const apellidos = partes.slice(-2);
+ const nombres = partes.slice(0, -2);
+ return {
+ primer_nombre: nombres[0] || null,
+ segundo_nombre: nombres.slice(1).join(' ') || null,
+ primer_apellido: apellidos[0] || null,
+ segundo_apellido: apellidos[1] || null,
+ };
+};
+
+async function procesarExcelIps(inputFilePath) {
+ await ensureIpsConvenio();
+
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.readFile(inputFilePath);
+ const sheet = workbook.worksheets[0];
+
+ if (!sheet) {
+ throw new Error('No se encontro una hoja en el Excel de IPS');
+ }
+
+ const headerRow = sheet.getRow(1);
+ const headers = {};
+ headerRow.eachCell((cell, colNumber) => {
+ const base = normalizeHeader(getExcelCellText(cell.value));
+ if (!base) return;
+ let key = base;
+ if (headers[key]) {
+ let idx = 1;
+ while (headers[`${base}${idx}`]) {
+ idx += 1;
+ }
+ key = `${base}${idx}`;
+ }
+ headers[key] = colNumber;
+ });
+
+ const getValue = (row, key) => {
+ const col = headers[key];
+ if (!col) return '';
+ return getExcelCellText(row.getCell(col).value);
+ };
+
+ const resumen = {
+ total: 0,
+ insertados: 0,
+ actualizados: 0,
+ omitidos: 0,
+ desactivados: 0,
+ errores: [],
+ };
+
+ const nitKeys = new Set();
+ const codigoKeys = new Set();
+ const nombreKeys = new Set();
+
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+
+ for (let i = 2; i <= sheet.rowCount; i++) {
+ const row = sheet.getRow(i);
+ const nit = getValue(row, 'NIT');
+ const nombre = getValue(row, 'PRESTADOR');
+
+ if (!nit && !nombre) {
+ continue;
+ }
+
+ resumen.total += 1;
+
+ const direccion = getValue(row, 'DIRECCION');
+ const departamento = getValue(row, 'DEPARTAMENTO');
+ const municipio = getValue(row, 'MUNICIPIO');
+ const telefono = getValue(row, 'TELEFONO');
+ const codigoIps = getValue(row, 'CODIGOIPS');
+
+ const nitDigits = normalizeDigits(nit);
+ const codigoDigits = normalizeDigits(codigoIps);
+ const lookupKey = nitDigits || codigoDigits;
+ const nombreKey = normalizeNameKey(nombre);
+
+ if (nitDigits) nitKeys.add(nitDigits);
+ if (codigoDigits) codigoKeys.add(codigoDigits);
+ if (nombreKey) nombreKeys.add(nombreKey);
+
+ let existente = null;
+ if (lookupKey) {
+ const res = await client.query(
+ `
+ SELECT id_ips
+ FROM ips
+ WHERE regexp_replace(nit, '\\D', '', 'g') = $1
+ OR regexp_replace(codigo_ips, '\\D', '', 'g') = $1
+ LIMIT 1
+ `,
+ [lookupKey]
+ );
+ existente = res.rows[0] || null;
+ }
+
+ if (!existente && nombreKey) {
+ const res = await client.query(
+ `
+ SELECT id_ips
+ FROM ips
+ WHERE regexp_replace(
+ translate(UPPER(nombre_ips), 'ÁÉÍÓÚÜÑ', 'AEIOUUN'),
+ '[^A-Z0-9]',
+ '',
+ 'g'
+ ) = $1
+ LIMIT 1
+ `,
+ [nombreKey]
+ );
+ existente = res.rows[0] || null;
+ }
+
+ if (existente) {
+ await client.query(
+ `
+ UPDATE ips
+ SET nombre_ips = COALESCE($1, nombre_ips),
+ codigo_ips = COALESCE($2, codigo_ips),
+ direccion = COALESCE($3, direccion),
+ telefono = COALESCE($4, telefono),
+ departamento = COALESCE($5, departamento),
+ municipio = COALESCE($6, municipio),
+ tiene_convenio = true
+ WHERE id_ips = $7
+ `,
+ [
+ nombre || null,
+ codigoIps || null,
+ direccion || null,
+ telefono || null,
+ departamento || null,
+ municipio || null,
+ existente.id_ips,
+ ]
+ );
+ resumen.actualizados += 1;
+ } else {
+ await client.query(
+ `
+ INSERT INTO ips
+ (nit, nombre_ips, codigo_ips, direccion, telefono, departamento, municipio, tiene_convenio)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, true)
+ `,
+ [
+ nit || null,
+ nombre || null,
+ codigoIps || null,
+ direccion || null,
+ telefono || null,
+ departamento || null,
+ municipio || null,
+ ]
+ );
+ resumen.insertados += 1;
+ }
+ }
+
+ if (resumen.total > 0) {
+ const nitList = Array.from(nitKeys);
+ const codigoList = Array.from(codigoKeys);
+ const nombreList = Array.from(nombreKeys);
+
+ const desactRes = await client.query(
+ `
+ UPDATE ips
+ SET tiene_convenio = false
+ WHERE NOT (
+ (regexp_replace(nit, '\\D', '', 'g') <> '' AND regexp_replace(nit, '\\D', '', 'g') = ANY($1::text[]))
+ OR (regexp_replace(codigo_ips, '\\D', '', 'g') <> '' AND regexp_replace(codigo_ips, '\\D', '', 'g') = ANY($2::text[]))
+ OR (
+ regexp_replace(
+ translate(UPPER(nombre_ips), 'ÁÉÍÓÚÜÑ', 'AEIOUUN'),
+ '[^A-Z0-9]',
+ '',
+ 'g'
+ ) <> ''
+ AND regexp_replace(
+ translate(UPPER(nombre_ips), 'ÁÉÍÓÚÜÑ', 'AEIOUUN'),
+ '[^A-Z0-9]',
+ '',
+ 'g'
+ ) = ANY($3::text[])
+ )
+ )
+ AND tiene_convenio IS DISTINCT FROM false
+ `,
+ [nitList, codigoList, nombreList]
+ );
+
+ resumen.desactivados = desactRes.rowCount || 0;
+ }
+
+ await client.query('COMMIT');
+ } catch (error) {
+ await client.query('ROLLBACK');
+ throw error;
+ } finally {
+ client.release();
+ }
+
+ return {
+ ok: true,
+ mensaje: 'IPS cargadas correctamente',
+ ...resumen,
+ };
+}
+
+async function procesarExcelReps(inputFilePath) {
+ await ensureProfesionalRepsTable();
+
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.readFile(inputFilePath);
+ const sheet = workbook.worksheets[0];
+
+ if (!sheet) {
+ throw new Error('No se encontro una hoja en el Excel de REPS');
+ }
+
+ const headerRow = sheet.getRow(1);
+ const headers = {};
+ headerRow.eachCell((cell, colNumber) => {
+ headers[normalizeHeader(getExcelCellText(cell.value))] = colNumber;
+ });
+
+ const getValue = (row, key) => {
+ const col = headers[key];
+ if (!col) return '';
+ return getExcelCellText(row.getCell(col).value);
+ };
+
+ const resumen = {
+ total: 0,
+ insertados: 0,
+ actualizados: 0,
+ omitidos: 0,
+ desactivados: 0,
+ };
+
+ const pares = new Set();
+
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+
+ for (let i = 2; i <= sheet.rowCount; i++) {
+ const row = sheet.getRow(i);
+ const nit = getValue(row, 'NITSNIT');
+ const nombre = getValue(row, 'RAZONSOCIAL');
+ const codigoHabilitacion = getValue(row, 'CODIGOHABILITACION');
+
+ if (!nit && !nombre && !codigoHabilitacion) {
+ continue;
+ }
+
+ resumen.total += 1;
+
+ const direccion = getValue(row, 'DIRECCION');
+ const telefono = getValue(row, 'TELEFONO');
+ const departamento = getValue(row, 'DEPANOMBRE');
+ const municipio = getValue(row, 'MUNINOMBRE');
+
+ const nitKey = normalizeDigits(nit);
+ const codigoKey = normalizeDigits(codigoHabilitacion);
+ if (nitKey && codigoKey) {
+ pares.add(`${nitKey}||${codigoKey}`);
+ }
+
+ let existente = null;
+ if (nitKey && codigoKey) {
+ const res = await client.query(
+ `
+ SELECT id
+ FROM profesional_reps
+ WHERE regexp_replace(nit, '\\D', '', 'g') = $1
+ AND regexp_replace(codigo_habilitacion, '\\D', '', 'g') = $2
+ LIMIT 1
+ `,
+ [nitKey, codigoKey]
+ );
+ existente = res.rows[0] || null;
+ }
+
+ if (existente) {
+ await client.query(
+ `
+ UPDATE profesional_reps
+ SET nombre_profesional = COALESCE($1, nombre_profesional),
+ direccion = COALESCE($2, direccion),
+ telefono = COALESCE($3, telefono),
+ departamento = COALESCE($4, departamento),
+ municipio = COALESCE($5, municipio),
+ activo = true
+ WHERE id = $6
+ `,
+ [
+ nombre || null,
+ direccion || null,
+ telefono || null,
+ departamento || null,
+ municipio || null,
+ existente.id,
+ ]
+ );
+ resumen.actualizados += 1;
+ } else {
+ await client.query(
+ `
+ INSERT INTO profesional_reps
+ (nit, nombre_profesional, codigo_habilitacion, direccion, telefono, departamento, municipio, activo)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, true)
+ `,
+ [
+ nit || null,
+ nombre || null,
+ codigoHabilitacion || null,
+ direccion || null,
+ telefono || null,
+ departamento || null,
+ municipio || null,
+ ]
+ );
+ resumen.insertados += 1;
+ }
+ }
+
+ const paresLista = Array.from(pares);
+ if (resumen.total > 0 && paresLista.length > 0) {
+ const nitList = paresLista.map((p) => p.split('||')[0]);
+ const codigoList = paresLista.map((p) => p.split('||')[1]);
+
+ const desactRes = await client.query(
+ `
+ UPDATE profesional_reps
+ SET activo = false
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM UNNEST($1::text[], $2::text[]) AS x(nit, codigo)
+ WHERE regexp_replace(profesional_reps.nit, '\\D', '', 'g') = x.nit
+ AND regexp_replace(profesional_reps.codigo_habilitacion, '\\D', '', 'g') = x.codigo
+ )
+ AND activo IS DISTINCT FROM false
+ `,
+ [nitList, codigoList]
+ );
+
+ resumen.desactivados = desactRes.rowCount || 0;
+ }
+
+ await client.query('COMMIT');
+ } catch (error) {
+ await client.query('ROLLBACK');
+ throw error;
+ } finally {
+ client.release();
+ }
+
+ return {
+ ok: true,
+ mensaje: 'REPS cargados correctamente',
+ ...resumen,
+ };
+}
+
+async function procesarExcelAutorizacionesMasivas(inputFilePath, usuario) {
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.readFile(inputFilePath);
+ const sheet = workbook.worksheets[0];
+
+ if (!sheet) {
+ throw new Error('No se encontro una hoja en el Excel de autorizaciones');
+ }
+
+ const headerRow = sheet.getRow(1);
+ const headers = {};
+ headerRow.eachCell((cell, colNumber) => {
+ headers[normalizeHeader(getExcelCellText(cell.value))] = colNumber;
+ });
+
+ const getValue = (row, key) => {
+ const col = headers[key];
+ if (!col) return '';
+ return getExcelCellText(row.getCell(col).value);
+ };
+
+ const autorizanteRes = await pool.query(
+ `
+ SELECT numero_documento
+ FROM autorizante
+ WHERE UPPER(nombre) LIKE '%CRISTIAN%YARA%'
+ LIMIT 1
+ `
+ );
+
+ if (autorizanteRes.rows.length === 0) {
+ throw new Error('No se encontro el autorizante CRISTIAN YARA en la base de datos');
+ }
+
+ const autorizanteDefault = autorizanteRes.rows[0].numero_documento;
+ const resumen = {
+ total: 0,
+ creadas: 0,
+ omitidas: 0,
+ duplicados: 0,
+ sin_paciente: 0,
+ sin_cups: 0,
+ sin_ips: 0,
+ cups_no_cubiertos: 0,
+ ips_sin_convenio: 0,
+ errores: [],
+ };
+
+ const cupCache = new Map();
+ const pacienteCache = new Map();
+ const ipsCache = new Map();
+ const establecimientoCache = new Map();
+
+ const getEstablecimientoInfo = async (interno) => {
+ if (establecimientoCache.has(interno)) {
+ return establecimientoCache.get(interno);
+ }
+ const res = await client.query(
+ `
+ SELECT e.epc_departamento, e.epc_ciudad
+ FROM ingreso i
+ JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento
+ WHERE i.interno = $1
+ LIMIT 1
+ `,
+ [interno]
+ );
+ const info = res.rows[0]
+ ? {
+ departamento: res.rows[0].epc_departamento || null,
+ municipio: res.rows[0].epc_ciudad || null,
+ }
+ : null;
+ establecimientoCache.set(interno, info);
+ return info;
+ };
+
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+
+ for (let i = 2; i <= sheet.rowCount; i++) {
+ const row = sheet.getRow(i);
+ const cedula = getValue(row, 'CEDULA');
+ const procedimiento = getValue(row, 'PROCEDIMIENTOQUESEAUTORIZA');
+
+ if (!cedula && !procedimiento) {
+ continue;
+ }
+
+ resumen.total += 1;
+
+ if (!cedula) {
+ resumen.omitidas += 1;
+ resumen.sin_paciente += 1;
+ if (resumen.errores.length < 50) {
+ resumen.errores.push({ fila: i, error: 'Cedula vacia' });
+ }
+ continue;
+ }
+
+ let interno = pacienteCache.get(cedula);
+ if (!interno) {
+ const pacienteRes = await client.query(
+ `
+ SELECT interno
+ FROM paciente
+ WHERE interno = $1 OR numero_documento = $1
+ LIMIT 1
+ `,
+ [cedula]
+ );
+ interno = pacienteRes.rows[0]?.interno || null;
+ if (interno) {
+ pacienteCache.set(cedula, interno);
+ }
+ }
+
+ if (!interno) {
+ resumen.omitidas += 1;
+ resumen.sin_paciente += 1;
+ if (resumen.errores.length < 50) {
+ resumen.errores.push({ fila: i, error: `Paciente no encontrado: ${cedula}` });
+ }
+ continue;
+ }
+
+ const cupCodigo = extractCupCodigo(procedimiento);
+ if (!cupCodigo) {
+ resumen.omitidas += 1;
+ resumen.sin_cups += 1;
+ if (resumen.errores.length < 50) {
+ resumen.errores.push({ fila: i, error: 'No se pudo obtener CUPS' });
+ }
+ continue;
+ }
+
+ let cupCubierto = cupCache.get(cupCodigo);
+ if (cupCubierto === undefined) {
+ const cupRes = await client.query(
+ 'SELECT 1 FROM cups_cubiertos WHERE codigo = $1 AND activo = true',
+ [cupCodigo]
+ );
+ cupCubierto = cupRes.rows.length > 0;
+ cupCache.set(cupCodigo, cupCubierto);
+ }
+
+ if (!cupCubierto) {
+ resumen.cups_no_cubiertos += 1;
+ }
+
+ const nitIps = getValue(row, 'NITIPS');
+ const hospital = getValue(row, 'HOSPITALCLINICA');
+ const departamentoExcel =
+ getValue(row, 'DEPARTAMENTO') ||
+ getValue(row, 'BOGOTA') ||
+ getValue(row, 'BOGOTA1');
+ const municipioExcel =
+ getValue(row, 'MUNICIPIO') ||
+ getValue(row, 'BOGOTA1') ||
+ getValue(row, 'BOGOTA');
+
+ let departamento = departamentoExcel;
+ let municipio = municipioExcel;
+ if (!departamento || !municipio) {
+ const estInfo = await getEstablecimientoInfo(interno);
+ if (!departamento) {
+ departamento = estInfo?.departamento || '';
+ }
+ if (!municipio) {
+ municipio = estInfo?.municipio || '';
+ }
+ }
+
+ const ipsKey = normalizeDigits(nitIps) || normalizeSearch(hospital);
+ let ipsInfo = ipsCache.get(ipsKey);
+
+ if (!ipsInfo) {
+ const nitDigits = normalizeDigits(nitIps);
+ let ipsRes = null;
+ if (nitDigits) {
+ ipsRes = await client.query(
+ `
+ SELECT id_ips, nombre_ips, tiene_convenio, departamento, municipio
+ FROM ips
+ WHERE regexp_replace(nit, '\\D', '', 'g') = $1
+ OR regexp_replace(codigo_ips, '\\D', '', 'g') = $1
+ LIMIT 1
+ `,
+ [nitDigits]
+ );
+ }
+
+ if (!ipsRes || ipsRes.rows.length === 0) {
+ if (hospital) {
+ ipsRes = await client.query(
+ `
+ SELECT id_ips, nombre_ips, tiene_convenio, departamento, municipio
+ FROM ips
+ WHERE UPPER(nombre_ips) = $1
+ LIMIT 1
+ `,
+ [normalizeSearch(hospital)]
+ );
+ }
+ }
+
+ if (ipsRes && ipsRes.rows.length > 0) {
+ const ipsRow = ipsRes.rows[0];
+ if (
+ (departamento && !ipsRow.departamento) ||
+ (municipio && !ipsRow.municipio)
+ ) {
+ await client.query(
+ `
+ UPDATE ips
+ SET departamento = COALESCE(departamento, $1),
+ municipio = COALESCE(municipio, $2)
+ WHERE id_ips = $3
+ `,
+ [departamento || null, municipio || null, ipsRow.id_ips]
+ );
+ }
+ ipsInfo = {
+ id_ips: ipsRow.id_ips,
+ nombre_ips: ipsRow.nombre_ips,
+ tiene_convenio: ipsRow.tiene_convenio !== false,
+ departamento: ipsRow.departamento || departamento || null,
+ municipio: ipsRow.municipio || municipio || null,
+ };
+ } else if (nitIps || hospital) {
+ const insertRes = await client.query(
+ `
+ INSERT INTO ips
+ (nit, nombre_ips, codigo_ips, direccion, telefono, departamento, municipio, tiene_convenio)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, false)
+ RETURNING id_ips
+ `,
+ [
+ nitIps || null,
+ hospital || null,
+ nitIps || null,
+ null,
+ null,
+ departamento || null,
+ municipio || null,
+ ]
+ );
+ ipsInfo = {
+ id_ips: insertRes.rows[0].id_ips,
+ nombre_ips: hospital || null,
+ tiene_convenio: false,
+ departamento: departamento || null,
+ municipio: municipio || null,
+ };
+ }
+
+ if (ipsInfo) {
+ ipsCache.set(ipsKey, ipsInfo);
+ }
+ }
+
+ if (!ipsInfo) {
+ resumen.omitidas += 1;
+ resumen.sin_ips += 1;
+ if (resumen.errores.length < 50) {
+ resumen.errores.push({ fila: i, error: 'IPS no encontrada' });
+ }
+ continue;
+ }
+
+ if (!ipsInfo.tiene_convenio) {
+ resumen.ips_sin_convenio += 1;
+ }
+
+ const servicioExcel = getValue(row, 'SERVICIO');
+ const servicioInfo = parseServicio(servicioExcel);
+
+ const dupRes = await client.query(
+ `
+ SELECT 1
+ FROM autorizacion
+ WHERE interno = $1
+ AND id_ips = $2
+ AND cup_codigo = $3
+ AND tipo_autorizacion = $4
+ AND COALESCE(tipo_servicio, '') = $5
+ LIMIT 1
+ `,
+ [
+ interno,
+ ipsInfo.id_ips,
+ cupCodigo,
+ servicioInfo.tipo_autorizacion,
+ servicioInfo.tipo_servicio || '',
+ ]
+ );
+
+ if (dupRes.rows.length > 0) {
+ resumen.omitidas += 1;
+ resumen.duplicados += 1;
+ continue;
+ }
+
+ const observaciones = [];
+ const obs1 = getValue(row, 'OBSERVACIONES');
+ const obs2 = getValue(row, 'OBSERVACIONES1');
+ const solicitante = getValue(row, 'QUIENAUTORIZA');
+
+ if (obs1) observaciones.push(obs1);
+ if (obs2) observaciones.push(obs2);
+ if (solicitante) observaciones.push(`Solicitante: ${solicitante}`);
+ if (!ipsInfo.tiene_convenio) {
+ observaciones.push(`IPS sin convenio${hospital ? `: ${hospital}` : ''}`);
+ }
+ if (!cupCubierto) {
+ observaciones.push(`CUPS no cubierto: ${cupCodigo}`);
+ }
+ if (usuario?.nombre_completo) {
+ observaciones.push(`Cargado por: ${usuario.nombre_completo}`);
+ }
+
+ const observacionFinal = observaciones.filter(Boolean).join(' | ') || null;
+
+ await client.query('SAVEPOINT row_sp');
+ try {
+ const insertRes = await client.query(
+ `
+ INSERT INTO autorizacion
+ (interno, id_ips, numero_documento_autorizante, fecha_autorizacion, observacion, cup_codigo, tipo_autorizacion, tipo_servicio, estado_autorizacion)
+ VALUES ($1, $2, $3, COALESCE($4::date, current_date), $5, $6, $7, $8, 'pendiente')
+ RETURNING numero_autorizacion, fecha_autorizacion, version;
+ `,
+ [
+ interno,
+ ipsInfo.id_ips,
+ autorizanteDefault,
+ null,
+ observacionFinal,
+ cupCodigo,
+ servicioInfo.tipo_autorizacion,
+ servicioInfo.tipo_servicio,
+ ]
+ );
+
+ const nuevaAut = insertRes.rows[0];
+
+ await client.query(
+ `
+ INSERT INTO autorizacion_version
+ (numero_autorizacion, version, id_ips, numero_documento_autorizante, fecha_autorizacion, observacion, cup_codigo, tipo_autorizacion, tipo_servicio)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
+ `,
+ [
+ nuevaAut.numero_autorizacion,
+ nuevaAut.version || 1,
+ ipsInfo.id_ips,
+ autorizanteDefault,
+ nuevaAut.fecha_autorizacion || null,
+ observacionFinal,
+ cupCodigo,
+ servicioInfo.tipo_autorizacion,
+ servicioInfo.tipo_servicio,
+ ]
+ );
+
+ await client.query('RELEASE SAVEPOINT row_sp');
+ resumen.creadas += 1;
+ } catch (error) {
+ await client.query('ROLLBACK TO SAVEPOINT row_sp');
+ resumen.omitidas += 1;
+ if (resumen.errores.length < 50) {
+ resumen.errores.push({
+ fila: i,
+ error: error.message || 'Error guardando autorizacion',
+ });
+ }
+ }
+ }
+
+ await client.query('COMMIT');
+ } catch (error) {
+ await client.query('ROLLBACK');
+ throw error;
+ } finally {
+ client.release();
+ }
+
+ return {
+ ok: true,
+ mensaje: 'Carga masiva finalizada',
+ ...resumen,
+ };
+}
// Función auxiliar para generar PDFs
async function generarPdfAutorizacionYObtenerPath(
numero_autorizacion,
@@ -1073,8 +2184,8 @@ const crearJobZipHandler = async (req, res) => {
return res.status(202).json(sanitizeJob(job));
};
-app.post('/api/jobs/autorizacion-pdf', verificarToken, esAdministrador, crearJobPdfHandler);
-app.get('/api/jobs/autorizacion-pdf', verificarToken, esAdministrador, crearJobPdfHandler);
+app.post('/api/jobs/autorizacion-pdf', verificarToken, puedeDescargarPdfAutorizacion, crearJobPdfHandler);
+app.get('/api/jobs/autorizacion-pdf', verificarToken, puedeDescargarPdfAutorizacion, crearJobPdfHandler);
app.post('/api/jobs/autorizaciones-zip', verificarToken, esAdministrador, crearJobZipHandler);
app.get('/api/jobs/autorizaciones-zip', verificarToken, esAdministrador, crearJobZipHandler);
@@ -1108,6 +2219,43 @@ app.get('/api/cups-cubiertos', verificarToken, async (req, res) => {
}
});
+// CUPS completos con indicador de cobertura
+app.get('/api/cups', verificarToken, async (req, res) => {
+ const qRaw = req.query.q || '';
+ const q = String(qRaw).trim();
+ const qNormalized = normalizeSearch(q);
+ const limit = Math.min(Number(req.query.limit) || 100, 500);
+
+ try {
+ let sql = `
+ SELECT
+ r.codigo,
+ COALESCE(c.descripcion, r.descripcion) AS descripcion,
+ c.nivel,
+ c.especialidad,
+ (c.codigo IS NOT NULL AND c.activo = true) AS cubierto
+ FROM cups_referencia r
+ LEFT JOIN cups_cubiertos c ON c.codigo = r.codigo
+ WHERE 1 = 1
+ `;
+ const params = [];
+
+ if (q) {
+ params.push(`%${q}%`, `%${qNormalized}%`);
+ sql += ` AND (r.codigo ILIKE $1 OR r.descripcion ILIKE $1 OR c.descripcion ILIKE $1 OR c.descripcion_busqueda LIKE $2)`;
+ }
+
+ params.push(limit);
+ sql += ` ORDER BY r.codigo ASC LIMIT $${params.length};`;
+
+ const { rows } = await pool.query(sql, params);
+ res.json(rows);
+ } catch (error) {
+ console.error('Error en /api/cups:', error.message);
+ res.status(500).json({ error: 'Error consultando CUPS' });
+ }
+});
+
app.get('/api/pacientes', verificarToken, async (req, res) => {
const { numero_documento, interno, nombre } = req.query;
@@ -1220,7 +2368,7 @@ app.get('/api/ips-por-interno', verificarToken, async (req, res) => {
if (verTodas === '1' || verTodas === 'true' || verTodas === 'si') {
qIps = `
- SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio
+ SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio, tiene_convenio
FROM ips
ORDER BY nombre_ips;
`;
@@ -1228,19 +2376,21 @@ app.get('/api/ips-por-interno', verificarToken, async (req, res) => {
} else if (esBogota) {
// Para Bogotá buscamos por "BOGOTA" en municipio o departamento
qIps = `
- SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio
+ SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio, tiene_convenio
FROM ips
- WHERE municipio ILIKE $1
- OR departamento ILIKE $1
+ WHERE tiene_convenio = true
+ AND (municipio ILIKE $1
+ OR departamento ILIKE $1)
ORDER BY nombre_ips;
`;
params = ['%BOGOTA%'];
} else {
// Resto del país: por departamento
qIps = `
- SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio
+ SELECT id_ips, nombre_ips, direccion, telefono, departamento, municipio, tiene_convenio
FROM ips
- WHERE departamento ILIKE $1
+ WHERE tiene_convenio = true
+ AND departamento ILIKE $1
ORDER BY nombre_ips;
`;
params = [`%${departamento}%`];
@@ -1337,6 +2487,72 @@ app.patch('/api/autorizantes/:numero_documento/estado', verificarToken, esAdmini
}
});
+/**
+ * PATCH /api/autorizantes/:numero_documento
+ * Body: { telefono?: string | null, cargo?: string | null }
+ */
+app.patch('/api/autorizantes/:numero_documento', verificarToken, esAdministrador, async (req, res) => {
+ const { numero_documento } = req.params;
+ const { telefono, cargo, nombre } = req.body || {};
+
+ const numero = Number(numero_documento);
+ if (!Number.isFinite(numero)) {
+ return res.status(400).json({ error: 'numero_documento invalido' });
+ }
+
+ if (telefono === undefined && cargo === undefined && nombre === undefined) {
+ return res.status(400).json({ error: 'Debes enviar al menos un campo a actualizar' });
+ }
+
+ const updates = [];
+ const values = [];
+ let idx = 1;
+ let idRolActualizado = null;
+
+ if (nombre !== undefined) {
+ const nombreValue = String(nombre || '').trim();
+ if (!nombreValue) {
+ return res.status(400).json({ error: 'nombre no puede estar vacio' });
+ }
+ updates.push(`nombre = $${idx++}`);
+ values.push(nombreValue);
+ }
+
+ if (telefono !== undefined) {
+ const telValue = String(telefono || '').trim();
+ updates.push(`telefono = $${idx++}`);
+ values.push(telValue.length ? telValue : null);
+ }
+
+ if (cargo !== undefined) {
+ const cargoValue = String(cargo || '').trim();
+ updates.push(`cargo = $${idx++}`);
+ values.push(cargoValue.length ? cargoValue : null);
+ }
+
+ values.push(numero);
+
+ try {
+ const sql = `
+ UPDATE autorizante
+ SET ${updates.join(', ')}
+ WHERE numero_documento = $${idx}
+ RETURNING numero_documento, tipo_documento, nombre, telefono, cargo, activo;
+ `;
+ const { rows } = await pool.query(sql, values);
+ if (rows.length === 0) {
+ return res.status(404).json({ error: 'Autorizante no encontrado' });
+ }
+ return res.json({
+ mensaje: 'Autorizante actualizado correctamente',
+ autorizante: rows[0],
+ });
+ } catch (error) {
+ console.error('Error en /api/autorizantes/:numero_documento:', error);
+ return res.status(500).json({ error: 'Error actualizando autorizante' });
+ }
+});
+
/**
* POST /api/autorizantes
* Body JSON:
@@ -1467,18 +2683,6 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
// ... aquí va tu lógica de permisos por rol / sede ...
- const cupRes = await pool.query(
- 'SELECT codigo, descripcion, nivel, especialidad FROM cups_cubiertos WHERE codigo = $1 AND activo = true',
- [cupCodigo]
- );
-
- if (cupRes.rows.length === 0) {
- return res.status(400).json({
- error: 'El CUPS no está cubierto según la nota técnica.',
- });
- }
-
- const cup = cupRes.rows[0];
const client = await pool.connect();
try {
@@ -1497,7 +2701,7 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
tipo_servicio
)
VALUES ($1, $2, $3, COALESCE($4::date, current_date), $5, $6, $7, $8)
- RETURNING numero_autorizacion, fecha_autorizacion, cup_codigo, tipo_autorizacion, tipo_servicio, version;
+ RETURNING numero_autorizacion, fecha_autorizacion, cup_codigo, tipo_autorizacion, tipo_servicio, version, estado_autorizacion;
`;
const params = [
@@ -1535,11 +2739,17 @@ app.post('/api/autorizaciones', verificarToken, puedeGenerarAutorizaciones, asyn
await client.query('COMMIT');
+ const cupInfoRes = await pool.query(
+ 'SELECT descripcion, nivel, especialidad FROM cups_cubiertos WHERE codigo = $1 AND activo = true',
+ [cupCodigo]
+ );
+ const cupInfo = cupInfoRes.rows[0] || null;
+
res.status(201).json({
...nuevaAutorizacion,
- cup_descripcion: cup.descripcion,
- cup_nivel: cup.nivel,
- cup_especialidad: cup.especialidad,
+ cup_descripcion: cupInfo?.descripcion || null,
+ cup_nivel: cupInfo?.nivel || null,
+ cup_especialidad: cupInfo?.especialidad || null,
});
} catch (err) {
await client.query('ROLLBACK');
@@ -1656,18 +2866,6 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
tipoServicio = servicio;
}
- const cupRes = await pool.query(
- 'SELECT codigo, descripcion, nivel, especialidad FROM cups_cubiertos WHERE codigo = $1 AND activo = true',
- [cupCodigo]
- );
-
- if (cupRes.rows.length === 0) {
- return res.status(400).json({
- error: 'El CUPS no está cubierto según la nota técnica.',
- });
- }
-
- const cup = cupRes.rows[0];
const client = await pool.connect();
try {
@@ -1727,7 +2925,7 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
tipo_servicio = $7,
version = $8
WHERE numero_autorizacion = $9
- RETURNING numero_autorizacion, fecha_autorizacion, cup_codigo, tipo_autorizacion, tipo_servicio, version;
+ RETURNING numero_autorizacion, fecha_autorizacion, cup_codigo, tipo_autorizacion, tipo_servicio, version, estado_autorizacion;
`,
[
id_ips,
@@ -1765,11 +2963,17 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
await client.query('COMMIT');
+ const cupInfoRes = await pool.query(
+ 'SELECT descripcion, nivel, especialidad FROM cups_cubiertos WHERE codigo = $1 AND activo = true',
+ [cupCodigo]
+ );
+ const cupInfo = cupInfoRes.rows[0] || null;
+
res.json({
...actualizado,
- cup_descripcion: cup.descripcion,
- cup_nivel: cup.nivel,
- cup_especialidad: cup.especialidad,
+ cup_descripcion: cupInfo?.descripcion || null,
+ cup_nivel: cupInfo?.nivel || null,
+ cup_especialidad: cupInfo?.especialidad || null,
});
} catch (err) {
await client.query('ROLLBACK');
@@ -1780,11 +2984,57 @@ app.put('/api/autorizaciones/:numero_autorizacion', verificarToken, puedeGenerar
}
});
+/**
+ * PATCH /api/autorizaciones/:numero_autorizacion/estado
+ * Body: { estado_autorizacion: 'pendiente' | 'autorizado' | 'no_autorizado' }
+ * Solo administrador
+ */
+app.patch(
+ '/api/autorizaciones/:numero_autorizacion/estado',
+ verificarToken,
+ esAdministrador,
+ async (req, res) => {
+ const { numero_autorizacion } = req.params;
+ const estado = String(req.body?.estado_autorizacion || '')
+ .trim()
+ .toLowerCase();
+
+ const estadosPermitidos = ['pendiente', 'autorizado', 'no_autorizado'];
+ if (!estadosPermitidos.includes(estado)) {
+ return res.status(400).json({ error: 'estado_autorizacion invalido' });
+ }
+
+ try {
+ const { rows } = await pool.query(
+ `
+ UPDATE autorizacion
+ SET estado_autorizacion = $1
+ WHERE numero_autorizacion = $2
+ RETURNING numero_autorizacion, estado_autorizacion;
+ `,
+ [estado, numero_autorizacion]
+ );
+
+ if (rows.length === 0) {
+ return res.status(404).json({ error: 'Autorizacion no encontrada' });
+ }
+
+ return res.json({
+ mensaje: 'Estado de autorizacion actualizado',
+ autorizacion: rows[0],
+ });
+ } catch (error) {
+ console.error('Error actualizando estado de autorizacion:', error.message);
+ return res.status(500).json({ error: 'Error actualizando autorizacion' });
+ }
+ }
+);
+
/**
* GET /api/autorizaciones?interno=1007362
* Devuelve todas las autorizaciones del interno.
*/
-app.get('/api/autorizaciones', async (req, res) => {
+app.get('/api/autorizaciones', verificarToken, async (req, res) => {
const { interno } = req.query;
if (!interno) {
@@ -1792,6 +3042,7 @@ app.get('/api/autorizaciones', async (req, res) => {
}
try {
+ const esAdmin = req.usuario?.nombre_rol === 'administrador';
const sql = `
SELECT
a.numero_autorizacion,
@@ -1801,11 +3052,13 @@ app.get('/api/autorizaciones', async (req, res) => {
a.tipo_autorizacion,
a.tipo_servicio,
a.version,
+ COALESCE(a.estado_autorizacion, 'pendiente') AS estado_autorizacion,
a.id_ips,
a.numero_documento_autorizante,
ips.nombre_ips,
- ips.municipio,
- ips.departamento,
+ COALESCE(ips.municipio, e.epc_ciudad) AS municipio,
+ COALESCE(ips.departamento, e.epc_departamento) AS departamento,
+ COALESCE(ips.tiene_convenio, false) AS ips_tiene_convenio,
aut.nombre AS nombre_autorizante,
cc.descripcion AS cup_descripcion,
cc.nivel AS cup_nivel,
@@ -1813,11 +3066,16 @@ app.get('/api/autorizaciones', async (req, res) => {
FROM autorizacion a
JOIN ips
ON a.id_ips = ips.id_ips
+ LEFT JOIN ingreso i
+ ON a.interno = i.interno
+ LEFT JOIN establecimiento e
+ ON i.codigo_establecimiento = e.codigo_establecimiento
JOIN autorizante aut
ON a.numero_documento_autorizante = aut.numero_documento
LEFT JOIN cups_cubiertos cc
ON a.cup_codigo = cc.codigo
WHERE a.interno = $1
+ ${esAdmin ? '' : "AND COALESCE(a.estado_autorizacion, 'pendiente') IN ('pendiente', 'autorizado')"}
ORDER BY a.fecha_autorizacion DESC, a.numero_autorizacion DESC;
`;
@@ -1910,7 +3168,7 @@ app.get('/api/generar-excel-autorizaciones', async (req, res) => {
/**
* GET /api/generar-pdf-autorizacion?numero_autorizacion=2025-000123
*/
-app.get('/api/generar-pdf-autorizacion', verificarToken, esAdministrador, crearJobPdfHandler);
+app.get('/api/generar-pdf-autorizacion', verificarToken, puedeDescargarPdfAutorizacion, crearJobPdfHandler);
// ===========================
@@ -1973,7 +3231,8 @@ app.post('/api/auth/login', async (req, res) => {
username: usuario.username,
nombre_completo: usuario.nombre_completo,
nombre_rol: usuario.nombre_rol,
- id_rol: usuario.id_rol
+ id_rol: usuario.id_rol,
+ token_version: usuario.token_version ?? 1
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
@@ -2046,10 +3305,22 @@ app.post('/api/auth/register', verificarToken, esAdministrador, async (req, res)
const usuarioCreado = newUsuario[0];
// Si es administrativo_sede, asignar sedes
- if (id_rol == 2 && Array.isArray(sedes) && sedes.length > 0) {
- for (const sede of sedes) {
+ if (id_rol == 2) {
+ let sedesAsignadas = Array.isArray(sedes) ? sedes : [];
+ sedesAsignadas = sedesAsignadas
+ .map((s) => String(s || '').trim())
+ .filter((s) => s.length > 0);
+
+ if (sedesAsignadas.length === 0) {
+ const { rows: sedesRows } = await pool.query(
+ 'SELECT codigo_establecimiento FROM establecimiento'
+ );
+ sedesAsignadas = sedesRows.map((row) => row.codigo_establecimiento);
+ }
+
+ for (const sede of sedesAsignadas) {
await pool.query(
- 'INSERT INTO usuario_sede (id_usuario, codigo_establecimiento) VALUES ($1, $2)',
+ 'INSERT INTO usuario_sede (id_usuario, codigo_establecimiento) VALUES ($1, $2) ON CONFLICT (id_usuario, codigo_establecimiento) DO NOTHING',
[usuarioCreado.id_usuario, sede]
);
}
@@ -2124,6 +3395,7 @@ app.get('/api/usuarios', verificarToken, esAdministrador, async (req, res) => {
u.username,
u.email,
u.nombre_completo,
+ u.id_rol,
u.activo,
u.fecha_creacion,
u.ultimo_login,
@@ -2159,9 +3431,10 @@ app.patch('/api/usuarios/:id/estado', verificarToken, esAdministrador, async (re
try {
const sql = `
UPDATE usuario
- SET activo = $1
+ SET activo = $1,
+ token_version = token_version + 1
WHERE id_usuario = $2
- RETURNING id_usuario, username, email, nombre_completo, activo
+ RETURNING id_usuario, username, email, nombre_completo, activo, token_version
`;
const { rows } = await pool.query(sql, [activo, id]);
@@ -2177,6 +3450,156 @@ app.patch('/api/usuarios/:id/estado', verificarToken, esAdministrador, async (re
}
});
+/**
+ * PATCH /api/usuarios/:id
+ * Body: { username?: string, password?: string }
+ * Solo administrador
+ */
+app.patch('/api/usuarios/:id', verificarToken, esAdministrador, async (req, res) => {
+ const { id } = req.params;
+ const { username, password, email, nombre_completo, id_rol } = req.body || {};
+
+ const idUsuario = Number(id);
+ if (!Number.isFinite(idUsuario)) {
+ return res.status(400).json({ error: 'id_usuario invalido' });
+ }
+
+ if (
+ username === undefined &&
+ password === undefined &&
+ email === undefined &&
+ nombre_completo === undefined &&
+ id_rol === undefined
+ ) {
+ return res.status(400).json({ error: 'Debes enviar al menos un campo a actualizar' });
+ }
+
+ const updates = [];
+ const values = [];
+ let idx = 1;
+
+ if (username !== undefined) {
+ const usernameValue = String(username || '').trim();
+ if (!usernameValue) {
+ return res.status(400).json({ error: 'username no puede estar vacio' });
+ }
+
+ const checkSql = 'SELECT id_usuario FROM usuario WHERE username = $1 AND id_usuario <> $2';
+ const checkResult = await pool.query(checkSql, [usernameValue, idUsuario]);
+ if (checkResult.rows.length > 0) {
+ return res.status(400).json({ error: 'El usuario ya existe' });
+ }
+
+ updates.push(`username = $${idx++}`);
+ values.push(usernameValue);
+ }
+
+ if (password !== undefined) {
+ const passwordValue = String(password || '');
+ if (!passwordValue.trim()) {
+ return res.status(400).json({ error: 'password no puede estar vacia' });
+ }
+
+ const sqlValidar = 'SELECT validar_contrasena($1) as valida';
+ const { rows: validacion } = await pool.query(sqlValidar, [passwordValue]);
+ if (!validacion[0]?.valida) {
+ return res.status(400).json({
+ error: 'La contrasena debe tener al menos 8 caracteres y contener letras y numeros'
+ });
+ }
+
+ const passwordHash = await bcrypt.hash(passwordValue, 10);
+ updates.push(`password_hash = $${idx++}`);
+ values.push(passwordHash);
+ }
+
+ if (email !== undefined) {
+ const emailValue = String(email || '').trim();
+ if (!emailValue) {
+ return res.status(400).json({ error: 'email no puede estar vacio' });
+ }
+
+ const checkSql = 'SELECT id_usuario FROM usuario WHERE email = $1 AND id_usuario <> $2';
+ const checkResult = await pool.query(checkSql, [emailValue, idUsuario]);
+ if (checkResult.rows.length > 0) {
+ return res.status(400).json({ error: 'El email ya existe' });
+ }
+
+ updates.push(`email = $${idx++}`);
+ values.push(emailValue);
+ }
+
+ if (nombre_completo !== undefined) {
+ const nombreValue = String(nombre_completo || '').trim();
+ if (!nombreValue) {
+ return res.status(400).json({ error: 'nombre_completo no puede estar vacio' });
+ }
+ updates.push(`nombre_completo = $${idx++}`);
+ values.push(nombreValue);
+ }
+
+ if (id_rol !== undefined) {
+ const idRol = Number(id_rol);
+ if (!Number.isFinite(idRol)) {
+ return res.status(400).json({ error: 'id_rol invalido' });
+ }
+
+ const rolResult = await pool.query('SELECT id_rol FROM rol WHERE id_rol = $1', [idRol]);
+ if (rolResult.rows.length === 0) {
+ return res.status(400).json({ error: 'El rol no existe' });
+ }
+
+ updates.push(`id_rol = $${idx++}`);
+ values.push(idRol);
+ idRolActualizado = idRol;
+ }
+
+ updates.push('token_version = token_version + 1');
+ values.push(idUsuario);
+
+ try {
+ const sql = `
+ UPDATE usuario
+ SET ${updates.join(', ')}
+ WHERE id_usuario = $${idx}
+ RETURNING id_usuario, username, email, nombre_completo, id_rol, activo;
+ `;
+ const { rows } = await pool.query(sql, values);
+
+ if (rows.length === 0) {
+ return res.status(404).json({ error: 'Usuario no encontrado' });
+ }
+
+ if (idRolActualizado === 2) {
+ const { rows: existeSede } = await pool.query(
+ 'SELECT 1 FROM usuario_sede WHERE id_usuario = $1 LIMIT 1',
+ [idUsuario]
+ );
+
+ if (existeSede.length === 0) {
+ const { rows: sedesRows } = await pool.query(
+ 'SELECT codigo_establecimiento FROM establecimiento'
+ );
+
+ for (const sede of sedesRows) {
+ await pool.query(
+ 'INSERT INTO usuario_sede (id_usuario, codigo_establecimiento) VALUES ($1, $2) ON CONFLICT (id_usuario, codigo_establecimiento) DO NOTHING',
+ [idUsuario, sede.codigo_establecimiento]
+ );
+ }
+ }
+ }
+
+ return res.json({
+ mensaje: 'Usuario actualizado correctamente',
+ usuario: rows[0]
+ });
+ } catch (error) {
+ console.error('Error actualizando usuario:', error);
+ return res.status(500).json({ error: 'Error actualizando usuario' });
+ }
+});
+
/**
* GET /api/roles
* Lista los roles disponibles (solo administrador)
@@ -2199,9 +3622,9 @@ app.get('/api/roles', verificarToken, esAdministrador, async (req, res) => {
/**
* GET /api/autorizaciones-por-fecha
* Query params: fecha_inicio, fecha_fin
- * Solo para administradores
+ * Administrador ve todo; otros solo autorizadas
*/
-app.get('/api/autorizaciones-por-fecha', verificarToken, esAdministrador, async (req, res) => {
+app.get('/api/autorizaciones-por-fecha', verificarToken, async (req, res) => {
const { fecha_inicio, fecha_fin } = req.query;
if (!fecha_inicio || !fecha_fin) {
@@ -2209,6 +3632,7 @@ app.get('/api/autorizaciones-por-fecha', verificarToken, esAdministrador, async
}
try {
+ const esAdmin = req.usuario?.nombre_rol === 'administrador';
const limit = Math.min(Number(req.query.limit) || 500, 2000);
const offset = Math.max(Number(req.query.offset) || 0, 0);
@@ -2221,17 +3645,20 @@ app.get('/api/autorizaciones-por-fecha', verificarToken, esAdministrador, async
a.tipo_autorizacion,
a.tipo_servicio,
a.version,
+ COALESCE(a.estado_autorizacion, 'pendiente') AS estado_autorizacion,
p.interno,
- p.primer_nombre || ' ' || COALESCE(p.segundo_nombre, '') || ' ' ||
- p.primer_apellido || ' ' || COALESCE(p.segundo_apellido, '') AS nombre_paciente,
- ips.nombre_ips,
- ips.municipio,
- ips.departamento,
- aut.nombre AS nombre_autorizante,
- e.nombre_establecimiento,
- cc.descripcion AS cup_descripcion,
- cc.nivel AS cup_nivel,
- cc.especialidad AS cup_especialidad
+ p.primer_nombre || ' ' || COALESCE(p.segundo_nombre, '') || ' ' ||
+ p.primer_apellido || ' ' || COALESCE(p.segundo_apellido, '') AS nombre_paciente,
+ ips.nombre_ips,
+ COALESCE(ips.municipio, e.epc_ciudad) AS municipio,
+ COALESCE(ips.departamento, e.epc_departamento) AS departamento,
+ COALESCE(ips.tiene_convenio, false) AS ips_tiene_convenio,
+ aut.nombre AS nombre_autorizante,
+ e.nombre_establecimiento,
+ CASE WHEN cc.codigo IS NULL THEN false ELSE true END AS cup_cubierto,
+ cc.descripcion AS cup_descripcion,
+ cc.nivel AS cup_nivel,
+ cc.especialidad AS cup_especialidad
FROM autorizacion a
JOIN paciente p ON a.interno = p.interno
JOIN ips ON a.id_ips = ips.id_ips
@@ -2240,6 +3667,7 @@ app.get('/api/autorizaciones-por-fecha', verificarToken, esAdministrador, async
JOIN establecimiento e ON i.codigo_establecimiento = e.codigo_establecimiento
LEFT JOIN cups_cubiertos cc ON a.cup_codigo = cc.codigo
WHERE a.fecha_autorizacion BETWEEN $1 AND $2
+ ${esAdmin ? '' : "AND COALESCE(a.estado_autorizacion, 'pendiente') IN ('pendiente', 'autorizado')"}
ORDER BY a.fecha_autorizacion DESC, a.numero_autorizacion DESC
LIMIT $3 OFFSET $4
`;
@@ -2253,6 +3681,125 @@ app.get('/api/autorizaciones-por-fecha', verificarToken, esAdministrador, async
}
});
+/**
+ * GET /api/autorizaciones-estadisticas?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
+ * Devuelve conteos por estado y un histograma diario en el rango solicitado.
+ */
+app.get('/api/autorizaciones-estadisticas', verificarToken, esAdministrador, async (req, res) => {
+ const hoy = new Date();
+ const primerDiaMes = new Date(hoy.getFullYear(), hoy.getMonth(), 1);
+
+ const rawInicio = req.query.fecha_inicio;
+ const rawFin = req.query.fecha_fin;
+
+ const inicio = rawInicio ? new Date(String(rawInicio)) : primerDiaMes;
+ const fin = rawFin ? new Date(String(rawFin)) : hoy;
+
+ if (Number.isNaN(inicio.getTime()) || Number.isNaN(fin.getTime())) {
+ return res.status(400).json({ error: 'fecha_inicio o fecha_fin invalida' });
+ }
+
+ if (inicio > fin) {
+ return res.status(400).json({ error: 'fecha_inicio no puede ser mayor que fecha_fin' });
+ }
+
+ const inicioStr = inicio.toISOString().slice(0, 10);
+ const finStr = fin.toISOString().slice(0, 10);
+
+ try {
+ const resumenSql = `
+ SELECT
+ COUNT(*) FILTER (WHERE COALESCE(estado_autorizacion, 'pendiente') = 'autorizado') AS autorizadas,
+ COUNT(*) FILTER (WHERE COALESCE(estado_autorizacion, 'pendiente') = 'no_autorizado') AS no_autorizadas,
+ COUNT(*) FILTER (WHERE COALESCE(estado_autorizacion, 'pendiente') = 'pendiente') AS pendientes,
+ COUNT(*) AS total
+ FROM autorizacion
+ WHERE fecha_autorizacion BETWEEN $1::date AND $2::date;
+ `;
+
+ const diasSql = `
+ WITH dias AS (
+ SELECT generate_series($1::date, $2::date, interval '1 day')::date AS fecha
+ )
+ SELECT
+ d.fecha,
+ COUNT(a.numero_autorizacion) AS total,
+ COUNT(*) FILTER (WHERE COALESCE(a.estado_autorizacion, 'pendiente') = 'autorizado') AS autorizadas,
+ COUNT(*) FILTER (WHERE COALESCE(a.estado_autorizacion, 'pendiente') = 'no_autorizado') AS no_autorizadas,
+ COUNT(*) FILTER (WHERE COALESCE(a.estado_autorizacion, 'pendiente') = 'pendiente') AS pendientes
+ FROM dias d
+ LEFT JOIN autorizacion a
+ ON a.fecha_autorizacion::date = d.fecha
+ GROUP BY d.fecha
+ ORDER BY d.fecha;
+ `;
+
+ const resumenRes = await pool.query(resumenSql, [inicioStr, finStr]);
+ const diasRes = await pool.query(diasSql, [inicioStr, finStr]);
+
+ return res.json({
+ rango: { inicio: inicioStr, fin: finStr },
+ resumen: resumenRes.rows[0] || {
+ autorizadas: 0,
+ no_autorizadas: 0,
+ pendientes: 0,
+ total: 0,
+ },
+ dias: diasRes.rows || [],
+ });
+ } catch (error) {
+ console.error('Error en /api/autorizaciones-estadisticas:', error.message);
+ return res.status(500).json({ error: 'Error consultando estadisticas' });
+ }
+});
+
+/**
+ * PATCH /api/autorizaciones/estado-masivo
+ * Body: { fecha_inicio, fecha_fin, estado_autorizacion, solo_pendientes? }
+ * Solo administrador
+ */
+app.patch('/api/autorizaciones/estado-masivo', verificarToken, esAdministrador, async (req, res) => {
+ const fecha_inicio = req.body?.fecha_inicio || req.query?.fecha_inicio;
+ const fecha_fin = req.body?.fecha_fin || req.query?.fecha_fin;
+ const estado = String(req.body?.estado_autorizacion || '')
+ .trim()
+ .toLowerCase();
+ const soloPendientes = req.body?.solo_pendientes === true;
+
+ if (!fecha_inicio || !fecha_fin) {
+ return res.status(400).json({ error: 'fecha_inicio y fecha_fin son requeridos' });
+ }
+
+ const estadosPermitidos = ['pendiente', 'autorizado', 'no_autorizado'];
+ if (!estadosPermitidos.includes(estado)) {
+ return res.status(400).json({ error: 'estado_autorizacion invalido' });
+ }
+
+ try {
+ const params = [estado, fecha_inicio, fecha_fin];
+ let sql = `
+ UPDATE autorizacion
+ SET estado_autorizacion = $1
+ WHERE fecha_autorizacion BETWEEN $2::date AND $3::date
+ `;
+
+ if (soloPendientes) {
+ sql += ` AND COALESCE(estado_autorizacion, 'pendiente') = 'pendiente'`;
+ }
+
+ sql += ' RETURNING numero_autorizacion;';
+
+ const result = await pool.query(sql, params);
+ return res.json({
+ mensaje: 'Estados actualizados',
+ actualizados: result.rowCount || 0,
+ });
+ } catch (error) {
+ console.error('Error en estado masivo:', error.message);
+ return res.status(500).json({ error: 'Error actualizando estados' });
+ }
+});
+
/**
* GET /api/autorizaciones-por-fecha/zip
* Query params: fecha_inicio, fecha_fin
@@ -2269,6 +3816,22 @@ ensureAutorizacionVersionTables().catch((error) => {
console.error('Error inicializando tablas de versiones:', error.message);
});
+ensureIpsConvenio().catch((error) => {
+ console.error('Error inicializando convenio de IPS:', error.message);
+});
+
+ensureProfesionalRepsTable().catch((error) => {
+ console.error('Error inicializando tabla profesional_reps:', error.message);
+});
+
+ensureAutorizacionEstado().catch((error) => {
+ console.error('Error inicializando estado de autorizaciones:', error.message);
+});
+
+ensureUsuarioTokenVersion().catch((error) => {
+ console.error('Error inicializando token_version de usuario:', error.message);
+});
+
ensureAdminFromEnv().catch((error) => {
console.warn('Error asegurando admin desde .env:', error.message);
});
diff --git a/saludut-inpec/angular.json b/saludut-inpec/angular.json
index ddbddb4..a5802cf 100644
--- a/saludut-inpec/angular.json
+++ b/saludut-inpec/angular.json
@@ -55,6 +55,9 @@
},
"serve": {
"builder": "@angular/build:dev-server",
+ "options": {
+ "proxyConfig": "proxy.conf.json"
+ },
"configurations": {
"production": {
"buildTarget": "saludut-inpec:build:production"
diff --git a/saludut-inpec/proxy.conf.json b/saludut-inpec/proxy.conf.json
new file mode 100644
index 0000000..527d410
--- /dev/null
+++ b/saludut-inpec/proxy.conf.json
@@ -0,0 +1,7 @@
+{
+ "/api": {
+ "target": "http://localhost:3000",
+ "secure": false,
+ "changeOrigin": true
+ }
+}
diff --git a/saludut-inpec/src/app/app.routes.ts b/saludut-inpec/src/app/app.routes.ts
index aa69c70..bcd61f0 100644
--- a/saludut-inpec/src/app/app.routes.ts
+++ b/saludut-inpec/src/app/app.routes.ts
@@ -7,6 +7,10 @@ import { AutorizacionesPorFechaComponent } from './components/autorizaciones-por
import { AutorizacionesComponent } from './components/autorizaciones/autorizaciones';
import { UsuariosComponent } from './components/usuarios/usuarios';
import { CargarCupsComponent } from './components/cargar-cups/cargar-cups';
+import { EstadisticasAutorizacionesComponent } from './components/estadisticas-autorizaciones/estadisticas-autorizaciones';
+import { CargarAutorizacionesMasivasComponent } from './components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas';
+import { CargarIpsRepsComponent } from './components/cargar-ips-reps/cargar-ips-reps';
+import { CargarPacientesComponent } from './components/cargar-pacientes/cargar-pacientes';
export const routes: Routes = [
@@ -30,6 +34,12 @@ export const routes: Routes = [
{
path: 'autorizaciones-por-fecha',
component: AutorizacionesPorFechaComponent,
+ canActivate: [AuthGuard],
+ },
+
+ {
+ path: 'estadisticas-autorizaciones',
+ component: EstadisticasAutorizacionesComponent,
canActivate: [AuthGuard, AdminGuard],
},
@@ -45,6 +55,24 @@ export const routes: Routes = [
canActivate: [AuthGuard, AdminGuard],
},
+ {
+ path: 'cargar-pacientes',
+ component: CargarPacientesComponent,
+ canActivate: [AuthGuard, AdminGuard],
+ },
+
+ {
+ path: 'cargar-autorizaciones-masivas',
+ component: CargarAutorizacionesMasivasComponent,
+ canActivate: [AuthGuard],
+ },
+
+ {
+ path: 'cargar-ips-reps',
+ component: CargarIpsRepsComponent,
+ canActivate: [AuthGuard, AdminGuard],
+ },
+
// cualquier cosa rara → dashboard
{ path: '**', redirectTo: 'dashboard' },
diff --git a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.css b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.css
index 3e1fa8b..2fcd612 100644
--- a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.css
+++ b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.css
@@ -12,15 +12,15 @@
}
.alert-error {
- background: #fef2f2;
- border-left: 4px solid #dc2626;
- color: #dc2626;
+ background: var(--color-permission-no-bg);
+ border-left: 4px solid var(--color-error);
+ color: var(--color-error);
}
.alert-success {
- background: #f0fdf4;
- border-left: 4px solid #16a34a;
- color: #16a34a;
+ background: var(--color-permission-yes-bg);
+ border-left: 4px solid var(--color-success);
+ color: var(--color-success);
}
.alert-icon {
@@ -62,7 +62,7 @@
.filtros-card h2 {
margin: 0 0 20px 0;
- color: #222222;
+ color: var(--color-text-main);
font-size: 1.3rem;
font-weight: 600;
}
@@ -83,18 +83,19 @@
display: block;
margin-bottom: 6px;
font-weight: 600;
- color: #222222;
+ color: var(--color-text-main);
font-size: 0.9rem;
}
.form-group input {
width: 100%;
padding: 12px 16px;
- border: 2px solid #e5e7eb;
+ border: 2px solid var(--color-input-border);
border-radius: 8px;
font-size: 0.95rem;
transition: all 0.2s ease;
- background-color: #ffffff;
+ background-color: var(--color-input-bg);
+ color: var(--color-text-main);
}
.form-group input:focus {
@@ -104,12 +105,12 @@
}
.form-group input.error {
- border-color: #dc2626;
- background-color: #fef2f2;
+ border-color: var(--color-error);
+ background-color: var(--color-permission-no-bg);
}
.error-message {
- color: #dc2626;
+ color: var(--color-error);
font-size: 0.8rem;
margin-top: 4px;
font-weight: 500;
@@ -167,7 +168,7 @@
.resultados-header h2 {
margin: 0;
- color: #222222;
+ color: var(--color-text-main);
font-size: 1.3rem;
font-weight: 600;
}
@@ -175,13 +176,75 @@
.resultados-actions {
display: flex;
gap: 12px;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.resultados-filtro-numero {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: auto;
+}
+
+.resultados-filtro-numero label {
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+ font-weight: 600;
+}
+
+.resultados-filtro-numero input {
+ min-width: 200px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1px solid var(--color-input-border);
+ font-size: 0.9rem;
+ color: var(--color-text-main);
+ background: var(--color-input-bg);
+}
+
+.resultados-filtro-numero input:focus {
+ outline: none;
+ border-color: #1976d2;
+ box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
+}
+
+.estado-masivo {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ border-radius: 8px;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+}
+
+.estado-masivo label {
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+ font-weight: 600;
+}
+
+.estado-masivo select {
+ min-width: 160px;
+ padding: 8px 10px;
+ border-radius: 6px;
+ border: 1px solid var(--color-input-border);
+ background: var(--color-input-bg);
+ font-size: 0.85rem;
+ color: var(--color-text-main);
+}
+
+.estado-masivo button {
+ min-width: 180px;
}
/* Table */
.table-container {
overflow-x: auto;
border-radius: 8px;
- border: 1px solid #e5e7eb;
+ border: 1px solid var(--color-border);
}
.autorizaciones-table {
@@ -197,23 +260,23 @@
}
.autorizaciones-table tr:hover td {
- background: #f8fafc;
+ background: var(--color-table-hover);
}
.autorizaciones-table tr.even-row td {
- background: #fafbfc;
+ background: var(--color-table-row-alt);
}
.numero-autorizacion {
font-weight: 600;
- color: #1976d2;
+ color: var(--color-primary);
font-family: "Courier New", monospace;
}
.fecha {
white-space: nowrap;
font-size: 0.85rem;
- color: #666666;
+ color: var(--color-text-muted);
}
.interno {
@@ -227,6 +290,29 @@
text-align: center;
}
+.cup-cobertura {
+ text-align: center;
+}
+
+.cobertura-badge {
+ display: inline-block;
+ padding: 6px 10px;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.cobertura-ok {
+ background: var(--color-permission-yes-bg);
+ color: var(--color-success);
+}
+
+.cobertura-no {
+ background: var(--color-permission-no-bg);
+ color: var(--color-error);
+}
+
.cup-nivel {
font-weight: 600;
text-align: center;
@@ -235,15 +321,58 @@
.nombre-paciente,
.ips,
.autorizante,
-.establecimiento {
+.establecimiento,
+.estado {
max-width: 180px;
line-height: 1.3;
}
+.ips-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.ips-convenio {
+ align-self: flex-start;
+ font-size: 0.7rem;
+ letter-spacing: 0.4px;
+ text-transform: uppercase;
+}
+
+.estado select {
+ width: 100%;
+ min-width: 140px;
+ padding: 10px 12px;
+ border-radius: 6px;
+ border: 1px solid var(--color-input-border);
+ background: var(--color-input-bg);
+ font-size: 0.95rem;
+ color: var(--color-text-main);
+}
+
+.estado-label {
+ display: inline-block;
+ padding: 6px 10px;
+ border-radius: 999px;
+ background: var(--color-surface-muted);
+ color: var(--color-text-muted);
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: capitalize;
+}
+
+.mini-status {
+ display: block;
+ margin-top: 6px;
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+}
+
.municipio,
.departamento {
font-size: 0.85rem;
- color: #666666;
+ color: var(--color-text-muted);
}
.acciones {
@@ -251,6 +380,13 @@
text-align: center;
}
+.no-result {
+ text-align: center;
+ padding: 24px 16px;
+ color: var(--color-text-muted);
+ font-weight: 600;
+}
+
.btn-descargar:hover:not(:disabled) {
transform: translateY(-1px);
}
@@ -269,14 +405,14 @@
.empty-state h3 {
margin: 0 0 8px 0;
- color: #222222;
+ color: var(--color-text-main);
font-size: 1.3rem;
font-weight: 600;
}
.empty-state p {
margin: 0;
- color: #666666;
+ color: var(--color-text-muted);
font-size: 1rem;
line-height: 1.5;
}
@@ -338,10 +474,24 @@
align-items: stretch;
}
+ .resultados-filtro-numero {
+ width: 100%;
+ margin-left: 0;
+ }
+
+ .resultados-filtro-numero input {
+ width: 100%;
+ }
+
.resultados-actions {
justify-content: center;
}
+ .estado-masivo {
+ width: 100%;
+ justify-content: space-between;
+ }
+
.autorizaciones-table th,
.autorizaciones-table td {
padding: 8px 12px;
@@ -350,7 +500,8 @@
.nombre-paciente,
.ips,
.autorizante,
- .establecimiento {
+ .establecimiento,
+ .estado {
max-width: 120px;
}
}
@@ -361,10 +512,23 @@
}
.btn-exportar,
- .btn-descargar-todos {
+ .btn-descargar-todos,
+ .estado-masivo,
+ .estado-masivo button {
justify-content: center;
}
+ .estado-masivo {
+ width: 100%;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .estado-masivo select,
+ .estado-masivo button {
+ width: 100%;
+ }
+
.empty-state {
padding: 40px 16px;
}
diff --git a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html
index e59cb6b..797645b 100644
--- a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html
+++ b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.html
@@ -3,10 +3,15 @@
+ La columna de numero de autorizacion se ignora. Todas las autorizaciones quedan en estado pendiente. +
+ +| Fila | +Error | +
|---|---|
| {{ e.fila || '-' }} | +{{ e.error || 'Error en fila' }} | +
+ Sube el archivo ips.xlsx para actualizar convenios y datos de IPS. +
+ +| Fila | +Error | +
|---|---|
| {{ e.fila || '-' }} | +{{ e.error || 'Error en fila' }} | +
+ Sube el archivo reps.xlsx para actualizar profesionales REPS. +
+ ++ El sistema procesa paciente, ingreso y establecimiento. Usa la plantilla oficial de pacientes. +
+ +- {{ cargandoExcel - ? 'Cargando archivo de pacientes...' - : 'Subir archivo Excel con datos de pacientes' }} + Subir archivo Excel con datos de pacientes
Subir plantilla Excel para crear autorizaciones pendientes
+Subir Excel de IPS y profesionales REPS
+Consultar y descargar autorizaciones por rango de fechas
+Resumen mensual con estados y volumen diario
- {{ estadoCargaExcel }} -
@@ -171,20 +234,35 @@{{ data.resumen.autorizadas }}
+{{ data.resumen.no_autorizadas }}
+{{ data.resumen.pendientes }}
+{{ data.resumen.total }}
+| Numero | +Interno | +Paciente | +CUPS | +Estado | +
|---|---|---|---|---|
| {{ aut.numero_autorizacion }} | +{{ aut.interno }} | +{{ aut.nombre_paciente }} | +{{ aut.cup_codigo }} | ++ + {{ getEstadoAutorizacionLabel(aut.estado_autorizacion) }} + + | +