producto final
This commit is contained in:
parent
4e7acc1720
commit
4444dbe92b
190
README.md
190
README.md
@ -1,59 +1,179 @@
|
|||||||
# SaludutInpec
|
# Saludut
|
||||||
|
|
||||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.0.
|
Frontend Angular (`saludut-inpec`) + backend Node.js (`backend`) + PostgreSQL + Python + LibreOffice.
|
||||||
|
|
||||||
## Development server
|
## Produccion (Alpine 3.21)
|
||||||
|
|
||||||
To start a local development server, run:
|
|
||||||
|
|
||||||
|
### 1) Preparar servidor
|
||||||
```bash
|
```bash
|
||||||
ng serve
|
apk add nginx certbot certbot-nginx nftables rsync nodejs npm \
|
||||||
|
postgresql postgresql-client python3 py3-pip libreoffice
|
||||||
|
rc-update add postgresql default
|
||||||
|
rc-service postgresql setup
|
||||||
|
rc-service postgresql start
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
### 2) Crear BD y usuario
|
||||||
|
|
||||||
## Code scaffolding
|
|
||||||
|
|
||||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng generate component component-name
|
su - postgres
|
||||||
|
psql -c "CREATE USER saludut_user WITH PASSWORD 'TU_PASSWORD_FUERTE';"
|
||||||
|
psql -c "CREATE DATABASE saludut_db OWNER saludut_user;"
|
||||||
|
exit
|
||||||
```
|
```
|
||||||
|
|
||||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
### 3) Subir codigo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng generate --help
|
mkdir -p /opt/saludut/backend /opt/saludut/frontend
|
||||||
|
tar -czf saludut-backend.tar.gz --exclude="node_modules" --exclude=".env" backend
|
||||||
|
tar -czf saludut-frontend.tar.gz --exclude="node_modules" saludut-inpec
|
||||||
|
scp -C saludut-backend.tar.gz root@tu-servidor:/opt/saludut/
|
||||||
|
scp -C saludut-frontend.tar.gz root@tu-servidor:/opt/saludut/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
En el servidor:
|
||||||
|
|
||||||
To build the project run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng build
|
tar -xzf /opt/saludut/saludut-backend.tar.gz -C /opt/saludut/
|
||||||
|
tar -xzf /opt/saludut/saludut-frontend.tar.gz -C /opt/saludut/
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
### 4) Inicializar esquema y datos
|
||||||
|
Este repo no incluye el esquema base (tablas `usuario`, `rol`, `autorizacion`, etc). Restaura tu dump base primero.
|
||||||
## Running unit tests
|
|
||||||
|
|
||||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
|
||||||
|
|
||||||
|
Luego ejecuta los SQL que si estan en el repo, en este orden:
|
||||||
```bash
|
```bash
|
||||||
ng test
|
psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/cups_schema.sql
|
||||||
|
psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/cups_referencia.sql
|
||||||
|
psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/cups_cubiertos.sql
|
||||||
|
psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/establecimiento.sql
|
||||||
|
psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/paciente.sql
|
||||||
|
psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/ingreso.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running end-to-end tests
|
Si tu esquema tiene restricciones, ajusta el orden de los inserts para respetar las FK.
|
||||||
|
|
||||||
For end-to-end (e2e) testing, run:
|
### 5) Backend (.env)
|
||||||
|
Crear `backend/.env` en el servidor:
|
||||||
```bash
|
```
|
||||||
ng e2e
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=saludut_user
|
||||||
|
DB_PASSWORD=TU_PASSWORD_FUERTE
|
||||||
|
DB_NAME=saludut_db
|
||||||
|
JWT_SECRET=CAMBIA_ESTE_SECRETO
|
||||||
|
JWT_EXPIRES_IN=24h
|
||||||
|
PORT=3000
|
||||||
|
SOFFICE_PATH=/usr/bin/soffice
|
||||||
|
PYTHON_PATH=python3
|
||||||
```
|
```
|
||||||
|
|
||||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
### 6) Crear usuario admin (manual)
|
||||||
|
Generar hash bcrypt:
|
||||||
|
```bash
|
||||||
|
node /opt/saludut/backend/src/generate-hash.js
|
||||||
|
```
|
||||||
|
|
||||||
## Additional Resources
|
Luego en PostgreSQL:
|
||||||
|
```sql
|
||||||
|
SELECT id_rol FROM rol WHERE nombre_rol = 'administrador';
|
||||||
|
|
||||||
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.
|
INSERT INTO usuario
|
||||||
|
(username, email, password_hash, nombre_completo, id_rol, activo, fecha_creacion)
|
||||||
|
VALUES
|
||||||
|
('admin', 'admin@saludut.local', '<HASH_BCRYPT>', 'Administrador Sistema', <ID_ROL>, true, NOW());
|
||||||
|
```
|
||||||
|
|
||||||
|
Si tu esquema tiene columnas obligatorias adicionales, completa esos campos.
|
||||||
|
|
||||||
|
### 7) Backend (instalar y correr)
|
||||||
|
```bash
|
||||||
|
cd /opt/saludut/backend
|
||||||
|
npm ci --omit=dev
|
||||||
|
node src/server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenRC (servicio backend)
|
||||||
|
```bash
|
||||||
|
cat <<'EOF' > /etc/init.d/saludut-backend
|
||||||
|
#!/sbin/openrc-run
|
||||||
|
|
||||||
|
name="saludut-backend"
|
||||||
|
description="Backend Node para Saludut"
|
||||||
|
directory="/opt/saludut/backend"
|
||||||
|
command="/usr/bin/node"
|
||||||
|
command_args="src/server.js"
|
||||||
|
command_background="yes"
|
||||||
|
pidfile="/run/${RC_SVCNAME}.pid"
|
||||||
|
output_log="/var/log/saludut-backend.log"
|
||||||
|
error_log="/var/log/saludut-backend.err"
|
||||||
|
|
||||||
|
depend() {
|
||||||
|
need net
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /etc/init.d/saludut-backend
|
||||||
|
rc-update add saludut-backend default
|
||||||
|
rc-service saludut-backend start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8) Frontend (build)
|
||||||
|
```bash
|
||||||
|
cd /opt/saludut/saludut-inpec
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
El build queda en `saludut-inpec/dist/saludut-inpec/`.
|
||||||
|
|
||||||
|
### 9) Configurar API base (sin localhost)
|
||||||
|
El frontend lee la base desde `window.__SALUDUT_CONFIG__`:
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
window.__SALUDUT_CONFIG__ = {
|
||||||
|
apiBaseUrl: '/api'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el backend esta en otro dominio, cambia `apiBaseUrl` antes de publicar.
|
||||||
|
|
||||||
|
### 10) Nginx + SSL (ejemplo)
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name saludut.tu-dominio.com;
|
||||||
|
|
||||||
|
root /var/www/saludut;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:3000/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Luego:
|
||||||
|
```bash
|
||||||
|
rc-service nginx start
|
||||||
|
certbot --nginx -d saludut.tu-dominio.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
node src/server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd saludut-inpec
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|||||||
13
aunarsalud/src/app/config.ts
Normal file
13
aunarsalud/src/app/config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__AUNAR_CONFIG__?: {
|
||||||
|
apiBaseUrl?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBase =
|
||||||
|
(typeof window !== 'undefined' && window.__AUNAR_CONFIG__?.apiBaseUrl) ||
|
||||||
|
'/api';
|
||||||
|
|
||||||
|
export const API_BASE_URL = rawBase.replace(/\/+$/, '');
|
||||||
BIN
backend/src/NOTA TECNICA PPL AJUSTADA. (1) 1.xlsx
Normal file
BIN
backend/src/NOTA TECNICA PPL AJUSTADA. (1) 1.xlsx
Normal file
Binary file not shown.
BIN
backend/src/TablaReferencia_CUPS__1-2024 (1).xlsx
Normal file
BIN
backend/src/TablaReferencia_CUPS__1-2024 (1).xlsx
Normal file
Binary file not shown.
201
backend/src/cargar_cups.py
Normal file
201
backend/src/cargar_cups.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
NOTA_TECNICA_FILE = Path(__file__).with_name("NOTA TECNICA PPL AJUSTADA. (1) 1.xlsx")
|
||||||
|
TABLA_REFERENCIA_FILE = Path(__file__).with_name("TablaReferencia_CUPS__1-2024 (1).xlsx")
|
||||||
|
|
||||||
|
NOTA_TECNICA_SHEET = "NOTA TECNICA GENERAL"
|
||||||
|
TABLA_REFERENCIA_SHEET = "Table"
|
||||||
|
|
||||||
|
OUTPUT_REFERENCIA_SQL = Path(__file__).with_name("cups_referencia.sql")
|
||||||
|
OUTPUT_CUBIERTOS_SQL = Path(__file__).with_name("cups_cubiertos.sql")
|
||||||
|
|
||||||
|
|
||||||
|
def _to_text(value):
|
||||||
|
if pd.isna(value):
|
||||||
|
return ""
|
||||||
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_text(value):
|
||||||
|
if value is None:
|
||||||
|
return "NULL"
|
||||||
|
text = str(value).strip()
|
||||||
|
if text == "":
|
||||||
|
return "NULL"
|
||||||
|
text = text.replace("'", "''")
|
||||||
|
return f"'{text}'"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_text(value):
|
||||||
|
raw = _to_text(value)
|
||||||
|
if raw == "":
|
||||||
|
return ""
|
||||||
|
normalized = unicodedata.normalize("NFD", raw)
|
||||||
|
without_marks = "".join(ch for ch in normalized if unicodedata.category(ch) != "Mn")
|
||||||
|
return without_marks.upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _find_header_row(df, marker="CUPS"):
|
||||||
|
upper_marker = marker.strip().upper()
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
if row.astype(str).str.strip().str.upper().eq(upper_marker).any():
|
||||||
|
return idx
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_column(columns, candidates):
|
||||||
|
upper = {str(col).strip().upper(): col for col in columns}
|
||||||
|
for candidate in candidates:
|
||||||
|
key = candidate.strip().upper()
|
||||||
|
if key in upper:
|
||||||
|
return upper[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_sheet(file_path, desired_name):
|
||||||
|
xls = pd.ExcelFile(file_path)
|
||||||
|
for name in xls.sheet_names:
|
||||||
|
if name.strip().lower() == desired_name.strip().lower():
|
||||||
|
return name
|
||||||
|
return desired_name
|
||||||
|
|
||||||
|
|
||||||
|
def cargar_cups_cubiertos():
|
||||||
|
sheet_name = _resolve_sheet(NOTA_TECNICA_FILE, NOTA_TECNICA_SHEET)
|
||||||
|
df = pd.read_excel(NOTA_TECNICA_FILE, sheet_name=sheet_name, dtype=str)
|
||||||
|
header_idx = _find_header_row(df, "CUPS")
|
||||||
|
if header_idx is None:
|
||||||
|
raise ValueError("No se encontro la fila de encabezados con CUPS.")
|
||||||
|
|
||||||
|
headers = df.iloc[header_idx].astype(str).str.strip()
|
||||||
|
data = df.iloc[header_idx + 1 :].copy()
|
||||||
|
data.columns = headers
|
||||||
|
|
||||||
|
col_codigo = _pick_column(data.columns, ["CUPS", "CODIGO", "CODIGO CUPS"])
|
||||||
|
col_descripcion = _pick_column(data.columns, ["DESCRIPCION", "DESCRIPCION CUPS", "NOMBRE"])
|
||||||
|
col_especialidad = _pick_column(data.columns, ["ESPECIALIDAD"])
|
||||||
|
|
||||||
|
if not col_codigo or not col_descripcion:
|
||||||
|
raise ValueError("No se encontraron columnas de CUPS o descripcion en la nota tecnica.")
|
||||||
|
|
||||||
|
select_cols = [col_codigo, col_descripcion]
|
||||||
|
if col_especialidad:
|
||||||
|
select_cols.append(col_especialidad)
|
||||||
|
|
||||||
|
data = data[select_cols].rename(
|
||||||
|
columns={
|
||||||
|
col_codigo: "codigo",
|
||||||
|
col_descripcion: "descripcion",
|
||||||
|
col_especialidad: "especialidad_raw",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
data["codigo"] = data["codigo"].map(_to_text)
|
||||||
|
data["descripcion"] = data["descripcion"].map(_to_text)
|
||||||
|
data["descripcion_busqueda"] = data["descripcion"].map(_normalize_text)
|
||||||
|
if "especialidad_raw" in data.columns:
|
||||||
|
data["especialidad_raw"] = data["especialidad_raw"].map(_to_text)
|
||||||
|
else:
|
||||||
|
data["especialidad_raw"] = ""
|
||||||
|
|
||||||
|
def _parse_nivel(value):
|
||||||
|
raw = _to_text(value).upper()
|
||||||
|
if raw.startswith("NIVEL"):
|
||||||
|
return raw
|
||||||
|
return ""
|
||||||
|
|
||||||
|
data["nivel"] = data["especialidad_raw"].map(_parse_nivel)
|
||||||
|
data["especialidad"] = data["especialidad_raw"].apply(
|
||||||
|
lambda v: "" if _parse_nivel(v) else _to_text(v)
|
||||||
|
)
|
||||||
|
data = data[data["codigo"] != ""]
|
||||||
|
data = data.drop_duplicates(subset=["codigo"])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def cargar_cups_referencia():
|
||||||
|
sheet_name = _resolve_sheet(TABLA_REFERENCIA_FILE, TABLA_REFERENCIA_SHEET)
|
||||||
|
df = pd.read_excel(TABLA_REFERENCIA_FILE, sheet_name=sheet_name, dtype=str)
|
||||||
|
col_codigo = _pick_column(df.columns, ["Codigo", "Código", "CODIGO"])
|
||||||
|
col_descripcion = _pick_column(df.columns, ["Nombre", "Descripcion", "Descripción"])
|
||||||
|
|
||||||
|
if not col_codigo or not col_descripcion:
|
||||||
|
raise ValueError("No se encontraron columnas de Codigo o Descripcion en la tabla de referencia.")
|
||||||
|
|
||||||
|
data = df[[col_codigo, col_descripcion]].rename(
|
||||||
|
columns={col_codigo: "codigo", col_descripcion: "descripcion"}
|
||||||
|
)
|
||||||
|
|
||||||
|
data["codigo"] = data["codigo"].map(_to_text)
|
||||||
|
data["descripcion"] = data["descripcion"].map(_to_text)
|
||||||
|
data = data[data["codigo"] != ""]
|
||||||
|
data = data.drop_duplicates(subset=["codigo"])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def generar_sql_referencia(dataframe):
|
||||||
|
lines = ["-- UPSERTS TABLA cups_referencia"]
|
||||||
|
for _, row in dataframe.iterrows():
|
||||||
|
codigo = _sql_text(row["codigo"])
|
||||||
|
descripcion = _sql_text(row["descripcion"])
|
||||||
|
if codigo == "NULL" or descripcion == "NULL":
|
||||||
|
continue
|
||||||
|
lines.append(
|
||||||
|
"INSERT INTO cups_referencia (codigo, descripcion) "
|
||||||
|
f"VALUES ({codigo}, {descripcion}) "
|
||||||
|
"ON CONFLICT (codigo) DO UPDATE SET descripcion = EXCLUDED.descripcion;"
|
||||||
|
)
|
||||||
|
OUTPUT_REFERENCIA_SQL.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def generar_sql_cubiertos(dataframe):
|
||||||
|
lines = [
|
||||||
|
"-- UPSERTS TABLA cups_cubiertos",
|
||||||
|
"UPDATE cups_cubiertos SET activo = false;",
|
||||||
|
]
|
||||||
|
for _, row in dataframe.iterrows():
|
||||||
|
codigo = _sql_text(row["codigo"])
|
||||||
|
descripcion = _sql_text(row["descripcion"])
|
||||||
|
descripcion_busqueda = _sql_text(row["descripcion_busqueda"])
|
||||||
|
nivel = _sql_text(row.get("nivel", ""))
|
||||||
|
especialidad = _sql_text(row.get("especialidad", ""))
|
||||||
|
if codigo == "NULL" or descripcion == "NULL":
|
||||||
|
continue
|
||||||
|
lines.append(
|
||||||
|
"INSERT INTO cups_cubiertos "
|
||||||
|
"(codigo, descripcion, descripcion_busqueda, nivel, especialidad, activo) "
|
||||||
|
f"VALUES ({codigo}, {descripcion}, {descripcion_busqueda}, {nivel}, {especialidad}, true) "
|
||||||
|
"ON CONFLICT (codigo) DO UPDATE SET "
|
||||||
|
"descripcion = EXCLUDED.descripcion, "
|
||||||
|
"descripcion_busqueda = EXCLUDED.descripcion_busqueda, "
|
||||||
|
"nivel = EXCLUDED.nivel, "
|
||||||
|
"especialidad = EXCLUDED.especialidad, "
|
||||||
|
"activo = true;"
|
||||||
|
)
|
||||||
|
OUTPUT_CUBIERTOS_SQL.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not NOTA_TECNICA_FILE.exists():
|
||||||
|
raise FileNotFoundError(f"No se encuentra el archivo: {NOTA_TECNICA_FILE}")
|
||||||
|
if not TABLA_REFERENCIA_FILE.exists():
|
||||||
|
raise FileNotFoundError(f"No se encuentra el archivo: {TABLA_REFERENCIA_FILE}")
|
||||||
|
|
||||||
|
cubiertos = cargar_cups_cubiertos()
|
||||||
|
referencia = cargar_cups_referencia()
|
||||||
|
|
||||||
|
generar_sql_referencia(referencia)
|
||||||
|
generar_sql_cubiertos(cubiertos)
|
||||||
|
|
||||||
|
print("Archivos generados:")
|
||||||
|
print(f" {OUTPUT_REFERENCIA_SQL} ({len(referencia)} registros)")
|
||||||
|
print(f" {OUTPUT_CUBIERTOS_SQL} ({len(cubiertos)} registros)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1771
backend/src/cups_cubiertos.sql
Normal file
1771
backend/src/cups_cubiertos.sql
Normal file
File diff suppressed because it is too large
Load Diff
9992
backend/src/cups_referencia.sql
Normal file
9992
backend/src/cups_referencia.sql
Normal file
File diff suppressed because it is too large
Load Diff
44
backend/src/cups_schema.sql
Normal file
44
backend/src/cups_schema.sql
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
-- Tablas y columna para CUPS
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cups_referencia (
|
||||||
|
codigo VARCHAR(20) PRIMARY KEY,
|
||||||
|
descripcion TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cups_cubiertos (
|
||||||
|
codigo VARCHAR(20) PRIMARY KEY,
|
||||||
|
descripcion TEXT NOT NULL,
|
||||||
|
descripcion_busqueda TEXT,
|
||||||
|
nivel VARCHAR(10),
|
||||||
|
especialidad TEXT,
|
||||||
|
activo BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE autorizacion
|
||||||
|
ADD COLUMN IF NOT EXISTS cup_codigo VARCHAR(20);
|
||||||
|
|
||||||
|
ALTER TABLE cups_cubiertos
|
||||||
|
ADD COLUMN IF NOT EXISTS activo BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
|
||||||
|
ALTER TABLE cups_cubiertos
|
||||||
|
ADD COLUMN IF NOT EXISTS descripcion_busqueda TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE cups_cubiertos
|
||||||
|
ADD COLUMN IF NOT EXISTS nivel VARCHAR(10);
|
||||||
|
|
||||||
|
ALTER TABLE cups_cubiertos
|
||||||
|
ADD COLUMN IF NOT EXISTS especialidad TEXT;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'autorizacion_cup_codigo_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE autorizacion
|
||||||
|
ADD CONSTRAINT autorizacion_cup_codigo_fk
|
||||||
|
FOREIGN KEY (cup_codigo)
|
||||||
|
REFERENCES cups_cubiertos(codigo);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
Binary file not shown.
@ -2609,9 +2609,11 @@ async function crearLibroAutorizacion(a) {
|
|||||||
sheet.mergeCells('L4:O4');
|
sheet.mergeCells('L4:O4');
|
||||||
sheet.mergeCells('P4:R4');
|
sheet.mergeCells('P4:R4');
|
||||||
sheet.mergeCells('L5:O5');
|
sheet.mergeCells('L5:O5');
|
||||||
// Hacer la fila 31 más alta
|
// Hacer la seccion de servicios autorizados mas alta
|
||||||
const fila31 = sheet.getRow(31);
|
const fila31 = sheet.getRow(31);
|
||||||
fila31.height = 150; // prueba 30–40 hasta que te guste
|
fila31.height = 200;
|
||||||
|
const fila32 = sheet.getRow(32);
|
||||||
|
fila32.height = 40;
|
||||||
|
|
||||||
|
|
||||||
// 3️⃣ Ajuste: borde derecho en la última columna utilizada
|
// 3️⃣ Ajuste: borde derecho en la última columna utilizada
|
||||||
@ -2669,6 +2671,12 @@ async function crearLibroAutorizacion(a) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 3) Fecha de autorización (P8:R8)
|
// 3) Fecha de autorización (P8:R8)
|
||||||
|
// Version de autorizacion (P3:R3)
|
||||||
|
const versionValor = a.version || 1;
|
||||||
|
['P3', 'Q3', 'R3'].forEach(celda => {
|
||||||
|
sheet.getCell(celda).value = versionValor;
|
||||||
|
});
|
||||||
|
|
||||||
if (a.fecha_autorizacion) {
|
if (a.fecha_autorizacion) {
|
||||||
const fecha = new Date(a.fecha_autorizacion);
|
const fecha = new Date(a.fecha_autorizacion);
|
||||||
['P8', 'Q8', 'R8'].forEach(celda => {
|
['P8', 'Q8', 'R8'].forEach(celda => {
|
||||||
@ -2734,8 +2742,20 @@ async function crearLibroAutorizacion(a) {
|
|||||||
sheet.getCell(celda).value = a.nombre_establecimiento || '';
|
sheet.getCell(celda).value = a.nombre_establecimiento || '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 10) Observación / servicios autorizados (H25:R25)
|
// 10) Observacion / servicios autorizados (H25:R25)
|
||||||
const observacion = a.observacion || '';
|
const cupCodigo = a.cup_codigo || '';
|
||||||
|
const cupDescripcion = a.cup_descripcion || '';
|
||||||
|
const cupNivel = a.cup_nivel || '';
|
||||||
|
const nivelTexto = cupNivel
|
||||||
|
? (String(cupNivel).toUpperCase().includes('NIVEL') ? cupNivel : `NIVEL ${cupNivel}`)
|
||||||
|
: '';
|
||||||
|
const cupInfoParts = [];
|
||||||
|
if (cupCodigo) cupInfoParts.push(`CUPS ${cupCodigo}`);
|
||||||
|
if (cupDescripcion) cupInfoParts.push(cupDescripcion);
|
||||||
|
if (nivelTexto) cupInfoParts.push(nivelTexto);
|
||||||
|
const cupInfo = cupInfoParts.join(' - ');
|
||||||
|
const observacionBase = a.observacion || '';
|
||||||
|
const observacion = [cupInfo, observacionBase].filter(Boolean).join(' | ');
|
||||||
['H31','R31'].forEach(celda => {
|
['H31','R31'].forEach(celda => {
|
||||||
sheet.getCell(celda).value = observacion;
|
sheet.getCell(celda).value = observacion;
|
||||||
});
|
});
|
||||||
@ -2777,4 +2797,3 @@ async function crearLibroAutorizacion(a) {
|
|||||||
|
|
||||||
// Exportamos la función para usarla en server.js
|
// Exportamos la función para usarla en server.js
|
||||||
module.exports = { crearLibroAutorizacion };
|
module.exports = { crearLibroAutorizacion };
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
9
saludut-inpec/package-lock.json
generated
9
saludut-inpec/package-lock.json
generated
@ -15,7 +15,8 @@
|
|||||||
"@angular/platform-browser": "^21.0.0",
|
"@angular/platform-browser": "^21.0.0",
|
||||||
"@angular/router": "^21.0.0",
|
"@angular/router": "^21.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "^0.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^21.0.0",
|
"@angular/build": "^21.0.0",
|
||||||
@ -9316,6 +9317,12 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25 || ^4"
|
"zod": "^3.25 || ^4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zone.js": {
|
||||||
|
"version": "0.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||||
|
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,8 @@
|
|||||||
"@angular/platform-browser": "^21.0.0",
|
"@angular/platform-browser": "^21.0.0",
|
||||||
"@angular/router": "^21.0.0",
|
"@angular/router": "^21.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "^0.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^21.0.0",
|
"@angular/build": "^21.0.0",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { AdminGuard } from './guards/auth-guard';
|
|||||||
import { AutorizacionesPorFechaComponent } from './components/autorizaciones-por-fecha/autorizaciones-por-fecha';
|
import { AutorizacionesPorFechaComponent } from './components/autorizaciones-por-fecha/autorizaciones-por-fecha';
|
||||||
import { AutorizacionesComponent } from './components/autorizaciones/autorizaciones';
|
import { AutorizacionesComponent } from './components/autorizaciones/autorizaciones';
|
||||||
import { UsuariosComponent } from './components/usuarios/usuarios';
|
import { UsuariosComponent } from './components/usuarios/usuarios';
|
||||||
|
import { CargarCupsComponent } from './components/cargar-cups/cargar-cups';
|
||||||
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
@ -38,6 +39,12 @@ export const routes: Routes = [
|
|||||||
canActivate: [AuthGuard]
|
canActivate: [AuthGuard]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'cargar-cups',
|
||||||
|
component: CargarCupsComponent,
|
||||||
|
canActivate: [AuthGuard, AdminGuard],
|
||||||
|
},
|
||||||
|
|
||||||
// cualquier cosa rara → dashboard
|
// cualquier cosa rara → dashboard
|
||||||
{ path: '**', redirectTo: 'dashboard' },
|
{ path: '**', redirectTo: 'dashboard' },
|
||||||
|
|
||||||
|
|||||||
@ -1,92 +1,13 @@
|
|||||||
/* ============================
|
/* ============================
|
||||||
Estilos del Componente Autorizaciones por Fecha
|
Estilos del modulo 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 {
|
.alert {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 24px;
|
margin: 20px 0 24px;
|
||||||
animation: slideIn 0.3s ease;
|
animation: slideIn 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +24,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-icon {
|
.alert-icon {
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +37,7 @@
|
|||||||
.alert-close {
|
.alert-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@ -133,13 +55,9 @@
|
|||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filtros Card */
|
|
||||||
.filtros-card {
|
.filtros-card {
|
||||||
background: white;
|
margin-top: 20px;
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filtros-card h2 {
|
.filtros-card h2 {
|
||||||
@ -203,18 +121,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-buscar {
|
.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;
|
min-width: 140px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@ -225,8 +131,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-buscar:disabled {
|
.btn-buscar:disabled {
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@ -247,17 +151,9 @@
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resultados Section */
|
/* Resultados Section */
|
||||||
.resultados-section {
|
.resultados-section {
|
||||||
background: white;
|
margin-bottom: 24px;
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultados-header {
|
.resultados-header {
|
||||||
@ -281,28 +177,6 @@
|
|||||||
gap: 12px;
|
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 */
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -311,24 +185,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.autorizaciones-table {
|
.autorizaciones-table {
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autorizaciones-table th {
|
.autorizaciones-table th {
|
||||||
background: #f8fafc;
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #222222;
|
|
||||||
border-bottom: 2px solid #e5e7eb;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autorizaciones-table td {
|
.autorizaciones-table td {
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,11 +204,10 @@
|
|||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Columnas específicas */
|
|
||||||
.numero-autorizacion {
|
.numero-autorizacion {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1976d2;
|
color: #1976d2;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fecha {
|
.fecha {
|
||||||
@ -355,15 +218,24 @@
|
|||||||
|
|
||||||
.interno {
|
.interno {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nombre-paciente {
|
.cup-codigo {
|
||||||
max-width: 200px;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
font-family: "Courier New", monospace;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ips {
|
.cup-nivel {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nombre-paciente,
|
||||||
|
.ips,
|
||||||
|
.autorizante,
|
||||||
|
.establecimiento {
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
@ -374,49 +246,23 @@
|
|||||||
color: #666666;
|
color: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autorizante {
|
|
||||||
max-width: 150px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.establecimiento {
|
|
||||||
max-width: 150px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acciones {
|
.acciones {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-descargar {
|
.btn-descargar:hover:not(:disabled) {
|
||||||
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);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty State */
|
/* Empty State */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 60px 24px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
padding: 60px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
font-size: 4rem;
|
font-size: 3rem;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
@ -435,7 +281,7 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Línea de estado inferior para procesos de carga */
|
/* Linea de estado inferior para procesos de carga */
|
||||||
.status-line {
|
.status-line {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@ -445,7 +291,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Spinner pequeñito en línea */
|
/* Spinner pequenito en linea */
|
||||||
.spinner-inline {
|
.spinner-inline {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
@ -455,13 +301,6 @@
|
|||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.loading-content p {
|
|
||||||
margin: 0;
|
|
||||||
color: #222222;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
@ -474,23 +313,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@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 {
|
.filtros-form .form-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@ -509,10 +342,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autorizaciones-table th,
|
.autorizaciones-table th,
|
||||||
.autorizaciones-table td {
|
.autorizaciones-table td {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@ -527,10 +356,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.page-header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resultados-actions {
|
.resultados-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -543,8 +368,4 @@
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 40px 16px;
|
padding: 40px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,157 +1,189 @@
|
|||||||
<div class="autorizaciones-fecha-container">
|
<div class="page-shell">
|
||||||
<!-- Header -->
|
<div class="content-container">
|
||||||
<div class="page-header">
|
<app-header
|
||||||
<div class="header-left">
|
title="Autorizaciones por fecha"
|
||||||
<button class="back-button" (click)="volverAtras()" title="Volver">
|
subtitle="Consulta y descarga de autorizaciones por rango de fechas"
|
||||||
← Volver
|
[showBack]="true"
|
||||||
</button>
|
backLabel="Volver"
|
||||||
<div>
|
(back)="volverAtras()"
|
||||||
<h1>Autorizaciones por Fecha</h1>
|
[showLogo]="false"
|
||||||
<p class="header-subtitle">Consulta y descarga de autorizaciones por rango de fechas</p>
|
></app-header>
|
||||||
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mensajes -->
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
<div class="alert alert-error" *ngIf="errorMessage">
|
<span class="alert-icon">✓</span>
|
||||||
<span class="alert-icon">⚠️</span>
|
<span class="alert-message">{{ successMessage }}</span>
|
||||||
<span class="alert-message">{{ errorMessage }}</span>
|
<button class="alert-close" (click)="limpiarMensajes()">×</button>
|
||||||
<button class="alert-close" (click)="limpiarMensajes()">×</button>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-success" *ngIf="successMessage">
|
<!-- Filtros -->
|
||||||
<span class="alert-icon">✅</span>
|
<div class="filtros-card card">
|
||||||
<span class="alert-message">{{ successMessage }}</span>
|
<h2>Rango de fechas</h2>
|
||||||
<button class="alert-close" (click)="limpiarMensajes()">×</button>
|
<form class="filtros-form" [formGroup]="filtroForm" (ngSubmit)="buscarAutorizaciones()">
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
<!-- Filtros -->
|
<div class="form-group">
|
||||||
<div class="filtros-card">
|
<label for="fecha_fin">Fecha de fin:</label>
|
||||||
<h2>📅 Rango de Fechas</h2>
|
<input
|
||||||
<form class="filtros-form" [formGroup]="filtroForm" (ngSubmit)="buscarAutorizaciones()">
|
id="fecha_fin"
|
||||||
<div class="form-row">
|
type="date"
|
||||||
<div class="form-group">
|
formControlName="fecha_fin"
|
||||||
<label for="fecha_inicio">Fecha de inicio:</label>
|
[class.error]="fecha_fin?.invalid && fecha_fin?.touched"
|
||||||
<input
|
(change)="validarFechas()"
|
||||||
id="fecha_inicio"
|
/>
|
||||||
type="date"
|
<div class="error-message" *ngIf="fecha_fin?.invalid && fecha_fin?.touched">
|
||||||
formControlName="fecha_inicio"
|
{{ getFechaFinErrorMessage() }}
|
||||||
[class.error]="fecha_inicio?.invalid && fecha_inicio?.touched"
|
</div>
|
||||||
(change)="validarFechas()"
|
</div>
|
||||||
/>
|
|
||||||
<div class="error-message" *ngIf="fecha_inicio?.invalid && fecha_inicio?.touched">
|
<div class="form-actions">
|
||||||
{{ getFechaInicioErrorMessage() }}
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary 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>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Resultados -->
|
||||||
<label for="fecha_fin">Fecha de fin:</label>
|
<div class="resultados-section card" *ngIf="hayResultados || autorizaciones.length > 0">
|
||||||
<input
|
<div class="resultados-header">
|
||||||
id="fecha_fin"
|
<h2>Resultados ({{ autorizaciones.length }} autorizaciones)</h2>
|
||||||
type="date"
|
<div class="resultados-actions">
|
||||||
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
|
<button
|
||||||
type="submit"
|
class="btn btn-secondary btn-exportar"
|
||||||
class="btn-buscar"
|
(click)="exportarAExcel()"
|
||||||
[disabled]="filtroForm.invalid || isLoading"
|
title="Exportar a Excel"
|
||||||
|
[disabled]="descargandoZip || descargandoPdf"
|
||||||
>
|
>
|
||||||
<span *ngIf="!isLoading">🔍 Buscar</span>
|
Exportar Excel
|
||||||
<span *ngIf="isLoading" class="loading-spinner">
|
</button>
|
||||||
<span class="spinner"></span>
|
<button
|
||||||
Buscando...
|
class="btn btn-secondary btn-descargar-todos"
|
||||||
</span>
|
(click)="descargarTodosLosPdfs()"
|
||||||
|
title="Descargar todos los PDFs"
|
||||||
|
[disabled]="descargandoZip"
|
||||||
|
>
|
||||||
|
Descargar todos los PDFs
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resultados -->
|
<!-- Tabla de resultados -->
|
||||||
<div class="resultados-section" *ngIf="hayResultados || autorizaciones.length > 0">
|
<div class="table-container">
|
||||||
<div class="resultados-header">
|
<table class="table autorizaciones-table">
|
||||||
<h2>📋 Resultados ({{ autorizaciones.length }} autorizaciones)</h2>
|
<thead>
|
||||||
<div class="resultados-actions">
|
<tr>
|
||||||
<button
|
<th>N° autorización</th>
|
||||||
class="btn-exportar"
|
<th>Fecha y hora</th>
|
||||||
(click)="exportarAExcel()"
|
<th>Interno</th>
|
||||||
title="Exportar a Excel"
|
<th>Nombre paciente</th>
|
||||||
>
|
<th>Version</th>
|
||||||
📊 Exportar Excel
|
<th>Tipo autorizacion</th>
|
||||||
</button>
|
<th>Tipo servicio</th>
|
||||||
<button
|
<th>CUPS</th>
|
||||||
class="btn-descargar-todos"
|
<th>Nivel</th>
|
||||||
(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>IPS</th>
|
||||||
<th>Municipio</th>
|
<th>Municipio</th>
|
||||||
<th>Departamento</th>
|
<th>Departamento</th>
|
||||||
<th>Autorizante</th>
|
<th>Autorizante</th>
|
||||||
<th>Establecimiento</th>
|
<th>Establecimiento</th>
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let aut of autorizaciones; let i = index" [class.even-row]="i % 2 === 0">
|
<tr
|
||||||
<td class="numero-autorizacion">
|
*ngFor="let aut of autorizaciones; let i = index; trackBy: trackByAutorizacion"
|
||||||
<strong>{{ aut.numero_autorizacion }}</strong>
|
[class.even-row]="i % 2 === 0"
|
||||||
</td>
|
>
|
||||||
<td class="fecha">{{ formatDateTime(aut.fecha_autorizacion) }}</td>
|
<td class="numero-autorizacion">
|
||||||
<td class="interno">{{ aut.interno }}</td>
|
<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="nombre-paciente">{{ aut.nombre_paciente }}</td>
|
||||||
<td class="ips">{{ aut.nombre_ips }}</td>
|
<td class="version">{{ aut.version || 1 }}</td>
|
||||||
<td class="municipio">{{ aut.municipio }}</td>
|
<td class="tipo-autorizacion">
|
||||||
<td class="departamento">{{ aut.departamento }}</td>
|
{{ getTipoAutorizacionLabel(aut.tipo_autorizacion) }}
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
<td class="tipo-servicio">
|
||||||
</tbody>
|
{{ getTipoServicioLabel(aut.tipo_servicio) }}
|
||||||
</table>
|
</td>
|
||||||
|
<td class="cup-codigo">{{ aut.cup_codigo }}</td>
|
||||||
|
<td class="cup-nivel">{{ aut.cup_nivel }}</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 btn-success btn-sm btn-descargar"
|
||||||
|
(click)="descargarPdf(aut.numero_autorizacion)"
|
||||||
|
title="Descargar PDF"
|
||||||
|
[disabled]="descargandoPdf"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" *ngIf="avisoAutorizaciones">
|
||||||
|
Mostrando hasta {{ limiteAutorizaciones }} autorizaciones.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado vacío -->
|
||||||
|
<div class="empty-state card" *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 class="status-line" *ngIf="descargandoZip">
|
||||||
|
<span class="spinner-inline"></span>
|
||||||
|
Generando ZIP de autorizaciones, por favor espera...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-line" *ngIf="descargandoPdf">
|
||||||
|
<span class="spinner-inline"></span>
|
||||||
|
Generando PDF, por favor espera...
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { Component, OnInit, Inject } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, Inject } from '@angular/core';
|
||||||
import { CommonModule, DOCUMENT } from '@angular/common';
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
import { AuthService } from '../../services/auth';
|
import { AuthService } from '../../services/auth';
|
||||||
|
import { AppHeaderComponent } from '../shared/app-header/app-header';
|
||||||
|
import { JobsService } from '../../services/jobs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-autorizaciones-por-fecha',
|
selector: 'app-autorizaciones-por-fecha',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule],
|
imports: [CommonModule, ReactiveFormsModule, AppHeaderComponent],
|
||||||
templateUrl: './autorizaciones-por-fecha.html',
|
templateUrl: './autorizaciones-por-fecha.html',
|
||||||
styleUrls: ['./autorizaciones-por-fecha.css']
|
styleUrls: ['./autorizaciones-por-fecha.css']
|
||||||
})
|
})
|
||||||
@ -17,6 +19,10 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
errorMessage: string | null = null;
|
errorMessage: string | null = null;
|
||||||
successMessage: string | null = null;
|
successMessage: string | null = null;
|
||||||
|
limiteAutorizaciones = 500;
|
||||||
|
avisoAutorizaciones = false;
|
||||||
|
descargandoZip = false;
|
||||||
|
descargandoPdf = false;
|
||||||
|
|
||||||
// Para saber si ya buscamos algo
|
// Para saber si ya buscamos algo
|
||||||
hayResultados = false;
|
hayResultados = false;
|
||||||
@ -28,6 +34,8 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private jobsService: JobsService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
@Inject(DOCUMENT) private document: Document
|
@Inject(DOCUMENT) private document: Document
|
||||||
) {
|
) {
|
||||||
this.filtroForm = this.fb.group({
|
this.filtroForm = this.fb.group({
|
||||||
@ -79,12 +87,22 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
this.hayResultados = false;
|
this.hayResultados = false;
|
||||||
|
|
||||||
this.authService
|
this.authService
|
||||||
.getAutorizacionesPorFecha(this.fechaInicioApi!, this.fechaFinApi!)
|
.getAutorizacionesPorFecha(
|
||||||
.pipe(finalize(() => (this.isLoading = false)))
|
this.fechaInicioApi!,
|
||||||
|
this.fechaFinApi!,
|
||||||
|
this.limiteAutorizaciones,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
.pipe(finalize(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
this.autorizaciones = data || [];
|
this.autorizaciones = data || [];
|
||||||
this.hayResultados = this.autorizaciones.length > 0;
|
this.hayResultados = this.autorizaciones.length > 0;
|
||||||
|
this.avisoAutorizaciones =
|
||||||
|
this.autorizaciones.length >= this.limiteAutorizaciones;
|
||||||
|
|
||||||
if (!this.hayResultados) {
|
if (!this.hayResultados) {
|
||||||
this.successMessage = 'No hay autorizaciones en ese rango de fechas.';
|
this.successMessage = 'No hay autorizaciones en ese rango de fechas.';
|
||||||
@ -100,10 +118,59 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
|
|
||||||
// ========= PDF INDIVIDUAL =========
|
// ========= PDF INDIVIDUAL =========
|
||||||
descargarPdf(numeroAutorizacion: string): void {
|
descargarPdf(numeroAutorizacion: string): void {
|
||||||
const url = `http://localhost:3000/api/generar-pdf-autorizacion?numero_autorizacion=${encodeURIComponent(
|
this.limpiarMensajes();
|
||||||
numeroAutorizacion
|
this.descargandoPdf = true;
|
||||||
)}`;
|
|
||||||
window.open(url, '_blank');
|
this.authService.crearJobPdfAutorizacion(numeroAutorizacion).subscribe({
|
||||||
|
next: (job) => {
|
||||||
|
this.jobsService.pollJob(job.id).subscribe({
|
||||||
|
next: (estado) => {
|
||||||
|
if (estado.status === 'completed') {
|
||||||
|
const fileName =
|
||||||
|
estado.result?.fileName || `autorizacion_${numeroAutorizacion}.pdf`;
|
||||||
|
|
||||||
|
this.jobsService.downloadJobFile(job.id).subscribe({
|
||||||
|
next: (blob) => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
this.successMessage = 'PDF descargado correctamente.';
|
||||||
|
this.finalizarDescargaPdf();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorMessage = 'Error descargando el PDF.';
|
||||||
|
this.finalizarDescargaPdf();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estado.status === 'failed') {
|
||||||
|
this.errorMessage =
|
||||||
|
estado.error?.message ||
|
||||||
|
'Error generando el PDF de la autorizaci?n.';
|
||||||
|
this.finalizarDescargaPdf();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorMessage = 'Error consultando el estado del PDF.';
|
||||||
|
this.finalizarDescargaPdf();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorMessage = 'Error solicitando la generaci?n del PDF.';
|
||||||
|
this.finalizarDescargaPdf();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========= ZIP CON TODAS LAS AUTORIZACIONES =========
|
// ========= ZIP CON TODAS LAS AUTORIZACIONES =========
|
||||||
@ -116,34 +183,63 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.fechaInicioApi || !this.fechaFinApi) {
|
if (!this.fechaInicioApi || !this.fechaFinApi) {
|
||||||
this.errorMessage = 'Primero realiza una búsqueda por fechas.';
|
this.errorMessage = 'Primero realiza una b?squeda por fechas.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.descargandoZip = true;
|
||||||
|
|
||||||
this.authService
|
this.authService
|
||||||
.descargarAutorizacionesZip(this.fechaInicioApi!, this.fechaFinApi!)
|
.crearJobZipAutorizaciones(this.fechaInicioApi, this.fechaFinApi)
|
||||||
.pipe(finalize(() => (this.isLoading = false)))
|
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (blob: Blob) => {
|
next: (job) => {
|
||||||
const nombreArchivo = `autorizaciones_${this.fechaInicioApi}_${this.fechaFinApi}.zip`;
|
this.jobsService.pollJob(job.id).subscribe({
|
||||||
|
next: (estado) => {
|
||||||
|
if (estado.status === 'completed') {
|
||||||
|
const fileName =
|
||||||
|
estado.result?.fileName ||
|
||||||
|
`autorizaciones_${this.fechaInicioApi}_${this.fechaFinApi}.zip`;
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
this.jobsService.downloadJobFile(job.id).subscribe({
|
||||||
const a = document.createElement('a');
|
next: (blob) => {
|
||||||
a.href = url;
|
const url = window.URL.createObjectURL(blob);
|
||||||
a.download = nombreArchivo;
|
const a = document.createElement('a');
|
||||||
document.body.appendChild(a);
|
a.href = url;
|
||||||
a.click();
|
a.download = fileName;
|
||||||
document.body.removeChild(a);
|
document.body.appendChild(a);
|
||||||
window.URL.revokeObjectURL(url);
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
this.successMessage = 'ZIP descargado correctamente.';
|
this.successMessage = 'ZIP descargado correctamente.';
|
||||||
|
this.finalizarDescargaZip();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorMessage = 'Error descargando el ZIP.';
|
||||||
|
this.finalizarDescargaZip();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estado.status === 'failed') {
|
||||||
|
this.errorMessage =
|
||||||
|
estado.error?.message ||
|
||||||
|
'Error generando el ZIP de autorizaciones.';
|
||||||
|
this.finalizarDescargaZip();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorMessage = 'Error consultando el estado del ZIP.';
|
||||||
|
this.finalizarDescargaZip();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (error) => {
|
||||||
console.error(err);
|
console.error(error);
|
||||||
this.errorMessage =
|
this.errorMessage = 'Error solicitando el ZIP de autorizaciones.';
|
||||||
err?.error?.error || 'Error descargando el ZIP de autorizaciones.';
|
this.finalizarDescargaZip();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -181,6 +277,31 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTipoAutorizacionLabel(tipo: string | null | undefined): string {
|
||||||
|
const normalizado = String(tipo || '').toLowerCase();
|
||||||
|
if (normalizado === 'consultas_externas') {
|
||||||
|
return 'Consultas externas';
|
||||||
|
}
|
||||||
|
if (normalizado === 'brigadas_ambulancias_hospitalarios') {
|
||||||
|
return 'Brigadas/Ambulancias/Hospitalarios';
|
||||||
|
}
|
||||||
|
return tipo ? String(tipo) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getTipoServicioLabel(tipo: string | null | undefined): string {
|
||||||
|
const normalizado = String(tipo || '').toLowerCase();
|
||||||
|
if (normalizado === 'brigadas') {
|
||||||
|
return 'Brigadas';
|
||||||
|
}
|
||||||
|
if (normalizado === 'ambulancias') {
|
||||||
|
return 'Ambulancias';
|
||||||
|
}
|
||||||
|
if (normalizado === 'hospitalarios') {
|
||||||
|
return 'Hospitalarios';
|
||||||
|
}
|
||||||
|
return tipo ? String(tipo) : '';
|
||||||
|
}
|
||||||
|
|
||||||
limpiarMensajes(): void {
|
limpiarMensajes(): void {
|
||||||
this.errorMessage = null;
|
this.errorMessage = null;
|
||||||
this.successMessage = null;
|
this.successMessage = null;
|
||||||
@ -232,10 +353,15 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
'Número Autorización',
|
'Numero Autorizacion',
|
||||||
|
'Version',
|
||||||
'Fecha',
|
'Fecha',
|
||||||
'Interno',
|
'Interno',
|
||||||
'Nombre Paciente',
|
'Nombre Paciente',
|
||||||
|
'Tipo autorizacion',
|
||||||
|
'Tipo servicio',
|
||||||
|
'CUPS',
|
||||||
|
'Nivel',
|
||||||
'IPS',
|
'IPS',
|
||||||
'Municipio',
|
'Municipio',
|
||||||
'Departamento',
|
'Departamento',
|
||||||
@ -243,20 +369,26 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
'Establecimiento'
|
'Establecimiento'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const separator = ';';
|
||||||
const csvContent = [
|
const csvContent = [
|
||||||
headers.join(','),
|
headers.map((header) => this.csvValue(header)).join(separator),
|
||||||
...this.autorizaciones.map((aut) =>
|
...this.autorizaciones.map((aut) =>
|
||||||
[
|
[
|
||||||
aut.numero_autorizacion,
|
this.csvValue(aut.numero_autorizacion),
|
||||||
this.formatDateTime(aut.fecha_autorizacion),
|
this.csvValue(aut.version || ''),
|
||||||
aut.interno,
|
this.csvValue(this.formatDateTime(aut.fecha_autorizacion)),
|
||||||
`"${aut.nombre_paciente}"`,
|
this.csvValue(aut.interno),
|
||||||
`"${aut.nombre_ips}"`,
|
this.csvValue(aut.nombre_paciente),
|
||||||
`"${aut.municipio}"`,
|
this.csvValue(this.getTipoAutorizacionLabel(aut.tipo_autorizacion)),
|
||||||
`"${aut.departamento}"`,
|
this.csvValue(this.getTipoServicioLabel(aut.tipo_servicio)),
|
||||||
`"${aut.nombre_autorizante}"`,
|
this.csvValue(aut.cup_codigo || ''),
|
||||||
`"${aut.nombre_establecimiento}"`
|
this.csvValue(aut.cup_nivel || ''),
|
||||||
].join(',')
|
this.csvValue(aut.nombre_ips),
|
||||||
|
this.csvValue(aut.municipio),
|
||||||
|
this.csvValue(aut.departamento),
|
||||||
|
this.csvValue(aut.nombre_autorizante),
|
||||||
|
this.csvValue(aut.nombre_establecimiento)
|
||||||
|
].join(separator)
|
||||||
)
|
)
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@ -277,4 +409,24 @@ export class AutorizacionesPorFechaComponent implements OnInit {
|
|||||||
|
|
||||||
this.successMessage = 'Archivo CSV exportado correctamente.';
|
this.successMessage = 'Archivo CSV exportado correctamente.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private finalizarDescargaPdf(): void {
|
||||||
|
this.descargandoPdf = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private finalizarDescargaZip(): void {
|
||||||
|
this.descargandoZip = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private csvValue(value: unknown): string {
|
||||||
|
const raw = value === null || value === undefined ? '' : String(value);
|
||||||
|
const escaped = raw.replace(/"/g, '""');
|
||||||
|
return `"${escaped}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByAutorizacion(_index: number, item: any): string {
|
||||||
|
return item?.numero_autorizacion ?? _index.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,351 @@
|
|||||||
|
/* ============================
|
||||||
|
Estilos del módulo de autorizaciones
|
||||||
|
============================ */
|
||||||
|
.search-card {
|
||||||
|
margin: 20px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 6px;
|
||||||
|
border: 1px solid var(--color-input-border);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--color-input-bg);
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-row .cup-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-row .cup-input input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-results {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-results label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-results {
|
||||||
|
background: var(--color-cup-bg);
|
||||||
|
border: 1px solid var(--color-cup-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-results-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-results-count {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-results-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-item {
|
||||||
|
border: 1px solid var(--color-cup-item-border);
|
||||||
|
background: var(--color-cup-item-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-item:hover {
|
||||||
|
border-color: var(--color-cup-item-hover);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-item.selected {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-code {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-desc {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cup-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-field select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-toggle {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-helper,
|
||||||
|
.ips-traslado,
|
||||||
|
.edit-info {
|
||||||
|
margin: -6px 0 12px 142px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-traslado {
|
||||||
|
color: #1f4f9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-stack select {
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d7dbe5;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accion-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultados-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-tabla {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabla td button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 td,
|
||||||
|
.tabla-autorizaciones th {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-restricted {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Modal de autorizacion === */
|
||||||
|
.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: 22px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aut-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ips-helper,
|
||||||
|
.ips-traslado,
|
||||||
|
.edit-info {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content-container {
|
||||||
|
padding: 0 8px 20px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,184 +1,230 @@
|
|||||||
<div class="container">
|
<div class="page-shell">
|
||||||
<!-- Header con logo SALUD UT -->
|
<div class="content-container">
|
||||||
<header class="app-header">
|
<app-header
|
||||||
<div class="header-left">
|
[title]="titulo"
|
||||||
<img
|
subtitle="Módulo de autorizaciones médicas"
|
||||||
src="logo_SALUDUT.png"
|
badgeText="SALUD UT"
|
||||||
alt="SALUD UT"
|
[showUserInfo]="isLoggedIn()"
|
||||||
class="logo-saludut"
|
[userName]="getCurrentUser()?.nombre_completo"
|
||||||
/>
|
[userRole]="getCurrentUser()?.nombre_rol"
|
||||||
<div class="header-text">
|
[showLogout]="isLoggedIn()"
|
||||||
<h1>{{ titulo }}</h1>
|
(logout)="logout()"
|
||||||
<p class="subtitle">Módulo de autorizaciones médicas</p>
|
></app-header>
|
||||||
</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>
|
|
||||||
|
|
||||||
|
<!-- Tarjeta de búsqueda -->
|
||||||
|
<div class="search-card card">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="ips">IPS / Hospital:</label>
|
<label for="tipo">Buscar por:</label>
|
||||||
<select id="ips" [(ngModel)]="formAutorizacion.id_ips">
|
<select id="tipo" [(ngModel)]="tipoBusqueda">
|
||||||
<option value="">-- Seleccione IPS --</option>
|
<option value="documento">Cédula / Documento</option>
|
||||||
<option *ngFor="let ips of ipsDisponibles" [value]="ips.id_ips">
|
<option value="interno">Número interno</option>
|
||||||
{{ ips.nombre_ips }} ({{ ips.municipio }} - {{ ips.departamento }})
|
<option value="nombre">Nombre completo</option>
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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 class="btn btn-primary" (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"
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="inputExcel.click()"
|
||||||
|
[disabled]="cargandoExcel"
|
||||||
|
>
|
||||||
|
{{ cargandoExcel ? 'Cargando Excel...' : 'Cargar Excel de PPL' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#inputExcel
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
(change)="onExcelSelected($event)"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="upload-msg" *ngIf="estadoCargaExcel">
|
||||||
|
{{ estadoCargaExcel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de ver autorizaciones por fecha (solo administradores) -->
|
||||||
|
<div class="admin-actions" *ngIf="puedeVerTodasAutorizaciones()">
|
||||||
|
<button
|
||||||
|
class="btn btn-gradient"
|
||||||
|
(click)="irAAutorizacionesPorFecha()"
|
||||||
|
title="Ver autorizaciones por fecha"
|
||||||
|
>
|
||||||
|
Ver autorizaciones por fecha
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
(click)="irADashboard()"
|
||||||
|
title="Volver al dashboard"
|
||||||
|
>
|
||||||
|
Volver al dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status 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="table 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
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
(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()"
|
||||||
|
aria-label="Cerrar"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2>{{ autorizacionEditando ? 'Editar autorizacion' : 'Nueva autorizacion' }}</h2>
|
||||||
|
<p class="edit-info" *ngIf="autorizacionEditando">
|
||||||
|
Editando {{ autorizacionEditando.numero_autorizacion }} (v{{ autorizacionEditando.version || 1 }})
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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="tipoAutorizacion">Tipo de autorizacion:</label>
|
||||||
|
<select
|
||||||
|
id="tipoAutorizacion"
|
||||||
|
[(ngModel)]="formAutorizacion.tipo_autorizacion"
|
||||||
|
(change)="onTipoAutorizacionChange()"
|
||||||
|
>
|
||||||
|
<option value="consultas_externas">Consultas externas</option>
|
||||||
|
<option value="brigadas_ambulancias_hospitalarios">
|
||||||
|
Brigadas/Ambulancias/Hospitalarios
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="form-row"
|
||||||
|
*ngIf="
|
||||||
|
formAutorizacion.tipo_autorizacion ===
|
||||||
|
'brigadas_ambulancias_hospitalarios'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<label for="tipoServicio">Tipo de servicio:</label>
|
||||||
|
<select id="tipoServicio" [(ngModel)]="formAutorizacion.tipo_servicio">
|
||||||
|
<option value="">-- Seleccione --</option>
|
||||||
|
<option value="brigadas">Brigadas</option>
|
||||||
|
<option value="ambulancias">Ambulancias</option>
|
||||||
|
<option value="hospitalarios">Hospitalarios</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="ips">IPS / Hospital:</label>
|
||||||
|
<div class="ips-field">
|
||||||
|
<select
|
||||||
|
id="ips"
|
||||||
|
[(ngModel)]="formAutorizacion.id_ips"
|
||||||
|
(change)="onIpsChange()"
|
||||||
|
>
|
||||||
|
<option value="">-- Seleccione IPS --</option>
|
||||||
|
<option *ngFor="let ips of ipsDisponibles" [value]="ips.id_ips">
|
||||||
|
{{ ips.nombre_ips }} ({{ ips.municipio }} - {{ ips.departamento }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm ips-toggle"
|
||||||
|
(click)="toggleVerMasIps()"
|
||||||
|
>
|
||||||
|
{{ verMasIps ? 'Ver solo cercanas' : 'Ver mas sedes' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ips-helper" *ngIf="verMasIps">
|
||||||
|
Mostrando IPS de todos los departamentos.
|
||||||
|
</div>
|
||||||
|
<div class="ips-traslado" *ngIf="observacionTraslado">
|
||||||
|
Se agregara a observaciones: {{ observacionTraslado }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="autorizante">Quién autoriza:</label>
|
<label for="autorizante">Quién autoriza:</label>
|
||||||
<select
|
<select
|
||||||
@ -186,15 +232,57 @@
|
|||||||
[(ngModel)]="formAutorizacion.numero_documento_autorizante"
|
[(ngModel)]="formAutorizacion.numero_documento_autorizante"
|
||||||
>
|
>
|
||||||
<option value="">-- Seleccione --</option>
|
<option value="">-- Seleccione --</option>
|
||||||
<option
|
<option *ngFor="let a of autorizantes" [value]="a.numero_documento">
|
||||||
*ngFor="let a of autorizantes"
|
{{ a.nombre }} ({{ a.cargo }})
|
||||||
[value]="a.numero_documento"
|
|
||||||
>
|
|
||||||
{{ a.nombre }} ({{ a.cargo }})
|
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row cup-row">
|
||||||
|
<label for="cupCodigo">CUPS:</label>
|
||||||
|
<div class="cup-input">
|
||||||
|
<input
|
||||||
|
id="cupCodigo"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="formAutorizacion.cup_codigo"
|
||||||
|
placeholder="Codigo o descripcion"
|
||||||
|
(keyup.enter)="buscarCups()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
(click)="buscarCups()"
|
||||||
|
[disabled]="buscandoCups"
|
||||||
|
>
|
||||||
|
{{ buscandoCups ? 'Buscando...' : 'Buscar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cup-results" *ngIf="cupsDisponibles.length > 0">
|
||||||
|
<div class="cup-results-header">
|
||||||
|
<span>Selecciona un CUPS:</span>
|
||||||
|
<span class="cup-results-count">{{ cupsDisponibles.length }} resultados</span>
|
||||||
|
</div>
|
||||||
|
<div class="cup-results-list">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cup-item"
|
||||||
|
*ngFor="let cup of cupsDisponibles"
|
||||||
|
[class.selected]="cup.codigo === formAutorizacion.cup_codigo"
|
||||||
|
(click)="seleccionarCup(cup)"
|
||||||
|
>
|
||||||
|
<div class="cup-code">{{ cup.codigo }}</div>
|
||||||
|
<div class="cup-desc">{{ cup.descripcion }}</div>
|
||||||
|
<div class="cup-meta" *ngIf="cup.nivel">Nivel {{ cup.nivel }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status error" *ngIf="errorCups">
|
||||||
|
{{ errorCups }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="fechaAut">Fecha autorización:</label>
|
<label for="fechaAut">Fecha autorización:</label>
|
||||||
<input
|
<input
|
||||||
@ -204,82 +292,142 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="obs">Observación / servicios autorizados:</label>
|
<label for="obs">Observación / servicios autorizados:</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="obs"
|
id="obs"
|
||||||
rows="4"
|
rows="4"
|
||||||
[(ngModel)]="formAutorizacion.observacion"
|
[(ngModel)]="formAutorizacion.observacion"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="aut-actions">
|
<div class="aut-actions">
|
||||||
<button
|
<button
|
||||||
(click)="guardarAutorizacion()"
|
class="btn btn-primary"
|
||||||
[disabled]="guardandoAutorizacion || !puedeGenerarAutorizaciones()"
|
(click)="guardarAutorizacion()"
|
||||||
|
[disabled]="guardandoAutorizacion || !puedeGenerarAutorizaciones()"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
guardandoAutorizacion
|
||||||
|
? 'Guardando...'
|
||||||
|
: (autorizacionEditando ? 'Guardar cambios' : 'Guardar autorizacion')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
*ngIf="autorizacionEditando"
|
||||||
|
(click)="cancelarEdicion()"
|
||||||
|
>
|
||||||
|
Cancelar edicion
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
*ngIf="pacienteSeleccionado"
|
||||||
|
(click)="verAutorizaciones()"
|
||||||
|
>
|
||||||
|
Ver autorizaciones
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status ok" *ngIf="mensajeAutorizacion">
|
||||||
|
{{ mensajeAutorizacion }}
|
||||||
|
</div>
|
||||||
|
<div class="status error" *ngIf="errorAutorizacion">
|
||||||
|
{{ errorAutorizacion }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="autorizaciones-previas"
|
||||||
|
*ngIf="pacienteSeleccionado && autorizacionesPaciente.length > 0"
|
||||||
>
|
>
|
||||||
{{ guardandoAutorizacion ? 'Guardando...' : 'Guardar autorización' }}
|
<h3>Autorizaciones previas</h3>
|
||||||
</button>
|
<table class="table tabla-autorizaciones">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>N° autorización</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>CUPS</th>
|
||||||
|
<th>Nivel</th>
|
||||||
|
<th>Tipo autorizacion</th>
|
||||||
|
<th>Tipo servicio</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>IPS</th>
|
||||||
|
<th>Autoriza</th>
|
||||||
|
<th>Acciones</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.cup_codigo }}</td>
|
||||||
|
<td>{{ a.cup_nivel }}</td>
|
||||||
|
<td>{{ getTipoAutorizacionLabel(a.tipo_autorizacion) }}</td>
|
||||||
|
<td>{{ getTipoServicioLabel(a.tipo_servicio) }}</td>
|
||||||
|
<td class="version-cell">
|
||||||
|
<div class="version-stack">
|
||||||
|
<span>v{{ a.version || 1 }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-link"
|
||||||
|
*ngIf="!versionesPorAutorizacion[a.numero_autorizacion] && !cargandoVersiones[a.numero_autorizacion]"
|
||||||
|
(click)="cargarVersiones(a.numero_autorizacion)"
|
||||||
|
>
|
||||||
|
Ver versiones
|
||||||
|
</button>
|
||||||
|
<span *ngIf="cargandoVersiones[a.numero_autorizacion]">Cargando...</span>
|
||||||
|
<select
|
||||||
|
*ngIf="versionesPorAutorizacion[a.numero_autorizacion]"
|
||||||
|
[(ngModel)]="versionSeleccionada[a.numero_autorizacion]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
*ngFor="let v of versionesPorAutorizacion[a.numero_autorizacion]"
|
||||||
|
[ngValue]="v.version"
|
||||||
|
>
|
||||||
|
v{{ v.version }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ a.nombre_ips }}</td>
|
||||||
|
<td>{{ a.nombre_autorizante }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="accion-buttons">
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
[disabled]="!puedeGenerarAutorizaciones()"
|
||||||
|
(click)="iniciarEdicion(a)"
|
||||||
|
>
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="puedeDescargarPdfs()"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
[disabled]="descargandoPdf"
|
||||||
|
(click)="descargarPdf(a.numero_autorizacion, getVersionSeleccionada(a.numero_autorizacion, a.version))"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
|
<span *ngIf="!puedeDescargarPdfs()" class="pdf-restricted">
|
||||||
|
Solo admin
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<button
|
<div class="status" *ngIf="descargandoPdf">
|
||||||
type="button"
|
Generando y descargando PDF, por favor espera...
|
||||||
class="btn-secondary"
|
</div>
|
||||||
*ngIf="pacienteSeleccionado"
|
|
||||||
(click)="verAutorizaciones()"
|
|
||||||
>
|
|
||||||
Ver autorizaciones
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="estado ok" *ngIf="mensajeAutorizacion">
|
<div class="status error" *ngIf="errorAutLista">
|
||||||
{{ mensajeAutorizacion }}
|
{{ errorAutLista }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
import { Component, ChangeDetectorRef } from '@angular/core';
|
||||||
import { Router, NavigationEnd } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
import { AuthService } from '../../services/auth';
|
import { AuthService } from '../../services/auth';
|
||||||
import { PacienteService } from '../../services/paciente';
|
import { PacienteService, AutorizacionVersion } from '../../services/paciente';
|
||||||
import { finalize, filter } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
import { AppHeaderComponent } from '../shared/app-header/app-header';
|
||||||
|
import { JobsService } from '../../services/jobs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-autorizaciones',
|
selector: 'app-autorizaciones',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, RouterOutlet],
|
imports: [CommonModule, FormsModule, AppHeaderComponent],
|
||||||
templateUrl: './autorizaciones.html',
|
templateUrl: './autorizaciones.html',
|
||||||
styleUrls: ['./autorizaciones.css']
|
styleUrls: ['./autorizaciones.css']
|
||||||
})
|
})
|
||||||
export class AutorizacionesComponent implements OnInit {
|
export class AutorizacionesComponent {
|
||||||
title = 'Consulta PPL INPEC';
|
|
||||||
titulo = 'Consulta PPL INPEC';
|
titulo = 'Consulta PPL INPEC';
|
||||||
|
|
||||||
// ---- Búsqueda ----
|
// ---- Búsqueda ----
|
||||||
@ -34,12 +34,27 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
pacienteSeleccionado: any = null;
|
pacienteSeleccionado: any = null;
|
||||||
ipsDisponibles: any[] = [];
|
ipsDisponibles: any[] = [];
|
||||||
autorizantes: any[] = [];
|
autorizantes: any[] = [];
|
||||||
|
cupsDisponibles: any[] = [];
|
||||||
|
buscandoCups = false;
|
||||||
|
errorCups: string | null = null;
|
||||||
|
verMasIps = false;
|
||||||
|
departamentoInterno = '';
|
||||||
|
observacionTraslado = '';
|
||||||
|
autorizacionEditando: any | null = null;
|
||||||
|
|
||||||
|
versionesPorAutorizacion: Record<string, AutorizacionVersion[]> = {};
|
||||||
|
versionActualPorAutorizacion: Record<string, number> = {};
|
||||||
|
versionSeleccionada: Record<string, number> = {};
|
||||||
|
cargandoVersiones: Record<string, boolean> = {};
|
||||||
|
|
||||||
formAutorizacion = {
|
formAutorizacion = {
|
||||||
id_ips: '',
|
id_ips: '',
|
||||||
numero_documento_autorizante: '',
|
numero_documento_autorizante: '',
|
||||||
fecha_autorizacion: '',
|
fecha_autorizacion: '',
|
||||||
observacion: '',
|
observacion: '',
|
||||||
|
cup_codigo: '',
|
||||||
|
tipo_autorizacion: 'consultas_externas',
|
||||||
|
tipo_servicio: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
guardandoAutorizacion = false;
|
guardandoAutorizacion = false;
|
||||||
@ -50,39 +65,16 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
cargandoAutorizaciones = false;
|
cargandoAutorizaciones = false;
|
||||||
errorAutLista: string | null = null;
|
errorAutLista: string | null = null;
|
||||||
|
|
||||||
showMainContent = false;
|
|
||||||
esBusquedaPacientes = false;
|
|
||||||
|
|
||||||
descargandoPdf = false;
|
descargandoPdf = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private pacienteService: PacienteService,
|
private pacienteService: PacienteService,
|
||||||
|
private jobsService: JobsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cdr: ChangeDetectorRef
|
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
|
// Búsqueda de pacientes
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@ -111,7 +103,8 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
obs$ = this.pacienteService.buscarPorNombre(valor);
|
obs$ = this.pacienteService.buscarPorNombre(valor);
|
||||||
}
|
}
|
||||||
|
|
||||||
obs$ .pipe(
|
obs$
|
||||||
|
.pipe(
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.cargando = false;
|
this.cargando = false;
|
||||||
this.cdr.markForCheck();
|
this.cdr.markForCheck();
|
||||||
@ -141,36 +134,60 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.cargandoExcel = true;
|
this.cargandoExcel = true;
|
||||||
this.estadoCargaExcel = 'Subiendo y procesando archivo...';
|
this.estadoCargaExcel = 'Subiendo archivo...';
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('archivo', file); // 👈 debe llamarse igual que en upload.single('archivo')
|
formData.append('archivo', file);
|
||||||
|
|
||||||
this.pacienteService.cargarExcelPacientes(formData).subscribe({
|
this.pacienteService.cargarExcelPacientes(formData).subscribe({
|
||||||
next: (resp) => {
|
next: (job) => {
|
||||||
this.cargandoExcel = false;
|
this.estadoCargaExcel = 'Archivo en cola. Procesando...';
|
||||||
|
|
||||||
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) {
|
if (input) {
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.jobsService.pollJob(job.id).subscribe({
|
||||||
|
next: (estado) => {
|
||||||
|
if (estado.status === 'completed') {
|
||||||
|
const partes: string[] = [];
|
||||||
|
if (estado.result?.mensaje) partes.push(estado.result.mensaje);
|
||||||
|
if (typeof estado.result?.activos === 'number') {
|
||||||
|
partes.push(`Pacientes activos: ${estado.result.activos}`);
|
||||||
|
}
|
||||||
|
if (typeof estado.result?.antiguos === 'number') {
|
||||||
|
partes.push(`Pacientes antiguos: ${estado.result.antiguos}`);
|
||||||
|
}
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
partes.join(' - ') || 'Archivo procesado correctamente.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estado.status === 'failed') {
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
estado.error?.message ||
|
||||||
|
'Error procesando el Excel de pacientes.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
}
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
'Error consultando el estado del procesamiento.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.cargandoExcel = false;
|
|
||||||
this.estadoCargaExcel =
|
this.estadoCargaExcel =
|
||||||
err?.error?.error || 'Error procesando el Excel de pacientes.';
|
err?.error?.error || 'Error subiendo el Excel de pacientes.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,18 +198,31 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
this.pacienteSeleccionado = p;
|
this.pacienteSeleccionado = p;
|
||||||
this.mensajeAutorizacion = null;
|
this.mensajeAutorizacion = null;
|
||||||
this.errorAutorizacion = null;
|
this.errorAutorizacion = null;
|
||||||
|
this.autorizacionEditando = null;
|
||||||
|
this.verMasIps = false;
|
||||||
|
this.departamentoInterno = String(p?.epc_departamento || '').trim();
|
||||||
|
this.observacionTraslado = '';
|
||||||
|
|
||||||
this.formAutorizacion = {
|
this.formAutorizacion = {
|
||||||
id_ips: '',
|
id_ips: '',
|
||||||
numero_documento_autorizante: '',
|
numero_documento_autorizante: '',
|
||||||
fecha_autorizacion: '',
|
fecha_autorizacion: '',
|
||||||
observacion: '',
|
observacion: '',
|
||||||
|
cup_codigo: '',
|
||||||
|
tipo_autorizacion: 'consultas_externas',
|
||||||
|
tipo_servicio: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.autorizacionesPaciente = [];
|
this.autorizacionesPaciente = [];
|
||||||
|
this.versionesPorAutorizacion = {};
|
||||||
|
this.versionActualPorAutorizacion = {};
|
||||||
|
this.versionSeleccionada = {};
|
||||||
|
this.cargandoVersiones = {};
|
||||||
this.errorAutLista = null;
|
this.errorAutLista = null;
|
||||||
|
this.cupsDisponibles = [];
|
||||||
|
this.errorCups = null;
|
||||||
|
|
||||||
this.cargarIps(p.interno);
|
this.cargarIps(p.interno, this.verMasIps);
|
||||||
this.cargarAutorizantes();
|
this.cargarAutorizantes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,13 +232,23 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
this.mensajeAutorizacion = null;
|
this.mensajeAutorizacion = null;
|
||||||
this.errorAutorizacion = null;
|
this.errorAutorizacion = null;
|
||||||
this.errorAutLista = null;
|
this.errorAutLista = null;
|
||||||
|
this.cupsDisponibles = [];
|
||||||
|
this.errorCups = null;
|
||||||
|
this.verMasIps = false;
|
||||||
|
this.departamentoInterno = '';
|
||||||
|
this.observacionTraslado = '';
|
||||||
|
this.autorizacionEditando = null;
|
||||||
|
this.versionesPorAutorizacion = {};
|
||||||
|
this.versionActualPorAutorizacion = {};
|
||||||
|
this.versionSeleccionada = {};
|
||||||
|
this.cargandoVersiones = {};
|
||||||
this.cdr.markForCheck();
|
this.cdr.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
private cargarIps(interno: string): void {
|
private cargarIps(interno: string, verTodas = false): void {
|
||||||
this.ipsDisponibles = [];
|
this.ipsDisponibles = [];
|
||||||
this.pacienteService
|
this.pacienteService
|
||||||
.obtenerIpsPorInterno(interno)
|
.obtenerIpsPorInterno(interno, verTodas)
|
||||||
.pipe(
|
.pipe(
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.cdr.markForCheck();
|
this.cdr.markForCheck();
|
||||||
@ -217,6 +257,19 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (data: any[]) => {
|
next: (data: any[]) => {
|
||||||
this.ipsDisponibles = data;
|
this.ipsDisponibles = data;
|
||||||
|
if (
|
||||||
|
this.autorizacionEditando &&
|
||||||
|
this.formAutorizacion.id_ips &&
|
||||||
|
!this.ipsDisponibles.some(
|
||||||
|
(ips) => String(ips.id_ips) === String(this.formAutorizacion.id_ips)
|
||||||
|
) &&
|
||||||
|
!this.verMasIps
|
||||||
|
) {
|
||||||
|
this.verMasIps = true;
|
||||||
|
this.cargarIps(interno, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onIpsChange();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -247,9 +300,181 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleVerMasIps(): void {
|
||||||
|
if (!this.pacienteSeleccionado) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.verMasIps = !this.verMasIps;
|
||||||
|
this.cargarIps(this.pacienteSeleccionado.interno, this.verMasIps);
|
||||||
|
}
|
||||||
|
|
||||||
|
onIpsChange(): void {
|
||||||
|
const idSeleccionado = String(this.formAutorizacion.id_ips || '');
|
||||||
|
const ipsSeleccionada = this.ipsDisponibles.find(
|
||||||
|
(ips) => String(ips.id_ips) === idSeleccionado
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ipsSeleccionada) {
|
||||||
|
this.observacionTraslado = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deptoIps = String(ipsSeleccionada.departamento || '').trim();
|
||||||
|
const deptoInterno = String(this.departamentoInterno || '').trim();
|
||||||
|
const requiereTraslado =
|
||||||
|
this.verMasIps &&
|
||||||
|
deptoIps &&
|
||||||
|
deptoInterno &&
|
||||||
|
deptoIps.toLowerCase() !== deptoInterno.toLowerCase();
|
||||||
|
|
||||||
|
this.observacionTraslado = requiereTraslado
|
||||||
|
? `Traslado a departamento ${deptoIps}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarCups(): void {
|
||||||
|
const termino = String(this.formAutorizacion.cup_codigo || '').trim();
|
||||||
|
if (!termino) {
|
||||||
|
this.cupsDisponibles = [];
|
||||||
|
this.errorCups = 'Ingresa un código o descripción para buscar.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buscandoCups = true;
|
||||||
|
this.errorCups = null;
|
||||||
|
|
||||||
|
this.pacienteService
|
||||||
|
.buscarCupsCubiertos(termino)
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.buscandoCups = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any[]) => {
|
||||||
|
this.cupsDisponibles = data || [];
|
||||||
|
if (this.cupsDisponibles.length === 0) {
|
||||||
|
this.errorCups = 'No se encontraron CUPS con ese criterio.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorCups = 'Error consultando CUPS cubiertos.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
seleccionarCup(cup: any): void {
|
||||||
|
if (!cup) return;
|
||||||
|
this.formAutorizacion.cup_codigo = cup.codigo;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Guardar autorización
|
// Guardar autorización
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
onTipoAutorizacionChange(): void {
|
||||||
|
if (
|
||||||
|
this.formAutorizacion.tipo_autorizacion !==
|
||||||
|
'brigadas_ambulancias_hospitalarios'
|
||||||
|
) {
|
||||||
|
this.formAutorizacion.tipo_servicio = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTipoAutorizacionLabel(tipo: string | null | undefined): string {
|
||||||
|
const normalizado = String(tipo || '').toLowerCase();
|
||||||
|
if (normalizado === 'consultas_externas') {
|
||||||
|
return 'Consultas externas';
|
||||||
|
}
|
||||||
|
if (normalizado === 'brigadas_ambulancias_hospitalarios') {
|
||||||
|
return 'Brigadas/Ambulancias/Hospitalarios';
|
||||||
|
}
|
||||||
|
return tipo ? String(tipo) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getTipoServicioLabel(tipo: string | null | undefined): string {
|
||||||
|
const normalizado = String(tipo || '').toLowerCase();
|
||||||
|
if (normalizado === 'brigadas') {
|
||||||
|
return 'Brigadas';
|
||||||
|
}
|
||||||
|
if (normalizado === 'ambulancias') {
|
||||||
|
return 'Ambulancias';
|
||||||
|
}
|
||||||
|
if (normalizado === 'hospitalarios') {
|
||||||
|
return 'Hospitalarios';
|
||||||
|
}
|
||||||
|
return tipo ? String(tipo) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildObservacionFinal(): string | undefined {
|
||||||
|
const base = String(this.formAutorizacion.observacion || '').trim();
|
||||||
|
const trasladoTexto = String(this.observacionTraslado || '').trim();
|
||||||
|
const trasladoRegex = /\s*\|\s*Traslado a departamento[^|]*$/i;
|
||||||
|
let baseSinTraslado = base;
|
||||||
|
|
||||||
|
if (trasladoRegex.test(baseSinTraslado)) {
|
||||||
|
baseSinTraslado = baseSinTraslado.replace(trasladoRegex, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trasladoTexto) {
|
||||||
|
return baseSinTraslado || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yaIncluye =
|
||||||
|
baseSinTraslado.toLowerCase().includes(trasladoTexto.toLowerCase());
|
||||||
|
if (yaIncluye) {
|
||||||
|
return baseSinTraslado || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [baseSinTraslado, trasladoTexto].filter(Boolean).join(' | ') || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDateInput(value: string | null | undefined): string {
|
||||||
|
if (!value) return '';
|
||||||
|
return value.substring(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
iniciarEdicion(autorizacion: any): void {
|
||||||
|
if (!autorizacion) return;
|
||||||
|
|
||||||
|
this.autorizacionEditando = autorizacion;
|
||||||
|
this.mensajeAutorizacion = null;
|
||||||
|
this.errorAutorizacion = null;
|
||||||
|
|
||||||
|
this.formAutorizacion = {
|
||||||
|
id_ips: String(autorizacion.id_ips || ''),
|
||||||
|
numero_documento_autorizante: String(
|
||||||
|
autorizacion.numero_documento_autorizante || ''
|
||||||
|
),
|
||||||
|
fecha_autorizacion: this.formatDateInput(autorizacion.fecha_autorizacion),
|
||||||
|
observacion: autorizacion.observacion || '',
|
||||||
|
cup_codigo: autorizacion.cup_codigo || '',
|
||||||
|
tipo_autorizacion: autorizacion.tipo_autorizacion || 'consultas_externas',
|
||||||
|
tipo_servicio: autorizacion.tipo_servicio || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onTipoAutorizacionChange();
|
||||||
|
this.onIpsChange();
|
||||||
|
if (this.pacienteSeleccionado) {
|
||||||
|
this.cargarIps(this.pacienteSeleccionado.interno, this.verMasIps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelarEdicion(): void {
|
||||||
|
this.autorizacionEditando = null;
|
||||||
|
this.observacionTraslado = '';
|
||||||
|
this.formAutorizacion = {
|
||||||
|
id_ips: '',
|
||||||
|
numero_documento_autorizante: '',
|
||||||
|
fecha_autorizacion: '',
|
||||||
|
observacion: '',
|
||||||
|
cup_codigo: '',
|
||||||
|
tipo_autorizacion: 'consultas_externas',
|
||||||
|
tipo_servicio: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
guardarAutorizacion(): void {
|
guardarAutorizacion(): void {
|
||||||
if (!this.pacienteSeleccionado) {
|
if (!this.pacienteSeleccionado) {
|
||||||
return;
|
return;
|
||||||
@ -266,6 +491,24 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
this.errorAutorizacion = 'Debe seleccionar quién autoriza.';
|
this.errorAutorizacion = 'Debe seleccionar quién autoriza.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.formAutorizacion.cup_codigo) {
|
||||||
|
this.errorAutorizacion = 'Debe seleccionar un CUPS.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tipoAutorizacion = String(
|
||||||
|
this.formAutorizacion.tipo_autorizacion || 'consultas_externas'
|
||||||
|
).toLowerCase();
|
||||||
|
const requiereServicio =
|
||||||
|
tipoAutorizacion === 'brigadas_ambulancias_hospitalarios';
|
||||||
|
const tipoServicio = String(
|
||||||
|
this.formAutorizacion.tipo_servicio || ''
|
||||||
|
).trim().toLowerCase();
|
||||||
|
|
||||||
|
if (requiereServicio && !tipoServicio) {
|
||||||
|
this.errorAutorizacion = 'Debe seleccionar un tipo de servicio.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
interno: this.pacienteSeleccionado.interno,
|
interno: this.pacienteSeleccionado.interno,
|
||||||
@ -273,15 +516,24 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
numero_documento_autorizante: Number(
|
numero_documento_autorizante: Number(
|
||||||
this.formAutorizacion.numero_documento_autorizante
|
this.formAutorizacion.numero_documento_autorizante
|
||||||
),
|
),
|
||||||
observacion: this.formAutorizacion.observacion,
|
observacion: this.buildObservacionFinal(),
|
||||||
fecha_autorizacion:
|
fecha_autorizacion:
|
||||||
this.formAutorizacion.fecha_autorizacion || undefined,
|
this.formAutorizacion.fecha_autorizacion || undefined,
|
||||||
|
cup_codigo: this.formAutorizacion.cup_codigo,
|
||||||
|
tipo_autorizacion: tipoAutorizacion,
|
||||||
|
tipo_servicio: requiereServicio ? tipoServicio : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.guardandoAutorizacion = true;
|
this.guardandoAutorizacion = true;
|
||||||
|
|
||||||
this.pacienteService
|
const request$ = this.autorizacionEditando
|
||||||
.crearAutorizacion(payload)
|
? this.pacienteService.actualizarAutorizacion(
|
||||||
|
this.autorizacionEditando.numero_autorizacion,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
: this.pacienteService.crearAutorizacion(payload);
|
||||||
|
|
||||||
|
request$
|
||||||
.pipe(
|
.pipe(
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.guardandoAutorizacion = false;
|
this.guardandoAutorizacion = false;
|
||||||
@ -290,16 +542,61 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (resp: any) => {
|
next: (resp: any) => {
|
||||||
this.mensajeAutorizacion = `Autorización N° ${resp.numero_autorizacion} creada el ${resp.fecha_autorizacion}.`;
|
if (this.autorizacionEditando) {
|
||||||
|
const versionTexto = resp?.version ? ` (v${resp.version})` : '';
|
||||||
|
this.mensajeAutorizacion = `Autorizacion ${resp.numero_autorizacion} actualizada${versionTexto}.`;
|
||||||
|
this.autorizacionEditando = null;
|
||||||
|
this.verAutorizaciones();
|
||||||
|
} else {
|
||||||
|
this.mensajeAutorizacion = `Autorizacion N ${resp.numero_autorizacion} creada el ${resp.fecha_autorizacion}.`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.errorAutorizacion =
|
this.errorAutorizacion =
|
||||||
err.error?.error || 'Error guardando la autorización.';
|
err.error?.error || 'Error guardando la autorizacion.';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cargarVersiones(numeroAutorizacion: string): void {
|
||||||
|
if (this.cargandoVersiones[numeroAutorizacion]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cargandoVersiones[numeroAutorizacion] = true;
|
||||||
|
|
||||||
|
this.pacienteService.obtenerVersionesAutorizacion(numeroAutorizacion).subscribe({
|
||||||
|
next: (resp) => {
|
||||||
|
const versiones = resp?.versiones || [];
|
||||||
|
const versionActual = resp?.version_actual || 1;
|
||||||
|
|
||||||
|
this.versionesPorAutorizacion[numeroAutorizacion] = versiones;
|
||||||
|
this.versionActualPorAutorizacion[numeroAutorizacion] = versionActual;
|
||||||
|
this.versionSeleccionada[numeroAutorizacion] =
|
||||||
|
this.versionSeleccionada[numeroAutorizacion] || versionActual;
|
||||||
|
this.cargandoVersiones[numeroAutorizacion] = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.cargandoVersiones[numeroAutorizacion] = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersionSeleccionada(numeroAutorizacion: string, fallback?: number | null): number | null {
|
||||||
|
const seleccion = this.versionSeleccionada[numeroAutorizacion];
|
||||||
|
if (typeof seleccion === 'number') {
|
||||||
|
return seleccion;
|
||||||
|
}
|
||||||
|
if (typeof fallback === 'number') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Ver autorizaciones del interno
|
// Ver autorizaciones del interno
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@ -309,6 +606,10 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
this.cargandoAutorizaciones = true;
|
this.cargandoAutorizaciones = true;
|
||||||
this.errorAutLista = null;
|
this.errorAutLista = null;
|
||||||
this.autorizacionesPaciente = [];
|
this.autorizacionesPaciente = [];
|
||||||
|
this.versionesPorAutorizacion = {};
|
||||||
|
this.versionActualPorAutorizacion = {};
|
||||||
|
this.versionSeleccionada = {};
|
||||||
|
this.cargandoVersiones = {};
|
||||||
|
|
||||||
this.pacienteService
|
this.pacienteService
|
||||||
.obtenerAutorizacionesPorInterno(this.pacienteSeleccionado.interno)
|
.obtenerAutorizacionesPorInterno(this.pacienteSeleccionado.interno)
|
||||||
@ -332,45 +633,78 @@ export class AutorizacionesComponent implements OnInit {
|
|||||||
// -------------------------
|
// -------------------------
|
||||||
// Descargar PDF
|
// Descargar PDF
|
||||||
// -------------------------
|
// -------------------------
|
||||||
descargarPdf(numeroAutorizacion: string): void {
|
descargarPdf(numeroAutorizacion: string, version?: number | null): void {
|
||||||
this.descargandoPdf = true;
|
this.descargandoPdf = true;
|
||||||
this.errorAutLista = null; // o la variable de error que uses
|
this.errorAutLista = null;
|
||||||
|
|
||||||
this.pacienteService.descargarPdfAutorizacion(numeroAutorizacion)
|
const versionNum = typeof version === 'number' ? version : undefined;
|
||||||
.pipe(finalize(() => {
|
this.authService.crearJobPdfAutorizacion(numeroAutorizacion, versionNum).subscribe({
|
||||||
this.descargandoPdf = false;
|
next: (job) => {
|
||||||
}))
|
this.jobsService.pollJob(job.id).subscribe({
|
||||||
.subscribe({
|
next: (estado) => {
|
||||||
next: (data: any) => {
|
if (estado.status === 'completed') {
|
||||||
const blob = new Blob([data], { type: 'application/pdf' });
|
const fileName =
|
||||||
const url = window.URL.createObjectURL(blob);
|
estado.result?.fileName || `autorizacion_${numeroAutorizacion}.pdf`;
|
||||||
|
|
||||||
// 👉 Forzar descarga
|
this.jobsService.downloadJobFile(job.id).subscribe({
|
||||||
const a = document.createElement('a');
|
next: (data) => {
|
||||||
a.href = url;
|
const blob = new Blob([data], { type: 'application/pdf' });
|
||||||
a.download = `autorizacion_${numeroAutorizacion}.pdf`;
|
const url = window.URL.createObjectURL(blob);
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
|
|
||||||
// Opcional: además abrir en una pestaña nueva
|
const a = document.createElement('a');
|
||||||
// window.open(url, '_blank');
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
window.URL.revokeObjectURL(url);
|
this.descargandoPdf = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error descargando PDF:', error);
|
||||||
|
this.errorAutLista =
|
||||||
|
'Error descargando el PDF de la autorizaci?n.';
|
||||||
|
this.descargandoPdf = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estado.status === 'failed') {
|
||||||
|
this.errorAutLista =
|
||||||
|
estado.error?.message ||
|
||||||
|
'Error generando el PDF de la autorizaci?n.';
|
||||||
|
this.descargandoPdf = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorAutLista = 'Error consultando el estado del PDF.';
|
||||||
|
this.descargandoPdf = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (error) => {
|
||||||
console.error('Error descargando PDF:', err);
|
console.error(error);
|
||||||
this.errorAutLista =
|
this.errorAutLista = 'Error solicitando la generaci?n del PDF.';
|
||||||
err?.error?.error || 'Error descargando el PDF de la autorización.';
|
this.descargandoPdf = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
irAAutorizacionesPorFecha(): void {
|
irAAutorizacionesPorFecha(): void {
|
||||||
this.router.navigate(['/autorizaciones-por-fecha']);
|
this.router.navigate(['/autorizaciones-por-fecha']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
irADashboard(): void {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Métodos de autenticación ----
|
// ---- Métodos de autenticación ----
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
|
|||||||
61
saludut-inpec/src/app/components/cargar-cups/cargar-cups.css
Normal file
61
saludut-inpec/src/app/components/cargar-cups/cargar-cups.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/* ============================
|
||||||
|
Estilos para carga de CUPS
|
||||||
|
============================ */
|
||||||
|
.cups-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cups-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cups-card h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input[type="file"] {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row.acciones {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.ok {
|
||||||
|
color: #1b5e20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: #b71c1c;
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
<div class="page-shell">
|
||||||
|
<div class="content-container cups-container">
|
||||||
|
<app-header
|
||||||
|
title="Cargar CUPS"
|
||||||
|
subtitle="Sube nota tecnica y tabla referencia"
|
||||||
|
[showBack]="true"
|
||||||
|
backLabel="Volver"
|
||||||
|
(back)="volverDashboard()"
|
||||||
|
[showLogo]="false"
|
||||||
|
></app-header>
|
||||||
|
|
||||||
|
<div class="card cups-card">
|
||||||
|
<h2>Subir archivos</h2>
|
||||||
|
<p class="subtitle">
|
||||||
|
Selecciona los dos archivos Excel. El sistema ejecuta el script de Python y carga los SQL.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Nota tecnica (Excel):</label>
|
||||||
|
<input type="file" accept=".xlsx,.xls" (change)="onNotaSelected($event)" />
|
||||||
|
<span class="file-name" *ngIf="notaFile">{{ notaFile.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Tabla referencia (Excel):</label>
|
||||||
|
<input type="file" accept=".xlsx,.xls" (change)="onReferenciaSelected($event)" />
|
||||||
|
<span class="file-name" *ngIf="referenciaFile">{{ referenciaFile.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row acciones">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="procesarCups()"
|
||||||
|
[disabled]="isLoading || !notaFile || !referenciaFile"
|
||||||
|
>
|
||||||
|
{{ isLoading ? 'Procesando...' : 'Procesar CUPS' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status ok" *ngIf="statusMessage">{{ statusMessage }}</div>
|
||||||
|
<div class="status error" *ngIf="errorMessage">{{ errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
123
saludut-inpec/src/app/components/cargar-cups/cargar-cups.ts
Normal file
123
saludut-inpec/src/app/components/cargar-cups/cargar-cups.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AppHeaderComponent } from '../shared/app-header/app-header';
|
||||||
|
import { AuthService } from '../../services/auth';
|
||||||
|
import { PacienteService } from '../../services/paciente';
|
||||||
|
import { JobsService } from '../../services/jobs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cargar-cups',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, AppHeaderComponent],
|
||||||
|
templateUrl: './cargar-cups.html',
|
||||||
|
styleUrls: ['./cargar-cups.css']
|
||||||
|
})
|
||||||
|
export class CargarCupsComponent implements OnInit {
|
||||||
|
notaFile: File | null = null;
|
||||||
|
referenciaFile: File | null = null;
|
||||||
|
isLoading = false;
|
||||||
|
statusMessage: string | null = null;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private pacienteService: PacienteService,
|
||||||
|
private jobsService: JobsService,
|
||||||
|
private router: Router,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!this.authService.isAdministrador()) {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volverDashboard(): void {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotaSelected(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
this.notaFile = input.files?.[0] || null;
|
||||||
|
this.limpiarMensajes();
|
||||||
|
}
|
||||||
|
|
||||||
|
onReferenciaSelected(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
this.referenciaFile = input.files?.[0] || null;
|
||||||
|
this.limpiarMensajes();
|
||||||
|
}
|
||||||
|
|
||||||
|
procesarCups(): void {
|
||||||
|
this.limpiarMensajes();
|
||||||
|
|
||||||
|
if (!this.notaFile || !this.referenciaFile) {
|
||||||
|
this.errorMessage = 'Debes seleccionar los dos archivos de CUPS.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.statusMessage = 'Subiendo archivos...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('nota_tecnica', this.notaFile);
|
||||||
|
formData.append('tabla_referencia', this.referenciaFile);
|
||||||
|
|
||||||
|
this.pacienteService.cargarExcelCups(formData).subscribe({
|
||||||
|
next: (job) => {
|
||||||
|
this.statusMessage = 'Archivos en cola. Procesando...';
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
|
||||||
|
this.jobsService.pollJob(job.id).subscribe({
|
||||||
|
next: (estado) => {
|
||||||
|
if (estado.status === 'completed') {
|
||||||
|
const partes: string[] = [];
|
||||||
|
if (estado.result?.mensaje) {
|
||||||
|
partes.push(estado.result.mensaje);
|
||||||
|
}
|
||||||
|
if (typeof estado.result?.referencia === 'number') {
|
||||||
|
partes.push(`CUPS referencia: ${estado.result.referencia}`);
|
||||||
|
}
|
||||||
|
if (typeof estado.result?.cubiertosActivos === 'number') {
|
||||||
|
partes.push(`CUPS cubiertos activos: ${estado.result.cubiertosActivos}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.statusMessage =
|
||||||
|
partes.join(' - ') || 'CUPS procesados correctamente.';
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estado.status === 'failed') {
|
||||||
|
this.errorMessage =
|
||||||
|
estado.error?.message || 'Error procesando los archivos de CUPS.';
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.errorMessage =
|
||||||
|
'Error consultando el estado del procesamiento de CUPS.';
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorMessage =
|
||||||
|
err?.error?.error || 'Error subiendo los archivos de CUPS.';
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private limpiarMensajes(): void {
|
||||||
|
this.errorMessage = null;
|
||||||
|
this.statusMessage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,125 +1,32 @@
|
|||||||
/* ============================
|
/* ============================
|
||||||
Estilos del Componente Dashboard
|
Estilos del 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 {
|
.dashboard-main {
|
||||||
padding: 24px;
|
padding: 24px 0 0;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Welcome Section */
|
/* Welcome Section */
|
||||||
.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;
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-section h2 {
|
.welcome-section h2 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
color: #222222;
|
color: var(--color-text-main);
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-date {
|
.welcome-date {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
color: #666666;
|
color: var(--color-text-muted);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-description {
|
.welcome-description {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #666666;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +37,7 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 24px;
|
margin: 20px 0 24px;
|
||||||
background: white;
|
background: white;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
animation: slideIn 0.3s ease;
|
animation: slideIn 0.3s ease;
|
||||||
@ -143,7 +50,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-icon {
|
.alert-icon {
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +63,7 @@
|
|||||||
.alert-close {
|
.alert-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
@ -179,16 +87,9 @@
|
|||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-actions h3 {
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
color: #222222;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-grid {
|
.actions-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,15 +111,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
font-size: 2.5rem;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #1976d2;
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-card h4 {
|
.action-icon svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card h4,
|
||||||
|
.action-card h3 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
color: #222222;
|
color: #222222;
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,21 +157,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Role Info */
|
/* 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 {
|
.role-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -299,20 +197,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Permissions Summary */
|
/* 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 {
|
.permissions-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
@ -336,13 +220,19 @@
|
|||||||
|
|
||||||
.permission-icon {
|
.permission-icon {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
flex-shrink: 0;
|
font-weight: 700;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.permission-text {
|
.permission-text {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #222222;
|
color: #222222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excel-status {
|
.excel-status {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@ -363,26 +253,6 @@
|
|||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@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 {
|
.actions-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@ -410,8 +280,4 @@
|
|||||||
.action-card {
|
.action-card {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,50 +1,48 @@
|
|||||||
<div class="dashboard-container">
|
<div class="page-shell">
|
||||||
<!-- Header -->
|
<div class="content-container">
|
||||||
<header class="dashboard-header">
|
<app-header
|
||||||
<div class="header-left">
|
title="SALUD UT"
|
||||||
<img src="logo_SALUDUT.png" alt="SALUD UT" class="header-logo" />
|
subtitle="Módulo de autorizaciones médicas"
|
||||||
<div class="header-text">
|
[showUserInfo]="true"
|
||||||
<h1>SALUD UT</h1>
|
[userName]="getNombreUsuario()"
|
||||||
<p class="header-subtitle">Módulo de autorizaciones médicas</p>
|
[userRole]="getNombreRolFormateado()"
|
||||||
</div>
|
[showLogout]="true"
|
||||||
</div>
|
(logout)="logout()"
|
||||||
<div class="header-right">
|
></app-header>
|
||||||
<div class="user-info">
|
|
||||||
<span class="user-name">{{ getNombreUsuario() }}</span>
|
<!-- Mensaje de error -->
|
||||||
<span class="user-role">{{ getNombreRolFormateado() }}</span>
|
<div class="alert alert-error" *ngIf="errorMessage">
|
||||||
</div>
|
<span class="alert-icon" aria-hidden="true">!</span>
|
||||||
<button class="logout-button" (click)="logout()" title="Cerrar sesión">
|
<span class="alert-message">{{ errorMessage }}</span>
|
||||||
Salir
|
<button class="alert-close" (click)="cerrarMensajeError()" title="Cerrar">
|
||||||
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Mensaje de error -->
|
<!-- Main Content -->
|
||||||
<div class="alert alert-error" *ngIf="errorMessage">
|
<main class="dashboard-main">
|
||||||
<span class="alert-icon">⚠️</span>
|
<!-- Welcome Section -->
|
||||||
<span class="alert-message">{{ errorMessage }}</span>
|
<section class="welcome-section card">
|
||||||
<button class="alert-close" (click)="cerrarMensajeError()" title="Cerrar">×</button>
|
<h2>Bienvenido, {{ getNombreUsuario() }}</h2>
|
||||||
</div>
|
<p class="welcome-date">{{ getFechaActual() }}</p>
|
||||||
|
<p class="welcome-description">
|
||||||
|
Sistema de autorizaciones médicas para el INPEC
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Quick Actions -->
|
||||||
<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">
|
<section class="quick-actions">
|
||||||
<h3>Acciones Rápidas</h3>
|
<h3 class="section-title">Acciones rápidas</h3>
|
||||||
<div class="actions-grid">
|
<div class="actions-grid">
|
||||||
<!-- Buscar Pacientes (disponible para todos) -->
|
<!-- Buscar Pacientes (disponible para todos) -->
|
||||||
<div class="action-card" (click)="irABusquedaPacientes()">
|
<div class="action-card" (click)="irABusquedaPacientes()">
|
||||||
<div class="action-icon">🔍</div>
|
<span class="action-icon" aria-hidden="true">
|
||||||
<h4>Buscar Pacientes</h4>
|
<svg viewBox="0 0 24 24" role="img" aria-label="">
|
||||||
|
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"></circle>
|
||||||
|
<line x1="16.5" y1="16.5" x2="22" y2="22" stroke="currentColor" stroke-width="2"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h4>Buscar pacientes</h4>
|
||||||
<p>Consultar información de pacientes y generar autorizaciones</p>
|
<p>Consultar información de pacientes y generar autorizaciones</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -54,12 +52,50 @@
|
|||||||
*ngIf="puedeCargarPacientes()"
|
*ngIf="puedeCargarPacientes()"
|
||||||
(click)="abrirCargadorPacientes(inputExcelPacientes)"
|
(click)="abrirCargadorPacientes(inputExcelPacientes)"
|
||||||
>
|
>
|
||||||
<div class="action-icon">📊</div>
|
<span class="action-icon" aria-hidden="true">
|
||||||
<h4>Cargar Pacientes</h4>
|
<svg viewBox="0 0 24 24" role="img" aria-label="">
|
||||||
|
<path
|
||||||
|
d="M12 3v12m0 0l-4-4m4 4l4-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></path>
|
||||||
|
<rect x="4" y="17" width="16" height="4" rx="1" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h4>Cargar pacientes</h4>
|
||||||
<p>
|
<p>
|
||||||
{{ cargandoExcel
|
{{ cargandoExcel
|
||||||
? 'Cargando archivo de pacientes...'
|
? 'Cargando archivo de pacientes...'
|
||||||
: 'Subir archivo Excel con datos de pacientes' }}
|
: 'Subir archivo Excel con datos de pacientes' }}
|
||||||
|
</p>
|
||||||
|
<div class="admin-badge">Solo admin</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cargar CUPS (solo administradores) -->
|
||||||
|
<div
|
||||||
|
class="action-card"
|
||||||
|
*ngIf="puedeCargarCups()"
|
||||||
|
(click)="irACargarCups()"
|
||||||
|
>
|
||||||
|
<span class="action-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" role="img" aria-label="">
|
||||||
|
<path
|
||||||
|
d="M12 3v12m0 0l-4-4m4 4l4-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></path>
|
||||||
|
<rect x="4" y="17" width="16" height="4" rx="1" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h4>Cargar CUPS</h4>
|
||||||
|
<p>
|
||||||
|
Subir nota tecnica y tabla referencia
|
||||||
</p>
|
</p>
|
||||||
<div class="admin-badge">Solo admin</div>
|
<div class="admin-badge">Solo admin</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,8 +106,15 @@
|
|||||||
(click)="irAVerAutorizacionesPorFecha()"
|
(click)="irAVerAutorizacionesPorFecha()"
|
||||||
*ngIf="puedeVerTodasAutorizaciones()"
|
*ngIf="puedeVerTodasAutorizaciones()"
|
||||||
>
|
>
|
||||||
<div class="action-icon">📋</div>
|
<span class="action-icon" aria-hidden="true">
|
||||||
<h4>Autorizaciones por Fecha</h4>
|
<svg viewBox="0 0 24 24" role="img" aria-label="">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
||||||
|
<line x1="8" y1="2.5" x2="8" y2="6.5" stroke="currentColor" stroke-width="2"></line>
|
||||||
|
<line x1="16" y1="2.5" x2="16" y2="6.5" stroke="currentColor" stroke-width="2"></line>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h4>Autorizaciones por fecha</h4>
|
||||||
<p>Consultar y descargar autorizaciones por rango de fechas</p>
|
<p>Consultar y descargar autorizaciones por rango de fechas</p>
|
||||||
<div class="admin-badge">Solo admin</div>
|
<div class="admin-badge">Solo admin</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,14 +124,23 @@
|
|||||||
*ngIf="puedeGestionarUsuarios()"
|
*ngIf="puedeGestionarUsuarios()"
|
||||||
(click)="irAUsuarios()"
|
(click)="irAUsuarios()"
|
||||||
>
|
>
|
||||||
<div class="action-icon">
|
<span class="action-icon" aria-hidden="true">
|
||||||
👥
|
<svg viewBox="0 0 24 24" role="img" aria-label="">
|
||||||
</div>
|
<circle cx="9" cy="8" r="4" fill="none" stroke="currentColor" stroke-width="2"></circle>
|
||||||
<h3>Gestionar Usuarios</h3>
|
<path
|
||||||
|
d="M2 21c0-3.5 3.1-6 7-6s7 2.5 7 6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
></path>
|
||||||
|
<circle cx="18" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="2"></circle>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<h3>Gestionar usuarios</h3>
|
||||||
<p>Crear y administrar usuarios del sistema.</p>
|
<p>Crear y administrar usuarios del sistema.</p>
|
||||||
<span class="admin-badge">Solo admin</span>
|
<span class="admin-badge">Solo admin</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input de archivo real (oculto) -->
|
<!-- Input de archivo real (oculto) -->
|
||||||
@ -107,56 +159,94 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- User Role Information -->
|
||||||
|
<section class="role-info card" *ngIf="currentUser">
|
||||||
|
<h3 class="section-title">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>
|
||||||
|
|
||||||
<!-- User Role Information -->
|
<!-- Sedes asignadas (solo para administrativos) -->
|
||||||
<section class="role-info" *ngIf="currentUser">
|
<div
|
||||||
<h3>Información de Acceso</h3>
|
class="role-item"
|
||||||
<div class="role-details">
|
*ngIf="
|
||||||
<div class="role-item">
|
currentUser.nombre_rol === 'administrativo_sede' &&
|
||||||
<span class="role-label">Rol:</span>
|
getSedesUsuario().length > 0
|
||||||
<span class="role-value">{{ getNombreRolFormateado() }}</span>
|
"
|
||||||
</div>
|
>
|
||||||
|
<span class="role-label">Sedes asignadas:</span>
|
||||||
<!-- Sedes asignadas (solo para administrativos) -->
|
<div class="sedes-list">
|
||||||
<div class="role-item" *ngIf="currentUser.nombre_rol === 'administrativo_sede' && getSedesUsuario().length > 0">
|
<span
|
||||||
<span class="role-label">Sedes asignadas:</span>
|
class="sede-badge"
|
||||||
<div class="sedes-list">
|
*ngFor="let sede of getSedesUsuario()"
|
||||||
<span
|
[title]="sede.nombre_establecimiento"
|
||||||
class="sede-badge"
|
>
|
||||||
*ngFor="let sede of getSedesUsuario()"
|
{{ sede.nombre_establecimiento }}
|
||||||
[title]="sede.nombre_establecimiento"
|
</span>
|
||||||
>
|
</div>
|
||||||
{{ sede.nombre_establecimiento }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Permissions Summary -->
|
<!-- Permissions Summary -->
|
||||||
<section class="permissions-summary">
|
<section class="permissions-summary card">
|
||||||
<h3>Permisos Disponibles</h3>
|
<h3 class="section-title">Permisos disponibles</h3>
|
||||||
<div class="permissions-grid">
|
<div class="permissions-grid">
|
||||||
<div class="permission-item" [class.has-permission]="puedeGenerarAutorizaciones()">
|
<div
|
||||||
<span class="permission-icon">{{ puedeGenerarAutorizaciones() ? '✅' : '❌' }}</span>
|
class="permission-item"
|
||||||
<span class="permission-text">Generar autorizaciones</span>
|
[class.has-permission]="puedeGenerarAutorizaciones()"
|
||||||
</div>
|
>
|
||||||
|
<span class="permission-icon" *ngIf="puedeGenerarAutorizaciones(); else noPermGen">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<ng-template #noPermGen>
|
||||||
|
<span class="permission-icon">×</span>
|
||||||
|
</ng-template>
|
||||||
|
<span class="permission-text">Generar autorizaciones</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="permission-item" [class.has-permission]="puedeCargarPacientes()">
|
<div
|
||||||
<span class="permission-icon">{{ puedeCargarPacientes() ? '✅' : '❌' }}</span>
|
class="permission-item"
|
||||||
<span class="permission-text">Cargar pacientes (Excel)</span>
|
[class.has-permission]="puedeCargarPacientes()"
|
||||||
</div>
|
>
|
||||||
|
<span class="permission-icon" *ngIf="puedeCargarPacientes(); else noPermExcel">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<ng-template #noPermExcel>
|
||||||
|
<span class="permission-icon">×</span>
|
||||||
|
</ng-template>
|
||||||
|
<span class="permission-text">Cargar pacientes (Excel)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="permission-item" [class.has-permission]="puedeDescargarPdfs()">
|
<div
|
||||||
<span class="permission-icon">{{ puedeDescargarPdfs() ? '✅' : '❌' }}</span>
|
class="permission-item"
|
||||||
<span class="permission-text">Descargar PDFs</span>
|
[class.has-permission]="puedeDescargarPdfs()"
|
||||||
</div>
|
>
|
||||||
|
<span class="permission-icon" *ngIf="puedeDescargarPdfs(); else noPermPdf">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<ng-template #noPermPdf>
|
||||||
|
<span class="permission-icon">×</span>
|
||||||
|
</ng-template>
|
||||||
|
<span class="permission-text">Descargar PDFs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="permission-item" [class.has-permission]="puedeVerTodasAutorizaciones()">
|
<div
|
||||||
<span class="permission-icon">{{ puedeVerTodasAutorizaciones() ? '✅' : '❌' }}</span>
|
class="permission-item"
|
||||||
<span class="permission-text">Ver todas las autorizaciones</span>
|
[class.has-permission]="puedeVerTodasAutorizaciones()"
|
||||||
|
>
|
||||||
|
<span class="permission-icon" *ngIf="puedeVerTodasAutorizaciones(); else noPermAll">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<ng-template #noPermAll>
|
||||||
|
<span class="permission-icon">×</span>
|
||||||
|
</ng-template>
|
||||||
|
<span class="permission-text">Ver todas las autorizaciones</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -7,14 +7,15 @@ import {
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { finalize } from 'rxjs/operators';
|
|
||||||
import { AuthService } from '../../services/auth';
|
import { AuthService } from '../../services/auth';
|
||||||
import { PacienteService } from '../../services/paciente';
|
import { PacienteService } from '../../services/paciente';
|
||||||
|
import { AppHeaderComponent } from '../shared/app-header/app-header';
|
||||||
|
import { JobsService } from '../../services/jobs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, AppHeaderComponent],
|
||||||
templateUrl: './dashboard.html',
|
templateUrl: './dashboard.html',
|
||||||
styleUrls: ['./dashboard.css']
|
styleUrls: ['./dashboard.css']
|
||||||
})
|
})
|
||||||
@ -32,6 +33,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private pacienteService: PacienteService,
|
private pacienteService: PacienteService,
|
||||||
|
private jobsService: JobsService,
|
||||||
private cdr: ChangeDetectorRef
|
private cdr: ChangeDetectorRef
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -96,6 +98,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(['/autorizaciones-por-fecha']);
|
this.router.navigate(['/autorizaciones-por-fecha']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
irACargarCups(): void {
|
||||||
|
this.router.navigate(['/cargar-cups']);
|
||||||
|
}
|
||||||
|
|
||||||
irAUsuarios(): void {
|
irAUsuarios(): void {
|
||||||
this.router.navigate(['/usuarios']);
|
this.router.navigate(['/usuarios']);
|
||||||
}
|
}
|
||||||
@ -139,6 +145,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
return this.authService.puedeCargarPacientes();
|
return this.authService.puedeCargarPacientes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
puedeCargarCups(): boolean {
|
||||||
|
return this.authService.isAdministrador();
|
||||||
|
}
|
||||||
|
|
||||||
puedeDescargarPdfs(): boolean {
|
puedeDescargarPdfs(): boolean {
|
||||||
return this.authService.puedeDescargarPdfs();
|
return this.authService.puedeDescargarPdfs();
|
||||||
}
|
}
|
||||||
@ -189,46 +199,62 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.cargandoExcel = true;
|
this.cargandoExcel = true;
|
||||||
this.estadoCargaExcel = 'Subiendo y procesando archivo...';
|
this.estadoCargaExcel = 'Subiendo archivo...';
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('archivo', file); // mismo nombre que en server.js
|
formData.append('archivo', file); // mismo nombre que en server.js
|
||||||
|
|
||||||
this.pacienteService
|
this.pacienteService.cargarExcelPacientes(formData).subscribe({
|
||||||
.cargarExcelPacientes(formData)
|
next: (job) => {
|
||||||
.pipe(
|
this.estadoCargaExcel = 'Archivo en cola. Procesando...';
|
||||||
finalize(() => {
|
if (input) {
|
||||||
this.cargandoExcel = false;
|
input.value = '';
|
||||||
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.';
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
this.jobsService.pollJob(job.id).subscribe({
|
||||||
|
next: (estado) => {
|
||||||
|
if (estado.status === 'completed') {
|
||||||
|
const partes: string[] = [];
|
||||||
|
if (estado.result?.mensaje) {
|
||||||
|
partes.push(estado.result.mensaje);
|
||||||
|
}
|
||||||
|
if (typeof estado.result?.activos === 'number') {
|
||||||
|
partes.push(`Pacientes activos: ${estado.result.activos}`);
|
||||||
|
}
|
||||||
|
if (typeof estado.result?.antiguos === 'number') {
|
||||||
|
partes.push(`Pacientes antiguos: ${estado.result.antiguos}`);
|
||||||
|
}
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
partes.join(' ? ') || 'Archivo procesado correctamente.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estado.status === 'failed') {
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
estado.error?.message ||
|
||||||
|
'Error procesando el Excel de pacientes.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
}
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
'Error consultando el estado del procesamiento.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.estadoCargaExcel =
|
||||||
|
err?.error?.error || 'Error subiendo el Excel de pacientes.';
|
||||||
|
this.cargandoExcel = false;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,7 +119,10 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.1rem;
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
(click)="togglePasswordVisibility()"
|
(click)="togglePasswordVisibility()"
|
||||||
[title]="hidePassword ? 'Mostrar contraseña' : 'Ocultar contraseña'"
|
[title]="hidePassword ? 'Mostrar contraseña' : 'Ocultar contraseña'"
|
||||||
>
|
>
|
||||||
{{ hidePassword ? '👁️' : '👁️🗨️' }}
|
{{ hidePassword ? 'Mostrar' : 'Ocultar' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="error-message" *ngIf="password?.invalid && password?.touched">
|
<div class="error-message" *ngIf="password?.invalid && password?.touched">
|
||||||
@ -62,7 +62,7 @@
|
|||||||
class="login-button"
|
class="login-button"
|
||||||
[disabled]="loginForm.invalid || isLoading"
|
[disabled]="loginForm.invalid || isLoading"
|
||||||
>
|
>
|
||||||
<span *ngIf="!isLoading">Iniciar Sesión</span>
|
<span *ngIf="!isLoading">Iniciar sesión</span>
|
||||||
<span *ngIf="isLoading" class="loading-spinner">
|
<span *ngIf="isLoading" class="loading-spinner">
|
||||||
<span class="spinner"></span>
|
<span class="spinner"></span>
|
||||||
Iniciando sesión...
|
Iniciando sesión...
|
||||||
|
|||||||
@ -0,0 +1,104 @@
|
|||||||
|
/* Shared header component */
|
||||||
|
.app-header {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-primary),
|
||||||
|
var(--color-header-grad-2)
|
||||||
|
);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 14px 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
height: 46px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-saludut {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-header {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left,
|
||||||
|
.header-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<header class="app-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button
|
||||||
|
*ngIf="showBack"
|
||||||
|
class="btn btn-secondary btn-back"
|
||||||
|
type="button"
|
||||||
|
(click)="back.emit()"
|
||||||
|
>
|
||||||
|
<span class="btn-icon" aria-hidden="true">←</span>
|
||||||
|
{{ backLabel }}
|
||||||
|
</button>
|
||||||
|
<img *ngIf="showLogo" [src]="logoSrc" [alt]="logoAlt" class="header-logo" />
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p *ngIf="subtitle" class="header-subtitle">{{ subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="user-info" *ngIf="showUserInfo">
|
||||||
|
<span class="user-name">{{ userName }}</span>
|
||||||
|
<span class="user-role">{{ userRole }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="badge-saludut" *ngIf="badgeText">{{ badgeText }}</span>
|
||||||
|
<button
|
||||||
|
*ngIf="showThemeToggle"
|
||||||
|
class="btn btn-ghost theme-toggle"
|
||||||
|
type="button"
|
||||||
|
(click)="toggleTheme()"
|
||||||
|
>
|
||||||
|
{{ isDark ? 'Modo claro' : 'Modo oscuro' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="showLogout"
|
||||||
|
class="btn btn-ghost"
|
||||||
|
type="button"
|
||||||
|
(click)="logout.emit()"
|
||||||
|
>
|
||||||
|
{{ logoutLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, Inject } from '@angular/core';
|
||||||
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './app-header.html',
|
||||||
|
styleUrls: ['./app-header.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class AppHeaderComponent implements OnInit {
|
||||||
|
@Input() title = '';
|
||||||
|
@Input() subtitle: string | null = null;
|
||||||
|
@Input() showLogo = true;
|
||||||
|
@Input() logoSrc = 'logo_SALUDUT.png';
|
||||||
|
@Input() logoAlt = 'SALUD UT';
|
||||||
|
@Input() badgeText: string | null = null;
|
||||||
|
@Input() userName: string | null = null;
|
||||||
|
@Input() userRole: string | null = null;
|
||||||
|
@Input() showUserInfo = false;
|
||||||
|
@Input() showLogout = false;
|
||||||
|
@Input() logoutLabel = 'Salir';
|
||||||
|
@Input() showBack = false;
|
||||||
|
@Input() backLabel = 'Volver';
|
||||||
|
@Input() showThemeToggle = true;
|
||||||
|
|
||||||
|
@Output() logout = new EventEmitter<void>();
|
||||||
|
@Output() back = new EventEmitter<void>();
|
||||||
|
|
||||||
|
isDark = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DOCUMENT) private document: Document,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const stored = localStorage.getItem('saludut_theme');
|
||||||
|
const isDark = stored === 'dark';
|
||||||
|
this.applyTheme(isDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTheme(): void {
|
||||||
|
this.applyTheme(!this.isDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyTheme(isDark: boolean): void {
|
||||||
|
this.isDark = isDark;
|
||||||
|
const root = this.document.documentElement;
|
||||||
|
if (isDark) {
|
||||||
|
root.setAttribute('data-theme', 'dark');
|
||||||
|
localStorage.setItem('saludut_theme', 'dark');
|
||||||
|
} else {
|
||||||
|
root.removeAttribute('data-theme');
|
||||||
|
localStorage.setItem('saludut_theme', 'light');
|
||||||
|
}
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,101 +1,17 @@
|
|||||||
/* ==== FONDO GENERAL ==== */
|
/* ============================
|
||||||
.usuarios-page {
|
Estilos de gestión de usuarios
|
||||||
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 {
|
.usuarios-container {
|
||||||
max-width: 1200px;
|
display: flex;
|
||||||
margin: 24px auto 40px;
|
flex-direction: column;
|
||||||
padding: 0 16px 32px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Título + botón volver */
|
|
||||||
|
|
||||||
.usuarios-header {
|
.usuarios-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 20px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usuarios-header h2 {
|
.usuarios-header h2 {
|
||||||
@ -105,29 +21,10 @@
|
|||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
color: #666;
|
color: #666666;
|
||||||
font-size: 0.9rem;
|
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 {
|
.usuarios-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.4fr);
|
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.4fr);
|
||||||
@ -135,23 +32,6 @@
|
|||||||
align-items: flex-start;
|
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 {
|
.form-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -185,69 +65,36 @@
|
|||||||
margin-top: 12px;
|
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 {
|
.hint {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #777;
|
color: #777777;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== TABLA DE USUARIOS ===== */
|
/* Tabla */
|
||||||
|
|
||||||
.tabla-usuarios-wrapper {
|
.tabla-usuarios-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 460px; /* limita altura y muestra scroll si hay muchos */
|
max-height: 460px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilo barra scroll (opcional) */
|
|
||||||
.tabla-usuarios-wrapper::-webkit-scrollbar {
|
.tabla-usuarios-wrapper::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla-usuarios-wrapper::-webkit-scrollbar-track {
|
.tabla-usuarios-wrapper::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb {
|
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb {
|
||||||
background: #c0c0c0;
|
background: #c0c0c0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb:hover {
|
.tabla-usuarios-wrapper::-webkit-scrollbar-thumb:hover {
|
||||||
background: #a0a0a0;
|
background: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla-usuarios {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabla-usuarios thead {
|
.tabla-usuarios thead {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -257,46 +104,8 @@ button:hover {
|
|||||||
.tabla-usuarios th,
|
.tabla-usuarios th,
|
||||||
.tabla-usuarios td {
|
.tabla-usuarios td {
|
||||||
padding: 8px 10px;
|
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) {
|
@media (max-width: 900px) {
|
||||||
.usuarios-grid {
|
.usuarios-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@ -305,12 +114,5 @@ button:hover {
|
|||||||
.usuarios-header {
|
.usuarios-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,24 @@
|
|||||||
<div class="usuarios-page">
|
<div class="page-shell">
|
||||||
<!-- Header igual al estilo del dashboard -->
|
<div class="content-container usuarios-container">
|
||||||
<header class="dashboard-header">
|
<app-header
|
||||||
<div class="header-left">
|
title="SALUD UT"
|
||||||
<img src="logo_SALUDUT.png" alt="SALUD UT" class="header-logo" />
|
subtitle="Gestión de Usuarios"
|
||||||
<div class="header-text">
|
[showUserInfo]="true"
|
||||||
<h1>SALUD UT</h1>
|
[userName]="getNombreUsuario()"
|
||||||
<p class="header-subtitle">Gestión de Usuarios</p>
|
[userRole]="getNombreRolFormateado()"
|
||||||
</div>
|
[showLogout]="true"
|
||||||
</div>
|
(logout)="logout()"
|
||||||
<div class="header-right">
|
></app-header>
|
||||||
<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">
|
<header class="usuarios-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Gestión de Usuarios</h2>
|
<h2>Gestión de usuarios</h2>
|
||||||
<p class="subtitle">
|
<p class="subtitle">
|
||||||
Crear, activar y desactivar usuarios del sistema
|
Crear, activar y desactivar usuarios del sistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-secondary" (click)="volverDashboard()">
|
<button class="btn btn-secondary" (click)="volverDashboard()">
|
||||||
← Volver al dashboard
|
Volver al dashboard
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -81,76 +71,197 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row acciones">
|
<div class="form-row acciones">
|
||||||
<button (click)="crearUsuario()" [disabled]="creando">
|
<button class="btn btn-primary" (click)="crearUsuario()" [disabled]="creando">
|
||||||
{{ creando ? 'Creando.' : 'Crear usuario' }}
|
{{ creando ? 'Creando...' : 'Crear usuario' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="msg-ok" *ngIf="mensajeOk">{{ mensajeOk }}</div>
|
<div class="status ok" *ngIf="mensajeOk">{{ mensajeOk }}</div>
|
||||||
<div class="msg-error" *ngIf="mensajeError">{{ mensajeError }}</div>
|
<div class="status error" *ngIf="mensajeError">{{ mensajeError }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Crear autorizante</h3>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Numero de documento:</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoAutorizante.numero_documento" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Tipo de documento:</label>
|
||||||
|
<select [(ngModel)]="nuevoAutorizante.tipo_documento">
|
||||||
|
<option value="CC">CC</option>
|
||||||
|
<option value="DE">DE</option>
|
||||||
|
<option value="RC">RC</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Nombre completo:</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoAutorizante.nombre" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Telefono:</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoAutorizante.telefono" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Cargo:</label>
|
||||||
|
<input type="text" [(ngModel)]="nuevoAutorizante.cargo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" [(ngModel)]="nuevoAutorizante.activo" />
|
||||||
|
Activo
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row acciones">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="crearAutorizante()"
|
||||||
|
[disabled]="creandoAutorizante"
|
||||||
|
>
|
||||||
|
{{ creandoAutorizante ? 'Creando...' : 'Crear autorizante' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status ok" *ngIf="mensajeAutorizanteOk">
|
||||||
|
{{ mensajeAutorizanteOk }}
|
||||||
|
</div>
|
||||||
|
<div class="status error" *ngIf="mensajeAutorizanteError">
|
||||||
|
{{ mensajeAutorizanteError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-usuarios">
|
||||||
|
<h3>Autorizantes registrados</h3>
|
||||||
|
|
||||||
|
<div class="status" *ngIf="cargandoAutorizantes">Cargando autorizantes...</div>
|
||||||
|
<div class="status error" *ngIf="errorAutorizantes">{{ errorAutorizantes }}</div>
|
||||||
|
|
||||||
|
<div class="tabla-usuarios-wrapper" *ngIf="!cargandoAutorizantes && autorizantes.length">
|
||||||
|
<table class="table tabla-usuarios">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Documento</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Telefono</th>
|
||||||
|
<th>Cargo</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let a of autorizantes; trackBy: trackByAutorizante">
|
||||||
|
<td>{{ a.numero_documento }}</td>
|
||||||
|
<td>{{ a.tipo_documento }}</td>
|
||||||
|
<td>{{ a.nombre }}</td>
|
||||||
|
<td>{{ a.telefono || '-' }}</td>
|
||||||
|
<td>{{ a.cargo || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
[class.badge-success]="a.activo"
|
||||||
|
[class.badge-danger]="!a.activo"
|
||||||
|
>
|
||||||
|
{{ a.activo ? 'Activo' : 'Inactivo' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
*ngIf="a.activo"
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
(click)="cambiarEstadoAutorizante(a, false)"
|
||||||
|
>
|
||||||
|
Desactivar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!a.activo"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
(click)="cambiarEstadoAutorizante(a, true)"
|
||||||
|
>
|
||||||
|
Activar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!cargandoAutorizantes && autorizantes.length === 0">
|
||||||
|
No hay autorizantes para mostrar.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lista de usuarios -->
|
<!-- Lista de usuarios -->
|
||||||
<div class="card card-usuarios">
|
<div class="card card-usuarios">
|
||||||
<h3>Usuarios registrados</h3>
|
<h3>Usuarios registrados</h3>
|
||||||
|
|
||||||
<div class="tabla-usuarios-wrapper">
|
<div class="tabla-usuarios-wrapper">
|
||||||
<table class="tabla-usuarios">
|
<table class="table tabla-usuarios">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Usuario</th>
|
<th>Usuario</th>
|
||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Rol</th>
|
<th>Rol</th>
|
||||||
<th>Estado</th>
|
<th>Estado</th>
|
||||||
<th>Último login</th>
|
<th>Último login</th>
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let u of usuarios">
|
<tr *ngFor="let u of usuarios; trackBy: trackByUsuario">
|
||||||
<td>{{ u.username }}</td>
|
<td>{{ u.username }}</td>
|
||||||
<td>{{ u.nombre_completo }}</td>
|
<td>{{ u.nombre_completo }}</td>
|
||||||
<td>{{ u.email }}</td>
|
<td>{{ u.email }}</td>
|
||||||
<td>{{ u.nombre_rol }}</td>
|
<td>{{ u.nombre_rol }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
class="badge"
|
class="badge"
|
||||||
[class.badge-activo]="u.activo"
|
[class.badge-success]="u.activo"
|
||||||
[class.badge-inactivo]="!u.activo"
|
[class.badge-danger]="!u.activo"
|
||||||
>
|
>
|
||||||
{{ u.activo ? 'Activo' : 'Inactivo' }}
|
{{ u.activo ? 'Activo' : 'Inactivo' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- date pipe para quitar la Z horrible 😅 -->
|
<td>
|
||||||
<td>
|
{{ u.ultimo_login ? (u.ultimo_login | date:'yyyy-MM-dd HH:mm') : '—' }}
|
||||||
{{ u.ultimo_login ? (u.ultimo_login | date:'yyyy-MM-dd HH:mm') : '—' }}
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<button
|
||||||
<button
|
*ngIf="u.activo"
|
||||||
*ngIf="u.activo"
|
class="btn btn-danger btn-sm"
|
||||||
class="btn-danger"
|
(click)="cambiarEstadoUsuario(u, false)"
|
||||||
(click)="cambiarEstadoUsuario(u, false)"
|
>
|
||||||
>
|
Desactivar
|
||||||
Desactivar
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
*ngIf="!u.activo"
|
||||||
*ngIf="!u.activo"
|
class="btn btn-secondary btn-sm"
|
||||||
class="btn-secondary"
|
(click)="cambiarEstadoUsuario(u, true)"
|
||||||
(click)="cambiarEstadoUsuario(u, true)"
|
>
|
||||||
>
|
Activar
|
||||||
Activar
|
</button>
|
||||||
</button>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="!cargandoUsuarios && usuarios.length === 0">
|
<div class="status" *ngIf="avisoUsuarios">
|
||||||
No hay usuarios para mostrar.
|
Mostrando hasta {{ limiteUsuarios }} usuarios. Ajusta el filtro o pagina si necesitas mas.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div *ngIf="!cargandoUsuarios && usuarios.length === 0">
|
||||||
|
No hay usuarios para mostrar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
@ -6,12 +6,18 @@ import {
|
|||||||
RegisterRequest,
|
RegisterRequest,
|
||||||
Rol
|
Rol
|
||||||
} from '../../services/auth';
|
} from '../../services/auth';
|
||||||
|
import {
|
||||||
|
Autorizante,
|
||||||
|
CrearAutorizantePayload,
|
||||||
|
PacienteService
|
||||||
|
} from '../../services/paciente';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { AppHeaderComponent } from '../shared/app-header/app-header';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-usuarios',
|
selector: 'app-usuarios',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, AppHeaderComponent],
|
||||||
templateUrl: './usuarios.html',
|
templateUrl: './usuarios.html',
|
||||||
styleUrls: ['./usuarios.css'],
|
styleUrls: ['./usuarios.css'],
|
||||||
})
|
})
|
||||||
@ -19,14 +25,24 @@ export class UsuariosComponent implements OnInit {
|
|||||||
|
|
||||||
usuarios: any[] = [];
|
usuarios: any[] = [];
|
||||||
roles: Rol[] = [];
|
roles: Rol[] = [];
|
||||||
|
autorizantes: Autorizante[] = [];
|
||||||
|
|
||||||
cargandoUsuarios = false;
|
cargandoUsuarios = false;
|
||||||
errorUsuarios: string | null = null;
|
errorUsuarios: string | null = null;
|
||||||
|
limiteUsuarios = 200;
|
||||||
|
avisoUsuarios = false;
|
||||||
|
|
||||||
|
cargandoAutorizantes = false;
|
||||||
|
errorAutorizantes: string | null = null;
|
||||||
|
|
||||||
creando = false;
|
creando = false;
|
||||||
mensajeOk: string | null = null;
|
mensajeOk: string | null = null;
|
||||||
mensajeError: string | null = null;
|
mensajeError: string | null = null;
|
||||||
|
|
||||||
|
creandoAutorizante = false;
|
||||||
|
mensajeAutorizanteOk: string | null = null;
|
||||||
|
mensajeAutorizanteError: string | null = null;
|
||||||
|
|
||||||
// para administrativos, códigos separados por coma
|
// para administrativos, códigos separados por coma
|
||||||
sedesTexto = '';
|
sedesTexto = '';
|
||||||
|
|
||||||
@ -39,8 +55,19 @@ export class UsuariosComponent implements OnInit {
|
|||||||
sedes: []
|
sedes: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
nuevoAutorizante = {
|
||||||
|
numero_documento: '',
|
||||||
|
tipo_documento: 'CC',
|
||||||
|
nombre: '',
|
||||||
|
telefono: '',
|
||||||
|
cargo: '',
|
||||||
|
activo: true
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private pacienteService: PacienteService,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -53,6 +80,7 @@ export class UsuariosComponent implements OnInit {
|
|||||||
|
|
||||||
this.cargarRoles();
|
this.cargarRoles();
|
||||||
this.cargarUsuarios();
|
this.cargarUsuarios();
|
||||||
|
this.cargarAutorizantesAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========= HEADER =============
|
// ========= HEADER =============
|
||||||
@ -90,9 +118,11 @@ export class UsuariosComponent implements OnInit {
|
|||||||
this.authService.getRoles().subscribe({
|
this.authService.getRoles().subscribe({
|
||||||
next: (roles) => {
|
next: (roles) => {
|
||||||
this.roles = roles;
|
this.roles = roles;
|
||||||
|
this.cdr.detectChanges();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -101,15 +131,18 @@ export class UsuariosComponent implements OnInit {
|
|||||||
this.cargandoUsuarios = true;
|
this.cargandoUsuarios = true;
|
||||||
this.errorUsuarios = null;
|
this.errorUsuarios = null;
|
||||||
|
|
||||||
this.authService.getUsuarios().subscribe({
|
this.authService.getUsuarios(this.limiteUsuarios, 0).subscribe({
|
||||||
next: (usuarios) => {
|
next: (usuarios) => {
|
||||||
this.usuarios = usuarios;
|
this.usuarios = usuarios;
|
||||||
|
this.avisoUsuarios = usuarios.length >= this.limiteUsuarios;
|
||||||
this.cargandoUsuarios = false;
|
this.cargandoUsuarios = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.errorUsuarios = 'Error cargando usuarios.';
|
this.errorUsuarios = 'Error cargando usuarios.';
|
||||||
this.cargandoUsuarios = false;
|
this.cargandoUsuarios = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -158,13 +191,113 @@ export class UsuariosComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
this.sedesTexto = '';
|
this.sedesTexto = '';
|
||||||
|
|
||||||
// 🔄 Recargar lista SIN refrescar la página
|
// Recargar lista sin refrescar la página
|
||||||
this.cargarUsuarios();
|
this.cargarUsuarios();
|
||||||
|
this.cdr.detectChanges();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.mensajeError = err?.error?.error || 'Error creando el usuario.';
|
this.mensajeError = err?.error?.error || 'Error creando el usuario.';
|
||||||
this.creando = false;
|
this.creando = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= CREAR AUTORIZANTE =============
|
||||||
|
|
||||||
|
crearAutorizante(): void {
|
||||||
|
this.mensajeAutorizanteOk = null;
|
||||||
|
this.mensajeAutorizanteError = null;
|
||||||
|
|
||||||
|
if (!this.nuevoAutorizante.numero_documento ||
|
||||||
|
!this.nuevoAutorizante.tipo_documento ||
|
||||||
|
!this.nuevoAutorizante.nombre) {
|
||||||
|
this.mensajeAutorizanteError = 'Completa los campos obligatorios.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numero = Number(this.nuevoAutorizante.numero_documento);
|
||||||
|
if (!Number.isFinite(numero)) {
|
||||||
|
this.mensajeAutorizanteError = 'El numero de documento debe ser numerico.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: CrearAutorizantePayload = {
|
||||||
|
numero_documento: numero,
|
||||||
|
tipo_documento: this.nuevoAutorizante.tipo_documento,
|
||||||
|
nombre: this.nuevoAutorizante.nombre,
|
||||||
|
telefono: this.nuevoAutorizante.telefono || undefined,
|
||||||
|
cargo: this.nuevoAutorizante.cargo || undefined,
|
||||||
|
activo: this.nuevoAutorizante.activo
|
||||||
|
};
|
||||||
|
|
||||||
|
this.creandoAutorizante = true;
|
||||||
|
|
||||||
|
this.pacienteService.crearAutorizante(payload).subscribe({
|
||||||
|
next: (resp: any) => {
|
||||||
|
this.mensajeAutorizanteOk =
|
||||||
|
resp?.mensaje || 'Autorizante creado correctamente.';
|
||||||
|
this.creandoAutorizante = false;
|
||||||
|
|
||||||
|
this.nuevoAutorizante = {
|
||||||
|
numero_documento: '',
|
||||||
|
tipo_documento: 'CC',
|
||||||
|
nombre: '',
|
||||||
|
telefono: '',
|
||||||
|
cargo: '',
|
||||||
|
activo: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cargarAutorizantesAdmin();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.mensajeAutorizanteError =
|
||||||
|
err?.error?.error || 'Error creando el autorizante.';
|
||||||
|
this.creandoAutorizante = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cambiarEstadoAutorizante(a: Autorizante, activo: boolean): void {
|
||||||
|
if (!confirm(`¿Seguro que deseas ${activo ? 'activar' : 'desactivar'} al autorizante "${a.nombre}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pacienteService.actualizarEstadoAutorizante(a.numero_documento, activo).subscribe({
|
||||||
|
next: (resp: any) => {
|
||||||
|
if (resp?.autorizante) {
|
||||||
|
a.activo = resp.autorizante.activo;
|
||||||
|
} else {
|
||||||
|
a.activo = activo;
|
||||||
|
}
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert('Error actualizando estado del autorizante.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarAutorizantesAdmin(): void {
|
||||||
|
this.cargandoAutorizantes = true;
|
||||||
|
this.errorAutorizantes = null;
|
||||||
|
|
||||||
|
this.pacienteService.obtenerAutorizantesAdmin().subscribe({
|
||||||
|
next: (autorizantes) => {
|
||||||
|
this.autorizantes = autorizantes;
|
||||||
|
this.cargandoAutorizantes = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorAutorizantes = 'Error cargando autorizantes.';
|
||||||
|
this.cargandoAutorizantes = false;
|
||||||
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -187,4 +320,12 @@ export class UsuariosComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackByUsuario(_index: number, usuario: any): number | string {
|
||||||
|
return usuario?.id_usuario ?? usuario?.username ?? _index;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByAutorizante(_index: number, autorizante: Autorizante): number | string {
|
||||||
|
return autorizante?.numero_documento ?? _index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
saludut-inpec/src/app/config.ts
Normal file
10
saludut-inpec/src/app/config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__SALUDUT_CONFIG__?: {
|
||||||
|
apiBaseUrl?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawApiBaseUrl = window.__SALUDUT_CONFIG__?.apiBaseUrl || '/api';
|
||||||
|
export const API_BASE_URL = rawApiBaseUrl.replace(/\/+$/, '');
|
||||||
@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Observable, BehaviorSubject, tap, catchError, throwError } from 'rxjs';
|
import { Observable, BehaviorSubject, tap, catchError, throwError } from 'rxjs';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { JobResponse } from './job-types';
|
||||||
|
import { API_BASE_URL } from '../config';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id_usuario: number;
|
id_usuario: number;
|
||||||
@ -44,7 +46,7 @@ export interface Rol {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly API_URL = 'http://localhost:3000';
|
private readonly API_URL = API_BASE_URL;
|
||||||
private readonly TOKEN_KEY = 'auth_token';
|
private readonly TOKEN_KEY = 'auth_token';
|
||||||
private readonly USER_KEY = 'current_user';
|
private readonly USER_KEY = 'current_user';
|
||||||
|
|
||||||
@ -208,9 +210,10 @@ export class AuthService {
|
|||||||
|
|
||||||
// =========== ADMIN: USUARIOS Y REPORTES ===========
|
// =========== ADMIN: USUARIOS Y REPORTES ===========
|
||||||
|
|
||||||
getUsuarios(): Observable<any[]> {
|
getUsuarios(limit = 200, offset = 0): Observable<any[]> {
|
||||||
const headers = this.getAuthHeaders();
|
const headers = this.getAuthHeaders();
|
||||||
return this.http.get<any[]>(`${this.API_URL}/api/usuarios`, { headers });
|
const params = { limit, offset };
|
||||||
|
return this.http.get<any[]>(`${this.API_URL}/api/usuarios`, { headers, params });
|
||||||
}
|
}
|
||||||
|
|
||||||
cambiarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
|
cambiarEstadoUsuario(idUsuario: number, activo: boolean): Observable<any> {
|
||||||
@ -228,28 +231,45 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Obtener autorizaciones por fecha (solo administradores)
|
// Obtener autorizaciones por fecha (solo administradores)
|
||||||
getAutorizacionesPorFecha(fechaInicio: string, fechaFin: string): Observable<any[]> {
|
getAutorizacionesPorFecha(
|
||||||
|
fechaInicio: string,
|
||||||
|
fechaFin: string,
|
||||||
|
limit = 500,
|
||||||
|
offset = 0
|
||||||
|
): Observable<any[]> {
|
||||||
const headers = this.getAuthHeaders();
|
const headers = this.getAuthHeaders();
|
||||||
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin };
|
const params = { fecha_inicio: fechaInicio, fecha_fin: fechaFin, limit, offset };
|
||||||
return this.http.get<any[]>(`${this.API_URL}/api/autorizaciones-por-fecha`, { headers, params });
|
return this.http.get<any[]>(`${this.API_URL}/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
|
// Obtener roles desde el backend
|
||||||
getRoles(): Observable<Rol[]> {
|
getRoles(): Observable<Rol[]> {
|
||||||
const headers = this.getAuthHeaders();
|
const headers = this.getAuthHeaders();
|
||||||
return this.http.get<Rol[]>(`${this.API_URL}/api/roles`, { headers });
|
return this.http.get<Rol[]>(`${this.API_URL}/api/roles`, { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crearJobPdfAutorizacion(
|
||||||
|
numeroAutorizacion: string,
|
||||||
|
version?: number
|
||||||
|
): Observable<JobResponse> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
const payload: any = { numero_autorizacion: numeroAutorizacion };
|
||||||
|
if (typeof version === 'number') {
|
||||||
|
payload.version = version;
|
||||||
|
}
|
||||||
|
return this.http.post<JobResponse>(
|
||||||
|
`${this.API_URL}/api/jobs/autorizacion-pdf`,
|
||||||
|
payload,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
crearJobZipAutorizaciones(fechaInicio: string, fechaFin: string): Observable<JobResponse> {
|
||||||
|
const headers = this.getAuthHeaders();
|
||||||
|
return this.http.post<JobResponse>(
|
||||||
|
`${this.API_URL}/api/jobs/autorizaciones-zip`,
|
||||||
|
{ fecha_inicio: fechaInicio, fecha_fin: fechaFin },
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
saludut-inpec/src/app/services/job-types.ts
Normal file
28
saludut-inpec/src/app/services/job-types.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export type JobStatus = 'queued' | 'running' | 'completed' | 'failed';
|
||||||
|
|
||||||
|
export interface JobError {
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobResult {
|
||||||
|
ok?: boolean;
|
||||||
|
mensaje?: string;
|
||||||
|
activos?: number | null;
|
||||||
|
antiguos?: number | null;
|
||||||
|
referencia?: number | null;
|
||||||
|
cubiertosActivos?: number | null;
|
||||||
|
downloadUrl?: string;
|
||||||
|
fileName?: string;
|
||||||
|
contentType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobResponse {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
status: JobStatus;
|
||||||
|
createdAt?: string;
|
||||||
|
startedAt?: string;
|
||||||
|
finishedAt?: string;
|
||||||
|
result?: JobResult | null;
|
||||||
|
error?: JobError | null;
|
||||||
|
}
|
||||||
42
saludut-inpec/src/app/services/jobs.ts
Normal file
42
saludut-inpec/src/app/services/jobs.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable, timer } from 'rxjs';
|
||||||
|
import { switchMap, takeWhile } from 'rxjs/operators';
|
||||||
|
import { AuthService } from './auth';
|
||||||
|
import { JobResponse } from './job-types';
|
||||||
|
import { API_BASE_URL } from '../config';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class JobsService {
|
||||||
|
private readonly API_URL = API_BASE_URL;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private authService: AuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getJob(jobId: string): Observable<JobResponse> {
|
||||||
|
const headers = this.authService.getAuthHeaders();
|
||||||
|
return this.http.get<JobResponse>(`${this.API_URL}/api/jobs/${jobId}`, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
pollJob(jobId: string, intervalMs = 2000): Observable<JobResponse> {
|
||||||
|
return timer(0, intervalMs).pipe(
|
||||||
|
switchMap(() => this.getJob(jobId)),
|
||||||
|
takeWhile(
|
||||||
|
(job) => job.status === 'queued' || job.status === 'running',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadJobFile(jobId: string): Observable<Blob> {
|
||||||
|
const headers = this.authService.getAuthHeaders();
|
||||||
|
return this.http.get(`${this.API_URL}/api/jobs/${jobId}/download`, {
|
||||||
|
headers,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AuthService } from './auth';
|
import { AuthService } from './auth';
|
||||||
|
import { JobResponse } from './job-types';
|
||||||
|
import { API_BASE_URL } from '../config';
|
||||||
|
|
||||||
|
|
||||||
// ====== Interfaces ======
|
// ====== Interfaces ======
|
||||||
@ -19,6 +21,8 @@ export interface Paciente {
|
|||||||
sexo: string;
|
sexo: string;
|
||||||
codigo_establecimiento?: string | null;
|
codigo_establecimiento?: string | null;
|
||||||
nombre_establecimiento?: string | null;
|
nombre_establecimiento?: string | null;
|
||||||
|
epc_departamento?: string | null;
|
||||||
|
epc_ciudad?: string | null;
|
||||||
estado?: string | null;
|
estado?: string | null;
|
||||||
fecha_ingreso?: string | null;
|
fecha_ingreso?: string | null;
|
||||||
tiempo_reclusion?: number | null;
|
tiempo_reclusion?: number | null;
|
||||||
@ -42,10 +46,22 @@ export interface Autorizante {
|
|||||||
activo: boolean;
|
activo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CrearAutorizantePayload {
|
||||||
|
numero_documento: number;
|
||||||
|
tipo_documento: string;
|
||||||
|
nombre: string;
|
||||||
|
telefono?: string;
|
||||||
|
cargo?: string;
|
||||||
|
activo?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CrearAutorizacionPayload {
|
export interface CrearAutorizacionPayload {
|
||||||
interno: string;
|
interno: string;
|
||||||
id_ips: number;
|
id_ips: number;
|
||||||
numero_documento_autorizante: number;
|
numero_documento_autorizante: number;
|
||||||
|
cup_codigo: string;
|
||||||
|
tipo_autorizacion?: string;
|
||||||
|
tipo_servicio?: string;
|
||||||
observacion?: string;
|
observacion?: string;
|
||||||
fecha_autorizacion?: string; // yyyy-MM-dd
|
fecha_autorizacion?: string; // yyyy-MM-dd
|
||||||
}
|
}
|
||||||
@ -53,23 +69,37 @@ export interface CrearAutorizacionPayload {
|
|||||||
export interface RespuestaAutorizacion {
|
export interface RespuestaAutorizacion {
|
||||||
numero_autorizacion: string;
|
numero_autorizacion: string;
|
||||||
fecha_autorizacion: string;
|
fecha_autorizacion: string;
|
||||||
|
version?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutorizacionListado {
|
export interface AutorizacionListado {
|
||||||
numero_autorizacion: string;
|
numero_autorizacion: string;
|
||||||
fecha_autorizacion: string;
|
fecha_autorizacion: string;
|
||||||
observacion: string | null;
|
observacion: string | null;
|
||||||
|
cup_codigo?: string | null;
|
||||||
|
cup_descripcion?: string | null;
|
||||||
|
cup_nivel?: string | null;
|
||||||
|
cup_especialidad?: string | null;
|
||||||
|
tipo_autorizacion?: string | null;
|
||||||
|
tipo_servicio?: string | null;
|
||||||
|
version?: number | null;
|
||||||
|
id_ips?: number | null;
|
||||||
|
numero_documento_autorizante?: number | null;
|
||||||
nombre_ips: string;
|
nombre_ips: string;
|
||||||
municipio: string;
|
municipio: string;
|
||||||
departamento: string;
|
departamento: string;
|
||||||
nombre_autorizante: string;
|
nombre_autorizante: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RespuestaCargaExcel {
|
export interface AutorizacionVersion {
|
||||||
ok: boolean;
|
version: number;
|
||||||
mensaje: string;
|
fecha_version?: string | null;
|
||||||
activos: number | null;
|
fecha_autorizacion?: string | null;
|
||||||
antiguos: number | null;
|
}
|
||||||
|
|
||||||
|
export interface AutorizacionVersionResponse {
|
||||||
|
version_actual: number;
|
||||||
|
versiones: AutorizacionVersion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Servicio ======
|
// ====== Servicio ======
|
||||||
@ -78,7 +108,7 @@ export interface RespuestaCargaExcel {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class PacienteService {
|
export class PacienteService {
|
||||||
private readonly API_URL = 'http://localhost:3000';
|
private readonly API_URL = API_BASE_URL;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
@ -118,8 +148,11 @@ export class PacienteService {
|
|||||||
|
|
||||||
// ---- IPS y autorizantes ----
|
// ---- IPS y autorizantes ----
|
||||||
|
|
||||||
obtenerIpsPorInterno(interno: string): Observable<Ips[]> {
|
obtenerIpsPorInterno(interno: string, verTodas = false): Observable<Ips[]> {
|
||||||
const params = new HttpParams().set('interno', interno);
|
let params = new HttpParams().set('interno', interno);
|
||||||
|
if (verTodas) {
|
||||||
|
params = params.set('ver_todas', '1');
|
||||||
|
}
|
||||||
return this.http.get<Ips[]>(`${this.API_URL}/api/ips-por-interno`, {
|
return this.http.get<Ips[]>(`${this.API_URL}/api/ips-por-interno`, {
|
||||||
params,
|
params,
|
||||||
headers: this.getAuthHeaders()
|
headers: this.getAuthHeaders()
|
||||||
@ -132,6 +165,20 @@ export class PacienteService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
obtenerAutorizantesAdmin(): Observable<Autorizante[]> {
|
||||||
|
return this.http.get<Autorizante[]>(`${this.API_URL}/api/autorizantes/admin`, {
|
||||||
|
headers: this.getAuthHeaders()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actualizarEstadoAutorizante(numeroDocumento: number, activo: boolean): Observable<any> {
|
||||||
|
return this.http.patch(
|
||||||
|
`${this.API_URL}/api/autorizantes/${numeroDocumento}/estado`,
|
||||||
|
{ activo },
|
||||||
|
{ headers: this.getAuthHeaders() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Autorizaciones ----
|
// ---- Autorizaciones ----
|
||||||
|
|
||||||
crearAutorizacion(payload: CrearAutorizacionPayload): Observable<RespuestaAutorizacion> {
|
crearAutorizacion(payload: CrearAutorizacionPayload): Observable<RespuestaAutorizacion> {
|
||||||
@ -142,6 +189,24 @@ export class PacienteService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualizarAutorizacion(
|
||||||
|
numeroAutorizacion: string,
|
||||||
|
payload: CrearAutorizacionPayload
|
||||||
|
): Observable<RespuestaAutorizacion> {
|
||||||
|
return this.http.put<RespuestaAutorizacion>(
|
||||||
|
`${this.API_URL}/api/autorizaciones/${numeroAutorizacion}`,
|
||||||
|
payload,
|
||||||
|
{ headers: this.getAuthHeaders() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
obtenerVersionesAutorizacion(numeroAutorizacion: string): Observable<AutorizacionVersionResponse> {
|
||||||
|
return this.http.get<AutorizacionVersionResponse>(
|
||||||
|
`${this.API_URL}/api/autorizaciones/${numeroAutorizacion}/versiones`,
|
||||||
|
{ headers: this.getAuthHeaders() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
obtenerAutorizacionesPorInterno(interno: string): Observable<AutorizacionListado[]> {
|
obtenerAutorizacionesPorInterno(interno: string): Observable<AutorizacionListado[]> {
|
||||||
const params = new HttpParams().set('interno', interno);
|
const params = new HttpParams().set('interno', interno);
|
||||||
return this.http.get<AutorizacionListado[]>(
|
return this.http.get<AutorizacionListado[]>(
|
||||||
@ -151,7 +216,7 @@ export class PacienteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Cargar Excel (Python + SQL) ----
|
// ---- Cargar Excel (Python + SQL) ----
|
||||||
cargarExcelPacientes(formData: FormData): Observable<RespuestaCargaExcel> {
|
cargarExcelPacientes(formData: FormData): Observable<JobResponse> {
|
||||||
// 1. Obtenemos el token desde el AuthService.
|
// 1. Obtenemos el token desde el AuthService.
|
||||||
// Asumimos que tu AuthService tiene un método para obtener el token.
|
// Asumimos que tu AuthService tiene un método para obtener el token.
|
||||||
const token = this.authService.getToken();
|
const token = this.authService.getToken();
|
||||||
@ -163,21 +228,40 @@ export class PacienteService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 3. Hacemos la petición con estos headers limpios.
|
// 3. Hacemos la petición con estos headers limpios.
|
||||||
return this.http.post<RespuestaCargaExcel>(
|
return this.http.post<JobResponse>(
|
||||||
`${this.API_URL}/api/cargar-excel-pacientes`,
|
`${this.API_URL}/api/cargar-excel-pacientes`,
|
||||||
formData,
|
formData,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Descargar PDF de autorización ----
|
cargarExcelCups(formData: FormData): Observable<JobResponse> {
|
||||||
descargarPdfAutorizacion(numeroAutorizacion: string) {
|
const token = this.authService.getToken();
|
||||||
const params = new HttpParams().set('numero_autorizacion', numeroAutorizacion);
|
const headers = new HttpHeaders({
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
});
|
||||||
|
|
||||||
return this.http.get(`${this.API_URL}/api/generar-pdf-autorizacion`, {
|
return this.http.post<JobResponse>(
|
||||||
headers: this.getAuthHeaders(),
|
`${this.API_URL}/api/cargar-cups`,
|
||||||
|
formData,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
crearAutorizante(payload: CrearAutorizantePayload): Observable<any> {
|
||||||
|
return this.http.post(
|
||||||
|
`${this.API_URL}/api/autorizantes`,
|
||||||
|
payload,
|
||||||
|
{ headers: this.getAuthHeaders() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarCupsCubiertos(termino: string): Observable<any[]> {
|
||||||
|
const params = new HttpParams().set('q', termino);
|
||||||
|
return this.http.get<any[]>(`${this.API_URL}/api/cups-cubiertos`, {
|
||||||
params,
|
params,
|
||||||
responseType: 'blob' as 'json' // <- importante para TypeScript
|
headers: this.getAuthHeaders()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>SaludutInpec</title>
|
<title>SaludutInpec</title>
|
||||||
@ -13,6 +13,12 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"/>
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.__SALUDUT_CONFIG__ = {
|
||||||
|
apiBaseUrl: '/api'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'zone.js';
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { AppComponent } from './app/app';
|
import { AppComponent } from './app/app';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* ============================
|
/* ============================
|
||||||
Variables de diseño (tema)
|
Variables de diseno (tema)
|
||||||
============================ */
|
============================ */
|
||||||
:root {
|
:root {
|
||||||
--font-main: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
--font-main: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
@ -20,446 +20,271 @@
|
|||||||
--color-success: #2e7d32;
|
--color-success: #2e7d32;
|
||||||
--color-error: #c62828;
|
--color-error: #c62828;
|
||||||
|
|
||||||
|
--color-table-head: #f2f2f2;
|
||||||
|
--color-table-row: #ffffff;
|
||||||
|
--color-table-row-alt: #fafafa;
|
||||||
|
--color-table-hover: #e8f2ff;
|
||||||
|
|
||||||
|
--color-input-bg: #ffffff;
|
||||||
|
--color-input-border: #cccccc;
|
||||||
|
|
||||||
|
--color-btn-secondary-bg: #ffffff;
|
||||||
|
--color-btn-secondary-text: #1976d2;
|
||||||
|
--color-btn-secondary-border: #1976d2;
|
||||||
|
|
||||||
|
--color-cup-bg: #f6f8fc;
|
||||||
|
--color-cup-border: #dbe3ef;
|
||||||
|
--color-cup-item-bg: #ffffff;
|
||||||
|
--color-cup-item-border: #dfe6f2;
|
||||||
|
--color-cup-item-hover: #8ab6f9;
|
||||||
|
|
||||||
--font-size-base: 14px;
|
--font-size-base: 14px;
|
||||||
--font-size-sm: 13px;
|
--font-size-sm: 13px;
|
||||||
--font-size-lg: 1.4rem;
|
--font-size-lg: 1.4rem;
|
||||||
--radius-card: 12px;
|
--radius-card: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host {
|
[data-theme="dark"] {
|
||||||
display: block;
|
--color-primary: #4fa3ff;
|
||||||
|
--color-primary-dark: #2d7dcc;
|
||||||
|
--color-primary-soft: #1d2b3b;
|
||||||
|
--color-header-grad-2: #1f4b87;
|
||||||
|
|
||||||
|
--color-bg: #0f141a;
|
||||||
|
--color-card: #151c24;
|
||||||
|
--color-border: #2a3440;
|
||||||
|
|
||||||
|
--color-text-main: #e7edf5;
|
||||||
|
--color-text-muted: #a8b3c2;
|
||||||
|
|
||||||
|
--color-success: #3bb273;
|
||||||
|
--color-error: #ff6b6b;
|
||||||
|
|
||||||
|
--color-table-head: #1f2a36;
|
||||||
|
--color-table-row: #151c24;
|
||||||
|
--color-table-row-alt: #19222c;
|
||||||
|
--color-table-hover: #223046;
|
||||||
|
|
||||||
|
--color-input-bg: #111821;
|
||||||
|
--color-input-border: #2c3a4a;
|
||||||
|
|
||||||
|
--color-btn-secondary-bg: #121a23;
|
||||||
|
--color-btn-secondary-text: #cfe4ff;
|
||||||
|
--color-btn-secondary-border: #375a8a;
|
||||||
|
|
||||||
|
--color-cup-bg: #101821;
|
||||||
|
--color-cup-border: #243244;
|
||||||
|
--color-cup-item-bg: #151c24;
|
||||||
|
--color-cup-item-border: #273241;
|
||||||
|
--color-cup-item-hover: #4fa3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
font-family: var(--font-main);
|
font-family: var(--font-main);
|
||||||
background-color: var(--color-bg);
|
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);
|
color: var(--color-text-main);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Header con logo SALUD UT === */
|
img {
|
||||||
|
|
||||||
.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;
|
display: block;
|
||||||
font-weight: 600;
|
max-width: 100%;
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-role {
|
button,
|
||||||
display: block;
|
input,
|
||||||
font-size: 0.8rem;
|
select,
|
||||||
opacity: 0.8;
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
/* === Layout base === */
|
||||||
background: rgba(255, 255, 255, 0.1);
|
.page-shell {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
min-height: 100vh;
|
||||||
color: white;
|
background-color: var(--color-bg);
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn:hover {
|
.content-container {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
max-width: 1200px;
|
||||||
|
margin: 24px auto 40px;
|
||||||
|
padding: 0 16px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-saludut {
|
/* === Tarjetas === */
|
||||||
padding: 6px 12px;
|
.card {
|
||||||
border-radius: 999px;
|
background: var(--color-card);
|
||||||
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);
|
border-radius: var(--radius-card);
|
||||||
padding: 20px 28px;
|
padding: 20px 24px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
margin-bottom: 24px;
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.card.compact {
|
||||||
display: flex;
|
padding: 16px 20px;
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row label {
|
.section-title {
|
||||||
min-width: 130px;
|
margin: 0 0 16px 0;
|
||||||
|
color: var(--color-text-main);
|
||||||
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
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 === */
|
/* === Botones === */
|
||||||
|
.btn {
|
||||||
button {
|
display: inline-flex;
|
||||||
padding: 8px 18px;
|
align-items: center;
|
||||||
border-radius: 4px;
|
gap: 6px;
|
||||||
border: none;
|
padding: 8px 16px;
|
||||||
background-color: var(--color-primary);
|
border-radius: 8px;
|
||||||
color: #fff;
|
border: 1px solid transparent;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #ffffff;
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
|
||||||
transition: background-color 0.15s ease, transform 0.05s ease,
|
transition: background-color 0.15s ease, transform 0.05s ease,
|
||||||
box-shadow 0.15s ease;
|
box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
.btn:hover:not(:disabled) {
|
||||||
background-color: var(--color-primary-dark);
|
background-color: var(--color-primary-dark);
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active {
|
.btn:active:not(:disabled) {
|
||||||
transform: scale(0.97);
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background-color: #ffffff;
|
background: var(--color-btn-secondary-bg);
|
||||||
color: var(--color-primary);
|
color: var(--color-btn-secondary-text);
|
||||||
border: 1px solid var(--color-primary);
|
border: 1px solid var(--color-btn-secondary-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover:not(:disabled) {
|
||||||
background-color: var(--color-primary-soft);
|
background: var(--color-primary-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-view-auths {
|
.btn-ghost {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
margin-top: 12px;
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-view-auths:hover {
|
.btn-ghost:hover:not(:disabled) {
|
||||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estado */
|
.btn-danger {
|
||||||
|
background: #c62828;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.estado {
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #10b981;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover:not(:disabled) {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Badges === */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Estado === */
|
||||||
|
.status {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.estado.error {
|
.status.error {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.estado.ok {
|
.status.ok {
|
||||||
color: var(--color-success);
|
color: var(--color-success);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Subir Excel === */
|
/* === Tabla base === */
|
||||||
|
.table {
|
||||||
.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%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
background-color: var(--color-card);
|
background-color: var(--color-card);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
|
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla th,
|
.table th,
|
||||||
.tabla td {
|
.table td {
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla th {
|
.table th {
|
||||||
background-color: #f2f2f2;
|
background-color: var(--color-table-head);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla tr:nth-child(even) td {
|
.table tr:nth-child(even) td {
|
||||||
background-color: #fafafa;
|
background-color: var(--color-table-row-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabla tr:hover td {
|
.table tr:hover td {
|
||||||
background-color: #e8f2ff;
|
background-color: var(--color-table-hover);
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user