utn-cli 2.1.14 → 2.1.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -7,7 +7,6 @@ button:focus {
7
7
  display: flex;
8
8
  flex-direction: column;
9
9
  padding: 20px;
10
- /* Padding uniforme en todos los lados */
11
10
  background-color: white;
12
11
  overflow: hidden;
13
12
  box-sizing: border-box;
@@ -15,7 +14,6 @@ button:focus {
15
14
 
16
15
  .lista {
17
16
  width: 100%;
18
- /* Ocupa todo el ancho disponible del contenedor */
19
17
  margin-bottom: 15px;
20
18
  max-height: 300px;
21
19
  overflow-y: auto;
@@ -23,10 +21,8 @@ button:focus {
23
21
  display: flex;
24
22
  flex-direction: column;
25
23
  gap: 8px;
26
- /* Espacio uniforme entre archivos */
27
24
  }
28
25
 
29
- /* Estilo de la fila de archivo */
30
26
  .lista .archivo {
31
27
  border: solid 1px #007bff;
32
28
  border-radius: 8px;
@@ -34,22 +30,34 @@ button:focus {
34
30
  justify-content: space-between;
35
31
  align-items: center;
36
32
  padding: 8px 15px;
37
- /* Espacio interno simétrico */
38
33
  box-sizing: border-box;
39
34
  }
40
35
 
41
- .lista .archivo p {
42
- margin: 0;
36
+ .nombre-archivo {
43
37
  flex: 1;
44
- /* El texto toma el espacio restante */
45
- text-align: left;
38
+ min-width: 0;
39
+ display: flex;
40
+ flex-direction: column;
41
+ justify-content: center;
46
42
  padding-right: 10px;
43
+ overflow: hidden;
44
+ }
45
+
46
+ .nombre-texto {
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ font-size: 14px;
51
+ }
52
+
53
+ .fecha-archivo {
54
+ font-size: 11px;
55
+ color: #7f8c8d;
56
+ white-space: nowrap;
47
57
  }
48
58
 
49
- /* Zona de Arrastre */
50
59
  .zona-archivo {
51
60
  width: 100%;
52
- /* Centrado automático */
53
61
  height: 100px;
54
62
  border: 2px dashed #3498db;
55
63
  border-radius: 8px;
@@ -72,13 +80,11 @@ button:focus {
72
80
  font-weight: bold;
73
81
  }
74
82
 
75
- /* Centrado del mensaje vacío */
76
83
  .mensaje-vacio {
77
84
  display: flex;
78
85
  justify-content: center;
79
86
  align-items: center;
80
87
  padding: 40px 0;
81
- /* Margen superior e inferior igual */
82
88
  color: #7f8c8d;
83
89
  width: 100%;
84
90
  }
@@ -91,11 +97,10 @@ button:focus {
91
97
  margin-top: 20px;
92
98
  display: flex;
93
99
  justify-content: flex-end;
94
- /* Alinea botones a la derecha */
95
100
  gap: 10px;
101
+ flex-shrink: 0;
96
102
  }
97
103
 
98
- /* Estilizador de Scrollbar */
99
104
  .lista::-webkit-scrollbar {
100
105
  width: 6px;
101
106
  }
@@ -105,7 +110,6 @@ button:focus {
105
110
  border-radius: 10px;
106
111
  }
107
112
 
108
- /* Iconos */
109
113
  .descargar {
110
114
  color: #007bff;
111
115
  }
@@ -116,4 +120,257 @@ button:focus {
116
120
 
117
121
  .deshabilitado {
118
122
  color: #bdc3c7;
123
+ }
124
+
125
+ .diff-icon {
126
+ color: #8e44ad;
127
+ }
128
+
129
+ /* ── Visor inline ─────────────────────────────────────────────────── */
130
+
131
+ .visor-contenedor {
132
+ display: flex;
133
+ flex-direction: column;
134
+ width: 100%;
135
+ height: 100%;
136
+ background-color: white;
137
+ box-sizing: border-box;
138
+ overflow: hidden;
139
+ }
140
+
141
+ .visor-encabezado {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 8px;
145
+ font-size: 15px;
146
+ font-weight: 600;
147
+ background: #1b3069;
148
+ color: #ffffff;
149
+ padding: 6px 12px 6px 8px;
150
+ flex-shrink: 0;
151
+ }
152
+
153
+ .visor-encabezado button {
154
+ color: #eef2ff;
155
+ }
156
+
157
+ .visor-icono-tipo {
158
+ color: #eef2ff;
159
+ font-size: 20px;
160
+ width: 20px;
161
+ height: 20px;
162
+ flex-shrink: 0;
163
+ }
164
+
165
+ .visor-nombre {
166
+ flex: 1;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .visor-estadisticas {
173
+ display: flex;
174
+ gap: 10px;
175
+ font-size: 13px;
176
+ font-weight: bold;
177
+ flex-shrink: 0;
178
+ }
179
+
180
+ .visor-stat.agregado {
181
+ color: #95d03a;
182
+ }
183
+
184
+ .visor-stat.eliminado {
185
+ color: #ff8a80;
186
+ }
187
+
188
+ .visor-cuerpo {
189
+ padding: 0 !important;
190
+ background: #f5f7fa !important;
191
+ max-height: 70vh !important;
192
+ overflow: auto !important;
193
+ }
194
+
195
+ .visor-cuerpo.sin-padding {
196
+ display: flex !important;
197
+ overflow: hidden !important;
198
+ padding: 0 !important;
199
+ }
200
+
201
+ .visor-estado-carga,
202
+ .visor-estado-error {
203
+ display: flex;
204
+ flex-direction: column;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: 12px;
208
+ padding: 48px;
209
+ color: #7f8c8d;
210
+ }
211
+
212
+ .visor-estado-error mat-icon {
213
+ font-size: 40px;
214
+ color: #e74c3c;
215
+ }
216
+
217
+ /* Diff */
218
+ .visor-codigo {
219
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
220
+ font-size: 13px;
221
+ line-height: 20px;
222
+ min-width: max-content;
223
+ background: #1e2a45;
224
+ }
225
+
226
+ .visor-linea {
227
+ display: flex;
228
+ align-items: stretch;
229
+ min-width: 100%;
230
+ }
231
+
232
+ .visor-linea:hover {
233
+ filter: brightness(1.1);
234
+ }
235
+
236
+ .visor-numero-linea {
237
+ min-width: 48px;
238
+ padding: 0 12px;
239
+ text-align: right;
240
+ color: #7a8cb0;
241
+ background: #162035;
242
+ border-right: 1px solid #2a3f6f;
243
+ user-select: none;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .visor-texto-linea {
248
+ margin: 0;
249
+ padding: 0 12px;
250
+ white-space: pre;
251
+ flex: 1;
252
+ }
253
+
254
+ .visor-linea.meta {
255
+ background: #1e2a45;
256
+ color: #8ea8d0;
257
+ }
258
+
259
+ .visor-linea.archivo {
260
+ background: #1b3069;
261
+ color: #c5d8ff;
262
+ }
263
+
264
+ .visor-linea.bloque {
265
+ background: #162850;
266
+ color: #7ab4ff;
267
+ }
268
+
269
+ .visor-linea.agregado {
270
+ background: #0e3a1e;
271
+ color: #b5f2a0;
272
+ }
273
+
274
+ .visor-linea.eliminado {
275
+ background: #3a1010;
276
+ color: #ffaaaa;
277
+ }
278
+
279
+ .visor-linea.contexto {
280
+ background: #1e2a45;
281
+ color: #d0daf0;
282
+ }
283
+
284
+ /* Texto */
285
+ .visor-texto {
286
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
287
+ font-size: 13px;
288
+ line-height: 1.6;
289
+ background: #1e2a45;
290
+ color: #d0daf0;
291
+ padding: 16px 20px;
292
+ margin: 0;
293
+ white-space: pre-wrap;
294
+ word-break: break-all;
295
+ min-height: 100%;
296
+ }
297
+
298
+ /* CSV */
299
+ .visor-csv {
300
+ overflow: auto;
301
+ background: #fff;
302
+ }
303
+
304
+ .visor-csv table {
305
+ border-collapse: collapse;
306
+ width: 100%;
307
+ font-size: 13px;
308
+ }
309
+
310
+ .visor-csv th {
311
+ background: #1b3069;
312
+ color: #fff;
313
+ padding: 8px 14px;
314
+ border: 1px solid #ccc;
315
+ text-align: left;
316
+ white-space: nowrap;
317
+ }
318
+
319
+ .visor-csv td {
320
+ padding: 6px 14px;
321
+ border: 1px solid #ddd;
322
+ white-space: nowrap;
323
+ }
324
+
325
+ .visor-csv tr:nth-child(even) td {
326
+ background: #f0f4fb;
327
+ }
328
+
329
+ .visor-csv tr:hover td {
330
+ background: #dde6f5;
331
+ }
332
+
333
+ /* Imagen */
334
+ .visor-imagen {
335
+ display: flex;
336
+ justify-content: center;
337
+ align-items: center;
338
+ width: 100%;
339
+ height: 100%;
340
+ background: #eef2ff;
341
+ padding: 16px;
342
+ box-sizing: border-box;
343
+ }
344
+
345
+ .visor-imagen img {
346
+ max-width: 100%;
347
+ max-height: 65vh;
348
+ object-fit: contain;
349
+ border-radius: 4px;
350
+ box-shadow: 0px 3px 10px #00000029;
351
+ }
352
+
353
+ /* PDF */
354
+ .visor-pdf {
355
+ width: 100%;
356
+ height: 65vh;
357
+ border: none;
358
+ display: block;
359
+ }
360
+
361
+ /* Footer del visor */
362
+ .visor-pie {
363
+ margin-top: 0;
364
+ background: #ffffff;
365
+ border-top: 2px solid #eef2ff;
366
+ padding: 8px 16px;
367
+ }
368
+
369
+ .visor-pie button {
370
+ color: #1b3069;
371
+ font-weight: 500;
372
+ }
373
+
374
+ .visor-pie button:hover {
375
+ background: #eef2ff;
119
376
  }
@@ -1,19 +1,27 @@
1
1
  <a #descargaLink style="display:none"></a>
2
+
3
+ @if (!archivoEnVisor) {
2
4
  <div class="contenedor">
3
5
  <div class="lista">
4
6
  @for(archivo of ListaArchivos;track $index){
5
7
  <div class="archivo">
6
- <p style="text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{archivo.Nombre}}
7
- </p>
8
+ <div class="nombre-archivo">
9
+ <span class="nombre-texto">{{ nombreSinFecha(archivo.Nombre) }}</span>
10
+ @if(fechaDelArchivo(archivo.Nombre)) {
11
+ <span class="fecha-archivo">Fecha de carga: {{ fechaDelArchivo(archivo.Nombre) }}</span>
12
+ }
13
+ </div>
14
+ <button mat-icon-button [disabled]="!esPrevisualizable(archivo.Nombre)" (click)="PreVisualizarArchivo(archivo)"
15
+ [matTooltip]="esPrevisualizable(archivo.Nombre) ? 'Vista previa' : 'Vista previa no disponible para este formato'">
16
+ <mat-icon [class.diff-icon]="esPrevisualizable(archivo.Nombre)"
17
+ [class.deshabilitado]="!esPrevisualizable(archivo.Nombre)">visibility</mat-icon>
18
+ </button>
8
19
  <button mat-icon-button (click)="DescargarArchivo(archivo.ArchivoId,archivo.Nombre)" matTooltip="Descargar">
9
- <!--pone deshabilitado si es estado coincide con el indicado -->
10
20
  <mat-icon class="descargar">download</mat-icon>
11
21
  </button>
12
22
  <button mat-icon-button [disabled]="!EsEditable" (click)="BorrarArchivo(archivo.ArchivoId)" matTooltip="Eliminar">
13
- <!--pone deshabilitado si es estado coincide con el indicado -->
14
23
  <mat-icon class="eliminar" [class.deshabilitado]="!EsEditable">delete</mat-icon>
15
24
  </button>
16
-
17
25
  </div>
18
26
  }
19
27
  </div>
@@ -31,7 +39,6 @@
31
39
  <p>Archivo seleccionado: {{ Archivo.name }}</p>
32
40
  }
33
41
  }
34
- <!-- Input oculto para seleccionar archivos manualmente -->
35
42
  <input type="file" #EntradaDeArchivo hidden (change)="ArchivoSeleccionado($event)"
36
43
  [accept]="FormatosPermitidos.join(',')">
37
44
  </div>
@@ -46,4 +53,79 @@
46
53
  <button mat-button (click)="Cancelar()">Cerrar</button>
47
54
  <button mat-button [disabled]="!EsEditable" (click)="SubirArchivo()">Guardar</button>
48
55
  </div>
49
- </div>
56
+ </div>
57
+ } @else {
58
+ <div class="visor-contenedor">
59
+ <div class="visor-encabezado">
60
+ <button mat-icon-button (click)="cerrarVisor()" matTooltip="Volver a la lista">
61
+ <mat-icon>arrow_back</mat-icon>
62
+ </button>
63
+ <mat-icon class="visor-icono-tipo">{{ iconoVisor }}</mat-icon>
64
+ <span class="visor-nombre" [title]="nombreSinFecha(archivoEnVisor.Nombre)">{{ nombreSinFecha(archivoEnVisor.Nombre)
65
+ }}</span>
66
+ @if (archivoEnVisor.tipo === 'diff' && !visorCargando && !visorError) {
67
+ <span class="visor-estadisticas">
68
+ <span class="visor-stat agregado">+{{ estadisticasDiff.agregadas }}</span>
69
+ <span class="visor-stat eliminado">-{{ estadisticasDiff.eliminadas }}</span>
70
+ </span>
71
+ }
72
+ </div>
73
+
74
+ <mat-dialog-content class="visor-cuerpo"
75
+ [class.sin-padding]="archivoEnVisor.tipo === 'imagen' || archivoEnVisor.tipo === 'pdf'">
76
+ @if (visorCargando) {
77
+ <div class="visor-estado-carga">
78
+ <mat-spinner diameter="40"></mat-spinner>
79
+ <p>Cargando archivo...</p>
80
+ </div>
81
+ } @else if (visorError) {
82
+ <div class="visor-estado-error">
83
+ <mat-icon>error_outline</mat-icon>
84
+ <p>No se pudo cargar el archivo.</p>
85
+ </div>
86
+ } @else {
87
+ @switch (archivoEnVisor.tipo) {
88
+ @case ('diff') {
89
+ <div class="visor-codigo">
90
+ @for (linea of visorLineasDiff; track linea.numero) {
91
+ <div class="visor-linea" [class]="linea.tipo">
92
+ <span class="visor-numero-linea">{{ linea.numero }}</span>
93
+ <pre class="visor-texto-linea">{{ linea.contenido }}</pre>
94
+ </div>
95
+ }
96
+ </div>
97
+ }
98
+ @case ('texto') {
99
+ <pre class="visor-texto">{{ visorContenidoTexto }}</pre>
100
+ }
101
+ @case ('csv') {
102
+ <div class="visor-csv">
103
+ <table>
104
+ @for (fila of visorFilasCsv; track $index; let i = $index) {
105
+ <tr>
106
+ @for (celda of fila; track $index) {
107
+ @if (i === 0) { <th>{{ celda }}</th> }
108
+ @else { <td>{{ celda }}</td> }
109
+ }
110
+ </tr>
111
+ }
112
+ </table>
113
+ </div>
114
+ }
115
+ @case ('imagen') {
116
+ <div class="visor-imagen">
117
+ <img [src]="visorImagenUrl" [alt]="nombreSinFecha(archivoEnVisor.Nombre)" />
118
+ </div>
119
+ }
120
+ @case ('pdf') {
121
+ <iframe class="visor-pdf" [src]="visorPdfUrl" [title]="nombreSinFecha(archivoEnVisor.Nombre)"></iframe>
122
+ }
123
+ }
124
+ }
125
+ </mat-dialog-content>
126
+
127
+ <mat-dialog-actions class="visor-pie">
128
+ <button mat-button (click)="cerrarVisor()">Volver</button>
129
+ </mat-dialog-actions>
130
+ </div>
131
+ }
@@ -1,24 +1,35 @@
1
1
  import { Component, ElementRef, inject, OnDestroy, OnInit, ViewChild, Output, EventEmitter } from '@angular/core';
2
2
  import { MatButtonModule } from '@angular/material/button';
3
- import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
3
+ import { MAT_DIALOG_DATA, MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { MatIconModule } from '@angular/material/icon';
6
+ import { MatTooltipModule } from '@angular/material/tooltip';
7
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
8
+ import { DomSanitizer, SafeUrl, SafeResourceUrl } from '@angular/platform-browser';
6
9
  import { DatosGlobalesService } from '../../../datos-globales.service';
7
10
  import { MensajeConfirmacionComponent } from '../mensaje-confirmacion/mensaje-confirmacion';
8
11
  import { MatDialog } from '@angular/material/dialog';
9
12
  import { Subject } from 'rxjs';
10
13
  import { takeUntil } from 'rxjs/operators';
11
14
 
15
+ type TipoDeVista = 'diff' | 'texto' | 'csv' | 'imagen' | 'pdf';
16
+
17
+ interface LineaDiff {
18
+ contenido: string;
19
+ tipo: 'meta' | 'archivo' | 'bloque' | 'agregado' | 'eliminado' | 'contexto';
20
+ numero: number;
21
+ }
22
+
12
23
  @Component({
13
24
  selector: 'app-subir-archivo',
14
- imports: [MatButtonModule, MatIconModule],
25
+ imports: [MatButtonModule, MatIconModule, MatTooltipModule, MatProgressSpinnerModule, MatDialogContent, MatDialogActions],
15
26
  templateUrl: './subir-archivo.component.html',
16
27
  styleUrl: './subir-archivo.component.css'
17
28
  })
18
29
  export class SubirArchivoComponent implements OnInit, OnDestroy {
19
30
  readonly dialogRef = inject(MatDialogRef<SubirArchivoComponent>);
20
31
  Archivo: any;
21
- ListaArchivos: any[] = []
32
+ ListaArchivos: any[] = [];
22
33
  HaCargadoArchivos: boolean = false;
23
34
  CantidadMaximaDeArchivos: number = 99;
24
35
  private _destroy$ = new Subject<void>();
@@ -35,25 +46,32 @@ export class SubirArchivoComponent implements OnInit, OnDestroy {
35
46
  public RutaParaListar: string = 'misc/listarArchivos/';
36
47
  public RutaParaDescargar: string = 'misc/descargarArchivo/';
37
48
  public RutaParaCargar: string = 'misc/cargarArchivo/';
38
- constructor(private dialog: MatDialog, private datosGlobalesService: DatosGlobalesService, private http: HttpClient) { }
49
+
50
+ // Visor inline
51
+ archivoEnVisor: any = null;
52
+ visorCargando = false;
53
+ visorError = false;
54
+ visorContenidoTexto = '';
55
+ visorLineasDiff: LineaDiff[] = [];
56
+ visorFilasCsv: string[][] = [];
57
+ visorImagenUrl: SafeUrl | null = null;
58
+ visorPdfUrl: SafeResourceUrl | null = null;
59
+ private visorObjectUrl = '';
60
+
61
+ constructor(
62
+ private dialog: MatDialog,
63
+ private datosGlobalesService: DatosGlobalesService,
64
+ private http: HttpClient,
65
+ private sanitizer: DomSanitizer
66
+ ) { }
39
67
 
40
68
  ngOnInit(): void {
41
- if (this.data.RutaParaCargar) {
42
- this.RutaParaCargar = this.data.RutaParaCargar;
43
- }
44
- if (this.data.RutaParaListar) {
45
- this.RutaParaListar = this.data.RutaParaListar;
46
- }
47
- if (this.data.RutaParaDescargar) {
48
- this.RutaParaDescargar = this.data.RutaParaDescargar;
49
- }
50
- if (this.data.FormatosPermitidos) {
51
- this.FormatosPermitidos = this.data.FormatosPermitidos;
52
- }
53
- if (this.data.CantidadMaximaDeArchivos) {
54
- this.CantidadMaximaDeArchivos = this.data.CantidadMaximaDeArchivos;
55
- }
56
- this.ListarArchivos(this.Etiqueta)
69
+ if (this.data.RutaParaCargar) this.RutaParaCargar = this.data.RutaParaCargar;
70
+ if (this.data.RutaParaListar) this.RutaParaListar = this.data.RutaParaListar;
71
+ if (this.data.RutaParaDescargar) this.RutaParaDescargar = this.data.RutaParaDescargar;
72
+ if (this.data.FormatosPermitidos) this.FormatosPermitidos = this.data.FormatosPermitidos;
73
+ if (this.data.CantidadMaximaDeArchivos) this.CantidadMaximaDeArchivos = this.data.CantidadMaximaDeArchivos;
74
+ this.ListarArchivos(this.Etiqueta);
57
75
  }
58
76
 
59
77
  ArrastrarAdentro(event: DragEvent) {
@@ -143,9 +161,7 @@ export class SubirArchivoComponent implements OnInit, OnDestroy {
143
161
  }
144
162
 
145
163
  ValidarFormato(archivo: File): boolean {
146
- if (this.FormatosPermitidos.length === 0) {
147
- return true;
148
- }
164
+ if (this.FormatosPermitidos.length === 0) return true;
149
165
  const nombreArchivo = archivo.name.toLowerCase();
150
166
  return this.FormatosPermitidos.some(formato => nombreArchivo.endsWith(formato.toLowerCase()));
151
167
  }
@@ -182,24 +198,23 @@ export class SubirArchivoComponent implements OnInit, OnDestroy {
182
198
  }
183
199
  let archivo = new FormData();
184
200
  archivo.append("file", this.Archivo);
185
- this.CargarArchivo(archivo, this.Etiqueta)
201
+ this.CargarArchivo(archivo, this.Etiqueta);
186
202
  }
187
203
  }
188
204
 
189
205
  CargarArchivo(archivo: any, Etiqueta: string) {
190
- this.http.post(this.datosGlobalesService.ObtenerURL() + this.RutaParaCargar + Etiqueta,
191
- archivo)
206
+ this.http.post(this.datosGlobalesService.ObtenerURL() + this.RutaParaCargar + Etiqueta, archivo)
192
207
  .pipe(takeUntil(this._destroy$))
193
208
  .subscribe({
194
209
  next: (data: any) => {
195
210
  this.Archivo = undefined;
196
211
  this.MetaDatosDelArchivoCargado = data.body;
197
- this.ListarArchivos(Etiqueta)
212
+ this.ListarArchivos(Etiqueta);
198
213
  },
199
214
  error: (error) => {
200
215
  console.error('Ocurrió un error al guardar el archivo:', error);
201
216
  }
202
- })
217
+ });
203
218
  }
204
219
 
205
220
  ListarArchivos(Etiqueta: string) {
@@ -214,43 +229,162 @@ export class SubirArchivoComponent implements OnInit, OnDestroy {
214
229
  error: (error) => {
215
230
  console.error('Ocurrió un error al listar los archivos:', error);
216
231
  }
217
- })
232
+ });
233
+ }
234
+
235
+ nombreSinFecha(nombre: string): string {
236
+ return nombre.split(' (')[0].trim();
237
+ }
238
+
239
+ fechaDelArchivo(nombre: string): string {
240
+ const match = nombre.match(/\(([^)]+)\)$/);
241
+ return match ? match[1].split(' ')[0] : '';
218
242
  }
219
243
 
220
244
  DescargarArchivo(ArchivoId: string, Nombre: string) {
221
- this.http.get(this.datosGlobalesService.ObtenerURL() + this.RutaParaDescargar + ArchivoId + this.Permiso
222
- , { responseType: 'blob' })
245
+ this.http.get(this.datosGlobalesService.ObtenerURL() + this.RutaParaDescargar + ArchivoId + this.Permiso, { responseType: 'blob' })
223
246
  .pipe(takeUntil(this._destroy$))
224
247
  .subscribe((pdfBlob) => {
225
248
  const url = window.URL.createObjectURL(pdfBlob);
226
249
  const a = this.descargaLinkRef.nativeElement;
227
250
  a.href = url;
228
- a.download = Nombre;
251
+ a.download = this.nombreSinFecha(Nombre);
229
252
  a.click();
230
253
  window.URL.revokeObjectURL(url);
231
- })
254
+ });
232
255
  }
233
256
 
234
257
  BorrarArchivo(ArchivoId: string) {
235
258
  this.http.delete(this.datosGlobalesService.ObtenerURL() + 'misc/borrarArchivo/' + ArchivoId)
236
259
  .pipe(takeUntil(this._destroy$))
237
260
  .subscribe({
238
- next: (data: any) => {
261
+ next: () => {
239
262
  this.MetaDatosDelArchivoCargado = null;
240
263
  this.ListarArchivos(this.Etiqueta);
241
264
  },
242
265
  error: (error) => {
243
266
  console.error('Ocurrió un error al borrar el archivo:', error);
244
267
  }
245
- })
268
+ });
269
+ }
270
+
271
+ Cancelar() {
272
+ this.dialogRef.close(this.MetaDatosDelArchivoCargado);
273
+ }
274
+
275
+ // ── Visor inline ─────────────────────────────────────────────────────────
276
+
277
+ obtenerTipoDeVista(nombre: string): TipoDeVista | null {
278
+ const n = nombre.split(' (')[0].trim().toLowerCase();
279
+ if (n.endsWith('.diff')) return 'diff';
280
+ if (n.endsWith('.txt')) return 'texto';
281
+ if (n.endsWith('.csv')) return 'csv';
282
+ if (n.endsWith('.pdf')) return 'pdf';
283
+ if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/.test(n)) return 'imagen';
284
+ return null;
285
+ }
286
+
287
+ esPrevisualizable(nombre: string): boolean {
288
+ return this.obtenerTipoDeVista(nombre) !== null;
289
+ }
290
+
291
+ PreVisualizarArchivo(archivo: any) {
292
+ const tipo = this.obtenerTipoDeVista(archivo.Nombre);
293
+ if (!tipo) return;
294
+
295
+ this.dialogRef.updateSize('min(90vw, 1400px)', '85vh');
296
+ this.archivoEnVisor = { ...archivo, tipo };
297
+ this.visorCargando = true;
298
+ this.visorError = false;
299
+ this.visorContenidoTexto = '';
300
+ this.visorLineasDiff = [];
301
+ this.visorFilasCsv = [];
302
+ this.visorImagenUrl = null;
303
+ this.visorPdfUrl = null;
304
+ if (this.visorObjectUrl) { URL.revokeObjectURL(this.visorObjectUrl); this.visorObjectUrl = ''; }
305
+
306
+ const url = this.datosGlobalesService.ObtenerURL() + this.RutaParaDescargar + archivo.ArchivoId + this.Permiso;
307
+
308
+ if (tipo === 'diff' || tipo === 'texto' || tipo === 'csv') {
309
+ this.http.get(url, { responseType: 'text' })
310
+ .pipe(takeUntil(this._destroy$))
311
+ .subscribe({
312
+ next: (contenido) => {
313
+ this.visorContenidoTexto = contenido;
314
+ if (tipo === 'diff') this.parsearDiff(contenido);
315
+ if (tipo === 'csv') this.parsearCsv(contenido);
316
+ this.visorCargando = false;
317
+ },
318
+ error: () => { this.visorError = true; this.visorCargando = false; }
319
+ });
320
+ } else {
321
+ this.http.get(url, { responseType: 'blob' })
322
+ .pipe(takeUntil(this._destroy$))
323
+ .subscribe({
324
+ next: (blob) => {
325
+ this.visorObjectUrl = URL.createObjectURL(blob);
326
+ if (tipo === 'imagen') {
327
+ this.visorImagenUrl = this.sanitizer.bypassSecurityTrustUrl(this.visorObjectUrl);
328
+ } else {
329
+ this.visorPdfUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.visorObjectUrl);
330
+ }
331
+ this.visorCargando = false;
332
+ },
333
+ error: () => { this.visorError = true; this.visorCargando = false; }
334
+ });
335
+ }
336
+ }
337
+
338
+ cerrarVisor() {
339
+ this.dialogRef.updateSize('', '');
340
+ this.archivoEnVisor = null;
341
+ if (this.visorObjectUrl) { URL.revokeObjectURL(this.visorObjectUrl); this.visorObjectUrl = ''; }
342
+ }
343
+
344
+ private parsearDiff(contenido: string) {
345
+ this.visorLineasDiff = contenido.split('\n').map((linea, i) => ({
346
+ contenido: linea,
347
+ numero: i + 1,
348
+ tipo: this.clasificarLinea(linea)
349
+ }));
350
+ }
351
+
352
+ private clasificarLinea(linea: string): LineaDiff['tipo'] {
353
+ if (linea.startsWith('diff ') || linea.startsWith('index ') || linea.startsWith('new file') ||
354
+ linea.startsWith('deleted file') || linea.startsWith('similarity') || linea.startsWith('rename')) return 'meta';
355
+ if (linea.startsWith('--- ') || linea.startsWith('+++ ')) return 'archivo';
356
+ if (linea.startsWith('@@ ')) return 'bloque';
357
+ if (linea.startsWith('+')) return 'agregado';
358
+ if (linea.startsWith('-')) return 'eliminado';
359
+ return 'contexto';
360
+ }
361
+
362
+ private parsearCsv(contenido: string) {
363
+ this.visorFilasCsv = contenido
364
+ .split('\n')
365
+ .filter(l => l.trim() !== '')
366
+ .map(l => l.split(',').map(c => c.trim().replace(/^"|"$/g, '')));
367
+ }
368
+
369
+ get estadisticasDiff() {
370
+ return {
371
+ agregadas: this.visorLineasDiff.filter(l => l.tipo === 'agregado').length,
372
+ eliminadas: this.visorLineasDiff.filter(l => l.tipo === 'eliminado').length
373
+ };
374
+ }
375
+
376
+ get iconoVisor(): string {
377
+ const tipo: TipoDeVista | undefined = this.archivoEnVisor?.tipo;
378
+ if (!tipo) return 'visibility';
379
+ const iconos: Record<TipoDeVista, string> = {
380
+ diff: 'code', imagen: 'image', pdf: 'picture_as_pdf', csv: 'table_chart', texto: 'description'
381
+ };
382
+ return iconos[tipo] ?? 'visibility';
246
383
  }
