utn-cli 2.1.1 → 2.1.2

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 (29) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/commands/commit.js +14 -1
  3. package/package.json +1 -1
  4. package/templates/backend/package.json +3 -3
  5. package/templates/backend/rutas/misc.js +51 -0
  6. package/templates/backend/servicios/Nucleo/Miscelaneas.js +176 -20
  7. package/templates/frontend/package.json +9 -8
  8. package/templates/frontend/public/Manual.md +1 -0
  9. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +11 -3
  10. package/templates/frontend/src/app/Componentes/Nucleo/graficos/graficos.component.ts +2 -4
  11. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.css +318 -0
  12. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +43 -0
  13. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +74 -0
  14. package/templates/frontend/src/app/Componentes/Nucleo/mensajes/mensajes.component.ts +5 -3
  15. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.ts +12 -4
  16. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.ts +12 -3
  17. package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.html +13 -10
  18. package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts +18 -6
  19. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.html +2 -2
  20. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.ts +9 -13
  21. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-personalizada/tarjeta-personalizada.component.ts +7 -9
  22. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.ts +1 -6
  23. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +0 -2
  24. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +34 -57
  25. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.html +3 -3
  26. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.ts +46 -68
  27. package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.html +14 -3
  28. package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.ts +52 -14
  29. package/templates/frontend/src/app/app.routes.ts +4 -0
