Merge branch 'dev'

This commit is contained in:
luis cespedes 2025-05-08 17:20:58 -04:00
commit ad653dbdb4
96 changed files with 6234 additions and 456 deletions

66
.dockerignore Normal file
View 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
View File

@ -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
View 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
View File

@ -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).

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/img/header2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

147
setup-project.js Normal file
View 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
View 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

View File

@ -1,2 +1,3 @@
<router-outlet />
<p-confirmDialog></p-confirmDialog>

View File

@ -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';

View File

@ -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
]
};

View File

@ -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' }
];

View 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>

View File

@ -0,0 +1,11 @@
:host ::ng-deep {
.alert-dialog {
.p-dialog-header {
padding-bottom: 0.5rem;
}
.p-dialog-content {
overflow-y: visible;
}
}
}

View 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';
}
}
}

View 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>&aacute;rea de Informaci&oacute;n y Tecnolog&iacute;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&oacute;n: Moneda 673 Piso 9 - Metro Santa Luc&iacute;a</div>
<div>Mesa Central: 2 2382 4000</div>
</div>
</div>
</footer>

View 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;
}
}

View 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();
});
});

View 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 {
}

View 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>

View 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);
}
}

View 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();
});
});

View 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);
}
}
}

View 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>

View 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;
}
}

View 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();
});
});

View 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']);
}
}

View File

@ -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();
}
}
}

View 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>

View 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;
}
}

View 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();
});
});

View 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 {
}

View 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>

View 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;
}

View 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();
});
});

View 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();
}
}

View 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;
};

View 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);
};

View 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;
}

View 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;
}

View 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';
}

View File

@ -0,0 +1,4 @@
export interface Empresa {
id?: number;
name: string;
}

View File

@ -0,0 +1,4 @@
export interface EstadoAprobacion {
id?: number;
name: string;
}

7
src/app/models/index.ts Normal file
View 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';

View File

@ -0,0 +1,4 @@
export interface TipoCarga {
id?: number;
name: string;
}

View File

@ -0,0 +1,6 @@
import { Cronograma } from './cronograma.model';
export interface UnidadInformacion extends Cronograma {
dato5?: string;
dato6?: string;
}

View 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&oacute;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&iacute;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&oacute;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&oacute;n</th>
<th class="tablaAzul font-bold text-white">C&oacute;digo de glosa PD</th>
<th class="tablaAzul font-bold text-white">Descripci&oacute;n glosa</th>
<th class="tablaAzul font-bold text-white">Monto Inversi&oacute;n Total (UF)</th>
<th class="tablaAzul font-bold text-white">A&ntilde;o de Inicio</th>
<th class="tablaAzul font-bold text-white">A&ntilde;o de T&eacute;rmino</th>
<th class="tablaAzul font-bold text-white">Mes de T&eacute;rmino</th>
<th class="tablaAzul font-bold text-white">Nota</th>
<th class="bg-green-400 font-bold text-white">Estado aprobaci&oacute;n</th>
<th class="bg-green-400 font-bold text-white">Observaci&oacute;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>

View File

@ -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();
});
});

View 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);
}
}

View 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&oacute;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&iacute;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&oacute;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&oacute;n</th>
<th class="tablaAzul text-white font-bold">C&oacute;digo de glosa PD</th>
<th class="tablaAzul text-white font-bold">Descripci&oacute;n glosa</th>
<th class="tablaAzul text-white font-bold">Monto Inversi&oacute;n Total (UF)</th>
<th class="tablaAzul text-white font-bold">A&ntilde;o de Inicio</th>
<th class="tablaAzul text-white font-bold">A&ntilde;o de T&eacute;rmino</th>
<th class="tablaAzul text-white font-bold">Mes de T&eacute;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&ntilde;o de Inicio</th>
<th class="bg-blue-700 text-white font-bold">A&ntilde;o de T&eacute;rmino</th>
<th class="bg-blue-700 text-white font-bold">Mes de T&eacute;rmino</th>
<th class="tablaAzul text-white font-bold">Nota</th>
<th class="bg-green-400 text-white font-bold">Estado aprobaci&oacute;n</th>
<th class="bg-green-400 text-white font-bold">Observaci&oacute;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>

View 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();
});
});

View 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));
}
}

View 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&oacute;digo de cronograma</th>
<th class="tablaAzul font-bold">C&oacute;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&oacute;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&aacute;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&oacute;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&ntilde;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&oacute;digo de cronograma SINAR <p-sortIcon field="codigoCronograma" />
</th>
<th pSortableColumn="codigoCronogramaAjuste" class="tablaAzul font-bold">
Contiene obras del a&ntilde;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>

View 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%;
}
}

View 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();
});
});

View 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'
},
];
}

View 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>

View 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;
}

View 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();
});
});

View 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 {
}

View File

@ -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&#64;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">&#64;</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>

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View 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>

View 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;
}

View 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();
});
});

View 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(['/']);
}
}

View 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&oacute;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&oacute;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>

View 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();
});
});

View 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'
},
});
}
}

View File

@ -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&oacute;digo de cronograma SINAR</th>
<th class="tablaAzul text-white font-bold">Contiene obras del a&ntilde;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&oacute;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&oacute;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&oacute;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>

View File

@ -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();
});
});

View File

@ -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',
},
];
}

View 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 });
}
}

View 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 });
}
}

View 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;
}
}
}

View 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;
}
}

View 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}`);
}
}

View 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}`);
}
}

View 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);
}
}

View 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';

View 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]
};
}
}

View 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);
}
}

View 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}`);
}
}

View 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);
}
}

View File

@ -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
View 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);
}
}

View File

@ -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;
}

View File

@ -4,7 +4,7 @@
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"strict": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,