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

Resultados ({{ autorizaciones.length }} autorizaciones)

-
+

+ Resultados ({{ autorizacionesFiltradas.length }} autorizaciones + + de {{ autorizaciones.length }} + + ) +

+
+ + +
+
+
+ + + +
- +
@@ -109,18 +150,25 @@ Tipo autorizacion Tipo servicio CUPS + Cubre Nivel IPS Municipio Departamento Autorizante Establecimiento - Acciones + Estado + Acciones + + + No hay autorizaciones para ese numero. + + @@ -137,18 +185,61 @@ {{ getTipoServicioLabel(aut.tipo_servicio) }} {{ aut.cup_codigo }} + + + {{ getCoberturaLabel(aut) }} + + {{ aut.cup_nivel }} - {{ aut.nombre_ips }} - {{ aut.municipio }} - {{ aut.departamento }} + +
+ {{ aut.nombre_ips }} + + No cubre IPS + +
+ + {{ aut.municipio || '-' }} + {{ aut.departamento || '-' }} {{ aut.nombre_autorizante }} {{ aut.nombre_establecimiento }} + + + + + Guardando... + + + + + {{ getEstadoAutorizacionLabel(aut.estado_autorizacion) }} + + + diff --git a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.ts b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.ts index 9e26105..7dbe157 100644 --- a/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.ts +++ b/saludut-inpec/src/app/components/autorizaciones-por-fecha/autorizaciones-por-fecha.ts @@ -1,15 +1,16 @@ import { ChangeDetectorRef, Component, OnInit, Inject } from '@angular/core'; 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 { AuthService } from '../../services/auth'; import { AppHeaderComponent } from '../shared/app-header/app-header'; import { JobsService } from '../../services/jobs'; +import { PacienteService } from '../../services/paciente'; @Component({ selector: 'app-autorizaciones-por-fecha', standalone: true, - imports: [CommonModule, ReactiveFormsModule, AppHeaderComponent], + imports: [CommonModule, ReactiveFormsModule, FormsModule, AppHeaderComponent], templateUrl: './autorizaciones-por-fecha.html', styleUrls: ['./autorizaciones-por-fecha.css'] }) @@ -23,6 +24,12 @@ export class AutorizacionesPorFechaComponent implements OnInit { avisoAutorizaciones = false; descargandoZip = false; descargandoPdf = false; + actualizandoEstado: Record = {}; + actualizandoMasivo = false; + estadoMasivo: 'pendiente' | 'autorizado' | 'no_autorizado' = 'autorizado'; + esAdmin = false; + filtroNumero = ''; + autorizacionesFiltradas: any[] = []; // Para saber si ya buscamos algo hayResultados = false; @@ -35,6 +42,7 @@ export class AutorizacionesPorFechaComponent implements OnInit { private fb: FormBuilder, private authService: AuthService, private jobsService: JobsService, + private pacienteService: PacienteService, private cdr: ChangeDetectorRef, @Inject(DOCUMENT) private document: Document ) { @@ -45,11 +53,7 @@ export class AutorizacionesPorFechaComponent implements OnInit { } ngOnInit(): void { - // Solo admin puede entrar (además del guard) - if (!this.authService.isAdministrador()) { - this.errorMessage = 'No tienes permisos para acceder a esta página.'; - return; - } + this.esAdmin = this.authService.isAdministrador(); // Rango por defecto: últimos 30 días const hoy = new Date(); @@ -84,6 +88,7 @@ export class AutorizacionesPorFechaComponent implements OnInit { this.isLoading = true; this.autorizaciones = []; + this.autorizacionesFiltradas = []; this.hayResultados = false; this.authService @@ -100,6 +105,7 @@ export class AutorizacionesPorFechaComponent implements OnInit { .subscribe({ next: (data) => { this.autorizaciones = data || []; + this.aplicarFiltroNumero(); this.hayResultados = this.autorizaciones.length > 0; this.avisoAutorizaciones = this.autorizaciones.length >= this.limiteAutorizaciones; @@ -117,8 +123,19 @@ export class AutorizacionesPorFechaComponent implements OnInit { } // ========= PDF INDIVIDUAL ========= - descargarPdf(numeroAutorizacion: string): void { + descargarPdf(autorizacion: any): void { 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.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 ========= descargarTodosLosPdfs(): void { this.limpiarMensajes(); + if (!this.esAdmin) { + this.errorMessage = 'No tienes permisos para descargar todos los PDFs.'; + return; + } + if (!this.autorizaciones || this.autorizaciones.length === 0) { this.errorMessage = 'No hay autorizaciones para descargar.'; return; @@ -302,6 +439,50 @@ export class AutorizacionesPorFechaComponent implements OnInit { 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 { this.errorMessage = null; this.successMessage = null; @@ -347,7 +528,14 @@ export class AutorizacionesPorFechaComponent implements OnInit { // ========= EXPORTAR A CSV SIMPLE ========= 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.'; return; } @@ -366,13 +554,14 @@ export class AutorizacionesPorFechaComponent implements OnInit { 'Municipio', 'Departamento', 'Autorizante', - 'Establecimiento' + 'Establecimiento', + 'Estado' ]; const separator = ';'; const csvContent = [ headers.map((header) => this.csvValue(header)).join(separator), - ...this.autorizaciones.map((aut) => + ...dataset.map((aut) => [ this.csvValue(aut.numero_autorizacion), this.csvValue(aut.version || ''), @@ -387,7 +576,8 @@ export class AutorizacionesPorFechaComponent implements OnInit { this.csvValue(aut.municipio), this.csvValue(aut.departamento), 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('\n'); diff --git a/saludut-inpec/src/app/components/autorizaciones/autorizaciones.css b/saludut-inpec/src/app/components/autorizaciones/autorizaciones.css index 997184d..06a86a0 100644 --- a/saludut-inpec/src/app/components/autorizaciones/autorizaciones.css +++ b/saludut-inpec/src/app/components/autorizaciones/autorizaciones.css @@ -121,6 +121,22 @@ 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 { margin-top: 12px; display: flex; @@ -244,12 +260,39 @@ 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 { color: var(--color-text-muted); font-size: var(--font-size-sm); 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 === */ .aut-modal-backdrop { position: fixed; diff --git a/saludut-inpec/src/app/components/autorizaciones/autorizaciones.html b/saludut-inpec/src/app/components/autorizaciones/autorizaciones.html index 5c7679f..74cc779 100644 --- a/saludut-inpec/src/app/components/autorizaciones/autorizaciones.html +++ b/saludut-inpec/src/app/components/autorizaciones/autorizaciones.html @@ -9,6 +9,9 @@ [userRole]="getCurrentUser()?.nombre_rol" [showLogout]="isLoggedIn()" (logout)="logout()" + [showBack]="true" + backLabel="Volver" + (back)="irADashboard()" > @@ -36,31 +39,8 @@
- -
- + - - - - {{ estadoCargaExcel }} - -
- -
-
- - @@ -205,7 +182,7 @@ >
{{ cup.descripcion }}
Nivel {{ cup.nivel }}
+
+ + {{ cup.cubierto ? 'CUBIERTO' : 'NO CUBIERTO' }} + +
+
+ + {{ cupSeleccionado.cubierto ? 'CUBIERTO' : 'NO CUBIERTO' }} + + + {{ + cupSeleccionado.cubierto + ? 'Este CUPS esta cubierto por la nota tecnica.' + : 'Este CUPS no esta cubierto por la nota tecnica.' + }} + +
+
{{ errorCups }}
@@ -357,6 +359,7 @@ Version IPS Autoriza + Estado Acciones @@ -393,8 +396,50 @@ - {{ a.nombre_ips }} + +
+ {{ a.nombre_ips }} + + No cubre IPS + +
+ {{ a.nombre_autorizante }} + + + + + Guardando... + + + + + {{ getEstadoAutorizacionLabel(a.estado_autorizacion) }} + + +
- - Solo admin + + Pendiente de aprobacion
diff --git a/saludut-inpec/src/app/components/autorizaciones/autorizaciones.ts b/saludut-inpec/src/app/components/autorizaciones/autorizaciones.ts index 6c5ed23..8e3f74e 100644 --- a/saludut-inpec/src/app/components/autorizaciones/autorizaciones.ts +++ b/saludut-inpec/src/app/components/autorizaciones/autorizaciones.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AuthService } from '../../services/auth'; -import { PacienteService, AutorizacionVersion } from '../../services/paciente'; +import { PacienteService, AutorizacionVersion, CupInfo } from '../../services/paciente'; import { finalize } from 'rxjs/operators'; import { AppHeaderComponent } from '../shared/app-header/app-header'; import { JobsService } from '../../services/jobs'; @@ -26,21 +26,19 @@ export class AutorizacionesComponent { cargando = false; error: string | null = null; - // ---- Excel ---- - cargandoExcel = false; - estadoCargaExcel: string | null = null; - // ---- Autorizaciones ---- pacienteSeleccionado: any = null; ipsDisponibles: any[] = []; autorizantes: any[] = []; - cupsDisponibles: any[] = []; + cupsDisponibles: CupInfo[] = []; + cupSeleccionado: CupInfo | null = null; buscandoCups = false; errorCups: string | null = null; verMasIps = false; departamentoInterno = ''; observacionTraslado = ''; autorizacionEditando: any | null = null; + actualizandoEstado: Record = {}; versionesPorAutorizacion: Record = {}; versionActualPorAutorizacion: Record = {}; @@ -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 // ------------------------- @@ -220,6 +148,7 @@ export class AutorizacionesComponent { this.cargandoVersiones = {}; this.errorAutLista = null; this.cupsDisponibles = []; + this.cupSeleccionado = null; this.errorCups = null; this.cargarIps(p.interno, this.verMasIps); @@ -233,6 +162,7 @@ export class AutorizacionesComponent { this.errorAutorizacion = null; this.errorAutLista = null; this.cupsDisponibles = []; + this.cupSeleccionado = null; this.errorCups = null; this.verMasIps = false; this.departamentoInterno = ''; @@ -336,15 +266,17 @@ export class AutorizacionesComponent { const termino = String(this.formAutorizacion.cup_codigo || '').trim(); if (!termino) { this.cupsDisponibles = []; + this.cupSeleccionado = null; this.errorCups = 'Ingresa un código o descripción para buscar.'; return; } this.buscandoCups = true; this.errorCups = null; + this.cupSeleccionado = null; this.pacienteService - .buscarCupsCubiertos(termino) + .buscarCups(termino) .pipe( finalize(() => { this.buscandoCups = false; @@ -352,22 +284,31 @@ export class AutorizacionesComponent { }) ) .subscribe({ - next: (data: any[]) => { + next: (data: CupInfo[]) => { this.cupsDisponibles = data || []; + const match = this.cupsDisponibles.find( + (cup) => cup.codigo === this.formAutorizacion.cup_codigo + ); + this.cupSeleccionado = match || null; if (this.cupsDisponibles.length === 0) { this.errorCups = 'No se encontraron CUPS con ese criterio.'; } }, 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; 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) : ''; } + 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 { const base = String(this.formAutorizacion.observacion || '').trim(); const trasladoTexto = String(this.observacionTraslado || '').trim(); @@ -453,6 +457,7 @@ export class AutorizacionesComponent { tipo_autorizacion: autorizacion.tipo_autorizacion || 'consultas_externas', tipo_servicio: autorizacion.tipo_servicio || '', }; + this.cupSeleccionado = null; this.onTipoAutorizacionChange(); this.onIpsChange(); @@ -464,6 +469,7 @@ export class AutorizacionesComponent { cancelarEdicion(): void { this.autorizacionEditando = null; this.observacionTraslado = ''; + this.cupSeleccionado = null; this.formAutorizacion = { id_ips: '', numero_documento_autorizante: '', @@ -495,7 +501,6 @@ export class AutorizacionesComponent { this.errorAutorizacion = 'Debe seleccionar un CUPS.'; return; } - const tipoAutorizacion = String( this.formAutorizacion.tipo_autorizacion || 'consultas_externas' ).toLowerCase(); @@ -633,7 +638,17 @@ export class AutorizacionesComponent { // ------------------------- // 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.errorAutLista = null; @@ -701,6 +716,10 @@ export class AutorizacionesComponent { this.router.navigate(['/autorizaciones-por-fecha']); } + irACargarPacientes(): void { + this.router.navigate(['/cargar-pacientes']); + } + irADashboard(): void { this.router.navigate(['/dashboard']); } diff --git a/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.css b/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.css new file mode 100644 index 0000000..1aefd3c --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.css @@ -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; +} diff --git a/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.html b/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.html new file mode 100644 index 0000000..ce26cd8 --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.html @@ -0,0 +1,117 @@ +
+
+ + +
+

Subir plantilla

+

+ La columna de numero de autorizacion se ignora. Todas las autorizaciones quedan en estado pendiente. +

+ +
+ + + {{ archivoFile.name }} +
+ +
+ +
+ +
{{ statusMessage }}
+
{{ errorMessage }}
+
+ +
+

Resumen de la carga

+
+
+ Total filas + {{ resumen.total || 0 }} +
+
+ Creadas + {{ resumen.creadas || 0 }} +
+
+ Omitidas + {{ resumen.omitidas || resumen.omitidos || 0 }} +
+
+ Duplicadas + {{ resumen.duplicados || 0 }} +
+
+ Sin paciente + {{ resumen.sin_paciente || 0 }} +
+
+ Sin CUPS + {{ resumen.sin_cups || 0 }} +
+
+ Sin IPS + {{ resumen.sin_ips || 0 }} +
+
+ CUPS no cubiertos + {{ resumen.cups_no_cubiertos || 0 }} +
+
+ IPS sin convenio + {{ resumen.ips_sin_convenio || 0 }} +
+
+ +
+ Se detectaron CUPS no cubiertos o IPS sin convenio. Las autorizaciones quedan pendientes para revision. +
+ +
+

Errores (max 50)

+
+ + + + + + + + + + + + + +
FilaError
{{ e.fila || '-' }}{{ e.error || 'Error en fila' }}
+
+
+
+
+
diff --git a/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.ts b/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.ts new file mode 100644 index 0000000..00cdd28 --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-autorizaciones-masivas/cargar-autorizaciones-masivas.ts @@ -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; + } +} diff --git a/saludut-inpec/src/app/components/cargar-cups/cargar-cups.html b/saludut-inpec/src/app/components/cargar-cups/cargar-cups.html index 1db017e..f358caf 100644 --- a/saludut-inpec/src/app/components/cargar-cups/cargar-cups.html +++ b/saludut-inpec/src/app/components/cargar-cups/cargar-cups.html @@ -3,10 +3,15 @@
diff --git a/saludut-inpec/src/app/components/cargar-cups/cargar-cups.ts b/saludut-inpec/src/app/components/cargar-cups/cargar-cups.ts index 0aa3fd6..9be3e2e 100644 --- a/saludut-inpec/src/app/components/cargar-cups/cargar-cups.ts +++ b/saludut-inpec/src/app/components/cargar-cups/cargar-cups.ts @@ -38,6 +38,18 @@ export class CargarCupsComponent implements OnInit { 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 { const input = event.target as HTMLInputElement; this.notaFile = input.files?.[0] || null; diff --git a/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.css b/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.css new file mode 100644 index 0000000..609d18d --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.css @@ -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; +} diff --git a/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.html b/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.html new file mode 100644 index 0000000..e2d0d76 --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.html @@ -0,0 +1,147 @@ +
+
+ + +
+
+

Cargar IPS

+

+ Sube el archivo ips.xlsx para actualizar convenios y datos de IPS. +

+ +
+ + + {{ ipsFile.name }} +
+ +
+ +
+ +
{{ statusIps }}
+
{{ errorIps }}
+ +
+
+ Total filas + {{ ipsResumen.total || 0 }} +
+
+ Insertadas + {{ ipsResumen.insertados || 0 }} +
+
+ Actualizadas + {{ ipsResumen.actualizados || 0 }} +
+
+ Omitidas + {{ ipsResumen.omitidos || 0 }} +
+
+ Desactivadas + {{ ipsResumen.desactivados || 0 }} +
+
+ +
+

Errores (max 50)

+
+ + + + + + + + + + + + + +
FilaError
{{ e.fila || '-' }}{{ e.error || 'Error en fila' }}
+
+
+
+ +
+

Cargar REPS

+

+ Sube el archivo reps.xlsx para actualizar profesionales REPS. +

+ +
+ + + {{ repsFile.name }} +
+ +
+ +
+ +
{{ statusReps }}
+
{{ errorReps }}
+ +
+
+ Total filas + {{ repsResumen.total || 0 }} +
+
+ Insertadas + {{ repsResumen.insertados || 0 }} +
+
+ Actualizadas + {{ repsResumen.actualizados || 0 }} +
+
+ Omitidas + {{ repsResumen.omitidos || 0 }} +
+
+ Desactivados + {{ repsResumen.desactivados || 0 }} +
+
+
+
+
+
diff --git a/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.ts b/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.ts new file mode 100644 index 0000000..31f47d3 --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-ips-reps/cargar-ips-reps.ts @@ -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; + } +} diff --git a/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.css b/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.css new file mode 100644 index 0000000..173617f --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.css @@ -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); +} diff --git a/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.html b/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.html new file mode 100644 index 0000000..90872ba --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.html @@ -0,0 +1,62 @@ +
+
+ + +
+