@@ -0,0 +1,318 @@
1
+ /* ═══════════════════════════════════════════════════════════
2
+ Layout general — rompe el text-align:center del contenedor
3
+ ══════════════════════════════════════════════════════════ */
4
+ :host {
5
+ display: block;
6
+ text-align: left;
7
+ }
8
+
9
+ .manual-layout {
10
+ display: grid;
11
+ grid-template-columns: 260px 1fr;
12
+ gap: 0;
13
+ align-items: start;
14
+ min-height: 100%;
15
+ font-family: 'Roboto', 'Helvetica Neue', sans-serif;
16
+ }
17
+
18
+ /* ═══════════════════════════════════════════════════════════
19
+ Índice lateral
20
+ ══════════════════════════════════════════════════════════ */
21
+ .manual-toc {
22
+ position: sticky;
23
+ top: 0;
24
+ max-height: calc(100vh - 120px);
25
+ overflow-y: auto;
26
+ border-right: 2px solid #e0e8f5;
27
+ padding: 1.25rem 0.75rem 2rem 0;
28
+ background: #f7faff;
29
+ scrollbar-width: thin;
30
+ scrollbar-color: #b0c4de transparent;
31
+ }
32
+
33
+ .toc-titulo {
34
+ font-size: 0.7rem;
35
+ font-weight: 700;
36
+ letter-spacing: 0.1em;
37
+ text-transform: uppercase;
38
+ color: #002f6b;
39
+ padding: 0 0 0.6rem 1rem;
40
+ border-bottom: 1px solid #d0ddf0;
41
+ margin-bottom: 0.6rem;
42
+ }
43
+
44
+ .toc-lista {
45
+ list-style: none;
46
+ padding: 0;
47
+ margin: 0;
48
+ }
49
+
50
+ .toc-lista li {
51
+ margin: 0;
52
+ }
53
+
54
+ .toc-enlace {
55
+ display: block;
56
+ width: 100%;
57
+ background: none;
58
+ border: none;
59
+ cursor: pointer;
60
+ text-align: left;
61
+ font-family: inherit;
62
+ font-size: 0.8rem;
63
+ line-height: 1.4;
64
+ color: #444;
65
+ padding: 0.3rem 0.75rem;
66
+ border-radius: 0 6px 6px 0;
67
+ transition: background 0.15s, color 0.15s;
68
+ white-space: normal;
69
+ word-break: break-word;
70
+ }
71
+
72
+ .toc-enlace:hover {
73
+ background: #dce9ff;
74
+ color: #002f6b;
75
+ }
76
+
77
+ .toc-nivel-1 .toc-enlace {
78
+ font-weight: 700;
79
+ color: #002f6b;
80
+ font-size: 0.82rem;
81
+ padding-left: 1rem;
82
+ }
83
+
84
+ .toc-nivel-2 .toc-enlace {
85
+ font-weight: 600;
86
+ color: #1976d2;
87
+ padding-left: 1.5rem;
88
+ }
89
+
90
+ .toc-nivel-3 .toc-enlace {
91
+ font-weight: 400;
92
+ color: #555;
93
+ padding-left: 2.25rem;
94
+ font-size: 0.78rem;
95
+ }
96
+
97
+ /* ═══════════════════════════════════════════════════════════
98
+ Área principal del artículo
99
+ ══════════════════════════════════════════════════════════ */
100
+ .manual-main {
101
+ padding: 1.5rem 2.5rem 4rem 2rem;
102
+ min-width: 0;
103
+ }
104
+
105
+ /* ═══════════════════════════════════════════════════════════
106
+ Estados: cargando / error
107
+ ══════════════════════════════════════════════════════════ */
108
+ .manual-estado {
109
+ padding: 1.25rem 1.5rem;
110
+ border-radius: 8px;
111
+ background: #f0f4ff;
112
+ color: #002f6b;
113
+ font-size: 0.95rem;
114
+ border-left: 4px solid #1976d2;
115
+ }
116
+
117
+ .manual-error {
118
+ background: #fff0f0;
119
+ color: #b00020;
120
+ border-left-color: #b00020;
121
+ }
122
+
123
+ /* ═══════════════════════════════════════════════════════════
124
+ Tipografía del artículo
125
+ ══════════════════════════════════════════════════════════ */
126
+ .manual-articulo {
127
+ color: #1a1a1a;
128
+ font-size: 0.95rem;
129
+ line-height: 1.8;
130
+ }
131
+
132
+ /* Encabezados */
133
+ .manual-articulo h1 {
134
+ font-size: 1.75rem;
135
+ font-weight: 700;
136
+ color: #002f6b;
137
+ margin: 0 0 1.25rem;
138
+ padding-bottom: 0.5rem;
139
+ border-bottom: 3px solid #1976d2;
140
+ scroll-margin-top: 1rem;
141
+ }
142
+
143
+ .manual-articulo h2 {
144
+ font-size: 1.25rem;
145
+ font-weight: 700;
146
+ color: #002f6b;
147
+ margin: 2.5rem 0 0.75rem;
148
+ padding: 0.4rem 0.9rem;
149
+ background: #e8f0fe;
150
+ border-left: 4px solid #1976d2;
151
+ border-radius: 0 6px 6px 0;
152
+ scroll-margin-top: 1rem;
153
+ }
154
+
155
+ .manual-articulo h3 {
156
+ font-size: 1.05rem;
157
+ font-weight: 600;
158
+ color: #0b4fce;
159
+ margin: 1.75rem 0 0.5rem;
160
+ scroll-margin-top: 1rem;
161
+ }
162
+
163
+ .manual-articulo h4 {
164
+ font-size: 0.95rem;
165
+ font-weight: 600;
166
+ color: #333;
167
+ margin: 1.25rem 0 0.4rem;
168
+ scroll-margin-top: 1rem;
169
+ }
170
+
171
+ /* Párrafos */
172
+ .manual-articulo p {
173
+ margin: 0 0 0.9rem;
174
+ }
175
+
176
+ /* Listas */
177
+ .manual-articulo ul,
178
+ .manual-articulo ol {
179
+ margin: 0.25rem 0 1rem 1.4rem;
180
+ }
181
+
182
+ .manual-articulo li {
183
+ margin-bottom: 0.3rem;
184
+ }
185
+
186
+ .manual-articulo li p {
187
+ margin: 0;
188
+ }
189
+
190
+ /* Código inline */
191
+ .manual-articulo code {
192
+ background: #eef2ff;
193
+ color: #1a237e;
194
+ border-radius: 3px;
195
+ padding: 0.1em 0.45em;
196
+ font-family: 'Consolas', 'Courier New', monospace;
197
+ font-size: 0.87em;
198
+ }
199
+
200
+ /* Bloques de código */
201
+ .manual-articulo pre {
202
+ background: #f3f4f6;
203
+ border: 1px solid #dde1ea;
204
+ border-left: 4px solid #1976d2;
205
+ border-radius: 0 6px 6px 0;
206
+ padding: 1rem 1.25rem;
207
+ overflow-x: auto;
208
+ margin: 0.75rem 0 1.25rem;
209
+ }
210
+
211
+ .manual-articulo pre code {
212
+ background: none;
213
+ padding: 0;
214
+ color: #1a1a1a;
215
+ font-size: 0.88em;
216
+ line-height: 1.65;
217
+ }
218
+
219
+ /* Tablas */
220
+ .manual-articulo table {
221
+ width: 100%;
222
+ border-collapse: collapse;
223
+ margin: 0.75rem 0 1.5rem;
224
+ font-size: 0.9rem;
225
+ border: 1px solid #d0daf0;
226
+ border-radius: 6px;
227
+ overflow: hidden;
228
+ }
229
+
230
+ .manual-articulo thead {
231
+ background: #002f6b;
232
+ color: #fff;
233
+ }
234
+
235
+ .manual-articulo th {
236
+ text-align: left;
237
+ padding: 0.6rem 1rem;
238
+ font-weight: 600;
239
+ font-size: 0.88rem;
240
+ letter-spacing: 0.02em;
241
+ }
242
+
243
+ .manual-articulo td {
244
+ padding: 0.55rem 1rem;
245
+ border-top: 1px solid #e0e8f5;
246
+ vertical-align: top;
247
+ color: #222;
248
+ }
249
+
250
+ .manual-articulo tbody tr:nth-child(even) td {
251
+ background: #f5f8ff;
252
+ }
253
+
254
+ .manual-articulo tbody tr:hover td {
255
+ background: #ebf1ff;
256
+ }
257
+
258
+ /* Separador */
259
+ .manual-articulo hr {
260
+ border: none;
261
+ border-top: 1px solid #dde3f0;
262
+ margin: 2rem 0;
263
+ }
264
+
265
+ /* Citas / notas */
266
+ .manual-articulo blockquote {
267
+ margin: 0.75rem 0 1rem;
268
+ padding: 0.65rem 1rem;
269
+ background: #fffbea;
270
+ border-left: 4px solid #f9a825;
271
+ border-radius: 0 6px 6px 0;
272
+ color: #4a3c00;
273
+ font-size: 0.92rem;
274
+ }
275
+
276
+ .manual-articulo blockquote p {
277
+ margin: 0;
278
+ }
279
+
280
+ /* Énfasis */
281
+ .manual-articulo strong {
282
+ font-weight: 700;
283
+ color: #002f6b;
284
+ }
285
+
286
+ /* Links */
287
+ .manual-articulo a {
288
+ color: #1976d2;
289
+ text-decoration: underline;
290
+ }
291
+
292
+ .manual-articulo a:focus {
293
+ outline: 2px solid #1976d2;
294
+ outline-offset: 2px;
295
+ border-radius: 2px;
296
+ }
297
+
298
+ /* ═══════════════════════════════════════════════════════════
299
+ Responsivo — pantallas pequeñas
300
+ ══════════════════════════════════════════════════════════ */
301
+ @media (max-width: 768px) {
302
+ .manual-layout {
303
+ grid-template-columns: 1fr;
304
+ }
305
+
306
+ .manual-toc {
307
+ position: static;
308
+ max-height: none;
309
+ border-right: none;
310
+ border-bottom: 2px solid #e0e8f5;
311
+ padding: 1rem;
312
+ margin-bottom: 0.5rem;
313
+ }
314
+
315
+ .manual-main {
316
+ padding: 1rem 1rem 3rem;
317
+ }
318
+ }
@@ -0,0 +1,43 @@
1
+ <div class="manual-layout">
2
+
3
+ <!-- ── Índice lateral ─────────────────────────────────── -->
4
+ <aside class="manual-toc" aria-label="Índice del manual">
5
+ <p class="toc-titulo">Contenido</p>
6
+ <nav>
7
+ <ul class="toc-lista">
8
+ @for (entrada of toc; track entrada.id) {
9
+ <li [class]="'toc-nivel-' + entrada.nivel">
10
+ <button class="toc-enlace" (click)="irA(entrada.id)" [title]="entrada.texto">
11
+ {{ entrada.texto }}
12
+ </button>
13
+ </li>
14
+ }
15
+ </ul>
16
+ </nav>
17
+ </aside>
18
+
19
+ <!-- ── Artículo principal ─────────────────────────────── -->
20
+ <main class="manual-main" role="main" aria-label="Manual de usuario">
21
+
22
+ @if (cargando) {
23
+ <div class="manual-estado" role="status" aria-live="polite" aria-busy="true">
24
+ <p>Cargando manual…</p>
25
+ </div>
26
+ }
27
+
28
+ @if (error) {
29
+ <div class="manual-estado manual-error" role="alert">
30
+ <p>
31
+ No se pudo cargar el manual. Verifique que el archivo
32
+ <code>Manual.md</code> esté en la carpeta <code>public/</code>.
33
+ </p>
34
+ </div>
35
+ }
36
+
37
+ @if (!cargando && !error) {
38
+ <article #contenidoManual class="manual-articulo" [innerHTML]="contenido"></article>
39
+ }
40
+
41
+ </main>
42
+
43
+ </div>
@@ -0,0 +1,74 @@
1
+ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
4
+ import { CommonModule } from '@angular/common';
5
+ import { marked } from 'marked';
6
+ import { Subject } from 'rxjs';
7
+ import { takeUntil } from 'rxjs/operators';
8
+
9
+ interface EntradaToc {
10
+ nivel: number;
11
+ texto: string;
12
+ id: string;
13
+ }
14
+
15
+ const DIACRITICOS = new RegExp('[̀-ͯ]', 'g');
16
+
17
+ @Component({
18
+ selector: 'app-manual',
19
+ standalone: true,
20
+ imports: [CommonModule],
21
+ templateUrl: './manual.component.html',
22
+ styleUrl: './manual.component.css'
23
+ })
24
+ export class ManualComponent implements OnInit, OnDestroy {
25
+ public contenido: SafeHtml = '';
26
+ public toc: EntradaToc[] = [];
27
+ public cargando: boolean = true;
28
+ public error: boolean = false;
29
+ private _destroy$ = new Subject<void>();
30
+ @ViewChild('contenidoManual') contenidoManualRef!: ElementRef;
31
+
32
+ constructor(private http: HttpClient, private sanitizer: DomSanitizer) {}
33
+
34
+ ngOnInit(): void {
35
+ this.http.get('/Manual.md', { responseType: 'text' }).pipe(takeUntil(this._destroy$)).subscribe({
36
+ next: (markdown) => {
37
+ const html = marked.parse(markdown) as string;
38
+
39
+ const parser = new DOMParser();
40
+ const doc = parser.parseFromString(html, 'text/html');
41
+
42
+ doc.querySelectorAll('h1, h2, h3').forEach((heading) => {
43
+ const texto = heading.textContent ?? '';
44
+ const id = 'sec-' + texto
45
+ .toLowerCase()
46
+ .normalize('NFD')
47
+ .replace(DIACRITICOS, '')
48
+ .replace(/[^a-z0-9\s-]/g, '')
49
+ .trim()
50
+ .replace(/\s+/g, '-');
51
+ heading.id = id;
52
+ const nivel = parseInt(heading.tagName[1], 10);
53
+ this.toc.push({ nivel, texto, id });
54
+ });
55
+
56
+ this.contenido = this.sanitizer.bypassSecurityTrustHtml(doc.body.innerHTML);
57
+ this.cargando = false;
58
+ },
59
+ error: () => {
60
+ this.error = true;
61
+ this.cargando = false;
62
+ }
63
+ });
64
+ }
65
+
66
+ ngOnDestroy(): void {
67
+ this._destroy$.next();
68
+ this._destroy$.complete();
69
+ }
70
+
71
+ irA(id: string): void {
72
+ this.contenidoManualRef?.nativeElement.querySelector('#' + id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
73
+ }
74
+ }
@@ -1,5 +1,5 @@
1
1
  import { Component, Inject } from '@angular/core';
2
- import { HttpClient, HttpHeaders } from '@angular/common/http';
2
+ import { HttpClient } from '@angular/common/http';
3
3
  import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
4
4
  import { MatIconModule } from '@angular/material/icon';
5
5
  import { MatButtonModule } from '@angular/material/button';
@@ -8,6 +8,8 @@ import { MatDialogContent, MatDialogActions, MatDialogTitle } from '@angular/mat
8
8
  import { CommonModule } from '@angular/common';
9
9
  import { DatosGlobalesService } from '../../../datos-globales.service';
10
10
 
11
+ interface FilaMensaje { llave: string; valor: string; tachado: boolean }
12
+
11
13
  @Component({
12
14
  selector: 'app-mensajes',
13
15
  templateUrl: './mensajes.component.html',
@@ -24,7 +26,7 @@ import { DatosGlobalesService } from '../../../datos-globales.service';
24
26
  ]
25
27
  })
