utn-cli 2.0.70 → 2.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. package/package.json +1 -1
  2. package/templates/backend/rutas/ConfiguracionDeTarjetas.js +52 -0
  3. package/templates/backend/rutas/rutas.js +2 -0
  4. package/templates/backend/servicios/Nucleo/ConfiguracionDeTarjetas.js +46 -0
  5. package/templates/bd/docker-scripts/1-crear estructura.sql +10 -0
  6. package/templates/frontend/src/app/Componentes/Nucleo/tabla/tabla.component.ts +13 -6
  7. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.css +24 -20
  8. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-multiple/tarjeta-multiple.component.css +11 -16
  9. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-multiple/tarjeta-multiple.component.html +1 -1
  10. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-multiple/tarjeta-multiple.component.ts +1 -0
  11. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.css +16 -20
  12. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.html +1 -1
  13. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.ts +1 -0
  14. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +30 -26
  15. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.css +49 -1
  16. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.html +33 -15
  17. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.ts +267 -20
  18. package/templates/frontend/src/app/app.config.ts +2 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.0.70",
3
+ "version": "2.0.72",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,52 @@
1
+ const express = require("express");
2
+ const Router = express.Router();
3
+
4
+ const ConfiguracionDeTarjetas = require('../servicios/Nucleo/ConfiguracionDeTarjetas.js');
5
+ const Miscelaneo = require('../servicios/Nucleo/Miscelaneas.js');
6
+ const ManejadorDeErrores = require('../servicios/Nucleo/ManejadorDeErrores.js');
7
+
8
+ Router.get("/obtener", async (solicitud, respuesta, next) => {
9
+ try {
10
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
11
+ try {
12
+ const Usuario = await Miscelaneo.obtenerDatosDelUsuario(solicitud.headers.authorization);
13
+ const Datos = {
14
+ Identificador: Usuario.uid
15
+ };
16
+ return respuesta.json({ body: await ConfiguracionDeTarjetas.obtener(Datos), error: undefined });
17
+ } catch (error) {
18
+ const MensajeDeError = "No fue posible obtener la configuración de las tarjetas";
19
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
20
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
21
+ }
22
+ }
23
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
24
+ } catch (error) {
25
+ next(error);
26
+ }
27
+ });
28
+
29
+ Router.post("/actualizar", async (solicitud, respuesta, next) => {
30
+ try {
31
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
32
+ try {
33
+ const Usuario = await Miscelaneo.obtenerDatosDelUsuario(solicitud.headers.authorization);
34
+ const Datos = {
35
+ Identificador: Usuario.uid,
36
+ Configuraciones: solicitud.body.Configuraciones,
37
+ LastUser: await Miscelaneo.generarLastUser(solicitud)
38
+ };
39
+ return respuesta.json({ body: await ConfiguracionDeTarjetas.actualizar(Datos), error: undefined });
40
+ } catch (error) {
41
+ const MensajeDeError = "No fue posible actualizar la configuración de las tarjetas";
42
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
43
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
44
+ }
45
+ }
46
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
47
+ } catch (error) {
48
+ next(error);
49
+ }
50
+ });
51
+
52
+ module.exports = Router;
@@ -1,12 +1,14 @@
1
1
  const rutasMiscelaneas = require('./misc.js');
2
2
  const rutasDeAPI = require('./API.js');
3
3
  const rutasDeConsentimientoInformado = require('./ConsentimientoInformado.js');
4
+ const rutasDeLaConfiguracionDeTarjetas = require('./ConfiguracionDeTarjetas.js');
4
5
  const rutasDelServicio1 = require('./Servicio1.js');
5
6
 
6
7
  function asignarRutasAExpress(app) {
7
8
  app.use('/misc', rutasMiscelaneas);
8
9
  app.use('/api', rutasDeAPI);
9
10
  app.use('/ConsentimientoInformado', rutasDeConsentimientoInformado);
11
+ app.use('/ConfiguracionDeTarjetas', rutasDeLaConfiguracionDeTarjetas);
10
12
  app.use('/Servicio1', rutasDelServicio1);
11
13
  }
12
14
 
