utn-cli 2.1.2 → 2.1.4

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.
@@ -42,7 +42,7 @@ export async function initFrontend() {
42
42
 
43
43
  export async function updateFrontend(opciones = { cerrarAlFinalizar: true }) {
44
44
  console.log('Actualizando el proyecto de frontend...');
45
- const archivosAExcluir = ['app.routes.ts', 'contenedor-principal.component.ts', '.vscode', 'dist'];
45
+ const archivosAExcluir = ['app.routes.ts', 'contenedor-principal.component.ts', '.vscode', 'dist', 'Manual.md'];
46
46
  const directoriodePlantillas = path.join(__dirname, '../templates/frontend');
47
47
  const directorioDestino = process.cwd();
48
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -209,6 +209,24 @@ Router.get('/obtenerDetalleDelModulo', async (solicitud, respuesta, next) => {
209
209
  }
210
210
  });
211
211
 
212
+ Router.get('/VistaDelManual', async (solicitud, respuesta, next) => {
213
+ try {
214
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
215
+ try {
216
+ await Miscelaneo.registrarVistaDelManual(solicitud.headers.authorization);
217
+ return respuesta.json({ body: undefined, error: undefined });
218
+ } catch (error) {
219
+ const MensajeDeError = 'No fue posible registrar la vista del manual';
220
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
221
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
222
+ }
223
+ }
224
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
225
+ } catch (error) {
226
+ next(error);
227
+ }
228
+ });
229
+
212
230
  Router.get('/obtenerDatosDelUsuario', async (solicitud, respuesta, next) => {
213
231
  try {
214
232
  if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
@@ -880,6 +880,14 @@ class Miscelaneo {
880
880
  });
881
881
  }
882
882
 
883
+ async registrarVistaDelManual(Token) {
884
+ const { uid } = await this.obtenerDatosDelUsuario(Token);
885
+ await ejecutarConsulta(
886
+ "INSERT INTO `DatosMiscelaneos` (`DatoMiscelaneo`, `Datos`, `LastUser`) VALUES ('VistasDelManual', JSON_OBJECT('Total', 1), ?) ON DUPLICATE KEY UPDATE `Datos` = JSON_SET(`Datos`, '$.Total', CAST(JSON_VALUE(`Datos`, '$.Total') AS UNSIGNED) + 1), `LastUser` = ?",
887
+ [uid, uid]
888
+ );
889
+ }
890
+
883
891
  async obtenerMensajesModulares() {
884
892
  return await ejecutarConsultaSIGU("SELECT `MensajeModularId`, `Titulo`, `Texto`, `FechaYHoraDeInicio`, `FechaYHoraDeFinalizacion` FROM `SIGU`.`SIGU_MensajesModulares` WHERE NOW(4) BETWEEN `FechaYHoraDeInicio` AND `FechaYHoraDeFinalizacion` AND `Modulo` = ?"
885
893
  , [this.NombreCanonicoDelModulo]);
@@ -1920,10 +1928,10 @@ class Miscelaneo {
1920
1928
  const informacionDelArchivo = await this.almacenarArchivoEnDisco(Solicitud, Resultado['uid']);
1921
1929
  const Respuesta = await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_Adjuntos` (`AdjuntosId`, `Identificador`, `Modulo`, `Seccion`, `Nombre`,\
1922
1930
  `NombreOriginal`, `Ruta`, `Tipo`, `Tamanio`, `Etiqueta`, `LastUpdate`, `LastUser`)\
1923
- VALUES (NULL, ?, ?, 'No aplica', ?, ?, ?, ?, ?, 'No aplica', NOW(4), ?)"
1931
+ VALUES (NULL, ?, ?, 'No aplica', ?, ?, ?, ?, ?, ?, NOW(4), ?)"
1924
1932
  , [Resultado['uid'], this.NombreCanonicoDelModulo, informacionDelArchivo.nombreDeArchivo, informacionDelArchivo.nombreDeArchivo
1925
1933
  , informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.tipoDeContenido, informacionDelArchivo.tamanioTotal
1926
- , Resultado['uid']]);
1934
+ , Etiquetas, Resultado['uid']]);
1927
1935
  informacionDelArchivo.insertId = Respuesta.insertId;
1928
1936
  informacionDelArchivo.Etiquetas = Etiquetas;
1929
1937
  await ejecutarConsulta("INSERT INTO `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
@@ -2508,6 +2516,8 @@ class Miscelaneo {
2508
2516
  return {
2509
2517
  TienePermiso: true,
2510
2518
  ...configuracion,
2519
+ Descripcion: detalleDelModulo[0]?.Descripcion ?? configuracion.Descripcion,
2520
+ Detalle: detalleDelModulo[0]?.Detalle ?? configuracion.Detalle,
2511
2521
  EnlaceDelManual: detalleDelModulo[0]?.EnlaceDelManual ?? '-',
2512
2522
  EnlaceDelVideo: detalleDelModulo[0]?.EnlaceDelVideo ?? '-',
2513
2523
  Notificaciones: notificaciones,
@@ -661,7 +661,7 @@ class ReportePDF {
661
661
 
662
662
  // Función auxiliar para aplicar el formato y escribir el buffer
663
663
  const flushText = (token, isLast) => {
664
- if (token === "") return;
664
+ if (!token) return;
665
665
 
666
666
  let fontName = this.config.fuenteBase;
667
667
  if (isBold && isItalic) fontName = this.config.fuenteNegritaCursiva;
@@ -9,6 +9,111 @@ class Servicio1 {
9
9
 
10
10
  RegistrarElServicio() {
11
11
  const NombreDelServicio = 'Servicio1';
12
+
13
+ // El segundo parámetro de RegistrarElServicio define la tarjeta (o tarjetas) que
14
+ // aparecerán en el dashboard del módulo. Puede ser un objeto o un arreglo de objetos.
15
+ // Si se omite, el servicio se registra sin crear ninguna tarjeta.
16
+ //
17
+ // Campos comunes a todos los tipos:
18
+ // 'Tipo' — 'Única' | 'Múltiple' | 'Reporte' | 'Personalizada'
19
+ // 'Posición' — Número entero; menor = aparece antes. Use múltiplos de 10.
20
+ // 'Título' — Texto visible en la tarjeta del dashboard.
21
+ // 'Descripción' — Texto secundario de la tarjeta.
22
+ // 'RequierePermisoExtra' — true: solo visible para usuarios con permiso extra.
23
+ // 'Activa' — false: la tarjeta existe pero no se muestra.
24
+ //
25
+ // ── Tipo 'Única' ────────────────────────────────────────────────────────────────
26
+ // Tarjeta de acceso directo a una sola ruta.
27
+ //
28
+ // Miscelaneas.RegistrarElServicio(NombreDelServicio, {
29
+ // 'Tipo': 'Única',
30
+ // 'Posición': 10,
31
+ // 'Título': 'Mi sección',
32
+ // 'Descripción': 'Descripción breve de la sección',
33
+ // 'Ícono': 'settings', // Nombre de ícono de Material Icons
34
+ // 'RutaASeguir': 'miRuta', // Ruta relativa sin barra inicial
35
+ // 'ColorDeBorde': '#3498db', // Opcional; omitir para color predeterminado
36
+ // 'RequierePermisoExtra': false,
37
+ // 'Activa': true
38
+ // });
39
+ //
40
+ // ── Tipo 'Múltiple' ──────────────────────────────────────────────────────────────
41
+ // Tarjeta de menú desplegable con varias opciones de navegación.
42
+ //
43
+ // Miscelaneas.RegistrarElServicio(NombreDelServicio, {
44
+ // 'Tipo': 'Múltiple',
45
+ // 'Posición': 20,
46
+ // 'Título': 'Mi sección',
47
+ // 'Descripción': 'Descripción breve de la sección',
48
+ // 'Rutas': [
49
+ // { 'Nombre': 'Opción A', 'Ruta': 'tablaOpcionA' },
50
+ // { 'Nombre': 'Opción B', 'Ruta': 'tablaOpcionB' },
51
+ // { 'Nombre': 'Opción C', 'Ruta': 'tablaOpcionC' }
52
+ // ],
53
+ // 'RequierePermisoExtra': false,
54
+ // 'Activa': true
55
+ // });
56
+ //
57
+ // ── Tipo 'Reporte' ───────────────────────────────────────────────────────────────
58
+ // Tarjeta que, al hacer clic, genera y descarga un reporte.
59
+ // El valor de 'ReporteAGenerar' debe coincidir con el identificador manejado
60
+ // en el método GenerarReporte() del componente ContenedorPrincipalComponent.
61
+ //
62
+ // Miscelaneas.RegistrarElServicio(NombreDelServicio, {
63
+ // 'Tipo': 'Reporte',
64
+ // 'Posición': 30,
65
+ // 'Título': 'Mi reporte',
66
+ // 'Descripción': 'Descarga el reporte de ejemplo en formato CSV',
67
+ // 'Ícono': 'download',
68
+ // 'ReporteAGenerar': 'ElReporte1', // Identificador del reporte en el frontend
69
+ // 'RequierePermisoExtra': false,
70
+ // 'Activa': true
71
+ // });
72
+ //
73
+ // ── Tipo 'Personalizada' ─────────────────────────────────────────────────────────
74
+ // Tarjeta que ejecuta una acción programada definida en el frontend.
75
+ // El valor de 'Acción' debe coincidir con el identificador manejado
76
+ // en el método EjecutarAccionPersonalizada() del componente ContenedorPrincipalComponent.
77
+ //
78
+ // Miscelaneas.RegistrarElServicio(NombreDelServicio, {
79
+ // 'Tipo': 'Personalizada',
80
+ // 'Posición': 40,
81
+ // 'Título': 'Mi acción',
82
+ // 'Descripción': 'Ejecuta una acción personalizada',
83
+ // 'Ícono': 'bolt',
84
+ // 'Etiqueta': 'Ejecutar', // Texto del botón dentro de la tarjeta
85
+ // 'Acción': 'MiAccionPersonalizada', // Identificador de la acción en el frontend
86
+ // 'RequierePermisoExtra': false,
87
+ // 'Activa': true
88
+ // });
89
+ //
90
+ // ── Múltiples tarjetas a la vez ──────────────────────────────────────────────────
91
+ // El segundo parámetro también acepta un arreglo para registrar varias tarjetas
92
+ // en una sola llamada (útil cuando un servicio expone más de una sección).
93
+ //
94
+ // Miscelaneas.RegistrarElServicio(NombreDelServicio, [
95
+ // {
96
+ // 'Tipo': 'Única',
97
+ // 'Posición': 10,
98
+ // 'Título': 'Sección A',
99
+ // 'Descripción': 'Descripción de la sección A',
100
+ // 'Ícono': 'dashboard',
101
+ // 'RutaASeguir': 'tablaSeccionA',
102
+ // 'RequierePermisoExtra': false,
103
+ // 'Activa': true
104
+ // },
105
+ // {
106
+ // 'Tipo': 'Única',
107
+ // 'Posición': 20,
108
+ // 'Título': 'Sección B',
109
+ // 'Descripción': 'Descripción de la sección B',
110
+ // 'Ícono': 'bar_chart',
111
+ // 'RutaASeguir': 'tablaSeccionB',
112
+ // 'RequierePermisoExtra': false,
113
+ // 'Activa': true
114
+ // }
115
+ // ]);
116
+
12
117
  Miscelaneas.RegistrarElServicio(NombreDelServicio);
13
118
  console.log("Se ha creado el servicio: " + NombreDelServicio);
14
119
  }
@@ -39,8 +144,8 @@ class Servicio1 {
39
144
  // informacionDelArchivo.Etiquetas = Etiquetas;
40
145
  // await ejecutarConsulta("INSERT INTO `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
41
146
  // VALUES (?, ?, ?, ?, ?, NOW(4), ?)"
42
- // , [Respuesta.insertId, Resultado.uid, informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.nombreDeArchivo
43
- // , Etiquetas, Resultado.uid]);
147
+ // , [Respuesta.insertId, Resultado.Identificador, informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.nombreDeArchivo
148
+ // , Etiquetas, Resultado.Identificador]);
44
149
  // return informacionDelArchivo;
45
150
  // }
46
151
  // return;
@@ -50,7 +155,7 @@ class Servicio1 {
50
155
  // return await ejecutarConsulta("SELECT `BeneficioEstudiantilId`, `Nombre`, `Tipo`, `Estado` FROM `vve-bybe`.`BYBE_BeneficiosEstudiantiles`");
51
156
  // }
52
157
 
53
- // // Ejemplo de listar con paginador
158
+ // // Ejemplo de listar con paginador
54
159
  // async listar(Parametros) {
55
160
  // let Resultados = undefined;
56
161
  // let ORDERBY = "";
@@ -109,7 +214,7 @@ class Servicio1 {
109
214
  // async agergar(Datos) {
110
215
  // return ejecutarConsulta("INSERT INTO `vve-bybe`.`BYBE_BeneficiosEstudiantiles` VALUES (NULL,\
111
216
  // ?, ?, 'Activo', NOW(4), ?)"
112
- // , [Datos.Nombre, Datos.Tipo, Resultado.uid]);
217
+ // , [Datos.Nombre, Datos.Tipo, Resultado.Identificador]);
113
218
  // }
114
219
 
115
220
  // async borrar(Datos) {
@@ -122,7 +227,7 @@ class Servicio1 {
122
227
  // SET `Nombre` = ?, `Tipo` = ?, `Estado` = ?\
123
228
  // , `LastUpdate` = NOW(4), `LastUser` = ?\
124
229
  // WHERE `BeneficioEstudiantilId` = ?"
125
- // , [Datos.Nombre, Datos.Tipo, Datos.Estado, Resultado.uid, Datos.BeneficioEstudiantilId]);
230
+ // , [Datos.Nombre, Datos.Tipo, Datos.Estado, Resultado.Identificador, Datos.BeneficioEstudiantilId]);
126
231
  // }
127
232
 
128
233
  // async detalle(BeneficioEstudiantilId) {
@@ -37,3 +37,11 @@ CREATE OR REPLACE TABLE `NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS`.`Configuracion
37
37
  `LastUser` VARCHAR(1000) NOT NULL DEFAULT '-' COMMENT 'Último usuario que modificó la fila',
38
38
  PRIMARY KEY (`Identificador`, `Titulo`)
39
39
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_spanish_ci COMMENT = 'Almacena la configuración de las tarjetas del contenedor principal por usuario';
40
+
41
+ CREATE OR REPLACE TABLE `NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS`.`DatosMiscelaneos` (
42
+ `DatoMiscelaneo` VARCHAR(200) NOT NULL COMMENT 'Nombre del datos misceláneo que se desea almacenar',
43
+ `Datos` JSON NOT NULL COMMENT 'Configuración completa de la tarjeta: Tipo, Posición, Título, Descripción, Ícono, RutaASeguir, Rutas, ColorDeBorde, RequierePermisoExtra, Activo',
44
+ `LastUpdate` DATETIME(4) NOT NULL DEFAULT CURRENT_TIMESTAMP(4) ON UPDATE CURRENT_TIMESTAMP(4) COMMENT 'Fecha de la última actualización de la fila',
45
+ `LastUser` VARCHAR(1000) NOT NULL DEFAULT '-' COMMENT 'Último usuario que modificó la fila',
46
+ PRIMARY KEY (`DatoMiscelaneo`)
47
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_spanish_ci COMMENT = 'Datos misceláneos sobre el módulo';
@@ -2,6 +2,7 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/co
2
2
  import { HttpClient } from '@angular/common/http';
3
3
  import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
4
4
  import { CommonModule } from '@angular/common';
5
+ import { DatosGlobalesService } from '../../../datos-globales.service';
5
6
  import { marked } from 'marked';
6
7
  import { Subject } from 'rxjs';
7
8
  import { takeUntil } from 'rxjs/operators';
@@ -29,9 +30,11 @@ export class ManualComponent implements OnInit, OnDestroy {
29
30
  private _destroy$ = new Subject<void>();
30
31
  @ViewChild('contenidoManual') contenidoManualRef!: ElementRef;
31
32
 
32
- constructor(private http: HttpClient, private sanitizer: DomSanitizer) {}
33
+ constructor(private http: HttpClient, private sanitizer: DomSanitizer, private datosGlobalesService: DatosGlobalesService) { }
33
34
 
34
35
  ngOnInit(): void {
36
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/VistaDelManual`).pipe(takeUntil(this._destroy$)).subscribe({ error: () => { } });
37
+
35
38
  this.http.get('/Manual.md', { responseType: 'text' }).pipe(takeUntil(this._destroy$)).subscribe({
36
39
  next: (markdown) => {
37
40
  const html = marked.parse(markdown) as string;
@@ -2,6 +2,15 @@
2
2
  .cabecera2 {
3
3
  width: 100%;
4
4
  border-bottom: 1px solid #707070;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ gap: 16px;
9
+ }
10
+
11
+ .cabecera2-info {
12
+ flex: 1;
13
+ min-width: 0;
5
14
  }
6
15
 
7
16
  .cabecera2 p {
@@ -9,6 +18,7 @@
9
18
  font-family: "Roboto";
10
19
  letter-spacing: 0;
11
20
  color: #000;
21
+ margin: 0;
12
22
  }
13
23
 
14
24
  .cabecera2 .titulo2 {
@@ -20,6 +30,68 @@
20
30
  font-size: small;
21
31
  }
22
32
 
33
+ /* Filtro de tarjetas */
34
+ .filtro-tarjetas {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 4px;
38
+ background: #f0f4f8;
39
+ border: 1px solid #c4d0de;
40
+ border-radius: 8px;
41
+ padding: 4px 8px;
42
+ flex-shrink: 0;
43
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
44
+ }
45
+
46
+ .filtro-tarjetas:focus-within {
47
+ border-color: #1976d2;
48
+ box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.15);
49
+ }
50
+
51
+ .filtro-icono {
52
+ font-size: 18px !important;
53
+ width: 18px !important;
54
+ height: 18px !important;
55
+ color: #6b7c8f;
56
+ flex-shrink: 0;
57
+ }
58
+
59
+ .filtro-input {
60
+ border: none;
61
+ background: transparent;
62
+ outline: none;
63
+ font-size: 14px;
64
+ font-family: 'Roboto', sans-serif;
65
+ color: #333;
66
+ width: 180px;
67
+ }
68
+
69
+ .filtro-input::placeholder {
70
+ color: #a0aab4;
71
+ }
72
+
73
+ .filtro-limpiar {
74
+ display: flex;
75
+ align-items: center;
76
+ border: none;
77
+ background: none;
78
+ cursor: pointer;
79
+ padding: 0;
80
+ color: #6b7c8f;
81
+ transition: color 0.15s ease;
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .filtro-limpiar mat-icon {
86
+ font-size: 16px !important;
87
+ width: 16px !important;
88
+ height: 16px !important;
89
+ }
90
+
91
+ .filtro-limpiar:hover {
92
+ color: #c0392b;
93
+ }
94
+
23
95
  /* Botones */
24
96
  .botonesDeNavegacion {
25
97
  padding: 10px;
@@ -66,8 +66,23 @@
66
66
  <div class="contenido">
67
67
  @if(TienePermiso){
68
68
  <div class="cabecera2">
69
- <p class="titulo2">{{ Descripcion }}</p>
70
- <p class="descripcion2">{{ Detalle }}</p>
69
+ <div class="cabecera2-info">
70
+ <p class="titulo2">{{ Descripcion }}</p>
71
+ <p class="descripcion2">{{ Detalle }}</p>
72
+ </div>
73
+ @if (esDashboard) {
74
+ <div class="filtro-tarjetas">
75
+ <mat-icon class="filtro-icono">search</mat-icon>
76
+ <input class="filtro-input" type="text" placeholder="Buscar tarjeta..." [value]="filtro"
77
+ (input)="onFiltroChange($event)" aria-label="Buscar tarjeta por nombre" />
78
+ @if (filtro) {
79
+ <button class="filtro-limpiar" (click)="limpiarFiltro()" title="Limpiar búsqueda"
80
+ aria-label="Limpiar búsqueda">
81
+ <mat-icon>close</mat-icon>
82
+ </button>
83
+ }
84
+ </div>
85
+ }
71
86
  </div>
72
87
  <ng-content><router-outlet /></ng-content>
73
88
  }@else{
@@ -37,6 +37,17 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy {
37
37
  public AnimarUsuariosActivos: boolean = true;
38
38
  private intervaloUsuarios: any;
39
39
 
40
+ get esDashboard(): boolean { return this.router.url === '/'; }
41
+ get filtro(): string { return this.datosGlobalesService.filtroDeTarjetas$.value; }
42
+
43
+ onFiltroChange(event: Event): void {
44
+ this.datosGlobalesService.filtroDeTarjetas$.next((event.target as HTMLInputElement).value);
45
+ }
46
+
47
+ limpiarFiltro(): void {
48
+ this.datosGlobalesService.filtroDeTarjetas$.next('');
49
+ }
50
+
40
51
  constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private location: Location, private dialog: MatDialog, private router: Router, private ngZone: NgZone) {
41
52
  if (datosGlobalesService.ObtenerToken() === '') {
42
53
  datosGlobalesService.RedirigirALogin();
@@ -14,6 +14,17 @@
14
14
  box-sizing: border-box;
15
15
  }
16
16
 
17
+ .tarjeta-opaca {
18
+ opacity: 0.15;
19
+ filter: grayscale(50%);
20
+ transition: opacity 0.25s ease, filter 0.25s ease;
21
+ pointer-events: none;
22
+ }
23
+
24
+ .contenido>div[cdkDrag] {
25
+ transition: opacity 0.25s ease, filter 0.25s ease;
26
+ }
27
+
17
28
  .cdk-drag-preview {
18
29
  box-sizing: border-box;
19
30
  border-radius: 8px;
@@ -1,6 +1,6 @@
1
1
  <div class="contenido" cdkDropList cdkDropListOrientation="mixed" (cdkDropListDropped)="drop($event)">
2
2
  @for (tarjeta of tarjetas; track tarjeta.titulo) {
3
- <div cdkDrag>
3
+ <div cdkDrag [class.tarjeta-opaca]="esDimmed(tarjeta)">
4
4
  @switch (tarjeta.type) {
5
5
  @case ('single') {
6
6
  <app-tarjeta [rutaASeguir]="$any(tarjeta).rutaASeguir" [titulo]="tarjeta.titulo" [descripcion]="tarjeta.descripcion"
@@ -1,5 +1,7 @@
1
- import { Component, OnInit } from "@angular/core";
1
+ import { Component, OnInit, OnDestroy } from "@angular/core";
2
2
  import { CommonModule } from "@angular/common";
3
+ import { Subject } from "rxjs";
4
+ import { takeUntil } from "rxjs/operators";
3
5
  import { TarjetaComponent } from "../../Componentes/Nucleo/tarjeta/tarjeta.component";
4
6
  import { TarjetaMultipleComponent } from "../../Componentes/Nucleo/tarjeta-multiple/tarjeta-multiple.component";
5
7
  import { TarjetaReporteComponent } from "../../Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component";
@@ -58,17 +60,33 @@ type AnyTarjetaConfig = TarjetaConfig | TarjetaMultipleConfig | TarjetaReporteCo
58
60
  styleUrl: "./contenedor-principal.component.css"
59
61
  })
60
62
 
61
- export class ContenedorPrincipalComponent implements OnInit {
63
+ export class ContenedorPrincipalComponent implements OnInit, OnDestroy {
62
64
  public cantidad: number = 0;
63
65
  public cantidadMaxima: number = 0;
64
66
  public tarjetas: AnyTarjetaConfig[] = [];
67
+ public filtro: string = '';
68
+ private destroy$ = new Subject<void>();
65
69
 
66
70
  constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService) {
67
71
  this.cantidad = 0;
68
72
  this.cantidadMaxima = 0;
69
73
  }
70
74
 
75
+ ngOnDestroy(): void {
76
+ this.destroy$.next();
77
+ this.destroy$.complete();
78
+ }
79
+
80
+ esDimmed(tarjeta: AnyTarjetaConfig): boolean {
81
+ if (!this.filtro.trim()) return false;
82
+ return !tarjeta.titulo.toLowerCase().includes(this.filtro.toLowerCase());
83
+ }
84
+
71
85
  ngOnInit() {
86
+ this.datosGlobalesService.filtroDeTarjetas$
87
+ .pipe(takeUntil(this.destroy$))
88
+ .subscribe(f => this.filtro = f);
89
+
72
90
  this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/obtenerTarjetasDelContenedor`).subscribe({
73
91
  next: (datos: any) => {
74
92
  this.tarjetas = (datos.body ?? []).map((d: any) => this.mapearTarjeta(d));
@@ -1,16 +1,19 @@
1
1
  import { Injectable } from '@angular/core';
2
+ import { BehaviorSubject } from 'rxjs';
2
3
 
3
4
  @Injectable({
4
5
  providedIn: 'root'
5
6
  })
6
7
  export class DatosGlobalesService {
7
8
 
9
+ readonly filtroDeTarjetas$ = new BehaviorSubject<string>('');
10
+
8
11
  constructor() { }
9
12
 
10
13
  ObtenerToken() {
11
14
  const baseUrl = this.ObtenerURL();
12
15
  if (baseUrl === 'http://localhost/') {
13
- return 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxMiIsImlhdCI6MTc2MzEzMDc0NSwiZXhwIjoxNzYzMTY2NzQ1fQ.0Ferkg8T4bOGmpCR2RWdKxUwzOJr2xuqNBbX502D7vs';
16
+ return 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxMiIsIklkZW50aWZpY2Fkb3IiOiIxMiIsImlhdCI6MTc3Nzk4OTgwOSwiZXhwIjoxNzc4MDI1ODA5fQ.T1kUm7H4hwbapQ3eFok31GVvezkPitdTlJL96MorozY';
14
17
  }
15
18
  const match = document.cookie.match(/(?:^|;\s*)_siguid=([^;]+)/);
16
19
  let token = match ? decodeURIComponent(match[1]) : '';