@jvsoft/mat-form-controls 1.0.0-alpha.13

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/README.md +184 -0
  2. package/base/index.d.ts +5 -0
  3. package/base/jvs-mat-form-control-base.d.ts +51 -0
  4. package/base/public-api.d.ts +1 -0
  5. package/fesm2022/jvsoft-mat-form-controls-base.mjs +145 -0
  6. package/fesm2022/jvsoft-mat-form-controls-base.mjs.map +1 -0
  7. package/fesm2022/jvsoft-mat-form-controls-jvs-autocomplete.mjs +101 -0
  8. package/fesm2022/jvsoft-mat-form-controls-jvs-autocomplete.mjs.map +1 -0
  9. package/fesm2022/jvsoft-mat-form-controls-jvs-file-upload.mjs +624 -0
  10. package/fesm2022/jvsoft-mat-form-controls-jvs-file-upload.mjs.map +1 -0
  11. package/fesm2022/jvsoft-mat-form-controls.mjs +145 -0
  12. package/fesm2022/jvsoft-mat-form-controls.mjs.map +1 -0
  13. package/index.d.ts +5 -0
  14. package/jvs-autocomplete/index.d.ts +5 -0
  15. package/jvs-autocomplete/jvs-autocomplete.component.d.ts +26 -0
  16. package/jvs-autocomplete/jvs-autocomplete.component.scss +58 -0
  17. package/jvs-autocomplete/public-api.d.ts +1 -0
  18. package/jvs-file-upload/README.md +613 -0
  19. package/jvs-file-upload/index.d.ts +5 -0
  20. package/jvs-file-upload/jvs-file-upload-item/jvs-file-upload-item.component.d.ts +29 -0
  21. package/jvs-file-upload/jvs-file-upload-item/jvs-file-upload-item.component.scss +118 -0
  22. package/jvs-file-upload/jvs-file-upload.component.d.ts +140 -0
  23. package/jvs-file-upload/jvs-file-upload.component.scss +163 -0
  24. package/jvs-file-upload/jvs-file-upload.directive.d.ts +42 -0
  25. package/jvs-file-upload/jvs-file-upload.interfaces.d.ts +77 -0
  26. package/jvs-file-upload/public-api.d.ts +4 -0
  27. package/package.json +39 -0
  28. package/public-api.d.ts +1 -0
  29. package/src/lib/mat-form-controls/mat-form-controls.component.css +0 -0