@@ -0,0 +1,46 @@
1
+ const { ejecutarConsulta } = require('./db.js');
2
+ const Miscelaneas = require('./Miscelaneas.js');
3
+
4
+ class ConfiguracionDeTarjetas {
5
+ constructor() {
6
+ }
7
+
8
+ async obtener(Datos) {
9
+ return await ejecutarConsulta(
10
+ "SELECT Titulo, Posicion, ColorDeBorde FROM `framework-mantenimientosv2`.`ConfiguracionDeTarjetas` WHERE Identificador = ? ORDER BY Posicion",
11
+ [Datos.Identificador]
12
+ );
13
+ }
14
+
15
+ async actualizar(Datos) {
16
+ const { Identificador, Configuraciones, LastUser } = Datos;
17
+
18
+ // Configuraciones is expected to be an array of { Titulo, Posicion, ColorDeBorde }
19
+ if (!Array.isArray(Configuraciones) || Configuraciones.length === 0) {
20
+ return;
21
+ }
22
+
23
+ const values = [];
24
+ const placeholders = [];
25
+
26
+ Configuraciones.forEach(config => {
27
+ placeholders.push('(?, ?, ?, ?, CURRENT_TIMESTAMP(4), ?)');
28
+ values.push(Identificador, config.Titulo, config.Posicion, config.ColorDeBorde || null, LastUser);
29
+ });
30
+
31
+ const sql = "\
32
+ INSERT INTO `framework-mantenimientosv2`.`ConfiguracionDeTarjetas` \
33
+ (Identificador, Titulo, Posicion, ColorDeBorde, LastUpdate, LastUser) \
34
+ VALUES " + placeholders.join(', ') +
35
+ " ON DUPLICATE KEY UPDATE \
36
+ Posicion = VALUES(Posicion), \
37
+ ColorDeBorde = VALUES(ColorDeBorde), \
38
+ LastUpdate = VALUES(LastUpdate), \
39
+ LastUser = VALUES(LastUser) \
40
+ ";
41
+
42
+ return await ejecutarConsulta(sql, values);
43
+ }
44
+ }
45
+
46
+ module.exports = new ConfiguracionDeTarjetas();
@@ -27,3 +27,13 @@ CREATE OR REPLACE TABLE `NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS`.`AceptacionesD
27
27
  PRIMARY KEY(`AceptacionDeConsentimientoInformadoId`),
28
28
  KEY (`Identificador`)
29
29
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_spanish_ci COMMENT = 'Almacena las aceptaciones de los consentimientos informados';
30
+
31
+ CREATE OR REPLACE TABLE `NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS`.`framework-mantenimientosv2`.`ConfiguracionDeTarjetas` (
32
+ `Identificador` INT UNSIGNED NOT NULL COMMENT 'Llave foránea virtual correspondiente a `Identificador` en la tabla `SIGU`.`SIGU_Personas`',
33
+ `Titulo` VARCHAR(200) NOT NULL COMMENT 'Título de la tarjeta que sirve como identificador único por usuario',
34
+ `Posicion` SMALLINT UNSIGNED NOT NULL COMMENT 'Posición de la tarjeta',
35
+ `ColorDeBorde` VARCHAR(10) DEFAULT NULL COMMENT 'Color de borde de la tarjeta',
36
+ `LastUpdate` DATETIME(4) NOT NULL DEFAULT CURRENT_TIMESTAMP(4) ON UPDATE CURRENT_TIMESTAMP(4) COMMENT 'Fecha de la última actualización de la fila',
37
+ `LastUser` VARCHAR(1000) NOT NULL DEFAULT '-' COMMENT 'Último usuario que modificó la fila',
38
+ PRIMARY KEY (`Identificador`, `Titulo`)
39
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_spanish_ci COMMENT = 'Almacena la configuración de las tarjetas del contenedor principal por usuario';
@@ -1,4 +1,4 @@
1
- import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
1
+ import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, OnDestroy, Output, SimpleChanges, ViewChild } from '@angular/core';
2
2
  import { MatTableDataSource, MatTableModule } from '@angular/material/table';
3
3
  import { MatIconModule } from '@angular/material/icon';
4
4
  import { MatButtonModule } from '@angular/material/button';
@@ -14,6 +14,7 @@ import { MatBadgeModule } from '@angular/material/badge';
14
14
  import { MatMenuModule } from '@angular/material/menu';
15
15
  import { MatCheckboxModule } from '@angular/material/checkbox';
16
16
  import { FormsModule } from '@angular/forms';
17
+ import { Subscription } from 'rxjs';
17
18
 
18
19
  @Component({
19
20
  selector: 'app-tabla',
@@ -42,7 +43,7 @@ import { FormsModule } from '@angular/forms';
42
43
  templateUrl: './tabla.component.html',
43
44
  styleUrl: './tabla.component.css'
44
45
  })
45
- export class TablaComponent implements OnInit, OnChanges {
46
+ export class TablaComponent implements OnInit, OnChanges, OnDestroy {
46
47
  @Input() columnas: { Llave: string, Alias: string, TipoDeFiltro: string, Visible?: boolean }[] = [];
47
48
  @Input() subColumnas: { Llave: string, Alias: string }[] = [];
48
49
  @Input() datos: any[] = [];
@@ -80,6 +81,8 @@ export class TablaComponent implements OnInit, OnChanges {
80
81
  @Input() TotalDeRegistros: number = 0;
81
82
  @Output() CambioDePagina: EventEmitter<{ accion: string, pageIndex: number, pageSize: number, valorParaFiltrar: string, columnaParaFiltrar: string, columnasParaFiltrar: string, ordenColumna: string, ordenDireccion: string }> = new EventEmitter();
82
83
 
84
+ private subscriptions: Subscription = new Subscription();
85
+
83
86
  constructor(private dialog: MatDialog) {
84
87
  const pageSize = localStorage.getItem('pageSize');
85
88
  if (pageSize) {
@@ -204,7 +207,7 @@ export class TablaComponent implements OnInit, OnChanges {
204
207
 
205
208
  ngAfterViewInit() {
206
209
  this.dataSource.sort = this.sort;
207
- this.paginator.page.subscribe((event: PageEvent) => {
210
+ this.subscriptions.add(this.paginator.page.subscribe((event: PageEvent) => {
208
211
  localStorage.setItem('pageSize', event.pageSize.toString());
209
212
 
210
213
  let accion = '';
@@ -229,8 +232,8 @@ export class TablaComponent implements OnInit, OnChanges {
229
232
 
230
233
  this.ultimaPagina = event.pageIndex;
231
234
  this.ultimoPageSize = event.pageSize;
232
- });
233
- this.sort.sortChange.subscribe(sort => {
235
+ }));
236
+ this.subscriptions.add(this.sort.sortChange.subscribe(sort => {
234
237
  if (this.paginarResultados) {
235
238
  this.CambioDePagina.emit({
236
239
  accion: 'Cambio de orden',
@@ -243,7 +246,11 @@ export class TablaComponent implements OnInit, OnChanges {
243
246
  ordenDireccion: sort.direction
244
247
  });
245
248
  }
246
- });
249
+ }));
247
250
  this.actualizarDataSource();
248
251
  }
252
+
253
+ ngOnDestroy() {
254
+ this.subscriptions.unsubscribe();
255
+ }
249
256
  }
@@ -5,32 +5,32 @@
5
5
  }
6
6
 
7
7
  .contenedor {
8
- margin-top: 3%;
9
- width: 20vw;
8
+ width: 100%;
9
+ /* Ocupa todo el espacio que le dé el padre (cdkDrag) */
10
10
  background: #ffffff 0% 0% no-repeat padding-box;
11
11
  box-shadow: 0px 3px 6px #00000029;
12
12
  border-radius: 10px;
13
13
  opacity: 1;
14
- margin-right: 3vw;
15
- padding: 5%;
14
+ padding: 16px;
16
15
  cursor: pointer;
16
+ display: flex;
17
+ flex-direction: column;
18
+ height: 100%;
19
+ /* Asegura que todas las tarjetas tengan la misma altura si se desea */
20
+ box-sizing: border-box;
17
21
  }
18
22
 
19
- @media (max-width:480px) {
20
- .contenedor {
21
- width: 80vw;
22
- }
23
- }
24
-
25
- @media (min-width:481px) and (max-width:1025px) {
26
- .contenedor {
27
- width: 50vw;
28
- }
29
- }
23
+ /* Eliminamos los media queries antiguos que forzaban anchos en vw */
30
24
 
31
25
  .contenedor .cabecera_contenedor {
32
26
  display: flex;
33
27
  flex-direction: row;
28
+ align-items: flex-start;
29
+ /* Alinea los elementos al inicio (arriba) */
30
+ justify-content: space-between;
31
+ /* Separa el texto del icono */
32
+ gap: 8px;
33
+ /* Espacio mínimo entre texto e icono */
34
34
  }
35
35
 
36
36
  .contenedor .cabecera_contenedor .icono {
@@ -39,13 +39,14 @@
39
39
  border-radius: 50%;
40
40
  display: flex;
41
41
  align-items: center;
42
- transform: translate(0);
43
- height: 75%;
42
+ justify-content: center;
43
+ width: 48px;
44
+ height: 48px;
45
+ min-width: 48px;
46
+ /* Evita que flexbox lo encoja */
44
47
  color: #1b3069;
45
48
  font-size: x-small;
46
49
  font-weight: bold;
47
- margin-left: 5%;
48
- padding: 2%;
49
50
  }
50
51
 
51
52
  .contenedor .cabecera_contenedor .icono .numero {
@@ -53,7 +54,10 @@
53
54
  }
54
55
 
55
56
  .contenedor .cabecera {
56
- width: 90%;
57
+ flex: 1;
58
+ /* Permite que el texto ocupe todo el espacio sobrante */
59
+ min-width: 0;
60
+ /* Permite truncar texto si es muy largo en móviles */
57
61
  }
58
62
 
59
63
  .contenedor .cabecera .titulo {
@@ -5,27 +5,19 @@
5
5
  }
6
6
 
7
7
  .contenedor {
8
- margin-top: 3%;
9
- margin-right: 3vw;
10
- width: 20vw;
11
- padding: 5%;
8
+ width: 100%;
9
+ padding: 16px;
12
10
  background: #ffffff;
13
11
  box-shadow: 0px 3px 6px #00000029;
14
12
  border-radius: 10px;
15
13
  cursor: pointer;
14
+ display: flex;
15
+ flex-direction: column;
16
+ height: 100%;
17
+ box-sizing: border-box;
16
18
  }
17
19
 
18
- @media (max-width: 480px) {
19
- .contenedor {
20
- width: 80vw;
21
- }
22
- }
23
-
24
- @media (min-width: 481px) and (max-width: 1025px) {
25
- .contenedor {
26
- width: 50vw;
27
- }
28
- }
20
+ /* Eliminamos los media queries antiguos que forzaban anchos en vw */
29
21
 
30
22
  .cabecera_contenedor {
31
23
  display: flex;
@@ -59,10 +51,13 @@
59
51
  display: flex;
60
52
  align-items: center;
61
53
  justify-content: center;
54
+ width: 48px;
55
+ height: 48px;
56
+ min-width: 48px; /* Evita que flexbox lo encoja */
62
57
  color: #1b3069;
63
- padding: 8px;
64
58
  cursor: pointer;
65
59
  position: relative;
60
+ margin-left: auto; /* Empuja el icono a la derecha si es necesario */
66
61
  }
67
62
 
68
63
  .menu-rutas {
@@ -1,4 +1,4 @@
1
- <div class="contenedor" (click)="irAPrimeraRuta()">
1
+ <div class="contenedor" (click)="irAPrimeraRuta()" [style.border]="ColorDeBorde ? '1px solid ' + ColorDeBorde : ''">
2
2
  <div class="cabecera_contenedor">
3
3
  <div class="cabecera">
4
4
  <p class="titulo">{{ titulo }}</p>
@@ -15,6 +15,7 @@ export class TarjetaMultipleComponent {
15
15
  @Input() titulo: string = "";
16
16
  @Input() descripcion: string = "";
17
17
  @Input() rutas: { nombre: string; ruta: string }[] = [];
18
+ @Input() ColorDeBorde: string = '';
18
19
 
19
20
  mostrarMenu: boolean = false;
20
21
 
@@ -5,32 +5,27 @@
5
5
  }
6
6
 
7
7
  .contenedor {
8
- margin-top: 3%;
9
- width: 20vw;
8
+ width: 100%;
10
9
  background: #ffffff 0% 0% no-repeat padding-box;
11
10
  box-shadow: 0px 3px 6px #00000029;
12
11
  border-radius: 10px;
13
12
  opacity: 1;
14
- margin-right: 3vw;
15
- padding: 5%;
13
+ padding: 16px;
16
14
  cursor: pointer;
15
+ display: flex;
16
+ flex-direction: column;
17
+ height: 100%;
18
+ box-sizing: border-box;
17
19
  }
18
20
 
19
- @media (max-width:480px) {
20
- .contenedor {
21
- width: 80vw;
22
- }
23
- }
24
-
25
- @media (min-width:481px) and (max-width:1025px) {
26
- .contenedor {
27
- width: 50vw;
28
- }
29
- }
21
+ /* Eliminamos los media queries antiguos que forzaban anchos en vw */
30
22
 
31
23
  .contenedor .cabecera_contenedor {
32
24
  display: flex;
33
25
  flex-direction: row;
26
+ align-items: flex-start;
27
+ justify-content: space-between;
28
+ gap: 8px;
34
29
  }
35
30
 
36
31
  .contenedor .cabecera_contenedor .icono {
@@ -39,13 +34,13 @@
39
34
  border-radius: 50%;
40
35
  display: flex;
41
36
  align-items: center;
42
- transform: translate(0);
43
- height: 75%;
37
+ justify-content: center;
38
+ width: 48px;
39
+ height: 48px;
40
+ min-width: 48px; /* Evita que flexbox lo encoja */
44
41
  color: #1b3069;
45
42
  font-size: x-small;
46
43
  font-weight: bold;
47
- margin-left: 5%;
48
- padding: 2%;
49
44
  }
50
45
 
51
46
  .contenedor .cabecera_contenedor .icono .numero {
@@ -53,7 +48,8 @@
53
48
  }
54
49
 
55
50
  .contenedor .cabecera {
56
- width: 90%;
51
+ flex: 1;
52
+ min-width: 0;
57
53
  }
58
54
 
59
55
  .contenedor .cabecera .titulo {
@@ -1,4 +1,4 @@
1
- <div class="contenedor" (click)="onClick()">
1
+ <div class="contenedor" (click)="onClick()" [style.border]="ColorDeBorde ? '1px solid ' + ColorDeBorde : ''">
2
2
  <div class="cabecera_contenedor">
3
3
  <div class="cabecera">
4
4
  <p class="titulo">
@@ -12,6 +12,7 @@ export class TarjetaReporteComponent {
12
12
  @Input() descripcion: string = "";
13
13
  @Input() icono: any = null;
14
14
  @Input() reporteAGenerar: string = '';
15
+ @Input() ColorDeBorde: string = '';
15
16
  @Output() GenerarReporte: EventEmitter<{ reporte: string }> = new EventEmitter();
16
17
 
17
18
  constructor() { }
@@ -1,5 +1,5 @@
1
1
  import { HttpClient, HttpHeaders } from '@angular/common/http';
2
- import { Component, OnInit, OnDestroy } from '@angular/core';
2
+ import { Component, OnInit, OnDestroy, NgZone } from '@angular/core';
3
3
  import { RouterOutlet, Router } from '@angular/router';
4
4
  import { DatosGlobalesService } from '../../../datos-globales.service';
5
5
  import { Location, CommonModule } from '@angular/common';
@@ -36,7 +36,7 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy {
36
36
  public AnimarUsuariosActivos: boolean = true;
37
37
  private intervaloUsuarios: any;
38
38
 
39
- constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private location: Location, private dialog: MatDialog, private router: Router) {
39
+ constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private location: Location, private dialog: MatDialog, private router: Router, private ngZone: NgZone) {
40
40
  if (datosGlobalesService.ObtenerToken() === '') {
41
41
  datosGlobalesService.RedirigirALogin();
42
42
  }
@@ -57,10 +57,12 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy {
57
57
  this.claseDelContenedor = 'contenedor';
58
58
  }
59
59
 
60
- this.obtenerUsuariosActuales();
61
- this.intervaloUsuarios = setInterval(() => {
60
+ this.ngZone.runOutsideAngular(() => {
62
61
  this.obtenerUsuariosActuales();
63
- }, 60000);
62
+ this.intervaloUsuarios = setInterval(() => {
63
+ this.obtenerUsuariosActuales();
64
+ }, 60000);
65
+ });
64
66
 
65
67
  this.http.get(this.datosGlobalesService.ObtenerURL() + 'misc/validarToken').subscribe((datos: any) => {
66
68
  this.TienePermiso = datos.body;
@@ -224,31 +226,33 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy {
224
226
 
225
227
  obtenerUsuariosActuales(): void {
226
228
  this.http.get(this.datosGlobalesService.ObtenerURL() + 'misc/UsuariosActuales').subscribe((datos: any) => {
227
- const data = datos.body;
229
+ this.ngZone.run(() => {
230
+ const data = datos.body;
228
231
 
229
- let actuales = data.UsuariosActuales;
230
- let activos = data.UsuariosActivos;
232
+ let actuales = data.UsuariosActuales;
233
+ let activos = data.UsuariosActivos;
231
234
 
232
- // Si activos es mayor a actuales, igualamos activos a actuales
233
- if (activos > actuales) {
234
- activos = actuales;
235
- }
235
+ // Si activos es mayor a actuales, igualamos activos a actuales
236
+ if (activos > actuales) {
237
+ activos = actuales;
238
+ }
236
239
 
237
- if (this.UsuariosActuales !== actuales) {
238
- this.UsuariosActuales = actuales;
239
- this.AnimarUsuarios = false;
240
- setTimeout(() => {
241
- this.AnimarUsuarios = true;
242
- }, 50);
243
- }
240
+ if (this.UsuariosActuales !== actuales) {
241
+ this.UsuariosActuales = actuales;
242
+ this.AnimarUsuarios = false;
243
+ setTimeout(() => {
244
+ this.AnimarUsuarios = true;
245
+ }, 50);
246
+ }
244
247
 
245
- if (this.UsuariosActivos !== activos) {
246
- this.UsuariosActivos = activos;
247
- this.AnimarUsuariosActivos = false;
248
- setTimeout(() => {
249
- this.AnimarUsuariosActivos = true;
250
- }, 50);
251
- }
248
+ if (this.UsuariosActivos !== activos) {
249
+ this.UsuariosActivos = activos;
250
+ this.AnimarUsuariosActivos = false;
251
+ setTimeout(() => {
252
+ this.AnimarUsuariosActivos = true;
253
+ }, 50);
254
+ }
255
+ });
252
256
  });
253
257
  }
254
258
  }
@@ -1,4 +1,52 @@
1
1
  .contenido {
2
2
  display: flex;
3
3
  flex-wrap: wrap;
4
- }
4
+ gap: 16px;
5
+ padding: 16px;
6
+ box-sizing: border-box;
7
+ width: 100%;
8
+ }
9
+
10
+ /* Forzamos 4 columnas calculando el espacio disponible */
11
+ .contenido > div[cdkDrag] {
12
+ width: calc(25% - 12px); /* 25% menos una parte del gap (16px * 3 / 4) */
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .cdk-drag-preview {
17
+ box-sizing: border-box;
18
+ border-radius: 8px;
19
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .cdk-drag-placeholder {
24
+ opacity: 0.3;
25
+ background: #ccc;
26
+ border: 2px dashed #999;
27
+ border-radius: 8px;
28
+ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
29
+ /* El placeholder debe mantener el tamaño de la tarjeta */
30
+ min-height: 150px; /* Ajustar según la altura promedio de tus tarjetas */
31
+ }
32
+
33
+ .cdk-drag-animating {
34
+ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
35
+ }
36
+
37
+ .contenido.cdk-drop-list-dragging div:not(.cdk-drag-placeholder) {
38
+ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
39
+ }
40
+
41
+ /* Responsividad: 2 columnas en tablets, 1 en móviles */
42
+ @media (max-width: 1200px) {
43
+ .contenido > div[cdkDrag] {
44
+ width: calc(50% - 8px);
45
+ }
46
+ }
47
+
48
+ @media (max-width: 600px) {
49
+ .contenido > div[cdkDrag] {
50
+ width: 100%;
51
+ }
52
+ }
@@ -1,17 +1,35 @@
1
- <div class="contenido">
2
- <app-tarjeta [rutaASeguir]="'tabla'" titulo="Registro"
3
- descripcion="Módulo para el registro de las ausencias por concepto de incapacidades, licencias o permisos"
4
- icono="inventory"></app-tarjeta>
5
- <app-tarjeta [rutaASeguir]="'graficoDeModulos'" titulo="Gráficos"
6
- descripcion="Módulo para la visualización de gráficos sobre módulosV2" icono="analytics"></app-tarjeta>
7
- <app-tarjeta [rutaASeguir]="'parametros-reportes'" titulo="Reportes sobre el módulo"
8
- descripcion="Módulo para la gestión de los reportes del módulo" icono="analytics"></app-tarjeta>
9
- <app-tarjeta [rutaASeguir]="'gestionIframe1'" titulo="Matrícula por sedes" descripcion="Matrícula por sedes"
10
- icono="mail"></app-tarjeta>
11
- @if(cantidadMaxima > 0) {
12
- <app-tarjeta [rutaASeguir]="'aprobaciones'" titulo="Aprobaciones" [cantidad]="cantidad"
13
- [cantidadMaxima]="cantidadMaxima"
14
- descripcion="Módulo para las aprobaciones de las ausencias por concepto de incapacidades, licencias o permisos por parte de las personas jefes"
15
- icono="check"></app-tarjeta>
1
+ <div class="contenido" cdkDropList cdkDropListOrientation="mixed" (cdkDropListDropped)="drop($event)">
2
+ @for (tarjeta of tarjetas; track tarjeta.titulo) {
3
+ <div cdkDrag>
4
+ @switch (tarjeta.type) {
5
+ @case ('single') {
6
+ <app-tarjeta
7
+ [rutaASeguir]="tarjeta.rutaASeguir"
8
+ [titulo]="tarjeta.titulo"
9
+ [descripcion]="tarjeta.descripcion"
10
+ [icono]="tarjeta.icono"
11
+ [ColorDeBorde]="tarjeta.ColorDeBorde ? tarjeta.ColorDeBorde : ''">
12
+ </app-tarjeta>
13
+ }
14
+ @case ('multiple') {
15
+ <app-tarjeta-multiple
16
+ [titulo]="tarjeta.titulo"
17
+ [descripcion]="tarjeta.descripcion"
18
+ [rutas]="tarjeta.rutas"
19
+ [ColorDeBorde]="tarjeta.ColorDeBorde ? tarjeta.ColorDeBorde : ''">
20
+ </app-tarjeta-multiple>
21
+ }
22
+ @case ('report') {
23
+ <app-tarjeta-reporte
24
+ [reporteAGenerar]="tarjeta.reporteAGenerar"
25
+ [titulo]="tarjeta.titulo"
26
+ [descripcion]="tarjeta.descripcion"
27
+ [icono]="tarjeta.icono"
28
+ [ColorDeBorde]="tarjeta.ColorDeBorde ? tarjeta.ColorDeBorde : ''"
29
+ (GenerarReporte)="GenerarReporte($event)">
30
+ </app-tarjeta-reporte>
31
+ }
32
+ }
33
+ </div>
16
34
  }
17
35
  </div>
@@ -1,36 +1,283 @@
1
- import { Component } from "@angular/core";
1
+ import { Component, OnInit } from "@angular/core";
2
+ import { CommonModule } from "@angular/common";
2
3
  import { TarjetaComponent } from "../../Componentes/Nucleo/tarjeta/tarjeta.component";
3
- // import { HttpClient } from "@angular/common/http";
4
- // import { DatosGlobalesService } from '../../datos-globales.service'
4
+ import { TarjetaMultipleComponent } from "../../Componentes/Nucleo/tarjeta-multiple/tarjeta-multiple.component";
5
+ import { TarjetaReporteComponent } from "../../Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component";
6
+ import { HttpClient } from "@angular/common/http";
7
+ import { DatosGlobalesService } from '../../datos-globales.service';
8
+ import { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
9
+
10
+ interface BaseTarjetaConfig {
11
+ type: 'single' | 'multiple' | 'report';
12
+ position: number;
13
+ titulo: string;
14
+ descripcion: string;
15
+ acceso?: boolean;
16
+ ColorDeBorde?: string;
17
+ }
18
+
19
+ interface TarjetaConfig extends BaseTarjetaConfig {
20
+ type: 'single';
21
+ rutaASeguir: string;
22
+ icono: string;
23
+ }
24
+
25
+ interface RutaMenuItem {
26
+ nombre: string;
27
+ ruta: string;
28
+ }
29
+
30
+ interface TarjetaMultipleConfig extends BaseTarjetaConfig {
31
+ type: 'multiple';
32
+ rutas: RutaMenuItem[];
33
+ }
34
+
35
+ interface TarjetaReporteConfig extends BaseTarjetaConfig {
36
+ type: 'report';
37
+ reporteAGenerar: string;
38
+ icono: string;
39
+ }
40
+
41
+ type AnyTarjetaConfig = TarjetaConfig | TarjetaMultipleConfig | TarjetaReporteConfig;
5
42
 
6
43
  @Component({
7
44
  selector: "app-contenedor-principal",
8
- imports: [TarjetaComponent],
45
+ standalone: true,
46
+ imports: [TarjetaComponent, TarjetaMultipleComponent, TarjetaReporteComponent, CommonModule, DragDropModule],
9
47
  templateUrl: "./contenedor-principal.component.html",
10
48
  styleUrl: "./contenedor-principal.component.css"
11
49
  })
12
- export class ContenedorPrincipalComponent {
50
+
51
+ export class ContenedorPrincipalComponent implements OnInit {
13
52
  public cantidad: number = 0;
14
53
  public cantidadMaxima: number = 0;
54
+ public tarjetas: AnyTarjetaConfig[] = [];
15
55
 
16
- constructor(/*private http: HttpClient, private datosGlobalesService: DatosGlobalesService */) {
56
+ constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService) {
17
57
  this.cantidad = 0;
18
58
  this.cantidadMaxima = 0;
19
59
  }
20
60
 
21
- ngOnInit() {
22
- // this.http.get(`${this.datosGlobalesService.ObtenerURL()}licencias/consolidadoDeLaJefatura`).subscribe({
23
- // next: (datos: any) => {
24
- // if (datos.error) {
25
- // console.error('Problemas al obtener los datos:', datos.error);
26
- // return;
27
- // }
28
- // this.cantidad = datos.body['cantidad'];
29
- // this.cantidadMaxima = datos.body['cantidadMaxima'];
30
- // },
31
- // error: (error) => {
32
- // console.error('Problemas al intentar obtener los datos: ', error);
33
- // },
34
- // });
61
+ async ngOnInit() {
62
+ // 1. Define base cards
63
+ let baseTarjetas: AnyTarjetaConfig[] = [
64
+ {
65
+ type: 'single',
66
+ position: 10,
67
+ rutaASeguir: 'FRM-ModulosV2',
68
+ titulo: 'Módulos',
69
+ descripcion: 'Módulo para el mantenimiento de los módulosV2',
70
+ icono: 'dashboard_2',
71
+ ColorDeBorde: '#0B4FCE'
72
+ },
73
+ {
74
+ type: 'single',
75
+ position: 20,
76
+ rutaASeguir: 'tablaTareasProgramadas',
77
+ titulo: 'Tareas programadas',
78
+ descripcion: 'Módulo para el mantenimiento de las tareas programadas',
79
+ icono: 'grading'
80
+ },
81
+ {
82
+ type: 'multiple',
83
+ position: 30,
84
+ titulo: 'Permisos',
85
+ descripcion: 'Módulo para el mantenimiento de los permisos',
86
+ rutas: [
87
+ { nombre: 'Permisos', ruta: 'tablaPermisos' },
88
+ { nombre: 'Permisos de personas', ruta: 'tablaPermisosDePersonas' },
89
+ { nombre: 'Permisos padres de personas', ruta: 'tablaPermisosDePersonasPadres' }
90
+ ]
91
+ },
92
+ {
93
+ type: 'multiple',
94
+ position: 40,
95
+ titulo: 'Permisos extra',
96
+ descripcion: 'Módulo para el mantenimiento de los permisos extra',
97
+ rutas: [
98
+ { nombre: 'Permisos extra', ruta: 'tablaPermisosExtra' },
99
+ { nombre: 'Permisos extra de personas', ruta: 'tablaPermisosExtraDePersonas' },
100
+ ]
101
+ },
102
+ {
103
+ type: 'single',
104
+ position: 50,
105
+ rutaASeguir: 'tablaMensajesInstitucionales',
106
+ titulo: 'Mensajes institucionales',
107
+ descripcion: 'Módulo para el mantenimiento de mensajes institucionales',
108
+ icono: 'mark_unread_chat_alt'
109
+ },
110
+ {
111
+ type: 'single',
112
+ position: 60,
113
+ rutaASeguir: 'tablaMensajesModulares',
114
+ titulo: 'Mensajes modulares',
115
+ descripcion: 'Módulo para el mantenimiento de mensajes modulares',
116
+ icono: 'mark_unread_chat_alt'
117
+ },
118
+ {
119
+ type: 'single',
120
+ position: 70,
121
+ rutaASeguir: 'tablaPeriodos',
122
+ titulo: 'Periodos',
123
+ descripcion: 'Módulo para el mantenimiento de periodos',
124
+ icono: 'edit_calendar'
125
+ },
126
+ {
127
+ type: 'single',
128
+ position: 80,
129
+ rutaASeguir: 'tablaSedes',
130
+ titulo: 'Sedes y recintos',
131
+ descripcion: 'Módulo para el mantenimiento de sedes y recintos',
132
+ icono: 'home_work'
133
+ },
134
+ {
135
+ type: 'single',
136
+ position: 90,
137
+ rutaASeguir: 'tablaFlujosAprobacion',
138
+ titulo: 'Flujos de aprobación',
139
+ descripcion: 'Módulo para el mantenimiento de flujos de aprobación, pasos y movimientos',
140
+ icono: 'library_add_check'
141
+ },
142
+ {
143
+ type: 'single',
144
+ position: 100,
145
+ rutaASeguir: 'tablaLocalidades',
146
+ titulo: 'Localidades',
147
+ descripcion: 'Módulo para la revisión de las diferentes localidades',
148
+ icono: 'location_on'
149
+ },
150
+ {
151
+ type: 'multiple',
152
+ position: 110,
153
+ titulo: 'Repositorios',
154
+ descripcion: 'Módulo para el mantenimiento de repositorios, variables del sistema y tareas programadas',
155
+ rutas: [
156
+ { nombre: 'Consentimientos informados', ruta: 'tablaConsentimientosInformados' },
157
+ { nombre: 'Repositorios', ruta: 'tablaRepositorios' },
158
+ { nombre: 'Accesos a repositorios', ruta: 'tablaRepositoriosAccesos' },
159
+ { nombre: 'Variables del sistema', ruta: 'tablaVariablesDeSistema' },
160
+ ]
161
+ },
162
+ {
163
+ type: 'single',
164
+ position: 120,
165
+ rutaASeguir: 'gestionGrafico',
166
+ titulo: 'Gráfico de solicitudes mensuales',
167
+ descripcion: 'Módulo para la visualización del gráfico de solicitudes por el mes actual',
168
+ icono: 'leaderboard'
169
+ },
170
+ {
171
+ type: 'single',
172
+ position: 130,
173
+ rutaASeguir: 'gestionGraficoSesiones',
174
+ titulo: 'Gráfico de las sesiones',
175
+ descripcion: 'Módulo para la visualización del gráfico de sesiones',
176
+ icono: 'pie_chart'
177
+ },
178
+ {
179
+ type: 'single',
180
+ position: 140,
181
+ rutaASeguir: 'tablaAdministradores',
182
+ titulo: 'Administradores',
183
+ descripcion: 'Módulo para el mantenimiento de administradores',
184
+ icono: 'supervisor_account'
185
+ }
186
+ ];
187
+
188
+ // 2. Check for extra permissions (Personas card)
189
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}Personas/PermisoExtra`).subscribe({
190
+ next: (datos: any) => {
191
+ if (datos.body) {
192
+ const personasCard: TarjetaMultipleConfig = {
193
+ type: 'multiple',
194
+ position: 25,
195
+ titulo: 'Personas',
196
+ descripcion: 'Módulo para el mantenimiento de los datos relacionados con personas',
197
+ rutas: [
198
+ { nombre: 'Personas', ruta: 'tablaPersonas' },
199
+ { nombre: 'Correos', ruta: 'tablaCorreosPersonas' },
200
+ { nombre: 'Cuentas bancarias', ruta: 'tablaCuentasPersonas' },
201
+ { nombre: 'Teléfonos', ruta: 'tablaTelefonosPersonas' },
202
+ { nombre: 'Ubicación', ruta: 'tablaUbicacionPersona' },
203
+ ]
204
+ };
205
+ baseTarjetas.push(personasCard);
206
+ }
207
+
208
+ // 3. Load user-specific positions from backend
209
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}ConfiguracionDeTarjetas/obtener`).subscribe({
210
+ next: (res: any) => {
211
+ if (res.body && res.body.length > 0) {
212
+ const savedConfigs = res.body;
213
+ baseTarjetas.forEach(tarjeta => {
214
+ const config = savedConfigs.find((c: any) => c.Titulo === tarjeta.titulo);
215
+ if (config) {
216
+ tarjeta.position = config.Posicion;
217
+ if (config.ColorDeBorde) {
218
+ tarjeta.ColorDeBorde = config.ColorDeBorde;
219
+ }
220
+ }
221
+ });
222
+ }
223
+ this.tarjetas = baseTarjetas.sort((a, b) => a.position - b.position);
224
+ },
225
+ error: (error) => {
226
+ console.error('Error al obtener configuración de tarjetas:', error);
227
+ this.tarjetas = baseTarjetas.sort((a, b) => a.position - b.position);
228
+ }
229
+ });
230
+ },
231
+ error: (error) => {
232
+ console.error('Problemas al intentar obtener los datos: ', error);
233
+ this.tarjetas = baseTarjetas.sort((a, b) => a.position - b.position);
234
+ },
235
+ });
236
+ }
237
+
238
+ drop(event: CdkDragDrop<AnyTarjetaConfig[]>) {
239
+ moveItemInArray(this.tarjetas, event.previousIndex, event.currentIndex);
240
+ // Update positions based on new order
241
+ this.tarjetas.forEach((tarjeta, index) => {
242
+ tarjeta.position = (index + 1) * 10;
243
+ });
244
+ this.persistirConfiguracion();
245
+ }
246
+
247
+ persistirConfiguracion() {
248
+ const Configuraciones = this.tarjetas.map(t => ({
249
+ Titulo: t.titulo,
250
+ Posicion: t.position,
251
+ ColorDeBorde: t.ColorDeBorde
252
+ }));
253
+
254
+ this.http.post(`${this.datosGlobalesService.ObtenerURL()}ConfiguracionDeTarjetas/actualizar`, {
255
+ Configuraciones
256
+ }).subscribe({
257
+ error: (error) => {
258
+ console.error('Error al persistir la configuración de tarjetas:', error);
259
+ }
260
+ });
261
+ }
262
+
263
+ GenerarReporte(event: { reporte: string }) {
264
+ if (event.reporte === 'ElReporte1') {
265
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/DatosParaReporteCSV`, {
266
+ responseType: 'text'
267
+ }).subscribe({
268
+ next: (datos: any) => {
269
+ const blob = new Blob([datos], { type: 'text/csv' });
270
+ const url = window.URL.createObjectURL(blob);
271
+ const a = document.createElement('a');
272
+ a.href = url;
273
+ a.download = 'ReporteDePrueba.csv';
274
+ a.click();
275
+ window.URL.revokeObjectURL(url);
276
+ },
277
+ error: (error) => {
278
+ console.error('Problemas al intentar obtener los datos para el reporte: ', error);
279
+ }
280
+ });
281
+ }
35
282
  }
36
283
  }
@@ -18,10 +18,8 @@ export const appConfig: ApplicationConfig = {
18
18
  provideRouter(routes),
19
19
  provideClientHydration(withEventReplay()),
20
20
  provideAnimationsAsync(),
21
- provideHttpClient(withFetch()),
22
- provideClientHydration(),
21
+ provideHttpClient(withFetch(), withInterceptors([AuthInterceptor])),
23
22
  provideCharts(withDefaultRegisterables()),
24
- ...AnalyticsModule.forRoot().providers!,
25
- provideHttpClient(withInterceptors([AuthInterceptor]))
23
+ ...AnalyticsModule.forRoot().providers!
26
24
  ]
27
25
  };