Merge branch 'dev'
This commit is contained in:
commit
ad653dbdb4
66
.dockerignore
Normal file
66
.dockerignore
Normal file
@ -0,0 +1,66 @@
|
||||
# Dependencias
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Archivos de compilación
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Archivos de IDE y editores
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.vscode/*
|
||||
|
||||
# Archivos del sistema
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Archivos de entorno (excepto los de ejemplo)
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Archivos de Docker (para evitar recursión)
|
||||
Dockerfile
|
||||
Dockerfile.*
|
||||
docker-compose.yml
|
||||
docker-compose.*.yml
|
||||
|
||||
# Archivos Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Archivos de test y coverage
|
||||
/coverage
|
||||
/tests
|
||||
*.spec.ts
|
||||
|
||||
# Archivos de configuración
|
||||
.angular/cache
|
||||
.sass-cache/
|
||||
connect.lock
|
||||
libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# Archivos de build temporales
|
||||
.build
|
||||
.buildinfo
|
||||
.cache
|
||||
|
||||
# Otros archivos innecesarios
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
*.md
|
||||
LICENSE
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@ -5,11 +5,18 @@
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
/build
|
||||
backend/dist
|
||||
backend/build
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
backend/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
@ -33,10 +40,36 @@ yarn-error.log
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
backend/coverage
|
||||
backend/.nyc_output
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.sonarlint
|
||||
.vscode
|
||||
.scannerwork
|
||||
package-lock.json
|
||||
backend/package-lock.json
|
||||
**/.claude/settings.local.json
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
67
.htaccess
Normal file
67
.htaccess
Normal file
@ -0,0 +1,67 @@
|
||||
# Habilitar compresión GZIP
|
||||
<IfModule mod_deflate.c>
|
||||
# Comprimir HTML, CSS, JavaScript, Text, XML y fonts
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
|
||||
AddOutputFilterByType DEFLATE application/x-font
|
||||
AddOutputFilterByType DEFLATE application/x-font-opentype
|
||||
AddOutputFilterByType DEFLATE application/x-font-otf
|
||||
AddOutputFilterByType DEFLATE application/x-font-truetype
|
||||
AddOutputFilterByType DEFLATE application/x-font-ttf
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE font/opentype
|
||||
AddOutputFilterByType DEFLATE font/otf
|
||||
AddOutputFilterByType DEFLATE font/ttf
|
||||
AddOutputFilterByType DEFLATE image/svg+xml
|
||||
AddOutputFilterByType DEFLATE image/x-icon
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/javascript
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
|
||||
# Eliminar bugs de navegadores antiguos
|
||||
BrowserMatch ^Mozilla/4 gzip-only-text/html
|
||||
BrowserMatch ^Mozilla/4\.0[678] no-gzip
|
||||
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
|
||||
|
||||
# No comprimir imágenes (ya están comprimidas)
|
||||
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp)$ no-gzip
|
||||
</IfModule>
|
||||
|
||||
# Habilitar caché de navegador
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
|
||||
# Imágenes
|
||||
ExpiresByType image/jpeg "access plus 1 year"
|
||||
ExpiresByType image/gif "access plus 1 year"
|
||||
ExpiresByType image/png "access plus 1 year"
|
||||
ExpiresByType image/webp "access plus 1 year"
|
||||
ExpiresByType image/svg+xml "access plus 1 year"
|
||||
ExpiresByType image/x-icon "access plus 1 year"
|
||||
|
||||
# Video
|
||||
ExpiresByType video/mp4 "access plus 1 year"
|
||||
ExpiresByType video/mpeg "access plus 1 year"
|
||||
|
||||
# CSS, JavaScript
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType text/javascript "access plus 1 month"
|
||||
ExpiresByType application/javascript "access plus 1 month"
|
||||
|
||||
# Otros
|
||||
ExpiresByType application/pdf "access plus 1 month"
|
||||
ExpiresByType application/x-shockwave-flash "access plus 1 month"
|
||||
</IfModule>
|
||||
|
||||
# Reglas de SPA para Angular
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.html$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
197
README.md
197
README.md
@ -1,59 +1,178 @@
|
||||
# CronogramasPrimeng
|
||||
# Cronogramas PrimeNG Application
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.9.
|
||||
Este proyecto es una aplicación Angular 19 utilizando PrimeNG para la gestión de cronogramas.
|
||||
|
||||
## Instalación
|
||||
|
||||
### Requisitos Previos
|
||||
|
||||
- Node.js (versión 20.x recomendada)
|
||||
- npm (incluido con Node.js)
|
||||
- Git
|
||||
|
||||
### Pasos de Instalación
|
||||
|
||||
1. **Clonar el Repositorio**
|
||||
|
||||
```bash
|
||||
git clone <URL-del-repositorio>
|
||||
cd cronogramas-primeng
|
||||
```
|
||||
|
||||
2. **Instalar Dependencias**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Iniciar el Servidor de Desarrollo**
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
La aplicación estará disponible en `http://localhost:4200/`
|
||||
|
||||
### Dependencias Principales
|
||||
|
||||
El proyecto utiliza las siguientes bibliotecas clave:
|
||||
|
||||
- **Angular 19**: Framework base
|
||||
```bash
|
||||
npm install @angular/core @angular/common @angular/forms @angular/router @angular/compiler
|
||||
```
|
||||
|
||||
- **PrimeNG 19.1.0**: Biblioteca de componentes UI
|
||||
```bash
|
||||
npm install primeng @primeng/themes
|
||||
```
|
||||
|
||||
- **PrimeFlex 4.0.0**: Sistema de CSS flexible
|
||||
```bash
|
||||
npm install primeflex
|
||||
```
|
||||
|
||||
- **PrimeIcons 7.0.0**: Conjunto de iconos
|
||||
```bash
|
||||
npm install primeicons
|
||||
```
|
||||
|
||||
- **ExcelJS 4.4.0**: Para exportación a Excel
|
||||
```bash
|
||||
npm install exceljs
|
||||
```
|
||||
|
||||
- **File-Saver 2.0.5**: Para guardar archivos en el cliente
|
||||
```bash
|
||||
npm install file-saver @types/file-saver
|
||||
```
|
||||
|
||||
## Estructura del Proyecto
|
||||
|
||||
```
|
||||
/cronogramas-primeng/
|
||||
├── src/ # Código fuente
|
||||
│ ├── app/ # Componentes Angular
|
||||
│ │ ├── components/ # Componentes compartidos
|
||||
│ │ ├── guards/ # Guardias de ruta
|
||||
│ │ ├── interceptors/ # Interceptores HTTP
|
||||
│ │ ├── models/ # Interfaces de datos
|
||||
│ │ ├── pages/ # Componentes de página
|
||||
│ │ ├── services/ # Servicios
|
||||
│ │ └── utils/ # Utilidades
|
||||
│ ├── index.html # HTML principal
|
||||
│ ├── main.ts # Punto de entrada
|
||||
│ └── styles.scss # Estilos globales
|
||||
├── public/ # Recursos estáticos
|
||||
├── angular.json # Configuración de Angular
|
||||
├── package.json # Dependencias y scripts
|
||||
├── tsconfig.json # Configuración TypeScript
|
||||
├── Dockerfile # Configuración de Docker
|
||||
└── docker-compose.yml # Configuración Docker Compose
|
||||
```
|
||||
|
||||
## Estructura de Interfaces
|
||||
|
||||
Se han creado las siguientes interfaces para los modelos de datos:
|
||||
|
||||
- **Cronograma**: Modelo base para todos los cronogramas
|
||||
- **Empresa**: Modelo para empresas
|
||||
- **TipoCarga**: Modelo para tipos de carga
|
||||
- **EstadoAprobacion**: Modelo para estados de aprobación
|
||||
- **ActualizacionPd**: Modelo para actualizaciones de PD
|
||||
- **AjustePd**: Modelo para ajustes de PD
|
||||
- **UnidadInformacion**: Modelo para unidades de información
|
||||
|
||||
## Servicios
|
||||
|
||||
Los servicios implementados permiten conectarse a un backend mediante HTTP:
|
||||
|
||||
- **CronogramaService**: CRUD para cronogramas
|
||||
- **EmpresaService**: CRUD para empresas
|
||||
- **ActualizacionPdService**: CRUD para actualizaciones de PD
|
||||
- **AjustePdService**: CRUD para ajustes de PD
|
||||
- **UnidadInformacionService**: CRUD para unidades de información
|
||||
- **TipoCargaService**: Consulta de tipos de carga
|
||||
- **EstadoAprobacionService**: Consulta de estados de aprobación
|
||||
- **AuthService**: Autenticación y gestión de tokens
|
||||
|
||||
## Seguridad
|
||||
|
||||
La aplicación incluye:
|
||||
|
||||
- Interceptor HTTP para añadir tokens de autenticación
|
||||
- Guard para proteger rutas
|
||||
- Login y sistema de autenticación
|
||||
|
||||
## Comandos Disponibles
|
||||
|
||||
| Comando | Descripción |
|
||||
|---------|-------------|
|
||||
| `npm start` | Inicia servidor de desarrollo |
|
||||
| `npm run build` | Compila el proyecto |
|
||||
| `npm run build:prod` | Compila para producción |
|
||||
| `npm run watch` | Compila en modo observador |
|
||||
| `npm test` | Ejecuta pruebas unitarias |
|
||||
|
||||
## Despliegue con Docker
|
||||
|
||||
El proyecto incluye archivos Docker para facilitar el despliegue:
|
||||
|
||||
1. Construir y ejecutar con Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Esto expone la aplicación en `http://localhost:80`.
|
||||
|
||||
2. Construir manualmente con Docker:
|
||||
|
||||
```bash
|
||||
docker build -t cronogramas-primeng .
|
||||
docker run -p 80:80 cronogramas-primeng
|
||||
```
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
Para iniciar un servidor de desarrollo local, ejecute:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
Una vez que el servidor esté en funcionamiento, abra su navegador y navegue a `http://localhost:4200/`. La aplicación se recargará automáticamente cuando modifique cualquiera de los archivos fuente.
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
Para compilar el proyecto ejecute:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
Esto compilará su proyecto y almacenará los artefactos de compilación en el directorio `dist/`. Por defecto, la compilación para producción optimiza su aplicación para el rendimiento y la velocidad.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
Para más información sobre el uso de Angular CLI, incluyendo referencias detalladas de comandos, visite la página [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli).
|
||||
63
angular.json
63
angular.json
@ -3,7 +3,7 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"cronogramas-primeng": {
|
||||
"cronogramas": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
@ -17,7 +17,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/cronogramas-primeng",
|
||||
"outputPath": "dist/cronogramas",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
@ -29,29 +29,44 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": ".htaccess",
|
||||
"input": ".",
|
||||
"output": "/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/animate.css/animate.min.css",
|
||||
{
|
||||
"input": "node_modules/animate.css/animate.min.css",
|
||||
"bundleName": "animate",
|
||||
"inject": true
|
||||
},
|
||||
{
|
||||
"input": "node_modules/primeflex/primeflex.css",
|
||||
"bundleName": "primeflex",
|
||||
"inject": true
|
||||
},
|
||||
{
|
||||
"input": "node_modules/primeicons/primeicons.css",
|
||||
"bundleName": "primeicons",
|
||||
"inject": true
|
||||
}
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"file-saver",
|
||||
"exceljs"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"outputHashing": "all",
|
||||
"optimization": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
@ -65,10 +80,10 @@
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "cronogramas-primeng:build:production"
|
||||
"buildTarget": "cronogramas:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "cronogramas-primeng:build:development"
|
||||
"buildTarget": "cronogramas:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
@ -89,6 +104,11 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": ".htaccess",
|
||||
"input": ".",
|
||||
"output": "/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
@ -99,5 +119,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
}
|
||||
1167
package-lock.json
generated
1167
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "cronogramas-primeng",
|
||||
"version": "0.0.0",
|
||||
"name": "cronogramas",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --configuration production",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
"test": "ng test",
|
||||
"postinstall": "node setup-project.js"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@ -19,6 +21,9 @@
|
||||
"@angular/router": "^19.2.0",
|
||||
"@primeng/themes": "^19.1.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"pdfmake": "^0.2.19",
|
||||
"primeflex": "^4.0.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primeng": "^19.1.0",
|
||||
@ -30,7 +35,10 @@
|
||||
"@angular-devkit/build-angular": "^19.2.9",
|
||||
"@angular/cli": "^19.2.9",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/pdfmake": "^0.2.11",
|
||||
"@types/xlsx": "^0.0.35",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
@ -38,5 +46,6 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
},
|
||||
"description": "cronogramas - Proyecto generado desde template"
|
||||
}
|
||||
|
||||
BIN
public/img/footer-logo.webp
Normal file
BIN
public/img/footer-logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/img/header2.webp
Normal file
BIN
public/img/header2.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
147
setup-project.js
Normal file
147
setup-project.js
Normal file
@ -0,0 +1,147 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log('🚀 Configurando nuevo proyecto basado en el template...');
|
||||
|
||||
// Preguntar por el nombre del proyecto
|
||||
rl.question('¿Cuál es el nombre del nuevo proyecto? ', (projectName) => {
|
||||
|
||||
// Modificar package.json
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// Guardar el nombre original para reportar cambios
|
||||
const originalName = packageJson.name;
|
||||
|
||||
// Actualizar el nombre del proyecto
|
||||
packageJson.name = projectName;
|
||||
|
||||
// Actualizar versión y descripción
|
||||
packageJson.version = '0.1.0';
|
||||
packageJson.description = `${projectName} - Proyecto generado desde template`;
|
||||
|
||||
// Guardar los cambios
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
console.log(`✅ package.json actualizado: nombre cambiado de ${originalName} a ${projectName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error al modificar package.json:', error);
|
||||
}
|
||||
|
||||
// Modificar angular.json
|
||||
const angularJsonPath = path.join(process.cwd(), 'angular.json');
|
||||
try {
|
||||
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
|
||||
|
||||
// Obtener el nombre del proyecto original (la primera clave en el objeto projects)
|
||||
const originalProjectName = Object.keys(angularJson.projects)[0];
|
||||
|
||||
// Crear nuevo objeto con el nuevo nombre del proyecto
|
||||
const projectConfig = angularJson.projects[originalProjectName];
|
||||
|
||||
// Buscar otros nombres de proyectos en el archivo (como en outputPath o en configuraciones)
|
||||
// Este es un enfoque para encontrar nombres de proyectos "ocultos" en otras partes del archivo
|
||||
let angularJsonString = JSON.stringify(angularJson);
|
||||
const possibleProjectNames = new Set();
|
||||
|
||||
// Buscar patrones como "dist/nombre-proyecto" o "nombre-proyecto:build"
|
||||
const outputPathRegex = /dist\/([a-zA-Z0-9-_]+)/g;
|
||||
const buildTargetRegex = /([a-zA-Z0-9-_]+):build/g;
|
||||
|
||||
let match;
|
||||
while (match = outputPathRegex.exec(angularJsonString)) {
|
||||
possibleProjectNames.add(match[1]);
|
||||
}
|
||||
|
||||
while (match = buildTargetRegex.exec(angularJsonString)) {
|
||||
possibleProjectNames.add(match[1]);
|
||||
}
|
||||
|
||||
// Actualizar el outputPath
|
||||
if (projectConfig.architect?.build?.options?.outputPath) {
|
||||
const oldOutputPath = projectConfig.architect.build.options.outputPath;
|
||||
projectConfig.architect.build.options.outputPath = `dist/${projectName}`;
|
||||
console.log(`✅ Actualizado outputPath: de "${oldOutputPath}" a "dist/${projectName}"`);
|
||||
}
|
||||
|
||||
// Actualizar referencias en las configuraciones de serve
|
||||
if (projectConfig.architect?.serve?.configurations) {
|
||||
Object.keys(projectConfig.architect.serve.configurations).forEach(configKey => {
|
||||
const config = projectConfig.architect.serve.configurations[configKey];
|
||||
if (config.buildTarget) {
|
||||
// Buscar el patrón "nombreProyecto:build:config"
|
||||
const parts = config.buildTarget.split(':');
|
||||
if (parts.length === 3) {
|
||||
const oldValue = config.buildTarget;
|
||||
config.buildTarget = `${projectName}:${parts[1]}:${parts[2]}`;
|
||||
console.log(`✅ Actualizada configuración de serve '${configKey}': de "${oldValue}" a "${config.buildTarget}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Buscar y reemplazar todas las posibles ocurrencias de otros nombres de proyectos
|
||||
possibleProjectNames.forEach(oldName => {
|
||||
if (oldName !== originalProjectName && oldName !== projectName) {
|
||||
// Convertir a string para hacer reemplazos globales
|
||||
const jsonStr = JSON.stringify(projectConfig);
|
||||
if (jsonStr.includes(oldName)) {
|
||||
const updatedJsonStr = jsonStr.replace(new RegExp(oldName, 'g'), projectName);
|
||||
// Convertir de vuelta a objeto
|
||||
const updatedConfig = JSON.parse(updatedJsonStr);
|
||||
// Reemplazar la configuración con la versión actualizada
|
||||
Object.assign(projectConfig, updatedConfig);
|
||||
console.log(`✅ Reemplazado nombre de proyecto adicional: "${oldName}" por "${projectName}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Eliminar la entrada original
|
||||
delete angularJson.projects[originalProjectName];
|
||||
|
||||
// Agregar nueva entrada con el nombre del proyecto
|
||||
angularJson.projects[projectName] = projectConfig;
|
||||
|
||||
// Actualizar defaultProject si existe
|
||||
if (angularJson.defaultProject === originalProjectName) {
|
||||
angularJson.defaultProject = projectName;
|
||||
}
|
||||
|
||||
// Actualizar rutas dentro de la configuración
|
||||
if (projectConfig.root === originalProjectName) {
|
||||
projectConfig.root = projectName;
|
||||
}
|
||||
|
||||
// Guardar los cambios
|
||||
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2));
|
||||
console.log(`✅ angular.json actualizado: nombre del proyecto cambiado de ${originalProjectName} a ${projectName}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error al modificar angular.json:', error);
|
||||
}
|
||||
|
||||
// Modificar también src/index.html para actualizar el título
|
||||
try {
|
||||
const indexHtmlPath = path.join(process.cwd(), 'src', 'index.html');
|
||||
let indexHtml = fs.readFileSync(indexHtmlPath, 'utf8');
|
||||
|
||||
// Reemplazar el título
|
||||
indexHtml = indexHtml.replace(/<title>.*<\/title>/, `<title>${projectName}</title>`);
|
||||
|
||||
// Guardar los cambios
|
||||
fs.writeFileSync(indexHtmlPath, indexHtml);
|
||||
console.log('✅ src/index.html actualizado: título actualizado');
|
||||
} catch (error) {
|
||||
console.error('❌ Error al modificar src/index.html:', error);
|
||||
}
|
||||
|
||||
console.log('\n🎉 Configuración completada! El proyecto está listo para ser usado.');
|
||||
console.log('Ejecuta `ng serve` para iniciar el servidor de desarrollo.');
|
||||
|
||||
rl.close();
|
||||
});
|
||||
47
sonar-project.properties
Normal file
47
sonar-project.properties
Normal file
@ -0,0 +1,47 @@
|
||||
# # Información del proyecto
|
||||
# sonar.projectKey=Cronogramas-siis-angular-primeng
|
||||
# sonar.projectName=Cronogramas-siis-angular-primeng
|
||||
# sonar.projectVersion=1.0.0
|
||||
|
||||
# # Ruta del código fuente
|
||||
# sonar.sources=src
|
||||
# sonar.exclusions=**/node_modules/**,**/*.spec.ts,**/environments/**,**/assets/**
|
||||
|
||||
# # Configuración TypeScript
|
||||
# sonar.typescript.lcov.reportPaths=coverage/lcov.info
|
||||
# sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
|
||||
# # URL de SonarQube y token de autenticación
|
||||
# sonar.host.url=https://sonar.valposystems.com/
|
||||
# sonar.token=sqp_487feb210e11c5b295651af4436a265b335cc063
|
||||
# sonar.scanner.responseTimeout=300
|
||||
# sonar.internal.analysis.failFast=false
|
||||
|
||||
# sonar-scanner \
|
||||
# -Dsonar.projectKey=cronogramas-valposystems \
|
||||
# -Dsonar.sources=. \
|
||||
# -Dsonar.host.url=https://sonarqubelts-community-production-662c.up.railway.app \
|
||||
# -Dsonar.login=sqp_a371c9d9d6b0099fd6287be83496cd3c16b3674f
|
||||
|
||||
|
||||
# Información del proyecto
|
||||
sonar.projectKey=cronogramas-valposystems
|
||||
sonar.projectName=cronogramas-valposystems
|
||||
sonar.projectVersion=1.0.0
|
||||
|
||||
# Ruta del código fuente
|
||||
sonar.sources=src
|
||||
sonar.exclusions=**/node_modules/**,**/*.spec.ts,**/environments/**,**/assets/**
|
||||
|
||||
# Configuración TypeScript
|
||||
sonar.typescript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
|
||||
# URL de SonarQube y token de autenticación
|
||||
sonar.host.url=https://sonar.lcespedes.dev/
|
||||
# Usa sonar.token en lugar de sonar.login
|
||||
sonar.login=sqp_a371c9d9d6b0099fd6287be83496cd3c16b3674f
|
||||
|
||||
# Configuración de timeout y análisis
|
||||
sonar.scanner.responseTimeout=300
|
||||
sonar.internal.analysis.failFast=false
|
||||
@ -1,2 +1,3 @@
|
||||
|
||||
<router-outlet />
|
||||
<p-confirmDialog></p-confirmDialog>
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||
import { ConfirmationService } from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, ConfirmDialogModule],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
styleUrl: './app.component.scss',
|
||||
providers: [ConfirmationService]
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'SACG - Sistema Administrador de Cronogramas';
|
||||
|
||||
@ -1,20 +1,31 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { providePrimeNG } from 'primeng/config';
|
||||
import Aura from '@primeng/themes/aura';
|
||||
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideRouter(
|
||||
routes,
|
||||
withPreloading(PreloadAllModules)
|
||||
),
|
||||
provideAnimations(),
|
||||
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
|
||||
providePrimeNG({
|
||||
theme: {
|
||||
preset: Aura
|
||||
preset: Aura,
|
||||
options: {
|
||||
darkModeSelector: false || 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
MessageService
|
||||
]
|
||||
};
|
||||
|
||||
@ -1,9 +1,54 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { Routes, PreloadAllModules } from '@angular/router';
|
||||
import { LoginComponent } from './pages/login/login.component';
|
||||
|
||||
import { LayoutComponent } from './components/layout/layout.component';
|
||||
import { authGuard } from './guards/auth.guard';
|
||||
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: '**', redirectTo: 'login' }
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
canActivate: [authGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'inicio', pathMatch: 'full' },
|
||||
{
|
||||
path: 'inicio',
|
||||
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
|
||||
data: { title: 'Inicio' }
|
||||
},
|
||||
{
|
||||
path: 'unidad-concesiones',
|
||||
loadComponent: () => import('./pages/concesiones/concesiones.component').then(m => m.ConcesionesComponent),
|
||||
data: { title: 'Unidad de Concesiones' }
|
||||
},
|
||||
{
|
||||
path: 'ct-actualizacion',
|
||||
loadComponent: () => import('./pages/actualizacion-pd/actualizacion-pd.component').then(m => m.ActualizacionPdComponent),
|
||||
data: { title: 'Cronograma temporal por actualización de PD' }
|
||||
},
|
||||
{
|
||||
path: 'ct-ajuste',
|
||||
loadComponent: () => import('./pages/ajuste-pd/ajuste-pd.component').then(m => m.AjustePdComponent),
|
||||
data: { title: 'Cronograma temporal por ajuste de PD' }
|
||||
},
|
||||
{
|
||||
path: 'resumen',
|
||||
loadComponent: () => import('./pages/resumen/resumen.component').then(m => m.ResumenComponent),
|
||||
data: { title: 'Resumen' }
|
||||
},
|
||||
{
|
||||
path: 'unidad-informacion',
|
||||
loadComponent: () => import('./pages/unidad-informacion/unidad-informacion.component').then(m => m.UnidadInformacionComponent),
|
||||
data: { title: 'Unidad de Información' }
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
component: NotFoundComponent,
|
||||
data: { title: 'Error 404' }
|
||||
},
|
||||
]
|
||||
},
|
||||
{ path: '**', redirectTo: '404' }
|
||||
|
||||
];
|
||||
|
||||
28
src/app/components/alert-dialog/alert-dialog.component.html
Normal file
28
src/app/components/alert-dialog/alert-dialog.component.html
Normal file
@ -0,0 +1,28 @@
|
||||
<p-dialog
|
||||
[(visible)]="visible"
|
||||
[modal]="true"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
[closeOnEscape]="true"
|
||||
[style]="{width: '400px'}"
|
||||
[contentStyle]="{'text-align': 'center', 'padding': '20px'}"
|
||||
styleClass="alert-dialog"
|
||||
[header]="title">
|
||||
|
||||
<div class="flex flex-column align-items-center">
|
||||
<i class="pi text-8xl mb-4" [ngClass]="getIconClass()"></i>
|
||||
<div class="text-xl font-semibold mb-3" *ngIf="title">{{ title }}</div>
|
||||
<div class="text-center mb-5">{{ message }}</div>
|
||||
|
||||
<div class="flex justify-content-center gap-2">
|
||||
<button *ngIf="showCancelButton" pButton (click)="cancel()"
|
||||
class="p-button-outlined p-button-secondary"
|
||||
[label]="cancelButtonText">
|
||||
</button>
|
||||
<button *ngIf="showConfirmButton" pButton (click)="confirm()"
|
||||
class="p-button-primary"
|
||||
[label]="confirmButtonText">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</p-dialog>
|
||||
11
src/app/components/alert-dialog/alert-dialog.component.scss
Normal file
11
src/app/components/alert-dialog/alert-dialog.component.scss
Normal file
@ -0,0 +1,11 @@
|
||||
:host ::ng-deep {
|
||||
.alert-dialog {
|
||||
.p-dialog-header {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.p-dialog-content {
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/app/components/alert-dialog/alert-dialog.component.ts
Normal file
75
src/app/components/alert-dialog/alert-dialog.component.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface AlertDialogOptions {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
icon: 'success' | 'error' | 'warning' | 'info' | 'question';
|
||||
showConfirmButton: boolean;
|
||||
confirmButtonText: string;
|
||||
showCancelButton: boolean;
|
||||
cancelButtonText: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-alert-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, DialogModule, ButtonModule],
|
||||
templateUrl: './alert-dialog.component.html',
|
||||
styleUrls: ['./alert-dialog.component.scss']
|
||||
})
|
||||
export class AlertDialogComponent {
|
||||
visible = false;
|
||||
title = '';
|
||||
message = '';
|
||||
icon: 'success' | 'error' | 'warning' | 'info' | 'question' = 'success';
|
||||
showConfirmButton = true;
|
||||
confirmButtonText = 'Aceptar';
|
||||
showCancelButton = false;
|
||||
cancelButtonText = 'Cancelar';
|
||||
|
||||
private confirmSubject = new Subject<boolean>();
|
||||
public onConfirm = this.confirmSubject.asObservable();
|
||||
|
||||
show(options: Partial<AlertDialogOptions> = {}): void {
|
||||
this.visible = true;
|
||||
this.title = options.title || this.title;
|
||||
this.message = options.message || this.message;
|
||||
this.icon = options.icon || this.icon;
|
||||
this.showConfirmButton = options.showConfirmButton !== undefined ? options.showConfirmButton : this.showConfirmButton;
|
||||
this.confirmButtonText = options.confirmButtonText || this.confirmButtonText;
|
||||
this.showCancelButton = options.showCancelButton !== undefined ? options.showCancelButton : this.showCancelButton;
|
||||
this.cancelButtonText = options.cancelButtonText || this.cancelButtonText;
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
this.visible = false;
|
||||
this.confirmSubject.next(true);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.visible = false;
|
||||
this.confirmSubject.next(false);
|
||||
}
|
||||
|
||||
getIconClass(): string {
|
||||
switch (this.icon) {
|
||||
case 'success':
|
||||
return 'pi-check-circle text-green-500';
|
||||
case 'error':
|
||||
return 'pi-times-circle text-red-500';
|
||||
case 'warning':
|
||||
return 'pi-exclamation-triangle text-yellow-500';
|
||||
case 'info':
|
||||
return 'pi-info-circle text-blue-500';
|
||||
case 'question':
|
||||
return 'pi-question-circle text-purple-500';
|
||||
default:
|
||||
return 'pi-check-circle text-green-500';
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/app/components/footer/footer.component.html
Normal file
18
src/app/components/footer/footer.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer mt-5">
|
||||
<div class="footer-content">
|
||||
<div class="footer-left">
|
||||
<div class="footer-text">
|
||||
<div>Superintendencia de Servicios Sanitarios</div>
|
||||
<div>área de Información y Tecnologías</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-center">
|
||||
<img src="img/footer-logo.webp" alt="SISS Logo" class="footer-logo" loading="lazy" width="150" height="50">
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<div>Dirección: Moneda 673 Piso 9 - Metro Santa Lucía</div>
|
||||
<div>Mesa Central: 2 2382 4000</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
50
src/app/components/footer/footer.component.scss
Normal file
50
src/app/components/footer/footer.component.scss
Normal file
@ -0,0 +1,50 @@
|
||||
/* Footer Styles */
|
||||
.footer {
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 768px) {
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
23
src/app/components/footer/footer.component.spec.ts
Normal file
23
src/app/components/footer/footer.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FooterComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
11
src/app/components/footer/footer.component.ts
Normal file
11
src/app/components/footer/footer.component.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
imports: [],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrl: './footer.component.scss'
|
||||
})
|
||||
export class FooterComponent {
|
||||
|
||||
}
|
||||
26
src/app/components/layout/layout.component.html
Normal file
26
src/app/components/layout/layout.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
<div class="layout-wrapper">
|
||||
<p-toast baseZIndex="2000"></p-toast>
|
||||
|
||||
<!-- Overlay para cerrar el sidebar al hacer clic fuera (solo en móviles) -->
|
||||
<div *ngIf="isSidebarVisible && window.innerWidth <= 992" class="sidebar-overlay" (click)="toggleSidebar()"></div>
|
||||
|
||||
<!-- Fixed sidebar -->
|
||||
<div class="sidebar-wrapper" [ngClass]="{'sidebar-visible': isSidebarVisible}">
|
||||
<app-sidebar></app-sidebar>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="main-content-wrapper" [ngClass]="{'with-sidebar': isSidebarVisible && window.innerWidth > 992}">
|
||||
<!-- Top navbar -->
|
||||
<app-navbar (sidebarToggle)="toggleSidebar()"></app-navbar>
|
||||
|
||||
<!-- Page content with animations -->
|
||||
<div class="page-content">
|
||||
<app-route-animations></app-route-animations>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
</div>
|
||||
115
src/app/components/layout/layout.component.scss
Normal file
115
src/app/components/layout/layout.component.scss
Normal file
@ -0,0 +1,115 @@
|
||||
/* src/app/components/layout/layout.component.scss */
|
||||
.layout-wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
flex: 0 0 250px;
|
||||
width: 250px;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
background-color: #0088cc;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* En pantallas pequeñas, asegurarse que esté fuera de la vista cuando no es visible */
|
||||
&:not(.sidebar-visible) {
|
||||
transform: translateX(-100%);
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Para todas las pantallas cuando el sidebar está oculto */
|
||||
.sidebar-wrapper:not(.sidebar-visible) {
|
||||
transform: translateX(-250px);
|
||||
}
|
||||
|
||||
.main-content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
transition: margin-left 0.3s ease;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Solo en pantallas grandes (>992px) ajustar el margen cuando el sidebar está visible */
|
||||
@media (min-width: 993px) {
|
||||
.main-content-wrapper.with-sidebar {
|
||||
margin-left: 250px;
|
||||
width: calc(100% - 250px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Cuando el sidebar está oculto, ajustar el margen */
|
||||
.sidebar-wrapper:not(.sidebar-visible) ~ .main-content-wrapper {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas pequeñas y tablets */
|
||||
@media (max-width: 992px) {
|
||||
.sidebar-wrapper {
|
||||
transform: translateX(-100%);
|
||||
width: 250px;
|
||||
z-index: 1030; /* Z-index mayor para asegurar que esté sobre el contenido */
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.main-content-wrapper {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-wrapper.sidebar-visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Overlay para cuando el sidebar está visible en pantallas pequeñas */
|
||||
body.sidebar-visible::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1025;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
23
src/app/components/layout/layout.component.spec.ts
Normal file
23
src/app/components/layout/layout.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LayoutComponent } from './layout.component';
|
||||
|
||||
describe('LayoutComponent', () => {
|
||||
let component: LayoutComponent;
|
||||
let fixture: ComponentFixture<LayoutComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LayoutComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
65
src/app/components/layout/layout.component.ts
Normal file
65
src/app/components/layout/layout.component.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Component, OnInit, HostListener } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NavbarComponent } from '../navbar/navbar.component';
|
||||
import { SidebarComponent } from '../sidebar/sidebar.component';
|
||||
import { FooterComponent } from "../footer/footer.component";
|
||||
import { RouteAnimationsComponent } from '../route-animations/route-animations.component';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-layout',
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
NavbarComponent,
|
||||
SidebarComponent,
|
||||
FooterComponent,
|
||||
RouteAnimationsComponent,
|
||||
ToastModule
|
||||
],
|
||||
templateUrl: './layout.component.html',
|
||||
styleUrl: './layout.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class LayoutComponent implements OnInit {
|
||||
isSidebarVisible: boolean = true;
|
||||
window = window; // Exposición de window para su uso en el template
|
||||
private readonly MOBILE_BREAKPOINT = 991; // Breakpoint para ocultar sidebar automáticamente
|
||||
|
||||
ngOnInit() {
|
||||
// Comprobar el ancho inicial y ajustar el sidebar
|
||||
this.checkScreenSize();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.checkScreenSize();
|
||||
}
|
||||
|
||||
private checkScreenSize() {
|
||||
// Si la pantalla es menor a 420px, ocultar el sidebar automáticamente
|
||||
if (window.innerWidth < this.MOBILE_BREAKPOINT) {
|
||||
this.isSidebarVisible = false;
|
||||
} else if (window.innerWidth >= 992) {
|
||||
// En pantallas grandes, mostrar el sidebar por defecto
|
||||
this.isSidebarVisible = true;
|
||||
}
|
||||
// Entre 420px y 992px mantener el estado actual (toggle manual)
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
this.isSidebarVisible = !this.isSidebarVisible;
|
||||
|
||||
// Aplicar clase al body para todas las resoluciones
|
||||
document.body.classList.toggle('sidebar-visible', this.isSidebarVisible);
|
||||
|
||||
// Si cerramos el sidebar en resolución pequeña, forzar que el contenido no tenga margen
|
||||
if (!this.isSidebarVisible && window.innerWidth <= 992) {
|
||||
setTimeout(() => {
|
||||
document.querySelector('.main-content-wrapper')?.classList.remove('with-sidebar');
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/app/components/navbar/navbar.component.html
Normal file
33
src/app/components/navbar/navbar.component.html
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="navbar-container">
|
||||
<div class="navbar-left">
|
||||
<button pButton icon="pi pi-bars" class="p-button-text p-button-rounded sidebar-toggle"
|
||||
(click)="toggleSidebar()"></button>
|
||||
<span class="app-title">Administrador de Cronogramas</span>
|
||||
</div>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
|
||||
<div style="flex-grow: 1; text-align: center;">
|
||||
<img src="/img/teclado1.png" alt="Logo" style="width: 90% !important;height: 50px !important; opacity: 0.8; ">
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
<div class="navbar-right">
|
||||
<span class="user-name">{{ userName }}</span>
|
||||
<button pButton icon="pi pi-sign-out" class="p-button-text p-button-rounded logout-button"
|
||||
(click)="confirmarAccion()"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page title and breadcrumb -->
|
||||
<div class="page-header">
|
||||
<div class="page-title tituloNavbar">{{ pageTitle }}</div>
|
||||
<div class="breadcrumb-container">
|
||||
<a routerLink="/inicio" class="breadcrumb-link">Inicio</a>
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<span class="breadcrumb-current">{{ pageTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Componentes para mensajes y diálogos -->
|
||||
<p-toast></p-toast>
|
||||
<p-confirmDialog [style]="{width: '450px'}"></p-confirmDialog>
|
||||
131
src/app/components/navbar/navbar.component.scss
Normal file
131
src/app/components/navbar/navbar.component.scss
Normal file
@ -0,0 +1,131 @@
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #bcdaef;
|
||||
height: 48px;
|
||||
padding: 0 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
color: #343a40;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-weight: bold;
|
||||
color: #0a2847;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #0a2847;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
color: #0a2847;
|
||||
}
|
||||
|
||||
/* Page header styles */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
color: #0088cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Sobrescribir estilos de PrimeNG para los botones en el navbar */
|
||||
:host ::ng-deep .p-button.p-button-text {
|
||||
padding: 0.5rem;
|
||||
color: #0a2847;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.p-button-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive para contraer navbar en md y sm */
|
||||
@media (max-width: 991.98px) {
|
||||
/* .navbar-left, */
|
||||
.navbar-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
width: 208px;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive para ocultar título para resoluciones sm */
|
||||
@media (max-width: 767.98px) {
|
||||
.app-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
max-height: 50px;
|
||||
width: auto;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.logo-img {
|
||||
max-height: 35px;
|
||||
}
|
||||
}
|
||||
23
src/app/components/navbar/navbar.component.spec.ts
Normal file
23
src/app/components/navbar/navbar.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NavbarComponent } from './navbar.component';
|
||||
|
||||
describe('NavbarComponent', () => {
|
||||
let component: NavbarComponent;
|
||||
let fixture: ComponentFixture<NavbarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NavbarComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NavbarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
95
src/app/components/navbar/navbar.component.ts
Normal file
95
src/app/components/navbar/navbar.component.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Component, EventEmitter, Output, OnInit } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { filter, map, mergeMap } from 'rxjs/operators';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
ButtonModule,
|
||||
ConfirmDialogModule,
|
||||
ToastModule
|
||||
],
|
||||
providers: [ConfirmationService, MessageService],
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrl: './navbar.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class NavbarComponent implements OnInit {
|
||||
@Output() sidebarToggle = new EventEmitter<void>();
|
||||
pageTitle: string = 'Starter Pages';
|
||||
userName: string = '';
|
||||
|
||||
constructor(
|
||||
private confirmationService: ConfirmationService,
|
||||
private messageService: MessageService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private authService: AuthService
|
||||
) {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
map(() => {
|
||||
let route = this.activatedRoute;
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
}),
|
||||
mergeMap(route => route.data)
|
||||
).subscribe(data => {
|
||||
this.pageTitle = data['title'] || 'Sin título';
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Obtener nombre de usuario del usuario logueado
|
||||
this.authService.user$.subscribe(user => {
|
||||
if (user) {
|
||||
this.userName = user.name || user.username || 'Usuario';
|
||||
} else {
|
||||
this.userName = 'Usuario';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
this.sidebarToggle.emit();
|
||||
}
|
||||
|
||||
confirmarAccion() {
|
||||
this.confirmationService.confirm({
|
||||
message: '¿Estás seguro de que deseas cerrar sesión?',
|
||||
header: 'Cerrar Sesión',
|
||||
icon: 'pi pi-sign-out',
|
||||
acceptLabel: 'Sí, cerrar',
|
||||
rejectLabel: 'Cancelar',
|
||||
acceptButtonStyleClass: 'p-button-danger',
|
||||
rejectButtonStyleClass: 'p-button-secondary',
|
||||
accept: () => {
|
||||
this.logout();
|
||||
},
|
||||
reject: () => {
|
||||
console.log('Canceló cierre de sesión');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Sesión cerrada',
|
||||
detail: 'Has cerrado sesión exitosamente'
|
||||
});
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router, RouterOutlet, NavigationEnd, Event } from '@angular/router';
|
||||
import { Subscription, filter } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-route-animations',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet],
|
||||
template: `
|
||||
<div class="content-container" [class]="animationClass">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.content-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.animate__animated {
|
||||
animation-duration: 1.2s;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class RouteAnimationsComponent implements OnDestroy {
|
||||
// Clase de animación actual
|
||||
animationClass: string = 'animate__animated animate__fadeIn ';
|
||||
|
||||
// Animaciones disponibles
|
||||
private animations: string[] = [
|
||||
'animate__fadeIn',
|
||||
];
|
||||
|
||||
private routerSub: Subscription;
|
||||
|
||||
constructor(private router: Router) {
|
||||
// Suscribirse a los eventos de navegación
|
||||
this.routerSub = this.router.events
|
||||
.pipe(filter((event: Event) => event instanceof NavigationEnd))
|
||||
.subscribe(() => {
|
||||
// Resetear la animación primero
|
||||
this.animationClass = '';
|
||||
|
||||
// Aplicar una nueva animación después de un breve retraso
|
||||
setTimeout(() => {
|
||||
const randomIndex = Math.floor(Math.random() * this.animations.length);
|
||||
this.animationClass = `animate__animated ${this.animations[randomIndex]} `;
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Cancelar suscripción para evitar memory leaks
|
||||
if (this.routerSub) {
|
||||
this.routerSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/app/components/sidebar/sidebar.component.html
Normal file
68
src/app/components/sidebar/sidebar.component.html
Normal file
@ -0,0 +1,68 @@
|
||||
<div class="sidebar">
|
||||
<!-- Logo container -->
|
||||
<div class="logo-container">
|
||||
<!-- Imagen del logo con posicionamiento para el badge -->
|
||||
<div class="logo-image-container">
|
||||
<img src="img/SAC-2.jpeg" alt="SISS Logo" class="logo-image">
|
||||
<div class="version-badge">1.0</div>
|
||||
</div>
|
||||
<!-- Subtítulo del logo -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Separador -->
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<div class="menu-container">
|
||||
<ul class="sidebar-menu">
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/inicio" class="menu-link">
|
||||
<i class="menu-icon pi pi-home"></i>
|
||||
<span class="menu-text">Inicio</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/unidad-concesiones" class="menu-link">
|
||||
<i class="menu-icon pi pi-building"></i>
|
||||
<span class="menu-text">Unidad de Concesiones</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/ct-actualizacion" class="menu-link">
|
||||
<i class="menu-icon pi pi-flag"></i>
|
||||
<span class="menu-text">CT Actualización de PD</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/ct-ajuste" class="menu-link">
|
||||
<i class="menu-icon pi pi-sliders-h"></i>
|
||||
<span class="menu-text">CT Ajuste de PD</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/resumen" class="menu-link">
|
||||
<i class="menu-icon pi pi-list"></i>
|
||||
<span class="menu-text">Resumen</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/unidad-informacion" class="menu-link">
|
||||
<i class="menu-icon pi pi-box"></i>
|
||||
<span class="menu-text">Unidad de Información</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
218
src/app/components/sidebar/sidebar.component.scss
Normal file
218
src/app/components/sidebar/sidebar.component.scss
Normal file
@ -0,0 +1,218 @@
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
background-color: #bcdaef;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-image-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #0a2847;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 0.7rem;
|
||||
color: #0a2847;
|
||||
max-width: 180px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #a3c5e6;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
padding: 0.5rem 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
margin: 2px 0;
|
||||
padding: 0 0.5rem; /* Añadimos padding lateral al ítem */
|
||||
transition: all 0.3s ease-in-out; /* Suaviza todas las transiciones */
|
||||
}
|
||||
|
||||
.menu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #0a2847;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease-in-out; /* Transición para todos los cambios */
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: .25rem;
|
||||
width: calc(100% - 1rem); /* Reducimos el ancho para crear margen */
|
||||
position: relative; /* Para los efectos de pseudo-elementos */
|
||||
}
|
||||
|
||||
.menu-link:hover {
|
||||
background-color: #a2c9ec !important;
|
||||
transform: translateX(2px); /* Pequeño movimiento al hacer hover */
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 0.75rem;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
color: #0a2847;
|
||||
transition: transform 0.3s ease-in-out; /* Transición para el icono */
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 0.9rem;
|
||||
transition: font-weight 0.3s ease-in-out, color 0.3s ease-in-out; /* Transición para el texto */
|
||||
}
|
||||
|
||||
/* Efectos especiales para los ítems activos */
|
||||
.menu-item.active {
|
||||
padding-left: 0.75rem; /* Añade un poco más de padding a la izquierda */
|
||||
}
|
||||
|
||||
.menu-item.active .menu-icon {
|
||||
transform: scale(1.2); /* Hace el icono ligeramente más grande */
|
||||
color: #0066cc; /* Color más brillante para el icono */
|
||||
}
|
||||
|
||||
/* Indicador de elemento activo */
|
||||
.active-indicator {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.menu-item.active .active-indicator {
|
||||
opacity: 1;
|
||||
background-color: #0066cc;
|
||||
box-shadow: 0 0 8px 1px rgba(0, 102, 204, 0.8);
|
||||
animation: pulse 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: translateY(-50%) scale(0.8);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-50%) scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) scale(0.8);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item.active .menu-link {
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 100%) !important;
|
||||
border-left: 3px solid white !important;
|
||||
color: #706f6f !important;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24) !important;
|
||||
margin: 0 auto; /* Centra el elemento */
|
||||
/* Añadir animación de entrada */
|
||||
animation: activeItemEffect 0.4s ease-in-out, glowEffect 2s ease-in-out infinite alternate;
|
||||
position: relative; /* Para el efecto de resplandor */
|
||||
}
|
||||
|
||||
/* Definir la animación para el elemento activo - movimiento lateral */
|
||||
@keyframes activeItemEffect {
|
||||
0% {
|
||||
transform: translateX(-10px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(5px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Efecto de resplandor suave */
|
||||
@keyframes glowEffect {
|
||||
0% {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 5px rgba(255,255,255,0.8), 0 0 10px rgba(10, 40, 71, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Añadir pseudo-elemento para un brillo en el borde izquierdo */
|
||||
.menu-item.active .menu-link::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
background-color: white;
|
||||
animation: borderPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes borderPulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
23
src/app/components/sidebar/sidebar.component.spec.ts
Normal file
23
src/app/components/sidebar/sidebar.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SidebarComponent } from './sidebar.component';
|
||||
|
||||
describe('SidebarComponent', () => {
|
||||
let component: SidebarComponent;
|
||||
let fixture: ComponentFixture<SidebarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SidebarComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SidebarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
14
src/app/components/sidebar/sidebar.component.ts
Normal file
14
src/app/components/sidebar/sidebar.component.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { PrimeIcons } from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrl: './sidebar.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class SidebarComponent {
|
||||
|
||||
}
|
||||
21
src/app/components/visor-pdf/visor-pdf.component.html
Normal file
21
src/app/components/visor-pdf/visor-pdf.component.html
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="pdf-container">
|
||||
<div class="pdf-viewer">
|
||||
<iframe [src]="pdfSrc | safe" width="100%" height="100%"></iframe>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button pButton type="button" class="p-button p-button-danger" (click)="descargarPDF()">
|
||||
<i class="pi pi-file-pdf" style="margin-right: 8px;"></i>
|
||||
Descargar PDF
|
||||
</button>
|
||||
<button pButton type="button" class="p-button p-button-success ml-2" (click)="enviarPDF()" [disabled]="enviando">
|
||||
<ng-container *ngIf="!enviando; else cargando">
|
||||
<i class="pi pi-send" style="margin-right: 8px;"></i>
|
||||
Enviar
|
||||
</ng-container>
|
||||
<ng-template #cargando>
|
||||
<p-progressSpinner [style]="{width: '20px', height: '20px'}" styleClass="custom-spinner" strokeWidth="4" fill="var(--surface-ground)" animationDuration=".5s"></p-progressSpinner>
|
||||
<span style="margin-left: 8px;">Enviando...</span>
|
||||
</ng-template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
22
src/app/components/visor-pdf/visor-pdf.component.scss
Normal file
22
src/app/components/visor-pdf/visor-pdf.component.scss
Normal file
@ -0,0 +1,22 @@
|
||||
.pdf-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pdf-viewer {
|
||||
flex: 1;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
}
|
||||
23
src/app/components/visor-pdf/visor-pdf.component.spec.ts
Normal file
23
src/app/components/visor-pdf/visor-pdf.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VisorPdfComponent } from './visor-pdf.component';
|
||||
|
||||
describe('VisorPdfComponent', () => {
|
||||
let component: VisorPdfComponent;
|
||||
let fixture: ComponentFixture<VisorPdfComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VisorPdfComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VisorPdfComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
73
src/app/components/visor-pdf/visor-pdf.component.ts
Normal file
73
src/app/components/visor-pdf/visor-pdf.component.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog';
|
||||
import { SafePipe } from '../../../pipes/safe.pipe';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { ProgressSpinnerModule } from 'primeng/progressspinner';
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
import { PdfService } from '../../services/pdf.service';
|
||||
import { AlertService } from '../../services/alert.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-visor-pdf',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SafePipe,
|
||||
ButtonModule,
|
||||
ToastModule,
|
||||
ProgressSpinnerModule
|
||||
],
|
||||
templateUrl: './visor-pdf.component.html',
|
||||
styleUrls: ['./visor-pdf.component.scss']
|
||||
})
|
||||
export class VisorPdfComponent implements OnInit {
|
||||
product: any;
|
||||
pdfSrc: string = '';
|
||||
enviando: boolean = false;
|
||||
|
||||
// Inyectar servicios usando la nueva sintaxis de Angular
|
||||
private dialogRef = inject(DynamicDialogRef);
|
||||
private config = inject(DynamicDialogConfig);
|
||||
private messageService = inject(MessageService);
|
||||
private pdfService = inject(PdfService);
|
||||
private alertService = inject(AlertService);
|
||||
|
||||
ngOnInit() {
|
||||
// Obtener el producto pasado a través del servicio de diálogo
|
||||
this.product = this.config.data?.product;
|
||||
|
||||
// Generar el PDF
|
||||
this.generarPDF();
|
||||
}
|
||||
|
||||
generarPDF() {
|
||||
this.pdfService.generateCronogramaPdf(this.product).then(dataUrl => {
|
||||
this.pdfSrc = dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
descargarPDF() {
|
||||
this.pdfService.downloadCronogramaPdf('cronograma', this.product);
|
||||
}
|
||||
|
||||
enviarPDF() {
|
||||
this.enviando = true;
|
||||
console.log('Enviando PDF...');
|
||||
|
||||
setTimeout(() => {
|
||||
this.enviando = false;
|
||||
console.log('Mostrando mensaje de envio...')
|
||||
this.alertService.success(
|
||||
'El cronograma ha sido enviado correctamente a la plataforma.',
|
||||
'¡Enviado con éxito!'
|
||||
);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
cerrar() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
16
src/app/guards/auth.guard.ts
Normal file
16
src/app/guards/auth.guard.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (authService.isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
router.navigate(['/login']);
|
||||
return false;
|
||||
};
|
||||
46
src/app/interceptors/auth.interceptor.ts
Normal file
46
src/app/interceptors/auth.interceptor.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpHandlerFn,
|
||||
HttpInterceptorFn,
|
||||
HttpErrorResponse
|
||||
} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (
|
||||
request: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn
|
||||
): Observable<any> => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Get the auth token
|
||||
const token = authService.getToken();
|
||||
|
||||
// Clone the request and add the token if it exists
|
||||
if (token) {
|
||||
const authRequest = request.clone({
|
||||
setHeaders: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the authenticated request
|
||||
return next(authRequest).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
// Handle 401 Unauthorized errors by logging out and redirecting to login
|
||||
if (error.status === 401) {
|
||||
authService.logout();
|
||||
router.navigate(['/login']);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// If no token, just pass the request through
|
||||
return next(request);
|
||||
};
|
||||
10
src/app/models/actualizacion-pd.model.ts
Normal file
10
src/app/models/actualizacion-pd.model.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Cronograma } from './cronograma.model';
|
||||
|
||||
export interface ActualizacionPd extends Cronograma {
|
||||
dato9?: string;
|
||||
dato10?: string;
|
||||
dato11?: string;
|
||||
dato12?: string;
|
||||
dato13?: string;
|
||||
dato14?: string;
|
||||
}
|
||||
12
src/app/models/ajuste-pd.model.ts
Normal file
12
src/app/models/ajuste-pd.model.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Cronograma } from './cronograma.model';
|
||||
|
||||
export interface AjustePd extends Cronograma {
|
||||
dato9?: string;
|
||||
dato10?: string;
|
||||
dato13?: string;
|
||||
dato14?: string;
|
||||
dato15?: string;
|
||||
dato16?: string;
|
||||
dato17?: string;
|
||||
dato18?: string;
|
||||
}
|
||||
11
src/app/models/cronograma.model.ts
Normal file
11
src/app/models/cronograma.model.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface Cronograma {
|
||||
id?: number;
|
||||
empresa: string;
|
||||
codigoCronograma: string;
|
||||
codigoCronogramaAjuste: string;
|
||||
tipoCarga: string;
|
||||
estadoRevision?: string;
|
||||
analista?: string;
|
||||
fechaIngreso?: string;
|
||||
semaforo?: 'green' | 'yellow' | 'red';
|
||||
}
|
||||
4
src/app/models/empresa.model.ts
Normal file
4
src/app/models/empresa.model.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Empresa {
|
||||
id?: number;
|
||||
name: string;
|
||||
}
|
||||
4
src/app/models/estado-aprobacion.model.ts
Normal file
4
src/app/models/estado-aprobacion.model.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface EstadoAprobacion {
|
||||
id?: number;
|
||||
name: string;
|
||||
}
|
||||
7
src/app/models/index.ts
Normal file
7
src/app/models/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './cronograma.model';
|
||||
export * from './empresa.model';
|
||||
export * from './tipo-carga.model';
|
||||
export * from './estado-aprobacion.model';
|
||||
export * from './actualizacion-pd.model';
|
||||
export * from './ajuste-pd.model';
|
||||
export * from './unidad-informacion.model';
|
||||
4
src/app/models/tipo-carga.model.ts
Normal file
4
src/app/models/tipo-carga.model.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface TipoCarga {
|
||||
id?: number;
|
||||
name: string;
|
||||
}
|
||||
6
src/app/models/unidad-informacion.model.ts
Normal file
6
src/app/models/unidad-informacion.model.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Cronograma } from './cronograma.model';
|
||||
|
||||
export interface UnidadInformacion extends Cronograma {
|
||||
dato5?: string;
|
||||
dato6?: string;
|
||||
}
|
||||
121
src/app/pages/actualizacion-pd/actualizacion-pd.component.html
Normal file
121
src/app/pages/actualizacion-pd/actualizacion-pd.component.html
Normal file
@ -0,0 +1,121 @@
|
||||
<div class="p-3 text-white">
|
||||
<div
|
||||
class="flex align-content-start flex-wrap gap-2 border-round border-blue-400 my-2 lg:px-0 py-3 my-1"
|
||||
>
|
||||
<div class="col-12 md:col-3 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro Empresa</div>
|
||||
<p-select appendTo="body"
|
||||
[options]="empresas"
|
||||
[(ngModel)]="selectedCity"
|
||||
optionLabel="name"
|
||||
placeholder="Seleccione..."
|
||||
class="w-full md:w-56"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-3 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro Código Cronograma</div>
|
||||
<p-select appendTo="body"
|
||||
[options]="empresas"
|
||||
[(ngModel)]="selectedCity"
|
||||
optionLabel="name"
|
||||
placeholder="Seleccione..."
|
||||
class="w-full md:w-56"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tabla1 -->
|
||||
<div class="flex justify-content-end align-items-center my-2 py-2">
|
||||
<!-- <div class="font-bold tituloTabla">Título de la tabla:</div> -->
|
||||
<p-button
|
||||
icon="pi pi-file-excel"
|
||||
(onClick)="exportExcelWithStyles(dt)"
|
||||
styleClass="p-button-success"
|
||||
pTooltip="Descargar planilla Excel"
|
||||
tooltipPosition="top"
|
||||
label="Exportar"
|
||||
[tooltipOptions]="{showDelay: 100, appendTo: 'body'}"
|
||||
>
|
||||
</p-button>
|
||||
</div>
|
||||
<p-table
|
||||
id="azul"
|
||||
#dt
|
||||
[value]="products"
|
||||
stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]"
|
||||
styleClass="p-datatable-sm"
|
||||
>
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th class="tablaAzul font-bold text-white">Empresa</th>
|
||||
<th class="tablaAzul font-bold text-white">Código de cronograma</th>
|
||||
<th class="tablaAzul font-bold text-white">Etapa del Servicio</th>
|
||||
<th class="tablaAzul font-bold text-white">Nombre sistema</th>
|
||||
<th class="tablaAzul font-bold text-white">Tipo de inversión</th>
|
||||
<th class="tablaAzul font-bold text-white">Código de glosa PD</th>
|
||||
<th class="tablaAzul font-bold text-white">Descripción glosa</th>
|
||||
<th class="tablaAzul font-bold text-white">Monto Inversión Total (UF)</th>
|
||||
<th class="tablaAzul font-bold text-white">Año de Inicio</th>
|
||||
<th class="tablaAzul font-bold text-white">Año de Término</th>
|
||||
<th class="tablaAzul font-bold text-white">Mes de Término</th>
|
||||
<th class="tablaAzul font-bold text-white">Nota</th>
|
||||
<th class="bg-green-400 font-bold text-white">Estado aprobación</th>
|
||||
<th class="bg-green-400 font-bold text-white">Observación</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td>{{ product.estadoRevision }}</td>
|
||||
<td>{{ product.estadoRevision }}</td>
|
||||
<td>{{ product.fechaIngreso }}</td>
|
||||
<td>{{ product.estadoRevision }}</td>
|
||||
<td>{{ product.dato9 }}</td>
|
||||
<td>{{ product.dato10 }}</td>
|
||||
<td>{{ product.dato11 }}</td>
|
||||
<td>{{ product.dato12 }}</td>
|
||||
<td class="bg-verde">
|
||||
<div class="border-round font-bold">
|
||||
<p-select appendTo="body"
|
||||
[options]="estadoAprobacion"
|
||||
optionValue="value"
|
||||
[(ngModel)]="product.dato13"
|
||||
appendTo="body"
|
||||
optionLabel="name"
|
||||
placeholder="Seleccione..."
|
||||
class="selectTabla"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="bg-verde">
|
||||
<input
|
||||
type="analista"
|
||||
pInputText
|
||||
[hidden]="product.dato13"
|
||||
[(ngModel)]="product.analista"
|
||||
placeholder="Analista"
|
||||
name="analista"
|
||||
class="input-with-icon w-full"
|
||||
style="
|
||||
background-color: white;
|
||||
color: black;
|
||||
width: 228px !important;
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActualizacionPdComponent } from './actualizacion-pd.component';
|
||||
|
||||
describe('ActualizacionPdComponent', () => {
|
||||
let component: ActualizacionPdComponent;
|
||||
let fixture: ComponentFixture<ActualizacionPdComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActualizacionPdComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActualizacionPdComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
198
src/app/pages/actualizacion-pd/actualizacion-pd.component.ts
Normal file
198
src/app/pages/actualizacion-pd/actualizacion-pd.component.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Table, TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { Workbook } from 'exceljs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-actualizacion-pd',
|
||||
imports: [
|
||||
FormsModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
SelectModule,
|
||||
ButtonModule,
|
||||
TooltipModule
|
||||
],
|
||||
templateUrl: './actualizacion-pd.component.html',
|
||||
styleUrl: './actualizacion-pd.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class ActualizacionPdComponent {
|
||||
pageTitle: string = 'Cronogramas cargados:';
|
||||
select1: any = '';
|
||||
selectedCity: any = '';
|
||||
empresas: any[] = [{ name: 'Empresa A' }, { name: 'Empresa B' }, { name: 'Empresa C' }];
|
||||
estadoAprobacion = [
|
||||
{ name: 'Aprobado', value: true },
|
||||
{ name: 'Rechazado', value: false },
|
||||
];
|
||||
products: any[] = [
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Ingresado',
|
||||
analista: 'No asignado',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato13: 'Rechazado',
|
||||
dato14: 'green',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'En revisión',
|
||||
analista: 'Gabriel Torres',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'yellow',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato13: 'Rechazado',
|
||||
dato14: '',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Rechazado',
|
||||
analista: 'Jorge Muñoz',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'red',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato13: 'Aprobado',
|
||||
dato14: '',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Aprobado',
|
||||
analista: 'Jarolt Matamoros',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato11: '2025-04-29',
|
||||
dato12: 'green',
|
||||
dato13: 'Aprobado',
|
||||
dato14: '',
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Exporta la tabla a Excel usando ExcelJS
|
||||
* @param table Referencia a la tabla PrimeNG
|
||||
*/
|
||||
|
||||
exportExcelWithStyles(table: Table): void {
|
||||
// Creamos un nuevo libro de trabajo
|
||||
const workbook = new Workbook();
|
||||
const worksheet = workbook.addWorksheet('Datos');
|
||||
|
||||
// Exportamos los datos y aplicamos estilos
|
||||
this.addDataToWorksheet(worksheet, table);
|
||||
this.applyHeaderStyles(worksheet);
|
||||
this.configureWorksheet(worksheet);
|
||||
this.saveExcelFile(workbook);
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
* Añade los datos de la tabla al worksheet
|
||||
*/
|
||||
private addDataToWorksheet(worksheet: any, table: Table): void {
|
||||
// Obtenemos los datos a exportar
|
||||
const data = table.filteredValue || table.value;
|
||||
|
||||
// Definimos las cabeceras
|
||||
const headers = [
|
||||
'Empresa', 'Código de cronograma', 'Etapa del Servicio',
|
||||
'Nombre sistema', 'Tipo de inversión', 'Código de glosa PD',
|
||||
'Descripción glosa', 'Año de Inicio', 'Año de Término',
|
||||
'Estado aprobación'
|
||||
];
|
||||
|
||||
// Añadimos cabeceras y datos
|
||||
worksheet.addRow(headers);
|
||||
data.forEach(item => {
|
||||
worksheet.addRow([
|
||||
item.empresa,
|
||||
item.codigoCronograma,
|
||||
item.codigoCronogramaAjuste,
|
||||
item.tipoCarga,
|
||||
item.estadoRevision,
|
||||
item.analista,
|
||||
item.fechaIngreso,
|
||||
item.dato9,
|
||||
item.dato10,
|
||||
item.dato13
|
||||
]);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Aplica estilos a los encabezados de la tabla
|
||||
*/
|
||||
private applyHeaderStyles(worksheet: any): void {
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.height = 25;
|
||||
|
||||
headerRow.eachCell((cell: any) => {
|
||||
cell.font = { name: 'Arial', size: 12, bold: true, color: { argb: '000000' } };
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura aspectos generales de la hoja de trabajo
|
||||
*/
|
||||
private configureWorksheet(worksheet: any): void {
|
||||
// Ajustamos el ancho de las columnas automáticamente
|
||||
if (worksheet.columns) {
|
||||
worksheet.columns.forEach((column: any) => {
|
||||
if (column) {
|
||||
let maxLength = 0;
|
||||
column.eachCell({ includeEmpty: true }, (cell: any) => {
|
||||
const columnLength = cell.value ? cell.value.toString().length : 10;
|
||||
if (columnLength > maxLength) {
|
||||
maxLength = columnLength;
|
||||
}
|
||||
});
|
||||
column.width = maxLength < 10 ? 10 : maxLength + 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Congelamos la primera fila
|
||||
worksheet.views = [{ state: 'frozen', xSplit: 0, ySplit: 1 }];
|
||||
}
|
||||
/**
|
||||
* Guarda el archivo Excel
|
||||
*/
|
||||
private async saveExcelFile(workbook: any): Promise<void> {
|
||||
const today = new Date();
|
||||
const fileName = `Cronograma_temporal_por_actualización_de_PD${today.getFullYear()}${(today.getMonth() + 1).toString().padStart(2, '0')}${today.getDate().toString().padStart(2, '0')}.xlsx`;
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
FileSaver.saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
}
|
||||
140
src/app/pages/ajuste-pd/ajuste-pd.component.html
Normal file
140
src/app/pages/ajuste-pd/ajuste-pd.component.html
Normal file
@ -0,0 +1,140 @@
|
||||
<div class="p-3 text-white">
|
||||
<div
|
||||
class="flex align-content-start flex-wrap gap-2 border-round border-blue-400 my-2 lg:px-0 py-3 my-1"
|
||||
>
|
||||
<div class="col-6 md:col-3 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro Empresa</div>
|
||||
<p-select appendTo="body"
|
||||
[options]="empresas"
|
||||
[(ngModel)]="selectedCity"
|
||||
optionLabel="name"
|
||||
placeholder="Seleccione..."
|
||||
class="w-full md:w-56"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 md:col-3 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro Código Cronograma</div>
|
||||
<p-select appendTo="body"
|
||||
[options]="empresas"
|
||||
[(ngModel)]="selectedCity"
|
||||
optionLabel="name"
|
||||
placeholder="Seleccione..."
|
||||
class="w-full md:w-56"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tabla1 -->
|
||||
<!-- <div class="font-bold text-black-alpha-90 my-2">Título de la tabla:</div> -->
|
||||
|
||||
<!-- Botón de exportar a Excel (movido arriba) -->
|
||||
<div class="flex justify-content-end align-items-center my-2 py-2">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
(click)="exportExcelWithStyles(dt)"
|
||||
icon="pi pi-file-excel"
|
||||
class="p-button-success"
|
||||
label="Exportar"
|
||||
pTooltip="Descargar planilla Excel"
|
||||
tooltipPosition="top"
|
||||
[tooltipOptions]="{showDelay: 100, appendTo: 'body'}"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
#dt
|
||||
id="azul"
|
||||
[value]="products"
|
||||
stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th colspan="9" style="background-color: #f8f9fa; border: none"></th>
|
||||
<th colspan="3" class="tablaAzul text-white font-bold text-center">Cronograma base vigente</th>
|
||||
<th colspan="4" class="bg-blue-700 text-white font-bold text-center">Cronograma base ajustado</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="tablaAzul text-white font-bold">Empresa</th>
|
||||
<th class="tablaAzul text-white font-bold">Código de cronograma</th>
|
||||
<th class="tablaAzul text-white font-bold">Etapa del Servicio</th>
|
||||
<th class="tablaAzul text-white font-bold">Nombre sistema</th>
|
||||
<th class="tablaAzul text-white font-bold">Nombre localidad</th>
|
||||
<th class="tablaAzul text-white font-bold">Tipo de inversión</th>
|
||||
<th class="tablaAzul text-white font-bold">Código de glosa PD</th>
|
||||
<th class="tablaAzul text-white font-bold">Descripción glosa</th>
|
||||
<th class="tablaAzul text-white font-bold">Monto Inversión Total (UF)</th>
|
||||
<th class="tablaAzul text-white font-bold">Año de Inicio</th>
|
||||
<th class="tablaAzul text-white font-bold">Año de Término</th>
|
||||
<th class="tablaAzul text-white font-bold">Mes de Término</th>
|
||||
<th class="bg-blue-700 text-white font-bold">Tipo de ajuste</th>
|
||||
<th class="bg-blue-700 text-white font-bold">Año de Inicio</th>
|
||||
<th class="bg-blue-700 text-white font-bold">Año de Término</th>
|
||||
<th class="bg-blue-700 text-white font-bold">Mes de Término</th>
|
||||
<th class="tablaAzul text-white font-bold">Nota</th>
|
||||
<th class="bg-green-400 text-white font-bold">Estado aprobación</th>
|
||||
<th class="bg-green-400 text-white font-bold">Observación</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td>{{ product.estadoRevision }}</td>
|
||||
<td>{{ product.fechaIngreso }}</td>
|
||||
<td>{{ product.estadoRevision }}</td>
|
||||
<td>{{ product.dato9 }}</td>
|
||||
<td>{{ product.dato10 }}</td>
|
||||
<td>{{ product.dato13 }}</td>
|
||||
<td>{{ product.dato14 }}</td>
|
||||
<td>{{ product.dato15 }}</td>
|
||||
<td class="bg-azulFuerte">{{ product.dato16 }}</td>
|
||||
<td class="bg-azulFuerte">{{ product.dato17 }}</td>
|
||||
<td class="bg-azulFuerte">{{ product.dato18 }}</td>
|
||||
<td class="bg-azulFuerte">{{ product.dato11 }}</td>
|
||||
<td>{{ product.dato12 }}</td>
|
||||
<td class="bg-verde">
|
||||
<div class="border-round font-bold">
|
||||
<p-select appendTo="body"
|
||||
[options]="estadoAprobacion"
|
||||
optionValue="value"
|
||||
[(ngModel)]="product.dato13"
|
||||
appendTo="body"
|
||||
optionLabel="name"
|
||||
placeholder="Seleccione..."
|
||||
class="selectTabla"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="bg-verde">
|
||||
<input
|
||||
type="analista"
|
||||
pInputText
|
||||
[hidden]="product.dato13"
|
||||
[(ngModel)]="product.analista"
|
||||
placeholder="Analista"
|
||||
name="analista"
|
||||
class="input-with-icon w-full"
|
||||
style="
|
||||
background-color: white;
|
||||
color: black;
|
||||
width: 228px !important;
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<!-- Se eliminó el botón que estaba aquí abajo -->
|
||||
</div>
|
||||
0
src/app/pages/ajuste-pd/ajuste-pd.component.scss
Normal file
0
src/app/pages/ajuste-pd/ajuste-pd.component.scss
Normal file
23
src/app/pages/ajuste-pd/ajuste-pd.component.spec.ts
Normal file
23
src/app/pages/ajuste-pd/ajuste-pd.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AjustePdComponent } from './ajuste-pd.component';
|
||||
|
||||
describe('AjustePdComponent', () => {
|
||||
let component: AjustePdComponent;
|
||||
let fixture: ComponentFixture<AjustePdComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AjustePdComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AjustePdComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
381
src/app/pages/ajuste-pd/ajuste-pd.component.ts
Normal file
381
src/app/pages/ajuste-pd/ajuste-pd.component.ts
Normal file
@ -0,0 +1,381 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TableModule, Table } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { Workbook } from 'exceljs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ajuste-pd',
|
||||
imports: [
|
||||
FormsModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
SelectModule,
|
||||
TooltipModule,
|
||||
ButtonModule
|
||||
],
|
||||
templateUrl: './ajuste-pd.component.html',
|
||||
styleUrl: './ajuste-pd.component.scss'
|
||||
})
|
||||
export class AjustePdComponent {
|
||||
selectedCity: any = '';
|
||||
empresas: any[] = [{ name: 'Empresa A' }, { name: 'Empresa B' }, { name: 'Empresa C' }];
|
||||
select1: any = '';
|
||||
estadoAprobacion = [
|
||||
{ name: 'Aprobado', value: true },
|
||||
{ name: 'Rechazado', value: false },
|
||||
];
|
||||
products: any[] = [
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Ingresado',
|
||||
analista: 'No asignado',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato15: 'green',
|
||||
dato16: 'green',
|
||||
dato17: 'green',
|
||||
dato18: 'green',
|
||||
dato13: 'Rechazado',
|
||||
dato14: 'green',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'En revisión',
|
||||
analista: 'Gabriel Torres',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'yellow',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato15: 'green',
|
||||
dato16: 'green',
|
||||
dato17: 'green',
|
||||
dato18: 'green',
|
||||
dato13: 'Rechazado',
|
||||
dato14: 'green',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Rechazado',
|
||||
analista: 'Jorge Muñoz',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'red',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato15: 'green',
|
||||
dato16: 'green',
|
||||
dato17: 'green',
|
||||
dato18: 'green',
|
||||
dato13: 'Rechazado',
|
||||
dato14: 'green',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Aprobado',
|
||||
analista: 'Jarolt Matamoros',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green',
|
||||
dato9: 'Ingresado',
|
||||
dato10: 'No asignado',
|
||||
dato15: 'green',
|
||||
dato16: 'green',
|
||||
dato17: 'green',
|
||||
dato18: 'green',
|
||||
dato13: 'Rechazado',
|
||||
dato14: 'green',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Exporta los datos de la tabla a Excel con estilos en los encabezados
|
||||
* @param table Tabla PrimeNG a exportar
|
||||
*/
|
||||
exportExcelWithStyles(table: Table): void {
|
||||
// Creamos un nuevo libro de trabajo
|
||||
const workbook = new Workbook();
|
||||
const worksheet = workbook.addWorksheet('Datos');
|
||||
|
||||
// Obtenemos los datos
|
||||
const data = table.filteredValue || table.value;
|
||||
|
||||
// Añadimos la fila de encabezados agrupados
|
||||
this.addGroupedHeaders(worksheet);
|
||||
|
||||
// Añadimos la fila de encabezados detallados
|
||||
const headers = this.getHeaders();
|
||||
worksheet.addRow(headers);
|
||||
|
||||
// Añadimos los datos a la hoja
|
||||
this.populateWorksheet(worksheet, data);
|
||||
|
||||
// Aplicamos estilos a los encabezados
|
||||
this.styleHeaders(worksheet);
|
||||
|
||||
// Configuramos propiedades generales de la hoja
|
||||
this.configureWorksheet(worksheet, data.length);
|
||||
|
||||
// Exportamos el archivo
|
||||
this.saveExcelFile(workbook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Añade la fila de encabezados agrupados
|
||||
*/
|
||||
private addGroupedHeaders(worksheet: any): void {
|
||||
// Añadir fila de encabezados agrupados
|
||||
const groupHeaderRow = worksheet.addRow([
|
||||
'', '', '', '', '', '', '', '', '',
|
||||
'Cronograma base vigente', '', '',
|
||||
'Cronograma base ajustado', '', '', '',
|
||||
'', '', ''
|
||||
]);
|
||||
|
||||
// Combinar celdas para los grupos de encabezados
|
||||
worksheet.mergeCells(1, 10, 1, 12); // Cronograma base vigente (columnas 10-12)
|
||||
worksheet.mergeCells(1, 13, 1, 16); // Cronograma base ajustado (columnas 13-16)
|
||||
|
||||
// Dar formato a las celdas combinadas
|
||||
groupHeaderRow.getCell(10).fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: '0066CC' } // Color azul oscuro para "Cronograma base vigente"
|
||||
};
|
||||
groupHeaderRow.getCell(13).fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: '0000CC' } // Color azul más fuerte para "Cronograma base ajustado"
|
||||
};
|
||||
|
||||
// Estilo de texto para encabezados agrupados
|
||||
[10, 13].forEach(cellIndex => {
|
||||
const cell = groupHeaderRow.getCell(cellIndex);
|
||||
cell.font = {
|
||||
name: 'Arial',
|
||||
size: 12,
|
||||
bold: true,
|
||||
color: { argb: 'FFFFFF' } // Texto blanco
|
||||
};
|
||||
cell.alignment = {
|
||||
horizontal: 'center',
|
||||
vertical: 'middle'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve los cabeceros para el archivo Excel
|
||||
*/
|
||||
private getHeaders(): string[] {
|
||||
return [
|
||||
'Empresa',
|
||||
'Código de cronograma',
|
||||
'Etapa del Servicio',
|
||||
'Nombre sistema',
|
||||
'Nombre localidad',
|
||||
'Tipo de inversión',
|
||||
'Código de glosa PD',
|
||||
'Descripción glosa',
|
||||
'Monto Inversión Total (UF)',
|
||||
'Año de Inicio',
|
||||
'Año de Término',
|
||||
'Mes de Término',
|
||||
'Tipo de ajuste',
|
||||
'Año de Inicio',
|
||||
'Año de Término',
|
||||
'Mes de Término',
|
||||
'Nota',
|
||||
'Estado aprobación',
|
||||
'Observación'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rellena la hoja con los datos de la tabla
|
||||
*/
|
||||
private populateWorksheet(worksheet: any, data: any[]): void {
|
||||
// Añadimos los datos
|
||||
data.forEach(item => {
|
||||
// Determinar estado de aprobación basado en el valor
|
||||
let estadoAprobacion;
|
||||
if (typeof item.dato13 === 'boolean') {
|
||||
estadoAprobacion = item.dato13 ? 'Aprobado' : 'Rechazado';
|
||||
} else {
|
||||
estadoAprobacion = item.dato13;
|
||||
}
|
||||
|
||||
worksheet.addRow([
|
||||
item.empresa, // Empresa
|
||||
item.codigoCronograma, // Código de cronograma
|
||||
item.codigoCronogramaAjuste, // Etapa del Servicio
|
||||
item.tipoCarga, // Nombre sistema
|
||||
item.estadoRevision, // Nombre localidad
|
||||
item.fechaIngreso, // Tipo de inversión
|
||||
item.estadoRevision, // Código de glosa PD
|
||||
item.dato9, // Descripción glosa
|
||||
item.dato10, // Monto Inversión Total (UF)
|
||||
item.dato13, // Año de Inicio (Cronograma base vigente)
|
||||
item.dato14, // Año de Término (Cronograma base vigente)
|
||||
item.dato15, // Mes de Término (Cronograma base vigente)
|
||||
item.dato16, // Tipo de ajuste (Cronograma base ajustado)
|
||||
item.dato17, // Año de Inicio (Cronograma base ajustado)
|
||||
item.dato18, // Año de Término (Cronograma base ajustado)
|
||||
item.dato11, // Mes de Término (Cronograma base ajustado)
|
||||
item.dato12, // Nota
|
||||
estadoAprobacion, // Estado aprobación
|
||||
item.analista // Observación
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica estilos a los encabezados
|
||||
*/
|
||||
private styleHeaders(worksheet: any): void {
|
||||
// Estilo para la segunda fila (encabezados detallados)
|
||||
const detailedHeaderRow = worksheet.getRow(2);
|
||||
detailedHeaderRow.height = 25;
|
||||
|
||||
detailedHeaderRow.eachCell(cell => {
|
||||
// Estilo de texto Encabezado
|
||||
cell.font = {
|
||||
name: 'Arial',
|
||||
size: 12,
|
||||
bold: true,
|
||||
color: { argb: 'FFFFFF' } // Texto blanco
|
||||
};
|
||||
|
||||
// Bordes
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
|
||||
// Alineación
|
||||
cell.alignment = {
|
||||
horizontal: 'center',
|
||||
vertical: 'middle'
|
||||
};
|
||||
|
||||
// Color de fondo para encabezados normales
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: '0066CC' } // Color azul por defecto
|
||||
};
|
||||
});
|
||||
|
||||
// Aplicar colores específicos según el grupo de encabezados
|
||||
// Cronograma base ajustado (columnas 13-16)
|
||||
for (let i = 13; i <= 16; i++) {
|
||||
const cell = detailedHeaderRow.getCell(i);
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: '0000CC' } // Azul más fuerte
|
||||
};
|
||||
}
|
||||
|
||||
// Estado aprobación y Observación (columnas 18-19)
|
||||
for (let i = 18; i <= 19; i++) {
|
||||
const cell = detailedHeaderRow.getCell(i);
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: '28a745' } // Color verde
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura propiedades generales de la hoja de trabajo
|
||||
*/
|
||||
private configureWorksheet(worksheet: any, dataLength: number): void {
|
||||
// Configuramos altura de fila predeterminada
|
||||
worksheet.properties.defaultRowHeight = 20;
|
||||
|
||||
// Ajustamos el ancho de las columnas automáticamente
|
||||
this.adjustColumnWidths(worksheet);
|
||||
|
||||
// Congelamos la primera fila
|
||||
worksheet.views = [{ state: 'frozen', xSplit: 0, ySplit: 2 }]; // Congelamos 2 filas
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajusta el ancho de las columnas según el contenido
|
||||
*/
|
||||
private adjustColumnWidths(worksheet: any): void {
|
||||
if (worksheet.columns) {
|
||||
worksheet.columns.forEach(column => {
|
||||
if (column) {
|
||||
let maxLength = 0;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const columnLength = cell.value ? cell.value.toString().length : 10;
|
||||
if (columnLength > maxLength) {
|
||||
maxLength = columnLength;
|
||||
}
|
||||
});
|
||||
column.width = maxLength < 10 ? 10 : maxLength + 2;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Si no hay columnas definidas, establecemos valores predeterminados
|
||||
const columnWidths = [
|
||||
{ width: 15 }, // Empresa
|
||||
{ width: 20 }, // Código de cronograma
|
||||
{ width: 18 }, // Etapa del Servicio
|
||||
{ width: 18 }, // Nombre sistema
|
||||
{ width: 18 }, // Nombre localidad
|
||||
{ width: 18 }, // Tipo de inversión
|
||||
{ width: 18 }, // Código de glosa PD
|
||||
{ width: 22 }, // Descripción glosa
|
||||
{ width: 18 }, // Monto Inversión Total (UF)
|
||||
{ width: 15 }, // Año de Inicio (vigente)
|
||||
{ width: 15 }, // Año de Término (vigente)
|
||||
{ width: 15 }, // Mes de Término (vigente)
|
||||
{ width: 15 }, // Tipo de ajuste
|
||||
{ width: 15 }, // Año de Inicio (ajustado)
|
||||
{ width: 15 }, // Año de Término (ajustado)
|
||||
{ width: 15 }, // Mes de Término (ajustado)
|
||||
{ width: 15 }, // Nota
|
||||
{ width: 18 }, // Estado aprobación
|
||||
{ width: 20 } // Observación
|
||||
];
|
||||
worksheet.columns = columnWidths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda el archivo Excel generado
|
||||
*/
|
||||
private saveExcelFile(workbook: any): void {
|
||||
const today = new Date();
|
||||
const fileName = `Cronograma_temporal_por_ajuste_de_PD${today.getFullYear()}${(today.getMonth() + 1).toString().padStart(2, '0')}${today.getDate().toString().padStart(2, '0')}.xlsx`;
|
||||
|
||||
workbook.xlsx.writeBuffer()
|
||||
.then(buffer => {
|
||||
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
FileSaver.saveAs(blob, fileName);
|
||||
})
|
||||
.catch(error => console.error('Error al exportar a Excel:', error));
|
||||
}
|
||||
}
|
||||
143
src/app/pages/concesiones/concesiones.component.html
Normal file
143
src/app/pages/concesiones/concesiones.component.html
Normal file
@ -0,0 +1,143 @@
|
||||
<div class="p-3 text-white">
|
||||
<!-- Tabla1 -->
|
||||
<div class="font-bold my-2 tituloTabla">Cronogramas cargados:</div>
|
||||
<p-table id="azul"
|
||||
[value]="products" stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th class="tablaAzul font-bold">Empresa</th>
|
||||
<th class="tablaAzul font-bold">Código de cronograma</th>
|
||||
<th class="tablaAzul font-bold">Código cronograma de ajuste</th>
|
||||
<th class="tablaAzul font-bold">Tipo de carga</th>
|
||||
<th class="bg-green-400 font-bold text-white font-bold">Estado de revisión</th>
|
||||
<th class="bg-green-400 font-bold text-white font-bold">Analista</th>
|
||||
<th class="bg-green-400 font-bold text-white font-bold">Fecha ingreso</th>
|
||||
<th class="bg-green-400 font-bold text-white font-bold">Semáforo</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td class="bg-verde">{{ product.estadoRevision }}</td>
|
||||
<td class="bg-verde">
|
||||
<!-- analista -->
|
||||
<input
|
||||
type="analista"
|
||||
pInputText
|
||||
[(ngModel)]="product.analista"
|
||||
placeholder="Analista"
|
||||
name="analista"
|
||||
class="input-with-icon w-full"
|
||||
style="background-color: white; color: black;width: 228px !important;"
|
||||
/>
|
||||
</td>
|
||||
<td class="bg-verde">{{ product.fechaIngreso }}</td>
|
||||
<td class="bg-verde">
|
||||
<!-- {{ product.semaforo }} -->
|
||||
<div class="text-center">
|
||||
<i class="pi pi-circle-fill" style="font-size: 24px;"
|
||||
[style.color]="product.semaforo === 'green' ? '#00bb00' : product.semaforo === 'yellow' ? 'yellow' : product.semaforo === 'red' ? 'red' : ''"></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<div class="flex justify-content-between flex-wrap gap-3 border-round border-blue-400 my-6 lg:px-0 py-3 my-1 ">
|
||||
<div class="col-12 md:col-5 lg:col-2 tablaAzul border-round ">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro Empresa</div>
|
||||
<p-select appendTo="body" [options]="empresas" [(ngModel)]="select1" optionLabel="name" placeholder="Seleccione..." class="w-full md:w-56" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-5 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro Código Cronograma SINAR</div>
|
||||
<p-select appendTo="body" [options]="empresas" [(ngModel)]="select2" optionLabel="name" placeholder="Seleccione..." class="w-full md:w-56" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-5 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">Filtro tipo de carga</div>
|
||||
<p-select appendTo="body" [options]="tipoCarga" [(ngModel)]="select3" optionLabel="name" placeholder="Seleccione..." class="w-full md:w-56" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-5 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">¿Contiene obras del año?</div>
|
||||
<p-select appendTo="body" [options]="contieneObras" [(ngModel)]="select4" optionLabel="name" placeholder="Seleccione..." class="w-full md:w-56" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-5 lg:col-2 tablaAzul border-round">
|
||||
<div class="border-round font-bold">
|
||||
<div class="h-4rem">N° Oficio que aprueba</div>
|
||||
<!-- Numero Oficio -->
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
placeholder="xxxxx"
|
||||
name="numeroOficio"
|
||||
class="input-with-icon w-full mt-1 text-center sm:text-start"
|
||||
style="background-color: white; color: black;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<button type="button" class="bg-primary border-primary-500 mt-3 px-5 py-3 text-base border-1 border-solid border-round cursor-pointer transition-all transition-duration-200 hover:bg-primary-600 hover:border-primary-600 active:bg-primary-700 active:border-primary-700">Crear solicitud</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla2 -->
|
||||
<div class="font-bold my-2 tituloTabla">Cronogramas solicitados y pendientes de carga:</div>
|
||||
<p-table id="azul" sortField="price" [sortOrder]="-1"
|
||||
[value]="products" stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th pSortableColumn="empresa" class="tablaAzul font-bold">
|
||||
Empresa <p-sortIcon field="empresa" />
|
||||
</th>
|
||||
<th pSortableColumn="codigoCronograma" class="tablaAzul font-bold">
|
||||
Código de cronograma SINAR <p-sortIcon field="codigoCronograma" />
|
||||
</th>
|
||||
<th pSortableColumn="codigoCronogramaAjuste" class="tablaAzul font-bold">
|
||||
Contiene obras del año <p-sortIcon field="codigoCronogramaAjuste" />
|
||||
</th>
|
||||
<th pSortableColumn="tipoCarga" class="tablaAzul font-bold">
|
||||
N° Oficio que aprueba <p-sortIcon field="tipoCarga" />
|
||||
</th>
|
||||
<th pSortableColumn="estadoRevision" class="tablaAzul font-bold">
|
||||
Tipo de carga <p-sortIcon field="estadoRevision" />
|
||||
</th>
|
||||
<th pSortableColumn="fechaIngreso" class="tablaAzul font-bold">
|
||||
Fecha de solicitud <p-sortIcon field="fechaIngreso" />
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td>{{ product.estadoRevision }}</td>
|
||||
<td>{{ product.fechaIngreso }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
</div>
|
||||
12
src/app/pages/concesiones/concesiones.component.scss
Normal file
12
src/app/pages/concesiones/concesiones.component.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.input-with-icon {
|
||||
background-size: 20px 20px;
|
||||
padding-right: 35px; /* espacio para que el texto no choque con el ícono */
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
.lg\:col-2 {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.5rem;
|
||||
width: 19%;
|
||||
}
|
||||
}
|
||||
23
src/app/pages/concesiones/concesiones.component.spec.ts
Normal file
23
src/app/pages/concesiones/concesiones.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConcesionesComponent } from './concesiones.component';
|
||||
|
||||
describe('ConcesionesComponent', () => {
|
||||
let component: ConcesionesComponent;
|
||||
let fixture: ComponentFixture<ConcesionesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ConcesionesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ConcesionesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
145
src/app/pages/concesiones/concesiones.component.ts
Normal file
145
src/app/pages/concesiones/concesiones.component.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-concesiones',
|
||||
imports: [FormsModule, TableModule, InputTextModule, SelectModule],
|
||||
templateUrl: './concesiones.component.html',
|
||||
styleUrl: './concesiones.component.scss'
|
||||
})
|
||||
export class ConcesionesComponent {
|
||||
select1: any = '';
|
||||
select2: any = '';
|
||||
select3: any = '';
|
||||
select4: any = '';
|
||||
pageTitle: string = 'Cronogramas cargados:';
|
||||
empresas: any[] = [{name: 'Empresa A'}, {name: 'Empresa B'}, {name: 'Empresa C'}];
|
||||
tipoCarga: any[] = [{name: 'Actualización'}, {name: 'Nueva'}, {name: 'Concesión'}, {name: 'Ajuste'}, {name: 'ATO'}];
|
||||
contieneObras: any[] = [{name:'Si'}, {name:'No'}];
|
||||
products: any[] = [
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Ingresado',
|
||||
analista: '',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green'
|
||||
},
|
||||
{
|
||||
empresa: 'Fmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'En revisión',
|
||||
analista: 'Gabriel Torres',
|
||||
fechaIngreso: '2024-04-29',
|
||||
semaforo: 'yellow'
|
||||
},
|
||||
{
|
||||
empresa: 'Gmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Rechazado',
|
||||
analista: 'Jorge Muñoz',
|
||||
fechaIngreso: '2023-04-29',
|
||||
semaforo: 'red'
|
||||
},
|
||||
{
|
||||
empresa: 'Hmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Aprobado',
|
||||
analista: 'Jarolt Matamoros',
|
||||
fechaIngreso: '2022-04-29',
|
||||
semaforo: 'green'
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Ingresado',
|
||||
analista: '',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green'
|
||||
},
|
||||
{
|
||||
empresa: 'Fmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'En revisión',
|
||||
analista: 'Gabriel Torres',
|
||||
fechaIngreso: '2024-04-29',
|
||||
semaforo: 'yellow'
|
||||
},
|
||||
{
|
||||
empresa: 'Gmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Rechazado',
|
||||
analista: 'Jorge Muñoz',
|
||||
fechaIngreso: '2023-04-29',
|
||||
semaforo: 'red'
|
||||
},
|
||||
{
|
||||
empresa: 'Hmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Aprobado',
|
||||
analista: 'Jarolt Matamoros',
|
||||
fechaIngreso: '2022-04-29',
|
||||
semaforo: 'green'
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Ingresado',
|
||||
analista: '',
|
||||
fechaIngreso: '2025-04-29',
|
||||
semaforo: 'green'
|
||||
},
|
||||
{
|
||||
empresa: 'Fmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'En revisión',
|
||||
analista: 'Gabriel Torres',
|
||||
fechaIngreso: '2024-04-29',
|
||||
semaforo: 'yellow'
|
||||
},
|
||||
{
|
||||
empresa: 'Gmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Rechazado',
|
||||
analista: 'Jorge Muñoz',
|
||||
fechaIngreso: '2023-04-29',
|
||||
semaforo: 'red'
|
||||
},
|
||||
{
|
||||
empresa: 'Hmpresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
estadoRevision: 'Aprobado',
|
||||
analista: 'Jarolt Matamoros',
|
||||
fechaIngreso: '2022-04-29',
|
||||
semaforo: 'green'
|
||||
},
|
||||
];
|
||||
|
||||
}
|
||||
88
src/app/pages/home/home.component.html
Normal file
88
src/app/pages/home/home.component.html
Normal file
@ -0,0 +1,88 @@
|
||||
|
||||
<div class="home-page">
|
||||
<!-- Simple Card -->
|
||||
<p-card styleClass="mb-4">
|
||||
<ng-template pTemplate="title">
|
||||
<div class="card-title">Simple Card</div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="content">
|
||||
<p>Lorem ipsum dolor sit amet...</p>
|
||||
</ng-template>
|
||||
</p-card>
|
||||
|
||||
<!-- Accordion sections -->
|
||||
<p-accordion [multiple]="true">
|
||||
<p-accordionTab header="Header I" [selected]="true">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
|
||||
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
|
||||
laborum.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
|
||||
<p-accordionTab header="Header II">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
|
||||
<p-accordionTab header="Header III">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
</p-accordion>
|
||||
|
||||
<p-accordion [multiple]="true">
|
||||
<p-accordionTab header="Header I" [selected]="true">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
|
||||
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
|
||||
laborum.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
|
||||
<p-accordionTab header="Header II">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
|
||||
<p-accordionTab header="Header III">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
</p-accordion>
|
||||
|
||||
<p-accordion [multiple]="true">
|
||||
<p-accordionTab header="Header I" [selected]="true">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
|
||||
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
|
||||
laborum.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
|
||||
<p-accordionTab header="Header II">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
|
||||
<p-accordionTab header="Header III">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</p>
|
||||
</p-accordionTab>
|
||||
</p-accordion>
|
||||
</div>
|
||||
62
src/app/pages/home/home.component.scss
Normal file
62
src/app/pages/home/home.component.scss
Normal file
@ -0,0 +1,62 @@
|
||||
/* src/app/pages/home/home.component.scss */
|
||||
.home-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Personalización del estilo de la card */
|
||||
:host ::ng-deep .p-card {
|
||||
border-radius: 0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
|
||||
.p-card-title {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.p-card-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Personalización del estilo del acordeón */
|
||||
:host ::ng-deep .p-accordion {
|
||||
.p-accordion-header {
|
||||
.p-accordion-header-link {
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
border-radius: 0;
|
||||
font-weight: 500;
|
||||
padding: 1rem;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.p-accordion-toggle-icon {
|
||||
color: #0088cc;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-highlight .p-accordion-header-link {
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
}
|
||||
|
||||
.p-accordion-content {
|
||||
background-color: #ffffff;
|
||||
border-color: #dee2e6;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
23
src/app/pages/home/home.component.spec.ts
Normal file
23
src/app/pages/home/home.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
21
src/app/pages/home/home.component.ts
Normal file
21
src/app/pages/home/home.component.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
// Importaciones de PrimeNG
|
||||
import { CardModule } from 'primeng/card';
|
||||
import { AccordionModule } from 'primeng/accordion';
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardModule,
|
||||
AccordionModule
|
||||
],
|
||||
standalone: true,
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss'
|
||||
})
|
||||
export class HomeComponent {
|
||||
|
||||
|
||||
}
|
||||
@ -1,71 +1,121 @@
|
||||
<!-- Header container -->
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<img src="assets/img/siss-logo.png" alt="SISS Logo" class="siss-logo">
|
||||
<div class="sacg-container">
|
||||
<div class="sacg-title">
|
||||
<span>SACG</span>
|
||||
<div class="version-badge">1.0</div>
|
||||
<div class="header-container">
|
||||
<div class="header-image">
|
||||
<div class="water-drop-container">
|
||||
<img src="img/gota.png" alt="gota" class="water-drop" width="41" height="60" fetchpriority="high">
|
||||
<div class="text-drop">1.0</div>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<div class="sub-title">Administrador de Cronogramas</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast para mensajes -->
|
||||
<p-toast></p-toast>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<main class="main-content flex align-items-center justify-content-center">
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<!-- Contenedor principal con posición relativa -->
|
||||
<div class="position-relative overflow-hidden">
|
||||
|
||||
<!-- PANEL DE LOGIN -->
|
||||
<div class="panel-container w-full"
|
||||
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery,
|
||||
'animate__animated animate__fadeIn': !showRecovery && !isInitialLoad}">
|
||||
<div class="login-card shadow-2 border-round">
|
||||
<div class="login-header">
|
||||
<h2>Iniciar Sesión</h2>
|
||||
</div>
|
||||
|
||||
<form (ngSubmit)="onLogin()" class="p-3">
|
||||
<!-- Email -->
|
||||
<div class="field mb-3">
|
||||
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email"
|
||||
class="input-with-icon w-full" required />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="field mb-3">
|
||||
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password"
|
||||
class="input-with-lock w-full" required />
|
||||
</div>
|
||||
|
||||
<!-- Mensaje de error -->
|
||||
<div *ngIf="errorMessage" class="error-message my-2">
|
||||
<p-message severity="error" [text]="errorMessage"></p-message>
|
||||
</div>
|
||||
|
||||
<!-- Botón -->
|
||||
<div class="login-actions">
|
||||
<button pButton type="submit" [label]="loading ? 'Autenticando...' : 'Autenticar'"
|
||||
class="p-button-primary w-full" [disabled]="loading || !email || !password">
|
||||
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Recuperar contraseña -->
|
||||
<div class="password-recovery px-3 pb-3">
|
||||
<a href="#" (click)="toggleRecovery($event)">Recuperar Contraseña</a>
|
||||
</div>
|
||||
|
||||
<!-- Credenciales de prueba -->
|
||||
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
|
||||
<p class="mb-1 font-bold">Credenciales de prueba:</p>
|
||||
<p class="mb-1">Email: admin@example.com</p>
|
||||
<p>Password: admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PANEL DE RECUPERACIÓN -->
|
||||
<div class="panel-container w-full"
|
||||
[ngClass]="{'animate__animated animate__fadeIn': showRecovery,
|
||||
'animate__animated animate__fadeOut d-none': !showRecovery && !isInitialLoad}">
|
||||
<div class="login-card shadow-2 border-round">
|
||||
<div class="login-header">
|
||||
<h2>Recuperar Contraseña</h2>
|
||||
</div>
|
||||
|
||||
<form (ngSubmit)="onRecoverPassword()" class="p-3">
|
||||
<!-- Email para recuperación -->
|
||||
<div class="field mb-3">
|
||||
<input type="email" pInputText [(ngModel)]="recoveryEmail" name="recoveryEmail"
|
||||
placeholder="Ingresa tu email" class="input-with-icon w-full" required />
|
||||
</div>
|
||||
|
||||
<!-- Mensaje informativo -->
|
||||
<div class="info-message mb-3">
|
||||
<p class="text-sm">Te enviaremos un enlace para restablecer tu contraseña.</p>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje de estado de recuperación -->
|
||||
<div *ngIf="recoveryMessage" class="recovery-message my-2">
|
||||
<p-message [severity]="recoveryStatus" [text]="recoveryMessage"></p-message>
|
||||
</div>
|
||||
|
||||
<!-- Botón de enviar -->
|
||||
<div class="login-actions">
|
||||
<button pButton type="submit" [label]="recoveryLoading ? 'Enviando...' : 'Enviar Enlace'"
|
||||
class="p-button-primary w-full" [disabled]="recoveryLoading || !recoveryEmail">
|
||||
<i *ngIf="recoveryLoading" class="pi pi-spin pi-spinner mr-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Volver al login -->
|
||||
<div class="password-recovery px-3 pb-3">
|
||||
<a href="#" (click)="toggleRecovery($event)">Volver al Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sacg-subtitle">Sistema Administrador de Cronogramas</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<main class="main-content">
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h2>Iniciar Sesión</h2>
|
||||
</div>
|
||||
|
||||
<form (ngSubmit)="onLogin()">
|
||||
<div class="p-inputgroup mb-3">
|
||||
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email" class="w-full">
|
||||
<span class="p-inputgroup-addon bg-red-600 text-white">@</span>
|
||||
<span class="p-inputgroup-addon">
|
||||
<i class="pi pi-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-inputgroup mb-4">
|
||||
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password" class="w-full">
|
||||
<span class="p-inputgroup-addon bg-red-600 text-white">***</span>
|
||||
<span class="p-inputgroup-addon">
|
||||
<i class="pi pi-lock"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="login-actions">
|
||||
<button pButton type="submit" label="Autenticar" class="p-button-primary"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="password-recovery">
|
||||
<a href="#">Recuperar Contraseña</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-left">
|
||||
<i class="pi pi-building"></i>
|
||||
<div class="footer-text">
|
||||
<div>Superintendencia de Servicios Sanitarios</div>
|
||||
<div>Área de Información y Tecnologías</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-center">
|
||||
<img src="assets/img/siss-logo-white.png" alt="SISS Logo" class="footer-logo">
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<div>Dirección: Moneda 673 Piso 9 - Metro Santa Lucía</div>
|
||||
<div>Mesa Central: 2 2382 4000</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- FOOTER -->
|
||||
<app-footer></app-footer>
|
||||
@ -1,200 +1,190 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.header {
|
||||
background-color: #d3e9f7;
|
||||
padding: 1rem 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.siss-logo {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.sacg-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sacg-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #0a2847;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 0.1rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1.5rem;
|
||||
}
|
||||
|
||||
.sacg-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: #0a2847;
|
||||
}
|
||||
|
||||
/* Main Content Styles */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f0f0f0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(240, 240, 240, 0.7);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.header-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
background-image: url('/img/header2.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
will-change: transform;
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
.water-drop-container {
|
||||
position: absolute;
|
||||
top: 26%;
|
||||
left: 49%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 41px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.water-drop {
|
||||
position: absolute;
|
||||
width: 41px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.text-drop {
|
||||
position: relative;
|
||||
color: white;
|
||||
top: 15%;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #002147;
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 3.0rem;
|
||||
margin-bottom: 0px;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
|
||||
transform: translate(-4%);
|
||||
}
|
||||
|
||||
/* Main Content Styles */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background-color: #f0f0f0;
|
||||
position: relative;
|
||||
min-height: 60vh; /* Aumentado para mayor espacio vertical */
|
||||
}
|
||||
|
||||
.main-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(240, 240, 240, 0.7);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Estilo para los paneles y animaciones */
|
||||
.position-relative {
|
||||
min-height: 450px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
position: absolute !important;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
/* Configuración para animate.css */
|
||||
.animate__animated {
|
||||
--animate-duration: 0.8s; /* Aumentado para una transición más suave */
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
color: #0088cc;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.password-recovery a {
|
||||
color: #0088cc;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.password-recovery a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Estilos para los inputs con iconos */
|
||||
.input-with-icon {
|
||||
background-image: url("data:image/svg+xml,%3Csvg fill='gray' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 4H4C2.897 4 2 4.897 2 6v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zM4 6h16l-8 5-8-5zm0 12V8l8 5 8-5v10H4z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 20px 20px;
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
.input-with-lock {
|
||||
background-image: url("data:image/svg+xml,%3Csvg fill='gray' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17 8h-1V6c0-2.757-2.243-5-5-5S6 3.243 6 6v2H5c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h12c1.103 0 2-.897 2-2V10c0-1.103-.897-2-2-2zM8 6c0-1.654 1.346-3 3-3s3 1.346 3 3v2H8V6zm9 16H5V10h12v12z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 20px 20px;
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 768px) {
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.login-card form {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-inputgroup-addon {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.p-button-primary {
|
||||
background-color: #0088cc;
|
||||
border-color: #0088cc;
|
||||
}
|
||||
|
||||
.password-recovery {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.password-recovery a {
|
||||
color: #0088cc;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.password-recovery a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
.footer {
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { CardModule } from 'primeng/card';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { PasswordModule } from 'primeng/password';
|
||||
import { DividerModule } from 'primeng/divider';
|
||||
import { MessagesModule } from 'primeng/messages';
|
||||
import { MessageModule } from 'primeng/message';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { FooterComponent } from "../../components/footer/footer.component";
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
@ -18,20 +23,114 @@ import { DividerModule } from 'primeng/divider';
|
||||
InputTextModule,
|
||||
ButtonModule,
|
||||
PasswordModule,
|
||||
DividerModule
|
||||
DividerModule,
|
||||
MessagesModule,
|
||||
MessageModule,
|
||||
ToastModule,
|
||||
FooterComponent
|
||||
],
|
||||
providers: [MessageService],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.scss'
|
||||
})
|
||||
export class LoginComponent {
|
||||
export class LoginComponent implements OnInit {
|
||||
// Login form
|
||||
email: string = '';
|
||||
password: string = '';
|
||||
|
||||
constructor(private router: Router) { }
|
||||
onLogin() {
|
||||
// Aquí iría la lógica de autenticación
|
||||
// Por ahora, solo navegamos a la página de inicio
|
||||
this.router.navigate(['/inicio']);
|
||||
loading: boolean = false;
|
||||
errorMessage: string = '';
|
||||
|
||||
// Password recovery form
|
||||
recoveryEmail: string = '';
|
||||
recoveryLoading: boolean = false;
|
||||
recoveryMessage: string = '';
|
||||
recoveryStatus: string = 'info';
|
||||
|
||||
// Control de formularios
|
||||
showRecovery: boolean = false;
|
||||
isInitialLoad: boolean = false;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private messageService: MessageService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambia entre el formulario de login y recuperación
|
||||
*/
|
||||
toggleRecovery(event: Event) {
|
||||
event.preventDefault();
|
||||
this.showRecovery = !this.showRecovery;
|
||||
this.errorMessage = '';
|
||||
this.recoveryMessage = '';
|
||||
|
||||
// Si estamos cambiando al formulario de recuperación, copiar el email actual
|
||||
if (this.showRecovery && this.email) {
|
||||
this.recoveryEmail = this.email;
|
||||
}
|
||||
|
||||
// Forzar actualización del DOM con un pequeño retraso
|
||||
setTimeout(() => {
|
||||
// Este timeout ayuda a que Angular aplique los cambios de clase completamente
|
||||
}, 50); // Aumentado para asegurar la transición suave
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceso de login
|
||||
*/
|
||||
onLogin() {
|
||||
this.loading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
// Llamar al servicio auth
|
||||
this.authService.login({ email: this.email, password: this.password })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
console.log('Login exitoso');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/inicio']);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error en login:', error);
|
||||
this.loading = false;
|
||||
this.errorMessage = 'Credenciales incorrectas';
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Credenciales incorrectas'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceso de recuperación de contraseña
|
||||
*/
|
||||
onRecoverPassword() {
|
||||
if (!this.recoveryEmail) {
|
||||
this.recoveryMessage = 'Debes ingresar un email';
|
||||
this.recoveryStatus = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
this.recoveryLoading = true;
|
||||
this.recoveryMessage = '';
|
||||
|
||||
// Simulamos la recuperación (en producción, esto llamaría a un servicio real)
|
||||
setTimeout(() => {
|
||||
this.recoveryLoading = false;
|
||||
this.recoveryMessage = 'Hemos enviado un enlace de recuperación a tu email';
|
||||
this.recoveryStatus = 'success';
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Email enviado',
|
||||
detail: 'Se ha enviado un enlace de recuperación a tu correo'
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
8
src/app/pages/not-found/not-found.component.html
Normal file
8
src/app/pages/not-found/not-found.component.html
Normal file
@ -0,0 +1,8 @@
|
||||
<!-- error-404.component.html -->
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<h1 class="error-title">404</h1>
|
||||
<p class="error-message">Página no encontrada</p>
|
||||
<button class="error-button" (click)="volverAlInicio()">Volver al inicio</button>
|
||||
</div>
|
||||
</div>
|
||||
43
src/app/pages/not-found/not-found.component.scss
Normal file
43
src/app/pages/not-found/not-found.component.scss
Normal file
@ -0,0 +1,43 @@
|
||||
/* error-404.component.css */
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 9rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.error-button {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.3s;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-button:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
23
src/app/pages/not-found/not-found.component.spec.ts
Normal file
23
src/app/pages/not-found/not-found.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NotFoundComponent } from './not-found.component';
|
||||
|
||||
describe('NotFoundComponent', () => {
|
||||
let component: NotFoundComponent;
|
||||
let fixture: ComponentFixture<NotFoundComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NotFoundComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NotFoundComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
19
src/app/pages/not-found/not-found.component.ts
Normal file
19
src/app/pages/not-found/not-found.component.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-not-found',
|
||||
imports: [],
|
||||
templateUrl: './not-found.component.html',
|
||||
styleUrl: './not-found.component.scss'
|
||||
})
|
||||
export class NotFoundComponent {
|
||||
|
||||
constructor(private readonly router: Router) {}
|
||||
|
||||
|
||||
volverAlInicio() {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
}
|
||||
87
src/app/pages/resumen/resumen.component.html
Normal file
87
src/app/pages/resumen/resumen.component.html
Normal file
@ -0,0 +1,87 @@
|
||||
<div class="p-3 text-white">
|
||||
<!-- Tabla1 -->
|
||||
<div class="font-bold tituloTabla my-2">Cronogramas aprobados:</div>
|
||||
<p-table
|
||||
id="verde"
|
||||
[value]="products"
|
||||
stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th class="bg-green-400 text-white font-bold">Empresa</th>
|
||||
<th class="bg-green-400 text-white font-bold">
|
||||
Código de cronograma
|
||||
</th>
|
||||
<th class="bg-green-400 text-white font-bold">Nombre sistema</th>
|
||||
<th class="bg-green-400 text-white font-bold">Tipo</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td style="background-color: #f8f9fa; border: none; width: 50px">
|
||||
<button
|
||||
pTooltip="Firma digital"
|
||||
tooltipPosition="left"
|
||||
(click)="firma(product)"
|
||||
showDelay="300"
|
||||
style="background: transparent; border: none; cursor: pointer"
|
||||
>
|
||||
<i class="pi pi-pen-to-square" style="font-size: 24px"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<div class="font-bold tituloTabla my-2">Cronogramas rechazados:</div>
|
||||
<p-table
|
||||
id="ploma"
|
||||
[value]="products"
|
||||
stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th class="tablaPloma text-white font-bold">Empresa</th>
|
||||
<th class="tablaPloma text-white font-bold">
|
||||
Código de cronograma
|
||||
</th>
|
||||
<th class="tablaPloma text-white font-bold">Nombre sistema</th>
|
||||
<th class="tablaPloma text-white font-bold">Tipo</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td style="background-color: #f8f9fa; border: none; width: 50px">
|
||||
<button
|
||||
pTooltip="Notificar a unidad de información o empresa"
|
||||
tooltipPosition="left"
|
||||
showDelay="300"
|
||||
style="background: transparent; border: none; cursor: pointer"
|
||||
>
|
||||
<i class="pi pi-arrow-circle-right" style="font-size: 24px"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
0
src/app/pages/resumen/resumen.component.scss
Normal file
0
src/app/pages/resumen/resumen.component.scss
Normal file
23
src/app/pages/resumen/resumen.component.spec.ts
Normal file
23
src/app/pages/resumen/resumen.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ResumenComponent } from './resumen.component';
|
||||
|
||||
describe('ResumenComponent', () => {
|
||||
let component: ResumenComponent;
|
||||
let fixture: ComponentFixture<ResumenComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ResumenComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ResumenComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
68
src/app/pages/resumen/resumen.component.ts
Normal file
68
src/app/pages/resumen/resumen.component.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||
import { VisorPdfComponent } from '../../components/visor-pdf/visor-pdf.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-resumen',
|
||||
imports: [FormsModule, TableModule, InputTextModule, TooltipModule],
|
||||
templateUrl: './resumen.component.html',
|
||||
styleUrl: './resumen.component.scss',
|
||||
providers: [DialogService]
|
||||
})
|
||||
export class ResumenComponent {
|
||||
|
||||
constructor(public dialogService: DialogService) { }
|
||||
|
||||
ref: DynamicDialogRef
|
||||
products: any[] = [
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
},
|
||||
];
|
||||
|
||||
firma(product: any): void {
|
||||
console.log('Firma digital del producto:', product);
|
||||
this.ref = this.dialogService.open(VisorPdfComponent, {
|
||||
header: 'Visor PDF',
|
||||
width: '80vw',
|
||||
height: '80vh',
|
||||
modal: true,
|
||||
closable: true,
|
||||
maximizable: true,
|
||||
data:{
|
||||
product: product
|
||||
},
|
||||
breakpoints: {
|
||||
'960px': '75vw',
|
||||
'640px': '90vw'
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<div class="p-3 text-white">
|
||||
<!-- Tabla1 -->
|
||||
<div class="font-bold tituloTabla my-2">Cronogramas solicitados y pendientes de carga:</div>
|
||||
<p-table id="azul"
|
||||
[value]="products" stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th class="tablaAzul text-white font-bold">Empresa</th>
|
||||
<th class="tablaAzul text-white font-bold">Código de cronograma SINAR</th>
|
||||
<th class="tablaAzul text-white font-bold">Contiene obras del año</th>
|
||||
<th class="tablaAzul text-white font-bold">N° Oficio que aprueba</th>
|
||||
<th class="tablaAzul text-white font-bold">Tipo de carga</th>
|
||||
<th class="tablaAzul text-white font-bold">Fecha de solicitud</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td>{{ product.dato5 }}</td>
|
||||
<td>{{ product.dato6 }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<!-- Tabla2 -->
|
||||
<div class="font-bold tituloTabla my-2">Cronogramas aprobados:</div>
|
||||
<p-table id="verde"
|
||||
[value]="products" stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th class="bg-green-400 text-white font-bold">Empresa</th>
|
||||
<th class="bg-green-400 text-white font-bold">Código cronograma</th>
|
||||
<th class="bg-green-400 text-white font-bold">Nombre sistema</th>
|
||||
<th class="bg-green-400 text-white font-bold">Tipo</th>
|
||||
<th class="bg-green-400 text-white font-bold">Fecha aprobación</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td>{{ product.dato5 }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<!-- Tabla3 -->
|
||||
<div class="font-bold tituloTabla my-2">Cronogramas rechazados:</div>
|
||||
<p-table id="ploma"
|
||||
[value]="products" stripedRows
|
||||
showGridlines
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Mostrando del {first} al {last} de un total de {totalRecords} registros"
|
||||
[rowsPerPageOptions]="[5, 10, 20]">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th class="tablaPloma text-white font-bold">Empresa</th>
|
||||
<th class="tablaPloma text-white font-bold">Código de cronograma</th>
|
||||
<th class="tablaPloma text-white font-bold">Nombre sistema</th>
|
||||
<th class="tablaPloma text-white font-bold">Tipo</th>
|
||||
<th class="tablaPloma text-white font-bold">Fecha rechazo</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product>
|
||||
<tr>
|
||||
<td>{{ product.empresa }}</td>
|
||||
<td>{{ product.codigoCronograma }}</td>
|
||||
<td>{{ product.codigoCronogramaAjuste }}</td>
|
||||
<td>{{ product.tipoCarga }}</td>
|
||||
<td>{{ product.dato5 }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UnidadInformacionComponent } from './unidad-informacion.component';
|
||||
|
||||
describe('UnidadInformacionComponent', () => {
|
||||
let component: UnidadInformacionComponent;
|
||||
let fixture: ComponentFixture<UnidadInformacionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UnidadInformacionComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UnidadInformacionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,48 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unidad-informacion',
|
||||
imports: [FormsModule, TableModule, InputTextModule],
|
||||
templateUrl: './unidad-informacion.component.html',
|
||||
styleUrl: './unidad-informacion.component.scss'
|
||||
})
|
||||
export class UnidadInformacionComponent {
|
||||
products: any[] = [
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
dato5: 'Inicial',
|
||||
dato6: 'Inicial',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
dato5: 'Inicial',
|
||||
dato6: 'Inicial',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
dato5: 'Inicial',
|
||||
dato6: 'Inicial',
|
||||
},
|
||||
{
|
||||
empresa: 'Empresa A',
|
||||
codigoCronograma: '123',
|
||||
codigoCronogramaAjuste: '456',
|
||||
tipoCarga: 'Inicial',
|
||||
dato5: 'Inicial',
|
||||
dato6: 'Inicial',
|
||||
},
|
||||
];
|
||||
|
||||
}
|
||||
41
src/app/services/actualizacion-pd.service.ts
Normal file
41
src/app/services/actualizacion-pd.service.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ActualizacionPd } from '../models/actualizacion-pd.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ActualizacionPdService {
|
||||
private apiUrl = 'api/actualizaciones';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getActualizaciones(): Observable<ActualizacionPd[]> {
|
||||
return this.http.get<ActualizacionPd[]>(this.apiUrl);
|
||||
}
|
||||
|
||||
getActualizacionById(id: number): Observable<ActualizacionPd> {
|
||||
return this.http.get<ActualizacionPd>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
createActualizacion(actualizacion: ActualizacionPd): Observable<ActualizacionPd> {
|
||||
return this.http.post<ActualizacionPd>(this.apiUrl, actualizacion);
|
||||
}
|
||||
|
||||
updateActualizacion(actualizacion: ActualizacionPd): Observable<ActualizacionPd> {
|
||||
return this.http.put<ActualizacionPd>(`${this.apiUrl}/${actualizacion.id}`, actualizacion);
|
||||
}
|
||||
|
||||
deleteActualizacion(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
aprobarActualizacion(id: number): Observable<ActualizacionPd> {
|
||||
return this.http.patch<ActualizacionPd>(`${this.apiUrl}/${id}/aprobar`, {});
|
||||
}
|
||||
|
||||
rechazarActualizacion(id: number, motivo: string): Observable<ActualizacionPd> {
|
||||
return this.http.patch<ActualizacionPd>(`${this.apiUrl}/${id}/rechazar`, { motivo });
|
||||
}
|
||||
}
|
||||
41
src/app/services/ajuste-pd.service.ts
Normal file
41
src/app/services/ajuste-pd.service.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AjustePd } from '../models/ajuste-pd.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AjustePdService {
|
||||
private apiUrl = 'api/ajustes';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getAjustes(): Observable<AjustePd[]> {
|
||||
return this.http.get<AjustePd[]>(this.apiUrl);
|
||||
}
|
||||
|
||||
getAjusteById(id: number): Observable<AjustePd> {
|
||||
return this.http.get<AjustePd>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
createAjuste(ajuste: AjustePd): Observable<AjustePd> {
|
||||
return this.http.post<AjustePd>(this.apiUrl, ajuste);
|
||||
}
|
||||
|
||||
updateAjuste(ajuste: AjustePd): Observable<AjustePd> {
|
||||
return this.http.put<AjustePd>(`${this.apiUrl}/${ajuste.id}`, ajuste);
|
||||
}
|
||||
|
||||
deleteAjuste(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
aprobarAjuste(id: number): Observable<AjustePd> {
|
||||
return this.http.patch<AjustePd>(`${this.apiUrl}/${id}/aprobar`, {});
|
||||
}
|
||||
|
||||
rechazarAjuste(id: number, motivo: string): Observable<AjustePd> {
|
||||
return this.http.patch<AjustePd>(`${this.apiUrl}/${id}/rechazar`, { motivo });
|
||||
}
|
||||
}
|
||||
128
src/app/services/alert.service.ts
Normal file
128
src/app/services/alert.service.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { Injectable, ComponentRef, createComponent, EnvironmentInjector, ApplicationRef } from '@angular/core';
|
||||
import { AlertDialogComponent, AlertDialogOptions } from '../components/alert-dialog/alert-dialog.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AlertService {
|
||||
private alertRef: ComponentRef<AlertDialogComponent> | null = null;
|
||||
|
||||
constructor(
|
||||
private injector: EnvironmentInjector,
|
||||
private appRef: ApplicationRef
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de alerta tipo Sweet Alert
|
||||
* @param options Opciones de configuración del alert
|
||||
* @returns Una promesa que se resuelve con true (confirmar) o false (cancelar)
|
||||
*/
|
||||
show(options: Partial<AlertDialogOptions> = {}): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// Si ya existe un alert, lo eliminamos
|
||||
this.closeCurrentAlert();
|
||||
|
||||
// Creamos el componente dinámicamente
|
||||
this.alertRef = createComponent(AlertDialogComponent, {
|
||||
environmentInjector: this.injector,
|
||||
});
|
||||
|
||||
// Agregamos a la aplicación y al DOM
|
||||
document.body.appendChild(this.alertRef.location.nativeElement);
|
||||
this.appRef.attachView(this.alertRef.hostView);
|
||||
|
||||
// Configuramos las opciones y mostramos
|
||||
this.alertRef.instance.show(options);
|
||||
|
||||
// Manejamos la respuesta
|
||||
const subscription = this.alertRef.instance.onConfirm.subscribe((result) => {
|
||||
subscription.unsubscribe();
|
||||
this.closeCurrentAlert();
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de éxito
|
||||
* @param message Mensaje a mostrar
|
||||
* @param title Título del alert (opcional)
|
||||
*/
|
||||
success(message: string, title: string = '¡Operación exitosa!'): Promise<boolean> {
|
||||
return this.show({
|
||||
title,
|
||||
message,
|
||||
icon: 'success',
|
||||
showCancelButton: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de error
|
||||
* @param message Mensaje a mostrar
|
||||
* @param title Título del alert (opcional)
|
||||
*/
|
||||
error(message: string, title: string = 'Error'): Promise<boolean> {
|
||||
return this.show({
|
||||
title,
|
||||
message,
|
||||
icon: 'error',
|
||||
showCancelButton: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de advertencia
|
||||
* @param message Mensaje a mostrar
|
||||
* @param title Título del alert (opcional)
|
||||
*/
|
||||
warning(message: string, title: string = 'Advertencia'): Promise<boolean> {
|
||||
return this.show({
|
||||
title,
|
||||
message,
|
||||
icon: 'warning',
|
||||
showCancelButton: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje informativo
|
||||
* @param message Mensaje a mostrar
|
||||
* @param title Título del alert (opcional)
|
||||
*/
|
||||
info(message: string, title: string = 'Información'): Promise<boolean> {
|
||||
return this.show({
|
||||
title,
|
||||
message,
|
||||
icon: 'info',
|
||||
showCancelButton: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un diálogo de confirmación
|
||||
* @param message Mensaje a mostrar
|
||||
* @param title Título del alert (opcional)
|
||||
*/
|
||||
confirm(message: string, title: string = '¿Está seguro?'): Promise<boolean> {
|
||||
return this.show({
|
||||
title,
|
||||
message,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Sí, confirmar',
|
||||
cancelButtonText: 'Cancelar'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cierra el alert actual si existe
|
||||
*/
|
||||
private closeCurrentAlert(): void {
|
||||
if (this.alertRef) {
|
||||
this.appRef.detachView(this.alertRef.hostView);
|
||||
this.alertRef.location.nativeElement.remove();
|
||||
this.alertRef = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/app/services/auth.service.ts
Normal file
111
src/app/services/auth.service.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
|
||||
import { tap, delay } from 'rxjs/operators';
|
||||
|
||||
interface LoginCredentials {
|
||||
username?: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
user: {
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
role: string;
|
||||
email?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = 'api/auth';
|
||||
private userSubject = new BehaviorSubject<any>(null);
|
||||
public user$ = this.userSubject.asObservable();
|
||||
|
||||
// Usuarios de prueba para simular login
|
||||
private mockUsers = [
|
||||
{
|
||||
email: 'admin@example.com',
|
||||
password: 'admin123',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
name: 'Administrador',
|
||||
role: 'admin',
|
||||
email: 'admin@example.com'
|
||||
}
|
||||
},
|
||||
{
|
||||
email: 'user@example.com',
|
||||
password: 'user123',
|
||||
user: {
|
||||
id: 2,
|
||||
username: 'user',
|
||||
name: 'Usuario',
|
||||
role: 'user',
|
||||
email: 'user@example.com'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
// Check if user is already logged in on service initialization
|
||||
const user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
this.userSubject.next(JSON.parse(user));
|
||||
}
|
||||
}
|
||||
|
||||
login(credentials: LoginCredentials): Observable<AuthResponse> {
|
||||
// Simular login con usuarios de prueba
|
||||
const user = this.mockUsers.find(u =>
|
||||
u.email === credentials.email && u.password === credentials.password);
|
||||
|
||||
if (user) {
|
||||
// Generar token falso
|
||||
const mockResponse: AuthResponse = {
|
||||
token: 'mock-jwt-token-' + Math.random().toString(36).substring(2, 15),
|
||||
user: user.user
|
||||
};
|
||||
|
||||
return of(mockResponse).pipe(
|
||||
// Simular retraso de red
|
||||
delay(800),
|
||||
tap(response => {
|
||||
// Store token and user info
|
||||
localStorage.setItem('token', response.token);
|
||||
localStorage.setItem('user', JSON.stringify(response.user));
|
||||
this.userSubject.next(response.user);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Simular error de credenciales inválidas
|
||||
return throwError(() => new Error('Credenciales incorrectas'));
|
||||
}
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
// Clear storage and update subject
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
this.userSubject.next(null);
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return !!localStorage.getItem('token');
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
getCurrentUser(): any {
|
||||
return this.userSubject.value;
|
||||
}
|
||||
}
|
||||
33
src/app/services/cronograma.service.ts
Normal file
33
src/app/services/cronograma.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Cronograma } from '../models/cronograma.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CronogramaService {
|
||||
private apiUrl = 'api/cronogramas';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getCronogramas(): Observable<Cronograma[]> {
|
||||
return this.http.get<Cronograma[]>(this.apiUrl);
|
||||
}
|
||||
|
||||
getCronogramaById(id: number): Observable<Cronograma> {
|
||||
return this.http.get<Cronograma>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
createCronograma(cronograma: Cronograma): Observable<Cronograma> {
|
||||
return this.http.post<Cronograma>(this.apiUrl, cronograma);
|
||||
}
|
||||
|
||||
updateCronograma(cronograma: Cronograma): Observable<Cronograma> {
|
||||
return this.http.put<Cronograma>(`${this.apiUrl}/${cronograma.id}`, cronograma);
|
||||
}
|
||||
|
||||
deleteCronograma(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
}
|
||||
33
src/app/services/empresa.service.ts
Normal file
33
src/app/services/empresa.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Empresa } from '../models/empresa.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class EmpresaService {
|
||||
private apiUrl = 'api/empresas';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getEmpresas(): Observable<Empresa[]> {
|
||||
return this.http.get<Empresa[]>(this.apiUrl);
|
||||
}
|
||||
|
||||
getEmpresaById(id: number): Observable<Empresa> {
|
||||
return this.http.get<Empresa>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
createEmpresa(empresa: Empresa): Observable<Empresa> {
|
||||
return this.http.post<Empresa>(this.apiUrl, empresa);
|
||||
}
|
||||
|
||||
updateEmpresa(empresa: Empresa): Observable<Empresa> {
|
||||
return this.http.put<Empresa>(`${this.apiUrl}/${empresa.id}`, empresa);
|
||||
}
|
||||
|
||||
deleteEmpresa(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
}
|
||||
17
src/app/services/estado-aprobacion.service.ts
Normal file
17
src/app/services/estado-aprobacion.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EstadoAprobacion } from '../models/estado-aprobacion.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class EstadoAprobacionService {
|
||||
private apiUrl = 'api/estadosAprobacion';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getEstadosAprobacion(): Observable<EstadoAprobacion[]> {
|
||||
return this.http.get<EstadoAprobacion[]>(this.apiUrl);
|
||||
}
|
||||
}
|
||||
9
src/app/services/index.ts
Normal file
9
src/app/services/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export * from './cronograma.service';
|
||||
export * from './empresa.service';
|
||||
export * from './actualizacion-pd.service';
|
||||
export * from './ajuste-pd.service';
|
||||
export * from './unidad-informacion.service';
|
||||
export * from './tipo-carga.service';
|
||||
export * from './estado-aprobacion.service';
|
||||
export * from './pdf.service';
|
||||
export * from './alert.service';
|
||||
214
src/app/services/pdf.service.ts
Normal file
214
src/app/services/pdf.service.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import pdfMake from 'pdfmake/build/pdfmake';
|
||||
import pdfFonts from 'pdfmake/build/vfs_fonts';
|
||||
import { TDocumentDefinitions } from 'pdfmake/interfaces';
|
||||
|
||||
pdfMake.vfs = pdfFonts.vfs;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PdfService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Genera un PDF con la definición por defecto para cronogramas
|
||||
* @param data Datos opcionales para personalizar el documento
|
||||
* @returns Promise con la URL de datos del PDF generado
|
||||
*/
|
||||
generateCronogramaPdf(data?: any): Promise<string> {
|
||||
const docDefinition = this.getCronogramaDocDefinition(data);
|
||||
return this.generatePdfDataUrl(docDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Descarga un PDF con la definición de cronograma
|
||||
* @param filename Nombre del archivo a descargar
|
||||
* @param data Datos opcionales para personalizar el documento
|
||||
*/
|
||||
downloadCronogramaPdf(filename: string = 'cronograma', data?: any): void {
|
||||
const docDefinition = this.getCronogramaDocDefinition(data);
|
||||
pdfMake.createPdf(docDefinition).download(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una URL de datos a partir de la definición del documento
|
||||
* @param docDefinition Definición del documento PDF
|
||||
* @returns Promise con la URL de datos del PDF
|
||||
*/
|
||||
private generatePdfDataUrl(docDefinition: TDocumentDefinitions): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const pdfDocGenerator = pdfMake.createPdf(docDefinition);
|
||||
pdfDocGenerator.getDataUrl((dataUrl) => {
|
||||
resolve(dataUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la definición del documento para un cronograma
|
||||
* @param data Datos opcionales para personalizar el documento
|
||||
* @returns Definición del documento PDF
|
||||
*/
|
||||
private getCronogramaDocDefinition(data?: any): TDocumentDefinitions {
|
||||
return {
|
||||
content: [
|
||||
// Encabezado con información de empresa y cronograma
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
width: '50%',
|
||||
text: 'Empresa: Constructora Los Andes S.A.',
|
||||
margin: [0, 0, 0, 5]
|
||||
},
|
||||
{
|
||||
width: '50%',
|
||||
text: [
|
||||
{ text: 'Cronograma: SC-23-45\n' },
|
||||
{ text: 'Nombre sistema: Terminal Portuario Norte' }
|
||||
],
|
||||
alignment: 'right',
|
||||
margin: [0, 0, 0, 5]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Título y subtítulo
|
||||
{
|
||||
text: 'CRONOGRAMA BASE DE OBRAS E INVERSIONES',
|
||||
alignment: 'center',
|
||||
bold: true,
|
||||
fontSize: 12,
|
||||
margin: [0, 10, 0, 0]
|
||||
},
|
||||
{
|
||||
text: 'ACTUALIZACIÓN PLAN DE DESARROLLO O NUEVA CONCESIÓN',
|
||||
alignment: 'center',
|
||||
bold: true,
|
||||
fontSize: 11,
|
||||
margin: [0, 0, 0, 10]
|
||||
},
|
||||
|
||||
// Tabla principal
|
||||
{
|
||||
table: {
|
||||
headerRows: 1,
|
||||
widths: ['10%', '12%', '23%', '12%', '10%', '10%', '10%', '13%'],
|
||||
body: [
|
||||
[
|
||||
{ text: 'ETAPA', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'CÓDIGO GLOSA', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'DESCRIPCIÓN GLOSA', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'MONTO INVERSIÓN UF', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'AÑO INICIO', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'AÑO TERMINO', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'MES TERMINO', style: 'tableHeader', alignment: 'center' },
|
||||
{ text: 'NOTA', style: 'tableHeader', alignment: 'center' }
|
||||
],
|
||||
[
|
||||
{ text: '1', alignment: 'center' },
|
||||
{ text: 'INF-001', alignment: 'center' },
|
||||
{ text: 'Estudios preliminares', alignment: 'left' },
|
||||
{ text: '2,500', alignment: 'right' },
|
||||
{ text: '2025', alignment: 'center' },
|
||||
{ text: '2025', alignment: 'center' },
|
||||
{ text: 'Julio', alignment: 'center' },
|
||||
{ text: 'Fase inicial', alignment: 'left' }
|
||||
],
|
||||
[
|
||||
{ text: '2', alignment: 'center' },
|
||||
{ text: 'MOV-102', alignment: 'center' },
|
||||
{ text: 'Movimiento de tierras', alignment: 'left' },
|
||||
{ text: '15,750', alignment: 'right' },
|
||||
{ text: '2025', alignment: 'center' },
|
||||
{ text: '2026', alignment: 'center' },
|
||||
{ text: 'Enero', alignment: 'center' },
|
||||
{ text: 'En terreno', alignment: 'left' }
|
||||
],
|
||||
[
|
||||
{ text: '3', alignment: 'center' },
|
||||
{ text: 'CIM-203', alignment: 'center' },
|
||||
{ text: 'Construcción de cimientos', alignment: 'left' },
|
||||
{ text: '32,800', alignment: 'right' },
|
||||
{ text: '2026', alignment: 'center' },
|
||||
{ text: '2026', alignment: 'center' },
|
||||
{ text: 'Mayo', alignment: 'center' },
|
||||
{ text: 'Crítico', alignment: 'left' }
|
||||
],
|
||||
[
|
||||
{ text: '4', alignment: 'center' },
|
||||
{ text: 'EST-304', alignment: 'center' },
|
||||
{ text: 'Estructura principal', alignment: 'left' },
|
||||
{ text: '65,420', alignment: 'right' },
|
||||
{ text: '2026', alignment: 'center' },
|
||||
{ text: '2027', alignment: 'center' },
|
||||
{ text: 'Febrero', alignment: 'center' },
|
||||
{ text: 'Alta inversión', alignment: 'left' }
|
||||
],
|
||||
[
|
||||
{ text: '5', alignment: 'center' },
|
||||
{ text: 'TER-405', alignment: 'center' },
|
||||
{ text: 'Terminaciones y acabados', alignment: 'left' },
|
||||
{ text: '18,300', alignment: 'right' },
|
||||
{ text: '2027', alignment: 'center' },
|
||||
{ text: '2027', alignment: 'center' },
|
||||
{ text: 'Octubre', alignment: 'center' },
|
||||
{ text: 'Etapa final', alignment: 'left' }
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Total
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
width: '80%',
|
||||
text: 'TOTAL:',
|
||||
alignment: 'right',
|
||||
bold: true,
|
||||
margin: [0, 15, 10, 20]
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
text: '134,770 UF',
|
||||
alignment: 'left',
|
||||
bold: true,
|
||||
margin: [0, 15, 0, 20]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Firma
|
||||
{
|
||||
text: 'X',
|
||||
alignment: 'center',
|
||||
bold: true,
|
||||
fontSize: 14,
|
||||
margin: [0, 15, 0, 0]
|
||||
},
|
||||
{
|
||||
text: 'Firma SSS',
|
||||
alignment: 'center',
|
||||
fontSize: 10,
|
||||
margin: [0, 0, 0, 10]
|
||||
}
|
||||
],
|
||||
|
||||
// Estilos
|
||||
styles: {
|
||||
tableHeader: {
|
||||
bold: true,
|
||||
fontSize: 10,
|
||||
fillColor: '#f2f2f2'
|
||||
}
|
||||
},
|
||||
|
||||
// Definiciones de página
|
||||
pageSize: 'A4',
|
||||
pageOrientation: 'portrait',
|
||||
pageMargins: [40, 40, 40, 40]
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/app/services/tipo-carga.service.ts
Normal file
17
src/app/services/tipo-carga.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TipoCarga } from '../models/tipo-carga.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TipoCargaService {
|
||||
private apiUrl = 'api/tiposCarga';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getTiposCarga(): Observable<TipoCarga[]> {
|
||||
return this.http.get<TipoCarga[]>(this.apiUrl);
|
||||
}
|
||||
}
|
||||
33
src/app/services/unidad-informacion.service.ts
Normal file
33
src/app/services/unidad-informacion.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { UnidadInformacion } from '../models/unidad-informacion.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UnidadInformacionService {
|
||||
private apiUrl = 'api/unidades';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getUnidades(): Observable<UnidadInformacion[]> {
|
||||
return this.http.get<UnidadInformacion[]>(this.apiUrl);
|
||||
}
|
||||
|
||||
getUnidadById(id: number): Observable<UnidadInformacion> {
|
||||
return this.http.get<UnidadInformacion>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
createUnidad(unidad: UnidadInformacion): Observable<UnidadInformacion> {
|
||||
return this.http.post<UnidadInformacion>(this.apiUrl, unidad);
|
||||
}
|
||||
|
||||
updateUnidad(unidad: UnidadInformacion): Observable<UnidadInformacion> {
|
||||
return this.http.put<UnidadInformacion>(`${this.apiUrl}/${unidad.id}`, unidad);
|
||||
}
|
||||
|
||||
deleteUnidad(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
}
|
||||
42
src/app/utils/custom-route-reuse-strategy.ts
Normal file
42
src/app/utils/custom-route-reuse-strategy.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Estrategia personalizada que evita la reutilización de rutas
|
||||
* para asegurar que los componentes se vuelvan a crear en cada navegación
|
||||
*/
|
||||
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
|
||||
/**
|
||||
* No almacenar ninguna ruta al desactivarse
|
||||
*/
|
||||
shouldDetach(route: ActivatedRouteSnapshot): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* No hay rutas almacenadas, así que esto nunca se llama
|
||||
*/
|
||||
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {}
|
||||
|
||||
/**
|
||||
* No recuperar rutas almacenadas
|
||||
*/
|
||||
shouldAttach(route: ActivatedRouteSnapshot): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* No hay rutas almacenadas, así que esto nunca se llama
|
||||
*/
|
||||
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* No reutilizar rutas para forzar la recreación de componentes
|
||||
*/
|
||||
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
|
||||
// Solo reutilizar si es la misma ruta exactamente (evita problemas con rutas anidadas)
|
||||
return future.routeConfig === curr.routeConfig &&
|
||||
JSON.stringify(future.params) === JSON.stringify(curr.params);
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CronogramasPrimeng</title>
|
||||
<title>Cronogramas</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preload" as="image" href="img/header2.webp" type="image/webp">
|
||||
<link rel="preload" as="image" href="img/footer-logo.webp" type="image/webp">
|
||||
<link rel="preload" as="image" href="img/gota.png" type="image/png">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
14
src/pipes/safe.pipe.ts
Normal file
14
src/pipes/safe.pipe.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'safe',
|
||||
standalone: true
|
||||
})
|
||||
export class SafePipe implements PipeTransform {
|
||||
constructor(private sanitizer: DomSanitizer) {}
|
||||
|
||||
transform(url: string): SafeResourceUrl {
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
}
|
||||
173
src/styles.scss
173
src/styles.scss
@ -1,69 +1,136 @@
|
||||
|
||||
|
||||
:root {
|
||||
--primary-color: #0088cc; // Color principal azul SISS
|
||||
--secondary-color: #0a2847; // Color azul oscuro
|
||||
--light-blue: #d3e9f7; // Color para el header
|
||||
--danger-color: #dc3545; // Color rojo para los indicadores en inputs
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
--primary-color: #0088cc; /* Azul principal SISS */
|
||||
--primary-light: #bcdaef; /* Azul claro para fondos */
|
||||
--text-color: #0a2847; /* Color texto principal */
|
||||
--secondary-text: #6c757d; /* Texto secundario */
|
||||
--border-color: #dee2e6; /* Color bordes */
|
||||
--background-color: #f8f9fa; /* Fondo gris claro */
|
||||
}
|
||||
|
||||
// Estilos generales
|
||||
|
||||
/* Estilos globales */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: var(--font-family);
|
||||
/* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; */
|
||||
/* font-family: "Museo Sans 300", "Museo Sans 700" !important; */
|
||||
font-family: "Museo Sans", sans-serif !important;
|
||||
font-weight: 300; /* o 700 según necesites */
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #f0f0f0;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
// Estilos específicos para inputs
|
||||
.p-inputtext {
|
||||
padding: 0.75rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
// Sobreescritura de estilos de PrimeNG
|
||||
.p-button {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 4px;
|
||||
|
||||
&.p-button-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
background-color: darken(#0088cc, 10%);
|
||||
border-color: darken(#0088cc, 10%);
|
||||
}
|
||||
|
||||
/* Clases de utilidad */
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
.mt-1 { margin-top: 0.25rem !important; }
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
.mt-4 { margin-top: 1.5rem !important; }
|
||||
|
||||
/* Personalizaciones globales de PrimeNG */
|
||||
/* .p-component {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
} */
|
||||
|
||||
.p-button.p-button-text {
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para fondos rojos en los addons de input
|
||||
.bg-red-600 {
|
||||
background-color: var(--danger-color) !important;
|
||||
border-color: var(--danger-color) !important;
|
||||
|
||||
.tituloTabla {
|
||||
color: #002147 !important;
|
||||
}
|
||||
|
||||
.tablaAzul{
|
||||
background-color: #008BC6 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
// Sobreescribe el estilo de los iconos en los input groups
|
||||
.p-inputgroup-addon {
|
||||
.pi {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
#azul .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(odd) {
|
||||
--p-datatable-row-striped-background: #EAF3FB;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
// Ajustes para espaciado consistente
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
|
||||
#azul .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(even) {
|
||||
--p-datatable-row-striped-background: #BCDAEF;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
|
||||
.tablaVerde{
|
||||
background-color: #008BC6 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#verde .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(odd) {
|
||||
--p-datatable-row-striped-background: #F5F9F5;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
#verde .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(even) {
|
||||
--p-datatable-row-striped-background: #D0E1CD;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
.tablaPloma {
|
||||
background-color: #706f6f !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#ploma .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(odd) {
|
||||
--p-datatable-row-striped-background: #E7E7E7;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
#ploma .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(even) {
|
||||
--p-datatable-row-striped-background: #CBCBCB;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
.tablaVerde {
|
||||
background-color: #706f6f !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#azul .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(odd) > td.bg-verde {
|
||||
--p-datatable-row-striped-background: #F5F9F5;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
#azul .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(even) > td.bg-verde {
|
||||
--p-datatable-row-striped-background: #D0E1CD;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
.tablaAzulFuerte{
|
||||
background-color: #156082 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#azul .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(odd) > td.bg-azulFuerte {
|
||||
--p-datatable-row-striped-background: #DCEAF7;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
#azul .p-datatable.p-datatable-striped .p-datatable-tbody > tr:nth-child(even) > td.bg-azulFuerte {
|
||||
--p-datatable-row-striped-background: #A6CAEC;
|
||||
background: var(--p-datatable-row-striped-background);
|
||||
}
|
||||
|
||||
/* Para redondear esquinas de tablas */
|
||||
.p-datatable-table-container{
|
||||
border-radius: var(--p-content-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectTabla{
|
||||
width: 228px !important;
|
||||
}
|
||||
|
||||
// Ajustes para el ancho completo
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"strict": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user