26
28
  export class MensajesComponent {
27
- public arregloDeDatos: { llave: any; valor: any; tachado: any }[] = [];
29
+ public arregloDeDatos: FilaMensaje[] = [];
28
30
 
29
31
  constructor(private http: HttpClient, public dialogRef: MatDialogRef<MensajesComponent>
30
32
  , @Inject(MAT_DIALOG_DATA) public data: any, private datosGlobalesService: DatosGlobalesService) {
@@ -35,7 +37,7 @@ export class MensajesComponent {
35
37
  this.dialogRef.close();
36
38
  }
37
39
 
38
- enviar(item: { llave: any; valor: any; tachado: any }) {
40
+ enviar(item: FilaMensaje) {
39
41
  item.tachado = true;
40
42
  this.http
41
43
  .post(`${this.datosGlobalesService.ObtenerURL()}misc/actualizarNotificacion`, { FechaYHoraDeCreacion: item.llave }).subscribe();
@@ -1,4 +1,4 @@
1
- import { Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
1
+ import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
2
2
  import { FormBuilder, FormGroup, Validators } from '@angular/forms';
3
3
  import { MatFormFieldModule } from '@angular/material/form-field';
4
4
  import { MatInputModule } from '@angular/material/input';
@@ -6,7 +6,9 @@ import { ReactiveFormsModule } from '@angular/forms';
6
6
  import { MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
7
7
  import { MatButtonModule } from '@angular/material/button';
8
8
  import { DatosGlobalesService } from '../../../datos-globales.service';
9
- import { HttpClient, HttpHeaders } from '@angular/common/http';
9
+ import { HttpClient } from '@angular/common/http';
10
+ import { Subject } from 'rxjs';
11
+ import { takeUntil } from 'rxjs/operators';
10
12
 
11
13
  @Component({
12
14
  selector: 'app-reporte-de-incidencias',
@@ -23,10 +25,10 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
23
25
  ]
24
26
  })
25
27
 
26
- export class ReporteDeIncidenciasComponent implements OnInit {
28
+ export class ReporteDeIncidenciasComponent implements OnInit, OnDestroy {
27
29
  formulario!: FormGroup;
28
- archivos: File[] = [];
29
30
  readonly dialogRef = inject(MatDialogRef<ReporteDeIncidenciasComponent>);
31
+ private _destroy$ = new Subject<void>();
30
32
  constructor(private fb: FormBuilder, private datosGlobalesService: DatosGlobalesService, private http: HttpClient) { }
31
33
  @ViewChild('EntradDeArchivo') EntradDeArchivo!: ElementRef<HTMLInputElement>;
32
34
  Archivo: any;
@@ -51,6 +53,7 @@ export class ReporteDeIncidenciasComponent implements OnInit {
51
53
  const Datos = JSON.stringify(this.formulario.value);
52
54
  this.http.post(this.datosGlobalesService.ObtenerURL() + 'misc/reporteDeIncidencia/' + Datos,
53
55
  archivo)
56
+ .pipe(takeUntil(this._destroy$))
54
57
  .subscribe({
55
58
  next: (data: any) => {
56
59
  alert('Mensaje enviado');
@@ -70,6 +73,11 @@ export class ReporteDeIncidenciasComponent implements OnInit {
70
73
  }
71
74
  }
72
75
 
76
+ ngOnDestroy(): void {
77
+ this._destroy$.next();
78
+ this._destroy$.complete();
79
+ }
80
+
73
81
  AbrirGestorDeArchivos() {
74
82
  this.EntradDeArchivo.nativeElement.click();
75
83
  }
@@ -1,4 +1,4 @@
1
- import { Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
1
+ import { Component, OnDestroy, OnInit, inject } from '@angular/core';
2
2
  import { FormBuilder, FormGroup, Validators } from '@angular/forms';
3
3
  import { MatFormFieldModule } from '@angular/material/form-field';
4
4
  import { MatInputModule } from '@angular/material/input';
@@ -6,7 +6,9 @@ import { ReactiveFormsModule } from '@angular/forms';
6
6
  import { MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
7
7
  import { MatButtonModule } from '@angular/material/button';
8
8
  import { DatosGlobalesService } from '../../../datos-globales.service';
9
- import { HttpClient, HttpHeaders } from '@angular/common/http';
9
+ import { HttpClient } from '@angular/common/http';
10
+ import { Subject } from 'rxjs';
11
+ import { takeUntil } from 'rxjs/operators';
10
12
 
11
13
  @Component({
12
14
  selector: 'app-reporte-de-sugerencias',
@@ -23,9 +25,10 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
23
25
  ]
24
26
  })
25
27
 
26
- export class ReporteDeSugerenciasComponent implements OnInit {
28
+ export class ReporteDeSugerenciasComponent implements OnInit, OnDestroy {
27
29
  formulario!: FormGroup;
28
30
  readonly dialogRef = inject(MatDialogRef<ReporteDeSugerenciasComponent>);
31
+ private _destroy$ = new Subject<void>();
29
32
  constructor(private fb: FormBuilder, private datosGlobalesService: DatosGlobalesService, private http: HttpClient) { }
30
33
 
31
34
  ngOnInit(): void {
@@ -41,6 +44,7 @@ export class ReporteDeSugerenciasComponent implements OnInit {
41
44
  Enviar() {
42
45
  const Datos = JSON.stringify(this.formulario.value);
43
46
  this.http.post(this.datosGlobalesService.ObtenerURL() + 'misc/reporteDeSugerencia/' + Datos, {})
47
+ .pipe(takeUntil(this._destroy$))
44
48
  .subscribe({
45
49
  next: (data: any) => {
46
50
  alert('Mensaje enviado');
@@ -51,4 +55,9 @@ export class ReporteDeSugerenciasComponent implements OnInit {
51
55
  })
52
56
  this.Cerrar();
53
57
  }
58
+
59
+ ngOnDestroy(): void {
60
+ this._destroy$.next();
61
+ this._destroy$.complete();
62
+ }
54
63
  }
@@ -1,3 +1,4 @@
1
+ <a #descargaLink style="display:none"></a>
1
2
  <div class="contenedor">
2
3
  <div class="lista">
3
4
  @for(archivo of ListaArchivos;track $index){
@@ -17,20 +18,22 @@
17
18
  }
18
19
  </div>
19
20
  @if(EsEditable) {
20
- <div class="zona-archivo" [class.deshabilitado]="ListaArchivos.length >= CantidadMaximaDeArchivos" (click)="AbrirGestorDeArchivos()" (dragover)="ArrastrarAdentro($event)"
21
- (dragleave)="ArrastrarAfuera($event)" (drop)="Soltar($event)">
21
+ <div class="zona-archivo" [class.deshabilitado]="ListaArchivos.length >= CantidadMaximaDeArchivos"
22
+ (click)="AbrirGestorDeArchivos()" (dragover)="ArrastrarAdentro($event)" (dragleave)="ArrastrarAfuera($event)"
23
+ (drop)="Soltar($event)">
22
24
  @if(ListaArchivos.length >= CantidadMaximaDeArchivos) {
23
- <p class="texto-limite">Límite de {{CantidadMaximaDeArchivos}} archivos alcanzado</p>
25
+ <p class="texto-limite">Límite de {{CantidadMaximaDeArchivos}} archivos alcanzado</p>
24
26
  } @else {
25
- @if(!Archivo){
26
- <p class="texto">Arrastra y suelta un archivo aquí o <span class="texto-clic">haz clic para subir</span></p>
27
- }
28
- @else{
29
- <p>Archivo seleccionado: {{ Archivo.name }}</p>
30
- }
27
+ @if(!Archivo){
28
+ <p class="texto">Arrastra y suelta un archivo aquí o <span class="texto-clic">haz clic para subir</span></p>
29
+ }
30
+ @else{
31
+ <p>Archivo seleccionado: {{ Archivo.name }}</p>
32
+ }
31
33
  }
32
34
  <!-- Input oculto para seleccionar archivos manualmente -->
33
- <input type="file" #EntradaDeArchivo hidden (change)="ArchivoSeleccionado($event)" [accept]="FormatosPermitidos.join(',')">
35
+ <input type="file" #EntradaDeArchivo hidden (change)="ArchivoSeleccionado($event)"
36
+ [accept]="FormatosPermitidos.join(',')">
34
37
  </div>
35
38
  } @else {
36
39
  @if(ListaArchivos.length === 0) {
@@ -1,11 +1,13 @@
1
- import { Component, ElementRef, inject, OnInit, ViewChild, Output, EventEmitter } from '@angular/core';
1
+ import { Component, ElementRef, inject, OnDestroy, OnInit, ViewChild, Output, EventEmitter } from '@angular/core';
2
2
  import { MatButtonModule } from '@angular/material/button';
3
3
  import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
4
- import { HttpClient, HttpHeaders } from '@angular/common/http';
4
+ import { HttpClient } from '@angular/common/http';
5
5
  import { MatIconModule } from '@angular/material/icon';
6
6
  import { DatosGlobalesService } from '../../../datos-globales.service';
7
7
  import { MensajeConfirmacionComponent } from '../mensaje-confirmacion/mensaje-confirmacion';
8
8
  import { MatDialog } from '@angular/material/dialog';
9
+ import { Subject } from 'rxjs';
10
+ import { takeUntil } from 'rxjs/operators';
9
11
 
10
12
  @Component({
11
13
  selector: 'app-subir-archivo',
@@ -13,14 +15,16 @@ import { MatDialog } from '@angular/material/dialog';
13
15
  templateUrl: './subir-archivo.component.html',
14
16
  styleUrl: './subir-archivo.component.css'
15
17
  })
16
- export class SubirArchivoComponent implements OnInit {
18
+ export class SubirArchivoComponent implements OnInit, OnDestroy {
17
19
  readonly dialogRef = inject(MatDialogRef<SubirArchivoComponent>);
18
20
  Archivo: any;
19
21
  ListaArchivos: any[] = []
20
22
  HaCargadoArchivos: boolean = false;
21
23
  CantidadMaximaDeArchivos: number = 99;
24
+ private _destroy$ = new Subject<void>();
22
25
  @Output() HaCargadoArchivosChange = new EventEmitter<boolean>();
23
26
  @ViewChild('EntradaDeArchivo') EntradaDeArchivo!: ElementRef<HTMLInputElement>;
27
+ @ViewChild('descargaLink') descargaLinkRef!: ElementRef<HTMLAnchorElement>;
24
28
  readonly data = inject(MAT_DIALOG_DATA);
25
29
  Permiso = '--Permiso=' + this.data.Permiso;
26
30
  Etiqueta = this.data.Etiqueta + this.Permiso;
@@ -185,6 +189,7 @@ export class SubirArchivoComponent implements OnInit {
185
189
  CargarArchivo(archivo: any, Etiqueta: string) {
186
190
  this.http.post(this.datosGlobalesService.ObtenerURL() + this.RutaParaCargar + Etiqueta,
187
191
  archivo)
192
+ .pipe(takeUntil(this._destroy$))
188
193
  .subscribe({
189
194
  next: (data: any) => {
190
195
  this.Archivo = undefined;
@@ -199,6 +204,7 @@ export class SubirArchivoComponent implements OnInit {
199
204
 
200
205
  ListarArchivos(Etiqueta: string) {
201
206
  this.http.get(this.datosGlobalesService.ObtenerURL() + this.RutaParaListar + Etiqueta)
207
+ .pipe(takeUntil(this._destroy$))
202
208
  .subscribe({
203
209
  next: (data: any) => {
204
210
  this.ListaArchivos = data.body;
@@ -214,19 +220,20 @@ export class SubirArchivoComponent implements OnInit {
214
220
  DescargarArchivo(ArchivoId: string, Nombre: string) {
215
221
  this.http.get(this.datosGlobalesService.ObtenerURL() + this.RutaParaDescargar + ArchivoId + this.Permiso
216
222
  , { responseType: 'blob' })
223
+ .pipe(takeUntil(this._destroy$))
217
224
  .subscribe((pdfBlob) => {
218
225
  const url = window.URL.createObjectURL(pdfBlob);
219
- const a = document.createElement('a');
226
+ const a = this.descargaLinkRef.nativeElement;
220
227
  a.href = url;
221
228
  a.download = Nombre;
222
- document.body.appendChild(a);
223
229
  a.click();
224
- document.body.removeChild(a);
230
+ window.URL.revokeObjectURL(url);
225
231
  })
226
232
  }
227
233
 
228
234
  BorrarArchivo(ArchivoId: string) {
229
235
  this.http.delete(this.datosGlobalesService.ObtenerURL() + 'misc/borrarArchivo/' + ArchivoId)
236
+ .pipe(takeUntil(this._destroy$))
230
237
  .subscribe({
231
238
  next: (data: any) => {
232
239
  this.MetaDatosDelArchivoCargado = null;
@@ -238,6 +245,11 @@ export class SubirArchivoComponent implements OnInit {
238
245
  })
239
246
  }
240
247
 
248
+ ngOnDestroy(): void {
249
+ this._destroy$.next();
250
+ this._destroy$.complete();
251
+ }
252
+
241
253
  Cancelar() {
242
254
  this.dialogRef.close(this.MetaDatosDelArchivoCargado);
243
255
  }