desarrollo
57
.gitignore
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node
|
||||||
|
**/node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Angular build
|
||||||
|
**/dist/
|
||||||
|
**/.angular/
|
||||||
|
**/.cache/
|
||||||
|
|
||||||
|
# Env
|
||||||
|
**/.env
|
||||||
59
README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# SaludutInpec
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.0.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
3507
backend/package-lock.json
generated
Normal file
33
backend/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "salud_ut",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "databasepg.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "nodemon src/server.js",
|
||||||
|
"start": "node src/server.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"libreoffice-convert": "^1.7.0",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"pdfkit": "^0.17.2",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"xlsx-populate": "^1.21.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"nodemon": "^3.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
backend/src/AUTORIZACIONES AMBULANCIAS EJEMPLO.xlsx
Normal file
47
backend/src/establecimiento.sql
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
-- 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;
|
||||||
BIN
backend/src/formato_ambulancias.xlsx
Normal file
BIN
backend/src/formato_autorizacion.xlsx
Normal file
166
backend/src/generar_codigo.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import ExcelJS from 'exceljs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
// Convierte letras de columna (ej: "Q") a número (17)
|
||||||
|
function colLettersToNumber(letters) {
|
||||||
|
let num = 0
|
||||||
|
for (const ch of letters.toUpperCase()) {
|
||||||
|
const val = ch.charCodeAt(0) - 64 // 'A' -> 1
|
||||||
|
num = num * 26 + val
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generarCodigoDesdeExcel() {
|
||||||
|
try {
|
||||||
|
const rutaExcel = path.join(__dirname, 'formato_ambulancias.xlsx')
|
||||||
|
console.log('Leyendo archivo desde:', rutaExcel)
|
||||||
|
|
||||||
|
const libro = new ExcelJS.Workbook()
|
||||||
|
await libro.xlsx.readFile(rutaExcel)
|
||||||
|
|
||||||
|
const hoja = libro.getWorksheet(1)
|
||||||
|
if (!hoja) {
|
||||||
|
console.error('No se encontró la primera hoja del Excel')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nombreHoja = hoja.name || 'Hoja1'
|
||||||
|
|
||||||
|
let codigo = ''
|
||||||
|
codigo += "import ExcelJS from 'exceljs'\n\n"
|
||||||
|
codigo += "async function crearArchivo() {\n"
|
||||||
|
codigo += " const workbook = new ExcelJS.Workbook()\n"
|
||||||
|
codigo += ` const sheet = workbook.addWorksheet(${JSON.stringify(nombreHoja)})\n\n`
|
||||||
|
|
||||||
|
// Vamos a detectar hasta dónde llega de verdad el formato
|
||||||
|
let ultimaColumnaConAlgo = 0
|
||||||
|
let ultimaFilaConAlgo = 0
|
||||||
|
|
||||||
|
// 1️⃣ Valores + formato de cada celda (tal cual la plantilla)
|
||||||
|
hoja.eachRow({ includeEmpty: true }, (row, rowNumber) => {
|
||||||
|
let filaTieneAlgo = false
|
||||||
|
|
||||||
|
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
||||||
|
const addr = cell.address
|
||||||
|
const val = cell.value
|
||||||
|
const fill = cell.fill
|
||||||
|
const border = cell.border
|
||||||
|
const alignment = cell.alignment
|
||||||
|
|
||||||
|
const tieneValor =
|
||||||
|
val !== null && val !== undefined && val !== ''
|
||||||
|
|
||||||
|
const tieneFormato =
|
||||||
|
!!fill ||
|
||||||
|
(border && Object.keys(border).length > 0) ||
|
||||||
|
(alignment && Object.keys(alignment).length > 0)
|
||||||
|
|
||||||
|
if (tieneValor || tieneFormato) {
|
||||||
|
filaTieneAlgo = true
|
||||||
|
if (colNumber > ultimaColumnaConAlgo) ultimaColumnaConAlgo = colNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Valor
|
||||||
|
if (tieneValor) {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
codigo += ` sheet.getCell('${addr}').value = ${JSON.stringify(val)};\n`
|
||||||
|
} else if (typeof val === 'number') {
|
||||||
|
codigo += ` sheet.getCell('${addr}').value = ${val};\n`
|
||||||
|
} else if (typeof val === 'boolean') {
|
||||||
|
codigo += ` sheet.getCell('${addr}').value = ${val};\n`
|
||||||
|
} else if (val instanceof Date) {
|
||||||
|
codigo += ` sheet.getCell('${addr}').value = new Date(${val.getFullYear()}, ${val.getMonth()}, ${val.getDate()});\n`
|
||||||
|
} else if (val && typeof val === 'object' && val.formula) {
|
||||||
|
codigo += ` sheet.getCell('${addr}').value = { formula: ${JSON.stringify(
|
||||||
|
val.formula
|
||||||
|
)}, result: ${JSON.stringify(val.result || null)} };\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎨 Color / relleno
|
||||||
|
if (fill) {
|
||||||
|
codigo += ` sheet.getCell('${addr}').fill = ${JSON.stringify(fill)};\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📏 Bordes
|
||||||
|
if (border && Object.keys(border).length > 0) {
|
||||||
|
codigo += ` sheet.getCell('${addr}').border = ${JSON.stringify(border)};\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📐 Alineación
|
||||||
|
if (alignment && Object.keys(alignment).length > 0) {
|
||||||
|
codigo += ` sheet.getCell('${addr}').alignment = ${JSON.stringify(alignment)};\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filaTieneAlgo && rowNumber > ultimaFilaConAlgo) {
|
||||||
|
ultimaFilaConAlgo = rowNumber
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
codigo += '\n'
|
||||||
|
codigo += ' // 2️⃣ Merges (bloques / uniones de celdas)\n'
|
||||||
|
const merges = (hoja.model && hoja.model.merges) ? hoja.model.merges : []
|
||||||
|
|
||||||
|
merges.forEach((rango) => {
|
||||||
|
// ejemplo rango: "B3:R3"
|
||||||
|
codigo += ` sheet.mergeCells('${rango}');\n`
|
||||||
|
|
||||||
|
const partes = rango.split(':')
|
||||||
|
if (partes.length === 2) {
|
||||||
|
const fin = partes[1] // "R3"
|
||||||
|
const match = fin.match(/^([A-Za-z]+)(\d+)$/)
|
||||||
|
if (match) {
|
||||||
|
const colLetters = match[1]
|
||||||
|
const rowStr = match[2]
|
||||||
|
const colNum = colLettersToNumber(colLetters)
|
||||||
|
const rowNum = parseInt(rowStr, 10)
|
||||||
|
|
||||||
|
if (!isNaN(colNum) && colNum > ultimaColumnaConAlgo) {
|
||||||
|
ultimaColumnaConAlgo = colNum
|
||||||
|
}
|
||||||
|
if (!isNaN(rowNum) && rowNum > ultimaFilaConAlgo) {
|
||||||
|
ultimaFilaConAlgo = rowNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Seguridad por si algo raro:
|
||||||
|
if (ultimaColumnaConAlgo === 0) ultimaColumnaConAlgo = hoja.columnCount || 1
|
||||||
|
if (ultimaFilaConAlgo === 0) ultimaFilaConAlgo = hoja.rowCount || 1
|
||||||
|
|
||||||
|
// 3️⃣ Ajuste: borde derecho en la última columna utilizada
|
||||||
|
codigo += '\n // 3️⃣ Ajuste: borde derecho en la última columna utilizada\n'
|
||||||
|
codigo += ` const ultimaColumna = ${ultimaColumnaConAlgo};\n`
|
||||||
|
codigo += ` const ultimaFila = ${ultimaFilaConAlgo};\n`
|
||||||
|
codigo += ' for (let fila = 1; fila <= ultimaFila; fila++) {\n'
|
||||||
|
codigo += ' const celda = sheet.getRow(fila).getCell(ultimaColumna);\n'
|
||||||
|
codigo += ' const bordeAnterior = celda.border || {};\n'
|
||||||
|
codigo += ' celda.border = {\n'
|
||||||
|
codigo += ' ...bordeAnterior,\n'
|
||||||
|
codigo += " right: bordeAnterior.right && bordeAnterior.right.style ? bordeAnterior.right : { style: 'thin' },\n"
|
||||||
|
codigo += ' };\n'
|
||||||
|
codigo += ' }\n\n'
|
||||||
|
|
||||||
|
codigo += " await workbook.xlsx.writeFile('./salida.xlsx')\n"
|
||||||
|
codigo += " console.log('Archivo generado desde plantilla_autorizacion.js')\n"
|
||||||
|
codigo += "}\n\n"
|
||||||
|
codigo += "crearArchivo().catch(console.error)\n"
|
||||||
|
|
||||||
|
const rutaSalidaCodigo = path.join(__dirname, 'plantilla_autorizacion_ambulancias.js')
|
||||||
|
await fs.writeFile(rutaSalidaCodigo, codigo, 'utf8')
|
||||||
|
|
||||||
|
console.log('✅ Código de plantilla generado en:', rutaSalidaCodigo)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error en generarCodigoDesdeExcel:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generarCodigoDesdeExcel()
|
||||||
17
backend/src/generate-hash.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
async function generateHash() {
|
||||||
|
const password = 'admin123';
|
||||||
|
|
||||||
|
console.log('Generando hash para:', password);
|
||||||
|
|
||||||
|
// Generar nuevo hash
|
||||||
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
console.log('Nuevo hash:', hash);
|
||||||
|
|
||||||
|
// Verificar que funciona
|
||||||
|
const isValid = await bcrypt.compare(password, hash);
|
||||||
|
console.log('¿Es válido?:', isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHash();
|
||||||
17
backend/src/generate-hash.js.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
async function generateHash() {
|
||||||
|
const password = 'admin123';
|
||||||
|
|
||||||
|
console.log('Generando hash para:', password);
|
||||||
|
|
||||||
|
// Generar nuevo hash
|
||||||
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
console.log('Nuevo hash:', hash);
|
||||||
|
|
||||||
|
// Verificar que funciona
|
||||||
|
const isValid = await bcrypt.compare(password, hash);
|
||||||
|
console.log('¿Es válido?:', isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHash();
|
||||||
36718
backend/src/ingreso.sql
Normal file
BIN
backend/src/logo_SALUDUT.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
36719
backend/src/paciente.sql
Normal file
164
backend/src/pacientes.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# === Configuración ===
|
||||||
|
archivo_excel = "pacientes.xlsx" # el Excel que vas a subir
|
||||||
|
archivo_paciente = "paciente.sql"
|
||||||
|
archivo_establec = "establecimiento.sql"
|
||||||
|
archivo_ingreso = "ingreso.sql"
|
||||||
|
|
||||||
|
|
||||||
|
def texto_a_sql(valor):
|
||||||
|
"""Devuelve un literal de texto SQL (o NULL si viene vacío)."""
|
||||||
|
if pd.isna(valor):
|
||||||
|
return "NULL"
|
||||||
|
s = str(valor).strip()
|
||||||
|
if s == "" or s.upper() == "NAN":
|
||||||
|
return "NULL"
|
||||||
|
s = s.replace("'", "''")
|
||||||
|
return f"'{s}'"
|
||||||
|
|
||||||
|
|
||||||
|
def fecha_a_sql(valor):
|
||||||
|
"""Convierte una fecha a 'YYYY-MM-DD' (o NULL)."""
|
||||||
|
if pd.isna(valor):
|
||||||
|
return "NULL"
|
||||||
|
if not isinstance(valor, (pd.Timestamp, datetime)):
|
||||||
|
valor = pd.to_datetime(valor, dayfirst=True, errors="coerce")
|
||||||
|
if pd.isna(valor):
|
||||||
|
return "NULL"
|
||||||
|
return f"'{valor.strftime('%Y-%m-%d')}'"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
df = pd.read_excel(archivo_excel, dtype=str)
|
||||||
|
|
||||||
|
print("Columnas detectadas:", list(df.columns))
|
||||||
|
|
||||||
|
sentencias_pac = []
|
||||||
|
sentencias_estab = []
|
||||||
|
sentencias_ing = []
|
||||||
|
|
||||||
|
vistos_estab = set()
|
||||||
|
vistos_paciente = set()
|
||||||
|
vistos_ingreso = set()
|
||||||
|
|
||||||
|
for i, fila in df.iterrows():
|
||||||
|
# ---------- INTERNO ----------
|
||||||
|
interno_raw = fila.get("INTERNO", "")
|
||||||
|
if interno_raw is None or str(interno_raw).strip() == "":
|
||||||
|
print(f"Fila {i}: sin INTERNO, se omite.")
|
||||||
|
continue
|
||||||
|
interno = str(interno_raw).strip()
|
||||||
|
interno_sql = texto_a_sql(interno)
|
||||||
|
|
||||||
|
# ---------- ESTABLECIMIENTO ----------
|
||||||
|
cod_est_raw = fila.get("CODIGO_ESTABLECIMIENTO", "")
|
||||||
|
cod_est = str(cod_est_raw).strip() if cod_est_raw is not None else ""
|
||||||
|
|
||||||
|
if cod_est != "" and cod_est not in vistos_estab:
|
||||||
|
cod_est_sql = texto_a_sql(cod_est)
|
||||||
|
nom_est_sql = texto_a_sql(fila.get("NOMBRE_ESTABLECIMIENTO"))
|
||||||
|
epc_ciudad_sql = texto_a_sql(fila.get("EPC_CIUDAD"))
|
||||||
|
epc_depart_sql = texto_a_sql(fila.get("EPC_DEPARTAMENTO"))
|
||||||
|
regional_sql = texto_a_sql(fila.get("REGIONAL"))
|
||||||
|
regional_norm_sql = texto_a_sql(fila.get("REGIONAL_NORMALIZADA"))
|
||||||
|
|
||||||
|
sentencia_e = (
|
||||||
|
"INSERT INTO establecimiento "
|
||||||
|
"(codigo_establecimiento, nombre_establecimiento, "
|
||||||
|
"epc_ciudad, epc_departamento, regional, regional_normalizada) "
|
||||||
|
f"VALUES ({cod_est_sql}, {nom_est_sql}, "
|
||||||
|
f"{epc_ciudad_sql}, {epc_depart_sql}, "
|
||||||
|
f"{regional_sql}, {regional_norm_sql}) "
|
||||||
|
"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;"
|
||||||
|
)
|
||||||
|
sentencias_estab.append(sentencia_e)
|
||||||
|
vistos_estab.add(cod_est)
|
||||||
|
|
||||||
|
# ---------- PACIENTE ----------
|
||||||
|
if interno not in vistos_paciente:
|
||||||
|
tipo_doc_sql = texto_a_sql(fila.get("TIPO_DOCUMENTO"))
|
||||||
|
num_doc_sql = texto_a_sql(fila.get("NUMERO_DOCUMENTO"))
|
||||||
|
primer_ape_sql = texto_a_sql(fila.get("PRIMER_APELLIDO"))
|
||||||
|
segundo_ape_sql = texto_a_sql(fila.get("SEGUNDO_APELLIDO"))
|
||||||
|
primer_nom_sql = texto_a_sql(fila.get("PRIMER_NOMBRE"))
|
||||||
|
segundo_nom_sql = texto_a_sql(fila.get("SEGUNDO_NOMBRE"))
|
||||||
|
sexo_sql = texto_a_sql(fila.get("SEXO"))
|
||||||
|
fecha_nac_sql = fecha_a_sql(fila.get("FECHA_NACIMIENTO"))
|
||||||
|
|
||||||
|
sentencia_p = (
|
||||||
|
"INSERT INTO paciente "
|
||||||
|
"(interno, tipo_documento, numero_documento, "
|
||||||
|
"primer_apellido, segundo_apellido, primer_nombre, segundo_nombre, "
|
||||||
|
"fecha_nacimiento, edad, sexo, activo) "
|
||||||
|
f"VALUES ({interno_sql}, {tipo_doc_sql}, {num_doc_sql}, "
|
||||||
|
f"{primer_ape_sql}, {segundo_ape_sql}, "
|
||||||
|
f"{primer_nom_sql}, {segundo_nom_sql}, "
|
||||||
|
f"{fecha_nac_sql}, NULL, {sexo_sql}, 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;"
|
||||||
|
)
|
||||||
|
sentencias_pac.append(sentencia_p)
|
||||||
|
vistos_paciente.add(interno)
|
||||||
|
|
||||||
|
# ---------- INGRESO ----------
|
||||||
|
if interno not in vistos_ingreso:
|
||||||
|
cod_est_sql_ing = texto_a_sql(cod_est) if cod_est != "" else "NULL"
|
||||||
|
estado_sql = texto_a_sql(fila.get("ESTADO"))
|
||||||
|
fecha_ing_sql = fecha_a_sql(fila.get("FECHA_INGRESO"))
|
||||||
|
nac_sql = texto_a_sql(fila.get("NACIONALIDAD"))
|
||||||
|
|
||||||
|
sentencia_i = (
|
||||||
|
"INSERT INTO ingreso "
|
||||||
|
"(interno, codigo_establecimiento, estado, fecha_ingreso, nacionalidad) "
|
||||||
|
f"VALUES ({interno_sql}, {cod_est_sql_ing}, {estado_sql}, "
|
||||||
|
f"{fecha_ing_sql}, {nac_sql}) "
|
||||||
|
"ON CONFLICT (interno) DO UPDATE SET "
|
||||||
|
"codigo_establecimiento = EXCLUDED.codigo_establecimiento, "
|
||||||
|
"estado = EXCLUDED.estado, "
|
||||||
|
"fecha_ingreso = EXCLUDED.fecha_ingreso, "
|
||||||
|
"nacionalidad = EXCLUDED.nacionalidad;"
|
||||||
|
)
|
||||||
|
sentencias_ing.append(sentencia_i)
|
||||||
|
vistos_ingreso.add(interno)
|
||||||
|
|
||||||
|
# ---------- Guardar archivos SQL ----------
|
||||||
|
with open(archivo_establec, "w", encoding="utf-8") as f_est:
|
||||||
|
f_est.write("-- UPSERTS TABLA ESTABLECIMIENTO\n")
|
||||||
|
for s in sentencias_estab:
|
||||||
|
f_est.write(s + "\n")
|
||||||
|
|
||||||
|
with open(archivo_paciente, "w", encoding="utf-8") as f_pac:
|
||||||
|
f_pac.write("-- UPSERTS TABLA PACIENTE\n")
|
||||||
|
# Marcamos todos como inactivos, y los del Excel quedarán activos
|
||||||
|
f_pac.write("UPDATE paciente SET activo = false;\n")
|
||||||
|
for s in sentencias_pac:
|
||||||
|
f_pac.write(s + "\n")
|
||||||
|
|
||||||
|
with open(archivo_ingreso, "w", encoding="utf-8") as f_ing:
|
||||||
|
f_ing.write("-- UPSERTS TABLA INGRESO\n")
|
||||||
|
for s in sentencias_ing:
|
||||||
|
f_ing.write(s + "\n")
|
||||||
|
|
||||||
|
print("Archivos generados:")
|
||||||
|
print(" ", archivo_establec, f"({len(sentencias_estab)} sentencias)")
|
||||||
|
print(" ", archivo_paciente, f"({len(sentencias_pac)} sentencias + UPDATE activo=false)")
|
||||||
|
print(" ", archivo_ingreso, f"({len(sentencias_ing)} sentencias)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
backend/src/pacientes.xlsx
Normal file
2780
backend/src/plantilla_autorizacion.js
Normal file
1874
backend/src/plantilla_autorizacion_ambulancias.js
Normal file
BIN
backend/src/salida.xlsx
Normal file
1150
backend/src/server.js
Normal file
1008
backend/src/server.js.txt
Normal file
14
backend/src/test-hash.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
async function testNewHash() {
|
||||||
|
const password = 'admin123';
|
||||||
|
const hash = '$2b$10$o/0xOZct1gOt3Oo3W/lVl.lS4bpVK20OWmkSN0cF/wpmUMkRsYyoO';
|
||||||
|
|
||||||
|
console.log('Password:', password);
|
||||||
|
console.log('Hash:', hash);
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(password, hash);
|
||||||
|
console.log('¿Es válido?:', isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
testNewHash();
|
||||||
17
saludut-inpec/.editorconfig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
4
saludut-inpec/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||||
|
"recommendations": ["angular.ng-template"]
|
||||||
|
}
|
||||||
20
saludut-inpec/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ng serve",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: start",
|
||||||
|
"url": "http://localhost:4200/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ng test",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: test",
|
||||||
|
"url": "http://localhost:9876/debug.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
saludut-inpec/.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "test",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
74
saludut-inpec/angular.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"cli": {
|
||||||
|
"packageManager": "npm",
|
||||||
|
"analytics": "46377033-163f-4782-b1ce-b4970431fa0d"
|
||||||
|
},
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"saludut-inpec": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "saludut-inpec:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "saludut-inpec:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:unit-test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9321
saludut-inpec/package-lock.json
generated
Normal file
43
saludut-inpec/package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "saludut-inpec",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "npm@10.9.3",
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^21.0.0",
|
||||||
|
"@angular/compiler": "^21.0.0",
|
||||||
|
"@angular/core": "^21.0.0",
|
||||||
|
"@angular/forms": "^21.0.0",
|
||||||
|
"@angular/platform-browser": "^21.0.0",
|
||||||
|
"@angular/router": "^21.0.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^21.0.0",
|
||||||
|
"@angular/cli": "^21.0.0",
|
||||||
|
"@angular/compiler-cli": "^21.0.0",
|
||||||
|
"jsdom": "^27.1.0",
|
||||||
|
"typescript": "~5.9.2",
|
||||||
|
"vitest": "^4.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
saludut-inpec/public/LOGOSALUDUT.svg
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
saludut-inpec/public/assets/logo_SALUDUT.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
saludut-inpec/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
saludut-inpec/public/logo_SALUDUT.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
1
saludut-inpec/src/app/LOGOSALUDUT.svg
Normal file
|
After Width: | Height: | Size: 233 KiB |
11
saludut-inpec/src/app/app.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(),
|
||||||
|
],
|
||||||
|
};
|
||||||
0
saludut-inpec/src/app/app.css
Normal file
1
saludut-inpec/src/app/app.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
||||||
253
saludut-inpec/src/app/app.html.txt
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<div class="container">
|
||||||
|
<!-- Header con logo SALUD UT -->
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<img
|
||||||
|
src="/logo_SALUDUT.png"
|
||||||
|
alt="SALUD UT"
|
||||||
|
class="logo-saludut"
|
||||||
|
/>
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>{{ titulo }}</h1>
|
||||||
|
<p class="subtitle">Módulo de autorizaciones médicas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="badge-saludut">SALUD UT</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Tarjeta de búsqueda -->
|
||||||
|
<div class="search-card">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="tipo">Buscar por:</label>
|
||||||
|
<select id="tipo" [(ngModel)]="tipoBusqueda">
|
||||||
|
<option value="documento">Cédula / Documento</option>
|
||||||
|
<option value="interno">Número interno</option>
|
||||||
|
<option value="nombre">Nombre completo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="termino">Valor:</label>
|
||||||
|
<input
|
||||||
|
id="termino"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="termino"
|
||||||
|
placeholder="Ej: 79427056, 372, JORGE IVAN"
|
||||||
|
(keyup.enter)="buscar()"
|
||||||
|
/>
|
||||||
|
<button (click)="buscar()" [disabled]="cargando">
|
||||||
|
{{ cargando ? 'Buscando...' : 'Buscar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-excel">
|
||||||
|
<button type="button" (click)="inputExcel.click()" [disabled]="cargandoExcel">
|
||||||
|
{{ cargandoExcel ? 'Cargando Excel...' : 'Cargar Excel de PPL' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#inputExcel
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
(change)="onExcelSelected($event)"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="upload-msg" *ngIf="estadoCargaExcel">
|
||||||
|
{{ estadoCargaExcel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="estado error" *ngIf="error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layout: tabla de pacientes -->
|
||||||
|
<div class="resultados-layout">
|
||||||
|
<div class="col-tabla">
|
||||||
|
<div *ngIf="pacientes.length > 0">
|
||||||
|
<table class="tabla">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Interno</th>
|
||||||
|
<th>Documento</th>
|
||||||
|
<th>Nombre completo</th>
|
||||||
|
<th>Sexo</th>
|
||||||
|
<th>Fecha nacimiento</th>
|
||||||
|
<th>Establecimiento</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Fecha ingreso</th>
|
||||||
|
<th>Tiempo reclusión (años)</th>
|
||||||
|
<th>Autorización</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let p of pacientes">
|
||||||
|
<td>{{ p.interno }}</td>
|
||||||
|
<td>{{ p.numero_documento }}</td>
|
||||||
|
<td>
|
||||||
|
{{ p.primer_nombre }} {{ p.segundo_nombre }}
|
||||||
|
{{ p.primer_apellido }} {{ p.segundo_apellido }}
|
||||||
|
</td>
|
||||||
|
<td>{{ p.sexo }}</td>
|
||||||
|
<td>{{ p.fecha_nacimiento | date: 'yyyy-MM-dd' }}</td>
|
||||||
|
<td>{{ p.nombre_establecimiento }}</td>
|
||||||
|
<td>{{ p.estado }}</td>
|
||||||
|
<td>{{ p.fecha_ingreso | date: 'yyyy-MM-dd' }}</td>
|
||||||
|
<td>{{ p.tiempo_reclusion }}</td>
|
||||||
|
<td>
|
||||||
|
<button (click)="seleccionarPaciente(p)">Autorizar</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!cargando && !error && pacientes.length === 0">
|
||||||
|
No hay resultados para mostrar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de autorización -->
|
||||||
|
<div class="aut-modal-backdrop" *ngIf="pacienteSeleccionado">
|
||||||
|
<div class="aut-modal">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="aut-modal-close"
|
||||||
|
(click)="cerrarAutorizacion()"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2>Nueva autorización</h2>
|
||||||
|
|
||||||
|
<p class="aut-paciente">
|
||||||
|
<strong>Interno:</strong> {{ pacienteSeleccionado?.interno }}<br />
|
||||||
|
<strong>Nombre:</strong>
|
||||||
|
{{ pacienteSeleccionado?.primer_nombre }}
|
||||||
|
{{ pacienteSeleccionado?.segundo_nombre }}
|
||||||
|
{{ pacienteSeleccionado?.primer_apellido }}
|
||||||
|
{{ pacienteSeleccionado?.segundo_apellido }}<br />
|
||||||
|
<strong>Establecimiento:</strong>
|
||||||
|
{{ pacienteSeleccionado?.nombre_establecimiento }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="numAut">Número autorización:</label>
|
||||||
|
<input
|
||||||
|
id="numAut"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="formAutorizacion.numero_autorizacion"
|
||||||
|
placeholder="Ej: 2025-000123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="ips">IPS / Hospital:</label>
|
||||||
|
<select id="ips" [(ngModel)]="formAutorizacion.id_ips">
|
||||||
|
<option value="">-- Seleccione IPS --</option>
|
||||||
|
<option *ngFor="let ips of ipsDisponibles" [value]="ips.id_ips">
|
||||||
|
{{ ips.nombre_ips }} ({{ ips.municipio }} - {{ ips.departamento }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="autorizante">Quién autoriza:</label>
|
||||||
|
<select
|
||||||
|
id="autorizante"
|
||||||
|
[(ngModel)]="formAutorizacion.numero_documento_autorizante"
|
||||||
|
>
|
||||||
|
<option value="">-- Seleccione --</option>
|
||||||
|
<option
|
||||||
|
*ngFor="let a of autorizantes"
|
||||||
|
[value]="a.numero_documento"
|
||||||
|
>
|
||||||
|
{{ a.nombre }} ({{ a.cargo }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="fechaAut">Fecha autorización:</label>
|
||||||
|
<input
|
||||||
|
id="fechaAut"
|
||||||
|
type="date"
|
||||||
|
[(ngModel)]="formAutorizacion.fecha_autorizacion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="obs">Observación / servicios autorizados:</label>
|
||||||
|
<textarea
|
||||||
|
id="obs"
|
||||||
|
rows="4"
|
||||||
|
[(ngModel)]="formAutorizacion.observacion"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aut-actions">
|
||||||
|
<button
|
||||||
|
(click)="guardarAutorizacion()"
|
||||||
|
[disabled]="guardandoAutorizacion"
|
||||||
|
>
|
||||||
|
{{ guardandoAutorizacion ? 'Guardando...' : 'Guardar autorización' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-secondary"
|
||||||
|
*ngIf="pacienteSeleccionado"
|
||||||
|
(click)="verAutorizaciones()"
|
||||||
|
>
|
||||||
|
Ver autorizaciones
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="estado ok" *ngIf="mensajeAutorizacion">
|
||||||
|
{{ mensajeAutorizacion }}
|
||||||
|
</div>
|
||||||
|
<div class="estado error" *ngIf="errorAutorizacion">
|
||||||
|
{{ errorAutorizacion }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="autorizaciones-previas"
|
||||||
|
*ngIf="pacienteSeleccionado && autorizacionesPaciente.length > 0"
|
||||||
|
>
|
||||||
|
<h3>Autorizaciones previas</h3>
|
||||||
|
<table class="tabla-autorizaciones">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>N° autorización</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>IPS</th>
|
||||||
|
<th>Autoriza</th>
|
||||||
|
<th>Ver PDF</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let a of autorizacionesPaciente">
|
||||||
|
<td>{{ a.numero_autorizacion }}</td>
|
||||||
|
<td>{{ a.fecha_autorizacion | date: 'yyyy-MM-dd' }}</td>
|
||||||
|
<td>{{ a.nombre_ips }}</td>
|
||||||
|
<td>{{ a.nombre_autorizante }}</td>
|
||||||
|
<td>
|
||||||
|
<button (click)="descargarPdf(a.numero_autorizacion)">
|
||||||
|
Ver PDF
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="estado error" *ngIf="errorAutLista">
|
||||||
|
{{ errorAutLista }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
44
saludut-inpec/src/app/app.routes.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { LoginComponent } from './components/login/login';
|
||||||
|
import { DashboardComponent } from './components/dashboard/dashboard';
|
||||||
|
import { AuthGuard } from './guards/auth-guard';
|
||||||
|
import { AdminGuard } from './guards/auth-guard';
|
||||||
|
import { AutorizacionesPorFechaComponent } from './components/autorizaciones-por-fecha/autorizaciones-por-fecha';
|
||||||
|
import { AutorizacionesComponent } from './components/autorizaciones/autorizaciones';
|
||||||
|
import { UsuariosComponent } from './components/usuarios/usuarios';
|
||||||
|
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
// base: siempre al login
|
||||||
|
{ path: '', pathMatch: 'full', redirectTo: 'login' },
|
||||||
|
|
||||||
|
{ path: 'login', component: LoginComponent },
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
component: DashboardComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'autorizaciones',
|
||||||
|
component: AutorizacionesComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'autorizaciones-por-fecha',
|
||||||
|
component: AutorizacionesPorFechaComponent,
|
||||||
|
canActivate: [AuthGuard, AdminGuard],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'usuarios',
|
||||||
|
component: UsuariosComponent,
|
||||||
|
canActivate: [AuthGuard]
|
||||||
|
},
|
||||||
|
|
||||||
|
// cualquier cosa rara → dashboard
|
||||||
|
{ path: '**', redirectTo: 'dashboard' },
|
||||||
|
|
||||||
|
];
|
||||||
23
saludut-inpec/src/app/app.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', async () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
await fixture.whenStable();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, saludut-inpec');
|
||||||
|
});
|
||||||
|
});
|
||||||
11
saludut-inpec/src/app/app.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrls: ['./app.css'],
|
||||||
|
})
|
||||||
|
export class AppComponent {}
|
||||||
320
saludut-inpec/src/app/app.ts.txt
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import { Component, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
PacienteService,
|
||||||
|
Paciente,
|
||||||
|
Ips,
|
||||||
|
Autorizante,
|
||||||
|
RespuestaAutorizacion,
|
||||||
|
AutorizacionListado,
|
||||||
|
} from './services/paciente';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrls: ['./app.css'],
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
titulo = 'Consulta PPL INPEC';
|
||||||
|
|
||||||
|
// ---- Búsqueda ----
|
||||||
|
tipoBusqueda: 'documento' | 'interno' | 'nombre' = 'documento';
|
||||||
|
termino = '';
|
||||||
|
|
||||||
|
pacientes: Paciente[] = [];
|
||||||
|
cargando = false;
|
||||||
|
error: string | null = null;
|
||||||
|
|
||||||
|
// ---- Excel ----
|
||||||
|
cargandoExcel = false;
|
||||||
|
estadoCargaExcel: string | null = null;
|
||||||
|
|
||||||
|
// ---- Autorizaciones ----
|
||||||
|
pacienteSeleccionado: Paciente | null = null;
|
||||||
|
ipsDisponibles: Ips[] = [];
|
||||||
|
autorizantes: Autorizante[] = [];
|
||||||
|
|
||||||
|
formAutorizacion = {
|
||||||
|
numero_autorizacion: '',
|
||||||
|
id_ips: '',
|
||||||
|
numero_documento_autorizante: '',
|
||||||
|
fecha_autorizacion: '',
|
||||||
|
observacion: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
guardandoAutorizacion = false;
|
||||||
|
mensajeAutorizacion: string | null = null;
|
||||||
|
errorAutorizacion: string | null = null;
|
||||||
|
|
||||||
|
autorizacionesPaciente: AutorizacionListado[] = [];
|
||||||
|
cargandoAutorizaciones = false;
|
||||||
|
errorAutLista: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private pacienteService: PacienteService,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Búsqueda de pacientes
|
||||||
|
// -------------------------
|
||||||
|
buscar(): void {
|
||||||
|
this.error = null;
|
||||||
|
this.cargando = true;
|
||||||
|
this.pacientes = [];
|
||||||
|
this.pacienteSeleccionado = null;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
|
||||||
|
const valor = this.termino.trim();
|
||||||
|
if (!valor) {
|
||||||
|
this.cargando = false;
|
||||||
|
this.error = 'Ingrese un valor para buscar.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obs$;
|
||||||
|
|
||||||
|
if (this.tipoBusqueda === 'documento') {
|
||||||
|
obs$ = this.pacienteService.buscarPorDocumento(valor);
|
||||||
|
} else if (this.tipoBusqueda === 'interno') {
|
||||||
|
obs$ = this.pacienteService.buscarPorInterno(valor);
|
||||||
|
} else {
|
||||||
|
obs$ = this.pacienteService.buscarPorNombre(valor);
|
||||||
|
}
|
||||||
|
|
||||||
|
obs$
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cargando = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: Paciente[]) => {
|
||||||
|
this.pacientes = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.error = 'Error consultando pacientes.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 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 = null;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('archivo', file);
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.cargarExcelPacientes(formData)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
input.value = '';
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (resp) => {
|
||||||
|
this.estadoCargaExcel = `${resp.mensaje}. Vigentes: ${resp.activos}, antiguos: ${resp.antiguos}.`;
|
||||||
|
// Refrescar la tabla con el último criterio si había algo
|
||||||
|
if (this.termino) {
|
||||||
|
this.buscar();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
err.error?.error || 'Error cargando el archivo Excel.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Seleccionar paciente
|
||||||
|
// -------------------------
|
||||||
|
seleccionarPaciente(p: Paciente): void {
|
||||||
|
this.pacienteSeleccionado = p;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
|
||||||
|
this.formAutorizacion = {
|
||||||
|
numero_autorizacion: '',
|
||||||
|
id_ips: '',
|
||||||
|
numero_documento_autorizante: '',
|
||||||
|
fecha_autorizacion: '',
|
||||||
|
observacion: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.autorizacionesPaciente = [];
|
||||||
|
this.errorAutLista = null;
|
||||||
|
|
||||||
|
this.cargarIps(p.interno);
|
||||||
|
this.cargarAutorizantes();
|
||||||
|
}
|
||||||
|
|
||||||
|
cerrarAutorizacion(): void {
|
||||||
|
this.pacienteSeleccionado = null;
|
||||||
|
this.autorizacionesPaciente = [];
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
this.errorAutLista = null;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private cargarIps(interno: string): void {
|
||||||
|
this.ipsDisponibles = [];
|
||||||
|
this.pacienteService
|
||||||
|
.obtenerIpsPorInterno(interno)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: Ips[]) => {
|
||||||
|
this.ipsDisponibles = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizacion =
|
||||||
|
'No se pudieron cargar las IPS para este interno.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private cargarAutorizantes(): void {
|
||||||
|
this.autorizantes = [];
|
||||||
|
this.pacienteService
|
||||||
|
.obtenerAutorizantes()
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: Autorizante[]) => {
|
||||||
|
this.autorizantes = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizacion =
|
||||||
|
'No se pudieron cargar las personas autorizantes.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Guardar autorización
|
||||||
|
// -------------------------
|
||||||
|
guardarAutorizacion(): void {
|
||||||
|
if (!this.pacienteSeleccionado) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
|
||||||
|
if (!this.formAutorizacion.numero_autorizacion.trim()) {
|
||||||
|
this.errorAutorizacion = 'Debe ingresar el número de autorización.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.formAutorizacion.id_ips) {
|
||||||
|
this.errorAutorizacion = 'Debe seleccionar una IPS.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.formAutorizacion.numero_documento_autorizante) {
|
||||||
|
this.errorAutorizacion = 'Debe seleccionar quién autoriza.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
numero_autorizacion: this.formAutorizacion.numero_autorizacion.trim(),
|
||||||
|
interno: this.pacienteSeleccionado.interno,
|
||||||
|
id_ips: Number(this.formAutorizacion.id_ips),
|
||||||
|
numero_documento_autorizante: Number(
|
||||||
|
this.formAutorizacion.numero_documento_autorizante
|
||||||
|
),
|
||||||
|
observacion: this.formAutorizacion.observacion,
|
||||||
|
fecha_autorizacion:
|
||||||
|
this.formAutorizacion.fecha_autorizacion || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.guardandoAutorizacion = true;
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.crearAutorizacion(payload)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.guardandoAutorizacion = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (resp: RespuestaAutorizacion) => {
|
||||||
|
this.mensajeAutorizacion = `Autorización N° ${resp.numero_autorizacion} creada el ${resp.fecha_autorizacion}.`;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizacion =
|
||||||
|
err.error?.error || 'Error guardando la autorización.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Ver autorizaciones del interno
|
||||||
|
// -------------------------
|
||||||
|
verAutorizaciones(): void {
|
||||||
|
if (!this.pacienteSeleccionado) return;
|
||||||
|
|
||||||
|
this.cargandoAutorizaciones = true;
|
||||||
|
this.errorAutLista = null;
|
||||||
|
this.autorizacionesPaciente = [];
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.obtenerAutorizacionesPorInterno(this.pacienteSeleccionado.interno)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cargandoAutorizaciones = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: AutorizacionListado[]) => {
|
||||||
|
this.autorizacionesPaciente = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutLista = 'Error consultando autorizaciones.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Descargar PDF
|
||||||
|
// -------------------------
|
||||||
|
descargarPdf(numero_autorizacion: string): void {
|
||||||
|
window.open(
|
||||||
|
`http://localhost:3000/api/generar-pdf-autorizacion?numero_autorizacion=${encodeURIComponent(
|
||||||
|
numero_autorizacion
|
||||||
|
)}`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,550 @@
|
|||||||
|
/* ============================
|
||||||
|
Estilos del Componente Autorizaciones por Fecha
|
||||||
|
============================ */
|
||||||
|
|
||||||
|
.autorizaciones-fecha-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-family: var(--font-main, "Inter", system-ui, -apple-system, sans-serif);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Header */
|
||||||
|
.page-header {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: #1976d2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: #145ca5;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-left: 4px solid #dc2626;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-left: 4px solid #16a34a;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-message {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filtros Card */
|
||||||
|
.filtros-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtros-card h2 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtros-form .form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1976d2;
|
||||||
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input.error {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background-color: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-buscar {
|
||||||
|
background: linear-gradient(90deg, #1976d2, #1565c0);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 140px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-buscar:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-buscar:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resultados Section */
|
||||||
|
.resultados-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-exportar,
|
||||||
|
.btn-descargar-todos {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #1976d2;
|
||||||
|
color: #1976d2;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-exportar:hover,
|
||||||
|
.btn-descargar-todos:hover {
|
||||||
|
background: #e3f2fd;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-table th {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222222;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-table tr:hover td {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-table tr.even-row td {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Columnas específicas */
|
||||||
|
.numero-autorizacion {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1976d2;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fecha {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interno {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nombre-paciente {
|
||||||
|
max-width: 200px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips {
|
||||||
|
max-width: 180px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.municipio,
|
||||||
|
.departamento {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizante {
|
||||||
|
max-width: 150px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.establecimiento {
|
||||||
|
max-width: 150px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acciones {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-descargar {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-descargar:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 60px 24px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Línea de estado inferior para procesos de carga */
|
||||||
|
.status-line {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #1976d2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner pequeñito en línea */
|
||||||
|
.spinner-inline {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(25, 118, 210, 0.3);
|
||||||
|
border-top-color: #1976d2;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.loading-content p {
|
||||||
|
margin: 0;
|
||||||
|
color: #222222;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.autorizaciones-fecha-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtros-form .form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-table th,
|
||||||
|
.autorizaciones-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nombre-paciente,
|
||||||
|
.ips,
|
||||||
|
.autorizante,
|
||||||
|
.establecimiento {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-exportar,
|
||||||
|
.btn-descargar-todos {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,157 @@
|
|||||||
|
<div class="autorizaciones-fecha-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="back-button" (click)="volverAtras()" title="Volver">
|
||||||
|
← Volver
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1>Autorizaciones por Fecha</h1>
|
||||||
|
<p class="header-subtitle">Consulta y descarga de autorizaciones por rango de fechas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensajes -->
|
||||||
|
<div class="alert alert-error" *ngIf="errorMessage">
|
||||||
|
<span class="alert-icon">⚠️</span>
|
||||||
|
<span class="alert-message">{{ errorMessage }}</span>
|
||||||
|
<button class="alert-close" (click)="limpiarMensajes()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<span class="alert-icon">✅</span>
|
||||||
|
<span class="alert-message">{{ successMessage }}</span>
|
||||||
|
<button class="alert-close" (click)="limpiarMensajes()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros -->
|
||||||
|
<div class="filtros-card">
|
||||||
|
<h2>📅 Rango de Fechas</h2>
|
||||||
|
<form class="filtros-form" [formGroup]="filtroForm" (ngSubmit)="buscarAutorizaciones()">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fecha_inicio">Fecha de inicio:</label>
|
||||||
|
<input
|
||||||
|
id="fecha_inicio"
|
||||||
|
type="date"
|
||||||
|
formControlName="fecha_inicio"
|
||||||
|
[class.error]="fecha_inicio?.invalid && fecha_inicio?.touched"
|
||||||
|
(change)="validarFechas()"
|
||||||
|
/>
|
||||||
|
<div class="error-message" *ngIf="fecha_inicio?.invalid && fecha_inicio?.touched">
|
||||||
|
{{ getFechaInicioErrorMessage() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fecha_fin">Fecha de fin:</label>
|
||||||
|
<input
|
||||||
|
id="fecha_fin"
|
||||||
|
type="date"
|
||||||
|
formControlName="fecha_fin"
|
||||||
|
[class.error]="fecha_fin?.invalid && fecha_fin?.touched"
|
||||||
|
(change)="validarFechas()"
|
||||||
|
/>
|
||||||
|
<div class="error-message" *ngIf="fecha_fin?.invalid && fecha_fin?.touched">
|
||||||
|
{{ getFechaFinErrorMessage() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn-buscar"
|
||||||
|
[disabled]="filtroForm.invalid || isLoading"
|
||||||
|
>
|
||||||
|
<span *ngIf="!isLoading">🔍 Buscar</span>
|
||||||
|
<span *ngIf="isLoading" class="loading-spinner">
|
||||||
|
<span class="spinner"></span>
|
||||||
|
Buscando...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resultados -->
|
||||||
|
<div class="resultados-section" *ngIf="hayResultados || autorizaciones.length > 0">
|
||||||
|
<div class="resultados-header">
|
||||||
|
<h2>📋 Resultados ({{ autorizaciones.length }} autorizaciones)</h2>
|
||||||
|
<div class="resultados-actions">
|
||||||
|
<button
|
||||||
|
class="btn-exportar"
|
||||||
|
(click)="exportarAExcel()"
|
||||||
|
title="Exportar a Excel"
|
||||||
|
>
|
||||||
|
📊 Exportar Excel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-descargar-todos"
|
||||||
|
(click)="descargarTodosLosPdfs()"
|
||||||
|
title="Descargar todos los PDFs"
|
||||||
|
>
|
||||||
|
📄 Descargar todos los PDFs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabla de resultados -->
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="autorizaciones-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>N° Autorización</th>
|
||||||
|
<th>Fecha y Hora</th>
|
||||||
|
<th>Interno</th>
|
||||||
|
<th>Nombre Paciente</th>
|
||||||
|
<th>IPS</th>
|
||||||
|
<th>Municipio</th>
|
||||||
|
<th>Departamento</th>
|
||||||
|
<th>Autorizante</th>
|
||||||
|
<th>Establecimiento</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let aut of autorizaciones; let i = index" [class.even-row]="i % 2 === 0">
|
||||||
|
<td class="numero-autorizacion">
|
||||||
|
<strong>{{ aut.numero_autorizacion }}</strong>
|
||||||
|
</td>
|
||||||
|
<td class="fecha">{{ formatDateTime(aut.fecha_autorizacion) }}</td>
|
||||||
|
<td class="interno">{{ aut.interno }}</td>
|
||||||
|
<td class="nombre-paciente">{{ aut.nombre_paciente }}</td>
|
||||||
|
<td class="ips">{{ aut.nombre_ips }}</td>
|
||||||
|
<td class="municipio">{{ aut.municipio }}</td>
|
||||||
|
<td class="departamento">{{ aut.departamento }}</td>
|
||||||
|
<td class="autorizante">{{ aut.nombre_autorizante }}</td>
|
||||||
|
<td class="establecimiento">{{ aut.nombre_establecimiento }}</td>
|
||||||
|
<td class="acciones">
|
||||||
|
<button
|
||||||
|
class="btn-descargar"
|
||||||
|
(click)="descargarPdf(aut.numero_autorizacion)"
|
||||||
|
title="Descargar PDF"
|
||||||
|
>
|
||||||
|
📄 PDF
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado vacío -->
|
||||||
|
<div class="empty-state" *ngIf="!hayResultados && !isLoading && autorizaciones.length === 0">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<h3>No hay resultados para mostrar</h3>
|
||||||
|
<p>Selecciona un rango de fechas y haz clic en "Buscar" para consultar las autorizaciones.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicador de carga simple (línea abajo) -->
|
||||||
|
<div class="status-line" *ngIf="isLoading">
|
||||||
|
<span class="spinner-inline"></span>
|
||||||
|
Consultando autorizaciones, por favor espera...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,280 @@
|
|||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||||
|
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
import { AuthService } from '../../services/auth';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-autorizaciones-por-fecha',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ReactiveFormsModule],
|
||||||
|
templateUrl: './autorizaciones-por-fecha.html',
|
||||||
|
styleUrls: ['./autorizaciones-por-fecha.css']
|
||||||
|
})
|
||||||
|
export class AutorizacionesPorFechaComponent implements OnInit {
|
||||||
|
filtroForm: FormGroup;
|
||||||
|
autorizaciones: any[] = [];
|
||||||
|
isLoading = false;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// Para saber si ya buscamos algo
|
||||||
|
hayResultados = false;
|
||||||
|
|
||||||
|
// Fechas en formato yyyy-MM-dd para usar en la API
|
||||||
|
private fechaInicioApi: string | null = null;
|
||||||
|
private fechaFinApi: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private authService: AuthService,
|
||||||
|
@Inject(DOCUMENT) private document: Document
|
||||||
|
) {
|
||||||
|
this.filtroForm = this.fb.group({
|
||||||
|
fecha_inicio: ['', Validators.required],
|
||||||
|
fecha_fin: ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rango por defecto: últimos 30 días
|
||||||
|
const hoy = new Date();
|
||||||
|
const hace30Dias = new Date(hoy.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
this.filtroForm.patchValue({
|
||||||
|
fecha_inicio: this.formatDateForInput(hace30Dias),
|
||||||
|
fecha_fin: this.formatDateForInput(hoy)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= BÚSQUEDA =========
|
||||||
|
buscarAutorizaciones(): void {
|
||||||
|
this.limpiarMensajes();
|
||||||
|
|
||||||
|
if (!this.fecha_inicio?.value || !this.fecha_fin?.value) {
|
||||||
|
this.errorMessage = 'Debes seleccionar ambas fechas.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inicio = new Date(this.fecha_inicio.value);
|
||||||
|
const fin = new Date(this.fecha_fin.value);
|
||||||
|
|
||||||
|
if (inicio > fin) {
|
||||||
|
this.errorMessage = 'La fecha de inicio no puede ser mayor que la fecha de fin.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardamos las fechas ya formateadas para reutilizarlas
|
||||||
|
this.fechaInicioApi = this.formatDateForInput(inicio);
|
||||||
|
this.fechaFinApi = this.formatDateForInput(fin);
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.autorizaciones = [];
|
||||||
|
this.hayResultados = false;
|
||||||
|
|
||||||
|
this.authService
|
||||||
|
.getAutorizacionesPorFecha(this.fechaInicioApi!, this.fechaFinApi!)
|
||||||
|
.pipe(finalize(() => (this.isLoading = false)))
|
||||||
|
.subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.autorizaciones = data || [];
|
||||||
|
this.hayResultados = this.autorizaciones.length > 0;
|
||||||
|
|
||||||
|
if (!this.hayResultados) {
|
||||||
|
this.successMessage = 'No hay autorizaciones en ese rango de fechas.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorMessage =
|
||||||
|
err?.error?.error || 'Error consultando las autorizaciones.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= PDF INDIVIDUAL =========
|
||||||
|
descargarPdf(numeroAutorizacion: string): void {
|
||||||
|
const url = `http://localhost:3000/api/generar-pdf-autorizacion?numero_autorizacion=${encodeURIComponent(
|
||||||
|
numeroAutorizacion
|
||||||
|
)}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= ZIP CON TODAS LAS AUTORIZACIONES =========
|
||||||
|
descargarTodosLosPdfs(): void {
|
||||||
|
this.limpiarMensajes();
|
||||||
|
|
||||||
|
if (!this.autorizaciones || this.autorizaciones.length === 0) {
|
||||||
|
this.errorMessage = 'No hay autorizaciones para descargar.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.fechaInicioApi || !this.fechaFinApi) {
|
||||||
|
this.errorMessage = 'Primero realiza una búsqueda por fechas.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.authService
|
||||||
|
.descargarAutorizacionesZip(this.fechaInicioApi!, this.fechaFinApi!)
|
||||||
|
.pipe(finalize(() => (this.isLoading = false)))
|
||||||
|
.subscribe({
|
||||||
|
next: (blob: Blob) => {
|
||||||
|
const nombreArchivo = `autorizaciones_${this.fechaInicioApi}_${this.fechaFinApi}.zip`;
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = nombreArchivo;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
this.successMessage = 'ZIP descargado correctamente.';
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorMessage =
|
||||||
|
err?.error?.error || 'Error descargando el ZIP de autorizaciones.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= NAVEGACIÓN =========
|
||||||
|
volverAtras(): void {
|
||||||
|
this.document.defaultView?.history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= UTILIDADES =========
|
||||||
|
private formatDateForInput(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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('es-ES', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDateTime(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('es-ES', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
limpiarMensajes(): void {
|
||||||
|
this.errorMessage = null;
|
||||||
|
this.successMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= GETTERS DEL FORM =========
|
||||||
|
get fecha_inicio() {
|
||||||
|
return this.filtroForm.get('fecha_inicio');
|
||||||
|
}
|
||||||
|
|
||||||
|
get fecha_fin() {
|
||||||
|
return this.filtroForm.get('fecha_fin');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= ERRORES DE FORM =========
|
||||||
|
getFechaInicioErrorMessage(): string {
|
||||||
|
if (this.fecha_inicio?.hasError('required')) {
|
||||||
|
return 'La fecha de inicio es requerida';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getFechaFinErrorMessage(): string {
|
||||||
|
if (this.fecha_fin?.hasError('required')) {
|
||||||
|
return 'La fecha de fin es requerida';
|
||||||
|
}
|
||||||
|
if (this.fecha_fin?.hasError('fechaInvalida')) {
|
||||||
|
return 'La fecha de fin debe ser posterior a la fecha de inicio';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
validarFechas(): void {
|
||||||
|
const fechaInicio = this.filtroForm.get('fecha_inicio')?.value;
|
||||||
|
const fechaFin = this.filtroForm.get('fecha_fin')?.value;
|
||||||
|
|
||||||
|
if (fechaInicio && fechaFin && new Date(fechaFin) < new Date(fechaInicio)) {
|
||||||
|
this.filtroForm.get('fecha_fin')?.setErrors({ fechaInvalida: true });
|
||||||
|
} else {
|
||||||
|
this.filtroForm.get('fecha_fin')?.setErrors(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= EXPORTAR A CSV SIMPLE =========
|
||||||
|
exportarAExcel(): void {
|
||||||
|
if (this.autorizaciones.length === 0) {
|
||||||
|
this.errorMessage = 'No hay datos para exportar.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Número Autorización',
|
||||||
|
'Fecha',
|
||||||
|
'Interno',
|
||||||
|
'Nombre Paciente',
|
||||||
|
'IPS',
|
||||||
|
'Municipio',
|
||||||
|
'Departamento',
|
||||||
|
'Autorizante',
|
||||||
|
'Establecimiento'
|
||||||
|
];
|
||||||
|
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...this.autorizaciones.map((aut) =>
|
||||||
|
[
|
||||||
|
aut.numero_autorizacion,
|
||||||
|
this.formatDateTime(aut.fecha_autorizacion),
|
||||||
|
aut.interno,
|
||||||
|
`"${aut.nombre_paciente}"`,
|
||||||
|
`"${aut.nombre_ips}"`,
|
||||||
|
`"${aut.municipio}"`,
|
||||||
|
`"${aut.departamento}"`,
|
||||||
|
`"${aut.nombre_autorizante}"`,
|
||||||
|
`"${aut.nombre_establecimiento}"`
|
||||||
|
].join(',')
|
||||||
|
)
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute(
|
||||||
|
'download',
|
||||||
|
`autorizaciones_${this.formatDateForInput(new Date())}.csv`
|
||||||
|
);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
this.successMessage = 'Archivo CSV exportado correctamente.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,287 @@
|
|||||||
|
<div class="container">
|
||||||
|
<!-- Header con logo SALUD UT -->
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<img
|
||||||
|
src="logo_SALUDUT.png"
|
||||||
|
alt="SALUD UT"
|
||||||
|
class="logo-saludut"
|
||||||
|
/>
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>{{ titulo }}</h1>
|
||||||
|
<p class="subtitle">Módulo de autorizaciones médicas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- Información del usuario -->
|
||||||
|
<div class="user-info" *ngIf="isLoggedIn()">
|
||||||
|
<span class="user-name">{{ getCurrentUser()?.nombre_completo }}</span>
|
||||||
|
<span class="user-role">{{ getCurrentUser()?.nombre_rol }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="badge-saludut">SALUD UT</span>
|
||||||
|
|
||||||
|
<!-- Botón de logout -->
|
||||||
|
<button
|
||||||
|
*ngIf="isLoggedIn()"
|
||||||
|
class="logout-btn"
|
||||||
|
(click)="logout()"
|
||||||
|
title="Cerrar sesión"
|
||||||
|
>
|
||||||
|
Salir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Tarjeta de búsqueda -->
|
||||||
|
<div class="search-card">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="tipo">Buscar por:</label>
|
||||||
|
<select id="tipo" [(ngModel)]="tipoBusqueda">
|
||||||
|
<option value="documento">Cédula / Documento</option>
|
||||||
|
<option value="interno">Número interno</option>
|
||||||
|
<option value="nombre">Nombre completo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="termino">Valor:</label>
|
||||||
|
<input
|
||||||
|
id="termino"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="termino"
|
||||||
|
placeholder="Ej: 79427056, 372, JORGE IVAN"
|
||||||
|
(keyup.enter)="buscar()"
|
||||||
|
/>
|
||||||
|
<button (click)="buscar()" [disabled]="cargando">
|
||||||
|
{{ cargando ? 'Buscando...' : 'Buscar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de cargar Excel (solo administradores) -->
|
||||||
|
<div class="upload-excel" *ngIf="puedeCargarPacientes()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="inputExcel.click()"
|
||||||
|
[disabled]="cargandoExcel"
|
||||||
|
>
|
||||||
|
{{ cargandoExcel ? 'Cargando Excel...' : 'Cargar Excel de PPL' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#inputExcel
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
(change)="onExcelSelected($event)"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="upload-msg" *ngIf="estadoCargaExcel">
|
||||||
|
{{ estadoCargaExcel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de ver autorizaciones por fecha (solo administradores) -->
|
||||||
|
<div class="admin-actions" *ngIf="puedeVerTodasAutorizaciones()">
|
||||||
|
<button
|
||||||
|
class="btn-view-auths"
|
||||||
|
(click)="irAAutorizacionesPorFecha()"
|
||||||
|
title="Ver autorizaciones por fecha"
|
||||||
|
>
|
||||||
|
📋 Ver Autorizaciones por Fecha
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="estado error" *ngIf="error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layout: tabla de pacientes -->
|
||||||
|
<div class="resultados-layout">
|
||||||
|
<div class="col-tabla">
|
||||||
|
<div *ngIf="pacientes.length > 0">
|
||||||
|
<table class="tabla">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Interno</th>
|
||||||
|
<th>Documento</th>
|
||||||
|
<th>Nombre completo</th>
|
||||||
|
<th>Sexo</th>
|
||||||
|
<th>Fecha nacimiento</th>
|
||||||
|
<th>Establecimiento</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Fecha ingreso</th>
|
||||||
|
<th>Tiempo reclusión (años)</th>
|
||||||
|
<th>Autorización</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let p of pacientes">
|
||||||
|
<td>{{ p.interno }}</td>
|
||||||
|
<td>{{ p.numero_documento }}</td>
|
||||||
|
<td>
|
||||||
|
{{ p.primer_nombre }} {{ p.segundo_nombre }}
|
||||||
|
{{ p.primer_apellido }} {{ p.segundo_apellido }}
|
||||||
|
</td>
|
||||||
|
<td>{{ p.sexo }}</td>
|
||||||
|
<td>{{ p.fecha_nacimiento | date: 'yyyy-MM-dd' }}</td>
|
||||||
|
<td>{{ p.nombre_establecimiento }}</td>
|
||||||
|
<td>{{ p.estado }}</td>
|
||||||
|
<td>{{ p.fecha_ingreso | date: 'yyyy-MM-dd' }}</td>
|
||||||
|
<td>{{ p.tiempo_reclusion }}</td>
|
||||||
|
<td>
|
||||||
|
<button (click)="seleccionarPaciente(p)" [disabled]="!puedeGenerarAutorizaciones()">
|
||||||
|
Autorizar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!cargando && !error && pacientes.length === 0">
|
||||||
|
No hay resultados para mostrar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de autorización -->
|
||||||
|
<div class="aut-modal-backdrop" *ngIf="pacienteSeleccionado">
|
||||||
|
<div class="aut-modal">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="aut-modal-close"
|
||||||
|
(click)="cerrarAutorizacion()"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2>Nueva autorización</h2>
|
||||||
|
|
||||||
|
<p class="aut-paciente">
|
||||||
|
<strong>Interno:</strong> {{ pacienteSeleccionado?.interno }}<br />
|
||||||
|
<strong>Nombre:</strong>
|
||||||
|
{{ pacienteSeleccionado?.primer_nombre }}
|
||||||
|
{{ pacienteSeleccionado?.segundo_nombre }}
|
||||||
|
{{ pacienteSeleccionado?.primer_apellido }}
|
||||||
|
{{ pacienteSeleccionado?.segundo_apellido }}<br />
|
||||||
|
<strong>Establecimiento:</strong>
|
||||||
|
{{ pacienteSeleccionado?.nombre_establecimiento }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="ips">IPS / Hospital:</label>
|
||||||
|
<select id="ips" [(ngModel)]="formAutorizacion.id_ips">
|
||||||
|
<option value="">-- Seleccione IPS --</option>
|
||||||
|
<option *ngFor="let ips of ipsDisponibles" [value]="ips.id_ips">
|
||||||
|
{{ ips.nombre_ips }} ({{ ips.municipio }} - {{ ips.departamento }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="autorizante">Quién autoriza:</label>
|
||||||
|
<select
|
||||||
|
id="autorizante"
|
||||||
|
[(ngModel)]="formAutorizacion.numero_documento_autorizante"
|
||||||
|
>
|
||||||
|
<option value="">-- Seleccione --</option>
|
||||||
|
<option
|
||||||
|
*ngFor="let a of autorizantes"
|
||||||
|
[value]="a.numero_documento"
|
||||||
|
>
|
||||||
|
{{ a.nombre }} ({{ a.cargo }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="fechaAut">Fecha autorización:</label>
|
||||||
|
<input
|
||||||
|
id="fechaAut"
|
||||||
|
type="date"
|
||||||
|
[(ngModel)]="formAutorizacion.fecha_autorizacion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="obs">Observación / servicios autorizados:</label>
|
||||||
|
<textarea
|
||||||
|
id="obs"
|
||||||
|
rows="4"
|
||||||
|
[(ngModel)]="formAutorizacion.observacion"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aut-actions">
|
||||||
|
<button
|
||||||
|
(click)="guardarAutorizacion()"
|
||||||
|
[disabled]="guardandoAutorizacion || !puedeGenerarAutorizaciones()"
|
||||||
|
>
|
||||||
|
{{ guardandoAutorizacion ? 'Guardando...' : 'Guardar autorización' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-secondary"
|
||||||
|
*ngIf="pacienteSeleccionado"
|
||||||
|
(click)="verAutorizaciones()"
|
||||||
|
>
|
||||||
|
Ver autorizaciones
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="estado ok" *ngIf="mensajeAutorizacion">
|
||||||
|
{{ mensajeAutorizacion }}
|
||||||
|
</div>
|
||||||
|
<div class="estado error" *ngIf="errorAutorizacion">
|
||||||
|
{{ errorAutorizacion }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="autorizaciones-previas"
|
||||||
|
*ngIf="pacienteSeleccionado && autorizacionesPaciente.length > 0"
|
||||||
|
>
|
||||||
|
<h3>Autorizaciones previas</h3>
|
||||||
|
<table class="tabla-autorizaciones">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>N° autorización</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>IPS</th>
|
||||||
|
<th>Autoriza</th>
|
||||||
|
<th>Ver PDF</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let a of autorizacionesPaciente">
|
||||||
|
<td>{{ a.numero_autorizacion }}</td>
|
||||||
|
<td>{{ a.fecha_autorizacion | date: 'yyyy-MM-dd' }}</td>
|
||||||
|
<td>{{ a.nombre_ips }}</td>
|
||||||
|
<td>{{ a.nombre_autorizante }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
*ngIf="puedeDescargarPdfs()"
|
||||||
|
[disabled]="descargandoPdf"
|
||||||
|
(click)="descargarPdf(a.numero_autorizacion)"
|
||||||
|
>
|
||||||
|
Ver PDF
|
||||||
|
</button>
|
||||||
|
<span *ngIf="!puedeDescargarPdfs()" class="pdf-restricted">
|
||||||
|
Solo admin
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<div class="estado" *ngIf="descargandoPdf">
|
||||||
|
Generando y descargando PDF, por favor espera...
|
||||||
|
</div>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="estado error" *ngIf="errorAutLista">
|
||||||
|
{{ errorAutLista }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,415 @@
|
|||||||
|
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { Router, NavigationEnd } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { AuthService } from '../../services/auth';
|
||||||
|
import { PacienteService } from '../../services/paciente';
|
||||||
|
import { finalize, filter } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-autorizaciones',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, RouterOutlet],
|
||||||
|
templateUrl: './autorizaciones.html',
|
||||||
|
styleUrls: ['./autorizaciones.css']
|
||||||
|
})
|
||||||
|
export class AutorizacionesComponent implements OnInit {
|
||||||
|
title = 'Consulta PPL INPEC';
|
||||||
|
titulo = 'Consulta PPL INPEC';
|
||||||
|
|
||||||
|
// ---- Búsqueda ----
|
||||||
|
tipoBusqueda: 'documento' | 'interno' | 'nombre' = 'documento';
|
||||||
|
termino = '';
|
||||||
|
|
||||||
|
pacientes: any[] = [];
|
||||||
|
cargando = false;
|
||||||
|
error: string | null = null;
|
||||||
|
|
||||||
|
// ---- Excel ----
|
||||||
|
cargandoExcel = false;
|
||||||
|
estadoCargaExcel: string | null = null;
|
||||||
|
|
||||||
|
// ---- Autorizaciones ----
|
||||||
|
pacienteSeleccionado: any = null;
|
||||||
|
ipsDisponibles: any[] = [];
|
||||||
|
autorizantes: any[] = [];
|
||||||
|
|
||||||
|
formAutorizacion = {
|
||||||
|
id_ips: '',
|
||||||
|
numero_documento_autorizante: '',
|
||||||
|
fecha_autorizacion: '',
|
||||||
|
observacion: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
guardandoAutorizacion = false;
|
||||||
|
mensajeAutorizacion: string | null = null;
|
||||||
|
errorAutorizacion: string | null = null;
|
||||||
|
|
||||||
|
autorizacionesPaciente: any[] = [];
|
||||||
|
cargandoAutorizaciones = false;
|
||||||
|
errorAutLista: string | null = null;
|
||||||
|
|
||||||
|
showMainContent = false;
|
||||||
|
esBusquedaPacientes = false;
|
||||||
|
|
||||||
|
descargandoPdf = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private pacienteService: PacienteService,
|
||||||
|
private router: Router,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.router.events
|
||||||
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
|
.subscribe((event: NavigationEnd) => {
|
||||||
|
const url = event.urlAfterRedirects || event.url;
|
||||||
|
|
||||||
|
// Solo ocultamos el layout principal en /login
|
||||||
|
this.showMainContent = url !== '/login';
|
||||||
|
|
||||||
|
// El módulo de búsqueda solo vive en la raíz "/"
|
||||||
|
this.esBusquedaPacientes = (url === '/' || url === '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Estado inicial al recargar la página
|
||||||
|
if (this.authService.isLoggedIn()) {
|
||||||
|
const url = this.router.url;
|
||||||
|
this.showMainContent = url !== '/login';
|
||||||
|
this.esBusquedaPacientes = (url === '/' || url === '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Búsqueda de pacientes
|
||||||
|
// -------------------------
|
||||||
|
buscar(): void {
|
||||||
|
this.error = null;
|
||||||
|
this.cargando = true;
|
||||||
|
this.pacientes = [];
|
||||||
|
this.pacienteSeleccionado = null;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
|
||||||
|
const valor = this.termino.trim();
|
||||||
|
if (!valor) {
|
||||||
|
this.cargando = false;
|
||||||
|
this.error = 'Ingrese un valor para buscar.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obs$;
|
||||||
|
|
||||||
|
if (this.tipoBusqueda === 'documento') {
|
||||||
|
obs$ = this.pacienteService.buscarPorDocumento(valor);
|
||||||
|
} else if (this.tipoBusqueda === 'interno') {
|
||||||
|
obs$ = this.pacienteService.buscarPorInterno(valor);
|
||||||
|
} else {
|
||||||
|
obs$ = this.pacienteService.buscarPorNombre(valor);
|
||||||
|
}
|
||||||
|
|
||||||
|
obs$ .pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cargando = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any[]) => {
|
||||||
|
this.pacientes = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.error = 'Error consultando pacientes.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 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 y procesando archivo...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('archivo', file); // 👈 debe llamarse igual que en upload.single('archivo')
|
||||||
|
|
||||||
|
this.pacienteService.cargarExcelPacientes(formData).subscribe({
|
||||||
|
next: (resp) => {
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
|
||||||
|
const partes: string[] = [];
|
||||||
|
if (resp.mensaje) partes.push(resp.mensaje);
|
||||||
|
if (resp.activos != null) partes.push(`Pacientes activos: ${resp.activos}`);
|
||||||
|
if (resp.antiguos != null) partes.push(`Pacientes antiguos: ${resp.antiguos}`);
|
||||||
|
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
partes.join(' · ') || 'Archivo procesado correctamente.';
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
err?.error?.error || 'Error procesando el Excel de pacientes.';
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Seleccionar paciente
|
||||||
|
// -------------------------
|
||||||
|
seleccionarPaciente(p: any): void {
|
||||||
|
this.pacienteSeleccionado = p;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
|
||||||
|
this.formAutorizacion = {
|
||||||
|
id_ips: '',
|
||||||
|
numero_documento_autorizante: '',
|
||||||
|
fecha_autorizacion: '',
|
||||||
|
observacion: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.autorizacionesPaciente = [];
|
||||||
|
this.errorAutLista = null;
|
||||||
|
|
||||||
|
this.cargarIps(p.interno);
|
||||||
|
this.cargarAutorizantes();
|
||||||
|
}
|
||||||
|
|
||||||
|
cerrarAutorizacion(): void {
|
||||||
|
this.pacienteSeleccionado = null;
|
||||||
|
this.autorizacionesPaciente = [];
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
this.errorAutLista = null;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private cargarIps(interno: string): void {
|
||||||
|
this.ipsDisponibles = [];
|
||||||
|
this.pacienteService
|
||||||
|
.obtenerIpsPorInterno(interno)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any[]) => {
|
||||||
|
this.ipsDisponibles = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizacion =
|
||||||
|
'No se pudieron cargar las IPS para este interno.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private cargarAutorizantes(): void {
|
||||||
|
this.autorizantes = [];
|
||||||
|
this.pacienteService
|
||||||
|
.obtenerAutorizantes()
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any[]) => {
|
||||||
|
this.autorizantes = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizacion =
|
||||||
|
'No se pudieron cargar las personas autorizantes.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Guardar autorización
|
||||||
|
// -------------------------
|
||||||
|
guardarAutorizacion(): void {
|
||||||
|
if (!this.pacienteSeleccionado) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
|
||||||
|
if (!this.formAutorizacion.id_ips) {
|
||||||
|
this.errorAutorizacion = 'Debe seleccionar una IPS.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.formAutorizacion.numero_documento_autorizante) {
|
||||||
|
this.errorAutorizacion = 'Debe seleccionar quién autoriza.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
interno: this.pacienteSeleccionado.interno,
|
||||||
|
id_ips: Number(this.formAutorizacion.id_ips),
|
||||||
|
numero_documento_autorizante: Number(
|
||||||
|
this.formAutorizacion.numero_documento_autorizante
|
||||||
|
),
|
||||||
|
observacion: this.formAutorizacion.observacion,
|
||||||
|
fecha_autorizacion:
|
||||||
|
this.formAutorizacion.fecha_autorizacion || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.guardandoAutorizacion = true;
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.crearAutorizacion(payload)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.guardandoAutorizacion = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (resp: any) => {
|
||||||
|
this.mensajeAutorizacion = `Autorización N° ${resp.numero_autorizacion} creada el ${resp.fecha_autorizacion}.`;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizacion =
|
||||||
|
err.error?.error || 'Error guardando la autorización.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Ver autorizaciones del interno
|
||||||
|
// -------------------------
|
||||||
|
verAutorizaciones(): void {
|
||||||
|
if (!this.pacienteSeleccionado) return;
|
||||||
|
|
||||||
|
this.cargandoAutorizaciones = true;
|
||||||
|
this.errorAutLista = null;
|
||||||
|
this.autorizacionesPaciente = [];
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.obtenerAutorizacionesPorInterno(this.pacienteSeleccionado.interno)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cargandoAutorizaciones = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any[]) => {
|
||||||
|
this.autorizacionesPaciente = data;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutLista = 'Error consultando autorizaciones.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Descargar PDF
|
||||||
|
// -------------------------
|
||||||
|
descargarPdf(numeroAutorizacion: string): void {
|
||||||
|
this.descargandoPdf = true;
|
||||||
|
this.errorAutLista = null; // o la variable de error que uses
|
||||||
|
|
||||||
|
this.pacienteService.descargarPdfAutorizacion(numeroAutorizacion)
|
||||||
|
.pipe(finalize(() => {
|
||||||
|
this.descargandoPdf = false;
|
||||||
|
}))
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
const blob = new Blob([data], { type: 'application/pdf' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// 👉 Forzar descarga
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `autorizacion_${numeroAutorizacion}.pdf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
// Opcional: además abrir en una pestaña nueva
|
||||||
|
// window.open(url, '_blank');
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error descargando PDF:', err);
|
||||||
|
this.errorAutLista =
|
||||||
|
err?.error?.error || 'Error descargando el PDF de la autorización.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
irAAutorizacionesPorFecha(): void {
|
||||||
|
this.router.navigate(['/autorizaciones-por-fecha']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Métodos de autenticación ----
|
||||||
|
logout(): void {
|
||||||
|
this.authService.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn(): boolean {
|
||||||
|
return this.authService.isLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser(): any {
|
||||||
|
return this.authService.getCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métodos para verificar permisos
|
||||||
|
puedeCargarPacientes(): boolean {
|
||||||
|
return this.authService.puedeCargarPacientes();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeDescargarPdfs(): boolean {
|
||||||
|
return this.authService.puedeDescargarPdfs();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeVerTodasAutorizaciones(): boolean {
|
||||||
|
return this.authService.puedeVerTodasAutorizaciones();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeGenerarAutorizaciones(): boolean {
|
||||||
|
return this.authService.puedeGenerarAutorizaciones();
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdministrador(): boolean {
|
||||||
|
return this.authService.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdministrativoSede(): boolean {
|
||||||
|
return this.authService.isAdministrativoSede();
|
||||||
|
}
|
||||||
|
|
||||||
|
tieneAccesoAEstablecimiento(codigoEstablecimiento: string): boolean {
|
||||||
|
return this.authService.tieneAccesoAEstablecimiento(codigoEstablecimiento);
|
||||||
|
}
|
||||||
|
}
|
||||||
417
saludut-inpec/src/app/components/dashboard/dashboard.css
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
/* ============================
|
||||||
|
Estilos del Componente Dashboard
|
||||||
|
============================ */
|
||||||
|
|
||||||
|
.dashboard-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-family: var(--font-main, "Inter", system-ui, -apple-system, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.dashboard-header {
|
||||||
|
background: linear-gradient(90deg, #1976d2, #1565c0);
|
||||||
|
color: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
height: 48px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.dashboard-main {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Welcome Section */
|
||||||
|
.welcome-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section h2 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-date {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-description {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
border-left: 4px solid #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-message {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.7;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Actions */
|
||||||
|
.quick-actions {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: #1976d2;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Role Info */
|
||||||
|
.role-info {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-info h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222222;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-value {
|
||||||
|
color: #666666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sedes-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sede-badge {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Permissions Summary */
|
||||||
|
.permissions-summary {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-summary h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f9fafb;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-item.has-permission {
|
||||||
|
background: #f0f9f0;
|
||||||
|
border: 1px solid #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-text {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #222222;
|
||||||
|
}
|
||||||
|
.excel-status {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #1b5e20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left,
|
||||||
|
.header-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-main {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-label {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.welcome-section h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
162
saludut-inpec/src/app/components/dashboard/dashboard.html
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<div class="dashboard-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="dashboard-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<img src="logo_SALUDUT.png" alt="SALUD UT" class="header-logo" />
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>SALUD UT</h1>
|
||||||
|
<p class="header-subtitle">Módulo de autorizaciones médicas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">{{ getNombreUsuario() }}</span>
|
||||||
|
<span class="user-role">{{ getNombreRolFormateado() }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="logout-button" (click)="logout()" title="Cerrar sesión">
|
||||||
|
Salir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Mensaje de error -->
|
||||||
|
<div class="alert alert-error" *ngIf="errorMessage">
|
||||||
|
<span class="alert-icon">⚠️</span>
|
||||||
|
<span class="alert-message">{{ errorMessage }}</span>
|
||||||
|
<button class="alert-close" (click)="cerrarMensajeError()" title="Cerrar">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="dashboard-main">
|
||||||
|
<!-- Welcome Section -->
|
||||||
|
<section class="welcome-section">
|
||||||
|
<h2>Bienvenido, {{ getNombreUsuario() }}</h2>
|
||||||
|
<p class="welcome-date">{{ getFechaActual() }}</p>
|
||||||
|
<p class="welcome-description">
|
||||||
|
Sistema de autorizaciones médicas para el INPEC
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<section class="quick-actions">
|
||||||
|
<h3>Acciones Rápidas</h3>
|
||||||
|
<div class="actions-grid">
|
||||||
|
<!-- Buscar Pacientes (disponible para todos) -->
|
||||||
|
<div class="action-card" (click)="irABusquedaPacientes()">
|
||||||
|
<div class="action-icon">🔍</div>
|
||||||
|
<h4>Buscar Pacientes</h4>
|
||||||
|
<p>Consultar información de pacientes y generar autorizaciones</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cargar Pacientes (solo administradores) -->
|
||||||
|
<div
|
||||||
|
class="action-card"
|
||||||
|
*ngIf="puedeCargarPacientes()"
|
||||||
|
(click)="abrirCargadorPacientes(inputExcelPacientes)"
|
||||||
|
>
|
||||||
|
<div class="action-icon">📊</div>
|
||||||
|
<h4>Cargar Pacientes</h4>
|
||||||
|
<p>
|
||||||
|
{{ cargandoExcel
|
||||||
|
? 'Cargando archivo de pacientes...'
|
||||||
|
: 'Subir archivo Excel con datos de pacientes' }}
|
||||||
|
</p>
|
||||||
|
<div class="admin-badge">Solo admin</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ver Autorizaciones por Fecha (solo administradores) -->
|
||||||
|
<div
|
||||||
|
class="action-card"
|
||||||
|
(click)="irAVerAutorizacionesPorFecha()"
|
||||||
|
*ngIf="puedeVerTodasAutorizaciones()"
|
||||||
|
>
|
||||||
|
<div class="action-icon">📋</div>
|
||||||
|
<h4>Autorizaciones por Fecha</h4>
|
||||||
|
<p>Consultar y descargar autorizaciones por rango de fechas</p>
|
||||||
|
<div class="admin-badge">Solo admin</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="action-card"
|
||||||
|
*ngIf="puedeGestionarUsuarios()"
|
||||||
|
(click)="irAUsuarios()"
|
||||||
|
>
|
||||||
|
<div class="action-icon">
|
||||||
|
👥
|
||||||
|
</div>
|
||||||
|
<h3>Gestionar Usuarios</h3>
|
||||||
|
<p>Crear y administrar usuarios del sistema.</p>
|
||||||
|
<span class="admin-badge">Solo admin</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input de archivo real (oculto) -->
|
||||||
|
<input
|
||||||
|
#inputExcelPacientes
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
hidden
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
(change)="onExcelSelected($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Mensaje de estado de la carga -->
|
||||||
|
<p class="excel-status" *ngIf="estadoCargaExcel">
|
||||||
|
{{ estadoCargaExcel }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- User Role Information -->
|
||||||
|
<section class="role-info" *ngIf="currentUser">
|
||||||
|
<h3>Información de Acceso</h3>
|
||||||
|
<div class="role-details">
|
||||||
|
<div class="role-item">
|
||||||
|
<span class="role-label">Rol:</span>
|
||||||
|
<span class="role-value">{{ getNombreRolFormateado() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sedes asignadas (solo para administrativos) -->
|
||||||
|
<div class="role-item" *ngIf="currentUser.nombre_rol === 'administrativo_sede' && getSedesUsuario().length > 0">
|
||||||
|
<span class="role-label">Sedes asignadas:</span>
|
||||||
|
<div class="sedes-list">
|
||||||
|
<span
|
||||||
|
class="sede-badge"
|
||||||
|
*ngFor="let sede of getSedesUsuario()"
|
||||||
|
[title]="sede.nombre_establecimiento"
|
||||||
|
>
|
||||||
|
{{ sede.nombre_establecimiento }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Permissions Summary -->
|
||||||
|
<section class="permissions-summary">
|
||||||
|
<h3>Permisos Disponibles</h3>
|
||||||
|
<div class="permissions-grid">
|
||||||
|
<div class="permission-item" [class.has-permission]="puedeGenerarAutorizaciones()">
|
||||||
|
<span class="permission-icon">{{ puedeGenerarAutorizaciones() ? '✅' : '❌' }}</span>
|
||||||
|
<span class="permission-text">Generar autorizaciones</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="permission-item" [class.has-permission]="puedeCargarPacientes()">
|
||||||
|
<span class="permission-icon">{{ puedeCargarPacientes() ? '✅' : '❌' }}</span>
|
||||||
|
<span class="permission-text">Cargar pacientes (Excel)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="permission-item" [class.has-permission]="puedeDescargarPdfs()">
|
||||||
|
<span class="permission-icon">{{ puedeDescargarPdfs() ? '✅' : '❌' }}</span>
|
||||||
|
<span class="permission-text">Descargar PDFs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="permission-item" [class.has-permission]="puedeVerTodasAutorizaciones()">
|
||||||
|
<span class="permission-icon">{{ puedeVerTodasAutorizaciones() ? '✅' : '❌' }}</span>
|
||||||
|
<span class="permission-text">Ver todas las autorizaciones</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
234
saludut-inpec/src/app/components/dashboard/dashboard.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
ChangeDetectorRef
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
import { AuthService } from '../../services/auth';
|
||||||
|
import { PacienteService } from '../../services/paciente';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './dashboard.html',
|
||||||
|
styleUrls: ['./dashboard.css']
|
||||||
|
})
|
||||||
|
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;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private pacienteService: PacienteService,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Suscribirse al usuario actual
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.authService.currentUser$.subscribe(user => {
|
||||||
|
this.currentUser = user;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verificar si hay mensajes de error en los query params
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.route.queryParamMap.subscribe(params => {
|
||||||
|
const error = params.get('error');
|
||||||
|
if (error) {
|
||||||
|
this.handleErrorMessage(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si no hay usuario autenticado, redirigir al login
|
||||||
|
if (!this.authService.isLoggedIn()) {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Limpiar suscripciones
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleErrorMessage(error: string): void {
|
||||||
|
switch (error) {
|
||||||
|
case 'no_admin':
|
||||||
|
this.errorMessage =
|
||||||
|
'No tienes permisos de administrador para acceder a esa página.';
|
||||||
|
break;
|
||||||
|
case 'no_permission':
|
||||||
|
this.errorMessage =
|
||||||
|
'No tienes los permisos necesarios para realizar esa acción.';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.errorMessage =
|
||||||
|
'Ha ocurrido un error. Por favor, intenta nuevamente.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar el mensaje después de 5 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Navegación
|
||||||
|
// =========================
|
||||||
|
irABusquedaPacientes(): void {
|
||||||
|
this.router.navigate(['/autorizaciones']);
|
||||||
|
}
|
||||||
|
|
||||||
|
irAVerAutorizacionesPorFecha(): void {
|
||||||
|
this.router.navigate(['/autorizaciones-por-fecha']);
|
||||||
|
}
|
||||||
|
|
||||||
|
irAUsuarios(): void {
|
||||||
|
this.router.navigate(['/usuarios']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ahora simplemente comprobamos si es admin
|
||||||
|
puedeGestionarUsuarios(): boolean {
|
||||||
|
return this.authService.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Logout y helpers de usuario
|
||||||
|
// =========================
|
||||||
|
logout(): void {
|
||||||
|
this.authService.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNombreUsuario(): string {
|
||||||
|
return this.currentUser?.nombre_completo || 'Usuario';
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolUsuario(): string {
|
||||||
|
return this.currentUser?.nombre_rol || 'Sin rol';
|
||||||
|
}
|
||||||
|
|
||||||
|
getNombreRolFormateado(): string {
|
||||||
|
const rol = this.getRolUsuario();
|
||||||
|
switch (rol) {
|
||||||
|
case 'administrador':
|
||||||
|
return 'Administrador';
|
||||||
|
case 'administrativo_sede':
|
||||||
|
return 'Administrativo de Sede';
|
||||||
|
default:
|
||||||
|
return rol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Permisos
|
||||||
|
// =========================
|
||||||
|
puedeCargarPacientes(): boolean {
|
||||||
|
return this.authService.puedeCargarPacientes();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeDescargarPdfs(): boolean {
|
||||||
|
return this.authService.puedeDescargarPdfs();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeVerTodasAutorizaciones(): boolean {
|
||||||
|
return this.authService.puedeVerTodasAutorizaciones();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeGenerarAutorizaciones(): boolean {
|
||||||
|
return this.authService.puedeGenerarAutorizaciones();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSedesUsuario(): any[] {
|
||||||
|
return this.currentUser?.sedes || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cerrarMensajeError(): void {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFechaActual(): string {
|
||||||
|
const hoy = new Date();
|
||||||
|
return hoy.toLocaleDateString('es-CO', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
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 y procesando archivo...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('archivo', file); // mismo nombre que en server.js
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.cargarExcelPacientes(formData)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
// Forzar refresco de la vista por si acaso
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: resp => {
|
||||||
|
if (resp?.mensaje) {
|
||||||
|
const partes: string[] = [resp.mensaje];
|
||||||
|
|
||||||
|
if (typeof resp.activos === 'number') {
|
||||||
|
partes.push(`Pacientes activos: ${resp.activos}`);
|
||||||
|
}
|
||||||
|
if (typeof resp.antiguos === 'number') {
|
||||||
|
partes.push(`Pacientes antiguos: ${resp.antiguos}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.estadoCargaExcel = partes.join(' · ');
|
||||||
|
} else {
|
||||||
|
this.estadoCargaExcel = 'Archivo procesado correctamente.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
console.error(err);
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
err?.error?.error ||
|
||||||
|
'Error procesando el Excel de pacientes.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
259
saludut-inpec/src/app/components/login/login.css
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
/* ============================
|
||||||
|
Estilos del Componente Login
|
||||||
|
============================ */
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--font-main, "Inter", system-ui, -apple-system, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
background: linear-gradient(90deg, #1976d2, #1565c0);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: 64px;
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alertas */
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form groups */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1976d2;
|
||||||
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input.error {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background-color: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Password input container */
|
||||||
|
.password-input-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input-container input {
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error messages */
|
||||||
|
.error-message {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login button */
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: linear-gradient(90deg, #1976d2, #1565c0);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 20px rgba(25, 118, 210, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.login-footer {
|
||||||
|
padding: 20px 24px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.login-card {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
background-color: #374151;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
background-color: #111827;
|
||||||
|
border-top-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
saludut-inpec/src/app/components/login/login.html
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<div class="login-container">
|
||||||
|
<div class="login-card">
|
||||||
|
<!-- Header con logo SALUD UT -->
|
||||||
|
<div class="login-header">
|
||||||
|
<img src="/logo_SALUDUT.png" alt="SALUD UT" class="login-logo" />
|
||||||
|
<h1>SALUD UT</h1>
|
||||||
|
<p class="login-subtitle">Módulo de autorizaciones médicas</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario de login -->
|
||||||
|
<form class="login-form" [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||||
|
<!-- Mensajes de error -->
|
||||||
|
<div class="alert alert-error" *ngIf="errorMessage">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Campo de usuario -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Usuario:</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
formControlName="username"
|
||||||
|
placeholder="Ingrese su usuario"
|
||||||
|
[class.error]="username?.invalid && username?.touched"
|
||||||
|
autocomplete="username"
|
||||||
|
/>
|
||||||
|
<div class="error-message" *ngIf="username?.invalid && username?.touched">
|
||||||
|
{{ getUsernameErrorMessage() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Campo de contraseña -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Contraseña:</label>
|
||||||
|
<div class="password-input-container">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
[type]="hidePassword ? 'password' : 'text'"
|
||||||
|
formControlName="password"
|
||||||
|
placeholder="Ingrese su contraseña"
|
||||||
|
[class.error]="password?.invalid && password?.touched"
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="password-toggle"
|
||||||
|
(click)="togglePasswordVisibility()"
|
||||||
|
[title]="hidePassword ? 'Mostrar contraseña' : 'Ocultar contraseña'"
|
||||||
|
>
|
||||||
|
{{ hidePassword ? '👁️' : '👁️🗨️' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" *ngIf="password?.invalid && password?.touched">
|
||||||
|
{{ getPasswordErrorMessage() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de envío -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="login-button"
|
||||||
|
[disabled]="loginForm.invalid || isLoading"
|
||||||
|
>
|
||||||
|
<span *ngIf="!isLoading">Iniciar Sesión</span>
|
||||||
|
<span *ngIf="isLoading" class="loading-spinner">
|
||||||
|
<span class="spinner"></span>
|
||||||
|
Iniciando sesión...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Información adicional -->
|
||||||
|
<div class="login-footer">
|
||||||
|
<p class="help-text">
|
||||||
|
Si tiene problemas para acceder, contacte al administrador del sistema.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
118
saludut-inpec/src/app/components/login/login.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService, LoginRequest } from '../../services/auth'; // <-- Verificar esta ruta
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ReactiveFormsModule],
|
||||||
|
templateUrl: './login.html',
|
||||||
|
styleUrls: ['./login.css']
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnInit {
|
||||||
|
loginForm: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
hidePassword = true;
|
||||||
|
returnUrl: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private authService: AuthService, // <-- Verificar esta inyección
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
this.loginForm = this.fb.group({
|
||||||
|
username: ['', [Validators.required, Validators.minLength(3)]],
|
||||||
|
password: ['', [Validators.required, Validators.minLength(6)]]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Obtener la URL de retorno si existe
|
||||||
|
this.returnUrl = this.route.snapshot.queryParamMap.get('returnUrl');
|
||||||
|
|
||||||
|
// Si ya está autenticado, redirigir al dashboard
|
||||||
|
if (this.authService.isLoggedIn()) {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.loginForm.invalid) {
|
||||||
|
this.markFormGroupTouched(this.loginForm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.errorMessage = null;
|
||||||
|
|
||||||
|
const loginData: LoginRequest = {
|
||||||
|
username: this.loginForm.value.username.trim(),
|
||||||
|
password: this.loginForm.value.password
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Datos del formulario:', loginData);
|
||||||
|
|
||||||
|
this.authService.login(loginData).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
console.log('Login exitoso:', response);
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
// Redirigir a la URL de retorno o al dashboard
|
||||||
|
if (this.returnUrl) {
|
||||||
|
this.router.navigateByUrl(this.returnUrl);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
console.error('Error en login:', error);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.errorMessage = error.error?.error || 'Error al iniciar sesión. Por favor, verifique sus credenciales.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marcar todos los controles del formulario como tocados para mostrar errores
|
||||||
|
private markFormGroupTouched(formGroup: FormGroup): void {
|
||||||
|
Object.values(formGroup.controls).forEach(control => {
|
||||||
|
control.markAsTouched();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePasswordVisibility(): void {
|
||||||
|
this.hidePassword = !this.hidePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
get username() {
|
||||||
|
return this.loginForm.get('username');
|
||||||
|
}
|
||||||
|
|
||||||
|
get password() {
|
||||||
|
return this.loginForm.get('password');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métodos para obtener mensajes de error
|
||||||
|
getUsernameErrorMessage(): string {
|
||||||
|
if (this.username?.hasError('required')) {
|
||||||
|
return 'El nombre de usuario es requerido';
|
||||||
|
}
|
||||||
|
if (this.username?.hasError('minlength')) {
|
||||||
|
return 'El nombre de usuario debe tener al menos 3 caracteres';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getPasswordErrorMessage(): string {
|
||||||
|
if (this.password?.hasError('required')) {
|
||||||
|
return 'La contraseña es requerida';
|
||||||
|
}
|
||||||
|
if (this.password?.hasError('minlength')) {
|
||||||
|
return 'La contraseña debe tener al menos 6 caracteres';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
316
saludut-inpec/src/app/components/usuarios/usuarios.css
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
/* ==== FONDO GENERAL ==== */
|
||||||
|
.usuarios-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-family: var(--font-main, "Inter", system-ui, -apple-system, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====== HEADER AZUL, IGUAL QUE EL DASHBOARD ====== */
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
background: linear-gradient(90deg, #1976d2, #1565c0);
|
||||||
|
color: white;
|
||||||
|
padding: 14px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
height: 44px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CONTENEDOR PRINCIPAL ===== */
|
||||||
|
|
||||||
|
.usuarios-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 24px auto 40px;
|
||||||
|
padding: 0 16px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Título + botón volver */
|
||||||
|
|
||||||
|
.usuarios-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usuarios-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #1976d2;
|
||||||
|
background: #fff;
|
||||||
|
color: #1976d2;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== GRID DOS COLUMNAS (FORM + TABLA) ===== */
|
||||||
|
|
||||||
|
.usuarios-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.4fr);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tarjetas */
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 18px 20px 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== FORMULARIO CREAR USUARIO ===== */
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input,
|
||||||
|
.form-row select,
|
||||||
|
.form-row textarea {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input:focus,
|
||||||
|
.form-row select:focus,
|
||||||
|
.form-row textarea:focus {
|
||||||
|
border-color: #1976d2;
|
||||||
|
box-shadow: 0 0 0 1px rgba(25, 118, 210, 0.15);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row.acciones {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
background: #1976d2;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #145ca5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mensajitos */
|
||||||
|
|
||||||
|
.msg-ok {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-error {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TABLA DE USUARIOS ===== */
|
||||||
|
|
||||||
|
.tabla-usuarios-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 460px; /* limita altura y muestra scroll si hay muchos */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo barra scroll (opcional) */
|
||||||
|
.tabla-usuarios-wrapper::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.tabla-usuarios-wrapper::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb {
|
||||||
|
background: #c0c0c0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios th,
|
||||||
|
.tabla-usuarios td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios tbody tr:nth-child(even) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-activo {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-inactivo {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.usuarios-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usuarios-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
saludut-inpec/src/app/components/usuarios/usuarios.css.txt
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
.usuarios-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 32px auto;
|
||||||
|
padding: 0 16px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usuarios-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usuarios-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #1976d2;
|
||||||
|
background: #fff;
|
||||||
|
color: #1976d2;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usuarios-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.4fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 18px 20px 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input,
|
||||||
|
.form-row select,
|
||||||
|
.form-row textarea {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input:focus,
|
||||||
|
.form-row select:focus,
|
||||||
|
.form-row textarea:focus {
|
||||||
|
border-color: #1976d2;
|
||||||
|
box-shadow: 0 0 0 1px rgba(25, 118, 210, 0.15);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row.acciones {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
background: #1976d2;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #145ca5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado.error {
|
||||||
|
color: #c62828;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado.ok {
|
||||||
|
color: #2e7d32;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios th,
|
||||||
|
.tabla-usuarios td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-usuarios th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-activo {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-inactivo {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.usuarios-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
saludut-inpec/src/app/components/usuarios/usuarios.html
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<div class="usuarios-page">
|
||||||
|
<!-- Header igual al estilo del dashboard -->
|
||||||
|
<header class="dashboard-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<img src="logo_SALUDUT.png" alt="SALUD UT" class="header-logo" />
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>SALUD UT</h1>
|
||||||
|
<p class="header-subtitle">Gestión de Usuarios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">{{ getNombreUsuario() }}</span>
|
||||||
|
<span class="user-role">{{ getNombreRolFormateado() }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="logout-button" (click)="logout()" title="Cerrar sesión">
|
||||||
|
Salir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="usuarios-container">
|
||||||
|
<header class="usuarios-header">
|
||||||
|
<div>
|
||||||
|
<h2>Gestión de Usuarios</h2>
|
||||||
|
<p class="subtitle">
|
||||||
|
Crear, activar y desactivar usuarios del sistema
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary" (click)="volverDashboard()">
|
||||||
|
← Volver al dashboard
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="usuarios-grid">
|
||||||
|
<!-- Formulario de creación -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>Crear nuevo usuario</h3>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Usuario (login):</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoUsuario.username" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Email:</label>
|
||||||
|
<input type="email" [(ngModel)]="nuevoUsuario.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Nombre completo:</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoUsuario.nombre_completo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Contraseña:</label>
|
||||||
|
<input type="password" [(ngModel)]="nuevoUsuario.password" />
|
||||||
|
<small class="hint">Mínimo 8 caracteres, con letras y números.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Rol:</label>
|
||||||
|
<select [(ngModel)]="nuevoUsuario.id_rol">
|
||||||
|
<option *ngFor="let r of roles" [value]="r.id_rol">
|
||||||
|
{{ r.nombre_rol }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row" *ngIf="nuevoUsuario.id_rol === 2">
|
||||||
|
<label>Sedes (códigos):</label>
|
||||||
|
<textarea
|
||||||
|
rows="2"
|
||||||
|
[(ngModel)]="sedesTexto"
|
||||||
|
placeholder="Ej: 113, 148, 205 (separadas por coma)"
|
||||||
|
></textarea>
|
||||||
|
<small class="hint">
|
||||||
|
Solo para rol <strong>administrativo_sede</strong>. Usa los códigos
|
||||||
|
de establecimiento, separados por coma.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row acciones">
|
||||||
|
<button (click)="crearUsuario()" [disabled]="creando">
|
||||||
|
{{ creando ? 'Creando.' : 'Crear usuario' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msg-ok" *ngIf="mensajeOk">{{ mensajeOk }}</div>
|
||||||
|
<div class="msg-error" *ngIf="mensajeError">{{ mensajeError }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de usuarios -->
|
||||||
|
<div class="card card-usuarios">
|
||||||
|
<h3>Usuarios registrados</h3>
|
||||||
|
|
||||||
|
<div class="tabla-usuarios-wrapper">
|
||||||
|
<table class="tabla-usuarios">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Usuario</th>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Rol</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Último login</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let u of usuarios">
|
||||||
|
<td>{{ u.username }}</td>
|
||||||
|
<td>{{ u.nombre_completo }}</td>
|
||||||
|
<td>{{ u.email }}</td>
|
||||||
|
<td>{{ u.nombre_rol }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
[class.badge-activo]="u.activo"
|
||||||
|
[class.badge-inactivo]="!u.activo"
|
||||||
|
>
|
||||||
|
{{ u.activo ? 'Activo' : 'Inactivo' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<!-- date pipe para quitar la Z horrible 😅 -->
|
||||||
|
<td>
|
||||||
|
{{ u.ultimo_login ? (u.ultimo_login | date:'yyyy-MM-dd HH:mm') : '—' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
*ngIf="u.activo"
|
||||||
|
class="btn-danger"
|
||||||
|
(click)="cambiarEstadoUsuario(u, false)"
|
||||||
|
>
|
||||||
|
Desactivar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!u.activo"
|
||||||
|
class="btn-secondary"
|
||||||
|
(click)="cambiarEstadoUsuario(u, true)"
|
||||||
|
>
|
||||||
|
Activar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!cargandoUsuarios && usuarios.length === 0">
|
||||||
|
No hay usuarios para mostrar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
128
saludut-inpec/src/app/components/usuarios/usuarios.html.txt
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<div class="usuarios-container">
|
||||||
|
<header class="usuarios-header">
|
||||||
|
<div>
|
||||||
|
<h1>Gestión de Usuarios</h1>
|
||||||
|
<p class="subtitle">Crear, activar y desactivar usuarios del sistema</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary" (click)="volverDashboard()">← Volver al dashboard</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="usuarios-grid">
|
||||||
|
<!-- Formulario de creación -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>Crear nuevo usuario</h2>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Usuario (login):</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoUsuario.username" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Email:</label>
|
||||||
|
<input type="email" [(ngModel)]="nuevoUsuario.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Nombre completo:</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoUsuario.nombre_completo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Contraseña:</label>
|
||||||
|
<input type="password" [(ngModel)]="nuevoUsuario.password" />
|
||||||
|
<small class="hint">Mínimo 8 caracteres, con letras y números.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Rol:</label>
|
||||||
|
<select [(ngModel)]="nuevoUsuario.id_rol">
|
||||||
|
<option *ngFor="let r of roles" [value]="r.id_rol">
|
||||||
|
{{ r.nombre_rol }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row" *ngIf="nuevoUsuario.id_rol === 2">
|
||||||
|
<label>Sedes (códigos):</label>
|
||||||
|
<textarea
|
||||||
|
rows="2"
|
||||||
|
[(ngModel)]="sedesTexto"
|
||||||
|
placeholder="Ej: 113, 148, 205 (separadas por coma)"
|
||||||
|
></textarea>
|
||||||
|
<small class="hint">
|
||||||
|
Solo para rol <strong>administrativo_sede</strong>. Usa los códigos de
|
||||||
|
establecimiento, separados por coma.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row acciones">
|
||||||
|
<button (click)="crearUsuario()" [disabled]="creando">
|
||||||
|
{{ creando ? 'Creando...' : 'Crear usuario' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msg-ok" *ngIf="mensajeOk">{{ mensajeOk }}</div>
|
||||||
|
<div class="msg-error" *ngIf="mensajeError">{{ mensajeError }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de usuarios -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>Usuarios registrados</h2>
|
||||||
|
|
||||||
|
<div class="estado" *ngIf="cargandoUsuarios">Cargando usuarios...</div>
|
||||||
|
<div class="estado error" *ngIf="errorUsuarios">{{ errorUsuarios }}</div>
|
||||||
|
|
||||||
|
<table class="tabla-usuarios" *ngIf="!cargandoUsuarios && usuarios.length">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Usuario</th>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Rol</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Último login</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let u of usuarios">
|
||||||
|
<td>{{ u.username }}</td>
|
||||||
|
<td>{{ u.nombre_completo }}</td>
|
||||||
|
<td>{{ u.email }}</td>
|
||||||
|
<td>{{ u.nombre_rol }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
[class.badge-activo]="u.activo"
|
||||||
|
[class.badge-inactivo]="!u.activo"
|
||||||
|
>
|
||||||
|
{{ u.activo ? 'Activo' : 'Inactivo' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ u.ultimo_login || '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
*ngIf="u.activo"
|
||||||
|
class="btn-danger"
|
||||||
|
(click)="cambiarEstadoUsuario(u, false)"
|
||||||
|
>
|
||||||
|
Desactivar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!u.activo"
|
||||||
|
class="btn-secondary"
|
||||||
|
(click)="cambiarEstadoUsuario(u, true)"
|
||||||
|
>
|
||||||
|
Activar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div *ngIf="!cargandoUsuarios && usuarios.length === 0">
|
||||||
|
No hay usuarios para mostrar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
190
saludut-inpec/src/app/components/usuarios/usuarios.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
AuthService,
|
||||||
|
RegisterRequest,
|
||||||
|
Rol
|
||||||
|
} from '../../services/auth';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-usuarios',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
templateUrl: './usuarios.html',
|
||||||
|
styleUrls: ['./usuarios.css'],
|
||||||
|
})
|
||||||
|
export class UsuariosComponent implements OnInit {
|
||||||
|
|
||||||
|
usuarios: any[] = [];
|
||||||
|
roles: Rol[] = [];
|
||||||
|
|
||||||
|
cargandoUsuarios = false;
|
||||||
|
errorUsuarios: string | null = null;
|
||||||
|
|
||||||
|
creando = false;
|
||||||
|
mensajeOk: string | null = null;
|
||||||
|
mensajeError: string | null = null;
|
||||||
|
|
||||||
|
// para administrativos, códigos separados por coma
|
||||||
|
sedesTexto = '';
|
||||||
|
|
||||||
|
nuevoUsuario: RegisterRequest = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
nombre_completo: '',
|
||||||
|
id_rol: 2, // por defecto administrativo_sede
|
||||||
|
sedes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// si no es admin, lo saco
|
||||||
|
if (!this.authService.isAdministrador()) {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cargarRoles();
|
||||||
|
this.cargarUsuarios();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= HEADER =============
|
||||||
|
|
||||||
|
getNombreUsuario(): string {
|
||||||
|
const user = this.authService.getCurrentUser();
|
||||||
|
return user?.nombre_completo || 'Usuario';
|
||||||
|
}
|
||||||
|
|
||||||
|
getNombreRolFormateado(): string {
|
||||||
|
const user = this.authService.getCurrentUser();
|
||||||
|
const rol = user?.nombre_rol || '';
|
||||||
|
|
||||||
|
switch (rol) {
|
||||||
|
case 'administrador':
|
||||||
|
return 'Administrador';
|
||||||
|
case 'administrativo_sede':
|
||||||
|
return 'Administrativo de Sede';
|
||||||
|
default:
|
||||||
|
return rol || 'Sin rol';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.authService.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
volverDashboard(): void {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= CARGA DE ROLES / USUARIOS =============
|
||||||
|
|
||||||
|
cargarRoles(): void {
|
||||||
|
this.authService.getRoles().subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.roles = roles;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarUsuarios(): void {
|
||||||
|
this.cargandoUsuarios = true;
|
||||||
|
this.errorUsuarios = null;
|
||||||
|
|
||||||
|
this.authService.getUsuarios().subscribe({
|
||||||
|
next: (usuarios) => {
|
||||||
|
this.usuarios = usuarios;
|
||||||
|
this.cargandoUsuarios = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorUsuarios = 'Error cargando usuarios.';
|
||||||
|
this.cargandoUsuarios = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= CREAR USUARIO =============
|
||||||
|
|
||||||
|
crearUsuario(): void {
|
||||||
|
this.mensajeOk = null;
|
||||||
|
this.mensajeError = null;
|
||||||
|
|
||||||
|
if (!this.nuevoUsuario.username ||
|
||||||
|
!this.nuevoUsuario.email ||
|
||||||
|
!this.nuevoUsuario.password ||
|
||||||
|
!this.nuevoUsuario.nombre_completo) {
|
||||||
|
this.mensajeError = 'Completa todos los campos obligatorios.';
|
||||||
|
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.creando = true;
|
||||||
|
|
||||||
|
this.authService.register(this.nuevoUsuario).subscribe({
|
||||||
|
next: (resp: any) => {
|
||||||
|
this.mensajeOk = resp?.mensaje || 'Usuario creado correctamente.';
|
||||||
|
this.creando = false;
|
||||||
|
|
||||||
|
// Limpiar formulario
|
||||||
|
this.nuevoUsuario = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
nombre_completo: '',
|
||||||
|
id_rol: 2,
|
||||||
|
sedes: []
|
||||||
|
};
|
||||||
|
this.sedesTexto = '';
|
||||||
|
|
||||||
|
// 🔄 Recargar lista SIN refrescar la página
|
||||||
|
this.cargarUsuarios();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.mensajeError = err?.error?.error || 'Error creando el usuario.';
|
||||||
|
this.creando = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= ACTIVAR / DESACTIVAR =============
|
||||||
|
|
||||||
|
cambiarEstadoUsuario(u: any, activo: boolean): void {
|
||||||
|
if (!confirm(`¿Seguro que deseas ${activo ? 'activar' : 'desactivar'} al usuario "${u.username}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authService.actualizarEstadoUsuario(u.id_usuario, activo).subscribe({
|
||||||
|
next: () => {
|
||||||
|
// Actualizamos en memoria para que se vea de una
|
||||||
|
u.activo = activo;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert('Error actualizando estado del usuario.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
157
saludut-inpec/src/app/components/usuarios/usuarios.ts.txt
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
AuthService,
|
||||||
|
RegisterRequest,
|
||||||
|
Rol
|
||||||
|
} from '../../services/auth';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-usuarios',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
templateUrl: './usuarios.html',
|
||||||
|
styleUrls: ['./usuarios.css'],
|
||||||
|
})
|
||||||
|
export class UsuariosComponent implements OnInit {
|
||||||
|
|
||||||
|
usuarios: any[] = [];
|
||||||
|
roles: Rol[] = [];
|
||||||
|
|
||||||
|
cargandoUsuarios = false;
|
||||||
|
errorUsuarios: string | null = null;
|
||||||
|
|
||||||
|
creando = false;
|
||||||
|
mensaje: string | null = null;
|
||||||
|
errorCrear: string | null = null;
|
||||||
|
|
||||||
|
sedesTexto = ''; // para administrativos, codigos separados por coma
|
||||||
|
|
||||||
|
nuevoUsuario: RegisterRequest = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
nombre_completo: '',
|
||||||
|
id_rol: 2, // por defecto administrativo_sede
|
||||||
|
sedes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// si no es admin, lo saco
|
||||||
|
if (!this.authService.isAdministrador()) {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cargarRoles();
|
||||||
|
this.cargarUsuarios();
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarRoles(): void {
|
||||||
|
this.authService.getRoles().subscribe({
|
||||||
|
next: (roles) => {
|
||||||
|
this.roles = roles;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarUsuarios(): void {
|
||||||
|
this.cargandoUsuarios = true;
|
||||||
|
this.errorUsuarios = null;
|
||||||
|
|
||||||
|
this.authService.getUsuarios().subscribe({
|
||||||
|
next: (usuarios) => {
|
||||||
|
this.usuarios = usuarios;
|
||||||
|
this.cargandoUsuarios = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorUsuarios = 'Error cargando usuarios.';
|
||||||
|
this.cargandoUsuarios = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
crearUsuario(): void {
|
||||||
|
this.errorCrear = null;
|
||||||
|
this.mensaje = null;
|
||||||
|
|
||||||
|
if (!this.nuevoUsuario.username ||
|
||||||
|
!this.nuevoUsuario.email ||
|
||||||
|
!this.nuevoUsuario.password ||
|
||||||
|
!this.nuevoUsuario.nombre_completo) {
|
||||||
|
this.errorCrear = 'Completa todos los campos obligatorios.';
|
||||||
|
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.creando = true;
|
||||||
|
|
||||||
|
this.authService.register(this.nuevoUsuario).subscribe({
|
||||||
|
next: (resp) => {
|
||||||
|
this.mensaje = resp?.mensaje || 'Usuario creado correctamente.';
|
||||||
|
this.creando = false;
|
||||||
|
|
||||||
|
// Limpiar formulario
|
||||||
|
this.nuevoUsuario = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
nombre_completo: '',
|
||||||
|
id_rol: 2,
|
||||||
|
sedes: []
|
||||||
|
};
|
||||||
|
this.sedesTexto = '';
|
||||||
|
|
||||||
|
// Recargar lista
|
||||||
|
this.cargarUsuarios();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorCrear = err?.error?.error || 'Error creando el usuario.';
|
||||||
|
this.creando = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cambiarEstadoUsuario(u: any, activo: boolean): void {
|
||||||
|
if (!confirm(`¿Seguro que deseas ${activo ? 'activar' : 'desactivar'} al usuario "${u.username}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authService.actualizarEstadoUsuario(u.id_usuario, activo).subscribe({
|
||||||
|
next: () => {
|
||||||
|
u.activo = activo;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert('Error actualizando estado del usuario.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
volverDashboard(): void {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
saludut-inpec/src/app/guards/auth-guard.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||||
|
import { Observable, map, take } from 'rxjs';
|
||||||
|
import { AuthService } from '../services/auth';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.authService.isAuthenticated$.pipe(
|
||||||
|
take(1),
|
||||||
|
map(isAuthenticated => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// No está autenticado, redirigir al login
|
||||||
|
this.router.navigate(['/login'], {
|
||||||
|
queryParams: { returnUrl: state.url }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AdminGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.authService.currentUser$.pipe(
|
||||||
|
take(1),
|
||||||
|
map(user => {
|
||||||
|
if (user && user.nombre_rol === 'administrador') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// No es administrador, redirigir al dashboard con mensaje de error
|
||||||
|
this.router.navigate(['/dashboard'], {
|
||||||
|
queryParams: { error: 'no_admin' }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
saludut-inpec/src/app/guards/auth-guard.ts.txt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||||
|
import { Observable, map, take } from 'rxjs';
|
||||||
|
import { AuthService } from '../services/auth';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.authService.isAuthenticated$.pipe(
|
||||||
|
take(1),
|
||||||
|
map(isAuthenticated => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// No está autenticado, redirigir al login
|
||||||
|
this.router.navigate(['/login'], {
|
||||||
|
queryParams: { returnUrl: state.url }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AdminGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.authService.currentUser$.pipe(
|
||||||
|
take(1),
|
||||||
|
map(user => {
|
||||||
|
if (user && user.nombre_rol === 'administrador') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// No es administrador, redirigir al dashboard con mensaje de error
|
||||||
|
this.router.navigate(['/dashboard'], {
|
||||||
|
queryParams: { error: 'no_admin' }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
saludut-inpec/src/app/services/auth.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Auth } from './auth';
|
||||||
|
|
||||||
|
describe('Auth', () => {
|
||||||
|
let service: Auth;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(Auth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
255
saludut-inpec/src/app/services/auth.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable, BehaviorSubject, tap, catchError, throwError } from 'rxjs';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id_usuario: number;
|
||||||
|
username: string;
|
||||||
|
nombre_completo: string;
|
||||||
|
nombre_rol: string;
|
||||||
|
sedes?: Array<{
|
||||||
|
codigo_establecimiento: string;
|
||||||
|
nombre_establecimiento: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
usuario: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
nombre_completo: string;
|
||||||
|
id_rol: number;
|
||||||
|
sedes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interfaz Rol para la pantalla de usuarios */
|
||||||
|
export interface Rol {
|
||||||
|
id_rol: number;
|
||||||
|
nombre_rol: string;
|
||||||
|
descripcion?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private readonly API_URL = 'http://localhost:3000';
|
||||||
|
private readonly TOKEN_KEY = 'auth_token';
|
||||||
|
private readonly USER_KEY = 'current_user';
|
||||||
|
|
||||||
|
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||||
|
public currentUser$ = this.currentUserSubject.asObservable();
|
||||||
|
|
||||||
|
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.initializeAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeAuth(): void {
|
||||||
|
const token = this.getToken();
|
||||||
|
const user = this.getUserFromStorage();
|
||||||
|
|
||||||
|
if (token && user) {
|
||||||
|
this.currentUserSubject.next(user);
|
||||||
|
this.isAuthenticatedSubject.next(true);
|
||||||
|
} else {
|
||||||
|
this.clearAuth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== LOGIN ===========
|
||||||
|
|
||||||
|
login(credentials: LoginRequest): Observable<LoginResponse> {
|
||||||
|
return this.http.post<LoginResponse>(`${this.API_URL}/api/auth/login`, credentials).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.setAuth(response.token, response.usuario);
|
||||||
|
}),
|
||||||
|
catchError((error: any) => {
|
||||||
|
console.error('Error en login:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== REGISTER (SOLO ADMIN) ===========
|
||||||
|
|
||||||
|
register(userData: RegisterRequest): Observable<any> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.post(`${this.API_URL}/api/auth/register`, userData, { headers }).pipe(
|
||||||
|
catchError((error: any) => {
|
||||||
|
console.error('Error en register:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== LOGOUT ===========
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.clearAuth();
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== VERIFY TOKEN ===========
|
||||||
|
|
||||||
|
verifyToken(): Observable<{ usuario: User }> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.get<{ usuario: User }>(`${this.API_URL}/api/auth/verify`, { headers }).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.updateUser(response.usuario);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== HELPERS DE AUTH ===========
|
||||||
|
|
||||||
|
getToken(): string | null {
|
||||||
|
return localStorage.getItem(this.TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAuth(token: string, user: User): void {
|
||||||
|
localStorage.setItem(this.TOKEN_KEY, token);
|
||||||
|
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
|
||||||
|
this.currentUserSubject.next(user);
|
||||||
|
this.isAuthenticatedSubject.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearAuth(): void {
|
||||||
|
localStorage.removeItem(this.TOKEN_KEY);
|
||||||
|
localStorage.removeItem(this.USER_KEY);
|
||||||
|
this.currentUserSubject.next(null);
|
||||||
|
this.isAuthenticatedSubject.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateUser(user: User): void {
|
||||||
|
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
|
||||||
|
this.currentUserSubject.next(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserFromStorage(): User | null {
|
||||||
|
const userStr = localStorage.getItem(this.USER_KEY);
|
||||||
|
return userStr ? JSON.parse(userStr) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthHeaders(): HttpHeaders {
|
||||||
|
const token = this.getToken();
|
||||||
|
return new HttpHeaders({
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser(): User | null {
|
||||||
|
return this.currentUserSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn(): boolean {
|
||||||
|
return this.isAuthenticatedSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRole(role: string): boolean {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
return user ? user.nombre_rol === role : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdministrador(): boolean {
|
||||||
|
return this.hasRole('administrador');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdministrativoSede(): boolean {
|
||||||
|
return this.hasRole('administrativo_sede');
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeCargarPacientes(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeDescargarPdfs(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeVerTodasAutorizaciones(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeGenerarAutorizaciones(): boolean {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
return !!user && (user.nombre_rol === 'administrador' || user.nombre_rol === 'administrativo_sede');
|
||||||
|
}
|
||||||
|
|
||||||
|
tieneAccesoAEstablecimiento(codigoEstablecimiento: string): boolean {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
if (!user) return false;
|
||||||
|
|
||||||
|
if (user.nombre_rol === 'administrador') return true;
|
||||||
|
|
||||||
|
if (user.nombre_rol === 'administrativo_sede' && user.sedes) {
|
||||||
|
return user.sedes.some(sede => sede.codigo_establecimiento === codigoEstablecimiento);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== ADMIN: USUARIOS Y REPORTES ===========
|
||||||
|
|
||||||
|
getUsuarios(): Observable<any[]> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.get<any[]>(`${this.API_URL}/api/usuarios`, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
cambiarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.patch(
|
||||||
|
`${this.API_URL}/api/usuarios/${idUsuario}/estado`,
|
||||||
|
{ activo },
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias usado en usuarios.ts
|
||||||
|
actualizarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
|
||||||
|
return this.cambiarEstadoUsuario(idUsuario, activo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener autorizaciones por fecha (solo administradores)
|
||||||
|
getAutorizacionesPorFecha(fechaInicio: string, fechaFin: string): Observable<any[]> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin };
|
||||||
|
return this.http.get<any[]>(`${this.API_URL}/api/autorizaciones-por-fecha`, { headers, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descargar ZIP con los PDFs de un rango de fechas
|
||||||
|
descargarAutorizacionesZip(fechaInicio: string, fechaFin: string): Observable<Blob> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin };
|
||||||
|
|
||||||
|
// 👇 Aquí estaba el problema: ahora devolvemos Observable<Blob> correctamente
|
||||||
|
return this.http.get<Blob>(`${this.API_URL}/api/autorizaciones-por-fecha/zip`, {
|
||||||
|
headers,
|
||||||
|
params,
|
||||||
|
responseType: 'blob' as 'json'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener roles desde el backend
|
||||||
|
getRoles(): Observable<Rol[]> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.get<Rol[]>(`${this.API_URL}/api/roles`, { headers });
|
||||||
|
}
|
||||||
|
}
|
||||||
250
saludut-inpec/src/app/services/auth.ts.txt
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable, BehaviorSubject, tap, catchError, throwError } from 'rxjs'; // <-- AGREGAR catchError y throwError
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id_usuario: number;
|
||||||
|
username: string;
|
||||||
|
nombre_completo: string;
|
||||||
|
nombre_rol: string;
|
||||||
|
sedes?: Array<{
|
||||||
|
codigo_establecimiento: string;
|
||||||
|
nombre_establecimiento: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
usuario: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
nombre_completo: string;
|
||||||
|
id_rol: number;
|
||||||
|
sedes?: string[]; // Array de códigos de establecimiento
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rol {
|
||||||
|
id_rol: number;
|
||||||
|
nombre_rol: string;
|
||||||
|
descripcion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private readonly API_URL = 'http://localhost:3000';
|
||||||
|
private readonly TOKEN_KEY = 'auth_token';
|
||||||
|
private readonly USER_KEY = 'current_user';
|
||||||
|
|
||||||
|
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||||
|
public currentUser$ = this.currentUserSubject.asObservable();
|
||||||
|
|
||||||
|
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.initializeAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeAuth(): void {
|
||||||
|
const token = this.getToken();
|
||||||
|
const user = this.getUserFromStorage();
|
||||||
|
|
||||||
|
if (token && user) {
|
||||||
|
this.currentUserSubject.next(user);
|
||||||
|
this.isAuthenticatedSubject.next(true);
|
||||||
|
} else {
|
||||||
|
this.clearAuth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login(credentials: LoginRequest): Observable<LoginResponse> {
|
||||||
|
console.log('Enviando login:', credentials); // Log para depuración
|
||||||
|
|
||||||
|
return this.http.post<LoginResponse>(`${this.API_URL}/api/auth/login`, credentials).pipe(
|
||||||
|
tap(response => {
|
||||||
|
console.log('Respuesta del servidor:', response); // Log para depuración
|
||||||
|
this.setAuth(response.token, response.usuario);
|
||||||
|
}),
|
||||||
|
catchError((error: any) => { // <-- Agregar tipo any al error
|
||||||
|
console.error('Error en login:', error); // Log para depuración
|
||||||
|
return throwError(() => error); // <-- Usar throwError correctamente
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
register(userData: RegisterRequest): Observable<any> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.post(`${this.API_URL}/api/auth/register`, userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.http.post(`${this.API_URL}/api/auth/logout`, {}).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.clearAuth();
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
// Even if API call fails, clear local auth
|
||||||
|
this.clearAuth();
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyToken(): Observable<{ usuario: User }> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.get<{ usuario: User }>(`${this.API_URL}/api/auth/verify`, { headers }).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.updateUser(response.usuario);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): string | null {
|
||||||
|
return localStorage.getItem(this.TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAuth(token: string, user: User): void {
|
||||||
|
localStorage.setItem(this.TOKEN_KEY, token);
|
||||||
|
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
|
||||||
|
this.currentUserSubject.next(user);
|
||||||
|
this.isAuthenticatedSubject.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearAuth(): void {
|
||||||
|
localStorage.removeItem(this.TOKEN_KEY);
|
||||||
|
localStorage.removeItem(this.USER_KEY);
|
||||||
|
this.currentUserSubject.next(null);
|
||||||
|
this.isAuthenticatedSubject.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateUser(user: User): void {
|
||||||
|
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
|
||||||
|
this.currentUserSubject.next(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserFromStorage(): User | null {
|
||||||
|
const userStr = localStorage.getItem(this.USER_KEY);
|
||||||
|
return userStr ? JSON.parse(userStr) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener lista de roles (solo admin)
|
||||||
|
getRoles(): Observable<Rol[]> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.get<Rol[]>(`${this.API_URL}/api/roles`, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activar / desactivar usuario (solo admin)
|
||||||
|
actualizarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
|
||||||
|
const headers = this.getAuthHeaders().set('Content-Type', 'application/json');
|
||||||
|
return this.http.patch(
|
||||||
|
`${this.API_URL}/api/usuarios/${idUsuario}/estado`,
|
||||||
|
{ activo },
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Por comodidad, un helper
|
||||||
|
puedeGestionarUsuarios(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getAuthHeaders(): HttpHeaders {
|
||||||
|
const token = this.getToken();
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
|
||||||
|
// ❌ NO ponemos Content-Type aquí.
|
||||||
|
// Angular se encarga:
|
||||||
|
// - Si el body es un objeto → application/json
|
||||||
|
// - Si el body es FormData → multipart/form-data con boundary
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers = headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getCurrentUser(): User | null {
|
||||||
|
return this.currentUserSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn(): boolean {
|
||||||
|
return this.isAuthenticatedSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRole(role: string): boolean {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
return user ? user.nombre_rol === role : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdministrador(): boolean {
|
||||||
|
return this.hasRole('administrador');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdministrativoSede(): boolean {
|
||||||
|
return this.hasRole('administrativo_sede');
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeCargarPacientes(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeDescargarPdfs(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeVerTodasAutorizaciones(): boolean {
|
||||||
|
return this.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
|
puedeGenerarAutorizaciones(): boolean {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
return user ? (user.nombre_rol === 'administrador' || user.nombre_rol === 'administrativo_sede') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tieneAccesoAEstablecimiento(codigoEstablecimiento: string): boolean {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
if (!user) return false;
|
||||||
|
|
||||||
|
// Los administradores tienen acceso a todos los establecimientos
|
||||||
|
if (user.nombre_rol === 'administrador') return true;
|
||||||
|
|
||||||
|
// Los administrativos solo tienen acceso a sus sedes asignadas
|
||||||
|
if (user.nombre_rol === 'administrativo_sede' && user.sedes) {
|
||||||
|
return user.sedes.some(sede => sede.codigo_establecimiento === codigoEstablecimiento);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener lista de usuarios (solo administradores)
|
||||||
|
getUsuarios(): Observable<any[]> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.get<any[]>(`${this.API_URL}/api/usuarios`, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener autorizaciones por fecha (solo administradores)
|
||||||
|
getAutorizacionesPorFecha(fechaInicio: string, fechaFin: string): Observable<any[]> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin };
|
||||||
|
return this.http.get<any[]>(`${this.API_URL}/api/autorizaciones-por-fecha`, { headers, params });
|
||||||
|
}
|
||||||
|
}
|
||||||
16
saludut-inpec/src/app/services/paciente.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Paciente } from './paciente';
|
||||||
|
|
||||||
|
describe('Paciente', () => {
|
||||||
|
let service: Paciente;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(Paciente);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
183
saludut-inpec/src/app/services/paciente.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AuthService } from './auth';
|
||||||
|
|
||||||
|
|
||||||
|
// ====== Interfaces ======
|
||||||
|
|
||||||
|
export interface Paciente {
|
||||||
|
interno: string;
|
||||||
|
tipo_documento: string;
|
||||||
|
numero_documento: string;
|
||||||
|
primer_apellido: string;
|
||||||
|
segundo_apellido: string | null;
|
||||||
|
primer_nombre: string;
|
||||||
|
segundo_nombre: string | null;
|
||||||
|
fecha_nacimiento: string; // viene como texto ISO del backend
|
||||||
|
edad: number;
|
||||||
|
sexo: string;
|
||||||
|
codigo_establecimiento?: string | null;
|
||||||
|
nombre_establecimiento?: string | null;
|
||||||
|
estado?: string | null;
|
||||||
|
fecha_ingreso?: string | null;
|
||||||
|
tiempo_reclusion?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ips {
|
||||||
|
id_ips: number;
|
||||||
|
nombre_ips: string;
|
||||||
|
direccion?: string;
|
||||||
|
telefono?: string;
|
||||||
|
departamento: string;
|
||||||
|
municipio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Autorizante {
|
||||||
|
numero_documento: number;
|
||||||
|
tipo_documento: string;
|
||||||
|
nombre: string;
|
||||||
|
telefono?: string;
|
||||||
|
cargo?: string;
|
||||||
|
activo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrearAutorizacionPayload {
|
||||||
|
interno: string;
|
||||||
|
id_ips: number;
|
||||||
|
numero_documento_autorizante: number;
|
||||||
|
observacion?: string;
|
||||||
|
fecha_autorizacion?: string; // yyyy-MM-dd
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespuestaAutorizacion {
|
||||||
|
numero_autorizacion: string;
|
||||||
|
fecha_autorizacion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutorizacionListado {
|
||||||
|
numero_autorizacion: string;
|
||||||
|
fecha_autorizacion: string;
|
||||||
|
observacion: string | null;
|
||||||
|
nombre_ips: string;
|
||||||
|
municipio: string;
|
||||||
|
departamento: string;
|
||||||
|
nombre_autorizante: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespuestaCargaExcel {
|
||||||
|
ok: boolean;
|
||||||
|
mensaje: string;
|
||||||
|
activos: number | null;
|
||||||
|
antiguos: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Servicio ======
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PacienteService {
|
||||||
|
private readonly API_URL = 'http://localhost:3000';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private authService: AuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private getAuthHeaders(): HttpHeaders {
|
||||||
|
// Este método probablemente devuelve un header con 'Content-Type': 'application/json'
|
||||||
|
return this.authService.getAuthHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Búsquedas de pacientes ----
|
||||||
|
|
||||||
|
buscarPorDocumento(numero_documento: string): Observable<Paciente[]> {
|
||||||
|
const params = new HttpParams().set('numero_documento', numero_documento);
|
||||||
|
return this.http.get<Paciente[]>(`${this.API_URL}/api/pacientes`, {
|
||||||
|
params,
|
||||||
|
headers: this.getAuthHeaders()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarPorInterno(interno: string): Observable<Paciente[]> {
|
||||||
|
const params = new HttpParams().set('interno', interno);
|
||||||
|
return this.http.get<Paciente[]>(`${this.API_URL}/api/pacientes`, {
|
||||||
|
params,
|
||||||
|
headers: this.getAuthHeaders()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarPorNombre(nombre: string): Observable<Paciente[]> {
|
||||||
|
const params = new HttpParams().set('nombre', nombre);
|
||||||
|
return this.http.get<Paciente[]>(`${this.API_URL}/api/pacientes`, {
|
||||||
|
params,
|
||||||
|
headers: this.getAuthHeaders()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- IPS y autorizantes ----
|
||||||
|
|
||||||
|
obtenerIpsPorInterno(interno: string): Observable<Ips[]> {
|
||||||
|
const params = new HttpParams().set('interno', interno);
|
||||||
|
return this.http.get<Ips[]>(`${this.API_URL}/api/ips-por-interno`, {
|
||||||
|
params,
|
||||||
|
headers: this.getAuthHeaders()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
obtenerAutorizantes(): Observable<Autorizante[]> {
|
||||||
|
return this.http.get<Autorizante[]>(`${this.API_URL}/api/autorizantes`, {
|
||||||
|
headers: this.getAuthHeaders()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Autorizaciones ----
|
||||||
|
|
||||||
|
crearAutorizacion(payload: CrearAutorizacionPayload): Observable<RespuestaAutorizacion> {
|
||||||
|
return this.http.post<RespuestaAutorizacion>(
|
||||||
|
`${this.API_URL}/api/autorizaciones`,
|
||||||
|
payload,
|
||||||
|
{ headers: this.getAuthHeaders() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
obtenerAutorizacionesPorInterno(interno: string): Observable<AutorizacionListado[]> {
|
||||||
|
const params = new HttpParams().set('interno', interno);
|
||||||
|
return this.http.get<AutorizacionListado[]>(
|
||||||
|
`${this.API_URL}/api/autorizaciones`,
|
||||||
|
{ params, headers: this.getAuthHeaders() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Cargar Excel (Python + SQL) ----
|
||||||
|
cargarExcelPacientes(formData: FormData): Observable<RespuestaCargaExcel> {
|
||||||
|
// 1. Obtenemos el token desde el AuthService.
|
||||||
|
// Asumimos que tu AuthService tiene un método para obtener el token.
|
||||||
|
const token = this.authService.getToken();
|
||||||
|
|
||||||
|
// 2. Creamos un objeto de headers nuevo y solo añadimos el de autorización.
|
||||||
|
// NO añadimos 'Content-Type' para que Angular lo gestione automáticamente.
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Hacemos la petición con estos headers limpios.
|
||||||
|
return this.http.post<RespuestaCargaExcel>(
|
||||||
|
`${this.API_URL}/api/cargar-excel-pacientes`,
|
||||||
|
formData,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Descargar PDF de autorización ----
|
||||||
|
descargarPdfAutorizacion(numeroAutorizacion: string) {
|
||||||
|
const params = new HttpParams().set('numero_autorizacion', numeroAutorizacion);
|
||||||
|
|
||||||
|
return this.http.get(`${this.API_URL}/api/generar-pdf-autorizacion`, {
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
params,
|
||||||
|
responseType: 'blob' as 'json' // <- importante para TypeScript
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
134
saludut-inpec/src/app/services/paciente.ts.txt
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface Paciente {
|
||||||
|
interno: string;
|
||||||
|
tipo_documento: string | null;
|
||||||
|
numero_documento: string | null;
|
||||||
|
primer_apellido: string | null;
|
||||||
|
segundo_apellido: string | null;
|
||||||
|
primer_nombre: string | null;
|
||||||
|
segundo_nombre: string | null;
|
||||||
|
fecha_nacimiento: string | null;
|
||||||
|
edad: number | null;
|
||||||
|
sexo: string | null;
|
||||||
|
|
||||||
|
// Campos que vienen del JOIN con ingreso + establecimiento
|
||||||
|
nombre_establecimiento?: string | null;
|
||||||
|
estado?: string | null;
|
||||||
|
fecha_ingreso?: string | null;
|
||||||
|
tiempo_reclusion?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ips {
|
||||||
|
id_ips: number;
|
||||||
|
nombre_ips: string;
|
||||||
|
nit?: string;
|
||||||
|
direccion?: string;
|
||||||
|
municipio?: string;
|
||||||
|
departamento?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Autorizante {
|
||||||
|
numero_documento: number;
|
||||||
|
tipo_documento: string;
|
||||||
|
nombre: string;
|
||||||
|
telefono?: string;
|
||||||
|
cargo?: string;
|
||||||
|
activo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespuestaAutorizacion {
|
||||||
|
numero_autorizacion: string;
|
||||||
|
fecha_autorizacion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutorizacionListado {
|
||||||
|
numero_autorizacion: string;
|
||||||
|
fecha_autorizacion: string;
|
||||||
|
nombre_ips: string;
|
||||||
|
nombre_autorizante: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class PacienteService {
|
||||||
|
// Ajusta si usas otra URL base
|
||||||
|
private readonly baseUrl = 'http://localhost:3000';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
// ---- Búsquedas de pacientes ----
|
||||||
|
buscarPorDocumento(numero_documento: string): Observable<Paciente[]> {
|
||||||
|
const params = new HttpParams().set('numero_documento', numero_documento);
|
||||||
|
return this.http.get<Paciente[]>(`${this.baseUrl}/api/pacientes`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarPorInterno(interno: string): Observable<Paciente[]> {
|
||||||
|
const params = new HttpParams().set('interno', interno);
|
||||||
|
return this.http.get<Paciente[]>(`${this.baseUrl}/api/pacientes`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarPorNombre(nombre: string): Observable<Paciente[]> {
|
||||||
|
const params = new HttpParams().set('nombre', nombre);
|
||||||
|
return this.http.get<Paciente[]>(`${this.baseUrl}/api/pacientes`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- IPS y autorizantes ----
|
||||||
|
obtenerIpsPorInterno(interno: string): Observable<Ips[]> {
|
||||||
|
const params = new HttpParams().set('interno', interno);
|
||||||
|
return this.http.get<Ips[]>(`${this.baseUrl}/api/ips-por-interno`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
obtenerAutorizantes(): Observable<Autorizante[]> {
|
||||||
|
return this.http.get<Autorizante[]>(`${this.baseUrl}/api/autorizantes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Autorizaciones ----
|
||||||
|
crearAutorizacion(payload: {
|
||||||
|
numero_autorizacion: string;
|
||||||
|
interno: string;
|
||||||
|
id_ips: number;
|
||||||
|
numero_documento_autorizante: number;
|
||||||
|
observacion?: string;
|
||||||
|
fecha_autorizacion?: string;
|
||||||
|
}): Observable<RespuestaAutorizacion> {
|
||||||
|
return this.http.post<RespuestaAutorizacion>(
|
||||||
|
`${this.baseUrl}/api/autorizaciones`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
obtenerAutorizacionesPorInterno(
|
||||||
|
interno: string
|
||||||
|
): Observable<AutorizacionListado[]> {
|
||||||
|
const params = new HttpParams().set('interno', interno);
|
||||||
|
return this.http.get<AutorizacionListado[]>(
|
||||||
|
`${this.baseUrl}/api/autorizaciones`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Cargar Excel (Python + SQL) ----
|
||||||
|
cargarExcelPacientes(formData: FormData): Observable<{
|
||||||
|
ok: boolean;
|
||||||
|
mensaje: string;
|
||||||
|
activos: number;
|
||||||
|
antiguos: number;
|
||||||
|
}> {
|
||||||
|
return this.http.post<{
|
||||||
|
ok: boolean;
|
||||||
|
mensaje: string;
|
||||||
|
activos: number;
|
||||||
|
antiguos: number;
|
||||||
|
}>(`${this.baseUrl}/api/cargar-excel-pacientes`, formData);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
saludut-inpec/src/assets/logo_SALUDUT.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
saludut-inpec/src/favicon.ico
Normal file
|
After Width: | Height: | Size: 257 KiB |
20
saludut-inpec/src/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SaludutInpec</title>
|
||||||
|
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico?v=3">
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"/>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
saludut-inpec/src/main.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
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 { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, {
|
||||||
|
providers: [
|
||||||
|
provideRouter(routes),
|
||||||
|
importProvidersFrom(
|
||||||
|
HttpClientModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
)
|
||||||
|
]
|
||||||
|
});
|
||||||
465
saludut-inpec/src/styles.css
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
/* ============================
|
||||||
|
Variables de diseño (tema)
|
||||||
|
============================ */
|
||||||
|
:root {
|
||||||
|
--font-main: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
--color-primary: #1976d2;
|
||||||
|
--color-primary-dark: #145ca5;
|
||||||
|
--color-primary-soft: #e3f2fd;
|
||||||
|
--color-header-grad-2: #1565c0;
|
||||||
|
|
||||||
|
--color-bg: #f5f5f5;
|
||||||
|
--color-card: #ffffff;
|
||||||
|
--color-border: #e0e0e0;
|
||||||
|
|
||||||
|
--color-text-main: #222222;
|
||||||
|
--color-text-muted: #666666;
|
||||||
|
|
||||||
|
--color-success: #2e7d32;
|
||||||
|
--color-error: #c62828;
|
||||||
|
|
||||||
|
--font-size-base: 14px;
|
||||||
|
--font-size-sm: 13px;
|
||||||
|
--font-size-lg: 1.4rem;
|
||||||
|
--radius-card: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--font-main);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Layout general === */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 16px 40px;
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Header con logo SALUD UT === */
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-primary),
|
||||||
|
var(--color-header-grad-2)
|
||||||
|
);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-saludut {
|
||||||
|
height: 52px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-saludut {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tarjeta de búsqueda === */
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
background-color: var(--color-card);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
padding: 20px 28px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
min-width: 130px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input,
|
||||||
|
.form-row select,
|
||||||
|
.form-row textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
outline: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input:focus,
|
||||||
|
.form-row select:focus,
|
||||||
|
.form-row textarea:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 1px rgba(25, 118, 210, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Botones === */
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.15s ease, transform 0.05s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--color-primary-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: var(--color-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-auths {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-auths:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estado */
|
||||||
|
|
||||||
|
.estado {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado.error {
|
||||||
|
color: var(--color-error);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado.ok {
|
||||||
|
color: var(--color-success);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Subir Excel === */
|
||||||
|
|
||||||
|
.upload-excel {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-excel .upload-msg {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tabla de resultados === */
|
||||||
|
|
||||||
|
.resultados-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-tabla {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: var(--color-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla th,
|
||||||
|
.tabla td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla tr:nth-child(even) td {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla tr:hover td {
|
||||||
|
background-color: #e8f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla td button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Autorizaciones previas === */
|
||||||
|
|
||||||
|
.autorizaciones-previas {
|
||||||
|
margin-top: 20px;
|
||||||
|
background: var(--color-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-previas h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones th,
|
||||||
|
.tabla-autorizaciones td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones tr:nth-child(even) td {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones tr:hover td {
|
||||||
|
background-color: #e8f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-restricted {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Modal de autorización === */
|
||||||
|
|
||||||
|
.aut-modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal {
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-card);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 24px 28px 28px;
|
||||||
|
max-width: 820px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--color-error);
|
||||||
|
font-size: 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
|
transition: background 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal-close:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-paciente {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Responsive === */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.resultados-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left,
|
||||||
|
.header-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 8px 20px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
377
saludut-inpec/src/styles.css.txt
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
/* ============================
|
||||||
|
Variables de diseño (tema)
|
||||||
|
============================ */
|
||||||
|
:root {
|
||||||
|
--font-main: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
--color-primary: #1976d2;
|
||||||
|
--color-primary-dark: #145ca5;
|
||||||
|
--color-primary-soft: #e3f2fd;
|
||||||
|
--color-header-grad-2: #1565c0;
|
||||||
|
|
||||||
|
--color-bg: #f5f5f5;
|
||||||
|
--color-card: #ffffff;
|
||||||
|
--color-border: #e0e0e0;
|
||||||
|
|
||||||
|
--color-text-main: #222222;
|
||||||
|
--color-text-muted: #666666;
|
||||||
|
|
||||||
|
--color-success: #2e7d32;
|
||||||
|
--color-error: #c62828;
|
||||||
|
|
||||||
|
--font-size-base: 14px;
|
||||||
|
--font-size-sm: 13px;
|
||||||
|
--font-size-lg: 1.4rem;
|
||||||
|
--radius-card: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--font-main);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Layout general === */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 16px 40px;
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Header con logo SALUD UT === */
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-primary),
|
||||||
|
var(--color-header-grad-2)
|
||||||
|
);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-saludut {
|
||||||
|
height: 52px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right .badge-saludut {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tarjeta de búsqueda === */
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
background-color: var(--color-card);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
padding: 20px 28px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
min-width: 130px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input,
|
||||||
|
.form-row select,
|
||||||
|
.form-row textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
outline: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input:focus,
|
||||||
|
.form-row select:focus,
|
||||||
|
.form-row textarea:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 1px rgba(25, 118, 210, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Botones === */
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.15s ease, transform 0.05s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--color-primary-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: var(--color-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estado */
|
||||||
|
|
||||||
|
.estado {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado.error {
|
||||||
|
color: var(--color-error);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estado.ok {
|
||||||
|
color: var(--color-success);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Subir Excel === */
|
||||||
|
|
||||||
|
.upload-excel {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-excel .upload-msg {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tabla de resultados === */
|
||||||
|
|
||||||
|
.resultados-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-tabla {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: var(--color-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla th,
|
||||||
|
.tabla td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla tr:nth-child(even) td {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla tr:hover td {
|
||||||
|
background-color: #e8f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla td button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Autorizaciones previas === */
|
||||||
|
|
||||||
|
.autorizaciones-previas {
|
||||||
|
margin-top: 20px;
|
||||||
|
background: var(--color-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autorizaciones-previas h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones th,
|
||||||
|
.tabla-autorizaciones td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones tr:nth-child(even) td {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla-autorizaciones tr:hover td {
|
||||||
|
background-color: #e8f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Modal de autorización === */
|
||||||
|
|
||||||
|
.aut-modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal {
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-card);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 24px 28px 28px;
|
||||||
|
max-width: 820px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--color-error);
|
||||||
|
font-size: 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
|
transition: background 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-modal-close:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-paciente {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Responsivo === */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.resultados-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
saludut-inpec/tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
saludut-inpec/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
saludut-inpec/tsconfig.spec.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||