Subir archivo

+

+ El sistema procesa paciente, ingreso y establecimiento. Usa la plantilla oficial de pacientes. +

+ +
+ + + {{ archivoFile.name }} +
+ +
+ +
+ +
{{ statusMessage }}
+
{{ errorMessage }}
+
+ +
+

Resumen de la carga

+
+
+ Pacientes activos + {{ resumen.activos || 0 }} +
+
+ Pacientes antiguos + {{ resumen.antiguos || 0 }} +
+
+
+
+
diff --git a/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.ts b/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.ts new file mode 100644 index 0000000..d11100f --- /dev/null +++ b/saludut-inpec/src/app/components/cargar-pacientes/cargar-pacientes.ts @@ -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; + } +} diff --git a/saludut-inpec/src/app/components/dashboard/dashboard.css b/saludut-inpec/src/app/components/dashboard/dashboard.css index f2a512d..cc831df 100644 --- a/saludut-inpec/src/app/components/dashboard/dashboard.css +++ b/saludut-inpec/src/app/components/dashboard/dashboard.css @@ -38,15 +38,16 @@ padding: 16px 20px; border-radius: 8px; margin: 20px 0 24px; - background: white; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + background: var(--color-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-card); animation: slideIn 0.3s ease; } .alert-error { - border-left: 4px solid #dc2626; - background: #fef2f2; - color: #dc2626; + border-left: 4px solid var(--color-error); + background: var(--color-permission-no-bg); + color: var(--color-error); } .alert-icon { @@ -94,20 +95,21 @@ } .action-card { - background: white; + background: var(--color-card); border-radius: 12px; 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; transition: all 0.3s ease; position: relative; - border: 2px solid transparent; + color: var(--color-text-main); } .action-card:hover { transform: translateY(-4px); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); - border-color: #1976d2; + box-shadow: var(--shadow-float); + border-color: var(--color-primary); } .action-icon { @@ -117,8 +119,8 @@ display: inline-flex; align-items: center; justify-content: center; - color: #1976d2; - background: #e3f2fd; + color: var(--color-primary); + background: var(--color-primary-soft); border-radius: 10px; } @@ -130,14 +132,14 @@ .action-card h4, .action-card h3 { margin: 0 0 8px 0; - color: #222222; + color: var(--color-text-main); font-size: 1.05rem; font-weight: 600; } .action-card p { margin: 0; - color: #666666; + color: var(--color-text-muted); font-size: 0.9rem; line-height: 1.4; } @@ -146,7 +148,7 @@ position: absolute; top: 12px; right: 12px; - background: #1976d2; + background: var(--color-primary); color: white; padding: 4px 8px; border-radius: 12px; @@ -171,12 +173,12 @@ .role-label { font-weight: 600; - color: #222222; + color: var(--color-text-main); min-width: 140px; } .role-value { - color: #666666; + color: var(--color-text-muted); font-weight: 500; } @@ -186,36 +188,90 @@ 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 { - background: #e3f2fd; - color: #1976d2; + background: var(--color-primary-soft); + color: var(--color-primary); padding: 4px 12px; border-radius: 16px; font-size: 0.8rem; font-weight: 500; - border: 1px solid #1976d2; + border: 1px solid var(--color-primary); } /* Permissions Summary */ +.permissions-summary { + margin-top: 24px; +} + +.section-subtitle { + margin: -8px 0 16px; + color: var(--color-text-muted); + font-size: 0.9rem; +} + .permissions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 12px; + gap: 16px; } .permission-item { display: flex; - align-items: center; + align-items: flex-start; gap: 12px; - padding: 12px; + padding: 14px 16px; border-radius: 8px; - background: #f9fafb; + background: var(--color-surface); + border: 1px solid var(--color-border); transition: all 0.2s ease; } .permission-item.has-permission { - background: #f0f9f0; - border: 1px solid #dcfce7; + background: var(--color-permission-yes-bg); + 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 { @@ -226,11 +282,31 @@ display: inline-flex; align-items: 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; - color: #222222; + color: var(--color-text-main); +} + +.permission-meta { + font-size: 0.82rem; + color: var(--color-text-muted); } .excel-status { diff --git a/saludut-inpec/src/app/components/dashboard/dashboard.html b/saludut-inpec/src/app/components/dashboard/dashboard.html index 762e4cd..6fac13f 100644 --- a/saludut-inpec/src/app/components/dashboard/dashboard.html +++ b/saludut-inpec/src/app/components/dashboard/dashboard.html @@ -3,6 +3,7 @@

Cargar pacientes

- {{ cargandoExcel - ? 'Cargando archivo de pacientes...' - : 'Subir archivo Excel con datos de pacientes' }} + Subir archivo Excel con datos de pacientes

Solo admin
@@ -100,11 +99,67 @@
Solo admin
- + +
+ +

Carga masiva autorizaciones

+

Subir plantilla Excel para crear autorizaciones pendientes

+
+ + +
+ +

Cargar IPS y REPS

+

Subir Excel de IPS y profesionales REPS

+
Solo admin
+
+ +

Autorizaciones por fecha

Consultar y descargar autorizaciones por rango de fechas

+
Solo admin
+
+ + +
+ +

Estadisticas

+

Resumen mensual con estados y volumen diario

Solo admin
@@ -143,20 +220,6 @@ - - - - -

- {{ estadoCargaExcel }} -

@@ -171,20 +234,35 @@
Sedes asignadas: -
- - {{ sede.nombre_establecimiento }} - +
+
+ + {{ getSedesUsuario().length }} sedes + + +
+
+ + {{ sede.nombre_establecimiento }} + +
+ + Sin sedes asignadas +
@@ -193,10 +271,14 @@

Permisos disponibles

+

+ Resumen de lo que puedes hacer con tu cuenta. +

✓ @@ -204,25 +286,56 @@ × - Generar autorizaciones +
+ Generar autorizaciones + Crear y editar solicitudes +
- + - + × - Cargar pacientes (Excel) +
+ Cargar autorizaciones masivas + Subir archivo con solicitudes +
+
+ +
+ + ✓ + + + × + +
+ Ver autorizaciones por fecha + Consultar por rango +
✓ @@ -230,12 +343,16 @@ × - Descargar PDFs +
+ Descargar PDFs + Generar y descargar documentos +
✓ @@ -243,7 +360,95 @@ × - Ver todas las autorizaciones +
+ Ver todas las autorizaciones + Incluye pendientes y no autorizadas +
+
+ +
+ + ✓ + + + × + +
+ Cargar pacientes (Excel) + Importar datos masivos +
+
+ +
+ + ✓ + + + × + +
+ Cargar CUPS + Actualizar procedimientos cubiertos +
+
+ +
+ + ✓ + + + × + +
+ Cargar IPS y REPS + Actualizar convenios y profesionales +
+
+ +
+ + ✓ + + + × + +
+ Ver estadisticas + Resumen por dia, semana y mes +
+
+ +
+ + ✓ + + + × + +
+ Gestionar usuarios + Crear y editar accesos +
diff --git a/saludut-inpec/src/app/components/dashboard/dashboard.ts b/saludut-inpec/src/app/components/dashboard/dashboard.ts index b4c0550..49b5c2a 100644 --- a/saludut-inpec/src/app/components/dashboard/dashboard.ts +++ b/saludut-inpec/src/app/components/dashboard/dashboard.ts @@ -1,16 +1,13 @@ import { Component, OnInit, - OnDestroy, - ChangeDetectorRef + OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router, ActivatedRoute } from '@angular/router'; import { Subscription } from 'rxjs'; import { AuthService } from '../../services/auth'; -import { PacienteService } from '../../services/paciente'; import { AppHeaderComponent } from '../shared/app-header/app-header'; -import { JobsService } from '../../services/jobs'; @Component({ selector: 'app-dashboard', @@ -23,18 +20,12 @@ export class DashboardComponent implements OnInit, OnDestroy { currentUser: any = null; errorMessage: string | null = null; private subscriptions: Subscription[] = []; - - // ---- Carga de Excel ---- - cargandoExcel = false; - estadoCargaExcel: string | null = null; + mostrarTodasSedes = false; constructor( private authService: AuthService, private router: Router, - private route: ActivatedRoute, - private pacienteService: PacienteService, - private jobsService: JobsService, - private cdr: ChangeDetectorRef + private route: ActivatedRoute ) {} ngOnInit(): void { @@ -98,10 +89,26 @@ export class DashboardComponent implements OnInit, OnDestroy { this.router.navigate(['/autorizaciones-por-fecha']); } + irAEstadisticasAutorizaciones(): void { + this.router.navigate(['/estadisticas-autorizaciones']); + } + irACargarCups(): void { 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 { this.router.navigate(['/usuarios']); } @@ -149,6 +156,10 @@ export class DashboardComponent implements OnInit, OnDestroy { return this.authService.isAdministrador(); } + puedeCargarIpsReps(): boolean { + return this.authService.isAdministrador(); + } + puedeDescargarPdfs(): boolean { return this.authService.puedeDescargarPdfs(); } @@ -157,14 +168,42 @@ export class DashboardComponent implements OnInit, OnDestroy { return this.authService.puedeVerTodasAutorizaciones(); } + puedeVerEstadisticas(): boolean { + return this.authService.isAdministrador(); + } + puedeGenerarAutorizaciones(): boolean { return this.authService.puedeGenerarAutorizaciones(); } + puedeCargarAutorizacionesMasivas(): boolean { + return this.authService.puedeGenerarAutorizaciones(); + } + + puedeVerAutorizacionesPorFecha(): boolean { + return this.authService.isLoggedIn(); + } + getSedesUsuario(): any[] { 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 { this.errorMessage = null; } @@ -178,83 +217,4 @@ export class DashboardComponent implements OnInit, OnDestroy { 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 = ''; - } - } - }); - } } diff --git a/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.css b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.css new file mode 100644 index 0000000..4d1ce09 --- /dev/null +++ b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.css @@ -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; + } +} diff --git a/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.html b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.html new file mode 100644 index 0000000..6d853d2 --- /dev/null +++ b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.html @@ -0,0 +1,199 @@ +
+
+ + +
+
+ + + + + + + + + + + + + +
+
+ +
{{ errorMessage }}
+ +
+
+

Autorizadas

+

{{ data.resumen.autorizadas }}

+
+
+

No autorizadas

+

{{ data.resumen.no_autorizadas }}

+
+
+

Pendientes

+

{{ data.resumen.pendientes }}

+
+
+

Total

+

{{ data.resumen.total }}

+
+
+ +
+
+

Autorizaciones por dia

+ + {{ data.rango.inicio }} a {{ data.rango.fin }} + +
+ +
+
+
{{ bucket.total }}
+
+
+
+
+
+
+
+
{{ bucket.label }}
+
+
+ +
+
+ + Autorizadas +
+
+ + No autorizadas +
+
+ + Pendientes +
+
+
+ +
+
+

Autorizaciones del {{ formatRangeLabel(selectedBucket.inicio, selectedBucket.fin) }}

+ +
+ +
+ Cargando autorizaciones del rango... +
+
{{ errorDetalle }}
+
+ No hay autorizaciones en este rango. +
+ +
+ Mostrando hasta 500 autorizaciones. +
+ +
+ + + + + + + + + + + + + + + + + + + +
NumeroInternoPacienteCUPSEstado
{{ aut.numero_autorizacion }}{{ aut.interno }}{{ aut.nombre_paciente }}{{ aut.cup_codigo }} + + {{ getEstadoAutorizacionLabel(aut.estado_autorizacion) }} + +
+
+
+
+
diff --git a/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts new file mode 100644 index 0000000..f5873bf --- /dev/null +++ b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts @@ -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(); + 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(); + 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(); + 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; +}; diff --git a/saludut-inpec/src/app/components/usuarios/usuarios.css b/saludut-inpec/src/app/components/usuarios/usuarios.css index 687c3cf..03e0459 100644 --- a/saludut-inpec/src/app/components/usuarios/usuarios.css +++ b/saludut-inpec/src/app/components/usuarios/usuarios.css @@ -21,7 +21,7 @@ .subtitle { margin: 4px 0 0; - color: #666666; + color: var(--color-text-muted); font-size: 0.9rem; } @@ -42,6 +42,7 @@ .form-row label { font-weight: 600; font-size: 0.9rem; + color: var(--color-text-main); } .form-row input, @@ -49,8 +50,10 @@ .form-row textarea { padding: 8px 10px; border-radius: 6px; - border: 1px solid #ccc; + border: 1px solid var(--color-input-border); font-size: 0.9rem; + background: var(--color-input-bg); + color: var(--color-text-main); } .form-row input:focus, @@ -67,7 +70,7 @@ .hint { font-size: 0.75rem; - color: #777777; + color: var(--color-text-muted); } /* Tabla */ @@ -82,17 +85,17 @@ } .tabla-usuarios-wrapper::-webkit-scrollbar-track { - background: #f1f1f1; + background: var(--color-surface-muted); border-radius: 4px; } .tabla-usuarios-wrapper::-webkit-scrollbar-thumb { - background: #c0c0c0; + background: var(--color-border); border-radius: 4px; } .tabla-usuarios-wrapper::-webkit-scrollbar-thumb:hover { - background: #a0a0a0; + background: var(--color-text-muted); } .tabla-usuarios thead { @@ -106,6 +109,43 @@ 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) { .usuarios-grid { grid-template-columns: 1fr; diff --git a/saludut-inpec/src/app/components/usuarios/usuarios.html b/saludut-inpec/src/app/components/usuarios/usuarios.html index a45559a..107292e 100644 --- a/saludut-inpec/src/app/components/usuarios/usuarios.html +++ b/saludut-inpec/src/app/components/usuarios/usuarios.html @@ -3,11 +3,15 @@
@@ -17,10 +21,7 @@ Crear, activar y desactivar usuarios del sistema

- -
+
@@ -57,19 +58,6 @@ -
- - - - Solo para rol administrativo_sede. Usa los códigos - de establecimiento, separados por coma. - -
-
+ + +
+
- Desactivar - - + {{ autorizanteEdicion[a.numero_documento].error }} +
@@ -210,6 +243,7 @@ Nombre Email Rol + Nueva contrasena Estado Último login Acciones @@ -217,10 +251,45 @@ - {{ u.username }} - {{ u.nombre_completo }} - {{ u.email }} - {{ u.nombre_rol }} + + + + + + + + + + + + + + + - + + + +
- Desactivar - - + {{ usuarioEdicion[u.id_usuario].error }} +
diff --git a/saludut-inpec/src/app/components/usuarios/usuarios.ts b/saludut-inpec/src/app/components/usuarios/usuarios.ts index 9cefd62..95541b2 100644 --- a/saludut-inpec/src/app/components/usuarios/usuarios.ts +++ b/saludut-inpec/src/app/components/usuarios/usuarios.ts @@ -3,17 +3,39 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AuthService, + ActualizarUsuarioPayload, RegisterRequest, Rol } from '../../services/auth'; import { Autorizante, + ActualizarAutorizantePayload, CrearAutorizantePayload, PacienteService } from '../../services/paciente'; import { Router } from '@angular/router'; 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({ selector: 'app-usuarios', standalone: true, @@ -26,6 +48,8 @@ export class UsuariosComponent implements OnInit { usuarios: any[] = []; roles: Rol[] = []; autorizantes: Autorizante[] = []; + autorizanteEdicion: Record = {}; + usuarioEdicion: Record = {}; cargandoUsuarios = false; errorUsuarios: string | null = null; @@ -43,8 +67,6 @@ export class UsuariosComponent implements OnInit { mensajeAutorizanteOk: string | null = null; mensajeAutorizanteError: string | null = null; - // para administrativos, códigos separados por coma - sedesTexto = ''; nuevoUsuario: RegisterRequest = { username: '', @@ -134,6 +156,7 @@ export class UsuariosComponent implements OnInit { this.authService.getUsuarios(this.limiteUsuarios, 0).subscribe({ next: (usuarios) => { this.usuarios = usuarios; + this.prepararEdicionUsuarios(); this.avisoUsuarios = usuarios.length >= this.limiteUsuarios; this.cargandoUsuarios = false; this.cdr.detectChanges(); @@ -161,17 +184,7 @@ export class UsuariosComponent implements OnInit { return; } - // Si es administrativo de sede (id_rol = 2) procesamos las 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.nuevoUsuario.sedes = []; this.creando = true; @@ -189,7 +202,6 @@ export class UsuariosComponent implements OnInit { id_rol: 2, sedes: [] }; - this.sedesTexto = ''; // Recargar lista sin refrescar la página this.cargarUsuarios(); @@ -290,6 +302,7 @@ export class UsuariosComponent implements OnInit { this.pacienteService.obtenerAutorizantesAdmin().subscribe({ next: (autorizantes) => { this.autorizantes = autorizantes; + this.prepararEdicionAutorizantes(); this.cargandoAutorizantes = false; this.cdr.detectChanges(); }, @@ -328,4 +341,219 @@ export class UsuariosComponent implements OnInit { trackByAutorizante(_index: number, autorizante: Autorizante): number | string { return autorizante?.numero_documento ?? _index; } + + private prepararEdicionAutorizantes(): void { + const edicion: Record = {}; + 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 = {}; + 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(); + } + }); + } } diff --git a/saludut-inpec/src/app/config.ts b/saludut-inpec/src/app/config.ts index 75987d2..1d4bfef 100644 --- a/saludut-inpec/src/app/config.ts +++ b/saludut-inpec/src/app/config.ts @@ -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(/\/+$/, ''); diff --git a/saludut-inpec/src/app/interceptors/auth.interceptor.ts b/saludut-inpec/src/app/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..5114ae2 --- /dev/null +++ b/saludut-inpec/src/app/interceptors/auth.interceptor.ts @@ -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, next: HttpHandler): Observable> { + return next.handle(req).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + this.authService.logout(); + } + return throwError(() => error); + }) + ); + } +} diff --git a/saludut-inpec/src/app/services/auth.ts b/saludut-inpec/src/app/services/auth.ts index f8d54a8..5108ab1 100644 --- a/saludut-inpec/src/app/services/auth.ts +++ b/saludut-inpec/src/app/services/auth.ts @@ -35,6 +35,14 @@ export interface RegisterRequest { sedes?: string[]; } +export interface ActualizarUsuarioPayload { + username?: string; + password?: string; + email?: string; + nombre_completo?: string; + id_rol?: number; +} + /* Interfaz Rol para la pantalla de usuarios */ export interface Rol { id_rol: number; @@ -42,6 +50,25 @@ export interface Rol { 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({ providedIn: 'root' }) @@ -55,6 +82,7 @@ export class AuthService { private isAuthenticatedSubject = new BehaviorSubject(false); public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); + private logoutTimeoutId: ReturnType | null = null; constructor( private http: HttpClient, @@ -70,6 +98,7 @@ export class AuthService { if (token && user) { this.currentUserSubject.next(user); this.isAuthenticatedSubject.next(true); + this.scheduleTokenExpiry(token); } else { this.clearAuth(); } @@ -78,7 +107,7 @@ export class AuthService { // =========== LOGIN =========== login(credentials: LoginRequest): Observable { - return this.http.post(`${this.API_URL}/api/auth/login`, credentials).pipe( + return this.http.post(`${this.API_URL}/auth/login`, credentials).pipe( tap(response => { this.setAuth(response.token, response.usuario); }), @@ -93,7 +122,7 @@ export class AuthService { register(userData: RegisterRequest): Observable { 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) => { console.error('Error en register:', error); return throwError(() => error); @@ -112,7 +141,7 @@ export class AuthService { verifyToken(): Observable<{ usuario: User }> { 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 => { this.updateUser(response.usuario); }) @@ -130,15 +159,54 @@ export class AuthService { localStorage.setItem(this.USER_KEY, JSON.stringify(user)); this.currentUserSubject.next(user); this.isAuthenticatedSubject.next(true); + this.scheduleTokenExpiry(token); } private clearAuth(): void { + this.clearLogoutTimer(); localStorage.removeItem(this.TOKEN_KEY); localStorage.removeItem(this.USER_KEY); this.currentUserSubject.next(null); 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 { localStorage.setItem(this.USER_KEY, JSON.stringify(user)); this.currentUserSubject.next(user); @@ -183,7 +251,8 @@ export class AuthService { } puedeDescargarPdfs(): boolean { - return this.isAdministrador(); + const user = this.getCurrentUser(); + return !!user && (user.nombre_rol === 'administrador' || user.nombre_rol === 'administrativo_sede'); } puedeVerTodasAutorizaciones(): boolean { @@ -213,18 +282,27 @@ export class AuthService { getUsuarios(limit = 200, offset = 0): Observable { const headers = this.getAuthHeaders(); const params = { limit, offset }; - return this.http.get(`${this.API_URL}/api/usuarios`, { headers, params }); + return this.http.get(`${this.API_URL}/usuarios`, { headers, params }); } cambiarEstadoUsuario(idUsuario: number, activo: boolean): Observable { const headers = this.getAuthHeaders(); return this.http.patch( - `${this.API_URL}/api/usuarios/${idUsuario}/estado`, + `${this.API_URL}/usuarios/${idUsuario}/estado`, { activo }, { headers } ); } + actualizarUsuario(idUsuario: number, payload: ActualizarUsuarioPayload): Observable { + const headers = this.getAuthHeaders(); + return this.http.patch( + `${this.API_URL}/usuarios/${idUsuario}`, + payload, + { headers } + ); + } + // Alias usado en usuarios.ts actualizarEstadoUsuario(idUsuario: number, activo: boolean): Observable { return this.cambiarEstadoUsuario(idUsuario, activo); @@ -239,13 +317,27 @@ export class AuthService { ): Observable { const headers = this.getAuthHeaders(); const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset }; - return this.http.get(`${this.API_URL}/api/autorizaciones-por-fecha`, { headers, params }); + return this.http.get(`${this.API_URL}/autorizaciones-por-fecha`, { headers, params }); } // Obtener roles desde el backend getRoles(): Observable { const headers = this.getAuthHeaders(); - return this.http.get(`${this.API_URL}/api/roles`, { headers }); + return this.http.get(`${this.API_URL}/roles`, { headers }); + } + + getEstadisticasAutorizaciones( + fechaInicio?: string, + fechaFin?: string + ): Observable { + const headers = this.getAuthHeaders(); + const params: any = {}; + if (fechaInicio) params.fecha_inicio = fechaInicio; + if (fechaFin) params.fecha_fin = fechaFin; + return this.http.get( + `${this.API_URL}/autorizaciones-estadisticas`, + { headers, params } + ); } crearJobPdfAutorizacion( @@ -258,7 +350,7 @@ export class AuthService { payload.version = version; } return this.http.post( - `${this.API_URL}/api/jobs/autorizacion-pdf`, + `${this.API_URL}/jobs/autorizacion-pdf`, payload, { headers } ); @@ -267,7 +359,7 @@ export class AuthService { crearJobZipAutorizaciones(fechaInicio: string, fechaFin: string): Observable { const headers = this.getAuthHeaders(); return this.http.post( - `${this.API_URL}/api/jobs/autorizaciones-zip`, + `${this.API_URL}/jobs/autorizaciones-zip`, { fecha_inicio: fechaInicio, fecha_fin: fechaFin }, { headers } ); diff --git a/saludut-inpec/src/app/services/job-types.ts b/saludut-inpec/src/app/services/job-types.ts index 316bd63..ddffb9d 100644 --- a/saludut-inpec/src/app/services/job-types.ts +++ b/saludut-inpec/src/app/services/job-types.ts @@ -4,6 +4,11 @@ export interface JobError { message?: string; } +export interface JobRowError { + fila?: number; + error?: string; +} + export interface JobResult { ok?: boolean; mensaje?: string; @@ -11,6 +16,20 @@ export interface JobResult { antiguos?: number | null; referencia?: 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; fileName?: string; contentType?: string; diff --git a/saludut-inpec/src/app/services/jobs.ts b/saludut-inpec/src/app/services/jobs.ts index 7e9b326..e23a9fc 100644 --- a/saludut-inpec/src/app/services/jobs.ts +++ b/saludut-inpec/src/app/services/jobs.ts @@ -19,7 +19,7 @@ export class JobsService { getJob(jobId: string): Observable { const headers = this.authService.getAuthHeaders(); - return this.http.get(`${this.API_URL}/api/jobs/${jobId}`, { headers }); + return this.http.get(`${this.API_URL}/jobs/${jobId}`, { headers }); } pollJob(jobId: string, intervalMs = 2000): Observable { @@ -34,7 +34,7 @@ export class JobsService { downloadJobFile(jobId: string): Observable { 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, responseType: 'blob' }); diff --git a/saludut-inpec/src/app/services/paciente.ts b/saludut-inpec/src/app/services/paciente.ts index 8803755..2966e62 100644 --- a/saludut-inpec/src/app/services/paciente.ts +++ b/saludut-inpec/src/app/services/paciente.ts @@ -35,6 +35,7 @@ export interface Ips { telefono?: string; departamento: string; municipio: string; + tiene_convenio?: boolean | null; } export interface Autorizante { @@ -55,6 +56,12 @@ export interface CrearAutorizantePayload { activo?: boolean; } +export interface ActualizarAutorizantePayload { + nombre?: string; + telefono?: string | null; + cargo?: string | null; +} + export interface CrearAutorizacionPayload { interno: string; id_ips: number; @@ -70,6 +77,7 @@ export interface RespuestaAutorizacion { numero_autorizacion: string; fecha_autorizacion: string; version?: number; + estado_autorizacion?: string; } export interface AutorizacionListado { @@ -83,11 +91,13 @@ export interface AutorizacionListado { tipo_autorizacion?: string | null; tipo_servicio?: string | null; version?: number | null; + estado_autorizacion?: string | null; id_ips?: number | null; numero_documento_autorizante?: number | null; nombre_ips: string; - municipio: string; - departamento: string; + municipio?: string | null; + departamento?: string | null; + ips_tiene_convenio?: boolean | null; nombre_autorizante: string; } @@ -102,6 +112,14 @@ export interface AutorizacionVersionResponse { versiones: AutorizacionVersion[]; } +export interface CupInfo { + codigo: string; + descripcion: string; + nivel?: string | null; + especialidad?: string | null; + cubierto: boolean; +} + // ====== Servicio ====== @Injectable({ @@ -124,7 +142,7 @@ export class PacienteService { buscarPorDocumento(numero_documento: string): Observable { const params = new HttpParams().set('numero_documento', numero_documento); - return this.http.get(`${this.API_URL}/api/pacientes`, { + return this.http.get(`${this.API_URL}/pacientes`, { params, headers: this.getAuthHeaders() }); @@ -132,7 +150,7 @@ export class PacienteService { buscarPorInterno(interno: string): Observable { const params = new HttpParams().set('interno', interno); - return this.http.get(`${this.API_URL}/api/pacientes`, { + return this.http.get(`${this.API_URL}/pacientes`, { params, headers: this.getAuthHeaders() }); @@ -140,7 +158,7 @@ export class PacienteService { buscarPorNombre(nombre: string): Observable { const params = new HttpParams().set('nombre', nombre); - return this.http.get(`${this.API_URL}/api/pacientes`, { + return this.http.get(`${this.API_URL}/pacientes`, { params, headers: this.getAuthHeaders() }); @@ -153,37 +171,48 @@ export class PacienteService { if (verTodas) { params = params.set('ver_todas', '1'); } - return this.http.get(`${this.API_URL}/api/ips-por-interno`, { + return this.http.get(`${this.API_URL}/ips-por-interno`, { params, headers: this.getAuthHeaders() }); } obtenerAutorizantes(): Observable { - return this.http.get(`${this.API_URL}/api/autorizantes`, { + return this.http.get(`${this.API_URL}/autorizantes`, { headers: this.getAuthHeaders() }); } obtenerAutorizantesAdmin(): Observable { - return this.http.get(`${this.API_URL}/api/autorizantes/admin`, { + return this.http.get(`${this.API_URL}/autorizantes/admin`, { headers: this.getAuthHeaders() }); } actualizarEstadoAutorizante(numeroDocumento: number, activo: boolean): Observable { return this.http.patch( - `${this.API_URL}/api/autorizantes/${numeroDocumento}/estado`, + `${this.API_URL}/autorizantes/${numeroDocumento}/estado`, { activo }, { headers: this.getAuthHeaders() } ); } + actualizarDatosAutorizante( + numeroDocumento: number, + payload: ActualizarAutorizantePayload + ): Observable { + return this.http.patch( + `${this.API_URL}/autorizantes/${numeroDocumento}`, + payload, + { headers: this.getAuthHeaders() } + ); + } + // ---- Autorizaciones ---- crearAutorizacion(payload: CrearAutorizacionPayload): Observable { return this.http.post( - `${this.API_URL}/api/autorizaciones`, + `${this.API_URL}/autorizaciones`, payload, { headers: this.getAuthHeaders() } ); @@ -194,7 +223,7 @@ export class PacienteService { payload: CrearAutorizacionPayload ): Observable { return this.http.put( - `${this.API_URL}/api/autorizaciones/${numeroAutorizacion}`, + `${this.API_URL}/autorizaciones/${numeroAutorizacion}`, payload, { headers: this.getAuthHeaders() } ); @@ -202,7 +231,7 @@ export class PacienteService { obtenerVersionesAutorizacion(numeroAutorizacion: string): Observable { return this.http.get( - `${this.API_URL}/api/autorizaciones/${numeroAutorizacion}/versiones`, + `${this.API_URL}/autorizaciones/${numeroAutorizacion}/versiones`, { headers: this.getAuthHeaders() } ); } @@ -210,7 +239,7 @@ export class PacienteService { obtenerAutorizacionesPorInterno(interno: string): Observable { const params = new HttpParams().set('interno', interno); return this.http.get( - `${this.API_URL}/api/autorizaciones`, + `${this.API_URL}/autorizaciones`, { params, headers: this.getAuthHeaders() } ); } @@ -229,7 +258,7 @@ export class PacienteService { // 3. Hacemos la petición con estos headers limpios. return this.http.post( - `${this.API_URL}/api/cargar-excel-pacientes`, + `${this.API_URL}/cargar-excel-pacientes`, formData, { headers } ); @@ -242,7 +271,46 @@ export class PacienteService { }); return this.http.post( - `${this.API_URL}/api/cargar-cups`, + `${this.API_URL}/cargar-cups`, + formData, + { headers } + ); + } + + cargarIps(formData: FormData): Observable { + const token = this.authService.getToken(); + const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}` + }); + + return this.http.post( + `${this.API_URL}/cargar-ips`, + formData, + { headers } + ); + } + + cargarReps(formData: FormData): Observable { + const token = this.authService.getToken(); + const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}` + }); + + return this.http.post( + `${this.API_URL}/cargar-reps`, + formData, + { headers } + ); + } + + cargarAutorizacionesMasivas(formData: FormData): Observable { + const token = this.authService.getToken(); + const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}` + }); + + return this.http.post( + `${this.API_URL}/cargar-autorizaciones-masivas`, formData, { headers } ); @@ -250,18 +318,49 @@ export class PacienteService { crearAutorizante(payload: CrearAutorizantePayload): Observable { return this.http.post( - `${this.API_URL}/api/autorizantes`, + `${this.API_URL}/autorizantes`, payload, { headers: this.getAuthHeaders() } ); } - buscarCupsCubiertos(termino: string): Observable { - const params = new HttpParams().set('q', termino); - return this.http.get(`${this.API_URL}/api/cups-cubiertos`, { + buscarCups(termino: string, limit = 200): Observable { + const params = new HttpParams() + .set('q', termino) + .set('limit', String(limit)); + return this.http.get(`${this.API_URL}/cups`, { params, headers: this.getAuthHeaders() }); } + actualizarEstadoAutorizacion( + numeroAutorizacion: string, + estado: 'pendiente' | 'autorizado' | 'no_autorizado' + ): Observable { + 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 { + 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() } + ); + } + } diff --git a/saludut-inpec/src/main.ts b/saludut-inpec/src/main.ts index 0ebe104..0d6b152 100644 --- a/saludut-inpec/src/main.ts +++ b/saludut-inpec/src/main.ts @@ -5,8 +5,9 @@ import { AppComponent } from './app/app'; import { provideRouter } from '@angular/router'; import { routes } from './app/app.routes'; 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 { AuthInterceptor } from './app/interceptors/auth.interceptor'; bootstrapApplication(AppComponent, { providers: [ @@ -15,6 +16,11 @@ bootstrapApplication(AppComponent, { HttpClientModule, FormsModule, ReactiveFormsModule - ) + ), + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + } ] }); diff --git a/saludut-inpec/src/styles.css b/saludut-inpec/src/styles.css index 55fc662..05ca96d 100644 --- a/saludut-inpec/src/styles.css +++ b/saludut-inpec/src/styles.css @@ -13,12 +13,23 @@ --color-bg: #f5f5f5; --color-card: #ffffff; --color-border: #e0e0e0; + --color-surface: #f8fafc; + --color-surface-alt: #f1f5f9; + --color-surface-muted: #eef2f7; + --color-surface-strong: #e7eef7; --color-text-main: #222222; --color-text-muted: #666666; --color-success: #2e7d32; --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-row: #ffffff; @@ -45,25 +56,33 @@ } [data-theme="dark"] { - --color-primary: #4fa3ff; - --color-primary-dark: #2d7dcc; - --color-primary-soft: #1d2b3b; - --color-header-grad-2: #1f4b87; + --color-primary: #3c7fc1; + --color-primary-dark: #2f628f; + --color-primary-soft: #1b2a3b; + --color-header-grad-2: #1e3f66; --color-bg: #0f141a; - --color-card: #151c24; - --color-border: #2a3440; + --color-card: #121a24; + --color-border: #1f2a36; + --color-surface: #0f1621; + --color-surface-alt: #151e2a; + --color-surface-muted: #1b2635; + --color-surface-strong: #1f2c3d; --color-text-main: #e7edf5; - --color-text-muted: #a8b3c2; + --color-text-muted: #a1adbd; --color-success: #3bb273; --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-row: #151c24; - --color-table-row-alt: #19222c; - --color-table-hover: #223046; + --color-table-head: #1b2430; + --color-table-row: #121a24; + --color-table-row-alt: #16202b; + --color-table-hover: #1f2a36; --color-input-bg: #111821; --color-input-border: #2c3a4a; @@ -77,6 +96,9 @@ --color-cup-item-bg: #151c24; --color-cup-item-border: #273241; --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); border-radius: var(--radius-card); 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 {