@@ -0,0 +1,624 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, EventEmitter, HostListener, HostBinding, Output, Directive, output, computed, Component, signal, forwardRef } from '@angular/core';
3
+ import { mensajeToast, convertirBytes, mensajeConfirmacion } from '@jvsoft/utils';
4
+ import { NgClass, formatDate, NgTemplateOutlet } from '@angular/common';
5
+ import * as i1 from '@angular/material/icon';
6
+ import { MatIconModule } from '@angular/material/icon';
7
+ import * as i2 from '@angular/material/progress-bar';
8
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
9
+ import * as i3 from '@angular/material/core';
10
+ import { MatRippleModule } from '@angular/material/core';
11
+ import * as i4 from '@angular/material/tooltip';
12
+ import { MatTooltipModule } from '@angular/material/tooltip';
13
+ import { ReactiveFormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
14
+ import { HttpEventType } from '@angular/common/http';
15
+ import * as i1$1 from '@angular/material/divider';
16
+ import { MatDividerModule } from '@angular/material/divider';
17
+ import * as i3$1 from '@angular/material/list';
18
+ import { MatListModule } from '@angular/material/list';
19
+ import { lastValueFrom, map, catchError, of } from 'rxjs';
20
+
21
+ /**
22
+ * Directiva que convierte cualquier elemento en una zona de drag-and-drop
23
+ * para subida de archivos.
24
+ *
25
+ * @example
26
+ * <div jvsFileUpload [controlFile]="inputRef" (filesChange)="onFiles($event)">
27
+ */
28
+ class JvsFileUploadDirective {
29
+ // ── Inputs ──────────────────────────────────────────────────────────────
30
+ fondoInicial = input('');
31
+ fondoDragOver = input('#e5e7eb');
32
+ controlFile = input.required();
33
+ extensionesPermitidas = input([]);
34
+ parteDeNombre = input([]);
35
+ parteDeNombreExclusivo = input([]);
36
+ tamanoMaximoMB = input(null);
37
+ isDisabled = input(false);
38
+ // ── Outputs ─────────────────────────────────────────────────────────────
39
+ filesChange = new EventEmitter();
40
+ filesInvalidChange = new EventEmitter();
41
+ // ── Host bindings ────────────────────────────────────────────────────────
42
+ background = '';
43
+ // ── Listeners ────────────────────────────────────────────────────────────
44
+ onDragOver(evt) {
45
+ evt.preventDefault();
46
+ evt.stopPropagation();
47
+ if (!this.isDisabled()) {
48
+ this.background = this.fondoDragOver();
49
+ }
50
+ }
51
+ onDragLeave(evt) {
52
+ evt.preventDefault();
53
+ evt.stopPropagation();
54
+ if (!this.isDisabled()) {
55
+ this.background = this.fondoInicial();
56
+ }
57
+ }
58
+ onDrop(evt) {
59
+ evt.preventDefault();
60
+ evt.stopPropagation();
61
+ if (this.isDisabled())
62
+ return;
63
+ this.background = this.fondoInicial();
64
+ const files = Array.from(evt.dataTransfer?.files ?? []);
65
+ const result = JvsFileUploadDirective.filtrarArchivos(files, {
66
+ extensionesPermitidas: this.extensionesPermitidas(),
67
+ parteDeNombre: this.parteDeNombre(),
68
+ parteDeNombreExclusivo: this.parteDeNombreExclusivo(),
69
+ tamanoMaximoMB: this.tamanoMaximoMB(),
70
+ }, this.controlFile().multiple);
71
+ this.filesChange.emit(result.valid);
72
+ this.filesInvalidChange.emit(result.invalid);
73
+ }
74
+ // ── Static helpers ───────────────────────────────────────────────────────
75
+ /**
76
+ * Filtra un array de archivos según extensión, partes de nombre y tamaño máximo.
77
+ *
78
+ * Lógica:
79
+ * 1. Si el archivo excede `tamanoMaximoMB` → inválido.
80
+ * 2. Si el nombre contiene algún valor de `parteDeNombreExclusivo` → válido sin checar extensión.
81
+ * 3. Si la extensión está en `extensionesPermitidas` Y el nombre contiene TODOS los valores
82
+ * de `parteDeNombre` → válido.
83
+ * 4. En cualquier otro caso → inválido.
84
+ */
85
+ static filtrarArchivos(files, params, multiple = true) {
86
+ const validFiles = [];
87
+ const invalidFiles = [];
88
+ const getExt = (nombre) => {
89
+ const idx = nombre.lastIndexOf('.');
90
+ return idx !== -1 ? nombre.slice(idx + 1).toLowerCase() : '';
91
+ };
92
+ for (const file of files) {
93
+ const ext = getExt(file.name);
94
+ const tamanoMB = file.size / (1024 * 1024);
95
+ // 1. Validar tamaño
96
+ if (params.tamanoMaximoMB && tamanoMB > params.tamanoMaximoMB) {
97
+ mensajeToast('error', 'Archivo no válido', `El archivo <strong>${file.name}</strong> supera el tamaño máximo permitido (${params.tamanoMaximoMB} MB).`);
98
+ invalidFiles.push(file);
99
+ continue;
100
+ }
101
+ // 2. Nombre exclusivo (bypass de extensión)
102
+ if (params.parteDeNombreExclusivo.length > 0 &&
103
+ params.parteDeNombreExclusivo.some(p => file.name.includes(p))) {
104
+ validFiles.push(file);
105
+ if (!multiple)
106
+ break;
107
+ continue;
108
+ }
109
+ // 3. Extensión permitida + partes de nombre requeridas
110
+ const extensionOk = params.extensionesPermitidas.length === 0 ||
111
+ params.extensionesPermitidas.includes(ext);
112
+ const nombreOk = params.parteDeNombre.every(p => file.name.includes(p));
113
+ if (extensionOk && nombreOk) {
114
+ validFiles.push(file);
115
+ if (!multiple)
116
+ break;
117
+ }
118
+ else {
119
+ mensajeToast('error', 'Archivo no válido', `El archivo <strong>${file.name}</strong> no es válido.`);
120
+ invalidFiles.push(file);
121
+ }
122
+ }
123
+ return { valid: validFiles, invalid: invalidFiles };
124
+ }
125
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: JvsFileUploadDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
126
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.14", type: JvsFileUploadDirective, isStandalone: true, selector: "[jvsFileUpload]", inputs: { fondoInicial: { classPropertyName: "fondoInicial", publicName: "fondoInicial", isSignal: true, isRequired: false, transformFunction: null }, fondoDragOver: { classPropertyName: "fondoDragOver", publicName: "fondoDragOver", isSignal: true, isRequired: false, transformFunction: null }, controlFile: { classPropertyName: "controlFile", publicName: "controlFile", isSignal: true, isRequired: true, transformFunction: null }, extensionesPermitidas: { classPropertyName: "extensionesPermitidas", publicName: "extensionesPermitidas", isSignal: true, isRequired: false, transformFunction: null }, parteDeNombre: { classPropertyName: "parteDeNombre", publicName: "parteDeNombre", isSignal: true, isRequired: false, transformFunction: null }, parteDeNombreExclusivo: { classPropertyName: "parteDeNombreExclusivo", publicName: "parteDeNombreExclusivo", isSignal: true, isRequired: false, transformFunction: null }, tamanoMaximoMB: { classPropertyName: "tamanoMaximoMB", publicName: "tamanoMaximoMB", isSignal: true, isRequired: false, transformFunction: null }, isDisabled: { classPropertyName: "isDisabled", publicName: "isDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesChange: "filesChange", filesInvalidChange: "filesInvalidChange" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "style.background": "this.background" } }, ngImport: i0 });
127
+ }
128
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: JvsFileUploadDirective, decorators: [{
129
+ type: Directive,
130
+ args: [{
131
+ standalone: true,
132
+ selector: '[jvsFileUpload]',
133
+ }]
134
+ }], propDecorators: { filesChange: [{
135
+ type: Output
136
+ }], filesInvalidChange: [{
137
+ type: Output
138
+ }], background: [{
139
+ type: HostBinding,
140
+ args: ['style.background']
141
+ }], onDragOver: [{
142
+ type: HostListener,
143
+ args: ['dragover', ['$event']]
144
+ }], onDragLeave: [{
145
+ type: HostListener,
146
+ args: ['dragleave', ['$event']]
147
+ }], onDrop: [{
148
+ type: HostListener,
149
+ args: ['drop', ['$event']]
150
+ }] } });
151
+
152
+ /**
153
+ * Componente de ítem individual de la lista de archivos en modo `temporal`.
154
+ * Muestra nombre, tamaño, barra de progreso y acciones (descargar, firmar, eliminar).
155
+ */
156
+ class JvsFileUploadItemComponent {
157
+ // ── Inputs ───────────────────────────────────────────────────────────────
158
+ file = input.required();
159
+ permitirEliminar = input(true);
160
+ isDisabled = input(false);
161
+ // ── Outputs ──────────────────────────────────────────────────────────────
162
+ eliminar = output();
163
+ descargar = output();
164
+ firmar = output();
165
+ // ── Computed ──────────────────────────────────────────────────────────────
166
+ extension = computed(() => {
167
+ const f = this.file();
168
+ return f.servFile?.extension ?? f.file?.name?.split('.').pop()?.toLowerCase() ?? '';
169
+ });
170
+ nombreMostrado = computed(() => {
171
+ const f = this.file();
172
+ return f.servFile?.nombre ?? f.file?.name ?? '';
173
+ });
174
+ estadoProgreso = computed(() => {
175
+ const f = this.file();
176
+ if (f.progress === 100 && f.servFile)
177
+ return 'completado';
178
+ if (f.progress === 100 && !f.servFile)
179
+ return 'incompleto';
180
+ if (f.progress > 0 && f.progress < 100)
181
+ return 'cargando';
182
+ return '';
183
+ });
184
+ modoProgreso = computed(() => {
185
+ const f = this.file();
186
+ return (f.progress === 100 && !f.servFile) ? 'indeterminate' : 'determinate';
187
+ });
188
+ mostrarBotonDescargar = computed(() => {
189
+ const sf = this.file().servFile;
190
+ // key → DB-stored file; path → freshly uploaded (backend returns plain path string); cArchivoData → PDF generation metadata
191
+ return !!sf?.key || !!sf?.path || !!sf?.cArchivoData;
192
+ });
193
+ mostrarBotonFirmar = computed(() => !!this.file().servFile && !this.isDisabled());
194
+ mostrarBotonEliminar = computed(() => this.permitirEliminar() && !this.isDisabled());
195
+ tamanioArchivo = computed(() => {
196
+ const f = this.file();
197
+ const file = f.file;
198
+ if (!file)
199
+ return 0;
200
+ return file.size ?? 0;
201
+ });
202
+ // ── Utilidades ────────────────────────────────────────────────────────────
203
+ convertirBytes = convertirBytes;
204
+ onEliminar() {
205
+ this.eliminar.emit(this.file());
206
+ }
207
+ onDescargar() {
208
+ this.descargar.emit(this.file());
209
+ }
210
+ onFirmar() {
211
+ if (this.file().servFile) {
212
+ this.firmar.emit(this.file().servFile);
213
+ }
214
+ }
215
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: JvsFileUploadItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
216
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: JvsFileUploadItemComponent, isStandalone: true, selector: "jvs-file-upload-item", inputs: { file: { classPropertyName: "file", publicName: "file", isSignal: true, isRequired: true, transformFunction: null }, permitirEliminar: { classPropertyName: "permitirEliminar", publicName: "permitirEliminar", isSignal: true, isRequired: false, transformFunction: null }, isDisabled: { classPropertyName: "isDisabled", publicName: "isDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { eliminar: "eliminar", descargar: "descargar", firmar: "firmar" }, ngImport: i0, template: "<div class=\"jvs-file-item\">\n\n <!-- \u00CDcono por tipo de archivo -->\n <div class=\"jvs-file-item__icon\">\n @switch (extension()) {\n @case ('pdf') {\n <mat-icon class=\"icon-pdf\" svgIcon=\"fa5FilePdf\" />\n }\n @case ('xls') {\n <mat-icon class=\"icon-excel\" svgIcon=\"fa5FileExcel\" />\n }\n @case ('xlsx') {\n <mat-icon class=\"icon-excel\" svgIcon=\"fa5FileExcel\" />\n }\n @case ('doc') {\n <mat-icon class=\"icon-word\" svgIcon=\"fa5FileWord\" />\n }\n @case ('docx') {\n <mat-icon class=\"icon-word\" svgIcon=\"fa5FileWord\" />\n }\n @case ('jpg') {\n <mat-icon class=\"icon-image\" svgIcon=\"roundImage\" />\n }\n @case ('jpeg') {\n <mat-icon class=\"icon-image\" svgIcon=\"roundImage\" />\n }\n @case ('png') {\n <mat-icon class=\"icon-image\" svgIcon=\"roundImage\" />\n }\n @default {\n <mat-icon svgIcon=\"fa5File\" />\n }\n }\n </div>\n\n <!-- Informaci\u00F3n del archivo -->\n <div class=\"jvs-file-item__info\">\n <p class=\"jvs-file-item__name\" [title]=\"nombreMostrado()\">\n {{ nombreMostrado() }}\n </p>\n\n @if (file().servFile) {\n <p class=\"jvs-file-item__status jvs-file-item__status--server\">En servidor</p>\n } @else {\n <p class=\"jvs-file-item__status\">\n {{ convertirBytes(tamanioArchivo()) }}\n </p>\n }\n\n @if (file().errorSubida) {\n <p class=\"jvs-file-item__status jvs-file-item__status--error\">Error al subir</p>\n }\n\n <mat-progress-bar\n [value]=\"file().progress\"\n [mode]=\"modoProgreso()\"\n [class]=\"estadoProgreso()\"\n />\n </div>\n\n <!-- Acciones -->\n <div class=\"jvs-file-item__actions\">\n\n @if (mostrarBotonDescargar()) {\n <button\n type=\"button\"\n class=\"jvs-file-item__btn jvs-file-item__btn--download\"\n matRipple\n (click)=\"onDescargar()\"\n matTooltip=\"Descargar\"\n >\n <mat-icon svgIcon=\"roundDownload\" />\n </button>\n }\n\n @if (mostrarBotonFirmar()) {\n <button\n type=\"button\"\n class=\"jvs-file-item__btn jvs-file-item__btn--sign\"\n matRipple\n (click)=\"onFirmar()\"\n matTooltip=\"Firmar\"\n >\n <mat-icon svgIcon=\"roundDraw\" />\n </button>\n }\n\n @if (mostrarBotonEliminar()) {\n <button\n type=\"button\"\n class=\"jvs-file-item__btn jvs-file-item__btn--delete\"\n matRipple\n (click)=\"onEliminar()\"\n matTooltip=\"Eliminar\"\n >\n <mat-icon svgIcon=\"roundDelete\" />\n </button>\n }\n\n </div>\n</div>\n", styles: [":host{display:block}.jvs-file-item{display:flex;align-items:center;gap:.5rem;padding:.5rem 0}.jvs-file-item__icon{flex-shrink:0;display:flex;align-items:center}.jvs-file-item__icon mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.jvs-file-item__icon .icon-pdf{color:#e53e3e}.jvs-file-item__icon .icon-excel{color:#38a169}.jvs-file-item__icon .icon-word{color:#3182ce}.jvs-file-item__icon .icon-image{color:#805ad5}.jvs-file-item__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.125rem}.jvs-file-item__name{margin:0;font-size:.7rem;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.jvs-file-item__status{margin:0;font-size:.65rem;line-height:1.2;color:#6b7280}.jvs-file-item__status--server{color:#d97706}.jvs-file-item__status--error{color:#dc2626}.jvs-file-item mat-progress-bar{border-radius:2px;height:4px;margin-top:2px}.jvs-file-item ::ng-deep .cargando .mdc-linear-progress__bar-inner{border-color:var(--mat-sys-primary, #6366f1)}.jvs-file-item ::ng-deep .incompleto .mdc-linear-progress__bar-inner{border-color:#f59e0b}.jvs-file-item ::ng-deep .completado .mdc-linear-progress__bar-inner{border-color:#10b981}.jvs-file-item ::ng-deep .mdc-linear-progress__buffer-bar{background:#e5e7eb}.jvs-file-item__actions{flex-shrink:0;display:flex;align-items:center;gap:.25rem}.jvs-file-item__btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:50%;border:none;cursor:pointer;background:transparent;transition:background .15s}.jvs-file-item__btn mat-icon{font-size:1rem;width:1rem;height:1rem}.jvs-file-item__btn--download{color:#3b82f6}.jvs-file-item__btn--download:hover{background:#eff6ff}.jvs-file-item__btn--sign{color:#8b5cf6}.jvs-file-item__btn--sign:hover{background:#f5f3ff}.jvs-file-item__btn--delete{color:#ef4444}.jvs-file-item__btn--delete:hover{background:#fef2f2}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i2.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i3.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
217
+ }
218
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: JvsFileUploadItemComponent, decorators: [{
219
+ type: Component,
220
+ args: [{ selector: 'jvs-file-upload-item', standalone: true, imports: [
221
+ NgClass,
222
+ MatIconModule,
223
+ MatProgressBarModule,
224
+ MatRippleModule,
225
+ MatTooltipModule,
226
+ ], template: "<div class=\"jvs-file-item\">\n\n <!-- \u00CDcono por tipo de archivo -->\n <div class=\"jvs-file-item__icon\">\n @switch (extension()) {\n @case ('pdf') {\n <mat-icon class=\"icon-pdf\" svgIcon=\"fa5FilePdf\" />\n }\n @case ('xls') {\n <mat-icon class=\"icon-excel\" svgIcon=\"fa5FileExcel\" />\n }\n @case ('xlsx') {\n <mat-icon class=\"icon-excel\" svgIcon=\"fa5FileExcel\" />\n }\n @case ('doc') {\n <mat-icon class=\"icon-word\" svgIcon=\"fa5FileWord\" />\n }\n @case ('docx') {\n <mat-icon class=\"icon-word\" svgIcon=\"fa5FileWord\" />\n }\n @case ('jpg') {\n <mat-icon class=\"icon-image\" svgIcon=\"roundImage\" />\n }\n @case ('jpeg') {\n <mat-icon class=\"icon-image\" svgIcon=\"roundImage\" />\n }\n @case ('png') {\n <mat-icon class=\"icon-image\" svgIcon=\"roundImage\" />\n }\n @default {\n <mat-icon svgIcon=\"fa5File\" />\n }\n }\n </div>\n\n <!-- Informaci\u00F3n del archivo -->\n <div class=\"jvs-file-item__info\">\n <p class=\"jvs-file-item__name\" [title]=\"nombreMostrado()\">\n {{ nombreMostrado() }}\n </p>\n\n @if (file().servFile) {\n <p class=\"jvs-file-item__status jvs-file-item__status--server\">En servidor</p>\n } @else {\n <p class=\"jvs-file-item__status\">\n {{ convertirBytes(tamanioArchivo()) }}\n </p>\n }\n\n @if (file().errorSubida) {\n <p class=\"jvs-file-item__status jvs-file-item__status--error\">Error al subir</p>\n }\n\n <mat-progress-bar\n [value]=\"file().progress\"\n [mode]=\"modoProgreso()\"\n [class]=\"estadoProgreso()\"\n />\n </div>\n\n <!-- Acciones -->\n <div class=\"jvs-file-item__actions\">\n\n @if (mostrarBotonDescargar()) {\n <button\n type=\"button\"\n class=\"jvs-file-item__btn jvs-file-item__btn--download\"\n matRipple\n (click)=\"onDescargar()\"\n matTooltip=\"Descargar\"\n >\n <mat-icon svgIcon=\"roundDownload\" />\n </button>\n }\n\n @if (mostrarBotonFirmar()) {\n <button\n type=\"button\"\n class=\"jvs-file-item__btn jvs-file-item__btn--sign\"\n matRipple\n (click)=\"onFirmar()\"\n matTooltip=\"Firmar\"\n >\n <mat-icon svgIcon=\"roundDraw\" />\n </button>\n }\n\n @if (mostrarBotonEliminar()) {\n <button\n type=\"button\"\n class=\"jvs-file-item__btn jvs-file-item__btn--delete\"\n matRipple\n (click)=\"onEliminar()\"\n matTooltip=\"Eliminar\"\n >\n <mat-icon svgIcon=\"roundDelete\" />\n </button>\n }\n\n </div>\n</div>\n", styles: [":host{display:block}.jvs-file-item{display:flex;align-items:center;gap:.5rem;padding:.5rem 0}.jvs-file-item__icon{flex-shrink:0;display:flex;align-items:center}.jvs-file-item__icon mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.jvs-file-item__icon .icon-pdf{color:#e53e3e}.jvs-file-item__icon .icon-excel{color:#38a169}.jvs-file-item__icon .icon-word{color:#3182ce}.jvs-file-item__icon .icon-image{color:#805ad5}.jvs-file-item__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.125rem}.jvs-file-item__name{margin:0;font-size:.7rem;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.jvs-file-item__status{margin:0;font-size:.65rem;line-height:1.2;color:#6b7280}.jvs-file-item__status--server{color:#d97706}.jvs-file-item__status--error{color:#dc2626}.jvs-file-item mat-progress-bar{border-radius:2px;height:4px;margin-top:2px}.jvs-file-item ::ng-deep .cargando .mdc-linear-progress__bar-inner{border-color:var(--mat-sys-primary, #6366f1)}.jvs-file-item ::ng-deep .incompleto .mdc-linear-progress__bar-inner{border-color:#f59e0b}.jvs-file-item ::ng-deep .completado .mdc-linear-progress__bar-inner{border-color:#10b981}.jvs-file-item ::ng-deep .mdc-linear-progress__buffer-bar{background:#e5e7eb}.jvs-file-item__actions{flex-shrink:0;display:flex;align-items:center;gap:.25rem}.jvs-file-item__btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:50%;border:none;cursor:pointer;background:transparent;transition:background .15s}.jvs-file-item__btn mat-icon{font-size:1rem;width:1rem;height:1rem}.jvs-file-item__btn--download{color:#3b82f6}.jvs-file-item__btn--download:hover{background:#eff6ff}.jvs-file-item__btn--sign{color:#8b5cf6}.jvs-file-item__btn--sign:hover{background:#f5f3ff}.jvs-file-item__btn--delete{color:#ef4444}.jvs-file-item__btn--delete:hover{background:#fef2f2}\n"] }]
227
+ }] });
228
+
229
+ /**
230
+ * Componente de subida de archivos reutilizable.
231
+ *
232
+ * Implementa `ControlValueAccessor` para integrarse con formularios reactivos y
233
+ * template-driven. El valor del control es `JvsArchivoServidor[]`.
234
+ *
235
+ * ## Modos de operación
236
+ *
237
+ * - **`temporal = false`** (defecto): los archivos se seleccionan localmente y se
238
+ * envían todos juntos llamando a `uploadFilesPreForm()` antes de guardar el form.
239
+ *
240
+ * - **`temporal = true`**: cada archivo se sube de forma inmediata al seleccionarse,
241
+ * mostrando barra de progreso individual. El valor del control se actualiza en
242
+ * tiempo real con los archivos ya subidos.
243
+ *
244
+ * ## Ejemplo de uso
245
+ *
246
+ * ```html
247
+ * <jvs-file-upload
248
+ * [temporal]="true"
249
+ * [extensionesPermitidas]="['pdf','docx']"
250
+ * [tamanoMaximoMB]="10"
251
+ * [uploadFn]="filesService.upload.bind(filesService)"
252
+ * [removeFn]="filesService.remove.bind(filesService)"
253
+ * (archivoDescarga)="onDescargar($event)"
254
+ * (firmarArchivo)="onFirmar($event)"
255
+ * formControlName="archivos"
256
+ * />
257
+ * ```
258
+ */
259
+ class JvsFileUploadComponent {
260
+ // ── Inputs ───────────────────────────────────────────────────────────────
261
+ /** Activa subida inmediata de cada archivo al seleccionarlo. */
262
+ temporal = input(false);
263
+ /** Habilita la opción de eliminar archivos. */
264
+ permitirEliminar = input(true);
265
+ /** Solo muestra la lista; oculta la zona de drop. */
266
+ readonly = input(false);
267
+ /** Permite seleccionar múltiples archivos. */
268
+ multiple = input(true);
269
+ /** Extensiones de archivo permitidas (sin punto, en minúsculas). */
270
+ extensionesPermitidas = input([]);
271
+ /**
272
+ * El nombre del archivo debe contener TODOS estos fragmentos.
273
+ * Solo aplica cuando `parteDeNombreExclusivo` está vacío.
274
+ */
275
+ parteDeNombre = input([]);
276
+ /**
277
+ * Si el nombre del archivo contiene ALGUNO de estos fragmentos,
278
+ * se acepta sin verificar extensión.
279
+ */
280
+ parteDeNombreExclusivo = input([]);
281
+ /** Tamaño máximo en MB. `null` = sin límite. */
282
+ tamanoMaximoMB = input(null);
283
+ /** Nombre fijo que se envía al servidor al subir. */
284
+ nombreArchivoFijo = input(null);
285
+ /**
286
+ * Carpeta destino en el servidor.
287
+ * Defecto: `temp/YYYY-MM-DD_HH`
288
+ */
289
+ carpetaSubida = input(null);
290
+ /** Disk de Laravel para el almacenamiento. `null` = usar default del servidor. */
291
+ diskSubida = input(null);
292
+ /**
293
+ * Función que recibe un `FormData` y devuelve un `Observable<HttpEvent<any>>`.
294
+ * Obligatoria en modo `temporal = true` o al llamar `uploadFilesPreForm()`.
295
+ *
296
+ * @example
297
+ * [uploadFn]="filesService.upload.bind(filesService)"
298
+ */
299
+ uploadFn = input(null);
300
+ /**
301
+ * Función que elimina un archivo del servidor.
302
+ * Recibe el path o parámetros necesarios y devuelve una Promise.
303
+ *
304
+ * @example
305
+ * [removeFn]="filesService.remove.bind(filesService)"
306
+ */
307
+ removeFn = input(null);
308
+ /** Clase CSS extra para el contenedor de la lista (modo temporal). */
309
+ cssContenedorAgregados = input('');
310
+ /** Clase CSS extra para el contenedor de lista de archivos válidos/inválidos. */
311
+ cssContenedorAgregadosLista = input('');
312
+ // ── Outputs ──────────────────────────────────────────────────────────────
313
+ /** Emite la lista de archivos restante tras eliminar un elemento. */
314
+ resultadoEliminado = output();
315
+ /**
316
+ * Emite el archivo del servidor cuando el usuario hace click en "Descargar".
317
+ * El componente padre es responsable de ejecutar la descarga real.
318
+ */
319
+ archivoDescarga = output();
320
+ /**
321
+ * Emite el archivo del servidor cuando el usuario hace click en "Firmar".
322
+ * El componente padre es responsable de abrir el diálogo de firma.
323
+ */
324
+ firmarArchivo = output();
325
+ // ── Estado interno (signals) ──────────────────────────────────────────────
326
+ fileList = signal([]);
327
+ invalidFiles = signal([]);
328
+ isDisabled = signal(false);
329
+ // ── Computed ──────────────────────────────────────────────────────────────
330
+ /** Verdadero si no hay archivos en la lista. */
331
+ isEmpty = computed(() => this.fileList().length === 0);
332
+ /** Extensiones normalizadas a minúsculas. */
333
+ extensionesNormalizadas = computed(() => this.extensionesPermitidas().map(e => e.toLowerCase()));
334
+ /** Texto informativo de extensiones permitidas. */
335
+ textoExtensiones = computed(() => {
336
+ const exc = this.parteDeNombreExclusivo();
337
+ const ext = this.extensionesNormalizadas();
338
+ if (exc.length > 0)
339
+ return `El nombre debe contener: ${exc.join(', ').toUpperCase()}`;
340
+ if (ext.length > 0)
341
+ return `Permitidos: ${ext.join(', ').toUpperCase()}`;
342
+ return 'Todos los archivos';
343
+ });
344
+ // ── CVA callbacks ─────────────────────────────────────────────────────────
345
+ onChange = () => { };
346
+ onTouched = () => { };
347
+ _onValidatorChange = () => { };
348
+ // ── ControlValueAccessor ──────────────────────────────────────────────────
349
+ writeValue(value) {
350
+ this.fileList.set([]);
351
+ if (!value || !Array.isArray(value))
352
+ return;
353
+ const entries = value.map(f => ({
354
+ file: { name: f.nombre ?? '' },
355
+ desdeServidor: true,
356
+ inProgress: false,
357
+ progress: 100,
358
+ servFile: f,
359
+ }));
360
+ this.fileList.set(entries);
361
+ }
362
+ registerOnChange(fn) { this.onChange = fn; }
363
+ registerOnTouched(fn) { this.onTouched = fn; }
364
+ setDisabledState(isDisabled) { this.isDisabled.set(isDisabled); }
365
+ registerOnValidatorChange(fn) { this._onValidatorChange = fn; }
366
+ // ── Validator ─────────────────────────────────────────────────────────────
367
+ // @ts-ignore
368
+ validate(_) {
369
+ if (this.fileList().some(f => f.inProgress))
370
+ return { uploadEnProgreso: true };
371
+ if (this.fileList().some(f => f.errorSubida))
372
+ return { errorSubida: true };
373
+ return null;
374
+ }
375
+ // ── Manejo de selección de archivos ───────────────────────────────────────
376
+ /** Llamado desde el template al cambiar el input[type=file]. */
377
+ onUploadChange(files) {
378
+ if (!files)
379
+ return;
380
+ const fileArray = Array.from(files);
381
+ const filtered = JvsFileUploadDirective.filtrarArchivos(fileArray, {
382
+ extensionesPermitidas: this.extensionesNormalizadas(),
383
+ parteDeNombre: this.parteDeNombre(),
384
+ parteDeNombreExclusivo: this.parteDeNombreExclusivo(),
385
+ tamanoMaximoMB: this.tamanoMaximoMB(),
386
+ }, this.multiple());
387
+ this.onFilesChange(filtered.valid);
388
+ this.invalidFiles.set(filtered.invalid);
389
+ }
390
+ /** Llamado desde la directiva drag-and-drop al soltar archivos válidos. */
391
+ onFilesChange(newFiles) {
392
+ if (!this.multiple() && (newFiles.length + this.fileList().length) > 1) {
393
+ mensajeToast('error', 'Error', 'Solo se permite un archivo.');
394
+ return;
395
+ }
396
+ if (this.temporal()) {
397
+ const nuevos = newFiles
398
+ .filter(f => !this.fileList().some(e => e.file?.name === f.name))
399
+ .map(f => ({
400
+ file: f,
401
+ desdeServidor: false,
402
+ inProgress: false,
403
+ progress: 0,
404
+ servFile: undefined,
405
+ }));
406
+ this.fileList.update(list => [...list, ...nuevos]);
407
+ // Subida inmediata en modo temporal
408
+ nuevos.forEach(entry => this.uploadFileTemporal(entry));
409
+ }
410
+ else {
411
+ const entries = newFiles.map(f => ({
412
+ file: f,
413
+ desdeServidor: false,
414
+ inProgress: false,
415
+ progress: 0,
416
+ }));
417
+ // Keep server-loaded entries, replace only locally selected ones
418
+ this.fileList.update(list => [
419
+ ...list.filter(f => f.desdeServidor),
420
+ ...entries,
421
+ ]);
422
+ }
423
+ this._notifyChange();
424
+ }
425
+ // ── Subida de archivos ────────────────────────────────────────────────────
426
+ /**
427
+ * Sube todos los archivos pendientes (sin `servFile`) al servidor.
428
+ * Para usar antes del envío del formulario en modo `temporal = false`.
429
+ *
430
+ * @param carpeta Carpeta destino en el servidor (opcional, sobreescribe el input).
431
+ * @param anonimo `true` para subir sin autenticación.
432
+ * @param disk Disk de almacenamiento (opcional, sobreescribe el input).
433
+ * @returns Promise con la lista de `JvsArchivoServidor[]` subidos correctamente.
434
+ */
435
+ async uploadFilesPreForm(carpeta, anonimo = false, disk) {
436
+ const uploadFn = this.uploadFn();
437
+ if (!uploadFn) {
438
+ console.warn('[JvsFileUpload] uploadFn no proporcionado. No se subirán archivos.');
439
+ return this.fileList()
440
+ .filter(f => f.servFile)
441
+ .map(f => f.servFile);
442
+ }
443
+ const carpetaFinal = carpeta ?? this.carpetaSubida() ??
444
+ 'temp/' + formatDate(new Date(), 'yyyy-MM-dd_HH', 'en');
445
+ const diskFinal = disk ?? this.diskSubida();
446
+ const pendientes = this.fileList().filter(f => !f.servFile);
447
+ const promises = pendientes.map(entry => this._subirEntrada(entry, uploadFn, carpetaFinal, anonimo, diskFinal));
448
+ await Promise.allSettled(promises);
449
+ const todosBien = pendientes.every(p => !p.errorSubida);
450
+ if (!todosBien) {
451
+ const hayMas = this.fileList().length > 1;
452
+ if (hayMas) {
453
+ const result = await mensajeConfirmacion('warning', 'Archivos no cargados', 'Algunos archivos no se cargaron. ¿Desea continuar de todos modos?');
454
+ if (!result.isConfirmed)
455
+ return [];
456
+ }
457
+ else {
458
+ return [];
459
+ }
460
+ }
461
+ return this.fileList()
462
+ .filter(f => f.servFile)
463
+ .map(f => f.servFile);
464
+ }
465
+ /** Subida automática individual (modo temporal). */
466
+ uploadFileTemporal(entry) {
467
+ const uploadFn = this.uploadFn();
468
+ if (!uploadFn) {
469
+ console.warn('[JvsFileUpload] uploadFn no proporcionado. No se puede subir en modo temporal.');
470
+ return;
471
+ }
472
+ const carpeta = this.carpetaSubida() ??
473
+ 'temp/' + formatDate(new Date(), 'yyyy-MM-dd_HH', 'en');
474
+ const disk = this.diskSubida();
475
+ this._subirEntrada(entry, uploadFn, carpeta, false, disk).catch(() => { });
476
+ }
477
+ /** Lógica central de subida de una entrada. */
478
+ _subirEntrada(entry, uploadFn, carpeta, anonimo, disk) {
479
+ const formData = new FormData();
480
+ if (this.nombreArchivoFijo()) {
481
+ formData.append('nombreArchivo', this.nombreArchivoFijo());
482
+ }
483
+ formData.append('archivo', entry.file);
484
+ formData.append('carpeta', carpeta);
485
+ if (disk)
486
+ formData.append('disk', disk);
487
+ this.fileList.update(list => list.map(f => f === entry ? { ...f, inProgress: true } : f));
488
+ this._onValidatorChange();
489
+ return lastValueFrom(uploadFn(formData, anonimo).pipe(map(event => {
490
+ switch (event.type) {
491
+ case HttpEventType.UploadProgress: {
492
+ const loaded = event.loaded ?? 0;
493
+ const total = event.total ?? 1;
494
+ const progress = Math.round(loaded * 100 / total);
495
+ this.fileList.update(list => list.map(f => f === entry ? { ...f, progress } : f));
496
+ break;
497
+ }
498
+ case HttpEventType.Response: {
499
+ const res = event;
500
+ if (res.status === 200) {
501
+ // Backend returns a plain path string (e.g. "temp/2024-01-15_14/file.pdf").
502
+ // Normalize to JvsArchivoServidor so downstream logic (download, remove) works uniformly.
503
+ const servFile = typeof res.body === 'string'
504
+ ? { path: res.body }
505
+ : res.body;
506
+ this.fileList.update(list => list.map(f => f === entry
507
+ ? { ...f, servFile, progress: 100, inProgress: false }
508
+ : f));
509
+ this._notifyChange();
510
+ }
511
+ break;
512
+ }
513
+ }
514
+ }), catchError(err => {
515
+ const mensaje = err?.error?.message ?? err?.message ?? 'Error al subir';
516
+ mensajeToast('error', 'Error', `No se pudo subir <strong>${entry.file?.name}</strong>: ${mensaje}`);
517
+ this.fileList.update(list => list.map(f => f === entry
518
+ ? { ...f, inProgress: false, progress: 0, errorSubida: true }
519
+ : f));
520
+ this._onValidatorChange();
521
+ return of(null);
522
+ }))).then(() => { });
523
+ }
524
+ // ── Eliminación ───────────────────────────────────────────────────────────
525
+ async onEliminar(entry) {
526
+ if (!this.permitirEliminar())
527
+ return;
528
+ if (!this.fileList().includes(entry))
529
+ return;
530
+ const remover = () => {
531
+ this.fileList.update(list => list.filter(f => f !== entry));
532
+ this._notifyChange();
533
+ this.resultadoEliminado.emit(this.fileList().filter(f => f.servFile).map(f => f.servFile));
534
+ };
535
+ if (entry.servFile) {
536
+ const removeFn = this.removeFn();
537
+ if (removeFn) {
538
+ // Archivo confirmado en servidor → confirmar y eliminar via API
539
+ const result = await mensajeConfirmacion('warning', 'Eliminar archivo', 'Se eliminará definitivamente este archivo. Esta acción no se puede deshacer.');
540
+ if (!result.isConfirmed)
541
+ return;
542
+ try {
543
+ await removeFn({ servFile: entry.servFile });
544
+ remover();
545
+ }
546
+ catch {
547
+ mensajeToast('error', 'Error', 'No se pudo eliminar el archivo del servidor.');
548
+ }
549
+ }
550
+ else {
551
+ // No hay removeFn: solo elimina de la lista local
552
+ remover();
553
+ }
554
+ }
555
+ else {
556
+ // Archivo local aún no subido → eliminar sin confirmación
557
+ remover();
558
+ }
559
+ }
560
+ // ── Métodos públicos ──────────────────────────────────────────────────────
561
+ /** Limpia toda la lista de archivos. */
562
+ reset() {
563
+ this.fileList.set([]);
564
+ this.invalidFiles.set([]);
565
+ this._notifyChange();
566
+ }
567
+ // ── Helpers ───────────────────────────────────────────────────────────────
568
+ _notifyChange() {
569
+ this.onTouched();
570
+ if (this.temporal()) {
571
+ this.onChange(this.fileList().map(f => f.servFile).filter(Boolean));
572
+ }
573
+ else {
574
+ // Server-loaded entries (desdeServidor=true) have a mock {name} object, not a real File.
575
+ // The parent only needs the real local File objects to upload later via uploadFilesPreForm().
576
+ this.onChange(this.fileList().filter(f => !f.desdeServidor).map(f => f.file));
577
+ }
578
+ }
579
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: JvsFileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
580
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: JvsFileUploadComponent, isStandalone: true, selector: "jvs-file-upload", inputs: { temporal: { classPropertyName: "temporal", publicName: "temporal", isSignal: true, isRequired: false, transformFunction: null }, permitirEliminar: { classPropertyName: "permitirEliminar", publicName: "permitirEliminar", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, extensionesPermitidas: { classPropertyName: "extensionesPermitidas", publicName: "extensionesPermitidas", isSignal: true, isRequired: false, transformFunction: null }, parteDeNombre: { classPropertyName: "parteDeNombre", publicName: "parteDeNombre", isSignal: true, isRequired: false, transformFunction: null }, parteDeNombreExclusivo: { classPropertyName: "parteDeNombreExclusivo", publicName: "parteDeNombreExclusivo", isSignal: true, isRequired: false, transformFunction: null }, tamanoMaximoMB: { classPropertyName: "tamanoMaximoMB", publicName: "tamanoMaximoMB", isSignal: true, isRequired: false, transformFunction: null }, nombreArchivoFijo: { classPropertyName: "nombreArchivoFijo", publicName: "nombreArchivoFijo", isSignal: true, isRequired: false, transformFunction: null }, carpetaSubida: { classPropertyName: "carpetaSubida", publicName: "carpetaSubida", isSignal: true, isRequired: false, transformFunction: null }, diskSubida: { classPropertyName: "diskSubida", publicName: "diskSubida", isSignal: true, isRequired: false, transformFunction: null }, uploadFn: { classPropertyName: "uploadFn", publicName: "uploadFn", isSignal: true, isRequired: false, transformFunction: null }, removeFn: { classPropertyName: "removeFn", publicName: "removeFn", isSignal: true, isRequired: false, transformFunction: null }, cssContenedorAgregados: { classPropertyName: "cssContenedorAgregados", publicName: "cssContenedorAgregados", isSignal: true, isRequired: false, transformFunction: null }, cssContenedorAgregadosLista: { classPropertyName: "cssContenedorAgregadosLista", publicName: "cssContenedorAgregadosLista", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { resultadoEliminado: "resultadoEliminado", archivoDescarga: "archivoDescarga", firmarArchivo: "firmarArchivo" }, providers: [
581
+ {
582
+ provide: NG_VALUE_ACCESSOR,
583
+ useExisting: forwardRef(() => JvsFileUploadComponent),
584
+ multi: true,
585
+ },
586
+ {
587
+ provide: NG_VALIDATORS,
588
+ useExisting: forwardRef(() => JvsFileUploadComponent),
589
+ multi: true,
590
+ },
591
+ ], ngImport: i0, template: "<!-- Solo lista: modo readonly -->\n@if (readonly()) {\n <ng-container [ngTemplateOutlet]=\"listaTemporal\" />\n}\n\n<!-- Input oculto de selecci\u00F3n de archivo -->\n<input\n #controlFile\n #fileInput\n hidden\n type=\"file\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onUploadChange(fileInput.files)\"\n/>\n\n<!-- Zona de drop (visible cuando NO es readonly) -->\n@if (!readonly()) {\n <div\n class=\"jvs-dropzone\"\n jvsFileUpload\n [controlFile]=\"controlFile\"\n [extensionesPermitidas]=\"extensionesNormalizadas()\"\n [parteDeNombre]=\"parteDeNombre()\"\n [tamanoMaximoMB]=\"tamanoMaximoMB()\"\n [parteDeNombreExclusivo]=\"parteDeNombreExclusivo()\"\n [isDisabled]=\"isDisabled()\"\n (filesChange)=\"onFilesChange($event)\"\n (filesInvalidChange)=\"invalidFiles.set($event)\"\n [class.jvs-dropzone--disabled]=\"isDisabled()\"\n >\n\n <!-- Instrucciones de selecci\u00F3n -->\n @if (!isDisabled()) {\n <div class=\"jvs-dropzone__prompt\" (click)=\"fileInput.click()\">\n <mat-icon svgIcon=\"roundCloudUpload\" class=\"jvs-dropzone__icon\" />\n <span class=\"jvs-dropzone__label\">\n Haz click o arrastra {{ multiple() ? 'archivos' : 'un archivo' }} aqu\u00ED\n </span>\n\n @if (tamanoMaximoMB()) {\n <small class=\"jvs-dropzone__hint\">Tama\u00F1o m\u00E1ximo: {{ tamanoMaximoMB() }} MB</small>\n }\n\n <small class=\"jvs-dropzone__hint\">{{ textoExtensiones() }}</small>\n\n <!-- Slot para mensajes de validaci\u00F3n del form -->\n <small class=\"jvs-dropzone__error\">\n <ng-content select=\"[validator]\" />\n </small>\n <ng-content select=\"[mensajeExtra]\" />\n </div>\n }\n\n <!-- Lista de archivos temporales (con progreso) -->\n <ng-container [ngTemplateOutlet]=\"listaTemporal\" />\n\n <!-- Lista simple: archivos v\u00E1lidos/inv\u00E1lidos (modo no-temporal) -->\n @if (!temporal()) {\n <div class=\"jvs-dropzone__simple-list\" [ngClass]=\"cssContenedorAgregadosLista()\">\n @if (fileList().length > 0) {\n <mat-list dense>\n <h4 class=\"jvs-list-title jvs-list-title--valid\">Archivos v\u00E1lidos</h4>\n @for (file of fileList(); track file.file?.name) {\n <mat-list-item>\n <mat-icon matListItemIcon svgIcon=\"roundDone\" class=\"icon-valid\" />\n <span matListItemTitle [title]=\"file.file?.name ?? ''\">\n {{ file.file?.name }}\n </span>\n </mat-list-item>\n }\n </mat-list>\n }\n\n @if (invalidFiles().length > 0) {\n <mat-divider />\n <mat-list dense>\n <h4 class=\"jvs-list-title jvs-list-title--invalid\">Archivos inv\u00E1lidos</h4>\n @for (file of invalidFiles(); track file.name) {\n <mat-list-item>\n <mat-icon matListItemIcon svgIcon=\"roundCancel\" class=\"icon-invalid\" />\n <span matListItemTitle [title]=\"file.name\">{{ file.name }}</span>\n </mat-list-item>\n }\n </mat-list>\n\n <p class=\"jvs-dropzone__warning\">\n <mat-icon svgIcon=\"roundWarning\" class=\"icon-warning\" />\n Solo se enviar\u00E1n los archivos\n <mat-icon svgIcon=\"roundDone\" class=\"icon-valid-inline\" />\n v\u00E1lidos.\n </p>\n }\n </div>\n }\n\n </div>\n}\n\n<!-- \u2500\u2500 Template: lista temporal con progreso \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #listaTemporal>\n @if (temporal() && fileList().length > 0) {\n <ul class=\"jvs-file-list\" [ngClass]=\"cssContenedorAgregados()\">\n @for (entry of fileList(); track entry.file?.name ?? entry.servFile?.id) {\n <li class=\"jvs-file-list__item\">\n <jvs-file-upload-item\n [file]=\"entry\"\n [permitirEliminar]=\"permitirEliminar()\"\n [isDisabled]=\"isDisabled()\"\n (eliminar)=\"onEliminar($event)\"\n (descargar)=\"archivoDescarga.emit($event)\"\n (firmar)=\"firmarArchivo.emit($event)\"\n />\n </li>\n }\n </ul>\n }\n</ng-template>\n", styles: [":host{display:block;width:100%}.jvs-dropzone{display:flex;flex-direction:column;align-items:center;min-height:60px;min-width:200px;width:100%;border:1.5px dashed #9ca3af;border-radius:.5rem;cursor:pointer;padding:.25rem .25rem .5rem;transition:border-color .2s,background .2s;box-sizing:border-box}.jvs-dropzone:hover:not(.jvs-dropzone--disabled){border-color:var(--mat-sys-primary, #6366f1)}.jvs-dropzone--disabled{cursor:default;opacity:.6;border-style:dotted}.jvs-dropzone__prompt{display:flex;flex-direction:column;align-items:center;text-align:center;padding:.5rem;gap:.125rem;color:#6b7280}.jvs-dropzone__icon{font-size:1.75rem;width:1.75rem;height:1.75rem;color:#9ca3af}.jvs-dropzone__label{font-size:.8rem;font-weight:500;color:#374151}.jvs-dropzone__hint{font-size:.65rem;color:#9ca3af;font-style:italic}.jvs-dropzone__error{font-size:.65rem;color:#ef4444;font-style:italic;min-height:.875rem}.jvs-dropzone__simple-list{display:flex;flex-direction:column;align-items:center;width:100%;max-height:200px;overflow-y:auto;color:#374151}.jvs-dropzone__simple-list mat-list{width:100%}.jvs-dropzone__warning{font-size:.7rem;text-align:center;display:flex;align-items:center;gap:.25rem;margin:.25rem 0 0}.jvs-list-title{font-size:.7rem;font-weight:700;margin:.25rem .5rem}.jvs-list-title--valid{color:#16a34a}.jvs-list-title--invalid{color:#dc2626}.icon-valid{color:#16a34a;font-size:1rem!important;width:1rem!important;height:1rem!important}.icon-invalid{color:#dc2626;font-size:1rem!important;width:1rem!important;height:1rem!important}.icon-warning{color:#f59e0b;font-size:.875rem!important;width:.875rem!important;height:.875rem!important}.icon-valid-inline{color:#16a34a;font-size:.875rem!important;width:.875rem!important;height:.875rem!important}.jvs-file-list{list-style:none;margin:.25rem 0 0;padding:0;width:100%;max-height:200px;overflow-y:auto;display:flex;flex-direction:column;gap:0}.jvs-file-list__item{border-top:1px solid #f3f4f6}.jvs-file-list__item:first-child{border-top:none}::ng-deep .mdc-list-item{height:auto!important}::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start{margin-inline:.25rem .25rem}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i1$1.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i3$1.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i3$1.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "directive", type: i3$1.MatListItemIcon, selector: "[matListItemIcon]" }, { kind: "directive", type: i3$1.MatListItemTitle, selector: "[matListItemTitle]" }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: JvsFileUploadDirective, selector: "[jvsFileUpload]", inputs: ["fondoInicial", "fondoDragOver", "controlFile", "extensionesPermitidas", "parteDeNombre", "parteDeNombreExclusivo", "tamanoMaximoMB", "isDisabled"], outputs: ["filesChange", "filesInvalidChange"] }, { kind: "component", type: JvsFileUploadItemComponent, selector: "jvs-file-upload-item", inputs: ["file", "permitirEliminar", "isDisabled"], outputs: ["eliminar", "descargar", "firmar"] }] });
592
+ }
593
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: JvsFileUploadComponent, decorators: [{
594
+ type: Component,
595
+ args: [{ selector: 'jvs-file-upload', standalone: true, imports: [
596
+ NgClass,
597
+ NgTemplateOutlet,
598
+ ReactiveFormsModule,
599
+ MatDividerModule,
600
+ MatIconModule,
601
+ MatListModule,
602
+ MatTooltipModule,
603
+ JvsFileUploadDirective,
604
+ JvsFileUploadItemComponent,
605
+ ], providers: [
606
+ {
607
+ provide: NG_VALUE_ACCESSOR,
608
+ useExisting: forwardRef(() => JvsFileUploadComponent),
609
+ multi: true,
610
+ },
611
+ {
612
+ provide: NG_VALIDATORS,
613
+ useExisting: forwardRef(() => JvsFileUploadComponent),
614
+ multi: true,
615
+ },
616
+ ], template: "<!-- Solo lista: modo readonly -->\n@if (readonly()) {\n <ng-container [ngTemplateOutlet]=\"listaTemporal\" />\n}\n\n<!-- Input oculto de selecci\u00F3n de archivo -->\n<input\n #controlFile\n #fileInput\n hidden\n type=\"file\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onUploadChange(fileInput.files)\"\n/>\n\n<!-- Zona de drop (visible cuando NO es readonly) -->\n@if (!readonly()) {\n <div\n class=\"jvs-dropzone\"\n jvsFileUpload\n [controlFile]=\"controlFile\"\n [extensionesPermitidas]=\"extensionesNormalizadas()\"\n [parteDeNombre]=\"parteDeNombre()\"\n [tamanoMaximoMB]=\"tamanoMaximoMB()\"\n [parteDeNombreExclusivo]=\"parteDeNombreExclusivo()\"\n [isDisabled]=\"isDisabled()\"\n (filesChange)=\"onFilesChange($event)\"\n (filesInvalidChange)=\"invalidFiles.set($event)\"\n [class.jvs-dropzone--disabled]=\"isDisabled()\"\n >\n\n <!-- Instrucciones de selecci\u00F3n -->\n @if (!isDisabled()) {\n <div class=\"jvs-dropzone__prompt\" (click)=\"fileInput.click()\">\n <mat-icon svgIcon=\"roundCloudUpload\" class=\"jvs-dropzone__icon\" />\n <span class=\"jvs-dropzone__label\">\n Haz click o arrastra {{ multiple() ? 'archivos' : 'un archivo' }} aqu\u00ED\n </span>\n\n @if (tamanoMaximoMB()) {\n <small class=\"jvs-dropzone__hint\">Tama\u00F1o m\u00E1ximo: {{ tamanoMaximoMB() }} MB</small>\n }\n\n <small class=\"jvs-dropzone__hint\">{{ textoExtensiones() }}</small>\n\n <!-- Slot para mensajes de validaci\u00F3n del form -->\n <small class=\"jvs-dropzone__error\">\n <ng-content select=\"[validator]\" />\n </small>\n <ng-content select=\"[mensajeExtra]\" />\n </div>\n }\n\n <!-- Lista de archivos temporales (con progreso) -->\n <ng-container [ngTemplateOutlet]=\"listaTemporal\" />\n\n <!-- Lista simple: archivos v\u00E1lidos/inv\u00E1lidos (modo no-temporal) -->\n @if (!temporal()) {\n <div class=\"jvs-dropzone__simple-list\" [ngClass]=\"cssContenedorAgregadosLista()\">\n @if (fileList().length > 0) {\n <mat-list dense>\n <h4 class=\"jvs-list-title jvs-list-title--valid\">Archivos v\u00E1lidos</h4>\n @for (file of fileList(); track file.file?.name) {\n <mat-list-item>\n <mat-icon matListItemIcon svgIcon=\"roundDone\" class=\"icon-valid\" />\n <span matListItemTitle [title]=\"file.file?.name ?? ''\">\n {{ file.file?.name }}\n </span>\n </mat-list-item>\n }\n </mat-list>\n }\n\n @if (invalidFiles().length > 0) {\n <mat-divider />\n <mat-list dense>\n <h4 class=\"jvs-list-title jvs-list-title--invalid\">Archivos inv\u00E1lidos</h4>\n @for (file of invalidFiles(); track file.name) {\n <mat-list-item>\n <mat-icon matListItemIcon svgIcon=\"roundCancel\" class=\"icon-invalid\" />\n <span matListItemTitle [title]=\"file.name\">{{ file.name }}</span>\n </mat-list-item>\n }\n </mat-list>\n\n <p class=\"jvs-dropzone__warning\">\n <mat-icon svgIcon=\"roundWarning\" class=\"icon-warning\" />\n Solo se enviar\u00E1n los archivos\n <mat-icon svgIcon=\"roundDone\" class=\"icon-valid-inline\" />\n v\u00E1lidos.\n </p>\n }\n </div>\n }\n\n </div>\n}\n\n<!-- \u2500\u2500 Template: lista temporal con progreso \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #listaTemporal>\n @if (temporal() && fileList().length > 0) {\n <ul class=\"jvs-file-list\" [ngClass]=\"cssContenedorAgregados()\">\n @for (entry of fileList(); track entry.file?.name ?? entry.servFile?.id) {\n <li class=\"jvs-file-list__item\">\n <jvs-file-upload-item\n [file]=\"entry\"\n [permitirEliminar]=\"permitirEliminar()\"\n [isDisabled]=\"isDisabled()\"\n (eliminar)=\"onEliminar($event)\"\n (descargar)=\"archivoDescarga.emit($event)\"\n (firmar)=\"firmarArchivo.emit($event)\"\n />\n </li>\n }\n </ul>\n }\n</ng-template>\n", styles: [":host{display:block;width:100%}.jvs-dropzone{display:flex;flex-direction:column;align-items:center;min-height:60px;min-width:200px;width:100%;border:1.5px dashed #9ca3af;border-radius:.5rem;cursor:pointer;padding:.25rem .25rem .5rem;transition:border-color .2s,background .2s;box-sizing:border-box}.jvs-dropzone:hover:not(.jvs-dropzone--disabled){border-color:var(--mat-sys-primary, #6366f1)}.jvs-dropzone--disabled{cursor:default;opacity:.6;border-style:dotted}.jvs-dropzone__prompt{display:flex;flex-direction:column;align-items:center;text-align:center;padding:.5rem;gap:.125rem;color:#6b7280}.jvs-dropzone__icon{font-size:1.75rem;width:1.75rem;height:1.75rem;color:#9ca3af}.jvs-dropzone__label{font-size:.8rem;font-weight:500;color:#374151}.jvs-dropzone__hint{font-size:.65rem;color:#9ca3af;font-style:italic}.jvs-dropzone__error{font-size:.65rem;color:#ef4444;font-style:italic;min-height:.875rem}.jvs-dropzone__simple-list{display:flex;flex-direction:column;align-items:center;width:100%;max-height:200px;overflow-y:auto;color:#374151}.jvs-dropzone__simple-list mat-list{width:100%}.jvs-dropzone__warning{font-size:.7rem;text-align:center;display:flex;align-items:center;gap:.25rem;margin:.25rem 0 0}.jvs-list-title{font-size:.7rem;font-weight:700;margin:.25rem .5rem}.jvs-list-title--valid{color:#16a34a}.jvs-list-title--invalid{color:#dc2626}.icon-valid{color:#16a34a;font-size:1rem!important;width:1rem!important;height:1rem!important}.icon-invalid{color:#dc2626;font-size:1rem!important;width:1rem!important;height:1rem!important}.icon-warning{color:#f59e0b;font-size:.875rem!important;width:.875rem!important;height:.875rem!important}.icon-valid-inline{color:#16a34a;font-size:.875rem!important;width:.875rem!important;height:.875rem!important}.jvs-file-list{list-style:none;margin:.25rem 0 0;padding:0;width:100%;max-height:200px;overflow-y:auto;display:flex;flex-direction:column;gap:0}.jvs-file-list__item{border-top:1px solid #f3f4f6}.jvs-file-list__item:first-child{border-top:none}::ng-deep .mdc-list-item{height:auto!important}::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start{margin-inline:.25rem .25rem}\n"] }]
617
+ }] });
618
+
619
+ /**
620
+ * Generated bundle index. Do not edit.
621
+ */
622
+
623
+ export { JvsFileUploadComponent, JvsFileUploadDirective, JvsFileUploadItemComponent };
624
+ //# sourceMappingURL=jvsoft-mat-form-controls-jvs-file-upload.mjs.map