cambios finales

This commit is contained in:
Jhonathan Guevara 2025-12-27 17:10:36 -05:00
parent d611cec040
commit 5844cfe523
Signed by: jhonathan_guevara
GPG Key ID: 619239F12DCBE55B
46 changed files with 5706 additions and 575 deletions

View File

@ -1,47 +1,47 @@
-- INSERTS TABLA ESTABLECIMIENTO -- 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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;

View File

@ -1,6 +1,5 @@
-- UPSERTS TABLA INGRESO -- 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 ('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 ('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 ('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; 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;

BIN
backend/src/ips.xlsx Normal file

Binary file not shown.

View File

@ -1,7 +1,6 @@
-- UPSERTS TABLA PACIENTE -- UPSERTS TABLA PACIENTE
UPDATE paciente SET activo = false; 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 ('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 ('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 ('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; 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;

Binary file not shown.

BIN
backend/src/plantilla.xlsx Normal file

Binary file not shown.

BIN
backend/src/reps.xlsx Normal file

Binary file not shown.

View File

@ -49,7 +49,8 @@ CREATE TABLE IF NOT EXISTS ips (
direccion text, direccion text,
telefono text, telefono text,
departamento text, departamento text,
municipio text municipio text,
tiene_convenio boolean DEFAULT true
); );
-- ===== Ingreso ===== -- ===== Ingreso =====
@ -91,7 +92,8 @@ CREATE TABLE IF NOT EXISTS usuario (
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ultimo_login TIMESTAMP, ultimo_login TIMESTAMP,
intentos_fallidos INTEGER DEFAULT 0, 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 ( CREATE TABLE IF NOT EXISTS usuario_sede (
@ -163,9 +165,23 @@ CREATE TABLE IF NOT EXISTS autorizacion (
cup_codigo varchar(20), cup_codigo varchar(20),
tipo_autorizacion varchar(50) NOT NULL DEFAULT 'consultas_externas', tipo_autorizacion varchar(50) NOT NULL DEFAULT 'consultas_externas',
tipo_servicio varchar(50), tipo_servicio varchar(50),
estado_autorizacion varchar(20) NOT NULL DEFAULT 'pendiente',
version integer NOT NULL DEFAULT 1 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) INSERT INTO consecutivo_autorizacion (id, codigo)
VALUES (1, COALESCE((SELECT MAX(numero_autorizacion) FROM autorizacion), 'UTUSCPGB00')) VALUES (1, COALESCE((SELECT MAX(numero_autorizacion) FROM autorizacion), 'UTUSCPGB00'))
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
@ -201,6 +217,19 @@ CREATE TABLE IF NOT EXISTS autorizacion_version (
fecha_version TIMESTAMP NOT NULL DEFAULT NOW() 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 CREATE UNIQUE INDEX IF NOT EXISTS autorizacion_version_unique
ON autorizacion_version (numero_autorizacion, version); ON autorizacion_version (numero_autorizacion, version);

File diff suppressed because it is too large Load Diff

View File

@ -55,6 +55,9 @@
}, },
"serve": { "serve": {
"builder": "@angular/build:dev-server", "builder": "@angular/build:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
},
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "saludut-inpec:build:production" "buildTarget": "saludut-inpec:build:production"

View File

@ -0,0 +1,7 @@
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"changeOrigin": true
}
}

View File

@ -7,6 +7,10 @@ import { AutorizacionesPorFechaComponent } from './components/autorizaciones-por
import { AutorizacionesComponent } from './components/autorizaciones/autorizaciones'; import { AutorizacionesComponent } from './components/autorizaciones/autorizaciones';
import { UsuariosComponent } from './components/usuarios/usuarios'; import { UsuariosComponent } from './components/usuarios/usuarios';
import { CargarCupsComponent } from './components/cargar-cups/cargar-cups'; 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 = [ export const routes: Routes = [
@ -30,6 +34,12 @@ export const routes: Routes = [
{ {
path: 'autorizaciones-por-fecha', path: 'autorizaciones-por-fecha',
component: AutorizacionesPorFechaComponent, component: AutorizacionesPorFechaComponent,
canActivate: [AuthGuard],
},
{
path: 'estadisticas-autorizaciones',
component: EstadisticasAutorizacionesComponent,
canActivate: [AuthGuard, AdminGuard], canActivate: [AuthGuard, AdminGuard],
}, },
@ -45,6 +55,24 @@ export const routes: Routes = [
canActivate: [AuthGuard, AdminGuard], 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 // cualquier cosa rara → dashboard
{ path: '**', redirectTo: 'dashboard' }, { path: '**', redirectTo: 'dashboard' },

View File

@ -12,15 +12,15 @@
} }
.alert-error { .alert-error {
background: #fef2f2; background: var(--color-permission-no-bg);
border-left: 4px solid #dc2626; border-left: 4px solid var(--color-error);
color: #dc2626; color: var(--color-error);
} }
.alert-success { .alert-success {
background: #f0fdf4; background: var(--color-permission-yes-bg);
border-left: 4px solid #16a34a; border-left: 4px solid var(--color-success);
color: #16a34a; color: var(--color-success);
} }
.alert-icon { .alert-icon {
@ -62,7 +62,7 @@
.filtros-card h2 { .filtros-card h2 {
margin: 0 0 20px 0; margin: 0 0 20px 0;
color: #222222; color: var(--color-text-main);
font-size: 1.3rem; font-size: 1.3rem;
font-weight: 600; font-weight: 600;
} }
@ -83,18 +83,19 @@
display: block; display: block;
margin-bottom: 6px; margin-bottom: 6px;
font-weight: 600; font-weight: 600;
color: #222222; color: var(--color-text-main);
font-size: 0.9rem; font-size: 0.9rem;
} }
.form-group input { .form-group input {
width: 100%; width: 100%;
padding: 12px 16px; padding: 12px 16px;
border: 2px solid #e5e7eb; border: 2px solid var(--color-input-border);
border-radius: 8px; border-radius: 8px;
font-size: 0.95rem; font-size: 0.95rem;
transition: all 0.2s ease; transition: all 0.2s ease;
background-color: #ffffff; background-color: var(--color-input-bg);
color: var(--color-text-main);
} }
.form-group input:focus { .form-group input:focus {
@ -104,12 +105,12 @@
} }
.form-group input.error { .form-group input.error {
border-color: #dc2626; border-color: var(--color-error);
background-color: #fef2f2; background-color: var(--color-permission-no-bg);
} }
.error-message { .error-message {
color: #dc2626; color: var(--color-error);
font-size: 0.8rem; font-size: 0.8rem;
margin-top: 4px; margin-top: 4px;
font-weight: 500; font-weight: 500;
@ -167,7 +168,7 @@
.resultados-header h2 { .resultados-header h2 {
margin: 0; margin: 0;
color: #222222; color: var(--color-text-main);
font-size: 1.3rem; font-size: 1.3rem;
font-weight: 600; font-weight: 600;
} }
@ -175,13 +176,75 @@
.resultados-actions { .resultados-actions {
display: flex; display: flex;
gap: 12px; 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 */
.table-container { .table-container {
overflow-x: auto; overflow-x: auto;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e5e7eb; border: 1px solid var(--color-border);
} }
.autorizaciones-table { .autorizaciones-table {
@ -197,23 +260,23 @@
} }
.autorizaciones-table tr:hover td { .autorizaciones-table tr:hover td {
background: #f8fafc; background: var(--color-table-hover);
} }
.autorizaciones-table tr.even-row td { .autorizaciones-table tr.even-row td {
background: #fafbfc; background: var(--color-table-row-alt);
} }
.numero-autorizacion { .numero-autorizacion {
font-weight: 600; font-weight: 600;
color: #1976d2; color: var(--color-primary);
font-family: "Courier New", monospace; font-family: "Courier New", monospace;
} }
.fecha { .fecha {
white-space: nowrap; white-space: nowrap;
font-size: 0.85rem; font-size: 0.85rem;
color: #666666; color: var(--color-text-muted);
} }
.interno { .interno {
@ -227,6 +290,29 @@
text-align: center; 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 { .cup-nivel {
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
@ -235,15 +321,58 @@
.nombre-paciente, .nombre-paciente,
.ips, .ips,
.autorizante, .autorizante,
.establecimiento { .establecimiento,
.estado {
max-width: 180px; max-width: 180px;
line-height: 1.3; 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, .municipio,
.departamento { .departamento {
font-size: 0.85rem; font-size: 0.85rem;
color: #666666; color: var(--color-text-muted);
} }
.acciones { .acciones {
@ -251,6 +380,13 @@
text-align: center; text-align: center;
} }
.no-result {
text-align: center;
padding: 24px 16px;
color: var(--color-text-muted);
font-weight: 600;
}
.btn-descargar:hover:not(:disabled) { .btn-descargar:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
} }
@ -269,14 +405,14 @@
.empty-state h3 { .empty-state h3 {
margin: 0 0 8px 0; margin: 0 0 8px 0;
color: #222222; color: var(--color-text-main);
font-size: 1.3rem; font-size: 1.3rem;
font-weight: 600; font-weight: 600;
} }
.empty-state p { .empty-state p {
margin: 0; margin: 0;
color: #666666; color: var(--color-text-muted);
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
} }
@ -338,10 +474,24 @@
align-items: stretch; align-items: stretch;
} }
.resultados-filtro-numero {
width: 100%;
margin-left: 0;
}
.resultados-filtro-numero input {
width: 100%;
}
.resultados-actions { .resultados-actions {
justify-content: center; justify-content: center;
} }
.estado-masivo {
width: 100%;
justify-content: space-between;
}
.autorizaciones-table th, .autorizaciones-table th,
.autorizaciones-table td { .autorizaciones-table td {
padding: 8px 12px; padding: 8px 12px;
@ -350,7 +500,8 @@
.nombre-paciente, .nombre-paciente,
.ips, .ips,
.autorizante, .autorizante,
.establecimiento { .establecimiento,
.estado {
max-width: 120px; max-width: 120px;
} }
} }
@ -361,10 +512,23 @@
} }
.btn-exportar, .btn-exportar,
.btn-descargar-todos { .btn-descargar-todos,
.estado-masivo,
.estado-masivo button {
justify-content: center; justify-content: center;
} }
.estado-masivo {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.estado-masivo select,
.estado-masivo button {
width: 100%;
}
.empty-state { .empty-state {
padding: 40px 16px; padding: 40px 16px;
} }

View File

@ -3,10 +3,15 @@
<app-header <app-header
title="Autorizaciones por fecha" title="Autorizaciones por fecha"
subtitle="Consulta y descarga de autorizaciones por rango de fechas" subtitle="Consulta y descarga de autorizaciones por rango de fechas"
badgeText="SALUD UT"
[showUserInfo]="isLoggedIn()"
[userName]="getCurrentUser()?.nombre_completo"
[userRole]="getCurrentUser()?.nombre_rol"
[showLogout]="isLoggedIn()"
(logout)="logout()"
[showBack]="true" [showBack]="true"
backLabel="Volver" backLabel="Volver"
(back)="volverAtras()" (back)="volverAtras()"
[showLogo]="false"
></app-header> ></app-header>
<!-- Mensajes --> <!-- Mensajes -->
@ -75,8 +80,44 @@
<!-- Resultados --> <!-- Resultados -->
<div class="resultados-section card" *ngIf="hayResultados || autorizaciones.length > 0"> <div class="resultados-section card" *ngIf="hayResultados || autorizaciones.length > 0">
<div class="resultados-header"> <div class="resultados-header">
<h2>Resultados ({{ autorizaciones.length }} autorizaciones)</h2> <h2>
<div class="resultados-actions"> Resultados ({{ autorizacionesFiltradas.length }} autorizaciones
<span *ngIf="filtroNumero.trim().length > 0">
de {{ autorizaciones.length }}
</span>
)
</h2>
<div class="resultados-filtro-numero">
<label for="filtroNumero">Buscar numero:</label>
<input
id="filtroNumero"
type="text"
[ngModel]="filtroNumero"
(ngModelChange)="onFiltroNumeroChange($event)"
placeholder="Numero de autorizacion"
/>
</div>
<div class="resultados-actions" *ngIf="esAdmin">
<div class="estado-masivo">
<label for="estadoMasivo">Estado masivo:</label>
<select
id="estadoMasivo"
[(ngModel)]="estadoMasivo"
[disabled]="actualizandoMasivo"
>
<option value="pendiente">Pendiente</option>
<option value="autorizado">Autorizado</option>
<option value="no_autorizado">No autorizado</option>
</select>
<button
class="btn btn-secondary"
type="button"
(click)="aplicarEstadoMasivo()"
[disabled]="actualizandoMasivo || autorizaciones.length === 0"
>
{{ actualizandoMasivo ? 'Actualizando...' : 'Aplicar a todo el rango' }}
</button>
</div>
<button <button
class="btn btn-secondary btn-exportar" class="btn btn-secondary btn-exportar"
(click)="exportarAExcel()" (click)="exportarAExcel()"
@ -85,14 +126,14 @@
> >
Exportar Excel Exportar Excel
</button> </button>
<button <button
class="btn btn-secondary btn-descargar-todos" class="btn btn-secondary btn-descargar-todos"
(click)="descargarTodosLosPdfs()" (click)="descargarTodosLosPdfs()"
title="Descargar todos los PDFs" title="Descargar todos los PDFs"
[disabled]="descargandoZip" [disabled]="descargandoZip"
> >
Descargar todos los PDFs Descargar todos los PDFs
</button> </button>
</div> </div>
</div> </div>
@ -109,18 +150,25 @@
<th>Tipo autorizacion</th> <th>Tipo autorizacion</th>
<th>Tipo servicio</th> <th>Tipo servicio</th>
<th>CUPS</th> <th>CUPS</th>
<th>Cubre</th>
<th>Nivel</th> <th>Nivel</th>
<th>IPS</th> <th>IPS</th>
<th>Municipio</th> <th>Municipio</th>
<th>Departamento</th> <th>Departamento</th>
<th>Autorizante</th> <th>Autorizante</th>
<th>Establecimiento</th> <th>Establecimiento</th>
<th>Acciones</th> <th>Estado</th>
<th>Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngIf="autorizacionesFiltradas.length === 0">
<td class="no-result" colspan="17">
No hay autorizaciones para ese numero.
</td>
</tr>
<tr <tr
*ngFor="let aut of autorizaciones; let i = index; trackBy: trackByAutorizacion" *ngFor="let aut of autorizacionesFiltradas; let i = index; trackBy: trackByAutorizacion"
[class.even-row]="i % 2 === 0" [class.even-row]="i % 2 === 0"
> >
<td class="numero-autorizacion"> <td class="numero-autorizacion">
@ -137,18 +185,61 @@
{{ getTipoServicioLabel(aut.tipo_servicio) }} {{ getTipoServicioLabel(aut.tipo_servicio) }}
</td> </td>
<td class="cup-codigo">{{ aut.cup_codigo }}</td> <td class="cup-codigo">{{ aut.cup_codigo }}</td>
<td class="cup-cobertura">
<span
class="cobertura-badge"
[class.cobertura-ok]="esCupCubierto(aut)"
[class.cobertura-no]="!esCupCubierto(aut)"
>
{{ getCoberturaLabel(aut) }}
</span>
</td>
<td class="cup-nivel">{{ aut.cup_nivel }}</td> <td class="cup-nivel">{{ aut.cup_nivel }}</td>
<td class="ips">{{ aut.nombre_ips }}</td> <td class="ips">
<td class="municipio">{{ aut.municipio }}</td> <div class="ips-info">
<td class="departamento">{{ aut.departamento }}</td> <span>{{ aut.nombre_ips }}</span>
<span
class="badge badge-danger ips-convenio"
*ngIf="aut.ips_tiene_convenio === false"
>
No cubre IPS
</span>
</div>
</td>
<td class="municipio">{{ aut.municipio || '-' }}</td>
<td class="departamento">{{ aut.departamento || '-' }}</td>
<td class="autorizante">{{ aut.nombre_autorizante }}</td> <td class="autorizante">{{ aut.nombre_autorizante }}</td>
<td class="establecimiento">{{ aut.nombre_establecimiento }}</td> <td class="establecimiento">{{ aut.nombre_establecimiento }}</td>
<td class="estado">
<ng-container *ngIf="esAdmin; else estadoTexto">
<select
[ngModel]="aut.estado_autorizacion || 'pendiente'"
(ngModelChange)="actualizarEstadoAutorizacion(aut, $event)"
[disabled]="actualizandoEstado[aut.numero_autorizacion]"
>
<option value="pendiente">Pendiente</option>
<option value="autorizado">Autorizado</option>
<option value="no_autorizado">No autorizado</option>
</select>
<span
class="mini-status"
*ngIf="actualizandoEstado[aut.numero_autorizacion]"
>
Guardando...
</span>
</ng-container>
<ng-template #estadoTexto>
<span class="estado-label">
{{ getEstadoAutorizacionLabel(aut.estado_autorizacion) }}
</span>
</ng-template>
</td>
<td class="acciones"> <td class="acciones">
<button <button
class="btn btn-success btn-sm btn-descargar" class="btn btn-success btn-sm btn-descargar"
(click)="descargarPdf(aut.numero_autorizacion)" (click)="descargarPdf(aut)"
title="Descargar PDF" title="Descargar PDF"
[disabled]="descargandoPdf" [disabled]="descargandoPdf || !puedeDescargarPdfAutorizacion(aut)"
> >
PDF PDF
</button> </button>

View File

@ -1,15 +1,16 @@
import { ChangeDetectorRef, Component, OnInit, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, OnInit, Inject } from '@angular/core';
import { CommonModule, DOCUMENT } from '@angular/common'; import { CommonModule, DOCUMENT } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { AuthService } from '../../services/auth'; import { AuthService } from '../../services/auth';
import { AppHeaderComponent } from '../shared/app-header/app-header'; import { AppHeaderComponent } from '../shared/app-header/app-header';
import { JobsService } from '../../services/jobs'; import { JobsService } from '../../services/jobs';
import { PacienteService } from '../../services/paciente';
@Component({ @Component({
selector: 'app-autorizaciones-por-fecha', selector: 'app-autorizaciones-por-fecha',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, AppHeaderComponent], imports: [CommonModule, ReactiveFormsModule, FormsModule, AppHeaderComponent],
templateUrl: './autorizaciones-por-fecha.html', templateUrl: './autorizaciones-por-fecha.html',
styleUrls: ['./autorizaciones-por-fecha.css'] styleUrls: ['./autorizaciones-por-fecha.css']
}) })
@ -23,6 +24,12 @@ export class AutorizacionesPorFechaComponent implements OnInit {
avisoAutorizaciones = false; avisoAutorizaciones = false;
descargandoZip = false; descargandoZip = false;
descargandoPdf = false; descargandoPdf = false;
actualizandoEstado: Record<string, boolean> = {};
actualizandoMasivo = false;
estadoMasivo: 'pendiente' | 'autorizado' | 'no_autorizado' = 'autorizado';
esAdmin = false;
filtroNumero = '';
autorizacionesFiltradas: any[] = [];
// Para saber si ya buscamos algo // Para saber si ya buscamos algo
hayResultados = false; hayResultados = false;
@ -35,6 +42,7 @@ export class AutorizacionesPorFechaComponent implements OnInit {
private fb: FormBuilder, private fb: FormBuilder,
private authService: AuthService, private authService: AuthService,
private jobsService: JobsService, private jobsService: JobsService,
private pacienteService: PacienteService,
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
@Inject(DOCUMENT) private document: Document @Inject(DOCUMENT) private document: Document
) { ) {
@ -45,11 +53,7 @@ export class AutorizacionesPorFechaComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
// Solo admin puede entrar (además del guard) this.esAdmin = this.authService.isAdministrador();
if (!this.authService.isAdministrador()) {
this.errorMessage = 'No tienes permisos para acceder a esta página.';
return;
}
// Rango por defecto: últimos 30 días // Rango por defecto: últimos 30 días
const hoy = new Date(); const hoy = new Date();
@ -84,6 +88,7 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.isLoading = true; this.isLoading = true;
this.autorizaciones = []; this.autorizaciones = [];
this.autorizacionesFiltradas = [];
this.hayResultados = false; this.hayResultados = false;
this.authService this.authService
@ -100,6 +105,7 @@ export class AutorizacionesPorFechaComponent implements OnInit {
.subscribe({ .subscribe({
next: (data) => { next: (data) => {
this.autorizaciones = data || []; this.autorizaciones = data || [];
this.aplicarFiltroNumero();
this.hayResultados = this.autorizaciones.length > 0; this.hayResultados = this.autorizaciones.length > 0;
this.avisoAutorizaciones = this.avisoAutorizaciones =
this.autorizaciones.length >= this.limiteAutorizaciones; this.autorizaciones.length >= this.limiteAutorizaciones;
@ -117,8 +123,19 @@ export class AutorizacionesPorFechaComponent implements OnInit {
} }
// ========= PDF INDIVIDUAL ========= // ========= PDF INDIVIDUAL =========
descargarPdf(numeroAutorizacion: string): void { descargarPdf(autorizacion: any): void {
this.limpiarMensajes(); this.limpiarMensajes();
if (!this.puedeDescargarPdfAutorizacion(autorizacion)) {
this.errorMessage = 'Autorizacion pendiente o no autorizada.';
return;
}
const numeroAutorizacion = String(autorizacion?.numero_autorizacion || '');
if (!numeroAutorizacion) {
this.errorMessage = 'Numero de autorizacion invalido.';
return;
}
this.descargandoPdf = true; this.descargandoPdf = true;
this.authService.crearJobPdfAutorizacion(numeroAutorizacion).subscribe({ this.authService.crearJobPdfAutorizacion(numeroAutorizacion).subscribe({
@ -173,10 +190,130 @@ export class AutorizacionesPorFechaComponent implements OnInit {
}); });
} }
// ========= ESTADO DE AUTORIZACION =========
actualizarEstadoAutorizacion(
autorizacion: any,
estado: 'pendiente' | 'autorizado' | 'no_autorizado'
): void {
if (!this.esAdmin) {
return;
}
if (!autorizacion) return;
const numero = String(autorizacion.numero_autorizacion || '');
if (!numero) return;
if (this.actualizandoEstado[numero]) {
return;
}
this.actualizandoEstado[numero] = true;
const estadoPrevio = autorizacion.estado_autorizacion || 'pendiente';
autorizacion.estado_autorizacion = estado;
this.pacienteService
.actualizarEstadoAutorizacion(numero, estado)
.pipe(
finalize(() => {
this.actualizandoEstado[numero] = false;
this.cdr.detectChanges();
})
)
.subscribe({
next: (resp) => {
autorizacion.estado_autorizacion =
resp?.autorizacion?.estado_autorizacion || estado;
},
error: (err) => {
console.error(err);
autorizacion.estado_autorizacion = estadoPrevio;
this.errorMessage =
err?.error?.error || 'Error actualizando el estado.';
}
});
}
aplicarEstadoMasivo(): void {
this.limpiarMensajes();
if (!this.esAdmin) {
this.errorMessage = 'No tienes permisos para actualizar estados.';
return;
}
if (!this.fechaInicioApi || !this.fechaFinApi) {
this.errorMessage = 'Primero realiza una busqueda por fechas.';
return;
}
const confirmacion = confirm(
`¿Seguro que deseas aplicar "${this.getEstadoAutorizacionLabel(this.estadoMasivo)}" a todas las autorizaciones del rango?`
);
if (!confirmacion) {
return;
}
this.actualizandoMasivo = true;
this.pacienteService
.actualizarEstadoAutorizacionesMasivo(
this.fechaInicioApi,
this.fechaFinApi,
this.estadoMasivo
)
.subscribe({
next: (resp) => {
const total = resp?.actualizados ?? 0;
this.autorizaciones = this.autorizaciones.map((aut) => ({
...aut,
estado_autorizacion: this.estadoMasivo,
}));
this.aplicarFiltroNumero();
this.successMessage = `Se actualizaron ${total} autorizaciones.`;
this.actualizandoMasivo = false;
this.cdr.detectChanges();
},
error: (err) => {
console.error(err);
this.errorMessage =
err?.error?.error || 'Error actualizando estados.';
this.actualizandoMasivo = false;
this.cdr.detectChanges();
}
});
}
// ========= USUARIO =========
logout(): void {
this.authService.logout();
}
isLoggedIn(): boolean {
return this.authService.isLoggedIn();
}
getCurrentUser(): any {
return this.authService.getCurrentUser();
}
puedeDescargarPdfAutorizacion(autorizacion: any): boolean {
if (this.esAdmin) {
return true;
}
const estado = String(autorizacion?.estado_autorizacion || 'pendiente').toLowerCase();
return estado === 'autorizado';
}
// ========= ZIP CON TODAS LAS AUTORIZACIONES ========= // ========= ZIP CON TODAS LAS AUTORIZACIONES =========
descargarTodosLosPdfs(): void { descargarTodosLosPdfs(): void {
this.limpiarMensajes(); this.limpiarMensajes();
if (!this.esAdmin) {
this.errorMessage = 'No tienes permisos para descargar todos los PDFs.';
return;
}
if (!this.autorizaciones || this.autorizaciones.length === 0) { if (!this.autorizaciones || this.autorizaciones.length === 0) {
this.errorMessage = 'No hay autorizaciones para descargar.'; this.errorMessage = 'No hay autorizaciones para descargar.';
return; return;
@ -302,6 +439,50 @@ export class AutorizacionesPorFechaComponent implements OnInit {
return tipo ? String(tipo) : ''; return tipo ? String(tipo) : '';
} }
getEstadoAutorizacionLabel(estado: string | null | undefined): string {
const normalizado = String(estado || 'pendiente').toLowerCase();
if (normalizado === 'autorizado') {
return 'Autorizado';
}
if (normalizado === 'no_autorizado') {
return 'No autorizado';
}
return 'Pendiente';
}
getCoberturaLabel(autorizacion: any): string {
if (autorizacion?.cup_cubierto === true) {
return 'Cubre';
}
if (autorizacion?.cup_cubierto === false) {
return 'No cubre';
}
if (autorizacion?.cup_descripcion || autorizacion?.cup_nivel) {
return 'Cubre';
}
return 'No cubre';
}
esCupCubierto(autorizacion: any): boolean {
return this.getCoberturaLabel(autorizacion) === 'Cubre';
}
onFiltroNumeroChange(valor: string): void {
this.filtroNumero = valor || '';
this.aplicarFiltroNumero();
}
private aplicarFiltroNumero(): void {
const filtro = this.filtroNumero.trim().toLowerCase();
if (!filtro) {
this.autorizacionesFiltradas = [...this.autorizaciones];
return;
}
this.autorizacionesFiltradas = this.autorizaciones.filter((aut) =>
String(aut?.numero_autorizacion || '').toLowerCase().includes(filtro)
);
}
limpiarMensajes(): void { limpiarMensajes(): void {
this.errorMessage = null; this.errorMessage = null;
this.successMessage = null; this.successMessage = null;
@ -347,7 +528,14 @@ export class AutorizacionesPorFechaComponent implements OnInit {
// ========= EXPORTAR A CSV SIMPLE ========= // ========= EXPORTAR A CSV SIMPLE =========
exportarAExcel(): void { exportarAExcel(): void {
if (this.autorizaciones.length === 0) { if (!this.esAdmin) {
this.errorMessage = 'No tienes permisos para exportar.';
return;
}
const dataset = this.autorizacionesFiltradas;
if (dataset.length === 0) {
this.errorMessage = 'No hay datos para exportar.'; this.errorMessage = 'No hay datos para exportar.';
return; return;
} }
@ -366,13 +554,14 @@ export class AutorizacionesPorFechaComponent implements OnInit {
'Municipio', 'Municipio',
'Departamento', 'Departamento',
'Autorizante', 'Autorizante',
'Establecimiento' 'Establecimiento',
'Estado'
]; ];
const separator = ';'; const separator = ';';
const csvContent = [ const csvContent = [
headers.map((header) => this.csvValue(header)).join(separator), headers.map((header) => this.csvValue(header)).join(separator),
...this.autorizaciones.map((aut) => ...dataset.map((aut) =>
[ [
this.csvValue(aut.numero_autorizacion), this.csvValue(aut.numero_autorizacion),
this.csvValue(aut.version || ''), this.csvValue(aut.version || ''),
@ -387,7 +576,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
this.csvValue(aut.municipio), this.csvValue(aut.municipio),
this.csvValue(aut.departamento), this.csvValue(aut.departamento),
this.csvValue(aut.nombre_autorizante), this.csvValue(aut.nombre_autorizante),
this.csvValue(aut.nombre_establecimiento) this.csvValue(aut.nombre_establecimiento),
this.csvValue(this.getEstadoAutorizacionLabel(aut.estado_autorizacion))
].join(separator) ].join(separator)
) )
].join('\n'); ].join('\n');

View File

@ -121,6 +121,22 @@
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.cup-status {
margin-top: 6px;
}
.cup-alert {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0 12px;
font-size: var(--font-size-sm);
}
.cup-alert-text {
color: var(--color-text-muted);
}
.upload-excel { .upload-excel {
margin-top: 12px; margin-top: 12px;
display: flex; display: flex;
@ -244,12 +260,39 @@
padding: 8px 10px; padding: 8px 10px;
} }
.estado-cell select {
padding: 4px 6px;
border-radius: 6px;
border: 1px solid var(--color-input-border);
font-size: 0.8rem;
}
.mini-status {
display: block;
margin-top: 4px;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.pdf-restricted { .pdf-restricted {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-style: italic; font-style: italic;
} }
.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;
}
/* === Modal de autorizacion === */ /* === Modal de autorizacion === */
.aut-modal-backdrop { .aut-modal-backdrop {
position: fixed; position: fixed;

View File

@ -9,6 +9,9 @@
[userRole]="getCurrentUser()?.nombre_rol" [userRole]="getCurrentUser()?.nombre_rol"
[showLogout]="isLoggedIn()" [showLogout]="isLoggedIn()"
(logout)="logout()" (logout)="logout()"
[showBack]="true"
backLabel="Volver"
(back)="irADashboard()"
></app-header> ></app-header>
<!-- Tarjeta de búsqueda --> <!-- Tarjeta de búsqueda -->
@ -36,31 +39,8 @@
</button> </button>
</div> </div>
<!-- Botón de cargar Excel (solo administradores) -->
<div class="upload-excel" *ngIf="puedeCargarPacientes()">
<button
type="button"
class="btn btn-primary"
(click)="inputExcel.click()"
[disabled]="cargandoExcel"
>
{{ cargandoExcel ? 'Cargando Excel...' : 'Cargar Excel de PPL' }}
</button>
<input
#inputExcel
type="file"
accept=".xlsx,.xls"
(change)="onExcelSelected($event)"
hidden
/>
<span class="upload-msg" *ngIf="estadoCargaExcel">
{{ estadoCargaExcel }}
</span>
</div>
<!-- Botón de ver autorizaciones por fecha (solo administradores) -->
<div class="admin-actions" *ngIf="puedeVerTodasAutorizaciones()"> <div class="admin-actions" *ngIf="puedeVerTodasAutorizaciones()">
<button <button
class="btn btn-gradient" class="btn btn-gradient"
@ -69,15 +49,12 @@
> >
Ver autorizaciones por fecha Ver autorizaciones por fecha
</button> </button>
</div>
<div class="nav-actions">
<button <button
class="btn btn-secondary" class="btn btn-secondary"
(click)="irADashboard()" (click)="irACargarPacientes()"
title="Volver al dashboard" title="Cargar pacientes"
> >
Volver al dashboard Cargar pacientes
</button> </button>
</div> </div>
@ -205,7 +182,7 @@
> >
<option value="">-- Seleccione IPS --</option> <option value="">-- Seleccione IPS --</option>
<option *ngFor="let ips of ipsDisponibles" [value]="ips.id_ips"> <option *ngFor="let ips of ipsDisponibles" [value]="ips.id_ips">
{{ ips.nombre_ips }} ({{ ips.municipio }} - {{ ips.departamento }}) {{ ips.nombre_ips }} ({{ ips.municipio || '-' }} - {{ ips.departamento || '-' }}){{ ips.tiene_convenio === false ? ' - NO CUBRE' : '' }}
</option> </option>
</select> </select>
<button <button
@ -247,6 +224,7 @@
[(ngModel)]="formAutorizacion.cup_codigo" [(ngModel)]="formAutorizacion.cup_codigo"
placeholder="Codigo o descripcion" placeholder="Codigo o descripcion"
(keyup.enter)="buscarCups()" (keyup.enter)="buscarCups()"
(input)="onCupInputChange()"
/> />
<button <button
type="button" type="button"
@ -275,10 +253,34 @@
<div class="cup-code">{{ cup.codigo }}</div> <div class="cup-code">{{ cup.codigo }}</div>
<div class="cup-desc">{{ cup.descripcion }}</div> <div class="cup-desc">{{ cup.descripcion }}</div>
<div class="cup-meta" *ngIf="cup.nivel">Nivel {{ cup.nivel }}</div> <div class="cup-meta" *ngIf="cup.nivel">Nivel {{ cup.nivel }}</div>
<div class="cup-status">
<span
class="badge"
[ngClass]="cup.cubierto ? 'badge-success' : 'badge-danger'"
>
{{ cup.cubierto ? 'CUBIERTO' : 'NO CUBIERTO' }}
</span>
</div>
</button> </button>
</div> </div>
</div> </div>
<div class="cup-alert" *ngIf="cupSeleccionado">
<span
class="badge"
[ngClass]="cupSeleccionado.cubierto ? 'badge-success' : 'badge-danger'"
>
{{ cupSeleccionado.cubierto ? 'CUBIERTO' : 'NO CUBIERTO' }}
</span>
<span class="cup-alert-text">
{{
cupSeleccionado.cubierto
? 'Este CUPS esta cubierto por la nota tecnica.'
: 'Este CUPS no esta cubierto por la nota tecnica.'
}}
</span>
</div>
<div class="status error" *ngIf="errorCups"> <div class="status error" *ngIf="errorCups">
{{ errorCups }} {{ errorCups }}
</div> </div>
@ -357,6 +359,7 @@
<th>Version</th> <th>Version</th>
<th>IPS</th> <th>IPS</th>
<th>Autoriza</th> <th>Autoriza</th>
<th>Estado</th>
<th>Acciones</th> <th>Acciones</th>
</tr> </tr>
</thead> </thead>
@ -393,8 +396,50 @@
</select> </select>
</div> </div>
</td> </td>
<td>{{ a.nombre_ips }}</td> <td>
<div class="ips-info">
<span>{{ a.nombre_ips }}</span>
<span
class="badge badge-danger ips-convenio"
*ngIf="a.ips_tiene_convenio === false"
>
No cubre IPS
</span>
</div>
</td>
<td>{{ a.nombre_autorizante }}</td> <td>{{ a.nombre_autorizante }}</td>
<td class="estado-cell">
<ng-container *ngIf="isAdministrador(); else estadoSoloLectura">
<select
[ngModel]="a.estado_autorizacion || 'pendiente'"
(ngModelChange)="actualizarEstadoAutorizacion(a, $event)"
>
<option value="pendiente">Pendiente</option>
<option value="autorizado">Autorizado</option>
<option value="no_autorizado">No autorizado</option>
</select>
<span
class="mini-status"
*ngIf="actualizandoEstado[a.numero_autorizacion]"
>
Guardando...
</span>
</ng-container>
<ng-template #estadoSoloLectura>
<span
class="badge"
[ngClass]="
(a.estado_autorizacion || 'pendiente') === 'autorizado'
? 'badge-success'
: (a.estado_autorizacion || 'pendiente') === 'no_autorizado'
? 'badge-danger'
: ''
"
>
{{ getEstadoAutorizacionLabel(a.estado_autorizacion) }}
</span>
</ng-template>
</td>
<td> <td>
<div class="accion-buttons"> <div class="accion-buttons">
<button <button
@ -405,15 +450,15 @@
Editar Editar
</button> </button>
<button <button
*ngIf="puedeDescargarPdfs()" *ngIf="puedeDescargarPdfAutorizacion(a)"
class="btn btn-secondary btn-sm" class="btn btn-secondary btn-sm"
[disabled]="descargandoPdf" [disabled]="descargandoPdf"
(click)="descargarPdf(a.numero_autorizacion, getVersionSeleccionada(a.numero_autorizacion, a.version))" (click)="descargarPdf(a, getVersionSeleccionada(a.numero_autorizacion, a.version))"
> >
PDF PDF
</button> </button>
<span *ngIf="!puedeDescargarPdfs()" class="pdf-restricted"> <span *ngIf="!puedeDescargarPdfAutorizacion(a)" class="pdf-restricted">
Solo admin Pendiente de aprobacion
</span> </span>
</div> </div>
</td> </td>

View File

@ -3,7 +3,7 @@ import { Router } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AuthService } from '../../services/auth'; import { AuthService } from '../../services/auth';
import { PacienteService, AutorizacionVersion } from '../../services/paciente'; import { PacienteService, AutorizacionVersion, CupInfo } from '../../services/paciente';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { AppHeaderComponent } from '../shared/app-header/app-header'; import { AppHeaderComponent } from '../shared/app-header/app-header';
import { JobsService } from '../../services/jobs'; import { JobsService } from '../../services/jobs';
@ -26,21 +26,19 @@ export class AutorizacionesComponent {
cargando = false; cargando = false;
error: string | null = null; error: string | null = null;
// ---- Excel ----
cargandoExcel = false;
estadoCargaExcel: string | null = null;
// ---- Autorizaciones ---- // ---- Autorizaciones ----
pacienteSeleccionado: any = null; pacienteSeleccionado: any = null;
ipsDisponibles: any[] = []; ipsDisponibles: any[] = [];
autorizantes: any[] = []; autorizantes: any[] = [];
cupsDisponibles: any[] = []; cupsDisponibles: CupInfo[] = [];
cupSeleccionado: CupInfo | null = null;
buscandoCups = false; buscandoCups = false;
errorCups: string | null = null; errorCups: string | null = null;
verMasIps = false; verMasIps = false;
departamentoInterno = ''; departamentoInterno = '';
observacionTraslado = ''; observacionTraslado = '';
autorizacionEditando: any | null = null; autorizacionEditando: any | null = null;
actualizandoEstado: Record<string, boolean> = {};
versionesPorAutorizacion: Record<string, AutorizacionVersion[]> = {}; versionesPorAutorizacion: Record<string, AutorizacionVersion[]> = {};
versionActualPorAutorizacion: Record<string, number> = {}; versionActualPorAutorizacion: Record<string, number> = {};
@ -121,76 +119,6 @@ export class AutorizacionesComponent {
}); });
} }
// -------------------------
// Cargar Excel (botón)
// -------------------------
onExcelSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) {
return;
}
this.cargandoExcel = true;
this.estadoCargaExcel = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', file);
this.pacienteService.cargarExcelPacientes(formData).subscribe({
next: (job) => {
this.estadoCargaExcel = 'Archivo en cola. Procesando...';
if (input) {
input.value = '';
}
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
const partes: string[] = [];
if (estado.result?.mensaje) partes.push(estado.result.mensaje);
if (typeof estado.result?.activos === 'number') {
partes.push(`Pacientes activos: ${estado.result.activos}`);
}
if (typeof estado.result?.antiguos === 'number') {
partes.push(`Pacientes antiguos: ${estado.result.antiguos}`);
}
this.estadoCargaExcel =
partes.join(' - ') || 'Archivo procesado correctamente.';
this.cargandoExcel = false;
}
if (estado.status === 'failed') {
this.estadoCargaExcel =
estado.error?.message ||
'Error procesando el Excel de pacientes.';
this.cargandoExcel = false;
}
this.cdr.markForCheck();
},
error: (error) => {
console.error(error);
this.estadoCargaExcel =
'Error consultando el estado del procesamiento.';
this.cargandoExcel = false;
this.cdr.markForCheck();
}
});
},
error: (err) => {
console.error(err);
this.estadoCargaExcel =
err?.error?.error || 'Error subiendo el Excel de pacientes.';
this.cargandoExcel = false;
if (input) {
input.value = '';
}
}
});
}
// ------------------------- // -------------------------
// Seleccionar paciente // Seleccionar paciente
// ------------------------- // -------------------------
@ -220,6 +148,7 @@ export class AutorizacionesComponent {
this.cargandoVersiones = {}; this.cargandoVersiones = {};
this.errorAutLista = null; this.errorAutLista = null;
this.cupsDisponibles = []; this.cupsDisponibles = [];
this.cupSeleccionado = null;
this.errorCups = null; this.errorCups = null;
this.cargarIps(p.interno, this.verMasIps); this.cargarIps(p.interno, this.verMasIps);
@ -233,6 +162,7 @@ export class AutorizacionesComponent {
this.errorAutorizacion = null; this.errorAutorizacion = null;
this.errorAutLista = null; this.errorAutLista = null;
this.cupsDisponibles = []; this.cupsDisponibles = [];
this.cupSeleccionado = null;
this.errorCups = null; this.errorCups = null;
this.verMasIps = false; this.verMasIps = false;
this.departamentoInterno = ''; this.departamentoInterno = '';
@ -336,15 +266,17 @@ export class AutorizacionesComponent {
const termino = String(this.formAutorizacion.cup_codigo || '').trim(); const termino = String(this.formAutorizacion.cup_codigo || '').trim();
if (!termino) { if (!termino) {
this.cupsDisponibles = []; this.cupsDisponibles = [];
this.cupSeleccionado = null;
this.errorCups = 'Ingresa un código o descripción para buscar.'; this.errorCups = 'Ingresa un código o descripción para buscar.';
return; return;
} }
this.buscandoCups = true; this.buscandoCups = true;
this.errorCups = null; this.errorCups = null;
this.cupSeleccionado = null;
this.pacienteService this.pacienteService
.buscarCupsCubiertos(termino) .buscarCups(termino)
.pipe( .pipe(
finalize(() => { finalize(() => {
this.buscandoCups = false; this.buscandoCups = false;
@ -352,22 +284,31 @@ export class AutorizacionesComponent {
}) })
) )
.subscribe({ .subscribe({
next: (data: any[]) => { next: (data: CupInfo[]) => {
this.cupsDisponibles = data || []; this.cupsDisponibles = data || [];
const match = this.cupsDisponibles.find(
(cup) => cup.codigo === this.formAutorizacion.cup_codigo
);
this.cupSeleccionado = match || null;
if (this.cupsDisponibles.length === 0) { if (this.cupsDisponibles.length === 0) {
this.errorCups = 'No se encontraron CUPS con ese criterio.'; this.errorCups = 'No se encontraron CUPS con ese criterio.';
} }
}, },
error: (err) => { error: (err) => {
console.error(err); console.error(err);
this.errorCups = 'Error consultando CUPS cubiertos.'; this.errorCups = 'Error consultando CUPS.';
}, },
}); });
} }
seleccionarCup(cup: any): void { seleccionarCup(cup: CupInfo): void {
if (!cup) return; if (!cup) return;
this.formAutorizacion.cup_codigo = cup.codigo; this.formAutorizacion.cup_codigo = cup.codigo;
this.cupSeleccionado = cup;
}
onCupInputChange(): void {
this.cupSeleccionado = null;
} }
// ------------------------- // -------------------------
@ -407,6 +348,69 @@ export class AutorizacionesComponent {
return tipo ? String(tipo) : ''; return tipo ? String(tipo) : '';
} }
getEstadoAutorizacionLabel(estado: string | null | undefined): string {
const normalizado = String(estado || 'pendiente').toLowerCase();
if (normalizado === 'autorizado') {
return 'Autorizado';
}
if (normalizado === 'no_autorizado') {
return 'No autorizado';
}
return 'Pendiente';
}
puedeDescargarPdfAutorizacion(autorizacion: any): boolean {
if (this.isAdministrador()) {
return true;
}
const estado = String(autorizacion?.estado_autorizacion || 'pendiente').toLowerCase();
return estado === 'autorizado';
}
actualizarEstadoAutorizacion(
autorizacion: any,
estado: 'pendiente' | 'autorizado' | 'no_autorizado'
): void {
if (!autorizacion || !this.isAdministrador()) {
return;
}
const numero = String(autorizacion.numero_autorizacion || '');
if (!numero) {
return;
}
if (this.actualizandoEstado[numero]) {
return;
}
this.actualizandoEstado[numero] = true;
this.errorAutLista = null;
const estadoPrevio = autorizacion.estado_autorizacion || 'pendiente';
autorizacion.estado_autorizacion = estado;
this.pacienteService
.actualizarEstadoAutorizacion(numero, estado)
.pipe(
finalize(() => {
this.actualizandoEstado[numero] = false;
this.cdr.markForCheck();
})
)
.subscribe({
next: (resp) => {
autorizacion.estado_autorizacion =
resp?.autorizacion?.estado_autorizacion || estado;
},
error: (err) => {
console.error(err);
autorizacion.estado_autorizacion = estadoPrevio;
this.errorAutLista =
err?.error?.error || 'Error actualizando el estado de la autorizacion.';
},
});
}
private buildObservacionFinal(): string | undefined { private buildObservacionFinal(): string | undefined {
const base = String(this.formAutorizacion.observacion || '').trim(); const base = String(this.formAutorizacion.observacion || '').trim();
const trasladoTexto = String(this.observacionTraslado || '').trim(); const trasladoTexto = String(this.observacionTraslado || '').trim();
@ -453,6 +457,7 @@ export class AutorizacionesComponent {
tipo_autorizacion: autorizacion.tipo_autorizacion || 'consultas_externas', tipo_autorizacion: autorizacion.tipo_autorizacion || 'consultas_externas',
tipo_servicio: autorizacion.tipo_servicio || '', tipo_servicio: autorizacion.tipo_servicio || '',
}; };
this.cupSeleccionado = null;
this.onTipoAutorizacionChange(); this.onTipoAutorizacionChange();
this.onIpsChange(); this.onIpsChange();
@ -464,6 +469,7 @@ export class AutorizacionesComponent {
cancelarEdicion(): void { cancelarEdicion(): void {
this.autorizacionEditando = null; this.autorizacionEditando = null;
this.observacionTraslado = ''; this.observacionTraslado = '';
this.cupSeleccionado = null;
this.formAutorizacion = { this.formAutorizacion = {
id_ips: '', id_ips: '',
numero_documento_autorizante: '', numero_documento_autorizante: '',
@ -495,7 +501,6 @@ export class AutorizacionesComponent {
this.errorAutorizacion = 'Debe seleccionar un CUPS.'; this.errorAutorizacion = 'Debe seleccionar un CUPS.';
return; return;
} }
const tipoAutorizacion = String( const tipoAutorizacion = String(
this.formAutorizacion.tipo_autorizacion || 'consultas_externas' this.formAutorizacion.tipo_autorizacion || 'consultas_externas'
).toLowerCase(); ).toLowerCase();
@ -633,7 +638,17 @@ export class AutorizacionesComponent {
// ------------------------- // -------------------------
// Descargar PDF // Descargar PDF
// ------------------------- // -------------------------
descargarPdf(numeroAutorizacion: string, version?: number | null): void { descargarPdf(autorizacion: any, version?: number | null): void {
if (!autorizacion) {
return;
}
if (!this.puedeDescargarPdfAutorizacion(autorizacion)) {
this.errorAutLista = 'Autorizacion pendiente o no autorizada.';
return;
}
const numeroAutorizacion = autorizacion.numero_autorizacion;
this.descargandoPdf = true; this.descargandoPdf = true;
this.errorAutLista = null; this.errorAutLista = null;
@ -701,6 +716,10 @@ export class AutorizacionesComponent {
this.router.navigate(['/autorizaciones-por-fecha']); this.router.navigate(['/autorizaciones-por-fecha']);
} }
irACargarPacientes(): void {
this.router.navigate(['/cargar-pacientes']);
}
irADashboard(): void { irADashboard(): void {
this.router.navigate(['/dashboard']); this.router.navigate(['/dashboard']);
} }

View File

@ -0,0 +1,108 @@
/* ============================
Carga masiva autorizaciones
============================ */
.masivas-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.masivas-card {
padding: 20px;
}
.masivas-card h2 {
margin: 0 0 6px;
font-size: 1.5rem;
}
.subtitle {
margin: 0 0 16px;
color: var(--color-text-muted);
font-size: 0.95rem;
}
.form-row {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.form-row label {
font-weight: 600;
font-size: 0.9rem;
}
.form-row input[type="file"] {
font-size: 0.9rem;
}
.file-name {
font-size: 0.85rem;
color: var(--color-text-main);
}
.form-row.acciones {
margin-top: 8px;
}
.resumen-card h2 {
margin: 0 0 12px;
font-size: 1.4rem;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
}
.summary-item {
background: var(--color-cup-bg);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.summary-label {
font-size: 0.8rem;
color: var(--color-text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--color-text-main);
}
.summary-note {
margin-top: 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--color-primary-soft);
border: 1px solid var(--color-border);
font-size: 0.9rem;
}
.error-list {
margin-top: 16px;
}
.error-list h3 {
margin: 0 0 8px;
font-size: 1rem;
}
.error-table {
border: 1px solid var(--color-border);
border-radius: 8px;
max-height: 260px;
overflow: auto;
}

View File

@ -0,0 +1,117 @@
<div class="page-shell">
<div class="content-container masivas-container">
<app-header
title="Carga masiva de autorizaciones"
subtitle="Sube la plantilla Excel y genera autorizaciones pendientes"
badgeText="SALUD UT"
[showUserInfo]="isLoggedIn()"
[userName]="getCurrentUser()?.nombre_completo"
[userRole]="getCurrentUser()?.nombre_rol"
[showLogout]="isLoggedIn()"
(logout)="logout()"
[showBack]="true"
backLabel="Volver"
(back)="volverDashboard()"
></app-header>
<div class="card masivas-card">
<h2>Subir plantilla</h2>
<p class="subtitle">
La columna de numero de autorizacion se ignora. Todas las autorizaciones quedan en estado pendiente.
</p>
<div class="form-row">
<label>Archivo Excel:</label>
<input
#archivoInput
type="file"
accept=".xlsx,.xls"
(change)="onArchivoSelected($event)"
/>
<span class="file-name" *ngIf="archivoFile">{{ archivoFile.name }}</span>
</div>
<div class="form-row acciones">
<button
class="btn btn-primary"
(click)="procesarMasivo(archivoInput)"
[disabled]="isLoading || !archivoFile"
>
{{ isLoading ? 'Procesando...' : 'Procesar autorizaciones' }}
</button>
</div>
<div class="status ok" *ngIf="statusMessage">{{ statusMessage }}</div>
<div class="status error" *ngIf="errorMessage">{{ errorMessage }}</div>
</div>
<div class="card resumen-card" *ngIf="resumen">
<h2>Resumen de la carga</h2>
<div class="summary-grid">
<div class="summary-item">
<span class="summary-label">Total filas</span>
<span class="summary-value">{{ resumen.total || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Creadas</span>
<span class="summary-value">{{ resumen.creadas || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Omitidas</span>
<span class="summary-value">{{ resumen.omitidas || resumen.omitidos || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Duplicadas</span>
<span class="summary-value">{{ resumen.duplicados || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Sin paciente</span>
<span class="summary-value">{{ resumen.sin_paciente || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Sin CUPS</span>
<span class="summary-value">{{ resumen.sin_cups || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Sin IPS</span>
<span class="summary-value">{{ resumen.sin_ips || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">CUPS no cubiertos</span>
<span class="summary-value">{{ resumen.cups_no_cubiertos || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">IPS sin convenio</span>
<span class="summary-value">{{ resumen.ips_sin_convenio || 0 }}</span>
</div>
</div>
<div
class="summary-note"
*ngIf="(resumen.cups_no_cubiertos || 0) > 0 || (resumen.ips_sin_convenio || 0) > 0"
>
Se detectaron CUPS no cubiertos o IPS sin convenio. Las autorizaciones quedan pendientes para revision.
</div>
<div class="error-list" *ngIf="errores.length">
<h3>Errores (max 50)</h3>
<div class="error-table">
<table class="table">
<thead>
<tr>
<th>Fila</th>
<th>Error</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of errores">
<td>{{ e.fila || '-' }}</td>
<td>{{ e.error || 'Error en fila' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,124 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { AppHeaderComponent } from '../shared/app-header/app-header';
import { AuthService } from '../../services/auth';
import { PacienteService } from '../../services/paciente';
import { JobsService } from '../../services/jobs';
import { JobResult, JobRowError } from '../../services/job-types';
@Component({
selector: 'app-cargar-autorizaciones-masivas',
standalone: true,
imports: [CommonModule, AppHeaderComponent],
templateUrl: './cargar-autorizaciones-masivas.html',
styleUrls: ['./cargar-autorizaciones-masivas.css']
})
export class CargarAutorizacionesMasivasComponent {
archivoFile: File | null = null;
isLoading = false;
statusMessage: string | null = null;
errorMessage: string | null = null;
resumen: JobResult | null = null;
errores: JobRowError[] = [];
constructor(
private authService: AuthService,
private pacienteService: PacienteService,
private jobsService: JobsService,
private router: Router,
private cdr: ChangeDetectorRef
) {}
volverDashboard(): void {
this.router.navigate(['/dashboard']);
}
logout(): void {
this.authService.logout();
}
isLoggedIn(): boolean {
return this.authService.isLoggedIn();
}
getCurrentUser(): any {
return this.authService.getCurrentUser();
}
onArchivoSelected(event: Event): void {
const input = event.target as HTMLInputElement;
this.archivoFile = input.files?.[0] || null;
this.limpiarMensajes();
this.resumen = null;
this.errores = [];
}
procesarMasivo(input?: HTMLInputElement): void {
this.limpiarMensajes();
this.resumen = null;
this.errores = [];
if (!this.archivoFile) {
this.errorMessage = 'Debes seleccionar el archivo Excel de autorizaciones.';
return;
}
this.isLoading = true;
this.statusMessage = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', this.archivoFile);
this.pacienteService.cargarAutorizacionesMasivas(formData).subscribe({
next: (job) => {
this.statusMessage = 'Archivo en cola. Procesando...';
if (input) {
input.value = '';
}
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
this.resumen = estado.result || null;
this.errores = Array.isArray(estado.result?.errores)
? estado.result?.errores || []
: [];
this.statusMessage =
estado.result?.mensaje || 'Carga masiva finalizada.';
this.isLoading = false;
}
if (estado.status === 'failed') {
this.errorMessage =
estado.error?.message ||
'Error procesando autorizaciones masivas.';
this.isLoading = false;
}
this.cdr.detectChanges();
},
error: (error) => {
console.error(error);
this.errorMessage =
'Error consultando el estado de la carga masiva.';
this.isLoading = false;
this.cdr.detectChanges();
}
});
},
error: (err) => {
console.error(err);
this.errorMessage =
err?.error?.error || 'Error subiendo el Excel de autorizaciones.';
this.isLoading = false;
this.cdr.detectChanges();
}
});
}
private limpiarMensajes(): void {
this.statusMessage = null;
this.errorMessage = null;
}
}

View File

@ -3,10 +3,15 @@
<app-header <app-header
title="Cargar CUPS" title="Cargar CUPS"
subtitle="Sube nota tecnica y tabla referencia" subtitle="Sube nota tecnica y tabla referencia"
badgeText="SALUD UT"
[showUserInfo]="isLoggedIn()"
[userName]="getCurrentUser()?.nombre_completo"
[userRole]="getCurrentUser()?.nombre_rol"
[showLogout]="isLoggedIn()"
(logout)="logout()"
[showBack]="true" [showBack]="true"
backLabel="Volver" backLabel="Volver"
(back)="volverDashboard()" (back)="volverDashboard()"
[showLogo]="false"
></app-header> ></app-header>
<div class="card cups-card"> <div class="card cups-card">

View File

@ -38,6 +38,18 @@ export class CargarCupsComponent implements OnInit {
this.router.navigate(['/dashboard']); this.router.navigate(['/dashboard']);
} }
logout(): void {
this.authService.logout();
}
isLoggedIn(): boolean {
return this.authService.isLoggedIn();
}
getCurrentUser(): any {
return this.authService.getCurrentUser();
}
onNotaSelected(event: Event): void { onNotaSelected(event: Event): void {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
this.notaFile = input.files?.[0] || null; this.notaFile = input.files?.[0] || null;

View File

@ -0,0 +1,101 @@
/* ============================
Carga IPS y REPS
============================ */
.ips-reps-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.upload-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.upload-card {
padding: 20px;
}
.upload-card h2 {
margin: 0 0 6px;
font-size: 1.4rem;
}
.subtitle {
margin: 0 0 16px;
color: var(--color-text-muted);
font-size: 0.95rem;
}
.form-row {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.form-row label {
font-weight: 600;
font-size: 0.9rem;
}
.form-row input[type="file"] {
font-size: 0.9rem;
}
.file-name {
font-size: 0.85rem;
color: var(--color-text-main);
}
.form-row.acciones {
margin-top: 8px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-top: 12px;
}
.summary-item {
background: var(--color-cup-bg);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.summary-label {
font-size: 0.8rem;
color: var(--color-text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-value {
font-size: 1.1rem;
font-weight: 700;
color: var(--color-text-main);
}
.error-list {
margin-top: 16px;
}
.error-list h3 {
margin: 0 0 8px;
font-size: 1rem;
}
.error-table {
border: 1px solid var(--color-border);
border-radius: 8px;
max-height: 220px;
overflow: auto;
}

View File

@ -0,0 +1,147 @@
<div class="page-shell">
<div class="content-container ips-reps-container">
<app-header
title="Cargar IPS y REPS"
subtitle="Actualiza prestadores e informacion de profesionales"
badgeText="SALUD UT"
[showUserInfo]="isLoggedIn()"
[userName]="getCurrentUser()?.nombre_completo"
[userRole]="getCurrentUser()?.nombre_rol"
[showLogout]="isLoggedIn()"
(logout)="logout()"
[showBack]="true"
backLabel="Volver"
(back)="volverDashboard()"
></app-header>
<div class="upload-grid">
<div class="card upload-card">
<h2>Cargar IPS</h2>
<p class="subtitle">
Sube el archivo ips.xlsx para actualizar convenios y datos de IPS.
</p>
<div class="form-row">
<label>Archivo Excel:</label>
<input
#ipsInput
type="file"
accept=".xlsx,.xls"
(change)="onIpsSelected($event)"
/>
<span class="file-name" *ngIf="ipsFile">{{ ipsFile.name }}</span>
</div>
<div class="form-row acciones">
<button
class="btn btn-primary"
(click)="procesarIps(ipsInput)"
[disabled]="isLoadingIps || !ipsFile"
>
{{ isLoadingIps ? 'Procesando...' : 'Cargar IPS' }}
</button>
</div>
<div class="status ok" *ngIf="statusIps">{{ statusIps }}</div>
<div class="status error" *ngIf="errorIps">{{ errorIps }}</div>
<div class="summary-grid" *ngIf="ipsResumen">
<div class="summary-item">
<span class="summary-label">Total filas</span>
<span class="summary-value">{{ ipsResumen.total || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Insertadas</span>
<span class="summary-value">{{ ipsResumen.insertados || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Actualizadas</span>
<span class="summary-value">{{ ipsResumen.actualizados || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Omitidas</span>
<span class="summary-value">{{ ipsResumen.omitidos || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Desactivadas</span>
<span class="summary-value">{{ ipsResumen.desactivados || 0 }}</span>
</div>
</div>
<div class="error-list" *ngIf="ipsErrores.length">
<h3>Errores (max 50)</h3>
<div class="error-table">
<table class="table">
<thead>
<tr>
<th>Fila</th>
<th>Error</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of ipsErrores">
<td>{{ e.fila || '-' }}</td>
<td>{{ e.error || 'Error en fila' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card upload-card">
<h2>Cargar REPS</h2>
<p class="subtitle">
Sube el archivo reps.xlsx para actualizar profesionales REPS.
</p>
<div class="form-row">
<label>Archivo Excel:</label>
<input
#repsInput
type="file"
accept=".xlsx,.xls"
(change)="onRepsSelected($event)"
/>
<span class="file-name" *ngIf="repsFile">{{ repsFile.name }}</span>
</div>
<div class="form-row acciones">
<button
class="btn btn-primary"
(click)="procesarReps(repsInput)"
[disabled]="isLoadingReps || !repsFile"
>
{{ isLoadingReps ? 'Procesando...' : 'Cargar REPS' }}
</button>
</div>
<div class="status ok" *ngIf="statusReps">{{ statusReps }}</div>
<div class="status error" *ngIf="errorReps">{{ errorReps }}</div>
<div class="summary-grid" *ngIf="repsResumen">
<div class="summary-item">
<span class="summary-label">Total filas</span>
<span class="summary-value">{{ repsResumen.total || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Insertadas</span>
<span class="summary-value">{{ repsResumen.insertados || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Actualizadas</span>
<span class="summary-value">{{ repsResumen.actualizados || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Omitidas</span>
<span class="summary-value">{{ repsResumen.omitidos || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Desactivados</span>
<span class="summary-value">{{ repsResumen.desactivados || 0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,205 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { AppHeaderComponent } from '../shared/app-header/app-header';
import { AuthService } from '../../services/auth';
import { PacienteService } from '../../services/paciente';
import { JobsService } from '../../services/jobs';
import { JobResult, JobRowError } from '../../services/job-types';
@Component({
selector: 'app-cargar-ips-reps',
standalone: true,
imports: [CommonModule, AppHeaderComponent],
templateUrl: './cargar-ips-reps.html',
styleUrls: ['./cargar-ips-reps.css']
})
export class CargarIpsRepsComponent implements OnInit {
ipsFile: File | null = null;
repsFile: File | null = null;
isLoadingIps = false;
isLoadingReps = false;
statusIps: string | null = null;
statusReps: string | null = null;
errorIps: string | null = null;
errorReps: string | null = null;
ipsResumen: JobResult | null = null;
repsResumen: JobResult | null = null;
ipsErrores: JobRowError[] = [];
constructor(
private authService: AuthService,
private pacienteService: PacienteService,
private jobsService: JobsService,
private router: Router,
private cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
if (!this.authService.isAdministrador()) {
this.router.navigate(['/dashboard']);
}
}
volverDashboard(): void {
this.router.navigate(['/dashboard']);
}
logout(): void {
this.authService.logout();
}
isLoggedIn(): boolean {
return this.authService.isLoggedIn();
}
getCurrentUser(): any {
return this.authService.getCurrentUser();
}
onIpsSelected(event: Event): void {
const input = event.target as HTMLInputElement;
this.ipsFile = input.files?.[0] || null;
this.limpiarIps();
this.ipsResumen = null;
this.ipsErrores = [];
}
onRepsSelected(event: Event): void {
const input = event.target as HTMLInputElement;
this.repsFile = input.files?.[0] || null;
this.limpiarReps();
this.repsResumen = null;
}
procesarIps(input?: HTMLInputElement): void {
this.limpiarIps();
this.ipsResumen = null;
this.ipsErrores = [];
if (!this.ipsFile) {
this.errorIps = 'Debes seleccionar el archivo Excel de IPS.';
return;
}
this.isLoadingIps = true;
this.statusIps = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', this.ipsFile);
this.pacienteService.cargarIps(formData).subscribe({
next: (job) => {
this.statusIps = 'Archivo en cola. Procesando...';
if (input) {
input.value = '';
}
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
this.ipsResumen = estado.result || null;
this.ipsErrores = Array.isArray(estado.result?.errores)
? estado.result?.errores || []
: [];
this.statusIps = estado.result?.mensaje || 'IPS cargadas correctamente.';
this.isLoadingIps = false;
}
if (estado.status === 'failed') {
this.errorIps =
estado.error?.message || 'Error procesando el Excel de IPS.';
this.isLoadingIps = false;
}
this.cdr.detectChanges();
},
error: (error) => {
console.error(error);
this.errorIps =
'Error consultando el estado de la carga de IPS.';
this.isLoadingIps = false;
this.cdr.detectChanges();
}
});
},
error: (err) => {
console.error(err);
this.errorIps =
err?.error?.error || 'Error subiendo el Excel de IPS.';
this.isLoadingIps = false;
this.cdr.detectChanges();
}
});
}
procesarReps(input?: HTMLInputElement): void {
this.limpiarReps();
this.repsResumen = null;
if (!this.repsFile) {
this.errorReps = 'Debes seleccionar el archivo Excel de REPS.';
return;
}
this.isLoadingReps = true;
this.statusReps = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', this.repsFile);
this.pacienteService.cargarReps(formData).subscribe({
next: (job) => {
this.statusReps = 'Archivo en cola. Procesando...';
if (input) {
input.value = '';
}
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
this.repsResumen = estado.result || null;
this.statusReps = estado.result?.mensaje || 'REPS cargados correctamente.';
this.isLoadingReps = false;
}
if (estado.status === 'failed') {
this.errorReps =
estado.error?.message || 'Error procesando el Excel de REPS.';
this.isLoadingReps = false;
}
this.cdr.detectChanges();
},
error: (error) => {
console.error(error);
this.errorReps =
'Error consultando el estado de la carga de REPS.';
this.isLoadingReps = false;
this.cdr.detectChanges();
}
});
},
error: (err) => {
console.error(err);
this.errorReps =
err?.error?.error || 'Error subiendo el Excel de REPS.';
this.isLoadingReps = false;
this.cdr.detectChanges();
}
});
}
private limpiarIps(): void {
this.statusIps = null;
this.errorIps = null;
}
private limpiarReps(): void {
this.statusReps = null;
this.errorReps = null;
}
}

View File

@ -0,0 +1,83 @@
/* ============================
Cargar pacientes
============================ */
.pacientes-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.pacientes-card {
padding: 20px;
}
.pacientes-card h2 {
margin: 0 0 6px;
font-size: 1.5rem;
}
.subtitle {
margin: 0 0 16px;
color: var(--color-text-muted);
font-size: 0.95rem;
}
.form-row {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.form-row label {
font-weight: 600;
font-size: 0.9rem;
}
.form-row input[type="file"] {
font-size: 0.9rem;
}
.file-name {
font-size: 0.85rem;
color: var(--color-text-main);
}
.form-row.acciones {
margin-top: 8px;
}
.resumen-card h2 {
margin: 0 0 12px;
font-size: 1.4rem;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.summary-item {
background: var(--color-cup-bg);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.summary-label {
font-size: 0.8rem;
color: var(--color-text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--color-text-main);
}

View File

@ -0,0 +1,62 @@
<div class="page-shell">
<div class="content-container pacientes-container">
<app-header
title="Cargar pacientes"
subtitle="Sube el Excel de PPL para actualizar pacientes e ingresos"
badgeText="SALUD UT"
[showUserInfo]="isLoggedIn()"
[userName]="getCurrentUser()?.nombre_completo"
[userRole]="getCurrentUser()?.nombre_rol"
[showLogout]="isLoggedIn()"
(logout)="logout()"
[showBack]="true"
backLabel="Volver"
(back)="volverDashboard()"
></app-header>
<div class="card pacientes-card">
<h2>Subir archivo</h2>
<p class="subtitle">
El sistema procesa paciente, ingreso y establecimiento. Usa la plantilla oficial de pacientes.
</p>
<div class="form-row">
<label>Archivo Excel:</label>
<input
#archivoInput
type="file"
accept=".xlsx,.xls"
(change)="onArchivoSelected($event)"
/>
<span class="file-name" *ngIf="archivoFile">{{ archivoFile.name }}</span>
</div>
<div class="form-row acciones">
<button
class="btn btn-primary"
(click)="procesarPacientes(archivoInput)"
[disabled]="isLoading || !archivoFile"
>
{{ isLoading ? 'Procesando...' : 'Procesar pacientes' }}
</button>
</div>
<div class="status ok" *ngIf="statusMessage">{{ statusMessage }}</div>
<div class="status error" *ngIf="errorMessage">{{ errorMessage }}</div>
</div>
<div class="card resumen-card" *ngIf="resumen">
<h2>Resumen de la carga</h2>
<div class="summary-grid">
<div class="summary-item">
<span class="summary-label">Pacientes activos</span>
<span class="summary-value">{{ resumen.activos || 0 }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Pacientes antiguos</span>
<span class="summary-value">{{ resumen.antiguos || 0 }}</span>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,133 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { AppHeaderComponent } from '../shared/app-header/app-header';
import { AuthService } from '../../services/auth';
import { PacienteService } from '../../services/paciente';
import { JobsService } from '../../services/jobs';
import { JobResult } from '../../services/job-types';
@Component({
selector: 'app-cargar-pacientes',
standalone: true,
imports: [CommonModule, AppHeaderComponent],
templateUrl: './cargar-pacientes.html',
styleUrls: ['./cargar-pacientes.css']
})
export class CargarPacientesComponent implements OnInit {
archivoFile: File | null = null;
isLoading = false;
statusMessage: string | null = null;
errorMessage: string | null = null;
resumen: JobResult | null = null;
constructor(
private authService: AuthService,
private pacienteService: PacienteService,
private jobsService: JobsService,
private router: Router,
private cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
if (!this.authService.isAdministrador()) {
this.router.navigate(['/dashboard']);
}
}
volverDashboard(): void {
this.router.navigate(['/dashboard']);
}
logout(): void {
this.authService.logout();
}
isLoggedIn(): boolean {
return this.authService.isLoggedIn();
}
getCurrentUser(): any {
return this.authService.getCurrentUser();
}
onArchivoSelected(event: Event): void {
const input = event.target as HTMLInputElement;
this.archivoFile = input.files?.[0] || null;
this.limpiarMensajes();
this.resumen = null;
}
procesarPacientes(input?: HTMLInputElement): void {
this.limpiarMensajes();
this.resumen = null;
if (!this.archivoFile) {
this.errorMessage = 'Debes seleccionar el archivo Excel de pacientes.';
return;
}
this.isLoading = true;
this.statusMessage = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', this.archivoFile);
this.pacienteService.cargarExcelPacientes(formData).subscribe({
next: (job) => {
this.statusMessage = 'Archivo en cola. Procesando...';
if (input) {
input.value = '';
}
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
this.resumen = estado.result || null;
const partes: string[] = [];
if (estado.result?.mensaje) {
partes.push(estado.result.mensaje);
}
if (typeof estado.result?.activos === 'number') {
partes.push(`Pacientes activos: ${estado.result.activos}`);
}
if (typeof estado.result?.antiguos === 'number') {
partes.push(`Pacientes antiguos: ${estado.result.antiguos}`);
}
this.statusMessage =
partes.join(' - ') || 'Pacientes cargados correctamente.';
this.isLoading = false;
}
if (estado.status === 'failed') {
this.errorMessage =
estado.error?.message || 'Error procesando el Excel de pacientes.';
this.isLoading = false;
}
this.cdr.detectChanges();
},
error: (error) => {
console.error(error);
this.errorMessage =
'Error consultando el estado de la carga de pacientes.';
this.isLoading = false;
this.cdr.detectChanges();
}
});
},
error: (err) => {
console.error(err);
this.errorMessage =
err?.error?.error || 'Error subiendo el Excel de pacientes.';
this.isLoading = false;
this.cdr.detectChanges();
}
});
}
private limpiarMensajes(): void {
this.statusMessage = null;
this.errorMessage = null;
}
}

View File

@ -38,15 +38,16 @@
padding: 16px 20px; padding: 16px 20px;
border-radius: 8px; border-radius: 8px;
margin: 20px 0 24px; margin: 20px 0 24px;
background: white; background: var(--color-card);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
animation: slideIn 0.3s ease; animation: slideIn 0.3s ease;
} }
.alert-error { .alert-error {
border-left: 4px solid #dc2626; border-left: 4px solid var(--color-error);
background: #fef2f2; background: var(--color-permission-no-bg);
color: #dc2626; color: var(--color-error);
} }
.alert-icon { .alert-icon {
@ -94,20 +95,21 @@
} }
.action-card { .action-card {
background: white; background: var(--color-card);
border-radius: 12px; border-radius: 12px;
padding: 24px; padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
border: 2px solid transparent; color: var(--color-text-main);
} }
.action-card:hover { .action-card:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); box-shadow: var(--shadow-float);
border-color: #1976d2; border-color: var(--color-primary);
} }
.action-icon { .action-icon {
@ -117,8 +119,8 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #1976d2; color: var(--color-primary);
background: #e3f2fd; background: var(--color-primary-soft);
border-radius: 10px; border-radius: 10px;
} }
@ -130,14 +132,14 @@
.action-card h4, .action-card h4,
.action-card h3 { .action-card h3 {
margin: 0 0 8px 0; margin: 0 0 8px 0;
color: #222222; color: var(--color-text-main);
font-size: 1.05rem; font-size: 1.05rem;
font-weight: 600; font-weight: 600;
} }
.action-card p { .action-card p {
margin: 0; margin: 0;
color: #666666; color: var(--color-text-muted);
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.4; line-height: 1.4;
} }
@ -146,7 +148,7 @@
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
background: #1976d2; background: var(--color-primary);
color: white; color: white;
padding: 4px 8px; padding: 4px 8px;
border-radius: 12px; border-radius: 12px;
@ -171,12 +173,12 @@
.role-label { .role-label {
font-weight: 600; font-weight: 600;
color: #222222; color: var(--color-text-main);
min-width: 140px; min-width: 140px;
} }
.role-value { .role-value {
color: #666666; color: var(--color-text-muted);
font-weight: 500; font-weight: 500;
} }
@ -186,36 +188,90 @@
gap: 8px; gap: 8px;
} }
.sedes-info {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.sedes-meta {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.sedes-count {
font-size: 0.85rem;
color: var(--color-text-muted);
font-weight: 500;
}
.sedes-toggle {
border: none;
background: transparent;
color: var(--color-primary);
font-weight: 600;
cursor: pointer;
padding: 0;
}
.sedes-toggle:hover {
text-decoration: underline;
}
.sedes-empty {
font-size: 0.85rem;
color: var(--color-text-muted);
}
.sede-badge { .sede-badge {
background: #e3f2fd; background: var(--color-primary-soft);
color: #1976d2; color: var(--color-primary);
padding: 4px 12px; padding: 4px 12px;
border-radius: 16px; border-radius: 16px;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
border: 1px solid #1976d2; border: 1px solid var(--color-primary);
} }
/* Permissions Summary */ /* Permissions Summary */
.permissions-summary {
margin-top: 24px;
}
.section-subtitle {
margin: -8px 0 16px;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.permissions-grid { .permissions-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px; gap: 16px;
} }
.permission-item { .permission-item {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 12px; gap: 12px;
padding: 12px; padding: 14px 16px;
border-radius: 8px; border-radius: 8px;
background: #f9fafb; background: var(--color-surface);
border: 1px solid var(--color-border);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.permission-item.has-permission { .permission-item.has-permission {
background: #f0f9f0; background: var(--color-permission-yes-bg);
border: 1px solid #dcfce7; border: 1px solid var(--color-permission-yes-border);
}
.permission-item.no-permission {
background: var(--color-permission-no-bg);
border: 1px solid var(--color-permission-no-border);
} }
.permission-icon { .permission-icon {
@ -226,11 +282,31 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--color-primary);
} }
.permission-text { .permission-item.has-permission .permission-icon {
color: var(--color-success);
}
.permission-item.no-permission .permission-icon {
color: var(--color-error);
}
.permission-body {
display: flex;
flex-direction: column;
gap: 4px;
}
.permission-title {
font-weight: 500; font-weight: 500;
color: #222222; color: var(--color-text-main);
}
.permission-meta {
font-size: 0.82rem;
color: var(--color-text-muted);
} }
.excel-status { .excel-status {

View File

@ -3,6 +3,7 @@
<app-header <app-header
title="SALUD UT" title="SALUD UT"
subtitle="Módulo de autorizaciones médicas" subtitle="Módulo de autorizaciones médicas"
badgeText="SALUD UT"
[showUserInfo]="true" [showUserInfo]="true"
[userName]="getNombreUsuario()" [userName]="getNombreUsuario()"
[userRole]="getNombreRolFormateado()" [userRole]="getNombreRolFormateado()"
@ -50,7 +51,7 @@
<div <div
class="action-card" class="action-card"
*ngIf="puedeCargarPacientes()" *ngIf="puedeCargarPacientes()"
(click)="abrirCargadorPacientes(inputExcelPacientes)" (click)="irACargarPacientes()"
> >
<span class="action-icon" aria-hidden="true"> <span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" role="img" aria-label=""> <svg viewBox="0 0 24 24" role="img" aria-label="">
@ -67,9 +68,7 @@
</span> </span>
<h4>Cargar pacientes</h4> <h4>Cargar pacientes</h4>
<p> <p>
{{ cargandoExcel Subir archivo Excel con datos de pacientes
? 'Cargando archivo de pacientes...'
: 'Subir archivo Excel con datos de pacientes' }}
</p> </p>
<div class="admin-badge">Solo admin</div> <div class="admin-badge">Solo admin</div>
</div> </div>
@ -100,11 +99,67 @@
<div class="admin-badge">Solo admin</div> <div class="admin-badge">Solo admin</div>
</div> </div>
<!-- Ver Autorizaciones por Fecha (solo administradores) --> <!-- Carga masiva de autorizaciones -->
<div
class="action-card"
(click)="irACargarAutorizacionesMasivas()"
>
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" role="img" aria-label="">
<path
d="M4 5h16v10H4z"
fill="none"
stroke="currentColor"
stroke-width="2"
></path>
<path
d="M8 19h8"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
></path>
<path
d="M9 9h6M9 12h6"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
></path>
</svg>
</span>
<h4>Carga masiva autorizaciones</h4>
<p>Subir plantilla Excel para crear autorizaciones pendientes</p>
</div>
<!-- Cargar IPS y REPS (solo administradores) -->
<div
class="action-card"
*ngIf="puedeCargarIpsReps()"
(click)="irACargarIpsReps()"
>
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" role="img" aria-label="">
<path
d="M12 3v12m0 0l-4-4m4 4l4-4"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<rect x="4" y="17" width="16" height="4" rx="1" fill="none" stroke="currentColor" stroke-width="2"></rect>
</svg>
</span>
<h4>Cargar IPS y REPS</h4>
<p>Subir Excel de IPS y profesionales REPS</p>
<div class="admin-badge">Solo admin</div>
</div>
<!-- Ver Autorizaciones por Fecha -->
<div <div
class="action-card" class="action-card"
(click)="irAVerAutorizacionesPorFecha()" (click)="irAVerAutorizacionesPorFecha()"
*ngIf="puedeVerTodasAutorizaciones()"
> >
<span class="action-icon" aria-hidden="true"> <span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" role="img" aria-label=""> <svg viewBox="0 0 24 24" role="img" aria-label="">
@ -116,6 +171,28 @@
</span> </span>
<h4>Autorizaciones por fecha</h4> <h4>Autorizaciones por fecha</h4>
<p>Consultar y descargar autorizaciones por rango de fechas</p> <p>Consultar y descargar autorizaciones por rango de fechas</p>
<div class="admin-badge" *ngIf="puedeVerTodasAutorizaciones()">Solo admin</div>
</div>
<!-- Estadisticas de autorizaciones (solo administradores) -->
<div
class="action-card"
*ngIf="puedeVerEstadisticas()"
(click)="irAEstadisticasAutorizaciones()"
>
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" role="img" aria-label="">
<path
d="M4 20V10M10 20V4M16 20v-7M22 20H2"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
></path>
</svg>
</span>
<h4>Estadisticas</h4>
<p>Resumen mensual con estados y volumen diario</p>
<div class="admin-badge">Solo admin</div> <div class="admin-badge">Solo admin</div>
</div> </div>
@ -143,20 +220,6 @@
</div> </div>
</div> </div>
<!-- Input de archivo real (oculto) -->
<input
#inputExcelPacientes
type="file"
accept=".xlsx,.xls"
hidden
(click)="$event.stopPropagation()"
(change)="onExcelSelected($event)"
/>
<!-- Mensaje de estado de la carga -->
<p class="excel-status" *ngIf="estadoCargaExcel">
{{ estadoCargaExcel }}
</p>
</section> </section>
<!-- User Role Information --> <!-- User Role Information -->
@ -171,20 +234,35 @@
<!-- Sedes asignadas (solo para administrativos) --> <!-- Sedes asignadas (solo para administrativos) -->
<div <div
class="role-item" class="role-item"
*ngIf=" *ngIf="currentUser.nombre_rol === 'administrativo_sede'"
currentUser.nombre_rol === 'administrativo_sede' &&
getSedesUsuario().length > 0
"
> >
<span class="role-label">Sedes asignadas:</span> <span class="role-label">Sedes asignadas:</span>
<div class="sedes-list"> <div class="sedes-info">
<span <div class="sedes-meta" *ngIf="getSedesUsuario().length > 0">
class="sede-badge" <span class="sedes-count">
*ngFor="let sede of getSedesUsuario()" {{ getSedesUsuario().length }} sedes
[title]="sede.nombre_establecimiento" </span>
> <button
{{ sede.nombre_establecimiento }} class="sedes-toggle"
</span> *ngIf="getSedesUsuario().length > 8"
(click)="toggleSedesAsignadas()"
type="button"
>
{{ getTextoToggleSedes() }}
</button>
</div>
<div class="sedes-list" *ngIf="getSedesUsuario().length > 0; else sinSedes">
<span
class="sede-badge"
*ngFor="let sede of getSedesMostradas()"
[title]="sede.nombre_establecimiento"
>
{{ sede.nombre_establecimiento }}
</span>
</div>
<ng-template #sinSedes>
<span class="sedes-empty">Sin sedes asignadas</span>
</ng-template>
</div> </div>
</div> </div>
</div> </div>
@ -193,10 +271,14 @@
<!-- Permissions Summary --> <!-- Permissions Summary -->
<section class="permissions-summary card"> <section class="permissions-summary card">
<h3 class="section-title">Permisos disponibles</h3> <h3 class="section-title">Permisos disponibles</h3>
<p class="section-subtitle">
Resumen de lo que puedes hacer con tu cuenta.
</p>
<div class="permissions-grid"> <div class="permissions-grid">
<div <div
class="permission-item" class="permission-item"
[class.has-permission]="puedeGenerarAutorizaciones()" [class.has-permission]="puedeGenerarAutorizaciones()"
[class.no-permission]="!puedeGenerarAutorizaciones()"
> >
<span class="permission-icon" *ngIf="puedeGenerarAutorizaciones(); else noPermGen"> <span class="permission-icon" *ngIf="puedeGenerarAutorizaciones(); else noPermGen">
&check; &check;
@ -204,25 +286,56 @@
<ng-template #noPermGen> <ng-template #noPermGen>
<span class="permission-icon">&times;</span> <span class="permission-icon">&times;</span>
</ng-template> </ng-template>
<span class="permission-text">Generar autorizaciones</span> <div class="permission-body">
<span class="permission-title">Generar autorizaciones</span>
<span class="permission-meta">Crear y editar solicitudes</span>
</div>
</div> </div>
<div <div
class="permission-item" class="permission-item"
[class.has-permission]="puedeCargarPacientes()" [class.has-permission]="puedeCargarAutorizacionesMasivas()"
[class.no-permission]="!puedeCargarAutorizacionesMasivas()"
> >
<span class="permission-icon" *ngIf="puedeCargarPacientes(); else noPermExcel"> <span
class="permission-icon"
*ngIf="puedeCargarAutorizacionesMasivas(); else noPermMasivas"
>
&check; &check;
</span> </span>
<ng-template #noPermExcel> <ng-template #noPermMasivas>
<span class="permission-icon">&times;</span> <span class="permission-icon">&times;</span>
</ng-template> </ng-template>
<span class="permission-text">Cargar pacientes (Excel)</span> <div class="permission-body">
<span class="permission-title">Cargar autorizaciones masivas</span>
<span class="permission-meta">Subir archivo con solicitudes</span>
</div>
</div>
<div
class="permission-item"
[class.has-permission]="puedeVerAutorizacionesPorFecha()"
[class.no-permission]="!puedeVerAutorizacionesPorFecha()"
>
<span
class="permission-icon"
*ngIf="puedeVerAutorizacionesPorFecha(); else noPermFechas"
>
&check;
</span>
<ng-template #noPermFechas>
<span class="permission-icon">&times;</span>
</ng-template>
<div class="permission-body">
<span class="permission-title">Ver autorizaciones por fecha</span>
<span class="permission-meta">Consultar por rango</span>
</div>
</div> </div>
<div <div
class="permission-item" class="permission-item"
[class.has-permission]="puedeDescargarPdfs()" [class.has-permission]="puedeDescargarPdfs()"
[class.no-permission]="!puedeDescargarPdfs()"
> >
<span class="permission-icon" *ngIf="puedeDescargarPdfs(); else noPermPdf"> <span class="permission-icon" *ngIf="puedeDescargarPdfs(); else noPermPdf">
&check; &check;
@ -230,12 +343,16 @@
<ng-template #noPermPdf> <ng-template #noPermPdf>
<span class="permission-icon">&times;</span> <span class="permission-icon">&times;</span>
</ng-template> </ng-template>
<span class="permission-text">Descargar PDFs</span> <div class="permission-body">
<span class="permission-title">Descargar PDFs</span>
<span class="permission-meta">Generar y descargar documentos</span>
</div>
</div> </div>
<div <div
class="permission-item" class="permission-item"
[class.has-permission]="puedeVerTodasAutorizaciones()" [class.has-permission]="puedeVerTodasAutorizaciones()"
[class.no-permission]="!puedeVerTodasAutorizaciones()"
> >
<span class="permission-icon" *ngIf="puedeVerTodasAutorizaciones(); else noPermAll"> <span class="permission-icon" *ngIf="puedeVerTodasAutorizaciones(); else noPermAll">
&check; &check;
@ -243,7 +360,95 @@
<ng-template #noPermAll> <ng-template #noPermAll>
<span class="permission-icon">&times;</span> <span class="permission-icon">&times;</span>
</ng-template> </ng-template>
<span class="permission-text">Ver todas las autorizaciones</span> <div class="permission-body">
<span class="permission-title">Ver todas las autorizaciones</span>
<span class="permission-meta">Incluye pendientes y no autorizadas</span>
</div>
</div>
<div
class="permission-item"
[class.has-permission]="puedeCargarPacientes()"
[class.no-permission]="!puedeCargarPacientes()"
>
<span class="permission-icon" *ngIf="puedeCargarPacientes(); else noPermPacientes">
&check;
</span>
<ng-template #noPermPacientes>
<span class="permission-icon">&times;</span>
</ng-template>
<div class="permission-body">
<span class="permission-title">Cargar pacientes (Excel)</span>
<span class="permission-meta">Importar datos masivos</span>
</div>
</div>
<div
class="permission-item"
[class.has-permission]="puedeCargarCups()"
[class.no-permission]="!puedeCargarCups()"
>
<span class="permission-icon" *ngIf="puedeCargarCups(); else noPermCups">
&check;
</span>
<ng-template #noPermCups>
<span class="permission-icon">&times;</span>
</ng-template>
<div class="permission-body">
<span class="permission-title">Cargar CUPS</span>
<span class="permission-meta">Actualizar procedimientos cubiertos</span>
</div>
</div>
<div
class="permission-item"
[class.has-permission]="puedeCargarIpsReps()"
[class.no-permission]="!puedeCargarIpsReps()"
>
<span class="permission-icon" *ngIf="puedeCargarIpsReps(); else noPermIpsReps">
&check;
</span>
<ng-template #noPermIpsReps>
<span class="permission-icon">&times;</span>
</ng-template>
<div class="permission-body">
<span class="permission-title">Cargar IPS y REPS</span>
<span class="permission-meta">Actualizar convenios y profesionales</span>
</div>
</div>
<div
class="permission-item"
[class.has-permission]="puedeVerEstadisticas()"
[class.no-permission]="!puedeVerEstadisticas()"
>
<span class="permission-icon" *ngIf="puedeVerEstadisticas(); else noPermStats">
&check;
</span>
<ng-template #noPermStats>
<span class="permission-icon">&times;</span>
</ng-template>
<div class="permission-body">
<span class="permission-title">Ver estadisticas</span>
<span class="permission-meta">Resumen por dia, semana y mes</span>
</div>
</div>
<div
class="permission-item"
[class.has-permission]="puedeGestionarUsuarios()"
[class.no-permission]="!puedeGestionarUsuarios()"
>
<span class="permission-icon" *ngIf="puedeGestionarUsuarios(); else noPermUsers">
&check;
</span>
<ng-template #noPermUsers>
<span class="permission-icon">&times;</span>
</ng-template>
<div class="permission-body">
<span class="permission-title">Gestionar usuarios</span>
<span class="permission-meta">Crear y editar accesos</span>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -1,16 +1,13 @@
import { import {
Component, Component,
OnInit, OnInit,
OnDestroy, OnDestroy
ChangeDetectorRef
} from '@angular/core'; } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AuthService } from '../../services/auth'; import { AuthService } from '../../services/auth';
import { PacienteService } from '../../services/paciente';
import { AppHeaderComponent } from '../shared/app-header/app-header'; import { AppHeaderComponent } from '../shared/app-header/app-header';
import { JobsService } from '../../services/jobs';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -23,18 +20,12 @@ export class DashboardComponent implements OnInit, OnDestroy {
currentUser: any = null; currentUser: any = null;
errorMessage: string | null = null; errorMessage: string | null = null;
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
mostrarTodasSedes = false;
// ---- Carga de Excel ----
cargandoExcel = false;
estadoCargaExcel: string | null = null;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute
private pacienteService: PacienteService,
private jobsService: JobsService,
private cdr: ChangeDetectorRef
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -98,10 +89,26 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.router.navigate(['/autorizaciones-por-fecha']); this.router.navigate(['/autorizaciones-por-fecha']);
} }
irAEstadisticasAutorizaciones(): void {
this.router.navigate(['/estadisticas-autorizaciones']);
}
irACargarCups(): void { irACargarCups(): void {
this.router.navigate(['/cargar-cups']); this.router.navigate(['/cargar-cups']);
} }
irACargarPacientes(): void {
this.router.navigate(['/cargar-pacientes']);
}
irACargarAutorizacionesMasivas(): void {
this.router.navigate(['/cargar-autorizaciones-masivas']);
}
irACargarIpsReps(): void {
this.router.navigate(['/cargar-ips-reps']);
}
irAUsuarios(): void { irAUsuarios(): void {
this.router.navigate(['/usuarios']); this.router.navigate(['/usuarios']);
} }
@ -149,6 +156,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
return this.authService.isAdministrador(); return this.authService.isAdministrador();
} }
puedeCargarIpsReps(): boolean {
return this.authService.isAdministrador();
}
puedeDescargarPdfs(): boolean { puedeDescargarPdfs(): boolean {
return this.authService.puedeDescargarPdfs(); return this.authService.puedeDescargarPdfs();
} }
@ -157,14 +168,42 @@ export class DashboardComponent implements OnInit, OnDestroy {
return this.authService.puedeVerTodasAutorizaciones(); return this.authService.puedeVerTodasAutorizaciones();
} }
puedeVerEstadisticas(): boolean {
return this.authService.isAdministrador();
}
puedeGenerarAutorizaciones(): boolean { puedeGenerarAutorizaciones(): boolean {
return this.authService.puedeGenerarAutorizaciones(); return this.authService.puedeGenerarAutorizaciones();
} }
puedeCargarAutorizacionesMasivas(): boolean {
return this.authService.puedeGenerarAutorizaciones();
}
puedeVerAutorizacionesPorFecha(): boolean {
return this.authService.isLoggedIn();
}
getSedesUsuario(): any[] { getSedesUsuario(): any[] {
return this.currentUser?.sedes || []; return this.currentUser?.sedes || [];
} }
getSedesMostradas(): any[] {
const sedes = this.getSedesUsuario();
if (this.mostrarTodasSedes || sedes.length <= 8) {
return sedes;
}
return sedes.slice(0, 8);
}
toggleSedesAsignadas(): void {
this.mostrarTodasSedes = !this.mostrarTodasSedes;
}
getTextoToggleSedes(): string {
return this.mostrarTodasSedes ? 'Ocultar' : 'Ver todas';
}
cerrarMensajeError(): void { cerrarMensajeError(): void {
this.errorMessage = null; this.errorMessage = null;
} }
@ -178,83 +217,4 @@ export class DashboardComponent implements OnInit, OnDestroy {
day: 'numeric' day: 'numeric'
}); });
} }
// =========================
// Cargar pacientes (Excel)
// =========================
abrirCargadorPacientes(input: HTMLInputElement): void {
if (!this.puedeCargarPacientes() || this.cargandoExcel) {
return;
}
this.estadoCargaExcel = null;
input.click();
}
onExcelSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) {
return;
}
this.cargandoExcel = true;
this.estadoCargaExcel = 'Subiendo archivo...';
const formData = new FormData();
formData.append('archivo', file); // mismo nombre que en server.js
this.pacienteService.cargarExcelPacientes(formData).subscribe({
next: (job) => {
this.estadoCargaExcel = 'Archivo en cola. Procesando...';
if (input) {
input.value = '';
}
this.jobsService.pollJob(job.id).subscribe({
next: (estado) => {
if (estado.status === 'completed') {
const partes: string[] = [];
if (estado.result?.mensaje) {
partes.push(estado.result.mensaje);
}
if (typeof estado.result?.activos === 'number') {
partes.push(`Pacientes activos: ${estado.result.activos}`);
}
if (typeof estado.result?.antiguos === 'number') {
partes.push(`Pacientes antiguos: ${estado.result.antiguos}`);
}
this.estadoCargaExcel =
partes.join(' ? ') || 'Archivo procesado correctamente.';
this.cargandoExcel = false;
}
if (estado.status === 'failed') {
this.estadoCargaExcel =
estado.error?.message ||
'Error procesando el Excel de pacientes.';
this.cargandoExcel = false;
}
this.cdr.detectChanges();
},
error: (error) => {
console.error(error);
this.estadoCargaExcel =
'Error consultando el estado del procesamiento.';
this.cargandoExcel = false;
this.cdr.detectChanges();
}
});
},
error: (err) => {
console.error(err);
this.estadoCargaExcel =
err?.error?.error || 'Error subiendo el Excel de pacientes.';
this.cargandoExcel = false;
if (input) {
input.value = '';
}
}
});
}
} }

View File

@ -0,0 +1,317 @@
.controles-card .form-row {
flex-wrap: wrap;
gap: 12px;
}
.controles-card label {
font-weight: 600;
color: var(--color-text-main);
}
.controles-card select,
.controles-card input[type="date"],
.controles-card input[type="month"],
.controles-card input[type="number"] {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--color-input-border);
background: var(--color-input-bg);
font-size: 0.9rem;
color: var(--color-text-main);
}
.controles-card select:focus,
.controles-card input[type="date"]:focus,
.controles-card input[type="month"]:focus,
.controles-card input[type="number"]:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-top: 16px;
}
.stat-card {
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 16px;
}
.stat-card h3 {
margin: 0;
font-size: 1rem;
color: var(--color-text-muted);
}
.stat-card.total {
background: var(--color-primary-soft);
}
.stat-value {
margin: 10px 0 0;
font-size: 1.8rem;
font-weight: 700;
}
.histogram-card {
margin-top: 20px;
position: relative;
overflow: hidden;
border-radius: 16px;
background: linear-gradient(180deg, var(--color-surface) 0%, var(--color-surface-alt) 100%);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
}
.histogram-card::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 20% 0%, rgba(25, 118, 210, 0.12), transparent 55%);
pointer-events: none;
}
.histogram-card > * {
position: relative;
z-index: 1;
}
.histogram-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.histogram-header h3 {
margin: 0;
font-size: 1.2rem;
color: var(--color-text-main);
}
.histogram-range {
font-size: 0.8rem;
color: var(--color-text-muted);
background: var(--color-surface-muted);
padding: 6px 10px;
border-radius: 999px;
}
.histogram {
display: flex;
align-items: flex-end;
gap: 6px;
height: 230px;
overflow-x: auto;
padding: 18px 14px 10px;
border-radius: 14px;
border: 1px solid var(--color-border);
background: var(--color-surface-muted);
background-image: linear-gradient(180deg, rgba(148, 163, 184, 0.15) 1px, transparent 1px);
background-size: 100% 26px;
}
.histogram-bar {
flex: 1 0 28px;
min-width: 26px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
}
.histogram-bar.selected .bar-track {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.25);
}
.histogram-bar.selected .bar-value {
opacity: 1;
transform: translateY(0);
border-color: var(--color-primary);
}
.bar-track {
width: 100%;
height: 170px;
background: transparent;
border-radius: 12px;
border: 1px solid transparent;
position: relative;
overflow: hidden;
}
.bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column-reverse;
border-radius: 10px;
overflow: hidden;
transition: height 0.4s ease;
}
.segment {
width: 100%;
transition: height 0.4s ease;
}
.segment.autorizadas {
background: #16a34a;
}
.segment.no-autorizadas {
background: #dc2626;
}
.segment.pendientes {
background: #f59e0b;
}
.segment:not(:first-child) {
border-top: 1px solid rgba(255, 255, 255, 0.65);
}
.bar-label {
margin-top: 4px;
font-size: 0.7rem;
color: var(--color-text-muted);
min-height: 14px;
}
.bar-value {
font-size: 0.75rem;
font-weight: 700;
color: var(--color-text-main);
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 999px;
padding: 2px 8px;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.histogram-bar:hover .bar-value {
opacity: 1;
transform: translateY(0);
}
.histogram-bar:hover .bar-track {
border-color: var(--color-primary-soft);
box-shadow: var(--shadow-card);
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
font-size: 0.85rem;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--color-surface);
border-radius: 999px;
border: 1px solid var(--color-border);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.legend-dot.autorizadas {
background: #2e7d32;
}
.legend-dot.no-autorizadas {
background: #c62828;
}
.legend-dot.pendientes {
background: #f9a825;
}
.detalle-card {
margin-top: 18px;
border-radius: 16px;
border: 1px solid var(--color-border);
background: var(--color-card);
box-shadow: var(--shadow-card);
}
.detalle-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.detalle-header h4 {
margin: 0;
font-size: 1.05rem;
color: var(--color-text-main);
}
.detalle-status {
font-size: 0.9rem;
color: var(--color-text-muted);
padding: 8px 0;
}
.detalle-status.error {
color: var(--color-error);
}
.detalle-empty {
font-size: 0.9rem;
color: var(--color-text-muted);
padding: 8px 0;
}
.detalle-table .estado-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
@media (max-width: 720px) {
.histogram-header {
flex-direction: column;
align-items: flex-start;
}
.histogram {
padding: 14px 10px 8px;
}
.detalle-header {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -0,0 +1,199 @@
<div class="page-shell">
<div class="content-container">
<app-header
[title]="titulo"
subtitle="Resumen mensual del estado de autorizaciones"
badgeText="SALUD UT"
[showUserInfo]="true"
[userName]="authService.getCurrentUser()?.nombre_completo || null"
[userRole]="authService.getCurrentUser()?.nombre_rol || null"
[showLogout]="true"
(logout)="authService.logout()"
[showBack]="true"
backLabel="Volver"
(back)="volverAtras()"
></app-header>
<div class="card controles-card">
<div class="form-row">
<label for="periodo">Periodo:</label>
<select id="periodo" [(ngModel)]="periodo" (change)="onPeriodoChange()">
<option value="dia">Dia</option>
<option value="semana">Semana</option>
<option value="mes">Mes</option>
<option value="anio">Año</option>
</select>
<ng-container *ngIf="periodo === 'dia' || periodo === 'semana'">
<label for="mesBase">Mes:</label>
<input
id="mesBase"
type="month"
[(ngModel)]="mesSeleccionado"
(change)="cargarEstadisticas()"
/>
</ng-container>
<ng-container *ngIf="periodo === 'mes' || periodo === 'anio'">
<label for="anio">Año:</label>
<input
id="anio"
type="number"
min="2020"
[(ngModel)]="anioSeleccionado"
(change)="cargarEstadisticas()"
/>
</ng-container>
<button class="btn btn-primary" (click)="cargarEstadisticas()" [disabled]="isLoading">
{{ isLoading ? 'Cargando...' : 'Actualizar' }}
</button>
</div>
</div>
<div class="status error" *ngIf="errorMessage">{{ errorMessage }}</div>
<div class="stats-grid" *ngIf="data">
<div class="stat-card">
<h3>Autorizadas</h3>
<p class="stat-value">{{ data.resumen.autorizadas }}</p>
</div>
<div class="stat-card">
<h3>No autorizadas</h3>
<p class="stat-value">{{ data.resumen.no_autorizadas }}</p>
</div>
<div class="stat-card">
<h3>Pendientes</h3>
<p class="stat-value">{{ data.resumen.pendientes }}</p>
</div>
<div class="stat-card total">
<h3>Total</h3>
<p class="stat-value">{{ data.resumen.total }}</p>
</div>
</div>
<div class="card histogram-card" *ngIf="data">
<div class="histogram-header">
<h3>Autorizaciones por dia</h3>
<span class="histogram-range">
{{ data.rango.inicio }} a {{ data.rango.fin }}
</span>
</div>
<div class="histogram">
<div
class="histogram-bar"
*ngFor="let bucket of chartBuckets"
[class.selected]="bucket.key === selectedBucket?.key"
[attr.title]="
bucket.inicio +
' a ' +
bucket.fin +
' | total: ' +
(bucket.total || 0) +
' | autorizadas: ' +
(bucket.autorizadas || 0) +
' | no autorizadas: ' +
(bucket.no_autorizadas || 0) +
' | pendientes: ' +
(bucket.pendientes || 0)
"
(click)="seleccionarBucket(bucket)"
>
<div class="bar-value" *ngIf="bucket.total">{{ bucket.total }}</div>
<div class="bar-track">
<div class="bar" [style.height]="getBarHeight(bucket)">
<div
class="segment autorizadas"
*ngIf="bucket.autorizadas > 0"
[style.height]="getSegmentHeight(bucket, 'autorizadas')"
></div>
<div
class="segment no-autorizadas"
*ngIf="bucket.no_autorizadas > 0"
[style.height]="getSegmentHeight(bucket, 'no_autorizadas')"
></div>
<div
class="segment pendientes"
*ngIf="tienePendientes && bucket.pendientes > 0"
[style.height]="getSegmentHeight(bucket, 'pendientes')"
></div>
</div>
</div>
<div class="bar-label">{{ bucket.label }}</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-dot autorizadas"></span>
Autorizadas
</div>
<div class="legend-item">
<span class="legend-dot no-autorizadas"></span>
No autorizadas
</div>
<div class="legend-item" *ngIf="tienePendientes">
<span class="legend-dot pendientes"></span>
Pendientes
</div>
</div>
</div>
<div class="card detalle-card" *ngIf="selectedBucket">
<div class="detalle-header">
<h4>Autorizaciones del {{ formatRangeLabel(selectedBucket.inicio, selectedBucket.fin) }}</h4>
<button class="btn btn-secondary btn-sm" type="button" (click)="limpiarDetalle()">
Cerrar
</button>
</div>
<div class="detalle-status" *ngIf="isLoadingDetalle">
Cargando autorizaciones del rango...
</div>
<div class="detalle-status error" *ngIf="errorDetalle">{{ errorDetalle }}</div>
<div
class="detalle-empty"
*ngIf="!isLoadingDetalle && !errorDetalle && autorizacionesDetalle.length === 0"
>
No hay autorizaciones en este rango.
</div>
<div
class="detalle-status"
*ngIf="detalleLimitado && !isLoadingDetalle && !errorDetalle"
>
Mostrando hasta 500 autorizaciones.
</div>
<div
class="table-container"
*ngIf="!isLoadingDetalle && !errorDetalle && autorizacionesDetalle.length > 0"
>
<table class="table detalle-table">
<thead>
<tr>
<th>Numero</th>
<th>Interno</th>
<th>Paciente</th>
<th>CUPS</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let aut of autorizacionesDetalle">
<td class="numero-autorizacion">{{ aut.numero_autorizacion }}</td>
<td class="interno">{{ aut.interno }}</td>
<td class="nombre-paciente">{{ aut.nombre_paciente }}</td>
<td class="cup-codigo">{{ aut.cup_codigo }}</td>
<td class="estado">
<span class="estado-pill">
{{ getEstadoAutorizacionLabel(aut.estado_autorizacion) }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,457 @@
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import {
AuthService,
EstadisticasAutorizaciones,
EstadisticasAutorizacionesDia,
} from '../../services/auth';
import { AppHeaderComponent } from '../shared/app-header/app-header';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'app-estadisticas-autorizaciones',
standalone: true,
imports: [CommonModule, FormsModule, AppHeaderComponent],
templateUrl: './estadisticas-autorizaciones.html',
styleUrls: ['./estadisticas-autorizaciones.css']
})
export class EstadisticasAutorizacionesComponent implements OnInit {
titulo = 'Estadisticas de autorizaciones';
mesSeleccionado = '';
anioSeleccionado = '';
periodo: 'dia' | 'semana' | 'mes' | 'anio' = 'mes';
isLoading = false;
errorMessage: string | null = null;
data: EstadisticasAutorizaciones | null = null;
maxTotal = 0;
chartBuckets: ChartBucket[] = [];
selectedBucket: ChartBucket | null = null;
autorizacionesDetalle: any[] = [];
isLoadingDetalle = false;
errorDetalle: string | null = null;
detalleLimitado = false;
tienePendientes = false;
constructor(
public authService: AuthService,
private router: Router,
private cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
if (!this.authService.isAdministrador()) {
this.errorMessage = 'No tienes permisos para acceder a esta pagina.';
return;
}
const hoy = new Date();
this.anioSeleccionado = String(hoy.getFullYear());
this.mesSeleccionado = this.formatMonthInput(hoy);
this.cargarEstadisticas();
}
onPeriodoChange(): void {
const hoy = new Date();
if (this.periodo === 'dia' || this.periodo === 'semana') {
if (!this.mesSeleccionado) {
this.mesSeleccionado = this.formatMonthInput(hoy);
}
}
if ((this.periodo === 'mes' || this.periodo === 'anio') && !this.anioSeleccionado) {
this.anioSeleccionado = String(hoy.getFullYear());
}
this.cargarEstadisticas();
}
cargarEstadisticas(): void {
this.errorMessage = null;
this.selectedBucket = null;
this.autorizacionesDetalle = [];
this.errorDetalle = null;
this.detalleLimitado = false;
let inicio = '';
let fin = '';
if (this.periodo === 'dia') {
if (!this.mesSeleccionado) {
this.errorMessage = 'Selecciona un mes para consultar.';
return;
}
[inicio, fin] = this.getRangoMes(this.mesSeleccionado);
} else if (this.periodo === 'semana') {
if (!this.mesSeleccionado) {
this.errorMessage = 'Selecciona un mes para consultar.';
return;
}
[inicio, fin] = this.getRangoMes(this.mesSeleccionado);
} else if (this.periodo === 'mes') {
if (!this.anioSeleccionado) {
this.errorMessage = 'Selecciona un año para consultar.';
return;
}
[inicio, fin] = this.getRangoAnio(this.anioSeleccionado);
} else {
if (!this.anioSeleccionado) {
this.errorMessage = 'Selecciona un año para consultar.';
return;
}
[inicio, fin] = this.getRangoAnios(this.anioSeleccionado);
}
if (!inicio || !fin) {
this.errorMessage = 'Rango de fechas invalido.';
return;
}
this.isLoading = true;
this.data = null;
this.maxTotal = 0;
this.authService
.getEstadisticasAutorizaciones(inicio, fin)
.pipe(
finalize(() => {
this.isLoading = false;
this.cdr.markForCheck();
})
)
.subscribe({
next: (resp) => {
this.data = resp;
this.chartBuckets = this.buildBuckets(resp?.dias || [], resp?.rango);
this.maxTotal = Math.max(
1,
...(this.chartBuckets.map((b) => Number(b.total) || 0))
);
this.tienePendientes = Number(resp?.resumen?.pendientes) > 0;
},
error: (err) => {
console.error(err);
this.errorMessage =
err?.error?.error || 'Error consultando estadisticas.';
},
});
}
volverAtras(): void {
this.router.navigate(['/dashboard']);
}
getBarHeight(bucket: ChartBucket): string {
if (!this.maxTotal) return '0%';
const total = Number(bucket.total) || 0;
const percent = Math.max(0, Math.min(100, (total / this.maxTotal) * 100));
return `${percent}%`;
}
getSegmentHeight(
bucket: ChartBucket,
key: 'autorizadas' | 'no_autorizadas' | 'pendientes'
): string {
const total = Number(bucket.total) || 0;
if (!total) return '0%';
const value = Number(bucket[key]) || 0;
const percent = Math.max(0, Math.min(100, (value / total) * 100));
return `${percent}%`;
}
seleccionarBucket(bucket: ChartBucket): void {
if (!bucket?.inicio || !bucket?.fin) {
return;
}
this.selectedBucket = bucket;
this.autorizacionesDetalle = [];
this.errorDetalle = null;
this.detalleLimitado = false;
this.isLoadingDetalle = false;
const total = Number(bucket.total) || 0;
if (!total) {
return;
}
this.isLoadingDetalle = true;
this.authService
.getAutorizacionesPorFecha(bucket.inicio, bucket.fin, 500, 0)
.pipe(
finalize(() => {
this.isLoadingDetalle = false;
this.cdr.markForCheck();
})
)
.subscribe({
next: (resp) => {
this.autorizacionesDetalle = resp || [];
this.detalleLimitado = this.autorizacionesDetalle.length >= 500;
},
error: (err) => {
console.error(err);
this.errorDetalle =
err?.error?.error || 'Error consultando autorizaciones del rango.';
}
});
}
limpiarDetalle(): void {
this.selectedBucket = null;
this.autorizacionesDetalle = [];
this.errorDetalle = null;
this.detalleLimitado = false;
}
formatDateLabel(fecha: string): string {
const date = new Date(fecha);
if (Number.isNaN(date.getTime())) return fecha;
return date.toLocaleDateString('es-CO', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
formatRangeLabel(inicio: string, fin: string): string {
if (inicio === fin) {
return this.formatDateLabel(inicio);
}
return `${this.formatDateLabel(inicio)} - ${this.formatDateLabel(fin)}`;
}
getEstadoAutorizacionLabel(estado: string | null | undefined): string {
const normalizado = String(estado || 'pendiente').toLowerCase();
if (normalizado === 'autorizado') {
return 'Autorizado';
}
if (normalizado === 'no_autorizado') {
return 'No autorizado';
}
return 'Pendiente';
}
private getRangoMes(mes: string): [string, string] {
const [yearStr, monthStr] = mes.split('-');
const year = Number(yearStr);
const monthIndex = Number(monthStr) - 1;
const inicio = new Date(year, monthIndex, 1);
const fin = new Date(year, monthIndex + 1, 0);
const hoy = new Date();
const esMesActual =
hoy.getFullYear() === year && hoy.getMonth() === monthIndex;
const fechaFin = esMesActual ? hoy : fin;
return [this.formatDate(inicio), this.formatDate(fechaFin)];
}
private getRangoAnio(anio: string): [string, string] {
const year = Number(anio);
if (!year) {
return ['', ''];
}
const inicio = new Date(year, 0, 1);
const fin = new Date(year, 11, 31);
const hoy = new Date();
const esAnioActual = hoy.getFullYear() === year;
const fechaFin = esAnioActual ? hoy : fin;
return [this.formatDate(inicio), this.formatDate(fechaFin)];
}
private getRangoAnios(anio: string): [string, string] {
const year = Number(anio);
if (!year) {
return ['', ''];
}
const inicio = new Date(year - 4, 0, 1);
const fin = new Date(year, 11, 31);
const hoy = new Date();
const fechaFin = hoy < fin ? hoy : fin;
return [this.formatDate(inicio), this.formatDate(fechaFin)];
}
private formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
private formatMonthInput(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
return `${year}-${month}`;
}
private buildBuckets(
dias: EstadisticasAutorizacionesDia[],
rango?: { inicio: string; fin: string }
): ChartBucket[] {
if (!rango) {
return [];
}
if (this.periodo === 'dia') {
return dias.map((dia) => {
const date = new Date(dia.fecha);
const label = Number.isNaN(date.getTime())
? ''
: String(date.getDate());
return {
key: dia.fecha,
label,
inicio: dia.fecha,
fin: dia.fecha,
total: Number(dia.total) || 0,
autorizadas: Number(dia.autorizadas) || 0,
no_autorizadas: Number(dia.no_autorizadas) || 0,
pendientes: Number(dia.pendientes) || 0,
};
});
}
const rangoInicio = new Date(rango.inicio);
const rangoFin = new Date(rango.fin);
if (this.periodo === 'semana') {
const buckets = new Map<string, ChartBucket>();
dias.forEach((dia) => {
const date = new Date(dia.fecha);
if (Number.isNaN(date.getTime())) return;
const weekStart = this.getWeekStart(date);
const weekEnd = this.addDays(weekStart, 6);
const key = this.formatDate(weekStart);
if (!buckets.has(key)) {
const inicio = this.maxDate(weekStart, rangoInicio);
const fin = this.minDate(weekEnd, rangoFin);
buckets.set(key, {
key,
label: this.formatWeekLabel(inicio, fin),
inicio: this.formatDate(inicio),
fin: this.formatDate(fin),
total: 0,
autorizadas: 0,
no_autorizadas: 0,
pendientes: 0,
});
}
const bucket = buckets.get(key)!;
bucket.total += Number(dia.total) || 0;
bucket.autorizadas += Number(dia.autorizadas) || 0;
bucket.no_autorizadas += Number(dia.no_autorizadas) || 0;
bucket.pendientes += Number(dia.pendientes) || 0;
});
return Array.from(buckets.values()).sort((a, b) =>
a.inicio.localeCompare(b.inicio)
);
}
if (this.periodo === 'mes') {
const buckets = new Map<string, ChartBucket>();
dias.forEach((dia) => {
const date = new Date(dia.fecha);
if (Number.isNaN(date.getTime())) return;
const key = `${date.getFullYear()}-${date.getMonth()}`;
if (!buckets.has(key)) {
const inicio = new Date(date.getFullYear(), date.getMonth(), 1);
const fin = new Date(date.getFullYear(), date.getMonth() + 1, 0);
const inicioClamp = this.maxDate(inicio, rangoInicio);
const finClamp = this.minDate(fin, rangoFin);
buckets.set(key, {
key,
label: date.toLocaleDateString('es-CO', { month: 'short' }),
inicio: this.formatDate(inicioClamp),
fin: this.formatDate(finClamp),
total: 0,
autorizadas: 0,
no_autorizadas: 0,
pendientes: 0,
});
}
const bucket = buckets.get(key)!;
bucket.total += Number(dia.total) || 0;
bucket.autorizadas += Number(dia.autorizadas) || 0;
bucket.no_autorizadas += Number(dia.no_autorizadas) || 0;
bucket.pendientes += Number(dia.pendientes) || 0;
});
return Array.from(buckets.values()).sort((a, b) =>
a.inicio.localeCompare(b.inicio)
);
}
const buckets = new Map<string, ChartBucket>();
dias.forEach((dia) => {
const date = new Date(dia.fecha);
if (Number.isNaN(date.getTime())) return;
const year = date.getFullYear();
const key = String(year);
if (!buckets.has(key)) {
const inicio = new Date(year, 0, 1);
const fin = new Date(year, 11, 31);
const inicioClamp = this.maxDate(inicio, rangoInicio);
const finClamp = this.minDate(fin, rangoFin);
buckets.set(key, {
key,
label: key,
inicio: this.formatDate(inicioClamp),
fin: this.formatDate(finClamp),
total: 0,
autorizadas: 0,
no_autorizadas: 0,
pendientes: 0,
});
}
const bucket = buckets.get(key)!;
bucket.total += Number(dia.total) || 0;
bucket.autorizadas += Number(dia.autorizadas) || 0;
bucket.no_autorizadas += Number(dia.no_autorizadas) || 0;
bucket.pendientes += Number(dia.pendientes) || 0;
});
return Array.from(buckets.values()).sort((a, b) =>
a.inicio.localeCompare(b.inicio)
);
}
private getWeekStart(date: Date): Date {
const base = new Date(date);
const day = base.getDay();
const diffToMonday = (day + 6) % 7;
base.setDate(base.getDate() - diffToMonday);
base.setHours(0, 0, 0, 0);
return base;
}
private addDays(date: Date, days: number): Date {
const next = new Date(date);
next.setDate(next.getDate() + days);
return next;
}
private maxDate(a: Date, b: Date): Date {
return a > b ? a : b;
}
private minDate(a: Date, b: Date): Date {
return a < b ? a : b;
}
private formatWeekLabel(inicio: Date, fin: Date): string {
const inicioDia = inicio.getDate();
const finDia = fin.getDate();
return `${inicioDia}-${finDia}`;
}
}
type ChartBucket = {
key: string;
label: string;
inicio: string;
fin: string;
total: number;
autorizadas: number;
no_autorizadas: number;
pendientes: number;
};

View File

@ -21,7 +21,7 @@
.subtitle { .subtitle {
margin: 4px 0 0; margin: 4px 0 0;
color: #666666; color: var(--color-text-muted);
font-size: 0.9rem; font-size: 0.9rem;
} }
@ -42,6 +42,7 @@
.form-row label { .form-row label {
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--color-text-main);
} }
.form-row input, .form-row input,
@ -49,8 +50,10 @@
.form-row textarea { .form-row textarea {
padding: 8px 10px; padding: 8px 10px;
border-radius: 6px; border-radius: 6px;
border: 1px solid #ccc; border: 1px solid var(--color-input-border);
font-size: 0.9rem; font-size: 0.9rem;
background: var(--color-input-bg);
color: var(--color-text-main);
} }
.form-row input:focus, .form-row input:focus,
@ -67,7 +70,7 @@
.hint { .hint {
font-size: 0.75rem; font-size: 0.75rem;
color: #777777; color: var(--color-text-muted);
} }
/* Tabla */ /* Tabla */
@ -82,17 +85,17 @@
} }
.tabla-usuarios-wrapper::-webkit-scrollbar-track { .tabla-usuarios-wrapper::-webkit-scrollbar-track {
background: #f1f1f1; background: var(--color-surface-muted);
border-radius: 4px; border-radius: 4px;
} }
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb { .tabla-usuarios-wrapper::-webkit-scrollbar-thumb {
background: #c0c0c0; background: var(--color-border);
border-radius: 4px; border-radius: 4px;
} }
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb:hover { .tabla-usuarios-wrapper::-webkit-scrollbar-thumb:hover {
background: #a0a0a0; background: var(--color-text-muted);
} }
.tabla-usuarios thead { .tabla-usuarios thead {
@ -106,6 +109,43 @@
padding: 8px 10px; padding: 8px 10px;
} }
.input-inline {
width: 100%;
min-width: 120px;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--color-input-border);
font-size: 0.85rem;
background: var(--color-input-bg);
color: var(--color-text-main);
}
.input-inline:focus {
border-color: #1976d2;
box-shadow: 0 0 0 1px rgba(25, 118, 210, 0.15);
outline: none;
}
.acciones-inline {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.inline-status {
margin-top: 4px;
font-size: 0.75rem;
}
.inline-status.ok {
color: var(--color-success);
}
.inline-status.error {
color: var(--color-error);
}
@media (max-width: 900px) { @media (max-width: 900px) {
.usuarios-grid { .usuarios-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@ -3,11 +3,15 @@
<app-header <app-header
title="SALUD UT" title="SALUD UT"
subtitle="Gestión de Usuarios" subtitle="Gestión de Usuarios"
badgeText="SALUD UT"
[showUserInfo]="true" [showUserInfo]="true"
[userName]="getNombreUsuario()" [userName]="getNombreUsuario()"
[userRole]="getNombreRolFormateado()" [userRole]="getNombreRolFormateado()"
[showLogout]="true" [showLogout]="true"
(logout)="logout()" (logout)="logout()"
[showBack]="true"
backLabel="Volver"
(back)="volverDashboard()"
></app-header> ></app-header>
<header class="usuarios-header"> <header class="usuarios-header">
@ -17,10 +21,7 @@
Crear, activar y desactivar usuarios del sistema Crear, activar y desactivar usuarios del sistema
</p> </p>
</div> </div>
<button class="btn btn-secondary" (click)="volverDashboard()"> </header>
Volver al dashboard
</button>
</header>
<section class="usuarios-grid"> <section class="usuarios-grid">
<!-- Formulario de creación --> <!-- Formulario de creación -->
@ -57,19 +58,6 @@
</select> </select>
</div> </div>
<div class="form-row" *ngIf="nuevoUsuario.id_rol === 2">
<label>Sedes (códigos):</label>
<textarea
rows="2"
[(ngModel)]="sedesTexto"
placeholder="Ej: 113, 148, 205 (separadas por coma)"
></textarea>
<small class="hint">
Solo para rol <strong>administrativo_sede</strong>. Usa los códigos
de establecimiento, separados por coma.
</small>
</div>
<div class="form-row acciones"> <div class="form-row acciones">
<button class="btn btn-primary" (click)="crearUsuario()" [disabled]="creando"> <button class="btn btn-primary" (click)="crearUsuario()" [disabled]="creando">
{{ creando ? 'Creando...' : 'Crear usuario' }} {{ creando ? 'Creando...' : 'Crear usuario' }}
@ -160,9 +148,29 @@
<tr *ngFor="let a of autorizantes; trackBy: trackByAutorizante"> <tr *ngFor="let a of autorizantes; trackBy: trackByAutorizante">
<td>{{ a.numero_documento }}</td> <td>{{ a.numero_documento }}</td>
<td>{{ a.tipo_documento }}</td> <td>{{ a.tipo_documento }}</td>
<td>{{ a.nombre }}</td> <td>
<td>{{ a.telefono || '-' }}</td> <input
<td>{{ a.cargo || '-' }}</td> class="input-inline"
type="text"
[(ngModel)]="autorizanteEdicion[a.numero_documento].nombre"
/>
</td>
<td>
<input
class="input-inline"
type="text"
[(ngModel)]="autorizanteEdicion[a.numero_documento].telefono"
placeholder="-"
/>
</td>
<td>
<input
class="input-inline"
type="text"
[(ngModel)]="autorizanteEdicion[a.numero_documento].cargo"
placeholder="-"
/>
</td>
<td> <td>
<span <span
class="badge" class="badge"
@ -173,20 +181,45 @@
</span> </span>
</td> </td>
<td> <td>
<button <div class="acciones-inline">
*ngIf="a.activo" <button
class="btn btn-danger btn-sm" class="btn btn-primary btn-sm"
(click)="cambiarEstadoAutorizante(a, false)" (click)="guardarAutorizante(a)"
[disabled]="autorizanteEdicion[a.numero_documento].guardando"
>
{{
autorizanteEdicion[a.numero_documento].guardando
? 'Guardando...'
: 'Guardar'
}}
</button>
<button
*ngIf="a.activo"
class="btn btn-danger btn-sm"
(click)="cambiarEstadoAutorizante(a, false)"
>
Desactivar
</button>
<button
*ngIf="!a.activo"
class="btn btn-secondary btn-sm"
(click)="cambiarEstadoAutorizante(a, true)"
>
Activar
</button>
</div>
<div
class="inline-status ok"
*ngIf="autorizanteEdicion[a.numero_documento]?.mensaje"
> >
Desactivar {{ autorizanteEdicion[a.numero_documento].mensaje }}
</button> </div>
<button <div
*ngIf="!a.activo" class="inline-status error"
class="btn btn-secondary btn-sm" *ngIf="autorizanteEdicion[a.numero_documento]?.error"
(click)="cambiarEstadoAutorizante(a, true)"
> >
Activar {{ autorizanteEdicion[a.numero_documento].error }}
</button> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -210,6 +243,7 @@
<th>Nombre</th> <th>Nombre</th>
<th>Email</th> <th>Email</th>
<th>Rol</th> <th>Rol</th>
<th>Nueva contrasena</th>
<th>Estado</th> <th>Estado</th>
<th>Último login</th> <th>Último login</th>
<th>Acciones</th> <th>Acciones</th>
@ -217,10 +251,45 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let u of usuarios; trackBy: trackByUsuario"> <tr *ngFor="let u of usuarios; trackBy: trackByUsuario">
<td>{{ u.username }}</td> <td>
<td>{{ u.nombre_completo }}</td> <input
<td>{{ u.email }}</td> class="input-inline"
<td>{{ u.nombre_rol }}</td> type="text"
[(ngModel)]="usuarioEdicion[u.id_usuario].username"
/>
</td>
<td>
<input
class="input-inline"
type="text"
[(ngModel)]="usuarioEdicion[u.id_usuario].nombre_completo"
/>
</td>
<td>
<input
class="input-inline"
type="email"
[(ngModel)]="usuarioEdicion[u.id_usuario].email"
/>
</td>
<td>
<select
class="input-inline"
[(ngModel)]="usuarioEdicion[u.id_usuario].id_rol"
>
<option *ngFor="let r of roles" [ngValue]="r.id_rol">
{{ r.nombre_rol }}
</option>
</select>
</td>
<td>
<input
class="input-inline"
type="password"
[(ngModel)]="usuarioEdicion[u.id_usuario].password"
placeholder="Nueva contrasena"
/>
</td>
<td> <td>
<span <span
class="badge" class="badge"
@ -234,20 +303,45 @@
{{ u.ultimo_login ? (u.ultimo_login | date:'yyyy-MM-dd HH:mm') : '—' }} {{ u.ultimo_login ? (u.ultimo_login | date:'yyyy-MM-dd HH:mm') : '—' }}
</td> </td>
<td> <td>
<button <div class="acciones-inline">
*ngIf="u.activo" <button
class="btn btn-danger btn-sm" class="btn btn-primary btn-sm"
(click)="cambiarEstadoUsuario(u, false)" (click)="guardarUsuario(u)"
[disabled]="usuarioEdicion[u.id_usuario].guardando"
>
{{
usuarioEdicion[u.id_usuario].guardando
? 'Guardando...'
: 'Guardar'
}}
</button>
<button
*ngIf="u.activo"
class="btn btn-danger btn-sm"
(click)="cambiarEstadoUsuario(u, false)"
>
Desactivar
</button>
<button
*ngIf="!u.activo"
class="btn btn-secondary btn-sm"
(click)="cambiarEstadoUsuario(u, true)"
>
Activar
</button>
</div>
<div
class="inline-status ok"
*ngIf="usuarioEdicion[u.id_usuario]?.mensaje"
> >
Desactivar {{ usuarioEdicion[u.id_usuario].mensaje }}
</button> </div>
<button <div
*ngIf="!u.activo" class="inline-status error"
class="btn btn-secondary btn-sm" *ngIf="usuarioEdicion[u.id_usuario]?.error"
(click)="cambiarEstadoUsuario(u, true)"
> >
Activar {{ usuarioEdicion[u.id_usuario].error }}
</button> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -3,17 +3,39 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { import {
AuthService, AuthService,
ActualizarUsuarioPayload,
RegisterRequest, RegisterRequest,
Rol Rol
} from '../../services/auth'; } from '../../services/auth';
import { import {
Autorizante, Autorizante,
ActualizarAutorizantePayload,
CrearAutorizantePayload, CrearAutorizantePayload,
PacienteService PacienteService
} from '../../services/paciente'; } from '../../services/paciente';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AppHeaderComponent } from '../shared/app-header/app-header'; import { AppHeaderComponent } from '../shared/app-header/app-header';
interface AutorizanteEdicion {
nombre: string;
telefono: string;
cargo: string;
guardando: boolean;
mensaje: string | null;
error: string | null;
}
interface UsuarioEdicion {
username: string;
nombre_completo: string;
email: string;
id_rol: number;
password: string;
guardando: boolean;
mensaje: string | null;
error: string | null;
}
@Component({ @Component({
selector: 'app-usuarios', selector: 'app-usuarios',
standalone: true, standalone: true,
@ -26,6 +48,8 @@ export class UsuariosComponent implements OnInit {
usuarios: any[] = []; usuarios: any[] = [];
roles: Rol[] = []; roles: Rol[] = [];
autorizantes: Autorizante[] = []; autorizantes: Autorizante[] = [];
autorizanteEdicion: Record<number, AutorizanteEdicion> = {};
usuarioEdicion: Record<number, UsuarioEdicion> = {};
cargandoUsuarios = false; cargandoUsuarios = false;
errorUsuarios: string | null = null; errorUsuarios: string | null = null;
@ -43,8 +67,6 @@ export class UsuariosComponent implements OnInit {
mensajeAutorizanteOk: string | null = null; mensajeAutorizanteOk: string | null = null;
mensajeAutorizanteError: string | null = null; mensajeAutorizanteError: string | null = null;
// para administrativos, códigos separados por coma
sedesTexto = '';
nuevoUsuario: RegisterRequest = { nuevoUsuario: RegisterRequest = {
username: '', username: '',
@ -134,6 +156,7 @@ export class UsuariosComponent implements OnInit {
this.authService.getUsuarios(this.limiteUsuarios, 0).subscribe({ this.authService.getUsuarios(this.limiteUsuarios, 0).subscribe({
next: (usuarios) => { next: (usuarios) => {
this.usuarios = usuarios; this.usuarios = usuarios;
this.prepararEdicionUsuarios();
this.avisoUsuarios = usuarios.length >= this.limiteUsuarios; this.avisoUsuarios = usuarios.length >= this.limiteUsuarios;
this.cargandoUsuarios = false; this.cargandoUsuarios = false;
this.cdr.detectChanges(); this.cdr.detectChanges();
@ -161,17 +184,7 @@ export class UsuariosComponent implements OnInit {
return; return;
} }
// Si es administrativo de sede (id_rol = 2) procesamos las sedes this.nuevoUsuario.sedes = [];
if (this.nuevoUsuario.id_rol === 2) {
const codigos = this.sedesTexto
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
this.nuevoUsuario.sedes = codigos;
} else {
this.nuevoUsuario.sedes = [];
}
this.creando = true; this.creando = true;
@ -189,7 +202,6 @@ export class UsuariosComponent implements OnInit {
id_rol: 2, id_rol: 2,
sedes: [] sedes: []
}; };
this.sedesTexto = '';
// Recargar lista sin refrescar la página // Recargar lista sin refrescar la página
this.cargarUsuarios(); this.cargarUsuarios();
@ -290,6 +302,7 @@ export class UsuariosComponent implements OnInit {
this.pacienteService.obtenerAutorizantesAdmin().subscribe({ this.pacienteService.obtenerAutorizantesAdmin().subscribe({
next: (autorizantes) => { next: (autorizantes) => {
this.autorizantes = autorizantes; this.autorizantes = autorizantes;
this.prepararEdicionAutorizantes();
this.cargandoAutorizantes = false; this.cargandoAutorizantes = false;
this.cdr.detectChanges(); this.cdr.detectChanges();
}, },
@ -328,4 +341,219 @@ export class UsuariosComponent implements OnInit {
trackByAutorizante(_index: number, autorizante: Autorizante): number | string { trackByAutorizante(_index: number, autorizante: Autorizante): number | string {
return autorizante?.numero_documento ?? _index; return autorizante?.numero_documento ?? _index;
} }
private prepararEdicionAutorizantes(): void {
const edicion: Record<number, AutorizanteEdicion> = {};
this.autorizantes.forEach((a) => {
edicion[a.numero_documento] = {
nombre: a.nombre || '',
telefono: a.telefono || '',
cargo: a.cargo || '',
guardando: false,
mensaje: null,
error: null
};
});
this.autorizanteEdicion = edicion;
}
private prepararEdicionUsuarios(): void {
const edicion: Record<number, UsuarioEdicion> = {};
this.usuarios.forEach((u) => {
edicion[u.id_usuario] = {
username: u.username || '',
nombre_completo: u.nombre_completo || '',
email: u.email || '',
id_rol: u.id_rol || 0,
password: '',
guardando: false,
mensaje: null,
error: null
};
});
this.usuarioEdicion = edicion;
}
guardarAutorizante(a: Autorizante): void {
const edicion = this.autorizanteEdicion[a.numero_documento];
if (!edicion) {
return;
}
edicion.mensaje = null;
edicion.error = null;
const nombreNuevo = (edicion.nombre || '').trim();
const telefonoNuevo = (edicion.telefono || '').trim();
const cargoNuevo = (edicion.cargo || '').trim();
const nombreActual = (a.nombre || '').trim();
const telefonoActual = (a.telefono || '').trim();
const cargoActual = (a.cargo || '').trim();
if (!nombreNuevo) {
edicion.error = 'El nombre no puede quedar vacio.';
return;
}
if (
nombreNuevo === nombreActual &&
telefonoNuevo === telefonoActual &&
cargoNuevo === cargoActual
) {
edicion.mensaje = 'Sin cambios.';
return;
}
const payload: ActualizarAutorizantePayload = {};
if (nombreNuevo !== nombreActual) {
payload.nombre = nombreNuevo;
}
if (telefonoNuevo !== telefonoActual) {
payload.telefono = telefonoNuevo.length ? telefonoNuevo : null;
}
if (cargoNuevo !== cargoActual) {
payload.cargo = cargoNuevo.length ? cargoNuevo : null;
}
edicion.guardando = true;
this.pacienteService.actualizarDatosAutorizante(a.numero_documento, payload).subscribe({
next: (resp: any) => {
const actualizado = resp?.autorizante;
if (actualizado) {
a.nombre = actualizado.nombre;
a.telefono = actualizado.telefono;
a.cargo = actualizado.cargo;
} else {
a.nombre = payload.nombre || a.nombre;
a.telefono = payload.telefono ?? undefined;
a.cargo = payload.cargo ?? undefined;
}
edicion.nombre = a.nombre || '';
edicion.telefono = a.telefono || '';
edicion.cargo = a.cargo || '';
edicion.guardando = false;
edicion.mensaje = 'Actualizado.';
this.cdr.detectChanges();
},
error: (err) => {
console.error(err);
edicion.guardando = false;
edicion.error = err?.error?.error || 'Error actualizando autorizante.';
this.cdr.detectChanges();
}
});
}
guardarUsuario(u: any): void {
const edicion = this.usuarioEdicion[u.id_usuario];
if (!edicion) {
return;
}
edicion.mensaje = null;
edicion.error = null;
const usernameNuevo = (edicion.username || '').trim();
const nombreNuevo = (edicion.nombre_completo || '').trim();
const emailNuevo = (edicion.email || '').trim();
const rolNuevo = Number(edicion.id_rol);
const usernameActual = (u.username || '').trim();
const nombreActual = (u.nombre_completo || '').trim();
const emailActual = (u.email || '').trim();
const rolActual = Number(u.id_rol);
const passwordNueva = edicion.password || '';
if (!usernameNuevo) {
edicion.error = 'El usuario no puede quedar vacio.';
return;
}
if (!nombreNuevo) {
edicion.error = 'El nombre no puede quedar vacio.';
return;
}
if (!emailNuevo) {
edicion.error = 'El email no puede quedar vacio.';
return;
}
if (!Number.isFinite(rolNuevo) || rolNuevo <= 0) {
edicion.error = 'El rol no es valido.';
return;
}
const payload: ActualizarUsuarioPayload = {};
if (usernameNuevo !== usernameActual) {
payload.username = usernameNuevo;
}
if (nombreNuevo !== nombreActual) {
payload.nombre_completo = nombreNuevo;
}
if (emailNuevo !== emailActual) {
payload.email = emailNuevo;
}
if (rolNuevo !== rolActual) {
payload.id_rol = rolNuevo;
}
if (passwordNueva) {
payload.password = passwordNueva;
}
if (!payload.username && !payload.password && !payload.nombre_completo && !payload.email && !payload.id_rol) {
edicion.mensaje = 'Sin cambios.';
return;
}
edicion.guardando = true;
this.authService.actualizarUsuario(u.id_usuario, payload).subscribe({
next: (resp: any) => {
const actualizado = resp?.usuario;
if (actualizado?.username) {
u.username = actualizado.username;
} else if (payload.username) {
u.username = payload.username;
}
if (actualizado?.nombre_completo) {
u.nombre_completo = actualizado.nombre_completo;
} else if (payload.nombre_completo) {
u.nombre_completo = payload.nombre_completo;
}
if (actualizado?.email) {
u.email = actualizado.email;
} else if (payload.email) {
u.email = payload.email;
}
if (actualizado?.id_rol) {
u.id_rol = actualizado.id_rol;
const rolEncontrado = this.roles.find((r) => r.id_rol === actualizado.id_rol);
if (rolEncontrado) {
u.nombre_rol = rolEncontrado.nombre_rol;
}
} else if (payload.id_rol) {
u.id_rol = payload.id_rol;
const rolEncontrado = this.roles.find((r) => r.id_rol === payload.id_rol);
if (rolEncontrado) {
u.nombre_rol = rolEncontrado.nombre_rol;
}
}
edicion.username = u.username || '';
edicion.nombre_completo = u.nombre_completo || '';
edicion.email = u.email || '';
edicion.id_rol = u.id_rol || rolNuevo;
edicion.password = '';
edicion.guardando = false;
edicion.mensaje = 'Actualizado.';
this.cdr.detectChanges();
},
error: (err) => {
console.error(err);
edicion.guardando = false;
edicion.error = err?.error?.error || 'Error actualizando usuario.';
this.cdr.detectChanges();
}
});
}
} }

View File

@ -6,5 +6,17 @@ declare global {
} }
} }
const rawApiBaseUrl = window.__SALUDUT_CONFIG__?.apiBaseUrl || '/api'; const getDefaultApiBaseUrl = (): string => {
const host = window.location.hostname;
const port = window.location.port;
const isLocalhost = host === 'localhost' || host === '127.0.0.1';
if (isLocalhost && port === '4200') {
return 'http://localhost:3000/api';
}
return '/api';
};
const rawApiBaseUrl = window.__SALUDUT_CONFIG__?.apiBaseUrl || getDefaultApiBaseUrl();
export const API_BASE_URL = rawApiBaseUrl.replace(/\/+$/, ''); export const API_BASE_URL = rawApiBaseUrl.replace(/\/+$/, '');

View File

@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from '../services/auth';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.authService.logout();
}
return throwError(() => error);
})
);
}
}

View File

@ -35,6 +35,14 @@ export interface RegisterRequest {
sedes?: string[]; sedes?: string[];
} }
export interface ActualizarUsuarioPayload {
username?: string;
password?: string;
email?: string;
nombre_completo?: string;
id_rol?: number;
}
/* Interfaz Rol para la pantalla de usuarios */ /* Interfaz Rol para la pantalla de usuarios */
export interface Rol { export interface Rol {
id_rol: number; id_rol: number;
@ -42,6 +50,25 @@ export interface Rol {
descripcion?: string | null; descripcion?: string | null;
} }
export interface EstadisticasAutorizacionesDia {
fecha: string;
total: number;
autorizadas: number;
no_autorizadas: number;
pendientes: number;
}
export interface EstadisticasAutorizaciones {
rango: { inicio: string; fin: string };
resumen: {
autorizadas: number;
no_autorizadas: number;
pendientes: number;
total: number;
};
dias: EstadisticasAutorizacionesDia[];
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -55,6 +82,7 @@ export class AuthService {
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false); private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
private logoutTimeoutId: ReturnType<typeof setTimeout> | null = null;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
@ -70,6 +98,7 @@ export class AuthService {
if (token && user) { if (token && user) {
this.currentUserSubject.next(user); this.currentUserSubject.next(user);
this.isAuthenticatedSubject.next(true); this.isAuthenticatedSubject.next(true);
this.scheduleTokenExpiry(token);
} else { } else {
this.clearAuth(); this.clearAuth();
} }
@ -78,7 +107,7 @@ export class AuthService {
// =========== LOGIN =========== // =========== LOGIN ===========
login(credentials: LoginRequest): Observable<LoginResponse> { login(credentials: LoginRequest): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`${this.API_URL}/api/auth/login`, credentials).pipe( return this.http.post<LoginResponse>(`${this.API_URL}/auth/login`, credentials).pipe(
tap(response => { tap(response => {
this.setAuth(response.token, response.usuario); this.setAuth(response.token, response.usuario);
}), }),
@ -93,7 +122,7 @@ export class AuthService {
register(userData: RegisterRequest): Observable<any> { register(userData: RegisterRequest): Observable<any> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
return this.http.post(`${this.API_URL}/api/auth/register`, userData, { headers }).pipe( return this.http.post(`${this.API_URL}/auth/register`, userData, { headers }).pipe(
catchError((error: any) => { catchError((error: any) => {
console.error('Error en register:', error); console.error('Error en register:', error);
return throwError(() => error); return throwError(() => error);
@ -112,7 +141,7 @@ export class AuthService {
verifyToken(): Observable<{ usuario: User }> { verifyToken(): Observable<{ usuario: User }> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
return this.http.get<{ usuario: User }>(`${this.API_URL}/api/auth/verify`, { headers }).pipe( return this.http.get<{ usuario: User }>(`${this.API_URL}/auth/verify`, { headers }).pipe(
tap(response => { tap(response => {
this.updateUser(response.usuario); this.updateUser(response.usuario);
}) })
@ -130,15 +159,54 @@ export class AuthService {
localStorage.setItem(this.USER_KEY, JSON.stringify(user)); localStorage.setItem(this.USER_KEY, JSON.stringify(user));
this.currentUserSubject.next(user); this.currentUserSubject.next(user);
this.isAuthenticatedSubject.next(true); this.isAuthenticatedSubject.next(true);
this.scheduleTokenExpiry(token);
} }
private clearAuth(): void { private clearAuth(): void {
this.clearLogoutTimer();
localStorage.removeItem(this.TOKEN_KEY); localStorage.removeItem(this.TOKEN_KEY);
localStorage.removeItem(this.USER_KEY); localStorage.removeItem(this.USER_KEY);
this.currentUserSubject.next(null); this.currentUserSubject.next(null);
this.isAuthenticatedSubject.next(false); this.isAuthenticatedSubject.next(false);
} }
private scheduleTokenExpiry(token: string): void {
const exp = this.getTokenExpiry(token);
if (!exp) {
return;
}
const timeoutMs = exp - Date.now();
if (timeoutMs <= 0) {
this.logout();
return;
}
this.clearLogoutTimer();
this.logoutTimeoutId = setTimeout(() => {
this.logout();
}, timeoutMs);
}
private clearLogoutTimer(): void {
if (this.logoutTimeoutId) {
clearTimeout(this.logoutTimeoutId);
this.logoutTimeoutId = null;
}
}
private getTokenExpiry(token: string): number | null {
try {
const payload = token.split('.')[1];
if (!payload) return null;
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '=');
const decoded = JSON.parse(atob(padded));
if (!decoded?.exp) return null;
return decoded.exp * 1000;
} catch {
return null;
}
}
private updateUser(user: User): void { private updateUser(user: User): void {
localStorage.setItem(this.USER_KEY, JSON.stringify(user)); localStorage.setItem(this.USER_KEY, JSON.stringify(user));
this.currentUserSubject.next(user); this.currentUserSubject.next(user);
@ -183,7 +251,8 @@ export class AuthService {
} }
puedeDescargarPdfs(): boolean { puedeDescargarPdfs(): boolean {
return this.isAdministrador(); const user = this.getCurrentUser();
return !!user && (user.nombre_rol === 'administrador' || user.nombre_rol === 'administrativo_sede');
} }
puedeVerTodasAutorizaciones(): boolean { puedeVerTodasAutorizaciones(): boolean {
@ -213,18 +282,27 @@ export class AuthService {
getUsuarios(limit = 200, offset = 0): Observable<any[]> { getUsuarios(limit = 200, offset = 0): Observable<any[]> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
const params = { limit, offset }; const params = { limit, offset };
return this.http.get<any[]>(`${this.API_URL}/api/usuarios`, { headers, params }); return this.http.get<any[]>(`${this.API_URL}/usuarios`, { headers, params });
} }
cambiarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> { cambiarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
return this.http.patch( return this.http.patch(
`${this.API_URL}/api/usuarios/${idUsuario}/estado`, `${this.API_URL}/usuarios/${idUsuario}/estado`,
{ activo }, { activo },
{ headers } { headers }
); );
} }
actualizarUsuario(idUsuario: number, payload: ActualizarUsuarioPayload): Observable<any> {
const headers = this.getAuthHeaders();
return this.http.patch(
`${this.API_URL}/usuarios/${idUsuario}`,
payload,
{ headers }
);
}
// Alias usado en usuarios.ts // Alias usado en usuarios.ts
actualizarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> { actualizarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
return this.cambiarEstadoUsuario(idUsuario, activo); return this.cambiarEstadoUsuario(idUsuario, activo);
@ -239,13 +317,27 @@ export class AuthService {
): Observable<any[]> { ): Observable<any[]> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset }; const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset };
return this.http.get<any[]>(`${this.API_URL}/api/autorizaciones-por-fecha`, { headers, params }); return this.http.get<any[]>(`${this.API_URL}/autorizaciones-por-fecha`, { headers, params });
} }
// Obtener roles desde el backend // Obtener roles desde el backend
getRoles(): Observable<Rol[]> { getRoles(): Observable<Rol[]> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
return this.http.get<Rol[]>(`${this.API_URL}/api/roles`, { headers }); return this.http.get<Rol[]>(`${this.API_URL}/roles`, { headers });
}
getEstadisticasAutorizaciones(
fechaInicio?: string,
fechaFin?: string
): Observable<EstadisticasAutorizaciones> {
const headers = this.getAuthHeaders();
const params: any = {};
if (fechaInicio) params.fecha_inicio = fechaInicio;
if (fechaFin) params.fecha_fin = fechaFin;
return this.http.get<EstadisticasAutorizaciones>(
`${this.API_URL}/autorizaciones-estadisticas`,
{ headers, params }
);
} }
crearJobPdfAutorizacion( crearJobPdfAutorizacion(
@ -258,7 +350,7 @@ export class AuthService {
payload.version = version; payload.version = version;
} }
return this.http.post<JobResponse>( return this.http.post<JobResponse>(
`${this.API_URL}/api/jobs/autorizacion-pdf`, `${this.API_URL}/jobs/autorizacion-pdf`,
payload, payload,
{ headers } { headers }
); );
@ -267,7 +359,7 @@ export class AuthService {
crearJobZipAutorizaciones(fechaInicio: string, fechaFin: string): Observable<JobResponse> { crearJobZipAutorizaciones(fechaInicio: string, fechaFin: string): Observable<JobResponse> {
const headers = this.getAuthHeaders(); const headers = this.getAuthHeaders();
return this.http.post<JobResponse>( return this.http.post<JobResponse>(
`${this.API_URL}/api/jobs/autorizaciones-zip`, `${this.API_URL}/jobs/autorizaciones-zip`,
{ fecha_inicio: fechaInicio, fecha_fin: fechaFin }, { fecha_inicio: fechaInicio, fecha_fin: fechaFin },
{ headers } { headers }
); );

View File

@ -4,6 +4,11 @@ export interface JobError {
message?: string; message?: string;
} }
export interface JobRowError {
fila?: number;
error?: string;
}
export interface JobResult { export interface JobResult {
ok?: boolean; ok?: boolean;
mensaje?: string; mensaje?: string;
@ -11,6 +16,20 @@ export interface JobResult {
antiguos?: number | null; antiguos?: number | null;
referencia?: number | null; referencia?: number | null;
cubiertosActivos?: number | null; cubiertosActivos?: number | null;
total?: number | null;
insertados?: number | null;
actualizados?: number | null;
omitidos?: number | null;
omitidas?: number | null;
desactivados?: number | null;
duplicados?: number | null;
creadas?: number | null;
sin_paciente?: number | null;
sin_cups?: number | null;
sin_ips?: number | null;
cups_no_cubiertos?: number | null;
ips_sin_convenio?: number | null;
errores?: JobRowError[];
downloadUrl?: string; downloadUrl?: string;
fileName?: string; fileName?: string;
contentType?: string; contentType?: string;

View File

@ -19,7 +19,7 @@ export class JobsService {
getJob(jobId: string): Observable<JobResponse> { getJob(jobId: string): Observable<JobResponse> {
const headers = this.authService.getAuthHeaders(); const headers = this.authService.getAuthHeaders();
return this.http.get<JobResponse>(`${this.API_URL}/api/jobs/${jobId}`, { headers }); return this.http.get<JobResponse>(`${this.API_URL}/jobs/${jobId}`, { headers });
} }
pollJob(jobId: string, intervalMs = 2000): Observable<JobResponse> { pollJob(jobId: string, intervalMs = 2000): Observable<JobResponse> {
@ -34,7 +34,7 @@ export class JobsService {
downloadJobFile(jobId: string): Observable<Blob> { downloadJobFile(jobId: string): Observable<Blob> {
const headers = this.authService.getAuthHeaders(); const headers = this.authService.getAuthHeaders();
return this.http.get(`${this.API_URL}/api/jobs/${jobId}/download`, { return this.http.get(`${this.API_URL}/jobs/${jobId}/download`, {
headers, headers,
responseType: 'blob' responseType: 'blob'
}); });

View File

@ -35,6 +35,7 @@ export interface Ips {
telefono?: string; telefono?: string;
departamento: string; departamento: string;
municipio: string; municipio: string;
tiene_convenio?: boolean | null;
} }
export interface Autorizante { export interface Autorizante {
@ -55,6 +56,12 @@ export interface CrearAutorizantePayload {
activo?: boolean; activo?: boolean;
} }
export interface ActualizarAutorizantePayload {
nombre?: string;
telefono?: string | null;
cargo?: string | null;
}
export interface CrearAutorizacionPayload { export interface CrearAutorizacionPayload {
interno: string; interno: string;
id_ips: number; id_ips: number;
@ -70,6 +77,7 @@ export interface RespuestaAutorizacion {
numero_autorizacion: string; numero_autorizacion: string;
fecha_autorizacion: string; fecha_autorizacion: string;
version?: number; version?: number;
estado_autorizacion?: string;
} }
export interface AutorizacionListado { export interface AutorizacionListado {
@ -83,11 +91,13 @@ export interface AutorizacionListado {
tipo_autorizacion?: string | null; tipo_autorizacion?: string | null;
tipo_servicio?: string | null; tipo_servicio?: string | null;
version?: number | null; version?: number | null;
estado_autorizacion?: string | null;
id_ips?: number | null; id_ips?: number | null;
numero_documento_autorizante?: number | null; numero_documento_autorizante?: number | null;
nombre_ips: string; nombre_ips: string;
municipio: string; municipio?: string | null;
departamento: string; departamento?: string | null;
ips_tiene_convenio?: boolean | null;
nombre_autorizante: string; nombre_autorizante: string;
} }
@ -102,6 +112,14 @@ export interface AutorizacionVersionResponse {
versiones: AutorizacionVersion[]; versiones: AutorizacionVersion[];
} }
export interface CupInfo {
codigo: string;
descripcion: string;
nivel?: string | null;
especialidad?: string | null;
cubierto: boolean;
}
// ====== Servicio ====== // ====== Servicio ======
@Injectable({ @Injectable({
@ -124,7 +142,7 @@ export class PacienteService {
buscarPorDocumento(numero_documento: string): Observable<Paciente[]> { buscarPorDocumento(numero_documento: string): Observable<Paciente[]> {
const params = new HttpParams().set('numero_documento', numero_documento); const params = new HttpParams().set('numero_documento', numero_documento);
return this.http.get<Paciente[]>(`${this.API_URL}/api/pacientes`, { return this.http.get<Paciente[]>(`${this.API_URL}/pacientes`, {
params, params,
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
@ -132,7 +150,7 @@ export class PacienteService {
buscarPorInterno(interno: string): Observable<Paciente[]> { buscarPorInterno(interno: string): Observable<Paciente[]> {
const params = new HttpParams().set('interno', interno); const params = new HttpParams().set('interno', interno);
return this.http.get<Paciente[]>(`${this.API_URL}/api/pacientes`, { return this.http.get<Paciente[]>(`${this.API_URL}/pacientes`, {
params, params,
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
@ -140,7 +158,7 @@ export class PacienteService {
buscarPorNombre(nombre: string): Observable<Paciente[]> { buscarPorNombre(nombre: string): Observable<Paciente[]> {
const params = new HttpParams().set('nombre', nombre); const params = new HttpParams().set('nombre', nombre);
return this.http.get<Paciente[]>(`${this.API_URL}/api/pacientes`, { return this.http.get<Paciente[]>(`${this.API_URL}/pacientes`, {
params, params,
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
@ -153,37 +171,48 @@ export class PacienteService {
if (verTodas) { if (verTodas) {
params = params.set('ver_todas', '1'); params = params.set('ver_todas', '1');
} }
return this.http.get<Ips[]>(`${this.API_URL}/api/ips-por-interno`, { return this.http.get<Ips[]>(`${this.API_URL}/ips-por-interno`, {
params, params,
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
} }
obtenerAutorizantes(): Observable<Autorizante[]> { obtenerAutorizantes(): Observable<Autorizante[]> {
return this.http.get<Autorizante[]>(`${this.API_URL}/api/autorizantes`, { return this.http.get<Autorizante[]>(`${this.API_URL}/autorizantes`, {
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
} }
obtenerAutorizantesAdmin(): Observable<Autorizante[]> { obtenerAutorizantesAdmin(): Observable<Autorizante[]> {
return this.http.get<Autorizante[]>(`${this.API_URL}/api/autorizantes/admin`, { return this.http.get<Autorizante[]>(`${this.API_URL}/autorizantes/admin`, {
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
} }
actualizarEstadoAutorizante(numeroDocumento: number, activo: boolean): Observable<any> { actualizarEstadoAutorizante(numeroDocumento: number, activo: boolean): Observable<any> {
return this.http.patch( return this.http.patch(
`${this.API_URL}/api/autorizantes/${numeroDocumento}/estado`, `${this.API_URL}/autorizantes/${numeroDocumento}/estado`,
{ activo }, { activo },
{ headers: this.getAuthHeaders() } { headers: this.getAuthHeaders() }
); );
} }
actualizarDatosAutorizante(
numeroDocumento: number,
payload: ActualizarAutorizantePayload
): Observable<any> {
return this.http.patch(
`${this.API_URL}/autorizantes/${numeroDocumento}`,
payload,
{ headers: this.getAuthHeaders() }
);
}
// ---- Autorizaciones ---- // ---- Autorizaciones ----
crearAutorizacion(payload: CrearAutorizacionPayload): Observable<RespuestaAutorizacion> { crearAutorizacion(payload: CrearAutorizacionPayload): Observable<RespuestaAutorizacion> {
return this.http.post<RespuestaAutorizacion>( return this.http.post<RespuestaAutorizacion>(
`${this.API_URL}/api/autorizaciones`, `${this.API_URL}/autorizaciones`,
payload, payload,
{ headers: this.getAuthHeaders() } { headers: this.getAuthHeaders() }
); );
@ -194,7 +223,7 @@ export class PacienteService {
payload: CrearAutorizacionPayload payload: CrearAutorizacionPayload
): Observable<RespuestaAutorizacion> { ): Observable<RespuestaAutorizacion> {
return this.http.put<RespuestaAutorizacion>( return this.http.put<RespuestaAutorizacion>(
`${this.API_URL}/api/autorizaciones/${numeroAutorizacion}`, `${this.API_URL}/autorizaciones/${numeroAutorizacion}`,
payload, payload,
{ headers: this.getAuthHeaders() } { headers: this.getAuthHeaders() }
); );
@ -202,7 +231,7 @@ export class PacienteService {
obtenerVersionesAutorizacion(numeroAutorizacion: string): Observable<AutorizacionVersionResponse> { obtenerVersionesAutorizacion(numeroAutorizacion: string): Observable<AutorizacionVersionResponse> {
return this.http.get<AutorizacionVersionResponse>( return this.http.get<AutorizacionVersionResponse>(
`${this.API_URL}/api/autorizaciones/${numeroAutorizacion}/versiones`, `${this.API_URL}/autorizaciones/${numeroAutorizacion}/versiones`,
{ headers: this.getAuthHeaders() } { headers: this.getAuthHeaders() }
); );
} }
@ -210,7 +239,7 @@ export class PacienteService {
obtenerAutorizacionesPorInterno(interno: string): Observable<AutorizacionListado[]> { obtenerAutorizacionesPorInterno(interno: string): Observable<AutorizacionListado[]> {
const params = new HttpParams().set('interno', interno); const params = new HttpParams().set('interno', interno);
return this.http.get<AutorizacionListado[]>( return this.http.get<AutorizacionListado[]>(
`${this.API_URL}/api/autorizaciones`, `${this.API_URL}/autorizaciones`,
{ params, headers: this.getAuthHeaders() } { params, headers: this.getAuthHeaders() }
); );
} }
@ -229,7 +258,7 @@ export class PacienteService {
// 3. Hacemos la petición con estos headers limpios. // 3. Hacemos la petición con estos headers limpios.
return this.http.post<JobResponse>( return this.http.post<JobResponse>(
`${this.API_URL}/api/cargar-excel-pacientes`, `${this.API_URL}/cargar-excel-pacientes`,
formData, formData,
{ headers } { headers }
); );
@ -242,7 +271,46 @@ export class PacienteService {
}); });
return this.http.post<JobResponse>( return this.http.post<JobResponse>(
`${this.API_URL}/api/cargar-cups`, `${this.API_URL}/cargar-cups`,
formData,
{ headers }
);
}
cargarIps(formData: FormData): Observable<JobResponse> {
const token = this.authService.getToken();
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`
});
return this.http.post<JobResponse>(
`${this.API_URL}/cargar-ips`,
formData,
{ headers }
);
}
cargarReps(formData: FormData): Observable<JobResponse> {
const token = this.authService.getToken();
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`
});
return this.http.post<JobResponse>(
`${this.API_URL}/cargar-reps`,
formData,
{ headers }
);
}
cargarAutorizacionesMasivas(formData: FormData): Observable<JobResponse> {
const token = this.authService.getToken();
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`
});
return this.http.post<JobResponse>(
`${this.API_URL}/cargar-autorizaciones-masivas`,
formData, formData,
{ headers } { headers }
); );
@ -250,18 +318,49 @@ export class PacienteService {
crearAutorizante(payload: CrearAutorizantePayload): Observable<any> { crearAutorizante(payload: CrearAutorizantePayload): Observable<any> {
return this.http.post( return this.http.post(
`${this.API_URL}/api/autorizantes`, `${this.API_URL}/autorizantes`,
payload, payload,
{ headers: this.getAuthHeaders() } { headers: this.getAuthHeaders() }
); );
} }
buscarCupsCubiertos(termino: string): Observable<any[]> { buscarCups(termino: string, limit = 200): Observable<CupInfo[]> {
const params = new HttpParams().set('q', termino); const params = new HttpParams()
return this.http.get<any[]>(`${this.API_URL}/api/cups-cubiertos`, { .set('q', termino)
.set('limit', String(limit));
return this.http.get<CupInfo[]>(`${this.API_URL}/cups`, {
params, params,
headers: this.getAuthHeaders() headers: this.getAuthHeaders()
}); });
} }
actualizarEstadoAutorizacion(
numeroAutorizacion: string,
estado: 'pendiente' | 'autorizado' | 'no_autorizado'
): Observable<any> {
return this.http.patch(
`${this.API_URL}/autorizaciones/${numeroAutorizacion}/estado`,
{ estado_autorizacion: estado },
{ headers: this.getAuthHeaders() }
);
}
actualizarEstadoAutorizacionesMasivo(
fechaInicio: string,
fechaFin: string,
estado: 'pendiente' | 'autorizado' | 'no_autorizado',
soloPendientes = false
): Observable<any> {
return this.http.patch(
`${this.API_URL}/autorizaciones/estado-masivo`,
{
fecha_inicio: fechaInicio,
fecha_fin: fechaFin,
estado_autorizacion: estado,
solo_pendientes: soloPendientes,
},
{ headers: this.getAuthHeaders() }
);
}
} }

View File

@ -5,8 +5,9 @@ import { AppComponent } from './app/app';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app/app.routes'; import { routes } from './app/app.routes';
import { importProvidersFrom } from '@angular/core'; import { importProvidersFrom } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AuthInterceptor } from './app/interceptors/auth.interceptor';
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, {
providers: [ providers: [
@ -15,6 +16,11 @@ bootstrapApplication(AppComponent, {
HttpClientModule, HttpClientModule,
FormsModule, FormsModule,
ReactiveFormsModule ReactiveFormsModule
) ),
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
] ]
}); });

View File

@ -13,12 +13,23 @@
--color-bg: #f5f5f5; --color-bg: #f5f5f5;
--color-card: #ffffff; --color-card: #ffffff;
--color-border: #e0e0e0; --color-border: #e0e0e0;
--color-surface: #f8fafc;
--color-surface-alt: #f1f5f9;
--color-surface-muted: #eef2f7;
--color-surface-strong: #e7eef7;
--color-text-main: #222222; --color-text-main: #222222;
--color-text-muted: #666666; --color-text-muted: #666666;
--color-success: #2e7d32; --color-success: #2e7d32;
--color-error: #c62828; --color-error: #c62828;
--color-permission-yes-bg: #f0f9f0;
--color-permission-yes-border: #dcfce7;
--color-permission-no-bg: #fff5f5;
--color-permission-no-border: #fed7d7;
--shadow-card: 0 4px 12px rgba(0, 0, 0, 0.06);
--shadow-float: 0 10px 24px rgba(0, 0, 0, 0.12);
--color-table-head: #f2f2f2; --color-table-head: #f2f2f2;
--color-table-row: #ffffff; --color-table-row: #ffffff;
@ -45,25 +56,33 @@
} }
[data-theme="dark"] { [data-theme="dark"] {
--color-primary: #4fa3ff; --color-primary: #3c7fc1;
--color-primary-dark: #2d7dcc; --color-primary-dark: #2f628f;
--color-primary-soft: #1d2b3b; --color-primary-soft: #1b2a3b;
--color-header-grad-2: #1f4b87; --color-header-grad-2: #1e3f66;
--color-bg: #0f141a; --color-bg: #0f141a;
--color-card: #151c24; --color-card: #121a24;
--color-border: #2a3440; --color-border: #1f2a36;
--color-surface: #0f1621;
--color-surface-alt: #151e2a;
--color-surface-muted: #1b2635;
--color-surface-strong: #1f2c3d;
--color-text-main: #e7edf5; --color-text-main: #e7edf5;
--color-text-muted: #a8b3c2; --color-text-muted: #a1adbd;
--color-success: #3bb273; --color-success: #3bb273;
--color-error: #ff6b6b; --color-error: #ff6b6b;
--color-permission-yes-bg: rgba(46, 125, 50, 0.18);
--color-permission-yes-border: rgba(46, 125, 50, 0.4);
--color-permission-no-bg: rgba(198, 40, 40, 0.16);
--color-permission-no-border: rgba(198, 40, 40, 0.45);
--color-table-head: #1f2a36; --color-table-head: #1b2430;
--color-table-row: #151c24; --color-table-row: #121a24;
--color-table-row-alt: #19222c; --color-table-row-alt: #16202b;
--color-table-hover: #223046; --color-table-hover: #1f2a36;
--color-input-bg: #111821; --color-input-bg: #111821;
--color-input-border: #2c3a4a; --color-input-border: #2c3a4a;
@ -77,6 +96,9 @@
--color-cup-item-bg: #151c24; --color-cup-item-bg: #151c24;
--color-cup-item-border: #273241; --color-cup-item-border: #273241;
--color-cup-item-hover: #4fa3ff; --color-cup-item-hover: #4fa3ff;
--shadow-card: 0 6px 18px rgba(0, 0, 0, 0.5);
--shadow-float: 0 16px 32px rgba(0, 0, 0, 0.55);
} }
*, *,
@ -127,7 +149,8 @@ textarea {
background: var(--color-card); background: var(--color-card);
border-radius: var(--radius-card); border-radius: var(--radius-card);
padding: 20px 24px; padding: 20px 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); border: 1px solid var(--color-border);
box-shadow: var(--shadow-card);
} }
.card.compact { .card.compact {