utn-cli 2.1.15 → 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 +1 -1
- package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.css +273 -16
- package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.html +89 -7
- package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts +173 -39
- package/templates/frontend/src/app/Componentes/Nucleo/tabla/tabla.component.ts +8 -9
package/package.json
CHANGED
package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.css
CHANGED
|
@@ -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
|
-
.
|
|
42
|
-
margin: 0;
|
|
36
|
+
.nombre-archivo {
|
|
43
37
|
flex: 1;
|
|
44
|
-
|
|
45
|
-
|
|
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
|
}
|
package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.html
CHANGED
|
@@ -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
|
-
<
|
|
7
|
-
|
|
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
|
+
}
|
package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
if (this.data.
|
|
45
|
-
|
|
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: (
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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() {
|