247
384
 
248
385
  ngOnDestroy(): void {
249
386
  this._destroy$.next();
250
387
  this._destroy$.complete();
251
- }
252
-
253
- Cancelar() {
254
- this.dialogRef.close(this.MetaDatosDelArchivoCargado);
388
+ if (this.visorObjectUrl) URL.revokeObjectURL(this.visorObjectUrl);
255
389
  }
256
390
  }
@@ -188,10 +188,11 @@ export class TablaComponent implements OnInit, OnChanges, OnDestroy {
188
188
  }
189
189
 
190
190
  private actualizarDataSource(): void {
191
- const paginaActual = this.paginator ? this.paginator.pageIndex : 0;
192
- const tamanioPagina = this.paginator ? this.paginator.pageSize : this.valorSeleccionadoPorElUsuario;
193
-
194
- this.dataSource = new MatTableDataSource(this.datos);
191
+ if (!this.dataSource) {
192
+ this.dataSource = new MatTableDataSource<any>(this.datos);
193
+ } else {
194
+ this.dataSource.data = this.datos;
195
+ }
195
196
 
196
197
  if (!this.paginarResultados && this.paginator) {
197
198
  this.dataSource.paginator = this.paginator;
@@ -199,14 +200,13 @@ export class TablaComponent implements OnInit, OnChanges, OnDestroy {
199
200
  if (this.sort) {
200
201
  this.dataSource.sort = this.sort;
201
202
  }
202
- if (this.paginarResultados && this.paginator) {
203
- this.paginator.pageIndex = paginaActual;
204
- this.paginator.pageSize = tamanioPagina;
205
- }
206
203
  }
207
204
 
208
205
  ngAfterViewInit() {
209
206
  this.dataSource.sort = this.sort;
207
+ if (!this.paginarResultados) {
208
+ this.dataSource.paginator = this.paginator;
209
+ }
210
210
  this.subscriptions.add(this.paginator.page.subscribe((event: PageEvent) => {
211
211
  localStorage.setItem('pageSize', event.pageSize.toString());
212
212
 
@@ -247,7 +247,6 @@ export class TablaComponent implements OnInit, OnChanges, OnDestroy {
247
247
  });
248
248
  }
249
249
  }));
250
- this.actualizarDataSource();
251
250
  }
252
251
 
253
252
  ngOnDestroy() {
@@ -65,7 +65,7 @@
65
65
  } -->
66
66
  <div class="contenido">
67
67
  @if(TienePermiso){
68
- @if(Descripcion !== '' && Descripcion !== '-') {
68
+ @if(!esDashboard || tieneTarjetas) {
69
69
  <div class="cabecera2">
70
70
  <div class="cabecera2-info">
71
71
  <p class="titulo2">{{ Descripcion }}</p>
@@ -41,6 +41,7 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy, AfterV
41
41
  private intervaloUsuarios: any;
42
42
 
43
43
  get esDashboard(): boolean { return this.router.url === '/'; }
44
+ get tieneTarjetas(): boolean { return this.datosGlobalesService.cantidadDeTarjetas$.value > 0; }
44
45
  get filtro(): string { return this.datosGlobalesService.filtroDeTarjetas$.value; }
45
46
 
46
47
  onFiltroChange(event: Event): void {
@@ -97,10 +97,12 @@ export class ContenedorPrincipalComponent implements OnInit, OnDestroy {
97
97
  this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/obtenerTarjetasDelContenedor`).subscribe({
98
98
  next: (datos: any) => {
99
99
  this.tarjetas = (datos.body ?? []).map((d: any) => this.mapearTarjeta(d));
100
+ this.datosGlobalesService.cantidadDeTarjetas$.next(this.tarjetas.length);
100
101
  },
101
102
  error: (error) => {
102
103
  console.error('Error al obtener las tarjetas del contenedor:', error);
103
104
  this.tarjetas = [];
105
+ this.datosGlobalesService.cantidadDeTarjetas$.next(0);
104
106
  }
105
107
  });
106
108
  }
@@ -7,13 +7,14 @@ import { BehaviorSubject } from 'rxjs';
7
7
  export class DatosGlobalesService {
8
8
 
9
9
  readonly filtroDeTarjetas$ = new BehaviorSubject<string>('');
10
+ readonly cantidadDeTarjetas$ = new BehaviorSubject<number>(0);
10
11
 
11
12
  constructor() { }
12
13
 
13
14
  ObtenerToken() {
14
15
  const baseUrl = this.ObtenerURL();
15
16
  if (baseUrl === 'http://localhost/') {
16
- return 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxMiIsIklkZW50aWZpY2Fkb3IiOiIxMiIsImlhdCI6MTc3Nzk4OTgwOSwiZXhwIjoxNzc4MDI1ODA5fQ.T1kUm7H4hwbapQ3eFok31GVvezkPitdTlJL96MorozY';
17
+ return 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxMiIsIklkZW50aWZpY2Fkb3IiOiIxMiIsImlhdCI6MTc3ODQzMDUzMSwiZXhwIjoxNzc4NDY2NTMxfQ.C0bScJZ00863G1GsjFLox-V4wBj4nLUc2eXfswiOaxg';
17
18
  }
18
19
  const match = document.cookie.match(/(?:^|;\s*)_siguid=([^;]+)/);
19
20
  let token = match ? decodeURIComponent(match[1]) : '';