ngx-dsxlibrary 2.21.70 → 2.21.72

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.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, forwardRef, Component, EventEmitter, effect, Output, viewChild, signal, Input, model, inject, computed, Injectable, isDevMode, InjectionToken, output, HostBinding, ChangeDetectorRef, untracked, Pipe, ViewEncapsulation, HostListener, Directive, NgZone, ElementRef, ChangeDetectionStrategy, NgModule, Injector, ViewChild } from '@angular/core';
2
+ import { input, forwardRef, Component, EventEmitter, effect, Output, viewChild, signal, Input, model, inject, computed, Injectable, isDevMode, InjectionToken, output, HostBinding, ChangeDetectorRef, untracked, ViewEncapsulation, HostListener, Directive, NgZone, ElementRef, ChangeDetectionStrategy, NgModule, Pipe, Injector, ViewChild } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormsModule, NG_VALUE_ACCESSOR, AbstractControl, FormControl, NG_VALIDATORS, FormBuilder, Validators, ReactiveFormsModule, NgControl, FormGroup } from '@angular/forms';
5
5
  import * as i2 from 'primeng/togglebutton';
6
6
  import { ToggleButtonModule } from 'primeng/togglebutton';
7
7
  import * as i1$1 from '@angular/common';
8
- import { CommonModule, AsyncPipe, DecimalPipe, Location, JsonPipe } from '@angular/common';
8
+ import { CommonModule, AsyncPipe, JsonPipe, DatePipe, DecimalPipe, Location } from '@angular/common';
9
9
  import * as i2$1 from 'primeng/tree';
10
10
  import { TreeModule } from 'primeng/tree';
11
11
  import * as i3 from 'primeng/api';
@@ -60,6 +60,7 @@ import { DrawerModule } from 'primeng/drawer';
60
60
  import { FieldsetModule } from 'primeng/fieldset';
61
61
  import { IconFieldModule } from 'primeng/iconfield';
62
62
  import { InputIconModule } from 'primeng/inputicon';
63
+ import * as i1$8 from 'primeng/inputmask';
63
64
  import { InputMaskModule } from 'primeng/inputmask';
64
65
  import { InputNumberModule, InputNumber } from 'primeng/inputnumber';
65
66
  import { InputTextModule } from 'primeng/inputtext';
@@ -2184,6 +2185,181 @@ function provideEnvironment(environment) {
2184
2185
  };
2185
2186
  }
2186
2187
 
2188
+ /**
2189
+ * action-message.model.ts
2190
+ *
2191
+ * Centraliza los tipos de acción y la configuración de mensajes para acciones de botones y operaciones comunes en la UI.
2192
+ *
2193
+ * - Define los tipos de acción estándar usados en dsx-button y otros componentes.
2194
+ * - Permite mantener uniformidad y evitar errores por typos en los tipos de acción.
2195
+ * - Provee la función getActionMessageConfig para obtener los textos, títulos e íconos de confirmación según la acción y el estado.
2196
+ * - Si se usa una acción válida pero sin mensaje configurado, en modo desarrollo muestra un warning para facilitar el mantenimiento.
2197
+ *
2198
+ * Uso recomendado:
2199
+ * - Importa ACTION_TYPES y ActionType donde se requiera validar o tipar acciones de botones.
2200
+ * - Usa getActionMessageConfig para obtener la configuración de confirmación antes de ejecutar acciones destructivas o de restauración.
2201
+ *
2202
+ * Ejemplo:
2203
+ * import { ACTION_TYPES, ActionType, getActionMessageConfig } from '.../action-message.model';
2204
+ * const config = getActionMessageConfig('softDelete', true);
2205
+ * // config: { title, message, icon }
2206
+ *
2207
+ * Si agregas un nuevo tipo de acción, recuerda añadir su mensaje en getActionMessageConfig.
2208
+ */
2209
+ /**
2210
+ * Tipos de acción disponibles para el componente `dsx-button`.
2211
+ *
2212
+ * Usa `ACTION_TYPES` en el consumidor para evitar typos.
2213
+ *
2214
+ * **Persistencia de datos (formulario)**
2215
+ * - `create` → Navega a la ruta de nuevo registro (azul)
2216
+ * - `saveOrUpdate` → Muestra "Guardar" si `id` es 0, "Actualizar" si `id` > 0 (amarillo)
2217
+ *
2218
+ * **Eliminar**
2219
+ * - `delete` → Eliminar registro permanentemente (rojo)
2220
+ * - `softDelete` → Borrar registro con borrado lógico (amarillo)
2221
+ *
2222
+ * **Formulario**
2223
+ * - `validate` → Validar formulario sin guardar (gris)
2224
+ * - `restore` → Restaurar datos originales (gris)
2225
+ *
2226
+ * **Navegación / Utilidades**
2227
+ * - `home` → Ir al inicio (gris)
2228
+ * - `refresh` → Recargar datos desde el servidor (morado)
2229
+ *
2230
+ * **Consulta / Descarga**
2231
+ * - `details` → Ver el detalle de un registro (azul índigo)
2232
+ * - `view` → Previsualizar o abrir detalle (azul cielo)
2233
+ * - `export` → Exportar información a Excel (verde Excel)
2234
+ * - `download` → Descargar archivo o recurso (ámbar cálido)
2235
+ * - `import` → Importar datos desde archivo (azul acero)
2236
+ */
2237
+ const ACTION_TYPES = {
2238
+ hardDelete: 'hardDelete',
2239
+ softDelete: 'softDelete',
2240
+ return: 'return',
2241
+ home: 'home',
2242
+ validate: 'validate',
2243
+ restore: 'restore',
2244
+ edit: 'edit',
2245
+ create: 'create',
2246
+ save: 'save',
2247
+ update: 'update',
2248
+ refresh: 'refresh',
2249
+ details: 'details',
2250
+ view: 'view',
2251
+ export: 'export',
2252
+ download: 'download',
2253
+ import: 'import',
2254
+ };
2255
+ function getActionMessageConfig(action, secondArg, thirdArg) {
2256
+ const isActive = action === 'softDelete' || action === 'hardDelete'
2257
+ ? typeof secondArg === 'boolean'
2258
+ ? secondArg
2259
+ : true
2260
+ : true;
2261
+ const dataPreview = action === 'softDelete' || action === 'hardDelete'
2262
+ ? thirdArg
2263
+ : typeof secondArg === 'string'
2264
+ ? secondArg
2265
+ : undefined;
2266
+ // Valores por defecto para todas las propiedades
2267
+ const defaults = {
2268
+ title: '',
2269
+ message: '',
2270
+ icon: undefined,
2271
+ confirmButtonText: undefined,
2272
+ cancelButtonText: undefined,
2273
+ showConfirmButton: undefined,
2274
+ showCancelButton: undefined,
2275
+ };
2276
+ if (action === 'softDelete') {
2277
+ if (isActive) {
2278
+ return {
2279
+ ...defaults,
2280
+ title: '¿Borrar registro?',
2281
+ message: 'El registro dejará de estar disponible en las listas principales.' +
2282
+ (dataPreview ? `<br>${dataPreview}` : ''),
2283
+ icono: 'icon/trash-bin.png',
2284
+ confirmButtonText: 'Eliminar',
2285
+ cancelButtonText: 'Cancelar',
2286
+ showConfirmButton: true,
2287
+ showCancelButton: true,
2288
+ };
2289
+ }
2290
+ else {
2291
+ return {
2292
+ ...defaults,
2293
+ title: '¿Restaurar registro?',
2294
+ message: 'El registro volverá a estar <strong>activo</strong> y visible en todas las listas principales del sistema.' +
2295
+ (dataPreview ? `<br>${dataPreview}` : ''),
2296
+ icono: 'icon/documentation.png',
2297
+ confirmButtonText: 'Sí, restaurar',
2298
+ cancelButtonText: 'Dejar archivado',
2299
+ showConfirmButton: true,
2300
+ showCancelButton: true,
2301
+ };
2302
+ }
2303
+ }
2304
+ if (action === 'hardDelete') {
2305
+ return {
2306
+ ...defaults,
2307
+ title: '¿Eliminar permanentemente?',
2308
+ message: 'Esta acción <strong>no se puede deshacer</strong>. Se perderán todos los datos asociados a este registro de forma inmediata.' +
2309
+ (dataPreview ? `<br>${dataPreview}` : ''),
2310
+ icono: 'icon/delete.png',
2311
+ confirmButtonText: 'Eliminar permanentemente',
2312
+ cancelButtonText: 'Cancelar',
2313
+ showConfirmButton: true,
2314
+ showCancelButton: true,
2315
+ };
2316
+ }
2317
+ if (action === 'save') {
2318
+ return {
2319
+ ...defaults,
2320
+ title: '¿Guardar registro?',
2321
+ message: 'El nuevo registro se guardará en el sistema.' +
2322
+ (dataPreview ? `<br>${dataPreview}` : ''),
2323
+ icono: 'icon2/save_2029637.png',
2324
+ confirmButtonText: 'Guardar',
2325
+ cancelButtonText: 'Cancelar',
2326
+ showConfirmButton: true,
2327
+ showCancelButton: true,
2328
+ };
2329
+ }
2330
+ if (action === 'update') {
2331
+ return {
2332
+ ...defaults,
2333
+ title: '¿Actualizar registro?',
2334
+ message: 'Los cambios realizados se actualizarán en el sistema.' +
2335
+ (dataPreview ? `<br>${dataPreview}` : ''),
2336
+ icono: 'icon2/save_2029637.png',
2337
+ confirmButtonText: 'Actualizar',
2338
+ cancelButtonText: 'Cancelar',
2339
+ showConfirmButton: true,
2340
+ showCancelButton: true,
2341
+ };
2342
+ }
2343
+ if (action === 'restore') {
2344
+ return {
2345
+ ...defaults,
2346
+ title: '¿Revertir cambios?',
2347
+ message: 'Se perderán todos los cambios que hayas realizado en el formulario y se volverán a cargar los datos originales.',
2348
+ icono: 'icon/data-recovery.png',
2349
+ confirmButtonText: 'Restaurar',
2350
+ cancelButtonText: 'Cancelar',
2351
+ showConfirmButton: true,
2352
+ showCancelButton: true,
2353
+ };
2354
+ }
2355
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
2356
+ console.warn(`[getActionMessageConfig] Acción "${action}" existe pero no tiene mensaje configurado. ` +
2357
+ 'Agrega la configuración correspondiente para evitar este warning.');
2358
+ }
2359
+ // Retorna los valores por defecto si la acción no es válida
2360
+ return { ...defaults };
2361
+ }
2362
+
2187
2363
  const INITIAL_PARAMETERS = new InjectionToken('InitialParameters');
2188
2364
 
2189
2365
  /**
@@ -4103,222 +4279,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
4103
4279
  }] });
4104
4280
 
4105
4281
  /**
4106
- * action-message.model.ts
4107
- *
4108
- * Centraliza los tipos de acción y la configuración de mensajes para acciones de botones y operaciones comunes en la UI.
4109
- *
4110
- * - Define los tipos de acción estándar usados en dsx-button y otros componentes.
4111
- * - Permite mantener uniformidad y evitar errores por typos en los tipos de acción.
4112
- * - Provee la función getActionMessageConfig para obtener los textos, títulos e íconos de confirmación según la acción y el estado.
4113
- * - Si se usa una acción válida pero sin mensaje configurado, en modo desarrollo muestra un warning para facilitar el mantenimiento.
4114
- *
4115
- * Uso recomendado:
4116
- * - Importa ACTION_TYPES y ActionType donde se requiera validar o tipar acciones de botones.
4117
- * - Usa getActionMessageConfig para obtener la configuración de confirmación antes de ejecutar acciones destructivas o de restauración.
4118
- *
4119
- * Ejemplo:
4120
- * import { ACTION_TYPES, ActionType, getActionMessageConfig } from '.../action-message.model';
4121
- * const config = getActionMessageConfig('softDelete', true);
4122
- * // config: { title, message, icon }
4123
- *
4124
- * Si agregas un nuevo tipo de acción, recuerda añadir su mensaje en getActionMessageConfig.
4125
- */
4126
- /**
4127
- * Tipos de acción disponibles para el componente `dsx-button`.
4128
- *
4129
- * Usa `ACTION_TYPES` en el consumidor para evitar typos.
4130
- *
4131
- * **Persistencia de datos (formulario)**
4132
- * - `create` → Navega a la ruta de nuevo registro (azul)
4133
- * - `saveOrUpdate` → Muestra "Guardar" si `id` es 0, "Actualizar" si `id` > 0 (amarillo)
4134
- *
4135
- * **Eliminar**
4136
- * - `delete` → Eliminar registro permanentemente (rojo)
4137
- * - `softDelete` → Borrar registro con borrado lógico (amarillo)
4138
- *
4139
- * **Formulario**
4140
- * - `validate` → Validar formulario sin guardar (gris)
4141
- * - `restore` → Restaurar datos originales (gris)
4282
+ * Paleta corporativa de colores para `dsx-button`.
4142
4283
  *
4143
- * **Navegación / Utilidades**
4144
- * - `home` Ir al inicio (gris)
4145
- * - `refresh` → Recargar datos desde el servidor (morado)
4284
+ * Cada token tiene un significado semántico fijo. Úsala como referencia
4285
+ * para asignar `colorToken` al añadir nuevos tipos de botón.
4146
4286
  *
4147
- * **Consulta / Descarga**
4148
- * - `details` → Ver el detalle de un registro (azul índigo)
4149
- * - `view` Previsualizar o abrir detalle (azul cielo)
4150
- * - `export` Exportar información a Excel (verde Excel)
4151
- * - `download` Descargar archivo o recurso (ámbar cálido)
4152
- * - `import` Importar datos desde archivo (azul acero)
4153
- */
4154
- const ACTION_TYPES = {
4155
- hardDelete: 'hardDelete',
4156
- softDelete: 'softDelete',
4157
- return: 'return',
4158
- home: 'home',
4159
- validate: 'validate',
4160
- restore: 'restore',
4161
- edit: 'edit',
4162
- create: 'create',
4163
- save: 'save',
4164
- update: 'update',
4165
- refresh: 'refresh',
4166
- details: 'details',
4167
- view: 'view',
4168
- export: 'export',
4169
- download: 'download',
4170
- import: 'import',
4171
- };
4172
- function getActionMessageConfig(action, secondArg, thirdArg) {
4173
- const isActive = action === 'softDelete' || action === 'hardDelete'
4174
- ? typeof secondArg === 'boolean'
4175
- ? secondArg
4176
- : true
4177
- : true;
4178
- const dataPreview = action === 'softDelete' || action === 'hardDelete'
4179
- ? thirdArg
4180
- : typeof secondArg === 'string'
4181
- ? secondArg
4182
- : undefined;
4183
- // Valores por defecto para todas las propiedades
4184
- const defaults = {
4185
- title: '',
4186
- message: '',
4187
- icon: undefined,
4188
- confirmButtonText: undefined,
4189
- cancelButtonText: undefined,
4190
- showConfirmButton: undefined,
4191
- showCancelButton: undefined,
4192
- };
4193
- if (action === 'softDelete') {
4194
- if (isActive) {
4195
- return {
4196
- ...defaults,
4197
- title: '¿Borrar registro?',
4198
- message: 'El registro dejará de estar disponible en las listas principales.' +
4199
- (dataPreview ? `<br>${dataPreview}` : ''),
4200
- icono: 'icon/trash-bin.png',
4201
- confirmButtonText: 'Eliminar',
4202
- cancelButtonText: 'Cancelar',
4203
- showConfirmButton: true,
4204
- showCancelButton: true,
4205
- };
4206
- }
4207
- else {
4208
- return {
4209
- ...defaults,
4210
- title: '¿Restaurar registro?',
4211
- message: 'El registro volverá a estar <strong>activo</strong> y visible en todas las listas principales del sistema.' +
4212
- (dataPreview ? `<br>${dataPreview}` : ''),
4213
- icono: 'icon/documentation.png',
4214
- confirmButtonText: 'Sí, restaurar',
4215
- cancelButtonText: 'Dejar archivado',
4216
- showConfirmButton: true,
4217
- showCancelButton: true,
4218
- };
4219
- }
4220
- }
4221
- if (action === 'hardDelete') {
4222
- return {
4223
- ...defaults,
4224
- title: '¿Eliminar permanentemente?',
4225
- message: 'Esta acción <strong>no se puede deshacer</strong>. Se perderán todos los datos asociados a este registro de forma inmediata.' +
4226
- (dataPreview ? `<br>${dataPreview}` : ''),
4227
- icono: 'icon/delete.png',
4228
- confirmButtonText: 'Eliminar permanentemente',
4229
- cancelButtonText: 'Cancelar',
4230
- showConfirmButton: true,
4231
- showCancelButton: true,
4232
- };
4233
- }
4234
- if (action === 'save') {
4235
- return {
4236
- ...defaults,
4237
- title: '¿Guardar registro?',
4238
- message: 'El nuevo registro se guardará en el sistema.' +
4239
- (dataPreview ? `<br>${dataPreview}` : ''),
4240
- icono: 'icon2/save_2029637.png',
4241
- confirmButtonText: 'Guardar',
4242
- cancelButtonText: 'Cancelar',
4243
- showConfirmButton: true,
4244
- showCancelButton: true,
4245
- };
4246
- }
4247
- if (action === 'update') {
4248
- return {
4249
- ...defaults,
4250
- title: '¿Actualizar registro?',
4251
- message: 'Los cambios realizados se actualizarán en el sistema.' +
4252
- (dataPreview ? `<br>${dataPreview}` : ''),
4253
- icono: 'icon2/save_2029637.png',
4254
- confirmButtonText: 'Actualizar',
4255
- cancelButtonText: 'Cancelar',
4256
- showConfirmButton: true,
4257
- showCancelButton: true,
4258
- };
4259
- }
4260
- if (action === 'restore') {
4261
- return {
4262
- ...defaults,
4263
- title: '¿Revertir cambios?',
4264
- message: 'Se perderán todos los cambios que hayas realizado en el formulario y se volverán a cargar los datos originales.',
4265
- icono: 'icon/data-recovery.png',
4266
- confirmButtonText: 'Restaurar',
4267
- cancelButtonText: 'Cancelar',
4268
- showConfirmButton: true,
4269
- showCancelButton: true,
4270
- };
4271
- }
4272
- if (typeof ngDevMode !== 'undefined' && ngDevMode) {
4273
- console.warn(`[getActionMessageConfig] Acción "${action}" existe pero no tiene mensaje configurado. ` +
4274
- 'Agrega la configuración correspondiente para evitar este warning.');
4275
- }
4276
- // Retorna los valores por defecto si la acción no es válida
4277
- return { ...defaults };
4278
- }
4279
-
4280
- /**
4281
- * Paleta corporativa de colores para `dsx-button`.
4282
- *
4283
- * Cada token tiene un significado semántico fijo. Úsala como referencia
4284
- * para asignar `colorToken` al añadir nuevos tipos de botón.
4285
- *
4286
- * | Token | Color | Uso semántico |
4287
- * |-----------|----------------|---------------------------------------------------|
4288
- * | danger | Coral rojo | Acción destructiva e irreversible (eliminar) |
4289
- * | warning | Naranja cálido | Acción con riesgo moderado / borrado lógico |
4290
- * | caution | Ámbar dorado | Requiere verificación del usuario (actualizar) |
4291
- * | success | Verde menta | Guardar nuevo registro / agregar |
4292
- * | primary | Azul claro | Editar / navegar con contexto de registro |
4293
- * | info | Cian suave | Recargar datos / refrescar vista |
4294
- * | neutral | Gris azulado | Acción secundaria / navegación sin contexto |
4295
- * | restore | Violeta suave | Deshacer / restaurar estado anterior |
4296
- * | view | Teal profundo | Ver / previsualizar detalle |
4297
- * | excel | Verde Excel | Exportar información a hoja de cálculo |
4298
- * | download | Naranja quemado| Descargar archivo o recurso |
4299
- * | import | Azul acero | Importar información desde archivo |
4300
- * | details | Azul índigo | Abrir detalle de registro |
4287
+ * | Token | Color | Uso semántico |
4288
+ * |-----------|----------------|---------------------------------------------------|
4289
+ * | danger | Coral rojo | Acción destructiva e irreversible (eliminar) |
4290
+ * | warning | Naranja cálido | Acción con riesgo moderado / borrado lógico |
4291
+ * | caution | Ámbar dorado | Requiere verificación del usuario (actualizar) |
4292
+ * | success | Verde menta | Guardar nuevo registro / agregar |
4293
+ * | primary | Azul claro | Editar / navegar con contexto de registro |
4294
+ * | info | Cian suave | Recargar datos / refrescar vista |
4295
+ * | neutral | Gris azulado | Acción secundaria / navegación sin contexto |
4296
+ * | restore | Violeta suave | Deshacer / restaurar estado anterior |
4297
+ * | view | Teal profundo | Ver / previsualizar detalle |
4298
+ * | excel | Verde Excel | Exportar información a hoja de cálculo |
4299
+ * | download | Naranja quemado| Descargar archivo o recurso |
4300
+ * | import | Azul acero | Importar información desde archivo |
4301
+ * | details | Azul índigo | Abrir detalle de registro |
4301
4302
  */
4302
4303
  const DSX_PALETTE = {
4303
4304
  danger: '#EF5350',
4304
4305
  warning: '#FF7043',
4305
4306
  caution: '#FFB300',
4306
- success: '#0ba055', // Verde para Guardar
4307
- primary: '#0066cc', // Azul para Actualizar
4307
+ success: '#0ba055',
4308
+ primary: '#0066cc',
4308
4309
  info: '#26C6DA',
4309
4310
  neutral: '#90A4AE',
4310
- restore: '#B0BEC5', // Gris suave, menos resaltado
4311
- return: '#11438d', // Azul violeta para "Regresar registro archivado"
4311
+ restore: '#B0BEC5',
4312
+ return: '#11438d',
4312
4313
  view: '#00838F',
4313
4314
  excel: '#217346',
4314
4315
  download: '#E65100',
4315
4316
  import: '#546E7A',
4316
4317
  details: '#3949AB',
4317
- magenta: '#D81B60', // Magenta vibrante
4318
- cyan: '#00BCD4', // Cian brillante
4319
- olive: '#808000', // Verde oliva
4320
- brown: '#8D6E63', // Marrón suave
4321
- slate: '#607D8B', // Azul pizarra
4318
+ magenta: '#D81B60',
4319
+ cyan: '#00BCD4',
4320
+ olive: '#808000',
4321
+ brown: '#8D6E63',
4322
+ slate: '#607D8B',
4322
4323
  };
4323
4324
  /** Tipos que necesitan un `[id]` mayor a 0 para poder mostrarse. */
4324
4325
  const TYPES_REQUIRING_ID = [
@@ -4331,23 +4332,56 @@ class DsxButtonComponent {
4331
4332
  renderCount = 0;
4332
4333
  _paramService = inject((ParameterValuesService));
4333
4334
  _router = inject(Router);
4334
- // Agrega un input para el tamaño
4335
+ _isDev = isDevMode();
4336
+ lastValidationState = null;
4335
4337
  size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4336
4338
  _environment = inject(ENVIRONMENT, {
4337
4339
  optional: true,
4338
4340
  });
4339
4341
  constructor() {
4340
- if (isDevMode()) {
4342
+ // Solo activar efectos de debug en desarrollo
4343
+ if (this._isDev) {
4344
+ // Efecto principal que captura todo el estado
4341
4345
  effect(() => {
4342
4346
  if (!this.debug())
4343
4347
  return;
4344
4348
  this.renderCount++;
4345
- console.log(`[dsx-button:${this.type()}] render #${this.renderCount}`, {
4346
- id: this.id(),
4347
- type: this.type(),
4348
- disabled: this.disabled(),
4349
- routerLink: this.routerLink(),
4350
- });
4349
+ this.logFullState();
4350
+ });
4351
+ // Efecto específico para validaciones
4352
+ effect(() => {
4353
+ if (!this.debug())
4354
+ return;
4355
+ const visible = this.visible();
4356
+ const validations = {
4357
+ global: this.passesGlobalVisibilityPolicy(),
4358
+ parameter: this.passesParameterValidation(),
4359
+ id: this.passesIdValidation(),
4360
+ visible,
4361
+ };
4362
+ const stateKey = JSON.stringify(validations);
4363
+ if (stateKey !== this.lastValidationState) {
4364
+ this.lastValidationState = stateKey;
4365
+ this.logValidationIssues(validations);
4366
+ }
4367
+ });
4368
+ // Efecto para detectar problemas con IDs
4369
+ effect(() => {
4370
+ if (!this.debug())
4371
+ return;
4372
+ const type = this.type();
4373
+ if (TYPES_REQUIRING_ID.includes(type)) {
4374
+ const id = this.getNormalizedId();
4375
+ const routeId = this.getIdFromNavigationTarget(this.effectiveRouterLink());
4376
+ if (!id && !routeId) {
4377
+ console.warn(`%c⚠️ [dsx-button:${type}] ID no disponible para acción que lo requiere`, 'color:#ed6c02;font-weight:bold', {
4378
+ type,
4379
+ explicitId: this.id(),
4380
+ routeId,
4381
+ requireIdInput: this.requireIdInput(),
4382
+ });
4383
+ }
4384
+ }
4351
4385
  });
4352
4386
  }
4353
4387
  }
@@ -4357,17 +4391,7 @@ class DsxButtonComponent {
4357
4391
  type = input(ACTION_TYPES.hardDelete, ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
4358
4392
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4359
4393
  routerLink = input(undefined, ...(ngDevMode ? [{ debugName: "routerLink" }] : /* istanbul ignore next */ []));
4360
- /**
4361
- * Ruta de navegación simplificada. La `/` inicial se agrega automáticamente.
4362
- * Cada segmento solo admite: letras, números, `-`, `_`, `.` y `:` (params de ruta).
4363
- * Ejemplos válidos: `'home'`, `'container/nuevo'`, `'item/123'`
4364
- * Ejemplos inválidos: `'item/*'`, `'item?foo=bar'`, `'item/../other'`
4365
- * Para rutas dinámicas complejas usa `[routerLink]` directamente.
4366
- */
4367
4394
  routerPath = input(undefined, ...(ngDevMode ? [{ debugName: "routerPath" }] : /* istanbul ignore next */ []));
4368
- /** Ancho del botón. Usa `rem` para que escale con la preferencia de fuente del usuario.
4369
- * Ejemplo: `'8rem'`, `'12rem'`. Valor por defecto: `'9rem'`.
4370
- */
4371
4395
  width = input('9rem', ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
4372
4396
  iconOnly = input(false, ...(ngDevMode ? [{ debugName: "iconOnly" }] : /* istanbul ignore next */ []));
4373
4397
  iconOnlyWidth = input('2.5rem', ...(ngDevMode ? [{ debugName: "iconOnlyWidth" }] : /* istanbul ignore next */ []));
@@ -4383,24 +4407,12 @@ class DsxButtonComponent {
4383
4407
  tooltipOverride = input(undefined, ...(ngDevMode ? [{ debugName: "tooltipOverride" }] : /* istanbul ignore next */ []));
4384
4408
  iconOverride = input(undefined, ...(ngDevMode ? [{ debugName: "iconOverride" }] : /* istanbul ignore next */ []));
4385
4409
  primeIconOverride = input(undefined, ...(ngDevMode ? [{ debugName: "primeIconOverride" }] : /* istanbul ignore next */ []));
4386
- /** Permite personalizar el color usando únicamente tokens de la paleta DSX. */
4387
4410
  colorOverride = input(undefined, ...(ngDevMode ? [{ debugName: "colorOverride" }] : /* istanbul ignore next */ []));
4388
- /** Permite forzar estilo outlined o sólido por instancia. */
4389
4411
  outlinedOverride = input(undefined, ...(ngDevMode ? [{ debugName: "outlinedOverride" }] : /* istanbul ignore next */ []));
4390
- /**
4391
- * Override por instancia para el botón `validate`.
4392
- * Tiene prioridad sobre `environment.showValidateButton`.
4393
- */
4394
4412
  showValidateButtonOverride = input(undefined, ...(ngDevMode ? [{ debugName: "showValidateButtonOverride" }] : /* istanbul ignore next */ []));
4395
- /**
4396
- * Override por instancia para el botón `restore`.
4397
- * Tiene prioridad sobre `environment.showRestoreButton`.
4398
- */
4399
4413
  showRestoreButtonOverride = input(undefined, ...(ngDevMode ? [{ debugName: "showRestoreButtonOverride" }] : /* istanbul ignore next */ []));
4400
4414
  action = output();
4401
- /** Valida y normaliza `routerPath`: agrega `/` inicial y rechaza caracteres inválidos. */
4402
4415
  resolvedRouterLink = computed(() => {
4403
- this.log('computed resolvedRouterLink');
4404
4416
  const raw = this.routerPath();
4405
4417
  if (!raw)
4406
4418
  return undefined;
@@ -4409,23 +4421,22 @@ class DsxButtonComponent {
4409
4421
  const rawSegments = normalizedRaw.split('/');
4410
4422
  const hasEmptySegments = normalizedRaw.length > 0 && rawSegments.some((s) => s.length === 0);
4411
4423
  if (hasEmptySegments) {
4412
- console.warn(`[dsx-button] routerPath contiene segmentos vacíos ("//" o "/" al final). ` +
4413
- `Ruta recibida: "${raw}"`);
4424
+ if (this._isDev && this.debug()) {
4425
+ console.warn(`[dsx-button] routerPath contiene segmentos vacíos: "${raw}"`);
4426
+ }
4414
4427
  return undefined;
4415
4428
  }
4416
4429
  const segments = rawSegments.filter(Boolean);
4417
4430
  const invalid = segments.filter((s) => !VALID_SEGMENT.test(s));
4418
4431
  if (invalid.length > 0) {
4419
- console.warn(`[dsx-button] routerPath contiene segmentos inválidos: "${invalid.join('", "')}". ` +
4420
- `Solo se permiten letras, números, guion (-), guion bajo (_), punto (.) y dos puntos (:). ` +
4421
- `Ruta recibida: "${raw}"`);
4432
+ if (this._isDev && this.debug()) {
4433
+ console.warn(`[dsx-button] routerPath contiene segmentos inválidos: "${invalid.join('", "')}"`);
4434
+ }
4422
4435
  return undefined;
4423
4436
  }
4424
4437
  return `/${segments.join('/')}`;
4425
4438
  }, ...(ngDevMode ? [{ debugName: "resolvedRouterLink" }] : /* istanbul ignore next */ []));
4426
- /** Prioriza routerPath (validado) sobre routerLink (libre). */
4427
4439
  effectiveRouterLink = computed(() => {
4428
- this.log('computed effectiveRouterLink');
4429
4440
  const resolvedPath = this.resolvedRouterLink();
4430
4441
  if (resolvedPath) {
4431
4442
  return resolvedPath;
@@ -4438,7 +4449,6 @@ class DsxButtonComponent {
4438
4449
  if (!trimmed) {
4439
4450
  return link;
4440
4451
  }
4441
- // URLs externas no se normalizan ni validan como rutas internas.
4442
4452
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed)) {
4443
4453
  return link;
4444
4454
  }
@@ -4448,20 +4458,20 @@ class DsxButtonComponent {
4448
4458
  const hasEmptySegments = pathForValidation.length > 0 &&
4449
4459
  rawSegments.some((segment) => segment.length === 0);
4450
4460
  if (hasEmptySegments) {
4451
- console.warn(`[dsx-button] routerLink contiene segmentos vacíos ("//" o "/" al final). ` +
4452
- `Ruta recibida: "${link}"`);
4461
+ if (this._isDev && this.debug()) {
4462
+ console.warn(`[dsx-button] routerLink contiene segmentos vacíos: "${link}"`);
4463
+ }
4453
4464
  return link;
4454
4465
  }
4455
4466
  const invalid = rawSegments
4456
4467
  .filter(Boolean)
4457
4468
  .filter((segment) => !VALID_SEGMENT.test(segment));
4458
4469
  if (invalid.length > 0) {
4459
- console.warn(`[dsx-button] routerLink contiene segmentos inválidos: "${invalid.join('", "')}". ` +
4460
- `Solo se permiten letras, números, guion (-), guion bajo (_), punto (.) y dos puntos (:). ` +
4461
- `Ruta recibida: "${link}"`);
4470
+ if (this._isDev && this.debug()) {
4471
+ console.warn(`[dsx-button] routerLink contiene segmentos inválidos: "${invalid.join('", "')}"`);
4472
+ }
4462
4473
  return link;
4463
4474
  }
4464
- // Si llega una ruta simple sin prefijo, se normaliza a absoluta para evitar navegación relativa inesperada.
4465
4475
  if (link.startsWith('/') ||
4466
4476
  link.startsWith('./') ||
4467
4477
  link.startsWith('../')) {
@@ -4472,23 +4482,6 @@ class DsxButtonComponent {
4472
4482
  }
4473
4483
  return `/${trimmed.split('/').filter(Boolean).join('/')}`;
4474
4484
  }, ...(ngDevMode ? [{ debugName: "effectiveRouterLink" }] : /* istanbul ignore next */ []));
4475
- // get buttonStyle(): Record<string, string> {
4476
- // this.log('getter buttonStyle');
4477
- // const color = DSX_PALETTE[this.config.colorToken];
4478
- // const width = this.iconOnly() ? this.iconOnlyWidth() : this.width();
4479
- // if (this.isCompactIconButton) {
4480
- // return {
4481
- // width,
4482
- // background: 'transparent',
4483
- // borderColor: 'transparent',
4484
- // color,
4485
- // };
4486
- // }
4487
- // if (this.config.outlined) {
4488
- // return { width, background: 'transparent', borderColor: color, color };
4489
- // }
4490
- // return { width, background: color, borderColor: color, color: '#ffffff' };
4491
- // }
4492
4485
  buttonStyleComputed = computed(() => {
4493
4486
  const color = DSX_PALETTE[this.config.colorToken];
4494
4487
  const width = this.iconOnly() ? this.iconOnlyWidth() : this.width();
@@ -4518,17 +4511,13 @@ class DsxButtonComponent {
4518
4511
  get buttonLabel() {
4519
4512
  return this.iconOnly() ? undefined : this.label;
4520
4513
  }
4521
- // Y modifica el getter buttonSize
4522
4514
  get buttonSize() {
4523
- // Si el usuario especificó un tamaño, úsalo
4524
4515
  if (this.size()) {
4525
4516
  return this.size();
4526
4517
  }
4527
- // Si es compacto, siempre small
4528
4518
  if (this.isCompactIconButton) {
4529
4519
  return 'small';
4530
4520
  }
4531
- // Por defecto, small para todos los botones
4532
4521
  return 'small';
4533
4522
  }
4534
4523
  get buttonStyleClass() {
@@ -4541,7 +4530,6 @@ class DsxButtonComponent {
4541
4530
  const cfg = ACTION_CONFIG[this.type()];
4542
4531
  const colorOverride = this.colorOverride();
4543
4532
  const outlinedOverride = this.outlinedOverride();
4544
- // Ya no se gestiona softDelete/return como modo, cada acción es independiente
4545
4533
  return {
4546
4534
  ...cfg,
4547
4535
  colorToken: colorOverride ?? cfg.colorToken,
@@ -4551,10 +4539,6 @@ class DsxButtonComponent {
4551
4539
  get label() {
4552
4540
  return this.labelOverride() ?? this.config.label;
4553
4541
  }
4554
- // get tooltip(): string {
4555
- // this.log('getter tooltip');
4556
- // return this.tooltipOverride() ?? this.config.tooltip;
4557
- // }
4558
4542
  tooltip = computed(() => {
4559
4543
  return this.tooltipOverride() ?? this.config.tooltip;
4560
4544
  }, ...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
@@ -4570,18 +4554,6 @@ class DsxButtonComponent {
4570
4554
  }
4571
4555
  return 'primeIcon' in this.config ? this.config.primeIcon : undefined;
4572
4556
  }
4573
- // showButton(): boolean {
4574
- // this.time('showButton');
4575
- // const result =
4576
- // this.passesGlobalVisibilityPolicy() &&
4577
- // this.passesParameterValidation() &&
4578
- // this.passesIdValidation();
4579
- // this.log('showButton()', {
4580
- // result,
4581
- // });
4582
- // this.timeEnd('showButton');
4583
- // return result;
4584
- // }
4585
4557
  visible = computed(() => {
4586
4558
  return (this.passesGlobalVisibilityPolicy() &&
4587
4559
  this.passesParameterValidation() &&
@@ -4606,45 +4578,44 @@ class DsxButtonComponent {
4606
4578
  return true;
4607
4579
  }
4608
4580
  async onButtonClick() {
4609
- this.log('click', {
4610
- id: this.id(),
4611
- target: this.effectiveRouterLink(),
4612
- });
4581
+ if (this._isDev && this.debug()) {
4582
+ console.group(`%c🖱️ [dsx-button:${this.type()}] Click`, 'color:#1976d2;font-weight:bold;font-size:13px');
4583
+ console.log('ID:', this.id());
4584
+ console.log('Target:', this.effectiveRouterLink());
4585
+ console.log('Disabled:', this.disabled());
4586
+ console.groupEnd();
4587
+ }
4613
4588
  this.action.emit();
4614
4589
  const target = this.effectiveRouterLink();
4615
4590
  if (target == null) {
4616
- if (this.type() === 'home') {
4617
- console.warn('[dsx-button] El botón tipo "home" no tiene routerLink ni routerPath configurado.');
4591
+ if (this.type() === 'home' && this._isDev && this.debug()) {
4592
+ console.warn(`[dsx-button:${this.type()}] No tiene routerLink ni routerPath configurado.`);
4618
4593
  }
4619
4594
  return;
4620
4595
  }
4621
4596
  try {
4622
4597
  const start = performance.now();
4623
4598
  const navigated = await this.navigateToTarget(target);
4624
- this.log('navigation', {
4625
- target,
4626
- navigated,
4627
- durationMs: Math.round(performance.now() - start),
4628
- });
4599
+ const duration = Math.round(performance.now() - start);
4600
+ if (this._isDev && this.debug()) {
4601
+ const emoji = navigated ? '✅' : '⚠️';
4602
+ const color = navigated ? '#2e7d32' : '#ed6c02';
4603
+ console.log(`%c${emoji} Navegación ${navigated ? 'exitosa' : 'rechazada'} (${duration}ms)`, `color:${color};font-weight:bold`, target);
4604
+ }
4629
4605
  if (!navigated) {
4630
- // El router rechazó la navegación, lo más común durante hot-reload
4631
- // es que un guard de autenticación aún no tiene el estado listo.
4632
- // Se reintenta una vez tras un tick para darle tiempo al guard.
4633
4606
  const retried = await new Promise((resolve) => setTimeout(async () => resolve(await this.navigateToTarget(target)), 0));
4634
- if (!retried) {
4635
- console.warn('[dsx-button] Navegación rechazada (posiblemente por un route guard). ' +
4636
- 'Si ocurre solo tras hot-reload, es normal que el guard rechace ' +
4637
- 'la primera navegación mientras los servicios de autenticación ' +
4638
- 'no han terminado de restaurar su estado.', target);
4607
+ if (!retried && this._isDev && this.debug()) {
4608
+ console.warn(`[dsx-button:${this.type()}] Navegación rechazada por route guard.`, target);
4639
4609
  }
4640
4610
  }
4641
4611
  }
4642
4612
  catch (error) {
4643
- console.warn('[dsx-button] Error al navegar desde el botón.', target, error);
4613
+ if (this._isDev && this.debug()) {
4614
+ console.error(`[dsx-button:${this.type()}] Error al navegar:`, target, error);
4615
+ }
4644
4616
  }
4645
4617
  }
4646
4618
  passesIdValidation() {
4647
- this.log('passesIdValidation');
4648
4619
  const explicitId = this.getNormalizedId();
4649
4620
  const routeId = this.getIdFromNavigationTarget(this.effectiveRouterLink());
4650
4621
  const id = explicitId ?? routeId;
@@ -4653,7 +4624,6 @@ class DsxButtonComponent {
4653
4624
  this.type() === 'softDelete' ||
4654
4625
  this.type() === 'restore' ||
4655
4626
  this.type() === 'return';
4656
- // Permite exigir que venga [id] cuando el caso de uso lo requiera.
4657
4627
  if (this.requireIdInput() && typeof id !== 'number') {
4658
4628
  return false;
4659
4629
  }
@@ -4713,16 +4683,6 @@ class DsxButtonComponent {
4713
4683
  }
4714
4684
  return undefined;
4715
4685
  }
4716
- hasExplicitIdInput() {
4717
- const value = this.id();
4718
- if (value === null || value === undefined) {
4719
- return false;
4720
- }
4721
- if (typeof value === 'string') {
4722
- return value.trim() !== '';
4723
- }
4724
- return true;
4725
- }
4726
4686
  navigateToTarget(target) {
4727
4687
  if (Array.isArray(target)) {
4728
4688
  return this._router.navigate(target);
@@ -4734,41 +4694,154 @@ class DsxButtonComponent {
4734
4694
  return this._router.navigateByUrl(target);
4735
4695
  }
4736
4696
  passesParameterValidation() {
4737
- this.log('passesParameterValidation');
4738
4697
  const parameterName = this.parameterName();
4739
- // Si no se define un parámetro de seguridad, el botón se muestra sin restricción.
4740
4698
  if (!parameterName) {
4741
4699
  return true;
4742
4700
  }
4743
- // Mantiene una UX estable durante refresh de parámetros: evita parpadeo
4744
- // y no evalúa permisos hasta que ParameterValuesService termine de cargar.
4745
4701
  if (!this._paramService.loaded) {
4746
4702
  return false;
4747
4703
  }
4748
- // Conserva el comportamiento original del servicio:
4749
- // - retorna false si no cumple el valor esperado
4750
- // - alerta si el parámetro no existe (ayuda a detectar typos)
4751
4704
  return this._paramService.isParameterValue(parameterName, this.parameterExpectedValue(), this.parameterIndex());
4752
4705
  }
4753
- log(message, data) {
4754
- if (!this.debug())
4706
+ // ============ MÉTODOS DE DEBUG ============
4707
+ logFullState() {
4708
+ if (!this._isDev || !this.debug())
4755
4709
  return;
4756
- console.log(`%c[dsx-button:${this.type()}] ${message}`, 'color:#1976d2;font-weight:bold', data ?? '');
4710
+ const id = this.getNormalizedId();
4711
+ const routeId = this.getIdFromNavigationTarget(this.effectiveRouterLink());
4712
+ const paramName = this.parameterName();
4713
+ const paramLoaded = this._paramService.loaded;
4714
+ const paramValue = paramName && paramLoaded
4715
+ ? this._paramService.getValue(paramName, this.parameterIndex())
4716
+ : undefined;
4717
+ console.group(`%c📊 [dsx-button:${this.type()}] Render #${this.renderCount}`, 'color:#1976d2;font-weight:bold;font-size:14px');
4718
+ // 1. Estado general
4719
+ console.log('🎯 Estado:', {
4720
+ visible: this.visible(),
4721
+ disabled: this.disabled(),
4722
+ type: this.type(),
4723
+ size: this.size(),
4724
+ iconOnly: this.iconOnly(),
4725
+ rounded: this.rounded(),
4726
+ isCompact: this.isCompactIconButton,
4727
+ });
4728
+ // 2. Configuración
4729
+ console.log('⚙️ Configuración:', {
4730
+ label: this.label,
4731
+ icon: this.icon,
4732
+ primeIcon: this.primeIcon,
4733
+ colorToken: this.config.colorToken,
4734
+ outlined: this.config.outlined,
4735
+ tooltip: this.tooltip(),
4736
+ });
4737
+ // 3. Router
4738
+ console.log('🔗 Router:', {
4739
+ routerLink: this.routerLink(),
4740
+ routerPath: this.routerPath(),
4741
+ resolvedRouterLink: this.resolvedRouterLink(),
4742
+ effectiveRouterLink: this.effectiveRouterLink(),
4743
+ });
4744
+ // 4. IDs
4745
+ console.log('🆔 IDs:', {
4746
+ inputId: this.id(),
4747
+ normalizedId: id,
4748
+ routeId: routeId,
4749
+ requireIdInput: this.requireIdInput(),
4750
+ requiresExistingRecord: this.requiresIdGreaterThanZero() ||
4751
+ ['hardDelete', 'softDelete', 'restore', 'return'].includes(this.type()),
4752
+ });
4753
+ // 5. Parámetros
4754
+ if (paramName) {
4755
+ console.log('🔐 Parámetros:', {
4756
+ name: paramName,
4757
+ expected: this.parameterExpectedValue(),
4758
+ index: this.parameterIndex(),
4759
+ loaded: paramLoaded,
4760
+ actual: paramValue,
4761
+ matches: paramLoaded
4762
+ ? paramValue === this.parameterExpectedValue()
4763
+ : 'No cargado',
4764
+ });
4765
+ }
4766
+ // 6. Overrides
4767
+ const overrides = {
4768
+ label: this.labelOverride(),
4769
+ tooltip: this.tooltipOverride(),
4770
+ icon: this.iconOverride(),
4771
+ primeIcon: this.primeIconOverride(),
4772
+ color: this.colorOverride(),
4773
+ outlined: this.outlinedOverride(),
4774
+ showValidate: this.showValidateButtonOverride(),
4775
+ showRestore: this.showRestoreButtonOverride(),
4776
+ };
4777
+ const activeOverrides = Object.entries(overrides)
4778
+ .filter(([_, value]) => value !== undefined)
4779
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
4780
+ if (Object.keys(activeOverrides).length > 0) {
4781
+ console.log('🔄 Overrides activos:', activeOverrides);
4782
+ }
4783
+ // 7. Validaciones
4784
+ console.log('✅ Validaciones:', {
4785
+ globalVisibility: this.passesGlobalVisibilityPolicy(),
4786
+ parameterValidation: this.passesParameterValidation(),
4787
+ idValidation: this.passesIdValidation(),
4788
+ visible: this.visible(),
4789
+ });
4790
+ // 8. Estilos
4791
+ console.log('🎨 Estilos:', {
4792
+ width: this.iconOnly() ? this.iconOnlyWidth() : this.width(),
4793
+ style: this.buttonStyleComputed(),
4794
+ styleClass: this.buttonStyleClass,
4795
+ size: this.buttonSize,
4796
+ });
4797
+ console.groupEnd();
4757
4798
  }
4758
- warn(message, data) {
4759
- if (!this.debug())
4799
+ logValidationIssues(validations) {
4800
+ if (!this._isDev || !this.debug())
4760
4801
  return;
4761
- console.warn(`[dsx-button:${this.type()}] ${message}`, data ?? '');
4802
+ if (!validations.visible) {
4803
+ console.group(`%c❌ [dsx-button:${this.type()}] NO VISIBLE`, 'color:#d32f2f;font-weight:bold;font-size:13px');
4804
+ console.log('Validaciones fallidas:', {
4805
+ globalVisibility: validations.global,
4806
+ parameterValidation: validations.parameter,
4807
+ idValidation: validations.id,
4808
+ });
4809
+ if (!validations.id) {
4810
+ const id = this.getNormalizedId();
4811
+ const routeId = this.getIdFromNavigationTarget(this.effectiveRouterLink());
4812
+ console.warn('⚠️ Problema con ID:', {
4813
+ explicitId: this.id(),
4814
+ normalizedId: id,
4815
+ routeId: routeId,
4816
+ requireIdInput: this.requireIdInput(),
4817
+ requiresExistingRecord: this.requiresIdGreaterThanZero() ||
4818
+ ['hardDelete', 'softDelete', 'restore', 'return'].includes(this.type()),
4819
+ });
4820
+ }
4821
+ if (!validations.parameter && this.parameterName()) {
4822
+ console.warn('⚠️ Problema con parámetro:', {
4823
+ name: this.parameterName(),
4824
+ expected: this.parameterExpectedValue(),
4825
+ index: this.parameterIndex(),
4826
+ loaded: this._paramService.loaded,
4827
+ actualValue: this._paramService.loaded
4828
+ ? this._paramService.getValue(this.parameterName(), this.parameterIndex())
4829
+ : 'No cargado',
4830
+ });
4831
+ }
4832
+ console.groupEnd();
4833
+ }
4762
4834
  }
4763
- time(label) {
4764
- if (!this.debug())
4835
+ // Métodos de log existentes mejorados
4836
+ log(message, data) {
4837
+ if (!this._isDev || !this.debug())
4765
4838
  return;
4766
- console.time(`[dsx-button:${this.type()}] ${label}`);
4839
+ console.log(`%c[dsx-button:${this.type()}] ${message}`, 'color:#1976d2;font-weight:bold', data ?? '');
4767
4840
  }
4768
- timeEnd(label) {
4769
- if (!this.debug())
4841
+ warn(message, data) {
4842
+ if (!this._isDev || !this.debug())
4770
4843
  return;
4771
- console.timeEnd(`[dsx-button:${this.type()}] ${label}`);
4844
+ console.warn(`[dsx-button:${this.type()}] ${message}`, data ?? '');
4772
4845
  }
4773
4846
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4774
4847
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DsxButtonComponent, isStandalone: true, selector: "dsx-button", inputs: { debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerPath: { classPropertyName: "routerPath", publicName: "routerPath", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null }, iconOnlyWidth: { classPropertyName: "iconOnlyWidth", publicName: "iconOnlyWidth", isSignal: true, isRequired: false, transformFunction: null }, raised: { classPropertyName: "raised", publicName: "raised", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, parameterName: { classPropertyName: "parameterName", publicName: "parameterName", isSignal: true, isRequired: false, transformFunction: null }, parameterExpectedValue: { classPropertyName: "parameterExpectedValue", publicName: "parameterExpectedValue", isSignal: true, isRequired: false, transformFunction: null }, parameterIndex: { classPropertyName: "parameterIndex", publicName: "parameterIndex", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, requireIdInput: { classPropertyName: "requireIdInput", publicName: "requireIdInput", isSignal: true, isRequired: false, transformFunction: null }, requiresIdGreaterThanZero: { classPropertyName: "requiresIdGreaterThanZero", publicName: "requiresIdGreaterThanZero", isSignal: true, isRequired: false, transformFunction: null }, labelOverride: { classPropertyName: "labelOverride", publicName: "labelOverride", isSignal: true, isRequired: false, transformFunction: null }, tooltipOverride: { classPropertyName: "tooltipOverride", publicName: "tooltipOverride", isSignal: true, isRequired: false, transformFunction: null }, iconOverride: { classPropertyName: "iconOverride", publicName: "iconOverride", isSignal: true, isRequired: false, transformFunction: null }, primeIconOverride: { classPropertyName: "primeIconOverride", publicName: "primeIconOverride", isSignal: true, isRequired: false, transformFunction: null }, colorOverride: { classPropertyName: "colorOverride", publicName: "colorOverride", isSignal: true, isRequired: false, transformFunction: null }, outlinedOverride: { classPropertyName: "outlinedOverride", publicName: "outlinedOverride", isSignal: true, isRequired: false, transformFunction: null }, showValidateButtonOverride: { classPropertyName: "showValidateButtonOverride", publicName: "showValidateButtonOverride", isSignal: true, isRequired: false, transformFunction: null }, showRestoreButtonOverride: { classPropertyName: "showRestoreButtonOverride", publicName: "showRestoreButtonOverride", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, host: { properties: { "class.dsx-button-compact-host": "this.compactHostClass" } }, ngImport: i0, template: "<!--Bot\u00F3n con las configuraciones din\u00E1micas para CRUD -->\r\n@if (visible()) {\r\n<p-button\r\n [label]=\"buttonLabel\"\r\n [style]=\"buttonStyleComputed()\"\r\n [styleClass]=\"buttonStyleClass\"\r\n [pTooltip]=\"tooltip()\"\r\n [tooltipDisabled]=\"disabled()\"\r\n tooltipPosition=\"top\"\r\n [rounded]=\"rounded()\"\r\n [text]=\"isCompactIconButton\"\r\n [size]=\"buttonSize\"\r\n severity=\"secondary\"\r\n [disabled]=\"disabled()\"\r\n [icon]=\"iconOnly() ? primeIcon : undefined\"\r\n (click)=\"onButtonClick()\"\r\n>\r\n @if (!iconOnly()) {\r\n <span class=\"material-symbols-outlined dsx-button-icon-small\"\r\n >{{ materialIcon }}</span\r\n >\r\n }\r\n</p-button>\r\n}\r\n", styles: [".dsx-button-icon-small{font-size:1.25rem;vertical-align:middle}:host{display:inline-flex;margin-bottom:.25rem}:host.dsx-button-compact-host{margin-bottom:0}:host ::ng-deep .p-button.dsx-button-compact{min-height:2rem;padding-block:.2rem}:host ::ng-deep .p-button .material-symbols-outlined.p-button-icon{line-height:1;vertical-align:middle}:host ::ng-deep .p-button.dsx-button-compact .p-button-icon{margin:0;font-size:1.2rem;line-height:1}:host ::ng-deep .p-button.dsx-button-compact .material-symbols-outlined.p-button-icon{font-variation-settings:\"FILL\" 1,\"wght\" 500,\"GRAD\" 0,\"opsz\" 24}:host ::ng-deep .p-button:disabled,:host ::ng-deep .p-button.p-disabled{cursor:not-allowed!important;opacity:.45!important;filter:grayscale(.2) saturate(.75)}:host ::ng-deep .p-button:disabled *,:host ::ng-deep .p-button.p-disabled *{cursor:not-allowed!important}.p-button.dsx-button-outlined-neutral,.p-button.dsx-button-outlined-info{background:#f5f7fa!important;border:1.5px solid #0066cc!important;color:#06c!important}.p-button.dsx-button-outlined-neutral:disabled,.p-button.dsx-button-outlined-info:disabled{background:#f5f7fa!important;border:1.5px solid #b0bec5!important;color:#b0bec5!important}\n"], dependencies: [{ kind: "component", type: Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$3.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }] });
@@ -4873,7 +4946,7 @@ const ACTION_CONFIG = {
4873
4946
  view: {
4874
4947
  label: 'Visualizar',
4875
4948
  icon: 'visibility',
4876
- primeIcon: 'pi pi-eye',
4949
+ primeIcon: 'fa-solid fa-eye',
4877
4950
  colorToken: 'view',
4878
4951
  tooltip: 'Vista previa',
4879
4952
  },
@@ -4906,7 +4979,6 @@ class FileComponent {
4906
4979
  existingFileName = input(null, ...(ngDevMode ? [{ debugName: "existingFileName" }] : /* istanbul ignore next */ []));
4907
4980
  overrideFileName = input(null, ...(ngDevMode ? [{ debugName: "overrideFileName" }] : /* istanbul ignore next */ []));
4908
4981
  debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : /* istanbul ignore next */ []));
4909
- required = input(true, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4910
4982
  accept = input('.xls,.xlsx', ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
4911
4983
  pTooltipOverride = input('Seleccione un archivo', ...(ngDevMode ? [{ debugName: "pTooltipOverride" }] : /* istanbul ignore next */ []));
4912
4984
  tooltipPositionOverride = input('top', ...(ngDevMode ? [{ debugName: "tooltipPositionOverride" }] : /* istanbul ignore next */ []));
@@ -4918,6 +4990,7 @@ class FileComponent {
4918
4990
  // Signals de estado
4919
4991
  isReplacing = signal(false, ...(ngDevMode ? [{ debugName: "isReplacing" }] : /* istanbul ignore next */ []));
4920
4992
  disabled = signal(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4993
+ errorMessage = signal(null, ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
4921
4994
  isViewActive = signal(false, ...(ngDevMode ? [{ debugName: "isViewActive" }] : /* istanbul ignore next */ []));
4922
4995
  value = null;
4923
4996
  touched = false;
@@ -4925,7 +4998,31 @@ class FileComponent {
4925
4998
  // Dependencias
4926
4999
  cdr = inject(ChangeDetectorRef);
4927
5000
  fileUpload = viewChild('fileUpload', ...(ngDevMode ? [{ debugName: "fileUpload" }] : /* istanbul ignore next */ []));
4928
- // Computed properties (solo se "activan" si la vista está activa)
5001
+ // --- Para el manejo de errores del formulario y debug ---
5002
+ _control = null;
5003
+ // Computed para el control de errores
5004
+ errorControl = computed(() => {
5005
+ return this._control;
5006
+ }, ...(ngDevMode ? [{ debugName: "errorControl" }] : /* istanbul ignore next */ []));
5007
+ // Signal para errores del control
5008
+ controlErrors = signal(null, ...(ngDevMode ? [{ debugName: "controlErrors" }] : /* istanbul ignore next */ []));
5009
+ // Estado del control
5010
+ controlValid = signal(true, ...(ngDevMode ? [{ debugName: "controlValid" }] : /* istanbul ignore next */ []));
5011
+ controlTouched = signal(false, ...(ngDevMode ? [{ debugName: "controlTouched" }] : /* istanbul ignore next */ []));
5012
+ controlDirty = signal(false, ...(ngDevMode ? [{ debugName: "controlDirty" }] : /* istanbul ignore next */ []));
5013
+ // --- Detección de modo desarrollo ---
5014
+ get isDevMode() {
5015
+ return (typeof ngDevMode !== 'undefined' &&
5016
+ ngDevMode !== null &&
5017
+ ngDevMode !== false);
5018
+ }
5019
+ // ✅ Método para detectar si el control es requerido
5020
+ isRequired() {
5021
+ if (!this._control)
5022
+ return false;
5023
+ return this._control.errors?.['required'] ?? false;
5024
+ }
5025
+ // Computed properties
4929
5026
  hasExistingFile = computed(() => {
4930
5027
  if (!this.isViewActive())
4931
5028
  return false;
@@ -4961,14 +5058,27 @@ class FileComponent {
4961
5058
  return true;
4962
5059
  return false;
4963
5060
  }, ...(ngDevMode ? [{ debugName: "isReplaceButtonDisabled" }] : /* istanbul ignore next */ []));
4964
- // --- Constructor: Aquí se crean los effects (contexto de inyección válido) ---
5061
+ allowedFileTypeLabel = computed(() => {
5062
+ if (!this.isViewActive())
5063
+ return 'archivo';
5064
+ const acceptStr = this.accept();
5065
+ if (!acceptStr)
5066
+ return 'archivo';
5067
+ const extensions = acceptStr.split(',').map((ext) => ext.trim());
5068
+ const cleanExtensions = extensions
5069
+ .map((ext) => ext.replace('.', '').toUpperCase())
5070
+ .filter((ext) => ext.length > 0);
5071
+ if (cleanExtensions.length === 0)
5072
+ return 'archivo';
5073
+ if (cleanExtensions.length === 1)
5074
+ return cleanExtensions[0];
5075
+ return cleanExtensions.join(' o ');
5076
+ }, ...(ngDevMode ? [{ debugName: "allowedFileTypeLabel" }] : /* istanbul ignore next */ []));
5077
+ // --- Constructor ---
4965
5078
  constructor() {
4966
- // Efecto 1: Re-evaluar validación cuando cambie el estado relevante
4967
5079
  effect(() => {
4968
- // Solo ejecutar si la vista está activa
4969
5080
  if (!this.isViewActive())
4970
5081
  return;
4971
- // "Tocar" estas signals para que el efecto dependa de ellas
4972
5082
  const hasFile = this.hasExistingFile();
4973
5083
  const replacing = this.isReplacing();
4974
5084
  const fileName = this.existingFileName();
@@ -4979,14 +5089,12 @@ class FileComponent {
4979
5089
  fileName,
4980
5090
  });
4981
5091
  if (this.control) {
4982
- // Usar untracked para no crear bucles infinitos
4983
5092
  untracked(() => {
4984
5093
  this.control?.updateValueAndValidity({ emitEvent: false });
4985
5094
  });
4986
5095
  }
4987
5096
  this.cdr.detectChanges();
4988
5097
  });
4989
- // Efecto 2: Cancelar reemplazo si el componente se deshabilita
4990
5098
  effect(() => {
4991
5099
  if (!this.isViewActive())
4992
5100
  return;
@@ -5000,104 +5108,341 @@ class FileComponent {
5000
5108
  });
5001
5109
  }
5002
5110
  ngAfterViewInit() {
5003
- // Activar el componente después de que la vista esté lista
5004
5111
  setTimeout(() => {
5005
5112
  this.isViewActive.set(true);
5006
- if (this.debug())
5007
- console.log('[FileComponent] View activated');
5113
+ if (this.debug()) {
5114
+ console.log('[FileComponent] View activated');
5115
+ console.log('[FileComponent] 📋 Configuración actual:', {
5116
+ accept: this.accept(),
5117
+ maxFileSize: this.maxFileSize(),
5118
+ debug: this.debug(),
5119
+ isDevMode: this.isDevMode,
5120
+ });
5121
+ }
5008
5122
  });
5009
5123
  }
5010
5124
  ngOnDestroy() {
5011
5125
  this.isViewActive.set(false);
5012
5126
  if (this.debug())
5013
- console.log('[FileComponent] Component destroyed');
5127
+ console.log('[FileComponent] 🗑️ Component destroyed');
5128
+ }
5129
+ // --- Método para validar archivo manualmente ---
5130
+ validateFile(file) {
5131
+ if (this.debug()) {
5132
+ this.log('🔍 validateFile - Iniciando validación manual', {
5133
+ fileName: file.name,
5134
+ fileSize: file.size,
5135
+ fileType: file.type,
5136
+ accept: this.accept(),
5137
+ maxSize: this.maxFileSize(),
5138
+ });
5139
+ }
5140
+ // Validar tipo de archivo
5141
+ const acceptStr = this.accept();
5142
+ if (acceptStr) {
5143
+ const acceptedExtensions = acceptStr
5144
+ .split(',')
5145
+ .map((ext) => ext.trim().toLowerCase());
5146
+ const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
5147
+ if (this.debug()) {
5148
+ this.log('🔍 validateFile - Verificando extensión', {
5149
+ fileExtension,
5150
+ acceptedExtensions,
5151
+ matches: acceptedExtensions.includes(fileExtension),
5152
+ });
5153
+ }
5154
+ if (!acceptedExtensions.includes(fileExtension)) {
5155
+ const detail = this.invalidDetail().replace('{0}', this.allowedFileTypeLabel());
5156
+ const message = `${this.invalidSummary()} ${detail}`;
5157
+ if (this.debug())
5158
+ this.log('❌ validateFile - Tipo de archivo no permitido', {
5159
+ message,
5160
+ });
5161
+ return { valid: false, error: message };
5162
+ }
5163
+ }
5164
+ // Validar tamaño
5165
+ const maxSizeBytes = this.maxFileSize() * 1024 * 1024;
5166
+ if (this.debug()) {
5167
+ this.log('🔍 validateFile - Verificando tamaño', {
5168
+ fileSize: file.size,
5169
+ maxSizeBytes,
5170
+ maxSizeMB: this.maxFileSize(),
5171
+ });
5172
+ }
5173
+ if (file.size > maxSizeBytes) {
5174
+ const detail = this.invalidSizeDetail().replace('{0}', `${this.maxFileSize()} MB`);
5175
+ const message = `${this.invalidSizeSummary()} ${detail}`;
5176
+ if (this.debug())
5177
+ this.log('❌ validateFile - Tamaño excedido', { message });
5178
+ return { valid: false, error: message };
5179
+ }
5180
+ if (this.debug())
5181
+ this.log('✅ validateFile - Archivo válido');
5182
+ return { valid: true };
5183
+ }
5184
+ // --- Métodos para el manejo de errores ---
5185
+ showError() {
5186
+ return !!(this._control && this._control.invalid && this._control.touched);
5187
+ }
5188
+ // Actualizar estado del control
5189
+ updateControlState() {
5190
+ if (this._control) {
5191
+ this.controlValid.set(this._control.valid);
5192
+ this.controlTouched.set(this._control.touched);
5193
+ this.controlDirty.set(this._control.dirty);
5194
+ this.controlErrors.set(this._control.errors);
5195
+ }
5014
5196
  }
5015
- // --- Métodos públicos y ControlValueAccessor ---
5197
+ // Forzar recarga (para debug)
5198
+ forceReload() {
5199
+ if (this.debug()) {
5200
+ console.log('[FileComponent] 🔄 Force reload');
5201
+ if (this._control) {
5202
+ this._control.updateValueAndValidity();
5203
+ this.updateControlState();
5204
+ }
5205
+ this.cdr.detectChanges();
5206
+ }
5207
+ }
5208
+ // --- Métodos públicos ---
5016
5209
  onSelect(event) {
5017
- if (!this.isViewActive())
5210
+ const startTime = performance.now();
5211
+ if (this.debug()) {
5212
+ this.log('📤 onSelect - Evento recibido', {
5213
+ files: event.files,
5214
+ currentFiles: event.currentFiles,
5215
+ isFileUploadEnabled: this.isFileUploadEnabled(),
5216
+ });
5217
+ }
5218
+ if (!this.isViewActive()) {
5219
+ if (this.debug())
5220
+ this.log('⚠️ Componente no activo, ignorando selección');
5018
5221
  return;
5222
+ }
5019
5223
  if (!this.isFileUploadEnabled()) {
5020
- console.warn('File upload is disabled');
5224
+ console.warn('[FileComponent] ⚠️ File upload is disabled');
5021
5225
  this.fileUpload()?.clear();
5022
5226
  return;
5023
5227
  }
5024
5228
  const file = event.files?.[0] ?? null;
5025
- if (this.debug())
5026
- this.log('File selected', { name: file?.name });
5027
- this.value = file;
5028
- this.onChange(file);
5229
+ if (!file) {
5230
+ if (this.debug())
5231
+ this.log('⚠️ No se seleccionó ningún archivo');
5232
+ return;
5233
+ }
5234
+ if (this.debug()) {
5235
+ this.log('📄 Archivo seleccionado', {
5236
+ name: file.name,
5237
+ size: file.size,
5238
+ type: file.type,
5239
+ sizeFormatted: this.formatFileSize(file.size),
5240
+ });
5241
+ }
5242
+ // Validar archivo manualmente
5243
+ const validation = this.validateFile(file);
5244
+ if (!validation.valid) {
5245
+ // Archivo inválido - mostrar error y limpiar
5246
+ this.errorMessage.set(validation.error || 'Archivo inválido');
5247
+ this.fileUpload()?.clear();
5248
+ this.value = null;
5249
+ this.onChange(null);
5250
+ if (this.debug()) {
5251
+ this.log('❌ Archivo inválido - Limpiando selección', {
5252
+ error: validation.error,
5253
+ });
5254
+ }
5255
+ // Auto-ocultar error después de 5 segundos
5256
+ setTimeout(() => {
5257
+ this.errorMessage.set(null);
5258
+ if (this.debug())
5259
+ this.log('⏰ Mensaje de error auto-ocultado después de 5s');
5260
+ }, 5000);
5261
+ // Actualizar estado del control
5262
+ this.updateControlState();
5263
+ return;
5264
+ }
5265
+ // Archivo válido
5266
+ this.errorMessage.set(null);
5267
+ this.value = file;
5268
+ this.onChange(file);
5029
5269
  this.markAsTouched();
5030
5270
  if (this.isReplacing() && file) {
5031
5271
  this.isReplacing.set(false);
5272
+ if (this.debug())
5273
+ this.log('🔄 Reemplazo completado');
5032
5274
  }
5033
5275
  if (this.control) {
5034
5276
  this.control.updateValueAndValidity();
5035
5277
  }
5278
+ // Actualizar estado del control
5279
+ this.updateControlState();
5280
+ const endTime = performance.now();
5281
+ if (this.debug()) {
5282
+ this.log(`⏱️ onSelect completado en ${(endTime - startTime).toFixed(2)}ms`);
5283
+ }
5036
5284
  }
5037
5285
  clear() {
5286
+ if (this.debug())
5287
+ this.log('🧹 clear() - Limpiando componente');
5038
5288
  if (!this.isViewActive())
5039
5289
  return;
5040
5290
  this.fileUpload()?.clear();
5041
5291
  this.value = null;
5292
+ this.errorMessage.set(null);
5042
5293
  this.onChange(null);
5043
5294
  this.markAsTouched();
5044
5295
  if (this.control)
5045
5296
  this.control.updateValueAndValidity();
5297
+ this.updateControlState();
5298
+ if (this.debug())
5299
+ this.log('✅ Limpieza completada');
5046
5300
  }
5047
- writeValue(value) {
5301
+ clearFile() {
5302
+ if (this.debug())
5303
+ this.log('🧹 clearFile() - Limpiando archivo seleccionado');
5048
5304
  if (!this.isViewActive())
5049
5305
  return;
5306
+ this.fileUpload()?.clear();
5307
+ this.value = null;
5308
+ this.errorMessage.set(null);
5309
+ this.onChange(null);
5310
+ this.markAsTouched();
5311
+ if (this.control)
5312
+ this.control.updateValueAndValidity();
5313
+ this.updateControlState();
5050
5314
  if (this.debug())
5051
- this.log('writeValue', { value: value?.name || null });
5315
+ this.log(' Archivo limpiado');
5316
+ }
5317
+ writeValue(value) {
5318
+ if (!this.isViewActive())
5319
+ return;
5320
+ if (this.debug()) {
5321
+ this.log('✍️ writeValue', {
5322
+ value: value?.name || null,
5323
+ size: value?.size ? this.formatFileSize(value.size) : null,
5324
+ });
5325
+ }
5052
5326
  this.value = value;
5053
5327
  }
5054
5328
  registerOnChange(fn) {
5329
+ if (this.debug())
5330
+ this.log('📝 registerOnChange');
5055
5331
  this.onChange = fn;
5056
5332
  }
5057
5333
  registerOnTouched(fn) {
5334
+ if (this.debug())
5335
+ this.log('📝 registerOnTouched');
5058
5336
  this.onTouched = fn;
5059
5337
  }
5060
5338
  setDisabledState(isDisabled) {
5061
5339
  if (!this.isViewActive())
5062
5340
  return;
5341
+ if (this.debug()) {
5342
+ this.log('🔒 setDisabledState', {
5343
+ isDisabled,
5344
+ previousState: this.disabled(),
5345
+ });
5346
+ }
5063
5347
  this.disabled.set(isDisabled);
5064
5348
  }
5065
5349
  startReplace() {
5350
+ if (this.debug())
5351
+ this.log('🔄 startReplace - Iniciando reemplazo');
5066
5352
  if (!this.isViewActive())
5067
5353
  return;
5068
- if (this.disabled())
5354
+ if (this.disabled()) {
5355
+ if (this.debug())
5356
+ this.log('⛔ Reemplazo cancelado - componente deshabilitado');
5069
5357
  return;
5358
+ }
5070
5359
  this.fileUpload()?.clear();
5071
5360
  this.value = null;
5361
+ this.errorMessage.set(null);
5072
5362
  this.onChange(null);
5073
5363
  this.isReplacing.set(true);
5074
5364
  if (this.debug())
5075
- this.log('startReplace');
5365
+ this.log('✅ Reemplazo iniciado');
5076
5366
  }
5077
5367
  cancelReplace() {
5368
+ if (this.debug())
5369
+ this.log('❌ cancelReplace - Cancelando reemplazo');
5078
5370
  if (!this.isViewActive())
5079
5371
  return;
5080
5372
  this.isReplacing.set(false);
5081
5373
  this.fileUpload()?.clear();
5082
5374
  this.value = null;
5375
+ this.errorMessage.set(null);
5083
5376
  this.onChange(null);
5084
5377
  this.markAsTouched();
5085
5378
  if (this.debug())
5086
- this.log('cancelReplace');
5379
+ this.log('✅ Reemplazo cancelado');
5380
+ }
5381
+ choose(event, chooseCallback) {
5382
+ if (this.debug())
5383
+ this.log('🔍 choose - Abriendo selector de archivos');
5384
+ chooseCallback();
5385
+ }
5386
+ formatFileSize(bytes) {
5387
+ if (!bytes)
5388
+ return '';
5389
+ const sizes = ['B', 'KB', 'MB', 'GB'];
5390
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
5391
+ const value = (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0);
5392
+ return `${value} ${sizes[i]}`;
5087
5393
  }
5088
5394
  // --- Validación ---
5089
5395
  validate(control) {
5396
+ const startTime = performance.now();
5397
+ this._control = control;
5090
5398
  this.control = control;
5091
- if (!this.isViewActive())
5399
+ if (!this.isViewActive()) {
5400
+ if (this.debug())
5401
+ this.log('⚠️ validate - Componente no activo');
5092
5402
  return null;
5093
- if (!this.required())
5403
+ }
5404
+ if (this.debug()) {
5405
+ this.log('🔍 validate - Ejecutando validación', {
5406
+ isRequired: this.isRequired(),
5407
+ hasExistingFile: this.hasExistingFile(),
5408
+ isReplacing: this.isReplacing(),
5409
+ hasValue: !!this.value,
5410
+ valueName: this.value?.name || null,
5411
+ errorMessage: this.errorMessage(),
5412
+ });
5413
+ }
5414
+ // Actualizar estado del control
5415
+ this.updateControlState();
5416
+ // Si hay un mensaje de error, consideramos que la validación falla
5417
+ if (this.errorMessage()) {
5418
+ if (this.debug())
5419
+ this.log('❌ validate - Error: Hay un mensaje de error activo');
5420
+ return { invalidFile: true };
5421
+ }
5422
+ if (!this.isRequired()) {
5423
+ if (this.debug())
5424
+ this.log('✅ validate - No requerido, pasa validación');
5094
5425
  return null;
5095
- if (this.hasExistingFile())
5426
+ }
5427
+ if (this.hasExistingFile()) {
5428
+ if (this.debug())
5429
+ this.log('✅ validate - Archivo existente, pasa validación');
5096
5430
  return null;
5097
- if (this.isReplacing() && !this.value)
5431
+ }
5432
+ if (this.isReplacing() && !this.value) {
5433
+ if (this.debug())
5434
+ this.log('❌ validate - Error: Reemplazando pero sin archivo');
5098
5435
  return { required: true };
5099
- if (!this.hasExistingFile() && !this.value)
5436
+ }
5437
+ if (!this.hasExistingFile() && !this.value) {
5438
+ if (this.debug())
5439
+ this.log('❌ validate - Error: Sin archivo seleccionado');
5100
5440
  return { required: true };
5441
+ }
5442
+ const endTime = performance.now();
5443
+ if (this.debug()) {
5444
+ this.log(`✅ validate - Validación exitosa en ${(endTime - startTime).toFixed(2)}ms`);
5445
+ }
5101
5446
  return null;
5102
5447
  }
5103
5448
  // --- Privados ---
@@ -5107,13 +5452,15 @@ class FileComponent {
5107
5452
  if (!this.touched) {
5108
5453
  this.touched = true;
5109
5454
  this.onTouched();
5455
+ if (this.debug())
5456
+ this.log('👆 markAsTouched - Componente marcado como touched');
5110
5457
  }
5111
5458
  }
5112
5459
  log(method, data) {
5113
5460
  console.log(`[FileComponent] ${method}`, data || '');
5114
5461
  }
5115
5462
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5116
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: FileComponent, isStandalone: true, selector: "dsx-file-upload", inputs: { existingFile: { classPropertyName: "existingFile", publicName: "existingFile", isSignal: true, isRequired: false, transformFunction: null }, existingFileName: { classPropertyName: "existingFileName", publicName: "existingFileName", isSignal: true, isRequired: false, transformFunction: null }, overrideFileName: { classPropertyName: "overrideFileName", publicName: "overrideFileName", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, pTooltipOverride: { classPropertyName: "pTooltipOverride", publicName: "pTooltipOverride", isSignal: true, isRequired: false, transformFunction: null }, tooltipPositionOverride: { classPropertyName: "tooltipPositionOverride", publicName: "tooltipPositionOverride", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, invalidSummary: { classPropertyName: "invalidSummary", publicName: "invalidSummary", isSignal: true, isRequired: false, transformFunction: null }, invalidDetail: { classPropertyName: "invalidDetail", publicName: "invalidDetail", isSignal: true, isRequired: false, transformFunction: null }, invalidSizeSummary: { classPropertyName: "invalidSizeSummary", publicName: "invalidSizeSummary", isSignal: true, isRequired: false, transformFunction: null }, invalidSizeDetail: { classPropertyName: "invalidSizeDetail", publicName: "invalidSizeDetail", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
5463
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: FileComponent, isStandalone: true, selector: "dsx-file-upload", inputs: { existingFile: { classPropertyName: "existingFile", publicName: "existingFile", isSignal: true, isRequired: false, transformFunction: null }, existingFileName: { classPropertyName: "existingFileName", publicName: "existingFileName", isSignal: true, isRequired: false, transformFunction: null }, overrideFileName: { classPropertyName: "overrideFileName", publicName: "overrideFileName", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, pTooltipOverride: { classPropertyName: "pTooltipOverride", publicName: "pTooltipOverride", isSignal: true, isRequired: false, transformFunction: null }, tooltipPositionOverride: { classPropertyName: "tooltipPositionOverride", publicName: "tooltipPositionOverride", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, invalidSummary: { classPropertyName: "invalidSummary", publicName: "invalidSummary", isSignal: true, isRequired: false, transformFunction: null }, invalidDetail: { classPropertyName: "invalidDetail", publicName: "invalidDetail", isSignal: true, isRequired: false, transformFunction: null }, invalidSizeSummary: { classPropertyName: "invalidSizeSummary", publicName: "invalidSizeSummary", isSignal: true, isRequired: false, transformFunction: null }, invalidSizeDetail: { classPropertyName: "invalidSizeDetail", publicName: "invalidSizeDetail", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
5117
5464
  {
5118
5465
  provide: NG_VALUE_ACCESSOR,
5119
5466
  useExisting: forwardRef(() => FileComponent),
@@ -5124,11 +5471,11 @@ class FileComponent {
5124
5471
  useExisting: forwardRef(() => FileComponent),
5125
5472
  multi: true,
5126
5473
  },
5127
- ], viewQueries: [{ propertyName: "fileUpload", first: true, predicate: ["fileUpload"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (showExistingFile() && !existingFile()) {\r\n <div class=\"flex items-center gap-2\">\r\n <span class=\"file-name\">\r\n \uD83D\uDCC4 {{ overrideFileName() ?? existingFileName() }}\r\n </span>\r\n\r\n <p-button\r\n icon=\"pi pi-cloud-upload\"\r\n [rounded]=\"true\"\r\n severity=\"info\"\r\n (click)=\"startReplace()\"\r\n [disabled]=\"isReplaceButtonDisabled()\"\r\n pTooltip=\"Reemplazar archivo\"\r\n tooltipPosition=\"top\"\r\n />\r\n </div>\r\n} @else {\r\n <div class=\"flex items-center gap-2\">\r\n <p-fileUpload\r\n #fileUpload\r\n mode=\"basic\"\r\n [accept]=\"accept()\"\r\n [maxFileSize]=\"maxFileSize() * 1024 * 1024\"\r\n [invalidFileTypeMessageSummary]=\"invalidSummary()\"\r\n [invalidFileTypeMessageDetail]=\"invalidDetail()\"\r\n [invalidFileSizeMessageSummary]=\"invalidSizeSummary()\"\r\n [invalidFileSizeMessageDetail]=\"invalidSizeDetail()\"\r\n (onSelect)=\"onSelect($event)\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n [pTooltip]=\"pTooltipOverride()\"\r\n [tooltipPosition]=\"tooltipPositionOverride()\"\r\n />\r\n\r\n @if (isReplacing() || existingFile()) {\r\n <p-button\r\n icon=\"pi pi-times\"\r\n [rounded]=\"true\"\r\n severity=\"secondary\"\r\n (click)=\"cancelReplace()\"\r\n [disabled]=\"disabled()\"\r\n pTooltip=\"Cancelar reemplazo\"\r\n tooltipPosition=\"top\"\r\n />\r\n }\r\n </div>\r\n}\r\n", styles: [".file-name{display:inline-block;font-size:clamp(.85rem,1vw + .5rem,1.4rem);font-weight:500;color:#2c3e50;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}\n"], dependencies: [{ kind: "component", type: FileUpload, selector: "p-fileupload, p-fileUpload", inputs: ["name", "url", "method", "multiple", "accept", "disabled", "auto", "withCredentials", "maxFileSize", "invalidFileSizeMessageSummary", "invalidFileSizeMessageDetail", "invalidFileTypeMessageSummary", "invalidFileTypeMessageDetail", "invalidFileLimitMessageDetail", "invalidFileLimitMessageSummary", "style", "styleClass", "previewWidth", "chooseLabel", "uploadLabel", "cancelLabel", "chooseIcon", "uploadIcon", "cancelIcon", "showUploadButton", "showCancelButton", "mode", "headers", "customUpload", "fileLimit", "uploadStyleClass", "cancelStyleClass", "removeStyleClass", "chooseStyleClass", "chooseButtonProps", "uploadButtonProps", "cancelButtonProps", "files"], outputs: ["onBeforeUpload", "onSend", "onUpload", "onError", "onClear", "onRemove", "onSelect", "onProgress", "uploadHandler", "onImageError", "onRemoveUploadedFile"] }, { kind: "directive", type: Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }] });
5474
+ ], viewQueries: [{ propertyName: "fileUpload", first: true, predicate: ["fileUpload"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (showExistingFile() && !existingFile()) {\r\n <div class=\"flex items-center gap-2\">\r\n <span class=\"file-name\">\r\n \uD83D\uDCC4 {{ overrideFileName() ?? existingFileName() }}\r\n </span>\r\n\r\n <p-button\r\n icon=\"pi pi-cloud-upload\"\r\n [rounded]=\"true\"\r\n severity=\"info\"\r\n (click)=\"startReplace()\"\r\n [disabled]=\"isReplaceButtonDisabled()\"\r\n pTooltip=\"Reemplazar archivo\"\r\n tooltipPosition=\"top\"\r\n />\r\n </div>\r\n} @else {\r\n <div class=\"flex flex-col gap-1\">\r\n <div class=\"flex items-center gap-2\">\r\n <p-fileUpload\r\n fluid\r\n #fileUpload\r\n chooseIcon=\"fa-solid fa-file-arrow-up\"\r\n [accept]=\"accept()\"\r\n [maxFileSize]=\"maxFileSize() * 1024 * 1024\"\r\n [invalidFileTypeMessageSummary]=\"invalidSummary()\"\r\n [invalidFileTypeMessageDetail]=\"invalidDetail()\"\r\n [invalidFileSizeMessageSummary]=\"invalidSizeSummary()\"\r\n [invalidFileSizeMessageDetail]=\"invalidSizeDetail()\"\r\n (onSelect)=\"onSelect($event)\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n [pTooltip]=\"`${pTooltipOverride()} ${allowedFileTypeLabel()}`\"\r\n [tooltipPosition]=\"tooltipPositionOverride()\"\r\n >\r\n <ng-template\r\n #header\r\n let-files\r\n let-chooseCallback=\"chooseCallback\"\r\n let-clearCallback=\"clearCallback\"\r\n >\r\n <!-- Mostrar el bot\u00F3n de upload SOLO si NO hay archivo seleccionado -->\r\n @if (!files || files.length === 0) {\r\n <p-button\r\n (onClick)=\"choose($event, chooseCallback)\"\r\n icon=\"fa-solid fa-cloud-arrow-up\"\r\n [rounded]=\"true\"\r\n [outlined]=\"true\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n />\r\n }\r\n\r\n <span class=\"file-upload-placeholder\">\r\n <i class=\"fa-regular fa-file-lines placeholder-icon\"></i>\r\n <span class=\"placeholder-text\">\r\n @if (files && files.length > 0) {\r\n <span class=\"file-selected\">\r\n <i class=\"fa-regular fa-file-pdf file-icon\"></i>\r\n {{ files[0]?.name }}\r\n <span class=\"file-size\"\r\n >({{ formatFileSize(files[0]?.size) }})</span\r\n >\r\n </span>\r\n } @else if (allowedFileTypeLabel() !== \"archivo\") {\r\n <span class=\"empty-warning\">Ning\u00FAn</span>\r\n <span class=\"file-type empty-warning-type\">{{\r\n allowedFileTypeLabel()\r\n }}</span>\r\n <span class=\"empty-warning\">seleccionado</span>\r\n } @else {\r\n <span class=\"empty-warning\">Ning\u00FAn archivo seleccionado</span>\r\n }\r\n </span>\r\n </span>\r\n\r\n <!-- Mostrar bot\u00F3n de limpiar SOLO si hay archivo seleccionado -->\r\n @if (files && files.length > 0) {\r\n <p-button\r\n icon=\"fa-solid fa-xmark\"\r\n [rounded]=\"true\"\r\n [outlined]=\"true\"\r\n (onClick)=\"clearFile()\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n styleClass=\"clear-button\"\r\n />\r\n }\r\n </ng-template>\r\n <!-- Ocultar la vista previa por defecto -->\r\n <ng-template #content></ng-template>\r\n <ng-template #file></ng-template>\r\n </p-fileUpload>\r\n\r\n @if (isReplacing() || existingFile()) {\r\n <p-button\r\n icon=\"pi pi-times\"\r\n [rounded]=\"true\"\r\n severity=\"secondary\"\r\n (click)=\"cancelReplace()\"\r\n [disabled]=\"disabled()\"\r\n pTooltip=\"Cancelar reemplazo\"\r\n tooltipPosition=\"top\"\r\n />\r\n }\r\n </div>\r\n\r\n <!-- Mensajes de error (validaci\u00F3n de PrimeNG) -->\r\n @if (errorMessage()) {\r\n <div class=\"file-error-message\">\r\n <i class=\"pi pi-exclamation-circle\"></i>\r\n {{ errorMessage() }}\r\n </div>\r\n }\r\n\r\n <!-- Mensaje de error del formulario (dsx-message-error) -->\r\n @if (showError() && errorControl()) {\r\n <dsx-message-error [control]=\"errorControl()\" />\r\n }\r\n </div>\r\n}\r\n\r\n<!-- Panel de Debug -->\r\n@if (debug() && isDevMode) {\r\n <div class=\"debug-panel\">\r\n <div class=\"debug-header\">\r\n <div class=\"debug-info\">\r\n <strong>\uD83D\uDD0D Debug File Upload:</strong>\r\n <span class=\"debug-item\"\r\n >Archivo: {{ value ? value.name : \"Ninguno\" }}</span\r\n >\r\n <span class=\"debug-item\"\r\n >Tama\u00F1o: {{ value ? formatFileSize(value.size) : \"-\" }}</span\r\n >\r\n <span class=\"debug-item\">Tipo: {{ value ? value.type : \"-\" }}</span>\r\n <span\r\n class=\"debug-item\"\r\n [style.color]=\"controlValid() ? '#10b981' : '#ef4444'\"\r\n >\r\n {{ controlValid() ? \"\u2705 V\u00E1lido\" : \"\u274C Inv\u00E1lido\" }}\r\n </span>\r\n <!-- Cambiar esta l\u00EDnea en el panel de debug -->\r\n <span class=\"debug-item\"\r\n >Required: {{ isRequired() ? \"\u2705\" : \"\u274C\" }}</span\r\n >\r\n <span class=\"debug-item\"\r\n >Reemplazando: {{ isReplacing() ? \"\u2705\" : \"\u274C\" }}</span\r\n >\r\n </div>\r\n <div class=\"debug-actions\">\r\n <button (click)=\"forceReload()\" class=\"debug-btn\" type=\"button\">\r\n \uD83D\uDD04 Recargar\r\n </button>\r\n <button (click)=\"clearFile()\" class=\"debug-btn\" type=\"button\">\r\n \uD83E\uDDF9 Limpiar\r\n </button>\r\n </div>\r\n </div>\r\n @if (errorMessage()) {\r\n <div class=\"debug-error\">\r\n <strong>\u26A0\uFE0F Error activo:</strong> {{ errorMessage() }}\r\n </div>\r\n }\r\n @if (controlErrors()) {\r\n <div class=\"debug-errors\">\r\n <strong>\u274C Errores del formulario:</strong>\r\n <pre>{{ controlErrors() | json }}</pre>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: [".file-name{display:inline-block;font-size:clamp(.85rem,1vw + .5rem,1.4rem);font-weight:500;color:#2c3e50;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}::ng-deep .p-fileupload .p-fileupload-header{display:flex!important;align-items:center!important;gap:.75rem!important;padding:0!important;border:none!important;background:transparent!important}::ng-deep .p-fileupload .p-fileupload-header .p-button{background:transparent!important;border:2px dashed #d1d5db!important;border-radius:8px!important;padding:.5rem 1rem!important;margin:.5rem!important;min-height:40px!important;min-width:40px!important;transition:all .25s ease!important;color:#6b7280!important}::ng-deep .p-fileupload .p-fileupload-header .p-button:not(:disabled):hover{border-color:#3b82f6!important;background:#eff6ff!important;box-shadow:0 0 0 3px #3b82f61a!important;color:#3b82f6!important}::ng-deep .p-fileupload .p-fileupload-header .p-button:disabled{opacity:.5!important;cursor:not-allowed!important;border-color:#e5e7eb!important;color:#9ca3af!important}::ng-deep .p-fileupload .p-fileupload-header .p-button .p-button-icon{font-size:1.2rem!important;transition:color .25s ease!important}.file-upload-placeholder{display:flex;align-items:center;gap:.75rem;pointer-events:none;white-space:nowrap;padding:0 .5rem}.placeholder-icon{font-size:1.35rem;color:#94a3b8;transition:all .3s ease}.placeholder-text{color:#94a3b8;font-size:.875rem;font-weight:400;transition:all .3s ease;letter-spacing:.01em}.placeholder-text .file-type{color:#64748b;font-size:.8rem;font-weight:600;transition:all .3s ease;letter-spacing:.03em;text-transform:uppercase;background:#f1f5f9;padding:.1rem .4rem;border-radius:4px;margin:0 .1rem}.placeholder-text .empty-warning{color:#d97706;font-weight:500;transition:color .3s ease}.placeholder-text .file-type.empty-warning-type{color:#b45309;background:#fef3c7;font-weight:600}.file-selected{display:flex;align-items:center;gap:.5rem;color:#1f2937;font-weight:500;font-size:.85rem}.file-selected .file-icon{font-size:1.2rem;color:#bb1589}.file-selected .file-size{color:#9ca3af;font-weight:400;font-size:.75rem;margin-left:.25rem}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .placeholder-icon{color:#3b82f6}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .placeholder-text{color:#1f2937}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .file-type{color:#2563eb;background:#dbeafe}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .placeholder-text .empty-warning{color:#2563eb}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .file-type.empty-warning-type{color:#2563eb;background:#dbeafe}::ng-deep .p-fileupload .p-fileupload-buttonbar{display:none!important}::ng-deep .p-fileupload .p-fileupload-content{padding:0!important;border:none!important;background:transparent!important}::ng-deep .p-button.clear-button{background:transparent!important;border:none!important;border-radius:8px!important;padding:.5rem!important;min-width:40px!important;height:40px!important;transition:all .25s ease!important;display:flex!important;align-items:center!important;justify-content:center!important;box-shadow:none!important}::ng-deep .p-fileupload .p-fileupload-header .p-button.clear-button:not(:disabled):hover{background:none!important;border:none!important;box-shadow:none!important;transform:scale(1.05)!important}::ng-deep .p-button.clear-button .p-button-icon{color:#6b7280!important;font-size:1.1rem!important;transition:all .25s ease!important}::ng-deep .p-button.clear-button:not(:disabled):hover .p-button-icon{color:#ef4444!important}::ng-deep .p-button.clear-button:disabled{opacity:.5!important;cursor:not-allowed!important}.file-error-message{display:flex;align-items:center;gap:.5rem;color:#dc2626;font-size:.875rem;font-weight:400;padding:.25rem .75rem;background:#fef2f2;border-radius:6px;border:1px solid #fecaca;animation:slideDown .3s ease}.file-error-message i{font-size:1rem;color:#dc2626;flex-shrink:0}@keyframes slideDown{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.debug-panel{margin-top:8px;font-size:11px;color:#374151;background:#f8fafc;padding:8px 12px;border-radius:6px;font-family:monospace;border:1px solid #e2e8f0;width:100%}.debug-header{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}.debug-info{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.debug-item{font-size:11px;color:#475569}.debug-item strong{color:#1e293b}.debug-actions{display:flex;gap:6px}.debug-btn{font-size:10px;padding:2px 10px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;transition:all .2s ease;color:#475569;font-family:monospace}.debug-btn:hover{background:#f1f5f9;border-color:#94a3b8}.debug-error,.debug-errors{margin-top:6px;padding:4px 8px;background:#fef2f2;border-radius:4px;color:#dc2626;font-size:11px;border:1px solid #fecaca}.debug-errors pre{margin:4px 0 0;font-size:10px;color:#991b1b;white-space:pre-wrap;word-break:break-all}@media(max-width:640px){.placeholder-icon{font-size:1.1rem!important}.placeholder-text{font-size:.75rem!important}.placeholder-text .file-type{font-size:.7rem!important;padding:.05rem .3rem!important}.file-selected{font-size:.75rem!important}.file-selected .file-icon{font-size:1rem!important}.file-selected .file-size{font-size:.65rem!important}::ng-deep .p-fileupload .p-fileupload-header .p-button{padding:.4rem .75rem!important;min-height:34px!important;min-width:34px!important}::ng-deep .p-button.clear-button{min-width:34px!important;height:34px!important}.file-error-message{font-size:.75rem!important;padding:.2rem .5rem!important}.debug-panel{font-size:10px;padding:6px 8px}.debug-item{font-size:10px}.debug-btn{font-size:9px;padding:2px 6px}}\n"], dependencies: [{ kind: "component", type: FileUpload, selector: "p-fileupload, p-fileUpload", inputs: ["name", "url", "method", "multiple", "accept", "disabled", "auto", "withCredentials", "maxFileSize", "invalidFileSizeMessageSummary", "invalidFileSizeMessageDetail", "invalidFileTypeMessageSummary", "invalidFileTypeMessageDetail", "invalidFileLimitMessageDetail", "invalidFileLimitMessageSummary", "style", "styleClass", "previewWidth", "chooseLabel", "uploadLabel", "cancelLabel", "chooseIcon", "uploadIcon", "cancelIcon", "showUploadButton", "showCancelButton", "mode", "headers", "customUpload", "fileLimit", "uploadStyleClass", "cancelStyleClass", "removeStyleClass", "chooseStyleClass", "chooseButtonProps", "uploadButtonProps", "cancelButtonProps", "files"], outputs: ["onBeforeUpload", "onSend", "onUpload", "onError", "onClear", "onRemove", "onSelect", "onProgress", "uploadHandler", "onImageError", "onRemoveUploadedFile"] }, { kind: "directive", type: Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: AppMessageErrorComponent, selector: "dsx-message-error", inputs: ["control", "form", "debugMode"] }, { kind: "pipe", type: JsonPipe, name: "json" }] });
5128
5475
  }
5129
5476
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FileComponent, decorators: [{
5130
5477
  type: Component,
5131
- args: [{ selector: 'dsx-file-upload', imports: [FileUpload, Tooltip, Button], providers: [
5478
+ args: [{ selector: 'dsx-file-upload', imports: [FileUpload, Tooltip, Button, JsonPipe, AppMessageErrorComponent], providers: [
5132
5479
  {
5133
5480
  provide: NG_VALUE_ACCESSOR,
5134
5481
  useExisting: forwardRef(() => FileComponent),
@@ -5139,119 +5486,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
5139
5486
  useExisting: forwardRef(() => FileComponent),
5140
5487
  multi: true,
5141
5488
  },
5142
- ], template: "@if (showExistingFile() && !existingFile()) {\r\n <div class=\"flex items-center gap-2\">\r\n <span class=\"file-name\">\r\n \uD83D\uDCC4 {{ overrideFileName() ?? existingFileName() }}\r\n </span>\r\n\r\n <p-button\r\n icon=\"pi pi-cloud-upload\"\r\n [rounded]=\"true\"\r\n severity=\"info\"\r\n (click)=\"startReplace()\"\r\n [disabled]=\"isReplaceButtonDisabled()\"\r\n pTooltip=\"Reemplazar archivo\"\r\n tooltipPosition=\"top\"\r\n />\r\n </div>\r\n} @else {\r\n <div class=\"flex items-center gap-2\">\r\n <p-fileUpload\r\n #fileUpload\r\n mode=\"basic\"\r\n [accept]=\"accept()\"\r\n [maxFileSize]=\"maxFileSize() * 1024 * 1024\"\r\n [invalidFileTypeMessageSummary]=\"invalidSummary()\"\r\n [invalidFileTypeMessageDetail]=\"invalidDetail()\"\r\n [invalidFileSizeMessageSummary]=\"invalidSizeSummary()\"\r\n [invalidFileSizeMessageDetail]=\"invalidSizeDetail()\"\r\n (onSelect)=\"onSelect($event)\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n [pTooltip]=\"pTooltipOverride()\"\r\n [tooltipPosition]=\"tooltipPositionOverride()\"\r\n />\r\n\r\n @if (isReplacing() || existingFile()) {\r\n <p-button\r\n icon=\"pi pi-times\"\r\n [rounded]=\"true\"\r\n severity=\"secondary\"\r\n (click)=\"cancelReplace()\"\r\n [disabled]=\"disabled()\"\r\n pTooltip=\"Cancelar reemplazo\"\r\n tooltipPosition=\"top\"\r\n />\r\n }\r\n </div>\r\n}\r\n", styles: [".file-name{display:inline-block;font-size:clamp(.85rem,1vw + .5rem,1.4rem);font-weight:500;color:#2c3e50;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}\n"] }]
5143
- }], ctorParameters: () => [], propDecorators: { existingFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFile", required: false }] }], existingFileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFileName", required: false }] }], overrideFileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "overrideFileName", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], pTooltipOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "pTooltipOverride", required: false }] }], tooltipPositionOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPositionOverride", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], invalidSummary: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidSummary", required: false }] }], invalidDetail: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidDetail", required: false }] }], invalidSizeSummary: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidSizeSummary", required: false }] }], invalidSizeDetail: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidSizeDetail", required: false }] }], fileUpload: [{ type: i0.ViewChild, args: ['fileUpload', { isSignal: true }] }] } });
5144
-
5145
- class JsonHighlightPipe {
5146
- /**
5147
- * Stringifica el JSON manteniendo arrays en una línea para una vista más compacta
5148
- */
5149
- compactStringify(obj, indent = 0) {
5150
- const spaces = ' '.repeat(indent);
5151
- if (obj === null) {
5152
- return 'null';
5153
- }
5154
- if (typeof obj !== 'object') {
5155
- if (typeof obj === 'string') {
5156
- return JSON.stringify(obj);
5157
- }
5158
- return String(obj);
5159
- }
5160
- if (obj instanceof Date) {
5161
- return JSON.stringify(obj.toISOString());
5162
- }
5163
- if (Array.isArray(obj)) {
5164
- // Mantener arrays en una línea
5165
- const items = obj.map((item) => {
5166
- if (item instanceof Date) {
5167
- return JSON.stringify(item.toISOString());
5168
- }
5169
- if (typeof item === 'object' && item !== null) {
5170
- return this.compactStringify(item, indent);
5171
- }
5172
- return typeof item === 'string'
5173
- ? JSON.stringify(item)
5174
- : item === null
5175
- ? 'null'
5176
- : String(item);
5177
- });
5178
- return `[${items.join(', ')}]`;
5179
- }
5180
- // Para objetos, mantener la indentación
5181
- const keys = Object.keys(obj);
5182
- if (keys.length === 0) {
5183
- return '{}';
5184
- }
5185
- const innerIndent = indent + 2;
5186
- const innerSpaces = ' '.repeat(innerIndent);
5187
- const lines = keys.map((key) => {
5188
- const value = obj[key];
5189
- const stringified = value instanceof Date
5190
- ? JSON.stringify(value.toISOString())
5191
- : typeof value === 'object' && value !== null
5192
- ? this.compactStringify(value, innerIndent)
5193
- : typeof value === 'string'
5194
- ? JSON.stringify(value)
5195
- : value === null
5196
- ? 'null'
5197
- : String(value);
5198
- return `${innerSpaces}"${key}": ${stringified}`;
5199
- });
5200
- return `{\n${lines.join(',\n')}\n${spaces}}`;
5201
- }
5202
- instanceId = Math.random().toString(36).slice(2, 8);
5203
- executions = 0;
5204
- transform(value) {
5205
- if (isDevMode()) {
5206
- this.executions++;
5207
- if (this.executions % 10 === 0) {
5208
- console.log(`[jsonHighlight:${this.instanceId}] ${this.executions}`);
5209
- }
5210
- }
5211
- if (!value) {
5212
- return '';
5213
- }
5214
- const json = this.compactStringify(value);
5215
- // Detecta fechas ISO: "YYYY-MM-DDTHH:MM:SS.mmmZ"
5216
- const ISO_DATE_RE = /^"(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}\.\d{3}Z)"$/;
5217
- // Aplica estilo y colores para diferenciar claves, valores, booleanos, números, etc.
5218
- return json
5219
- .replace(/&/g, '&amp;')
5220
- .replace(/</g, '&lt;')
5221
- .replace(/>/g, '&gt;')
5222
- .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => {
5223
- // Si es una clave (tiene dos puntos al final)
5224
- if (/:$/.test(match)) {
5225
- return `<span class="json-key">${match}</span>`;
5226
- }
5227
- // Detectar fechas ISO (YYYY-MM-DDTHH:MM:SS.mmmZ)
5228
- const isoMatch = match.match(ISO_DATE_RE);
5229
- if (isoMatch) {
5230
- return `<span class="json-string json-date">"<span class="json-date-part">${isoMatch[1]}</span><span class="json-date-sep">T</span><span class="json-date-time">${isoMatch[2]}</span>"</span>`;
5231
- }
5232
- // Si es un string (no tiene dos puntos)
5233
- return `<span class="json-string">${match}</span>`;
5234
- })
5235
- .replace(/\b(true|false|null)\b/g, (match) => {
5236
- if (match === 'null') {
5237
- return '<span class="json-null">null</span>';
5238
- }
5239
- return `<span class="json-boolean">${match}</span>`;
5240
- })
5241
- .replace(/\b(\d+\.?\d*)\b/g, '<span class="json-number">$1</span>')
5242
- .replace(/[{}\[\]]/g, '<span class="json-bracket">$&</span>')
5243
- .replace(/,/g, '<span class="json-comma">$&</span>')
5244
- .replace(/:/g, '<span class="json-colon">$&</span>');
5245
- }
5246
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JsonHighlightPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
5247
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: JsonHighlightPipe, isStandalone: true, name: "jsonHighlight" });
5248
- }
5249
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JsonHighlightPipe, decorators: [{
5250
- type: Pipe,
5251
- args: [{
5252
- name: 'jsonHighlight',
5253
- }]
5254
- }] });
5489
+ ], template: "@if (showExistingFile() && !existingFile()) {\r\n <div class=\"flex items-center gap-2\">\r\n <span class=\"file-name\">\r\n \uD83D\uDCC4 {{ overrideFileName() ?? existingFileName() }}\r\n </span>\r\n\r\n <p-button\r\n icon=\"pi pi-cloud-upload\"\r\n [rounded]=\"true\"\r\n severity=\"info\"\r\n (click)=\"startReplace()\"\r\n [disabled]=\"isReplaceButtonDisabled()\"\r\n pTooltip=\"Reemplazar archivo\"\r\n tooltipPosition=\"top\"\r\n />\r\n </div>\r\n} @else {\r\n <div class=\"flex flex-col gap-1\">\r\n <div class=\"flex items-center gap-2\">\r\n <p-fileUpload\r\n fluid\r\n #fileUpload\r\n chooseIcon=\"fa-solid fa-file-arrow-up\"\r\n [accept]=\"accept()\"\r\n [maxFileSize]=\"maxFileSize() * 1024 * 1024\"\r\n [invalidFileTypeMessageSummary]=\"invalidSummary()\"\r\n [invalidFileTypeMessageDetail]=\"invalidDetail()\"\r\n [invalidFileSizeMessageSummary]=\"invalidSizeSummary()\"\r\n [invalidFileSizeMessageDetail]=\"invalidSizeDetail()\"\r\n (onSelect)=\"onSelect($event)\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n [pTooltip]=\"`${pTooltipOverride()} ${allowedFileTypeLabel()}`\"\r\n [tooltipPosition]=\"tooltipPositionOverride()\"\r\n >\r\n <ng-template\r\n #header\r\n let-files\r\n let-chooseCallback=\"chooseCallback\"\r\n let-clearCallback=\"clearCallback\"\r\n >\r\n <!-- Mostrar el bot\u00F3n de upload SOLO si NO hay archivo seleccionado -->\r\n @if (!files || files.length === 0) {\r\n <p-button\r\n (onClick)=\"choose($event, chooseCallback)\"\r\n icon=\"fa-solid fa-cloud-arrow-up\"\r\n [rounded]=\"true\"\r\n [outlined]=\"true\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n />\r\n }\r\n\r\n <span class=\"file-upload-placeholder\">\r\n <i class=\"fa-regular fa-file-lines placeholder-icon\"></i>\r\n <span class=\"placeholder-text\">\r\n @if (files && files.length > 0) {\r\n <span class=\"file-selected\">\r\n <i class=\"fa-regular fa-file-pdf file-icon\"></i>\r\n {{ files[0]?.name }}\r\n <span class=\"file-size\"\r\n >({{ formatFileSize(files[0]?.size) }})</span\r\n >\r\n </span>\r\n } @else if (allowedFileTypeLabel() !== \"archivo\") {\r\n <span class=\"empty-warning\">Ning\u00FAn</span>\r\n <span class=\"file-type empty-warning-type\">{{\r\n allowedFileTypeLabel()\r\n }}</span>\r\n <span class=\"empty-warning\">seleccionado</span>\r\n } @else {\r\n <span class=\"empty-warning\">Ning\u00FAn archivo seleccionado</span>\r\n }\r\n </span>\r\n </span>\r\n\r\n <!-- Mostrar bot\u00F3n de limpiar SOLO si hay archivo seleccionado -->\r\n @if (files && files.length > 0) {\r\n <p-button\r\n icon=\"fa-solid fa-xmark\"\r\n [rounded]=\"true\"\r\n [outlined]=\"true\"\r\n (onClick)=\"clearFile()\"\r\n [disabled]=\"!isFileUploadEnabled()\"\r\n styleClass=\"clear-button\"\r\n />\r\n }\r\n </ng-template>\r\n <!-- Ocultar la vista previa por defecto -->\r\n <ng-template #content></ng-template>\r\n <ng-template #file></ng-template>\r\n </p-fileUpload>\r\n\r\n @if (isReplacing() || existingFile()) {\r\n <p-button\r\n icon=\"pi pi-times\"\r\n [rounded]=\"true\"\r\n severity=\"secondary\"\r\n (click)=\"cancelReplace()\"\r\n [disabled]=\"disabled()\"\r\n pTooltip=\"Cancelar reemplazo\"\r\n tooltipPosition=\"top\"\r\n />\r\n }\r\n </div>\r\n\r\n <!-- Mensajes de error (validaci\u00F3n de PrimeNG) -->\r\n @if (errorMessage()) {\r\n <div class=\"file-error-message\">\r\n <i class=\"pi pi-exclamation-circle\"></i>\r\n {{ errorMessage() }}\r\n </div>\r\n }\r\n\r\n <!-- Mensaje de error del formulario (dsx-message-error) -->\r\n @if (showError() && errorControl()) {\r\n <dsx-message-error [control]=\"errorControl()\" />\r\n }\r\n </div>\r\n}\r\n\r\n<!-- Panel de Debug -->\r\n@if (debug() && isDevMode) {\r\n <div class=\"debug-panel\">\r\n <div class=\"debug-header\">\r\n <div class=\"debug-info\">\r\n <strong>\uD83D\uDD0D Debug File Upload:</strong>\r\n <span class=\"debug-item\"\r\n >Archivo: {{ value ? value.name : \"Ninguno\" }}</span\r\n >\r\n <span class=\"debug-item\"\r\n >Tama\u00F1o: {{ value ? formatFileSize(value.size) : \"-\" }}</span\r\n >\r\n <span class=\"debug-item\">Tipo: {{ value ? value.type : \"-\" }}</span>\r\n <span\r\n class=\"debug-item\"\r\n [style.color]=\"controlValid() ? '#10b981' : '#ef4444'\"\r\n >\r\n {{ controlValid() ? \"\u2705 V\u00E1lido\" : \"\u274C Inv\u00E1lido\" }}\r\n </span>\r\n <!-- Cambiar esta l\u00EDnea en el panel de debug -->\r\n <span class=\"debug-item\"\r\n >Required: {{ isRequired() ? \"\u2705\" : \"\u274C\" }}</span\r\n >\r\n <span class=\"debug-item\"\r\n >Reemplazando: {{ isReplacing() ? \"\u2705\" : \"\u274C\" }}</span\r\n >\r\n </div>\r\n <div class=\"debug-actions\">\r\n <button (click)=\"forceReload()\" class=\"debug-btn\" type=\"button\">\r\n \uD83D\uDD04 Recargar\r\n </button>\r\n <button (click)=\"clearFile()\" class=\"debug-btn\" type=\"button\">\r\n \uD83E\uDDF9 Limpiar\r\n </button>\r\n </div>\r\n </div>\r\n @if (errorMessage()) {\r\n <div class=\"debug-error\">\r\n <strong>\u26A0\uFE0F Error activo:</strong> {{ errorMessage() }}\r\n </div>\r\n }\r\n @if (controlErrors()) {\r\n <div class=\"debug-errors\">\r\n <strong>\u274C Errores del formulario:</strong>\r\n <pre>{{ controlErrors() | json }}</pre>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: [".file-name{display:inline-block;font-size:clamp(.85rem,1vw + .5rem,1.4rem);font-weight:500;color:#2c3e50;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}::ng-deep .p-fileupload .p-fileupload-header{display:flex!important;align-items:center!important;gap:.75rem!important;padding:0!important;border:none!important;background:transparent!important}::ng-deep .p-fileupload .p-fileupload-header .p-button{background:transparent!important;border:2px dashed #d1d5db!important;border-radius:8px!important;padding:.5rem 1rem!important;margin:.5rem!important;min-height:40px!important;min-width:40px!important;transition:all .25s ease!important;color:#6b7280!important}::ng-deep .p-fileupload .p-fileupload-header .p-button:not(:disabled):hover{border-color:#3b82f6!important;background:#eff6ff!important;box-shadow:0 0 0 3px #3b82f61a!important;color:#3b82f6!important}::ng-deep .p-fileupload .p-fileupload-header .p-button:disabled{opacity:.5!important;cursor:not-allowed!important;border-color:#e5e7eb!important;color:#9ca3af!important}::ng-deep .p-fileupload .p-fileupload-header .p-button .p-button-icon{font-size:1.2rem!important;transition:color .25s ease!important}.file-upload-placeholder{display:flex;align-items:center;gap:.75rem;pointer-events:none;white-space:nowrap;padding:0 .5rem}.placeholder-icon{font-size:1.35rem;color:#94a3b8;transition:all .3s ease}.placeholder-text{color:#94a3b8;font-size:.875rem;font-weight:400;transition:all .3s ease;letter-spacing:.01em}.placeholder-text .file-type{color:#64748b;font-size:.8rem;font-weight:600;transition:all .3s ease;letter-spacing:.03em;text-transform:uppercase;background:#f1f5f9;padding:.1rem .4rem;border-radius:4px;margin:0 .1rem}.placeholder-text .empty-warning{color:#d97706;font-weight:500;transition:color .3s ease}.placeholder-text .file-type.empty-warning-type{color:#b45309;background:#fef3c7;font-weight:600}.file-selected{display:flex;align-items:center;gap:.5rem;color:#1f2937;font-weight:500;font-size:.85rem}.file-selected .file-icon{font-size:1.2rem;color:#bb1589}.file-selected .file-size{color:#9ca3af;font-weight:400;font-size:.75rem;margin-left:.25rem}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .placeholder-icon{color:#3b82f6}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .placeholder-text{color:#1f2937}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .file-type{color:#2563eb;background:#dbeafe}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .placeholder-text .empty-warning{color:#2563eb}::ng-deep .p-fileupload .p-fileupload-header:has(.p-button:not(:disabled):hover) .file-type.empty-warning-type{color:#2563eb;background:#dbeafe}::ng-deep .p-fileupload .p-fileupload-buttonbar{display:none!important}::ng-deep .p-fileupload .p-fileupload-content{padding:0!important;border:none!important;background:transparent!important}::ng-deep .p-button.clear-button{background:transparent!important;border:none!important;border-radius:8px!important;padding:.5rem!important;min-width:40px!important;height:40px!important;transition:all .25s ease!important;display:flex!important;align-items:center!important;justify-content:center!important;box-shadow:none!important}::ng-deep .p-fileupload .p-fileupload-header .p-button.clear-button:not(:disabled):hover{background:none!important;border:none!important;box-shadow:none!important;transform:scale(1.05)!important}::ng-deep .p-button.clear-button .p-button-icon{color:#6b7280!important;font-size:1.1rem!important;transition:all .25s ease!important}::ng-deep .p-button.clear-button:not(:disabled):hover .p-button-icon{color:#ef4444!important}::ng-deep .p-button.clear-button:disabled{opacity:.5!important;cursor:not-allowed!important}.file-error-message{display:flex;align-items:center;gap:.5rem;color:#dc2626;font-size:.875rem;font-weight:400;padding:.25rem .75rem;background:#fef2f2;border-radius:6px;border:1px solid #fecaca;animation:slideDown .3s ease}.file-error-message i{font-size:1rem;color:#dc2626;flex-shrink:0}@keyframes slideDown{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.debug-panel{margin-top:8px;font-size:11px;color:#374151;background:#f8fafc;padding:8px 12px;border-radius:6px;font-family:monospace;border:1px solid #e2e8f0;width:100%}.debug-header{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}.debug-info{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.debug-item{font-size:11px;color:#475569}.debug-item strong{color:#1e293b}.debug-actions{display:flex;gap:6px}.debug-btn{font-size:10px;padding:2px 10px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;transition:all .2s ease;color:#475569;font-family:monospace}.debug-btn:hover{background:#f1f5f9;border-color:#94a3b8}.debug-error,.debug-errors{margin-top:6px;padding:4px 8px;background:#fef2f2;border-radius:4px;color:#dc2626;font-size:11px;border:1px solid #fecaca}.debug-errors pre{margin:4px 0 0;font-size:10px;color:#991b1b;white-space:pre-wrap;word-break:break-all}@media(max-width:640px){.placeholder-icon{font-size:1.1rem!important}.placeholder-text{font-size:.75rem!important}.placeholder-text .file-type{font-size:.7rem!important;padding:.05rem .3rem!important}.file-selected{font-size:.75rem!important}.file-selected .file-icon{font-size:1rem!important}.file-selected .file-size{font-size:.65rem!important}::ng-deep .p-fileupload .p-fileupload-header .p-button{padding:.4rem .75rem!important;min-height:34px!important;min-width:34px!important}::ng-deep .p-button.clear-button{min-width:34px!important;height:34px!important}.file-error-message{font-size:.75rem!important;padding:.2rem .5rem!important}.debug-panel{font-size:10px;padding:6px 8px}.debug-item{font-size:10px}.debug-btn{font-size:9px;padding:2px 6px}}\n"] }]
5490
+ }], ctorParameters: () => [], propDecorators: { existingFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFile", required: false }] }], existingFileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFileName", required: false }] }], overrideFileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "overrideFileName", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], pTooltipOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "pTooltipOverride", required: false }] }], tooltipPositionOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPositionOverride", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], invalidSummary: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidSummary", required: false }] }], invalidDetail: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidDetail", required: false }] }], invalidSizeSummary: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidSizeSummary", required: false }] }], invalidSizeDetail: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidSizeDetail", required: false }] }], fileUpload: [{ type: i0.ViewChild, args: ['fileUpload', { isSignal: true }] }] } });
5255
5491
 
5256
5492
  function uid() {
5257
5493
  return Math.random().toString(36).substring(2, 8);
@@ -5259,67 +5495,245 @@ function uid() {
5259
5495
  class JsonValuesDebujComponent {
5260
5496
  form = input(null, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
5261
5497
  debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : /* istanbul ignore next */ []));
5498
+ debounceTime = input(300, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ [])); // ✅ Configurable
5262
5499
  debugValue = signal(null, ...(ngDevMode ? [{ debugName: "debugValue" }] : /* istanbul ignore next */ []));
5500
+ serializedValue = signal('', ...(ngDevMode ? [{ debugName: "serializedValue" }] : /* istanbul ignore next */ []));
5501
+ fileInfo = signal(null, ...(ngDevMode ? [{ debugName: "fileInfo" }] : /* istanbul ignore next */ []));
5502
+ isDevModeSignal = computed(() => isDevMode(), ...(ngDevMode ? [{ debugName: "isDevModeSignal" }] : /* istanbul ignore next */ []));
5503
+ isDebugEnabledSignal = computed(() => {
5504
+ const isDev = this.isDevModeSignal();
5505
+ const debug = this.debug();
5506
+ const result = isDev && debug;
5507
+ if (this._lastDebugState !== result) {
5508
+ this._lastDebugState = result;
5509
+ if (result) {
5510
+ console.log(`[JsonDebug:${this.id}] 📝 Logs activados`, {
5511
+ isDevMode: isDev,
5512
+ debug: debug,
5513
+ });
5514
+ }
5515
+ }
5516
+ return result;
5517
+ }, ...(ngDevMode ? [{ debugName: "isDebugEnabledSignal" }] : /* istanbul ignore next */ []));
5518
+ _lastDebugState = null;
5519
+ _lastVisualState = null;
5263
5520
  id = uid();
5264
5521
  subscription;
5265
5522
  changesCount = 0;
5266
- // Indica si debemos mostrar el componente visualmente
5267
- get shouldShowVisual() {
5268
- // Solo mostrar visualmente en modo desarrollo
5269
- return isDevMode();
5270
- }
5271
- // Indica si debemos mostrar logs en consola
5272
- get shouldShowLogs() {
5273
- // Solo logs en desarrollo Y cuando debug está explícitamente true
5274
- return isDevMode() && this.debug() === true;
5275
- }
5523
+ isInitialized = false;
5524
+ lastLogTime = 0;
5525
+ shouldShowVisual = computed(() => {
5526
+ const show = this.isDevModeSignal();
5527
+ if (this._lastVisualState !== show) {
5528
+ this._lastVisualState = show;
5529
+ if (this.shouldShowLogs()) {
5530
+ console.log(`[JsonDebug:${this.id}] 👁️ Visual: ${show}`, {
5531
+ isDevMode: isDevMode(),
5532
+ reason: show ? 'Modo desarrollo activo' : 'Modo producción',
5533
+ });
5534
+ }
5535
+ }
5536
+ return show;
5537
+ }, ...(ngDevMode ? [{ debugName: "shouldShowVisual" }] : /* istanbul ignore next */ []));
5538
+ shouldShowLogs = computed(() => {
5539
+ const isDev = this.isDevModeSignal();
5540
+ const debug = this.debug();
5541
+ const show = isDev && debug;
5542
+ if (this._lastDebugState !== show) {
5543
+ this._lastDebugState = show;
5544
+ if (show) {
5545
+ console.log(`[JsonDebug:${this.id}] 📝 Logs activados`, {
5546
+ isDevMode: isDev,
5547
+ debug: debug,
5548
+ });
5549
+ }
5550
+ }
5551
+ return show;
5552
+ }, ...(ngDevMode ? [{ debugName: "shouldShowLogs" }] : /* istanbul ignore next */ []));
5276
5553
  constructor() {
5277
- if (this.shouldShowLogs) {
5278
- console.log('[JsonDebug] CONSTRUCTOR', this.id);
5554
+ if (this.shouldShowLogs()) {
5555
+ console.log(`[JsonDebug:${this.id}] 🏗️ Constructor`, {
5556
+ id: this.id,
5557
+ isDevMode: isDevMode(),
5558
+ debug: this.debug(),
5559
+ debounceTime: this.debounceTime(),
5560
+ });
5279
5561
  }
5280
5562
  effect((onCleanup) => {
5281
5563
  const form = this.form();
5282
- if (!form)
5564
+ if (!form) {
5565
+ if (this.shouldShowLogs() && this.isInitialized) {
5566
+ console.log(`[JsonDebug:${this.id}] ⚠️ Formulario eliminado`);
5567
+ }
5568
+ if (this.subscription) {
5569
+ this.subscription.unsubscribe();
5570
+ this.subscription = undefined;
5571
+ }
5572
+ this.isInitialized = false;
5573
+ return;
5574
+ }
5575
+ if (this.isInitialized && this.subscription) {
5576
+ if (this.shouldShowLogs()) {
5577
+ console.log(`[JsonDebug:${this.id}] 🔄 Formulario ya inicializado, saltando...`);
5578
+ }
5283
5579
  return;
5284
- if (this.shouldShowLogs) {
5285
- console.log(`[json-debug:${this.id}] EFFECT INIT`, {
5580
+ }
5581
+ if (this.shouldShowLogs()) {
5582
+ console.log(`[JsonDebug:${this.id}] 🔄 Effect iniciado`, {
5286
5583
  controls: Object.keys(form.controls).length,
5584
+ timestamp: new Date().toISOString(),
5585
+ debounceTime: this.debounceTime(),
5586
+ });
5587
+ }
5588
+ if (this.subscription) {
5589
+ this.subscription.unsubscribe();
5590
+ this.subscription = undefined;
5591
+ }
5592
+ const rawValue = form.getRawValue();
5593
+ this.debugValue.set(rawValue);
5594
+ this.updateSerializedValue(rawValue);
5595
+ this.updateFileInfo(rawValue);
5596
+ if (this.shouldShowLogs()) {
5597
+ console.log(`[JsonDebug:${this.id}] 📊 Procesamiento inicial`, {
5598
+ hasFile: !!rawValue?.file,
5599
+ fileType: rawValue?.file ? typeof rawValue.file : 'N/A',
5600
+ serializedLength: this.serializedValue().length,
5287
5601
  });
5288
5602
  }
5289
- this.subscription?.unsubscribe();
5290
- this.debugValue.set(form.getRawValue());
5291
- this.subscription = form.valueChanges.subscribe(() => {
5292
- this.debugValue.set(form.getRawValue());
5293
- if (this.shouldShowLogs) {
5294
- this.changesCount++;
5295
- console.log(`[json-debug:${this.id}] CHANGE #${this.changesCount}`, {
5296
- formValue: form.getRawValue(),
5603
+ // ✅ Aplicar debounce y distinctUntilChanged para reducir logs
5604
+ this.subscription = form.valueChanges
5605
+ .pipe(debounceTime(this.debounceTime()), // ✅ Esperar 300ms antes de emitir
5606
+ distinctUntilChanged((prev, curr) => {
5607
+ // Comparar si los valores son iguales
5608
+ return JSON.stringify(prev) === JSON.stringify(curr);
5609
+ }))
5610
+ .subscribe(() => {
5611
+ const startTime = performance.now();
5612
+ const rawValue = form.getRawValue();
5613
+ this.debugValue.set(rawValue);
5614
+ this.updateSerializedValue(rawValue);
5615
+ this.updateFileInfo(rawValue);
5616
+ this.changesCount++;
5617
+ const endTime = performance.now();
5618
+ if (this.shouldShowLogs()) {
5619
+ const elapsed = endTime - startTime;
5620
+ console.log(`[JsonDebug:${this.id}] 🔄 Cambio #${this.changesCount}`, {
5621
+ tiempoMs: elapsed.toFixed(2),
5622
+ hasFile: !!rawValue?.file,
5623
+ serializedLength: this.serializedValue().length,
5624
+ warning: elapsed > 10 ? '⚠️ Cambio lento' : '✅ OK',
5297
5625
  });
5626
+ if (this.changesCount % 10 === 0) {
5627
+ console.log(`[JsonDebug:${this.id}] 📊 Estadísticas`, {
5628
+ cambios: this.changesCount,
5629
+ promedioMs: (elapsed / this.changesCount).toFixed(2),
5630
+ ultimoMs: elapsed.toFixed(2),
5631
+ });
5632
+ }
5298
5633
  }
5299
5634
  });
5635
+ this.isInitialized = true;
5300
5636
  onCleanup(() => {
5301
- this.subscription?.unsubscribe();
5637
+ if (this.shouldShowLogs()) {
5638
+ console.log(`[JsonDebug:${this.id}] 🧹 Limpieza effect`, {
5639
+ cambiosProcesados: this.changesCount,
5640
+ isInitialized: this.isInitialized,
5641
+ });
5642
+ }
5302
5643
  });
5303
5644
  });
5304
5645
  }
5305
- ngOnDestroy() {
5306
- if (this.shouldShowLogs) {
5307
- console.log('[JsonDebug] DESTROY', this.id);
5646
+ updateSerializedValue(value) {
5647
+ if (!value) {
5648
+ this.serializedValue.set('');
5649
+ return;
5650
+ }
5651
+ try {
5652
+ const copy = { ...value };
5653
+ if (copy.file && typeof copy.file === 'object') {
5654
+ const fileProps = {};
5655
+ if ('name' in copy.file && copy.file.name !== undefined) {
5656
+ fileProps.name = copy.file.name;
5657
+ }
5658
+ if ('size' in copy.file && copy.file.size !== undefined) {
5659
+ fileProps.size = copy.file.size;
5660
+ }
5661
+ if ('type' in copy.file && copy.file.type !== undefined) {
5662
+ fileProps.type = copy.file.type;
5663
+ }
5664
+ if ('lastModified' in copy.file &&
5665
+ copy.file.lastModified !== undefined) {
5666
+ fileProps.lastModified = copy.file.lastModified;
5667
+ }
5668
+ const otherProps = Object.keys(copy.file).filter((key) => !['name', 'size', 'type', 'lastModified'].includes(key));
5669
+ otherProps.forEach((key) => {
5670
+ fileProps[key] = copy.file[key];
5671
+ });
5672
+ copy.file = fileProps;
5673
+ }
5674
+ this.serializedValue.set(JSON.stringify(copy, null, 2));
5675
+ }
5676
+ catch (error) {
5677
+ this.serializedValue.set('Error al serializar: ' + error);
5678
+ if (this.shouldShowLogs()) {
5679
+ console.error('[JsonDebug] Error serializing value:', error);
5680
+ }
5681
+ }
5682
+ }
5683
+ updateFileInfo(value) {
5684
+ if (!value || !value.file) {
5685
+ this.fileInfo.set(null);
5686
+ return;
5687
+ }
5688
+ const file = value.file;
5689
+ const info = {
5690
+ exists: true,
5691
+ isFile: file instanceof File,
5692
+ isBlob: file instanceof Blob,
5693
+ hasName: 'name' in file,
5694
+ hasSize: 'size' in file,
5695
+ hasType: 'type' in file,
5696
+ hasLastModified: 'lastModified' in file,
5697
+ };
5698
+ if ('name' in file && file.name !== undefined) {
5699
+ info.name = file.name;
5700
+ }
5701
+ if ('size' in file && file.size !== undefined) {
5702
+ info.size = file.size;
5308
5703
  }
5309
- this.subscription?.unsubscribe();
5310
- if (this.shouldShowLogs) {
5311
- console.log(`[json-debug:${this.id}] DESTROY SUMMARY`, {
5704
+ if ('type' in file && file.type !== undefined) {
5705
+ info.mimeType = file.type;
5706
+ }
5707
+ if ('lastModified' in file && file.lastModified !== undefined) {
5708
+ info.lastModified = file.lastModified;
5709
+ }
5710
+ this.fileInfo.set(info);
5711
+ }
5712
+ hasFile() {
5713
+ return !!this.fileInfo();
5714
+ }
5715
+ getFileInfo() {
5716
+ return this.fileInfo();
5717
+ }
5718
+ ngOnDestroy() {
5719
+ if (this.shouldShowLogs()) {
5720
+ console.log(`[JsonDebug:${this.id}] 💀 Destroy`, {
5312
5721
  totalChanges: this.changesCount,
5722
+ tiempoVida: `${(performance.now() / 1000).toFixed(2)}s`,
5313
5723
  });
5314
5724
  }
5725
+ if (this.subscription) {
5726
+ this.subscription.unsubscribe();
5727
+ this.subscription = undefined;
5728
+ }
5315
5729
  }
5316
5730
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JsonValuesDebujComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5317
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JsonValuesDebujComponent, isStandalone: true, selector: "app-json-values-debuj", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (form() && shouldShowVisual) {\r\n <div class=\"custom-container\">\r\n <pre class=\"custom-pre\" [innerHTML]=\"debugValue() | jsonHighlight\"></pre>\r\n </div>\r\n}\r\n", styles: [".custom-container{width:100%;overflow:auto;max-height:700px;background:#1e1e1e;border:1px solid #333;border-radius:8px;box-shadow:0 4px 12px #0000004d;padding:1.25rem;margin-bottom:1.25rem}.custom-pre{margin:0;white-space:pre-wrap;word-break:break-word;word-wrap:break-word;font-family:Segoe UI,Fira Code,JetBrains Mono,Consolas,Monaco,Courier New,monospace;font-size:15px;line-height:1.65;color:#d4d4d4;font-weight:400}.json-key{color:#9cdcfe;font-weight:600;font-size:15px}.json-string{color:#ce9178;font-size:15px}.json-number{color:#b5cea8;font-weight:600;font-size:15px}.json-boolean{color:#569cd6;font-weight:700;font-size:15px}.json-null{color:gray;font-style:italic;font-size:15px}.json-bracket{color:gold;font-weight:700;font-size:16px}.json-colon{color:#d4d4d4;font-weight:500}.json-comma{color:gray;font-size:14px}.json-date-part{color:#4fc1ff;font-weight:600;font-size:15px}.json-date-sep{color:gray}.json-date-time{color:#c586c0;font-size:15px}.custom-container::-webkit-scrollbar{width:12px;height:12px}.custom-container::-webkit-scrollbar-track{background:#252526;border-radius:6px}.custom-container::-webkit-scrollbar-thumb{background:#555;border-radius:6px;transition:background .2s ease}.custom-container::-webkit-scrollbar-thumb:hover{background:#777}.custom-container::-webkit-scrollbar-corner{background:#1e1e1e}.custom-container{font-smooth:antialiased;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.json-null,.json-undefined{color:#f48771;font-style:italic;opacity:.8}.json-empty{color:gray;font-style:italic}.json-comma+br{margin-bottom:2px}\n"], dependencies: [{ kind: "pipe", type: JsonHighlightPipe, name: "jsonHighlight" }], encapsulation: i0.ViewEncapsulation.None });
5731
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JsonValuesDebujComponent, isStandalone: true, selector: "app-json-values-debuj", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!-- json-values-debuj.component.html -->\r\n@if (shouldShowVisual()) {\r\n <div class=\"json-debug-container\">\r\n <div class=\"json-debug-header\">\r\n <span class=\"json-debug-id\">\uD83D\uDD0D Debug #{{ id }}</span>\r\n <span class=\"json-debug-info\">\r\n @if (debugValue()) {\r\n <span class=\"badge badge-success\">\u2713 Formulario activo</span>\r\n } @else {\r\n <span class=\"badge badge-danger\">\u2717 Sin formulario</span>\r\n }\r\n </span>\r\n </div>\r\n\r\n @if (debugValue()) {\r\n <div class=\"json-debug-content\">\r\n <!-- Informaci\u00F3n del archivo (si existe) -->\r\n @if (hasFile()) {\r\n <div class=\"file-info-box\">\r\n <div class=\"file-info-header\">\uD83D\uDCC1 Archivo seleccionado</div>\r\n <div class=\"file-info-grid\">\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Nombre:</span>\r\n <span class=\"value\">{{ getFileInfo()?.name || \"N/A\" }}</span>\r\n </div>\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Tama\u00F1o:</span>\r\n <span class=\"value\"\r\n >{{ getFileInfo()?.size | number }} bytes</span\r\n >\r\n </div>\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Tipo:</span>\r\n <span class=\"value\">{{\r\n getFileInfo()?.mimeType || \"N/A\"\r\n }}</span>\r\n </div>\r\n @if (getFileInfo()?.lastModified) {\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Modificado:</span>\r\n <span class=\"value\">{{\r\n getFileInfo()?.lastModified | date: \"medium\"\r\n }}</span>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- JSON -->\r\n <div class=\"json-display\">\r\n <pre class=\"json-colored\">{{ serializedValue() }}</pre>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: [".json-debug-container{margin:10px 0;border:1px solid #dee2e6;border-radius:8px;background-color:#f8f9fa;font-family:Consolas,Monaco,Courier New,monospace;font-size:13px;box-shadow:0 2px 8px #0000001a}.json-debug-header{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:8px 8px 0 0;color:#fff}.json-debug-id{font-weight:700;font-size:14px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.2)}.badge{padding:4px 14px;border-radius:20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.badge-success{background-color:#10b981;color:#fff;box-shadow:0 2px 4px #10b9814d}.badge-danger{background-color:#ef4444;color:#fff;box-shadow:0 2px 4px #ef44444d}.json-debug-content{padding:16px 20px}.file-info-box{background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border:1px solid #bae6fd;border-radius:6px;padding:12px 16px;margin-bottom:16px}.file-info-header{font-weight:600;color:#0369a1;font-size:13px;margin-bottom:8px}.file-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:6px}.file-info-item{padding:4px 8px;background-color:#fff;border-radius:4px;border:1px solid #e5e7eb}.file-info-item .label{font-weight:600;color:#6b7280;margin-right:8px;font-size:12px}.file-info-item .value{color:#0369a1;font-family:Consolas,Monaco,Courier New,monospace;font-size:12px}.json-display{margin-top:0}.json-colored{margin:0;padding:16px 20px;background-color:#1e1e1e;border-radius:6px;overflow:auto;font-size:13px;line-height:1.7;color:#d4d4d4;min-height:50px;max-height:500px;font-family:Consolas,Monaco,Courier New,monospace;white-space:pre}.json-colored::-webkit-scrollbar{width:10px;height:10px}.json-colored::-webkit-scrollbar-track{background:#2d2d2d;border-radius:4px}.json-colored::-webkit-scrollbar-thumb{background:#666;border-radius:4px}.json-colored::-webkit-scrollbar-thumb:hover{background:#888}@media(max-width:768px){.json-debug-header{flex-direction:column;gap:8px;padding:10px 16px}.json-debug-content{padding:12px 16px}.file-info-grid{grid-template-columns:1fr}.json-colored{font-size:11px;padding:12px 14px;max-height:300px}}\n"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: DecimalPipe, name: "number" }], encapsulation: i0.ViewEncapsulation.None });
5318
5732
  }
5319
5733
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JsonValuesDebujComponent, decorators: [{
5320
5734
  type: Component,
5321
- args: [{ selector: 'app-json-values-debuj', standalone: true, imports: [JsonHighlightPipe], encapsulation: ViewEncapsulation.None, template: "@if (form() && shouldShowVisual) {\r\n <div class=\"custom-container\">\r\n <pre class=\"custom-pre\" [innerHTML]=\"debugValue() | jsonHighlight\"></pre>\r\n </div>\r\n}\r\n", styles: [".custom-container{width:100%;overflow:auto;max-height:700px;background:#1e1e1e;border:1px solid #333;border-radius:8px;box-shadow:0 4px 12px #0000004d;padding:1.25rem;margin-bottom:1.25rem}.custom-pre{margin:0;white-space:pre-wrap;word-break:break-word;word-wrap:break-word;font-family:Segoe UI,Fira Code,JetBrains Mono,Consolas,Monaco,Courier New,monospace;font-size:15px;line-height:1.65;color:#d4d4d4;font-weight:400}.json-key{color:#9cdcfe;font-weight:600;font-size:15px}.json-string{color:#ce9178;font-size:15px}.json-number{color:#b5cea8;font-weight:600;font-size:15px}.json-boolean{color:#569cd6;font-weight:700;font-size:15px}.json-null{color:gray;font-style:italic;font-size:15px}.json-bracket{color:gold;font-weight:700;font-size:16px}.json-colon{color:#d4d4d4;font-weight:500}.json-comma{color:gray;font-size:14px}.json-date-part{color:#4fc1ff;font-weight:600;font-size:15px}.json-date-sep{color:gray}.json-date-time{color:#c586c0;font-size:15px}.custom-container::-webkit-scrollbar{width:12px;height:12px}.custom-container::-webkit-scrollbar-track{background:#252526;border-radius:6px}.custom-container::-webkit-scrollbar-thumb{background:#555;border-radius:6px;transition:background .2s ease}.custom-container::-webkit-scrollbar-thumb:hover{background:#777}.custom-container::-webkit-scrollbar-corner{background:#1e1e1e}.custom-container{font-smooth:antialiased;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.json-null,.json-undefined{color:#f48771;font-style:italic;opacity:.8}.json-empty{color:gray;font-style:italic}.json-comma+br{margin-bottom:2px}\n"] }]
5322
- }], ctorParameters: () => [], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }] } });
5735
+ args: [{ selector: 'app-json-values-debuj', standalone: true, imports: [DatePipe, DecimalPipe], encapsulation: ViewEncapsulation.None, template: "<!-- json-values-debuj.component.html -->\r\n@if (shouldShowVisual()) {\r\n <div class=\"json-debug-container\">\r\n <div class=\"json-debug-header\">\r\n <span class=\"json-debug-id\">\uD83D\uDD0D Debug #{{ id }}</span>\r\n <span class=\"json-debug-info\">\r\n @if (debugValue()) {\r\n <span class=\"badge badge-success\">\u2713 Formulario activo</span>\r\n } @else {\r\n <span class=\"badge badge-danger\">\u2717 Sin formulario</span>\r\n }\r\n </span>\r\n </div>\r\n\r\n @if (debugValue()) {\r\n <div class=\"json-debug-content\">\r\n <!-- Informaci\u00F3n del archivo (si existe) -->\r\n @if (hasFile()) {\r\n <div class=\"file-info-box\">\r\n <div class=\"file-info-header\">\uD83D\uDCC1 Archivo seleccionado</div>\r\n <div class=\"file-info-grid\">\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Nombre:</span>\r\n <span class=\"value\">{{ getFileInfo()?.name || \"N/A\" }}</span>\r\n </div>\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Tama\u00F1o:</span>\r\n <span class=\"value\"\r\n >{{ getFileInfo()?.size | number }} bytes</span\r\n >\r\n </div>\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Tipo:</span>\r\n <span class=\"value\">{{\r\n getFileInfo()?.mimeType || \"N/A\"\r\n }}</span>\r\n </div>\r\n @if (getFileInfo()?.lastModified) {\r\n <div class=\"file-info-item\">\r\n <span class=\"label\">Modificado:</span>\r\n <span class=\"value\">{{\r\n getFileInfo()?.lastModified | date: \"medium\"\r\n }}</span>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- JSON -->\r\n <div class=\"json-display\">\r\n <pre class=\"json-colored\">{{ serializedValue() }}</pre>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: [".json-debug-container{margin:10px 0;border:1px solid #dee2e6;border-radius:8px;background-color:#f8f9fa;font-family:Consolas,Monaco,Courier New,monospace;font-size:13px;box-shadow:0 2px 8px #0000001a}.json-debug-header{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:8px 8px 0 0;color:#fff}.json-debug-id{font-weight:700;font-size:14px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.2)}.badge{padding:4px 14px;border-radius:20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.badge-success{background-color:#10b981;color:#fff;box-shadow:0 2px 4px #10b9814d}.badge-danger{background-color:#ef4444;color:#fff;box-shadow:0 2px 4px #ef44444d}.json-debug-content{padding:16px 20px}.file-info-box{background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border:1px solid #bae6fd;border-radius:6px;padding:12px 16px;margin-bottom:16px}.file-info-header{font-weight:600;color:#0369a1;font-size:13px;margin-bottom:8px}.file-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:6px}.file-info-item{padding:4px 8px;background-color:#fff;border-radius:4px;border:1px solid #e5e7eb}.file-info-item .label{font-weight:600;color:#6b7280;margin-right:8px;font-size:12px}.file-info-item .value{color:#0369a1;font-family:Consolas,Monaco,Courier New,monospace;font-size:12px}.json-display{margin-top:0}.json-colored{margin:0;padding:16px 20px;background-color:#1e1e1e;border-radius:6px;overflow:auto;font-size:13px;line-height:1.7;color:#d4d4d4;min-height:50px;max-height:500px;font-family:Consolas,Monaco,Courier New,monospace;white-space:pre}.json-colored::-webkit-scrollbar{width:10px;height:10px}.json-colored::-webkit-scrollbar-track{background:#2d2d2d;border-radius:4px}.json-colored::-webkit-scrollbar-thumb{background:#666;border-radius:4px}.json-colored::-webkit-scrollbar-thumb:hover{background:#888}@media(max-width:768px){.json-debug-header{flex-direction:column;gap:8px;padding:10px 16px}.json-debug-content{padding:12px 16px}.file-info-grid{grid-template-columns:1fr}.json-colored{font-size:11px;padding:12px 14px;max-height:300px}}\n"] }]
5736
+ }], ctorParameters: () => [], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }] } });
5323
5737
 
5324
5738
  class IcoLabel {
5325
5739
  ico = input.required(...(ngDevMode ? [{ debugName: "ico" }] : /* istanbul ignore next */ []));
@@ -8197,67 +8611,395 @@ class JoinByPipe {
8197
8611
  .filter((value) => value.length > 0)
8198
8612
  .join(separator);
8199
8613
  }
8200
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JoinByPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
8201
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: JoinByPipe, isStandalone: true, name: "joinBy" });
8202
- }
8203
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JoinByPipe, decorators: [{
8204
- type: Pipe,
8205
- args: [{
8206
- name: 'joinBy',
8207
- standalone: true,
8208
- }]
8209
- }] });
8210
-
8211
- class TruncatePipe {
8614
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JoinByPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
8615
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: JoinByPipe, isStandalone: true, name: "joinBy" });
8616
+ }
8617
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JoinByPipe, decorators: [{
8618
+ type: Pipe,
8619
+ args: [{
8620
+ name: 'joinBy',
8621
+ standalone: true,
8622
+ }]
8623
+ }] });
8624
+
8625
+ class JsonHighlightPipe {
8626
+ /**
8627
+ * Stringifica el JSON manteniendo arrays en una línea para una vista más compacta
8628
+ */
8629
+ compactStringify(obj, indent = 0) {
8630
+ const spaces = ' '.repeat(indent);
8631
+ if (obj === null) {
8632
+ return 'null';
8633
+ }
8634
+ if (typeof obj !== 'object') {
8635
+ if (typeof obj === 'string') {
8636
+ return JSON.stringify(obj);
8637
+ }
8638
+ return String(obj);
8639
+ }
8640
+ if (obj instanceof Date) {
8641
+ return JSON.stringify(obj.toISOString());
8642
+ }
8643
+ if (Array.isArray(obj)) {
8644
+ // Mantener arrays en una línea
8645
+ const items = obj.map((item) => {
8646
+ if (item instanceof Date) {
8647
+ return JSON.stringify(item.toISOString());
8648
+ }
8649
+ if (typeof item === 'object' && item !== null) {
8650
+ return this.compactStringify(item, indent);
8651
+ }
8652
+ return typeof item === 'string'
8653
+ ? JSON.stringify(item)
8654
+ : item === null
8655
+ ? 'null'
8656
+ : String(item);
8657
+ });
8658
+ return `[${items.join(', ')}]`;
8659
+ }
8660
+ // Para objetos, mantener la indentación
8661
+ const keys = Object.keys(obj);
8662
+ if (keys.length === 0) {
8663
+ return '{}';
8664
+ }
8665
+ const innerIndent = indent + 2;
8666
+ const innerSpaces = ' '.repeat(innerIndent);
8667
+ const lines = keys.map((key) => {
8668
+ const value = obj[key];
8669
+ const stringified = value instanceof Date
8670
+ ? JSON.stringify(value.toISOString())
8671
+ : typeof value === 'object' && value !== null
8672
+ ? this.compactStringify(value, innerIndent)
8673
+ : typeof value === 'string'
8674
+ ? JSON.stringify(value)
8675
+ : value === null
8676
+ ? 'null'
8677
+ : String(value);
8678
+ return `${innerSpaces}"${key}": ${stringified}`;
8679
+ });
8680
+ return `{\n${lines.join(',\n')}\n${spaces}}`;
8681
+ }
8682
+ instanceId = Math.random().toString(36).slice(2, 8);
8683
+ executions = 0;
8684
+ transform(value) {
8685
+ if (isDevMode()) {
8686
+ this.executions++;
8687
+ if (this.executions % 10 === 0) {
8688
+ console.log(`[jsonHighlight:${this.instanceId}] ${this.executions}`);
8689
+ }
8690
+ }
8691
+ if (!value) {
8692
+ return '';
8693
+ }
8694
+ const json = this.compactStringify(value);
8695
+ // Detecta fechas ISO: "YYYY-MM-DDTHH:MM:SS.mmmZ"
8696
+ const ISO_DATE_RE = /^"(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}\.\d{3}Z)"$/;
8697
+ // Aplica estilo y colores para diferenciar claves, valores, booleanos, números, etc.
8698
+ return json
8699
+ .replace(/&/g, '&amp;')
8700
+ .replace(/</g, '&lt;')
8701
+ .replace(/>/g, '&gt;')
8702
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => {
8703
+ // Si es una clave (tiene dos puntos al final)
8704
+ if (/:$/.test(match)) {
8705
+ return `<span class="json-key">${match}</span>`;
8706
+ }
8707
+ // Detectar fechas ISO (YYYY-MM-DDTHH:MM:SS.mmmZ)
8708
+ const isoMatch = match.match(ISO_DATE_RE);
8709
+ if (isoMatch) {
8710
+ return `<span class="json-string json-date">"<span class="json-date-part">${isoMatch[1]}</span><span class="json-date-sep">T</span><span class="json-date-time">${isoMatch[2]}</span>"</span>`;
8711
+ }
8712
+ // Si es un string (no tiene dos puntos)
8713
+ return `<span class="json-string">${match}</span>`;
8714
+ })
8715
+ .replace(/\b(true|false|null)\b/g, (match) => {
8716
+ if (match === 'null') {
8717
+ return '<span class="json-null">null</span>';
8718
+ }
8719
+ return `<span class="json-boolean">${match}</span>`;
8720
+ })
8721
+ .replace(/\b(\d+\.?\d*)\b/g, '<span class="json-number">$1</span>')
8722
+ .replace(/[{}\[\]]/g, '<span class="json-bracket">$&</span>')
8723
+ .replace(/,/g, '<span class="json-comma">$&</span>')
8724
+ .replace(/:/g, '<span class="json-colon">$&</span>');
8725
+ }
8726
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JsonHighlightPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
8727
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: JsonHighlightPipe, isStandalone: true, name: "jsonHighlight" });
8728
+ }
8729
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JsonHighlightPipe, decorators: [{
8730
+ type: Pipe,
8731
+ args: [{
8732
+ name: 'jsonHighlight',
8733
+ }]
8734
+ }] });
8735
+
8736
+ class TruncatePipe {
8737
+ /**
8738
+ * Transforma una cadena de texto truncándola según los parámetros proporcionados.
8739
+ *
8740
+ * @param value - La cadena de texto que se desea truncar.
8741
+ * @param limit - El número máximo de caracteres permitidos. Por defecto es 100. Si no se encuentra un espacio dentro del límite, usa el límite original (15).
8742
+ * @param completeWords - Si es true, evita cortar palabras a la mitad. Por defecto es false.
8743
+ * @param ellipsis - La cadena que se añadirá al final del texto truncado. Por defecto es '...'.
8744
+ * @returns La cadena truncada con el ellipsis añadido, si es necesario.
8745
+ */
8746
+ transform(value, limit = 20, completeWords = false, ellipsis = '...') {
8747
+ // Si el valor es nulo o indefinido, devuelve una cadena vacía para evitar errores.
8748
+ if (!value) {
8749
+ return '';
8750
+ }
8751
+ // Si completeWords es true, ajusta el límite para no cortar palabras.
8752
+ if (completeWords) {
8753
+ // Encuentra el último espacio dentro del límite para evitar cortar palabras.
8754
+ limit = value.slice(0, limit).lastIndexOf(' ');
8755
+ // Si no se encuentra un espacio dentro del límite, usa el límite original.
8756
+ if (limit < 0) {
8757
+ limit = 15; // Valor por defecto si no hay espacios.
8758
+ }
8759
+ }
8760
+ // Trunca el texto y añade el ellipsis si la longitud del texto supera el límite.
8761
+ return value.length > limit ? value.slice(0, limit) + ellipsis : value;
8762
+ }
8763
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TruncatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
8764
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: TruncatePipe, isStandalone: true, name: "truncate" });
8765
+ }
8766
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TruncatePipe, decorators: [{
8767
+ type: Pipe,
8768
+ args: [{
8769
+ name: 'truncate',
8770
+ standalone: true,
8771
+ }]
8772
+ }] });
8773
+
8774
+ // dsxdate-short.ts
8775
+ class DsxdateShort {
8776
+ injector = inject(Injector);
8777
+ // ✅ Exponer isDevMode como propiedad pública
8778
+ isDevMode = isDevMode();
8779
+ // === INPUTS CONFIGURABLES ===
8780
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
8781
+ placeholder = input('dd/mm/yyyy', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
8782
+ dateFormat = input('dd/mm/yy', ...(ngDevMode ? [{ debugName: "dateFormat" }] : /* istanbul ignore next */ []));
8783
+ showIcon = input(true, ...(ngDevMode ? [{ debugName: "showIcon" }] : /* istanbul ignore next */ []));
8784
+ showOnFocus = input(false, ...(ngDevMode ? [{ debugName: "showOnFocus" }] : /* istanbul ignore next */ []));
8785
+ showButtonBar = input(true, ...(ngDevMode ? [{ debugName: "showButtonBar" }] : /* istanbul ignore next */ []));
8786
+ debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : /* istanbul ignore next */ []));
8787
+ // === STATE ===
8788
+ debugEnabled = signal(false, ...(ngDevMode ? [{ debugName: "debugEnabled" }] : /* istanbul ignore next */ []));
8789
+ // === OBTENER EL CONTROL PADRE ===
8790
+ ngControl = computed(() => {
8791
+ try {
8792
+ return this.injector.get(NgControl);
8793
+ }
8794
+ catch {
8795
+ return null;
8796
+ }
8797
+ }, ...(ngDevMode ? [{ debugName: "ngControl" }] : /* istanbul ignore next */ []));
8798
+ // === CONTROL PADRE PARA ERRORES ===
8799
+ parentControl = computed(() => {
8800
+ const control = this.ngControl();
8801
+ return control?.control || null;
8802
+ }, ...(ngDevMode ? [{ debugName: "parentControl" }] : /* istanbul ignore next */ []));
8803
+ // === CONTROL INTERNO (para el template) ===
8804
+ fechaControl = new FormControl(null);
8805
+ // === CONTROL VALUE ACCESSOR ===
8806
+ onChange = () => { };
8807
+ onTouched = () => { };
8808
+ constructor() {
8809
+ // Debug effect
8810
+ effect(() => {
8811
+ const isDebug = this.debug() && this.isDevMode;
8812
+ this.debugEnabled.set(isDebug);
8813
+ if (isDebug) {
8814
+ console.log('[dsxdate-short] === DEBUG MODE ACTIVADO ===');
8815
+ console.log('[dsxdate-short] Configuración:', {
8816
+ label: this.label(),
8817
+ placeholder: this.placeholder(),
8818
+ dateFormat: this.dateFormat(),
8819
+ showIcon: this.showIcon(),
8820
+ showOnFocus: this.showOnFocus(),
8821
+ showButtonBar: this.showButtonBar(),
8822
+ });
8823
+ console.log('[dsxdate-short] Control padre:', this.parentControl());
8824
+ }
8825
+ });
8826
+ // ✅ SINCRONIZAR VALIDADORES DEL PADRE AL CONTROL INTERNO
8827
+ effect(() => {
8828
+ const parentCtrl = this.parentControl();
8829
+ if (parentCtrl) {
8830
+ // Copiar validadores del padre al control interno
8831
+ const validators = parentCtrl.validator;
8832
+ const asyncValidators = parentCtrl.asyncValidator;
8833
+ if (this.debugEnabled()) {
8834
+ console.log('[dsxdate-short] 🔄 Sincronizando validadores del padre');
8835
+ console.log('[dsxdate-short] Validadores del padre:', validators);
8836
+ }
8837
+ this.fechaControl.setValidators(validators ? [validators] : []);
8838
+ this.fechaControl.setAsyncValidators(asyncValidators ? [asyncValidators] : []);
8839
+ this.fechaControl.updateValueAndValidity({ emitEvent: false });
8840
+ if (this.debugEnabled()) {
8841
+ console.log('[dsxdate-short] ✅ Validadores sincronizados');
8842
+ console.log('[dsxdate-short] Estado del control interno:', {
8843
+ valid: this.fechaControl.valid,
8844
+ invalid: this.fechaControl.invalid,
8845
+ errors: this.fechaControl.errors,
8846
+ });
8847
+ }
8848
+ }
8849
+ });
8850
+ // Monitorear cambios del control padre
8851
+ effect(() => {
8852
+ if (this.debugEnabled()) {
8853
+ const parentCtrl = this.parentControl();
8854
+ console.log('[dsxdate-short] 📊 Parent Control state:', {
8855
+ value: parentCtrl?.value,
8856
+ status: parentCtrl?.status,
8857
+ errors: parentCtrl?.errors,
8858
+ valid: parentCtrl?.valid,
8859
+ invalid: parentCtrl?.invalid,
8860
+ touched: parentCtrl?.touched,
8861
+ dirty: parentCtrl?.dirty,
8862
+ });
8863
+ }
8864
+ });
8865
+ // Sincronizar cambios del control interno al padre
8866
+ this.fechaControl.valueChanges.subscribe((value) => {
8867
+ if (this.debugEnabled()) {
8868
+ console.log('[dsxdate-short] 🔄 valueChanges - Nuevo valor:', value);
8869
+ }
8870
+ this.onChange(value);
8871
+ });
8872
+ // Sincronizar touched
8873
+ this.fechaControl.statusChanges.subscribe(() => {
8874
+ if (this.debugEnabled()) {
8875
+ console.log('[dsxdate-short] 📊 statusChanges - Estado:', this.fechaControl.status);
8876
+ }
8877
+ this.onTouched();
8878
+ });
8879
+ }
8880
+ // === MÉTODOS DE DEBUG ===
8881
+ log(message, data) {
8882
+ if (this.debugEnabled() && this.isDevMode) {
8883
+ if (data !== undefined) {
8884
+ console.log(`[dsxdate-short] ${message}`, data);
8885
+ }
8886
+ else {
8887
+ console.log(`[dsxdate-short] ${message}`);
8888
+ }
8889
+ }
8890
+ }
8212
8891
  /**
8213
- * Transforma una cadena de texto truncándola según los parámetros proporcionados.
8214
- *
8215
- * @param value - La cadena de texto que se desea truncar.
8216
- * @param limit - El número máximo de caracteres permitidos. Por defecto es 100. Si no se encuentra un espacio dentro del límite, usa el límite original (15).
8217
- * @param completeWords - Si es true, evita cortar palabras a la mitad. Por defecto es false.
8218
- * @param ellipsis - La cadena que se añadirá al final del texto truncado. Por defecto es '...'.
8219
- * @returns La cadena truncada con el ellipsis añadido, si es necesario.
8892
+ * Convierte cualquier fecha (string ISO, Date, moment) a Date válido
8220
8893
  */
8221
- transform(value, limit = 20, completeWords = false, ellipsis = '...') {
8222
- // Si el valor es nulo o indefinido, devuelve una cadena vacía para evitar errores.
8223
- if (!value) {
8224
- return '';
8894
+ convertirFechaADate(fecha) {
8895
+ if (this.debugEnabled()) {
8896
+ this.log('🔄 convertirFechaADate - entrada:', fecha);
8225
8897
  }
8226
- // Si completeWords es true, ajusta el límite para no cortar palabras.
8227
- if (completeWords) {
8228
- // Encuentra el último espacio dentro del límite para evitar cortar palabras.
8229
- limit = value.slice(0, limit).lastIndexOf(' ');
8230
- // Si no se encuentra un espacio dentro del límite, usa el límite original.
8231
- if (limit < 0) {
8232
- limit = 15; // Valor por defecto si no hay espacios.
8898
+ if (!fecha) {
8899
+ this.log('convertirFechaADate - fecha vacía, retornando null');
8900
+ return null;
8901
+ }
8902
+ if (fecha instanceof Date && !isNaN(fecha.getTime())) {
8903
+ this.log('✅ convertirFechaADate - es Date válido:', fecha);
8904
+ return fecha;
8905
+ }
8906
+ if (typeof fecha === 'string') {
8907
+ this.log('📝 convertirFechaADate - procesando string:', fecha);
8908
+ const momentDate = moment(fecha);
8909
+ if (momentDate.isValid()) {
8910
+ const result = momentDate.toDate();
8911
+ this.log('✅ convertirFechaADate - string convertido a Date:', result);
8912
+ return result;
8233
8913
  }
8914
+ this.log('❌ convertirFechaADate - string inválido para moment');
8234
8915
  }
8235
- // Trunca el texto y añade el ellipsis si la longitud del texto supera el límite.
8236
- return value.length > limit ? value.slice(0, limit) + ellipsis : value;
8916
+ if (moment.isMoment(fecha)) {
8917
+ this.log('✅ convertirFechaADate - es objeto moment');
8918
+ return fecha.toDate();
8919
+ }
8920
+ const parsed = new Date(fecha);
8921
+ if (!isNaN(parsed.getTime())) {
8922
+ this.log('✅ convertirFechaADate - parseo directo exitoso:', parsed);
8923
+ return parsed;
8924
+ }
8925
+ this.log('❌ convertirFechaADate - no se pudo convertir, retornando null');
8926
+ return null;
8237
8927
  }
8238
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TruncatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
8239
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: TruncatePipe, isStandalone: true, name: "truncate" });
8928
+ // === CONTROL VALUE ACCESSOR IMPLEMENTATION ===
8929
+ writeValue(value) {
8930
+ if (this.debugEnabled()) {
8931
+ this.log('📥 writeValue - Valor recibido:', value);
8932
+ }
8933
+ const fechaConvertida = this.convertirFechaADate(value);
8934
+ if (this.debugEnabled()) {
8935
+ this.log('📥 writeValue - Valor convertido:', fechaConvertida);
8936
+ }
8937
+ this.fechaControl.setValue(fechaConvertida, { emitEvent: false });
8938
+ }
8939
+ registerOnChange(fn) {
8940
+ if (this.debugEnabled()) {
8941
+ this.log('📞 registerOnChange - Callback registrado');
8942
+ }
8943
+ this.onChange = fn;
8944
+ }
8945
+ registerOnTouched(fn) {
8946
+ if (this.debugEnabled()) {
8947
+ this.log('📞 registerOnTouched - Callback registrado');
8948
+ }
8949
+ this.onTouched = fn;
8950
+ }
8951
+ setDisabledState(isDisabled) {
8952
+ if (this.debugEnabled()) {
8953
+ this.log(`🚫 setDisabledState - Deshabilitado: ${isDisabled}`);
8954
+ }
8955
+ if (isDisabled) {
8956
+ this.fechaControl.disable({ emitEvent: false });
8957
+ }
8958
+ else {
8959
+ this.fechaControl.enable({ emitEvent: false });
8960
+ }
8961
+ }
8962
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxdateShort, deps: [], target: i0.ɵɵFactoryTarget.Component });
8963
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DsxdateShort, isStandalone: true, selector: "dsxdate-short", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, showIcon: { classPropertyName: "showIcon", publicName: "showIcon", isSignal: true, isRequired: false, transformFunction: null }, showOnFocus: { classPropertyName: "showOnFocus", publicName: "showOnFocus", isSignal: true, isRequired: false, transformFunction: null }, showButtonBar: { classPropertyName: "showButtonBar", publicName: "showButtonBar", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
8964
+ {
8965
+ provide: NG_VALUE_ACCESSOR,
8966
+ useExisting: forwardRef(() => DsxdateShort),
8967
+ multi: true,
8968
+ },
8969
+ ], ngImport: i0, template: "<!-- dsxdate-short.html -->\r\n<p-floatLabel variant=\"on\">\r\n <p-datepicker\r\n fluid\r\n appendTo=\"body\"\r\n [formControl]=\"fechaControl\"\r\n [fluid]=\"true\"\r\n [showIcon]=\"showIcon()\"\r\n [showOnFocus]=\"showOnFocus()\"\r\n [showButtonBar]=\"showButtonBar()\"\r\n [dateFormat]=\"dateFormat()\"\r\n [placeholder]=\"placeholder()\"\r\n pInputMask=\"99/99/9999\"\r\n />\r\n <label>{{ label() }}</label>\r\n</p-floatLabel>\r\n\r\n<!-- \u2705 SIN CONDICI\u00D3N - IGUAL QUE EL ORIGINAL -->\r\n<dsx-message-error [control]=\"fechaControl\" />\r\n\r\n<!-- Debug info -->\r\n@if (debugEnabled() && isDevMode) {\r\n<div\r\n class=\"debug-info\"\r\n style=\"\r\n font-size: 12px;\r\n color: #666;\r\n margin-top: 4px;\r\n border-top: 1px dashed #ccc;\r\n padding-top: 4px;\r\n \"\r\n>\r\n <small>\r\n <strong>Debug:</strong>\r\n Value: {{ fechaControl.value | json }} | Valid: {{ fechaControl.valid }} |\r\n Invalid: {{ fechaControl.invalid }} | Errors: {{ fechaControl.errors | json\r\n }} | Dirty: {{ fechaControl.dirty }} | Touched: {{ fechaControl.touched }} |\r\n Status: {{ fechaControl.status }}\r\n </small>\r\n</div>\r\n}\r\n", styles: [""], dependencies: [{ kind: "component", type: AppMessageErrorComponent, selector: "dsx-message-error", inputs: ["control", "form", "debugMode"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo", "motionOptions"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "component", type: FloatLabel, selector: "p-floatlabel, p-floatLabel, p-float-label", inputs: ["variant"] }, { kind: "ngmodule", type: InputMaskModule }, { kind: "directive", type: i1$8.InputMaskDirective, selector: "[pInputMask]", inputs: ["pInputMaskPT", "pInputMaskUnstyled", "pInputMask", "slotChar", "autoClear", "characterPattern", "keepBuffer"], outputs: ["onComplete", "onUnmaskedChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] });
8240
8970
  }
8241
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TruncatePipe, decorators: [{
8242
- type: Pipe,
8243
- args: [{
8244
- name: 'truncate',
8245
- standalone: true,
8246
- }]
8247
- }] });
8971
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxdateShort, decorators: [{
8972
+ type: Component,
8973
+ args: [{ selector: 'dsxdate-short', imports: [
8974
+ AppMessageErrorComponent,
8975
+ CommonModule,
8976
+ DatePicker,
8977
+ FloatLabel,
8978
+ InputMaskModule,
8979
+ JsonPipe,
8980
+ ReactiveFormsModule,
8981
+ ], providers: [
8982
+ {
8983
+ provide: NG_VALUE_ACCESSOR,
8984
+ useExisting: forwardRef(() => DsxdateShort),
8985
+ multi: true,
8986
+ },
8987
+ ], template: "<!-- dsxdate-short.html -->\r\n<p-floatLabel variant=\"on\">\r\n <p-datepicker\r\n fluid\r\n appendTo=\"body\"\r\n [formControl]=\"fechaControl\"\r\n [fluid]=\"true\"\r\n [showIcon]=\"showIcon()\"\r\n [showOnFocus]=\"showOnFocus()\"\r\n [showButtonBar]=\"showButtonBar()\"\r\n [dateFormat]=\"dateFormat()\"\r\n [placeholder]=\"placeholder()\"\r\n pInputMask=\"99/99/9999\"\r\n />\r\n <label>{{ label() }}</label>\r\n</p-floatLabel>\r\n\r\n<!-- \u2705 SIN CONDICI\u00D3N - IGUAL QUE EL ORIGINAL -->\r\n<dsx-message-error [control]=\"fechaControl\" />\r\n\r\n<!-- Debug info -->\r\n@if (debugEnabled() && isDevMode) {\r\n<div\r\n class=\"debug-info\"\r\n style=\"\r\n font-size: 12px;\r\n color: #666;\r\n margin-top: 4px;\r\n border-top: 1px dashed #ccc;\r\n padding-top: 4px;\r\n \"\r\n>\r\n <small>\r\n <strong>Debug:</strong>\r\n Value: {{ fechaControl.value | json }} | Valid: {{ fechaControl.valid }} |\r\n Invalid: {{ fechaControl.invalid }} | Errors: {{ fechaControl.errors | json\r\n }} | Dirty: {{ fechaControl.dirty }} | Touched: {{ fechaControl.touched }} |\r\n Status: {{ fechaControl.status }}\r\n </small>\r\n</div>\r\n}\r\n" }]
8988
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }], showOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "showOnFocus", required: false }] }], showButtonBar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtonBar", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }] } });
8248
8989
 
8249
- // dsxdate-short.ts
8250
- class DsxdateShort {
8990
+ class DsxdateTime {
8251
8991
  injector = inject(Injector);
8252
8992
  // ✅ Exponer isDevMode como propiedad pública
8253
8993
  isDevMode = isDevMode();
8254
8994
  // === INPUTS CONFIGURABLES ===
8255
8995
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
8256
- placeholder = input('dd/mm/yyyy', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
8996
+ placeholder = input('dd/mm/yyyy 00:00', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
8257
8997
  dateFormat = input('dd/mm/yy', ...(ngDevMode ? [{ debugName: "dateFormat" }] : /* istanbul ignore next */ []));
8258
8998
  showIcon = input(true, ...(ngDevMode ? [{ debugName: "showIcon" }] : /* istanbul ignore next */ []));
8259
8999
  showOnFocus = input(false, ...(ngDevMode ? [{ debugName: "showOnFocus" }] : /* istanbul ignore next */ []));
8260
9000
  showButtonBar = input(true, ...(ngDevMode ? [{ debugName: "showButtonBar" }] : /* istanbul ignore next */ []));
9001
+ showTime = input(true, ...(ngDevMode ? [{ debugName: "showTime" }] : /* istanbul ignore next */ []));
9002
+ hourFormat = input('24', ...(ngDevMode ? [{ debugName: "hourFormat" }] : /* istanbul ignore next */ []));
8261
9003
  debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : /* istanbul ignore next */ []));
8262
9004
  // === STATE ===
8263
9005
  debugEnabled = signal(false, ...(ngDevMode ? [{ debugName: "debugEnabled" }] : /* istanbul ignore next */ []));
@@ -8286,16 +9028,18 @@ class DsxdateShort {
8286
9028
  const isDebug = this.debug() && this.isDevMode;
8287
9029
  this.debugEnabled.set(isDebug);
8288
9030
  if (isDebug) {
8289
- console.log('[dsxdate-short] === DEBUG MODE ACTIVADO ===');
8290
- console.log('[dsxdate-short] Configuración:', {
9031
+ console.log('[dsxdate-datetime] === DEBUG MODE ACTIVADO ===');
9032
+ console.log('[dsxdate-datetime] Configuración:', {
8291
9033
  label: this.label(),
8292
9034
  placeholder: this.placeholder(),
8293
9035
  dateFormat: this.dateFormat(),
8294
9036
  showIcon: this.showIcon(),
8295
9037
  showOnFocus: this.showOnFocus(),
8296
9038
  showButtonBar: this.showButtonBar(),
9039
+ showTime: this.showTime(),
9040
+ hourFormat: this.hourFormat(),
8297
9041
  });
8298
- console.log('[dsxdate-short] Control padre:', this.parentControl());
9042
+ console.log('[dsxdate-datetime] Control padre:', this.parentControl());
8299
9043
  }
8300
9044
  });
8301
9045
  // ✅ SINCRONIZAR VALIDADORES DEL PADRE AL CONTROL INTERNO
@@ -8306,15 +9050,15 @@ class DsxdateShort {
8306
9050
  const validators = parentCtrl.validator;
8307
9051
  const asyncValidators = parentCtrl.asyncValidator;
8308
9052
  if (this.debugEnabled()) {
8309
- console.log('[dsxdate-short] 🔄 Sincronizando validadores del padre');
8310
- console.log('[dsxdate-short] Validadores del padre:', validators);
9053
+ console.log('[dsxdate-datetime] 🔄 Sincronizando validadores del padre');
9054
+ console.log('[dsxdate-datetime] Validadores del padre:', validators);
8311
9055
  }
8312
9056
  this.fechaControl.setValidators(validators ? [validators] : []);
8313
9057
  this.fechaControl.setAsyncValidators(asyncValidators ? [asyncValidators] : []);
8314
9058
  this.fechaControl.updateValueAndValidity({ emitEvent: false });
8315
9059
  if (this.debugEnabled()) {
8316
- console.log('[dsxdate-short] ✅ Validadores sincronizados');
8317
- console.log('[dsxdate-short] Estado del control interno:', {
9060
+ console.log('[dsxdate-datetime] ✅ Validadores sincronizados');
9061
+ console.log('[dsxdate-datetime] Estado del control interno:', {
8318
9062
  valid: this.fechaControl.valid,
8319
9063
  invalid: this.fechaControl.invalid,
8320
9064
  errors: this.fechaControl.errors,
@@ -8326,7 +9070,7 @@ class DsxdateShort {
8326
9070
  effect(() => {
8327
9071
  if (this.debugEnabled()) {
8328
9072
  const parentCtrl = this.parentControl();
8329
- console.log('[dsxdate-short] 📊 Parent Control state:', {
9073
+ console.log('[dsxdate-datetime] 📊 Parent Control state:', {
8330
9074
  value: parentCtrl?.value,
8331
9075
  status: parentCtrl?.status,
8332
9076
  errors: parentCtrl?.errors,
@@ -8340,14 +9084,14 @@ class DsxdateShort {
8340
9084
  // Sincronizar cambios del control interno al padre
8341
9085
  this.fechaControl.valueChanges.subscribe((value) => {
8342
9086
  if (this.debugEnabled()) {
8343
- console.log('[dsxdate-short] 🔄 valueChanges - Nuevo valor:', value);
9087
+ console.log('[dsxdate-datetime] 🔄 valueChanges - Nuevo valor:', value);
8344
9088
  }
8345
9089
  this.onChange(value);
8346
9090
  });
8347
9091
  // Sincronizar touched
8348
9092
  this.fechaControl.statusChanges.subscribe(() => {
8349
9093
  if (this.debugEnabled()) {
8350
- console.log('[dsxdate-short] 📊 statusChanges - Estado:', this.fechaControl.status);
9094
+ console.log('[dsxdate-datetime] 📊 statusChanges - Estado:', this.fechaControl.status);
8351
9095
  }
8352
9096
  this.onTouched();
8353
9097
  });
@@ -8356,10 +9100,10 @@ class DsxdateShort {
8356
9100
  log(message, data) {
8357
9101
  if (this.debugEnabled() && this.isDevMode) {
8358
9102
  if (data !== undefined) {
8359
- console.log(`[dsxdate-short] ${message}`, data);
9103
+ console.log(`[dsxdate-datetime] ${message}`, data);
8360
9104
  }
8361
9105
  else {
8362
- console.log(`[dsxdate-short] ${message}`);
9106
+ console.log(`[dsxdate-datetime] ${message}`);
8363
9107
  }
8364
9108
  }
8365
9109
  }
@@ -8434,32 +9178,33 @@ class DsxdateShort {
8434
9178
  this.fechaControl.enable({ emitEvent: false });
8435
9179
  }
8436
9180
  }
8437
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxdateShort, deps: [], target: i0.ɵɵFactoryTarget.Component });
8438
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DsxdateShort, isStandalone: true, selector: "dsxdate-short", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, showIcon: { classPropertyName: "showIcon", publicName: "showIcon", isSignal: true, isRequired: false, transformFunction: null }, showOnFocus: { classPropertyName: "showOnFocus", publicName: "showOnFocus", isSignal: true, isRequired: false, transformFunction: null }, showButtonBar: { classPropertyName: "showButtonBar", publicName: "showButtonBar", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
9181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxdateTime, deps: [], target: i0.ɵɵFactoryTarget.Component });
9182
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DsxdateTime, isStandalone: true, selector: "dsxdate-time", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, dateFormat: { classPropertyName: "dateFormat", publicName: "dateFormat", isSignal: true, isRequired: false, transformFunction: null }, showIcon: { classPropertyName: "showIcon", publicName: "showIcon", isSignal: true, isRequired: false, transformFunction: null }, showOnFocus: { classPropertyName: "showOnFocus", publicName: "showOnFocus", isSignal: true, isRequired: false, transformFunction: null }, showButtonBar: { classPropertyName: "showButtonBar", publicName: "showButtonBar", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null }, hourFormat: { classPropertyName: "hourFormat", publicName: "hourFormat", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
8439
9183
  {
8440
9184
  provide: NG_VALUE_ACCESSOR,
8441
- useExisting: forwardRef(() => DsxdateShort),
9185
+ useExisting: forwardRef(() => DsxdateTime),
8442
9186
  multi: true,
8443
9187
  },
8444
- ], ngImport: i0, template: "<!-- dsxdate-short.html -->\r\n<p-floatLabel variant=\"on\">\r\n <p-datepicker\r\n appendTo=\"body\"\r\n [formControl]=\"fechaControl\"\r\n [fluid]=\"true\"\r\n [showIcon]=\"showIcon()\"\r\n [showOnFocus]=\"showOnFocus()\"\r\n [showButtonBar]=\"showButtonBar()\"\r\n [dateFormat]=\"dateFormat()\"\r\n [placeholder]=\"placeholder()\"\r\n pInputMask=\"99/99/9999\"\r\n />\r\n <label>{{ label() }}</label>\r\n</p-floatLabel>\r\n\r\n<!-- \u2705 SIN CONDICI\u00D3N - IGUAL QUE EL ORIGINAL -->\r\n<dsx-message-error [control]=\"fechaControl\" />\r\n\r\n<!-- Debug info -->\r\n@if (debugEnabled() && isDevMode) {\r\n<div\r\n class=\"debug-info\"\r\n style=\"\r\n font-size: 12px;\r\n color: #666;\r\n margin-top: 4px;\r\n border-top: 1px dashed #ccc;\r\n padding-top: 4px;\r\n \"\r\n>\r\n <small>\r\n <strong>Debug:</strong>\r\n Value: {{ fechaControl.value | json }} | Valid: {{ fechaControl.valid }} |\r\n Invalid: {{ fechaControl.invalid }} | Errors: {{ fechaControl.errors | json\r\n }} | Dirty: {{ fechaControl.dirty }} | Touched: {{ fechaControl.touched }} |\r\n Status: {{ fechaControl.status }}\r\n </small>\r\n</div>\r\n}\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FloatLabel, selector: "p-floatlabel, p-floatLabel, p-float-label", inputs: ["variant"] }, { kind: "component", type: DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo", "motionOptions"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: AppMessageErrorComponent, selector: "dsx-message-error", inputs: ["control", "form", "debugMode"] }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] });
9188
+ ], ngImport: i0, template: "<p-floatLabel variant=\"on\">\r\n <p-datepicker\r\n fluid\r\n appendTo=\"body\"\r\n [formControl]=\"fechaControl\"\r\n [fluid]=\"true\"\r\n [showIcon]=\"showIcon()\"\r\n [showOnFocus]=\"showOnFocus()\"\r\n [showButtonBar]=\"showButtonBar()\"\r\n [showTime]=\"showTime()\"\r\n [hourFormat]=\"hourFormat()\"\r\n [dateFormat]=\"dateFormat()\"\r\n [placeholder]=\"placeholder()\"\r\n pInputMask=\"99/99/9999 99:99\"\r\n />\r\n <label>{{ label() }}</label>\r\n</p-floatLabel>\r\n\r\n<!-- \u2705 SIN CONDICI\u00D3N - IGUAL QUE EL ORIGINAL -->\r\n<dsx-message-error [control]=\"fechaControl\" />\r\n\r\n<!-- Debug info -->\r\n@if (debugEnabled() && isDevMode) {\r\n<div\r\n class=\"debug-info\"\r\n style=\"\r\n font-size: 12px;\r\n color: #666;\r\n margin-top: 4px;\r\n border-top: 1px dashed #ccc;\r\n padding-top: 4px;\r\n \"\r\n>\r\n <small>\r\n <strong>Debug:</strong>\r\n Value: {{ fechaControl.value | json }} | Valid: {{ fechaControl.valid }} |\r\n Invalid: {{ fechaControl.invalid }} | Errors: {{ fechaControl.errors | json\r\n }} | Dirty: {{ fechaControl.dirty }} | Touched: {{ fechaControl.touched }} |\r\n Status: {{ fechaControl.status }}\r\n </small>\r\n</div>\r\n}\r\n", styles: [""], dependencies: [{ kind: "component", type: AppMessageErrorComponent, selector: "dsx-message-error", inputs: ["control", "form", "debugMode"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo", "motionOptions"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "component", type: FloatLabel, selector: "p-floatlabel, p-floatLabel, p-float-label", inputs: ["variant"] }, { kind: "ngmodule", type: InputMaskModule }, { kind: "directive", type: i1$8.InputMaskDirective, selector: "[pInputMask]", inputs: ["pInputMaskPT", "pInputMaskUnstyled", "pInputMask", "slotChar", "autoClear", "characterPattern", "keepBuffer"], outputs: ["onComplete", "onUnmaskedChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] });
8445
9189
  }
8446
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxdateShort, decorators: [{
9190
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxdateTime, decorators: [{
8447
9191
  type: Component,
8448
- args: [{ selector: 'dsxdate-short', imports: [
9192
+ args: [{ selector: 'dsxdate-time', imports: [
9193
+ AppMessageErrorComponent,
8449
9194
  CommonModule,
8450
- FloatLabel,
8451
9195
  DatePicker,
8452
- ReactiveFormsModule,
8453
- AppMessageErrorComponent,
9196
+ FloatLabel,
9197
+ InputMaskModule,
8454
9198
  JsonPipe,
9199
+ ReactiveFormsModule,
8455
9200
  ], providers: [
8456
9201
  {
8457
9202
  provide: NG_VALUE_ACCESSOR,
8458
- useExisting: forwardRef(() => DsxdateShort),
9203
+ useExisting: forwardRef(() => DsxdateTime),
8459
9204
  multi: true,
8460
9205
  },
8461
- ], template: "<!-- dsxdate-short.html -->\r\n<p-floatLabel variant=\"on\">\r\n <p-datepicker\r\n appendTo=\"body\"\r\n [formControl]=\"fechaControl\"\r\n [fluid]=\"true\"\r\n [showIcon]=\"showIcon()\"\r\n [showOnFocus]=\"showOnFocus()\"\r\n [showButtonBar]=\"showButtonBar()\"\r\n [dateFormat]=\"dateFormat()\"\r\n [placeholder]=\"placeholder()\"\r\n pInputMask=\"99/99/9999\"\r\n />\r\n <label>{{ label() }}</label>\r\n</p-floatLabel>\r\n\r\n<!-- \u2705 SIN CONDICI\u00D3N - IGUAL QUE EL ORIGINAL -->\r\n<dsx-message-error [control]=\"fechaControl\" />\r\n\r\n<!-- Debug info -->\r\n@if (debugEnabled() && isDevMode) {\r\n<div\r\n class=\"debug-info\"\r\n style=\"\r\n font-size: 12px;\r\n color: #666;\r\n margin-top: 4px;\r\n border-top: 1px dashed #ccc;\r\n padding-top: 4px;\r\n \"\r\n>\r\n <small>\r\n <strong>Debug:</strong>\r\n Value: {{ fechaControl.value | json }} | Valid: {{ fechaControl.valid }} |\r\n Invalid: {{ fechaControl.invalid }} | Errors: {{ fechaControl.errors | json\r\n }} | Dirty: {{ fechaControl.dirty }} | Touched: {{ fechaControl.touched }} |\r\n Status: {{ fechaControl.status }}\r\n </small>\r\n</div>\r\n}\r\n" }]
8462
- }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }], showOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "showOnFocus", required: false }] }], showButtonBar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtonBar", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }] } });
9206
+ ], template: "<p-floatLabel variant=\"on\">\r\n <p-datepicker\r\n fluid\r\n appendTo=\"body\"\r\n [formControl]=\"fechaControl\"\r\n [fluid]=\"true\"\r\n [showIcon]=\"showIcon()\"\r\n [showOnFocus]=\"showOnFocus()\"\r\n [showButtonBar]=\"showButtonBar()\"\r\n [showTime]=\"showTime()\"\r\n [hourFormat]=\"hourFormat()\"\r\n [dateFormat]=\"dateFormat()\"\r\n [placeholder]=\"placeholder()\"\r\n pInputMask=\"99/99/9999 99:99\"\r\n />\r\n <label>{{ label() }}</label>\r\n</p-floatLabel>\r\n\r\n<!-- \u2705 SIN CONDICI\u00D3N - IGUAL QUE EL ORIGINAL -->\r\n<dsx-message-error [control]=\"fechaControl\" />\r\n\r\n<!-- Debug info -->\r\n@if (debugEnabled() && isDevMode) {\r\n<div\r\n class=\"debug-info\"\r\n style=\"\r\n font-size: 12px;\r\n color: #666;\r\n margin-top: 4px;\r\n border-top: 1px dashed #ccc;\r\n padding-top: 4px;\r\n \"\r\n>\r\n <small>\r\n <strong>Debug:</strong>\r\n Value: {{ fechaControl.value | json }} | Valid: {{ fechaControl.valid }} |\r\n Invalid: {{ fechaControl.invalid }} | Errors: {{ fechaControl.errors | json\r\n }} | Dirty: {{ fechaControl.dirty }} | Touched: {{ fechaControl.touched }} |\r\n Status: {{ fechaControl.status }}\r\n </small>\r\n</div>\r\n}\r\n" }]
9207
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], dateFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFormat", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }], showOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "showOnFocus", required: false }] }], showButtonBar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtonBar", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], hourFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "hourFormat", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }] } });
8463
9208
 
8464
9209
  // dsx-input-currency.ts
8465
9210
  class DsxInputCurrency {
@@ -8702,6 +9447,10 @@ class DsxAutocomplete {
8702
9447
  ngControl = null;
8703
9448
  isDevModeInternal = isDevMode();
8704
9449
  controlSignal = signal(null, ...(ngDevMode ? [{ debugName: "controlSignal" }] : /* istanbul ignore next */ []));
9450
+ // ✅ Almacenar IDs pendientes (privado)
9451
+ pendingIdsInternal = [];
9452
+ pendingValue = null;
9453
+ pendingLogs = [];
8705
9454
  datasource = input.required(...(ngDevMode ? [{ debugName: "datasource" }] : /* istanbul ignore next */ []));
8706
9455
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : /* istanbul ignore next */ []));
8707
9456
  label = input('Seleccionar', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
@@ -8723,6 +9472,14 @@ class DsxAutocomplete {
8723
9472
  get isDevMode() {
8724
9473
  return this.isDevModeInternal;
8725
9474
  }
9475
+ // ✅ Getter público para IDs pendientes (solo para debug)
9476
+ get pendingIds() {
9477
+ return this.pendingIdsInternal;
9478
+ }
9479
+ // ✅ Getter para logs de pendientes
9480
+ get pendingLogsInfo() {
9481
+ return this.pendingLogs.join(' | ');
9482
+ }
8726
9483
  get formValue() {
8727
9484
  return this.ngControl?.control?.value;
8728
9485
  }
@@ -8773,11 +9530,19 @@ class DsxAutocomplete {
8773
9530
  });
8774
9531
  }
8775
9532
  });
9533
+ // ✅ Effect mejorado para recargar cuando cambia el datasource
8776
9534
  effect(() => {
8777
9535
  const ds = this.datasource();
8778
- if (ds.length > 0 && this.initialValueSet) {
9536
+ if (ds.length > 0) {
8779
9537
  this.logDebug('📚 Datasource actualizado:', ds.length);
8780
- if (this.ngControl?.control?.value) {
9538
+ // Si hay IDs pendientes, intentar cargarlos
9539
+ if (this.pendingIdsInternal.length > 0) {
9540
+ this.logDebug('🔄 Procesando IDs pendientes:', this.pendingIdsInternal);
9541
+ this.procesarIdsPendientes(this.pendingIdsInternal);
9542
+ // ✅ No limpiamos inmediatamente para poder ver el estado en debug
9543
+ }
9544
+ // ✅ Si ya se inicializó y hay valor en el formulario, recargar
9545
+ if (this.initialValueSet && this.ngControl?.control?.value) {
8781
9546
  this.logDebug('🔄 Recargando valor con nuevo datasource');
8782
9547
  this.writeValue(this.ngControl.control.value);
8783
9548
  }
@@ -8824,6 +9589,70 @@ class DsxAutocomplete {
8824
9589
  this.lastLogTime = now;
8825
9590
  console.log(`[DsxAutocomplete] ${message}`, data || '');
8826
9591
  }
9592
+ // ✅ Método para registrar eventos de pendientes
9593
+ logPending(message, ids) {
9594
+ if (!this.shouldDebug())
9595
+ return;
9596
+ const timestamp = new Date().toISOString().substring(11, 19);
9597
+ const logEntry = `[${timestamp}] ${message} ${ids ? `IDs: [${ids.join(', ')}]` : ''}`;
9598
+ this.pendingLogs.push(logEntry);
9599
+ // Mantener solo los últimos 10 logs
9600
+ if (this.pendingLogs.length > 10) {
9601
+ this.pendingLogs.shift();
9602
+ }
9603
+ console.log(`[DsxAutocomplete] ⏳ PENDING: ${logEntry}`, {
9604
+ pendingIds: this.pendingIdsInternal,
9605
+ totalPending: this.pendingIdsInternal.length,
9606
+ });
9607
+ }
9608
+ // ✅ Nuevo método: Procesar IDs pendientes con logs mejorados
9609
+ procesarIdsPendientes(ids) {
9610
+ if (ids.length === 0) {
9611
+ this.logDebug('✅ No hay IDs pendientes para procesar');
9612
+ return;
9613
+ }
9614
+ const sourceData = this.datasource();
9615
+ if (sourceData.length === 0) {
9616
+ this.logPending('⚠️ Datasource vacío, no se pueden procesar IDs pendientes', ids);
9617
+ return;
9618
+ }
9619
+ this.logPending(`🔍 Procesando ${ids.length} IDs pendientes`, ids);
9620
+ const objetos = sourceData.filter((item) => ids.includes(Number(item[this.idKey()])));
9621
+ const objetosOrdenados = ids
9622
+ .map((id) => objetos.find((item) => Number(item[this.idKey()]) === id))
9623
+ .filter((item) => item !== undefined);
9624
+ const encontrados = objetosOrdenados.length;
9625
+ const noEncontrados = ids.length - encontrados;
9626
+ this.logPending(`✅ Procesados: ${encontrados} encontrados, ${noEncontrados} no encontrados`, ids);
9627
+ if (noEncontrados > 0) {
9628
+ const idsNoEncontrados = ids.filter((id) => !objetos.some((item) => Number(item[this.idKey()]) === id));
9629
+ this.logPending(`⚠️ IDs no encontrados en datasource:`, idsNoEncontrados);
9630
+ }
9631
+ // Si se encontraron todos los objetos, actualizar la vista
9632
+ if (encontrados > 0) {
9633
+ if (this.isMultiple()) {
9634
+ this.value = objetosOrdenados;
9635
+ }
9636
+ else {
9637
+ this.value = objetosOrdenados.length > 0 ? objetosOrdenados[0] : null;
9638
+ }
9639
+ this.cdr.detectChanges();
9640
+ // ✅ Limpiar pendientes solo si se encontraron todos
9641
+ if (noEncontrados === 0) {
9642
+ this.pendingIdsInternal = [];
9643
+ this.logPending('✅ Todos los IDs pendientes fueron resueltos', ids);
9644
+ }
9645
+ else {
9646
+ // Mantener solo los no encontrados
9647
+ const idsNoEncontrados = ids.filter((id) => !objetos.some((item) => Number(item[this.idKey()]) === id));
9648
+ this.pendingIdsInternal = idsNoEncontrados;
9649
+ this.logPending(`⏳ ${idsNoEncontrados.length} IDs pendientes aún no resueltos`, idsNoEncontrados);
9650
+ }
9651
+ }
9652
+ else {
9653
+ this.logPending('❌ Ningún ID pendiente pudo ser resuelto', ids);
9654
+ }
9655
+ }
8827
9656
  searchRequisitos(event) {
8828
9657
  const inicio = performance.now();
8829
9658
  const label = this.optionLabel();
@@ -8862,13 +9691,10 @@ class DsxAutocomplete {
8862
9691
  .replace(/\s+/g, ' ')
8863
9692
  .trim();
8864
9693
  }
8865
- // ✨ CORREGIDO: evaluarYAgregar para manejar valores no encontrados
8866
9694
  evaluarYAgregar(event) {
8867
9695
  const valorInput = event.target.value?.trim();
8868
9696
  if (!valorInput) {
8869
9697
  this.dataFiltrada.set([]);
8870
- // Si el input está vacío y hay valor seleccionado, mantenerlo
8871
- // Si no hay valor, asegurar que el formulario tenga null o []
8872
9698
  if (this.obtenerArrayValue().length === 0) {
8873
9699
  this.actualizarFormulario([]);
8874
9700
  }
@@ -8929,11 +9755,8 @@ class DsxAutocomplete {
8929
9755
  this.logDebug('🆕 Nuevo elemento creado', valorInput);
8930
9756
  }
8931
9757
  else {
8932
- // ✅ NO permite crear: limpiar el valor a null o []
8933
9758
  this.logDebug('⚠️ No se encontraron coincidencias y no se permite crear', valorInput);
8934
- // ✨ IMPORTANTE: Limpiar la selección a null o []
8935
9759
  this.actualizarFormulario([]);
8936
- // Marcar como touched para mostrar validación
8937
9760
  if (this.ngControl?.control) {
8938
9761
  this.ngControl.control.markAsTouched();
8939
9762
  this.onTouched();
@@ -9059,9 +9882,7 @@ class DsxAutocomplete {
9059
9882
  this.timeoutLimpieza = null;
9060
9883
  }
9061
9884
  }
9062
- // ✨ MEJORADO: actualizarFormulario para manejar correctamente valores vacíos
9063
9885
  actualizarFormulario(nuevaLista) {
9064
- // Si la lista está vacía, siempre enviar null o [] según el modo
9065
9886
  if (nuevaLista.length === 0) {
9066
9887
  if (this.isMultiple()) {
9067
9888
  this.value = [];
@@ -9077,7 +9898,6 @@ class DsxAutocomplete {
9077
9898
  });
9078
9899
  return;
9079
9900
  }
9080
- // Si hay elementos, procesar normalmente
9081
9901
  if (this.isMultiple()) {
9082
9902
  this.value = nuevaLista;
9083
9903
  }
@@ -9096,6 +9916,7 @@ class DsxAutocomplete {
9096
9916
  getFormControlName() {
9097
9917
  return this.ngControl?.name || null;
9098
9918
  }
9919
+ // ✨ MEJORADO: writeValue con manejo de IDs pendientes y logs
9099
9920
  writeValue(value) {
9100
9921
  const timestamp = new Date().toISOString();
9101
9922
  this.logDebug(`🔄 writeValue INVOCADO ${timestamp}`, {
@@ -9113,10 +9934,16 @@ class DsxAutocomplete {
9113
9934
  isMultiple: this.isMultiple(),
9114
9935
  hasDatasource: this.datasource().length > 0,
9115
9936
  datasourceSize: this.datasource().length,
9937
+ pendingIdsActuales: this.pendingIdsInternal,
9116
9938
  });
9117
9939
  if (value === null || value === undefined) {
9118
9940
  this.logDebug(`⚠️ Valor null/undefined, limpiando selección`);
9119
9941
  this.value = this.isMultiple() ? [] : null;
9942
+ if (this.pendingIdsInternal.length > 0) {
9943
+ this.logPending('🧹 Limpiando IDs pendientes por null/undefined', this.pendingIdsInternal);
9944
+ this.pendingIdsInternal = [];
9945
+ }
9946
+ this.pendingValue = null;
9120
9947
  this.initialValueSet = true;
9121
9948
  this.cdr.markForCheck();
9122
9949
  this.cdr.detectChanges();
@@ -9127,6 +9954,11 @@ class DsxAutocomplete {
9127
9954
  if (Array.isArray(value)) {
9128
9955
  if (value.length === 0) {
9129
9956
  this.value = this.isMultiple() ? [] : null;
9957
+ if (this.pendingIdsInternal.length > 0) {
9958
+ this.logPending('🧹 Limpiando IDs pendientes por array vacío', this.pendingIdsInternal);
9959
+ this.pendingIdsInternal = [];
9960
+ }
9961
+ this.pendingValue = null;
9130
9962
  this.initialValueSet = true;
9131
9963
  this.cdr.markForCheck();
9132
9964
  this.cdr.detectChanges();
@@ -9134,41 +9966,73 @@ class DsxAutocomplete {
9134
9966
  }
9135
9967
  if (typeof value[0] === 'number') {
9136
9968
  ids = value;
9969
+ this.logDebug(`📊 Recibidos ${ids.length} IDs:`, ids);
9137
9970
  }
9138
9971
  else if (typeof value[0] === 'object' && value[0] !== null) {
9139
9972
  items = value;
9973
+ this.logDebug(`📊 Recibidos ${items.length} objetos:`, items.map((i) => i[this.optionLabel()]));
9140
9974
  }
9141
9975
  }
9142
9976
  else if (typeof value === 'number') {
9143
9977
  ids = [value];
9978
+ this.logDebug(`📊 Recibido 1 ID:`, [value]);
9144
9979
  }
9145
9980
  else if (typeof value === 'object' && value !== null) {
9146
9981
  items = [value];
9982
+ this.logDebug(`📊 Recibido 1 objeto:`, value[this.optionLabel()]);
9147
9983
  }
9148
9984
  else {
9149
9985
  this.value = this.isMultiple() ? [] : null;
9986
+ if (this.pendingIdsInternal.length > 0) {
9987
+ this.logPending('🧹 Limpiando IDs pendientes por tipo no soportado', this.pendingIdsInternal);
9988
+ this.pendingIdsInternal = [];
9989
+ }
9990
+ this.pendingValue = null;
9150
9991
  this.initialValueSet = true;
9151
9992
  this.cdr.markForCheck();
9152
9993
  this.cdr.detectChanges();
9153
9994
  return;
9154
9995
  }
9155
9996
  const sourceData = this.datasource();
9156
- if (ids.length > 0) {
9157
- if (sourceData.length === 0) {
9158
- this.logDebug(`⚠️ Datasource vacío, guardando IDs para luego`);
9159
- this.value = this.isMultiple() ? [] : null;
9160
- this.initialValueSet = true;
9161
- this.cdr.markForCheck();
9162
- this.cdr.detectChanges();
9163
- return;
9164
- }
9997
+ // Si tenemos IDs y el datasource está vacío, guardarlos para después
9998
+ if (ids.length > 0 && sourceData.length === 0) {
9999
+ this.logPending(`⏳ Datasource vacío (${sourceData.length}), guardando ${ids.length} IDs para procesar después`, ids);
10000
+ this.pendingIdsInternal = ids;
10001
+ this.pendingValue = value;
10002
+ this.initialValueSet = true;
10003
+ // ✅ Mostrar un placeholder visual mientras se cargan los datos
10004
+ this.value = this.isMultiple() ? [] : null;
10005
+ this.cdr.markForCheck();
10006
+ this.cdr.detectChanges();
10007
+ this.logDebug(`⏳ IDs guardados como pendientes:`, this.pendingIdsInternal);
10008
+ return;
10009
+ }
10010
+ // ✅ Si tenemos IDs y el datasource existe, buscar los objetos
10011
+ if (ids.length > 0 && sourceData.length > 0) {
10012
+ this.logDebug(`🔍 Buscando ${ids.length} IDs en datasource de ${sourceData.length} elementos`);
9165
10013
  const objetos = sourceData.filter((item) => ids.includes(Number(item[this.idKey()])));
9166
10014
  const objetosOrdenados = ids
9167
10015
  .map((id) => objetos.find((item) => Number(item[this.idKey()]) === id))
9168
10016
  .filter((item) => item !== undefined);
9169
10017
  items = objetosOrdenados;
10018
+ // ✅ Si no se encontraron algunos IDs, guardarlos como pendientes
10019
+ const idsNoEncontrados = ids.filter((id) => !objetos.some((item) => Number(item[this.idKey()]) === id));
10020
+ if (idsNoEncontrados.length > 0) {
10021
+ this.logPending(`⚠️ ${idsNoEncontrados.length} IDs no encontrados en datasource`, idsNoEncontrados);
10022
+ this.pendingIdsInternal = idsNoEncontrados;
10023
+ this.pendingValue = value;
10024
+ }
10025
+ else {
10026
+ // ✅ Todos los IDs fueron encontrados
10027
+ if (this.pendingIdsInternal.length > 0) {
10028
+ this.logPending('✅ Todos los IDs pendientes fueron resueltos', this.pendingIdsInternal);
10029
+ this.pendingIdsInternal = [];
10030
+ this.pendingValue = null;
10031
+ }
10032
+ }
9170
10033
  this.logDebug(`🔍 Encontrados ${items.length} de ${ids.length} IDs`);
9171
10034
  }
10035
+ // ✅ Verificar que los items existan en el datasource
9172
10036
  if (items.length > 0 && sourceData.length > 0) {
9173
10037
  const itemsValidos = items.filter((item) => sourceData.some((s) => s[this.idKey()] === item[this.idKey()]));
9174
10038
  if (itemsValidos.length !== items.length) {
@@ -9179,6 +10043,7 @@ class DsxAutocomplete {
9179
10043
  items = itemsValidos;
9180
10044
  }
9181
10045
  }
10046
+ // ✅ Asignar el valor
9182
10047
  if (this.isMultiple()) {
9183
10048
  this.value = items;
9184
10049
  }
@@ -9188,6 +10053,9 @@ class DsxAutocomplete {
9188
10053
  this.initialValueSet = true;
9189
10054
  this.cdr.detectChanges();
9190
10055
  this.logDebug(`✅ Vista actualizada con ${items.length} elementos (${this.isMultiple() ? 'multiple' : 'single'})`);
10056
+ if (this.pendingIdsInternal.length > 0) {
10057
+ this.logDebug(`⏳ IDs pendientes actuales:`, this.pendingIdsInternal);
10058
+ }
9191
10059
  }
9192
10060
  forceReload() {
9193
10061
  this.logDebug('🔄 Forzando recarga');
@@ -9214,7 +10082,7 @@ class DsxAutocomplete {
9214
10082
  useExisting: forwardRef(() => DsxAutocomplete),
9215
10083
  multi: true,
9216
10084
  },
9217
- ], ngImport: i0, template: "<p-floatlabel variant=\"on\">\r\n <p-autoComplete\r\n fluid\r\n appendTo=\"body\"\r\n [multiple]=\"isMultiple()\"\r\n [dropdown]=\"true\"\r\n [showClear]=\"true\"\r\n [suggestions]=\"dataFiltrada()\"\r\n (completeMethod)=\"searchRequisitos($event)\"\r\n [(ngModel)]=\"value\"\r\n (ngModelChange)=\"onNgModelChange($event)\"\r\n (onBlur)=\"onTouched()\"\r\n (onShow)=\"onShow()\"\r\n (onHide)=\"onHide()\"\r\n [disabled]=\"disabled\"\r\n [optionLabel]=\"optionLabel()\"\r\n [dataKey]=\"dataKey() || idKey()\"\r\n (keyup.enter)=\"evaluarYAgregar($event)\"\r\n [delay]=\"delay()\"\r\n placeholder=\"Ingresa un texto....\"\r\n [inputStyle]=\"{'text-transform': 'uppercase'}\"\r\n >\r\n <ng-template let-item pTemplate=\"selectedItem\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n\r\n <ng-template let-item pTemplate=\"item\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n </p-autoComplete>\r\n <label>{{ label() }}</label>\r\n</p-floatlabel>\r\n\r\n<!-- Bot\u00F3n de limpiar para modo single -->\r\n@if (!isMultiple() && value) {\r\n<button\r\n type=\"button\"\r\n (click)=\"clearSelection()\"\r\n style=\"\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n background: transparent;\r\n border: none;\r\n cursor: pointer;\r\n color: #999;\r\n font-size: 14px;\r\n z-index: 1;\r\n \"\r\n aria-label=\"Limpiar selecci\u00F3n\"\r\n>\r\n \u2715\r\n</button>\r\n}\r\n\r\n<!-- Mensaje de error -->\r\n@if (showError() && errorControl()) {\r\n<dsx-message-error [control]=\"errorControl()\" />\r\n}\r\n\r\n<!-- Panel de Debug -->\r\n@if (debug() && isDevMode) {\r\n<div\r\n style=\"\r\n margin-top: 8px;\r\n font-size: 11px;\r\n color: #666;\r\n background: #f5f5f5;\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n font-family: monospace;\r\n border: 1px solid #ddd;\r\n \"\r\n>\r\n <div\r\n style=\"\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n \"\r\n >\r\n <div>\r\n <strong>\uD83D\uDD0D Debug:</strong>\r\n <span style=\"margin-left: 8px\">Items: {{ getValueLength() }}</span>\r\n <span style=\"margin-left: 8px\">IDs: {{ getValueDisplay('id') }}</span>\r\n <span style=\"margin-left: 8px\"\r\n >Nombres: {{ getValueDisplay('nombre') }}</span\r\n >\r\n <span style=\"margin-left: 8px; color: #888; font-size: 10px\">\r\n | returnID: {{ returnID() ? '\u2705 IDs' : '\u274C Objetos' }} | Multiple: {{\r\n isMultiple() ? '\u2705' : '\u274C' }} | Control: {{ controlName || 'sin nombre'\r\n }}\r\n </span>\r\n </div>\r\n <div\r\n style=\"\r\n font-size: 10px;\r\n color: #333;\r\n background: #e8e8e8;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n \"\r\n >\r\n <strong>\uD83D\uDCCA Validaci\u00F3n:</strong>\r\n <span [style.color]=\"controlValid ? 'green' : 'red'\">\r\n {{ controlValid ? '\u2705 V\u00E1lido' : '\u274C Inv\u00E1lido' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Touched: {{ controlTouched ? '\u2705' : '\u274C' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Dirty: {{ controlDirty ? '\u2705' : '\u274C' }}\r\n </span>\r\n @if (controlErrors) {\r\n <span style=\"margin-left: 4px; color: red\">\r\n | Errors: {{ controlErrors | json }}\r\n </span>\r\n }\r\n </div>\r\n <button\r\n (click)=\"forceReload()\"\r\n style=\"\r\n font-size: 10px;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n border: 1px solid #ccc;\r\n background: white;\r\n cursor: pointer;\r\n \"\r\n type=\"button\"\r\n >\r\n \uD83D\uDD04 Recargar\r\n </button>\r\n </div>\r\n</div>\r\n}\r\n", styles: [""], dependencies: [{ kind: "component", type: AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: FloatLabel, selector: "p-floatlabel, p-floatLabel, p-float-label", inputs: ["variant"] }, { kind: "component", type: AppMessageErrorComponent, selector: "dsx-message-error", inputs: ["control", "form", "debugMode"] }, { kind: "pipe", type: JsonPipe, name: "json" }] });
10085
+ ], ngImport: i0, template: "<p-floatlabel variant=\"on\">\r\n <p-autoComplete\r\n fluid\r\n [multiple]=\"isMultiple()\"\r\n [dropdown]=\"true\"\r\n [showClear]=\"true\"\r\n [suggestions]=\"dataFiltrada()\"\r\n (completeMethod)=\"searchRequisitos($event)\"\r\n [(ngModel)]=\"value\"\r\n (ngModelChange)=\"onNgModelChange($event)\"\r\n (onBlur)=\"onTouched()\"\r\n (onShow)=\"onShow()\"\r\n (onHide)=\"onHide()\"\r\n [disabled]=\"disabled\"\r\n [optionLabel]=\"optionLabel()\"\r\n [dataKey]=\"dataKey() || idKey()\"\r\n (keyup.enter)=\"evaluarYAgregar($event)\"\r\n [delay]=\"delay()\"\r\n placeholder=\"Ingresa un texto....\"\r\n [inputStyle]=\"{'text-transform': 'uppercase'}\"\r\n >\r\n <ng-template let-item pTemplate=\"selectedItem\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n\r\n <ng-template let-item pTemplate=\"item\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n </p-autoComplete>\r\n <label>{{ label() }}</label>\r\n</p-floatlabel>\r\n\r\n<!-- \u2728 Indicador de carga (cuando hay IDs pendientes) -->\r\n@if (pendingIds.length > 0 && debug() && isDevMode) {\r\n<div\r\n style=\"\r\n font-size: 10px;\r\n color: #f59e0b;\r\n margin-top: 2px;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n \"\r\n>\r\n <span>\u23F3</span>\r\n <span\r\n >Cargando datos pendientes ({{ pendingIds.length }} IDs: [{{\r\n pendingIds.join(', ') }}])</span\r\n >\r\n</div>\r\n}\r\n\r\n<!-- \u2728 Bot\u00F3n de limpiar para modo single -->\r\n@if (!isMultiple() && value) {\r\n<button\r\n type=\"button\"\r\n (click)=\"clearSelection()\"\r\n style=\"\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n background: transparent;\r\n border: none;\r\n cursor: pointer;\r\n color: #999;\r\n font-size: 14px;\r\n z-index: 1;\r\n \"\r\n aria-label=\"Limpiar selecci\u00F3n\"\r\n>\r\n \u2715\r\n</button>\r\n}\r\n\r\n<!-- Mensaje de error -->\r\n@if (showError() && errorControl()) {\r\n<dsx-message-error [control]=\"errorControl()\" />\r\n}\r\n\r\n<!-- Panel de Debug -->\r\n@if (debug() && isDevMode) {\r\n<div\r\n style=\"\r\n margin-top: 8px;\r\n font-size: 11px;\r\n color: #666;\r\n background: #f5f5f5;\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n font-family: monospace;\r\n border: 1px solid #ddd;\r\n \"\r\n>\r\n <div\r\n style=\"\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n \"\r\n >\r\n <div>\r\n <strong>\uD83D\uDD0D Debug:</strong>\r\n <span style=\"margin-left: 8px\">Items: {{ getValueLength() }}</span>\r\n <span style=\"margin-left: 8px\">IDs: {{ getValueDisplay('id') }}</span>\r\n <span style=\"margin-left: 8px\"\r\n >Nombres: {{ getValueDisplay('nombre') }}</span\r\n >\r\n <span style=\"margin-left: 8px; color: #888; font-size: 10px\">\r\n | returnID: {{ returnID() ? '\u2705 IDs' : '\u274C Objetos' }} | Multiple: {{\r\n isMultiple() ? '\u2705' : '\u274C' }} | Control: {{ controlName || 'sin nombre'\r\n }}\r\n </span>\r\n @if (pendingIds.length > 0) {\r\n <span style=\"margin-left: 8px; color: #f59e0b; font-weight: bold\">\r\n | \u23F3 Pendientes: {{ pendingIds.length }} ({{ pendingIds.join(', ') }})\r\n </span>\r\n } @else {\r\n <span style=\"margin-left: 8px; color: #10b981; font-weight: bold\">\r\n | \u2705 Sin pendientes\r\n </span>\r\n }\r\n <span style=\"margin-left: 8px; color: #888; font-size: 9px\">\r\n | Logs: {{ pendingLogsInfo }}\r\n </span>\r\n </div>\r\n <div\r\n style=\"\r\n font-size: 10px;\r\n color: #333;\r\n background: #e8e8e8;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n \"\r\n >\r\n <strong>\uD83D\uDCCA Validaci\u00F3n:</strong>\r\n <span [style.color]=\"controlValid ? 'green' : 'red'\">\r\n {{ controlValid ? '\u2705 V\u00E1lido' : '\u274C Inv\u00E1lido' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Touched: {{ controlTouched ? '\u2705' : '\u274C' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Dirty: {{ controlDirty ? '\u2705' : '\u274C' }}\r\n </span>\r\n @if (controlErrors) {\r\n <span style=\"margin-left: 4px; color: red\">\r\n | Errors: {{ controlErrors | json }}\r\n </span>\r\n }\r\n </div>\r\n <button\r\n (click)=\"forceReload()\"\r\n style=\"\r\n font-size: 10px;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n border: 1px solid #ccc;\r\n background: white;\r\n cursor: pointer;\r\n \"\r\n type=\"button\"\r\n >\r\n \uD83D\uDD04 Recargar\r\n </button>\r\n </div>\r\n</div>\r\n}\r\n", styles: [""], dependencies: [{ kind: "component", type: AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: FloatLabel, selector: "p-floatlabel, p-floatLabel, p-float-label", inputs: ["variant"] }, { kind: "component", type: AppMessageErrorComponent, selector: "dsx-message-error", inputs: ["control", "form", "debugMode"] }, { kind: "pipe", type: JsonPipe, name: "json" }] });
9218
10086
  }
9219
10087
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxAutocomplete, decorators: [{
9220
10088
  type: Component,
@@ -9230,7 +10098,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
9230
10098
  useExisting: forwardRef(() => DsxAutocomplete),
9231
10099
  multi: true,
9232
10100
  },
9233
- ], template: "<p-floatlabel variant=\"on\">\r\n <p-autoComplete\r\n fluid\r\n appendTo=\"body\"\r\n [multiple]=\"isMultiple()\"\r\n [dropdown]=\"true\"\r\n [showClear]=\"true\"\r\n [suggestions]=\"dataFiltrada()\"\r\n (completeMethod)=\"searchRequisitos($event)\"\r\n [(ngModel)]=\"value\"\r\n (ngModelChange)=\"onNgModelChange($event)\"\r\n (onBlur)=\"onTouched()\"\r\n (onShow)=\"onShow()\"\r\n (onHide)=\"onHide()\"\r\n [disabled]=\"disabled\"\r\n [optionLabel]=\"optionLabel()\"\r\n [dataKey]=\"dataKey() || idKey()\"\r\n (keyup.enter)=\"evaluarYAgregar($event)\"\r\n [delay]=\"delay()\"\r\n placeholder=\"Ingresa un texto....\"\r\n [inputStyle]=\"{'text-transform': 'uppercase'}\"\r\n >\r\n <ng-template let-item pTemplate=\"selectedItem\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n\r\n <ng-template let-item pTemplate=\"item\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n </p-autoComplete>\r\n <label>{{ label() }}</label>\r\n</p-floatlabel>\r\n\r\n<!-- Bot\u00F3n de limpiar para modo single -->\r\n@if (!isMultiple() && value) {\r\n<button\r\n type=\"button\"\r\n (click)=\"clearSelection()\"\r\n style=\"\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n background: transparent;\r\n border: none;\r\n cursor: pointer;\r\n color: #999;\r\n font-size: 14px;\r\n z-index: 1;\r\n \"\r\n aria-label=\"Limpiar selecci\u00F3n\"\r\n>\r\n \u2715\r\n</button>\r\n}\r\n\r\n<!-- Mensaje de error -->\r\n@if (showError() && errorControl()) {\r\n<dsx-message-error [control]=\"errorControl()\" />\r\n}\r\n\r\n<!-- Panel de Debug -->\r\n@if (debug() && isDevMode) {\r\n<div\r\n style=\"\r\n margin-top: 8px;\r\n font-size: 11px;\r\n color: #666;\r\n background: #f5f5f5;\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n font-family: monospace;\r\n border: 1px solid #ddd;\r\n \"\r\n>\r\n <div\r\n style=\"\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n \"\r\n >\r\n <div>\r\n <strong>\uD83D\uDD0D Debug:</strong>\r\n <span style=\"margin-left: 8px\">Items: {{ getValueLength() }}</span>\r\n <span style=\"margin-left: 8px\">IDs: {{ getValueDisplay('id') }}</span>\r\n <span style=\"margin-left: 8px\"\r\n >Nombres: {{ getValueDisplay('nombre') }}</span\r\n >\r\n <span style=\"margin-left: 8px; color: #888; font-size: 10px\">\r\n | returnID: {{ returnID() ? '\u2705 IDs' : '\u274C Objetos' }} | Multiple: {{\r\n isMultiple() ? '\u2705' : '\u274C' }} | Control: {{ controlName || 'sin nombre'\r\n }}\r\n </span>\r\n </div>\r\n <div\r\n style=\"\r\n font-size: 10px;\r\n color: #333;\r\n background: #e8e8e8;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n \"\r\n >\r\n <strong>\uD83D\uDCCA Validaci\u00F3n:</strong>\r\n <span [style.color]=\"controlValid ? 'green' : 'red'\">\r\n {{ controlValid ? '\u2705 V\u00E1lido' : '\u274C Inv\u00E1lido' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Touched: {{ controlTouched ? '\u2705' : '\u274C' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Dirty: {{ controlDirty ? '\u2705' : '\u274C' }}\r\n </span>\r\n @if (controlErrors) {\r\n <span style=\"margin-left: 4px; color: red\">\r\n | Errors: {{ controlErrors | json }}\r\n </span>\r\n }\r\n </div>\r\n <button\r\n (click)=\"forceReload()\"\r\n style=\"\r\n font-size: 10px;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n border: 1px solid #ccc;\r\n background: white;\r\n cursor: pointer;\r\n \"\r\n type=\"button\"\r\n >\r\n \uD83D\uDD04 Recargar\r\n </button>\r\n </div>\r\n</div>\r\n}\r\n" }]
10101
+ ], template: "<p-floatlabel variant=\"on\">\r\n <p-autoComplete\r\n fluid\r\n [multiple]=\"isMultiple()\"\r\n [dropdown]=\"true\"\r\n [showClear]=\"true\"\r\n [suggestions]=\"dataFiltrada()\"\r\n (completeMethod)=\"searchRequisitos($event)\"\r\n [(ngModel)]=\"value\"\r\n (ngModelChange)=\"onNgModelChange($event)\"\r\n (onBlur)=\"onTouched()\"\r\n (onShow)=\"onShow()\"\r\n (onHide)=\"onHide()\"\r\n [disabled]=\"disabled\"\r\n [optionLabel]=\"optionLabel()\"\r\n [dataKey]=\"dataKey() || idKey()\"\r\n (keyup.enter)=\"evaluarYAgregar($event)\"\r\n [delay]=\"delay()\"\r\n placeholder=\"Ingresa un texto....\"\r\n [inputStyle]=\"{'text-transform': 'uppercase'}\"\r\n >\r\n <ng-template let-item pTemplate=\"selectedItem\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n\r\n <ng-template let-item pTemplate=\"item\">\r\n {{ item[optionLabel()] }}\r\n </ng-template>\r\n </p-autoComplete>\r\n <label>{{ label() }}</label>\r\n</p-floatlabel>\r\n\r\n<!-- \u2728 Indicador de carga (cuando hay IDs pendientes) -->\r\n@if (pendingIds.length > 0 && debug() && isDevMode) {\r\n<div\r\n style=\"\r\n font-size: 10px;\r\n color: #f59e0b;\r\n margin-top: 2px;\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n \"\r\n>\r\n <span>\u23F3</span>\r\n <span\r\n >Cargando datos pendientes ({{ pendingIds.length }} IDs: [{{\r\n pendingIds.join(', ') }}])</span\r\n >\r\n</div>\r\n}\r\n\r\n<!-- \u2728 Bot\u00F3n de limpiar para modo single -->\r\n@if (!isMultiple() && value) {\r\n<button\r\n type=\"button\"\r\n (click)=\"clearSelection()\"\r\n style=\"\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n background: transparent;\r\n border: none;\r\n cursor: pointer;\r\n color: #999;\r\n font-size: 14px;\r\n z-index: 1;\r\n \"\r\n aria-label=\"Limpiar selecci\u00F3n\"\r\n>\r\n \u2715\r\n</button>\r\n}\r\n\r\n<!-- Mensaje de error -->\r\n@if (showError() && errorControl()) {\r\n<dsx-message-error [control]=\"errorControl()\" />\r\n}\r\n\r\n<!-- Panel de Debug -->\r\n@if (debug() && isDevMode) {\r\n<div\r\n style=\"\r\n margin-top: 8px;\r\n font-size: 11px;\r\n color: #666;\r\n background: #f5f5f5;\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n font-family: monospace;\r\n border: 1px solid #ddd;\r\n \"\r\n>\r\n <div\r\n style=\"\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n \"\r\n >\r\n <div>\r\n <strong>\uD83D\uDD0D Debug:</strong>\r\n <span style=\"margin-left: 8px\">Items: {{ getValueLength() }}</span>\r\n <span style=\"margin-left: 8px\">IDs: {{ getValueDisplay('id') }}</span>\r\n <span style=\"margin-left: 8px\"\r\n >Nombres: {{ getValueDisplay('nombre') }}</span\r\n >\r\n <span style=\"margin-left: 8px; color: #888; font-size: 10px\">\r\n | returnID: {{ returnID() ? '\u2705 IDs' : '\u274C Objetos' }} | Multiple: {{\r\n isMultiple() ? '\u2705' : '\u274C' }} | Control: {{ controlName || 'sin nombre'\r\n }}\r\n </span>\r\n @if (pendingIds.length > 0) {\r\n <span style=\"margin-left: 8px; color: #f59e0b; font-weight: bold\">\r\n | \u23F3 Pendientes: {{ pendingIds.length }} ({{ pendingIds.join(', ') }})\r\n </span>\r\n } @else {\r\n <span style=\"margin-left: 8px; color: #10b981; font-weight: bold\">\r\n | \u2705 Sin pendientes\r\n </span>\r\n }\r\n <span style=\"margin-left: 8px; color: #888; font-size: 9px\">\r\n | Logs: {{ pendingLogsInfo }}\r\n </span>\r\n </div>\r\n <div\r\n style=\"\r\n font-size: 10px;\r\n color: #333;\r\n background: #e8e8e8;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n \"\r\n >\r\n <strong>\uD83D\uDCCA Validaci\u00F3n:</strong>\r\n <span [style.color]=\"controlValid ? 'green' : 'red'\">\r\n {{ controlValid ? '\u2705 V\u00E1lido' : '\u274C Inv\u00E1lido' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Touched: {{ controlTouched ? '\u2705' : '\u274C' }}\r\n </span>\r\n <span style=\"margin-left: 4px\">\r\n | Dirty: {{ controlDirty ? '\u2705' : '\u274C' }}\r\n </span>\r\n @if (controlErrors) {\r\n <span style=\"margin-left: 4px; color: red\">\r\n | Errors: {{ controlErrors | json }}\r\n </span>\r\n }\r\n </div>\r\n <button\r\n (click)=\"forceReload()\"\r\n style=\"\r\n font-size: 10px;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n border: 1px solid #ccc;\r\n background: white;\r\n cursor: pointer;\r\n \"\r\n type=\"button\"\r\n >\r\n \uD83D\uDD04 Recargar\r\n </button>\r\n </div>\r\n</div>\r\n}\r\n" }]
9234
10102
  }], ctorParameters: () => [], propDecorators: { datasource: [{ type: i0.Input, args: [{ isSignal: true, alias: "datasource", required: true }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], delay: [{ type: i0.Input, args: [{ isSignal: true, alias: "delay", required: false }] }], idKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "idKey", required: true }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], permitirCrear: [{ type: i0.Input, args: [{ isSignal: true, alias: "permitirCrear", required: false }] }], isMultiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "isMultiple", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], factoryNuevoRegistro: [{ type: i0.Input, args: [{ isSignal: true, alias: "factoryNuevoRegistro", required: false }] }], returnID: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnID", required: false }] }], showError: [{ type: i0.Input, args: [{ isSignal: true, alias: "showError", required: false }] }] } });
9235
10103
 
9236
10104
  // dsx-select.component.ts
@@ -9823,35 +10691,210 @@ class ResultFileService {
9823
10691
  * FileContents|fileContents, FileDownloadName|fileDownloadName,
9824
10692
  * ContentType|contentType, title y message.
9825
10693
  *
9826
- * Si el backend no cumple alguno de estos formatos, la conversión puede fallar
9827
- * y el cliente recibirá data: null con isSuccess: false.
9828
- */
9829
- /**
9830
- * Procesa un ServiceResult<FileData> para visualizar o descargar.
9831
- */
9832
- processFileResult(result, visualizar = false) {
9833
- if (!result.isSuccess || !result.data?.blob) {
9834
- this._alertaService.alertaHtmlSuccess(result.title, result.message, {
9835
- icon: 'question',
9836
- icono: '',
9837
- showConfirmButton: true,
9838
- });
9839
- return;
9840
- }
9841
- const { blob, fileName } = result.data;
9842
- if (visualizar) {
9843
- this.PdfView(blob);
9844
- return;
9845
- }
9846
- this.descargarBlob(blob, fileName);
9847
- }
9848
- /**
9849
- * Realiza un GET para descarga y normaliza la respuesta a ServiceResult<FileData>.
10694
+ * Si el backend no cumple alguno de estos formatos, la conversión puede fallar
10695
+ * y el cliente recibirá data: null con isSuccess: false.
10696
+ */
10697
+ /**
10698
+ * Procesa un ServiceResult<FileData> para visualizar o descargar.
10699
+ */
10700
+ processFileResult(result, visualizar = false) {
10701
+ if (!result.isSuccess || !result.data?.blob) {
10702
+ this._alertaService.alertaHtmlSuccess(result.title, result.message, {
10703
+ icon: 'question',
10704
+ icono: '',
10705
+ showConfirmButton: true,
10706
+ });
10707
+ return;
10708
+ }
10709
+ const { blob, fileName } = result.data;
10710
+ if (visualizar) {
10711
+ this.PdfView(blob);
10712
+ return;
10713
+ }
10714
+ this.descargarBlob(blob, fileName);
10715
+ }
10716
+ /**
10717
+ * Realiza una petición GET para descargar un archivo y normaliza la respuesta a un ServiceResult<FileData>.
10718
+ *
10719
+ * 📋 **Descripción General:**
10720
+ * Este método unifica la forma en que se manejan las descargas de archivos, soportando múltiples
10721
+ * formatos de respuesta del backend y normalizándolos a una estructura consistente.
10722
+ *
10723
+ * 🔄 **Formatos de Respuesta Soportados:**
10724
+ *
10725
+ * 1. **Binario Directo** (Recomendado):
10726
+ * - El backend retorna directamente el archivo binario.
10727
+ * - El nombre del archivo se obtiene del header `Content-Disposition`.
10728
+ * - Mensajes opcionales via headers: `x-title`, `x-message`, `title`, `message`.
10729
+ * - Ejemplo:
10730
+ * ```
10731
+ * Content-Type: application/pdf
10732
+ * Content-Disposition: attachment; filename="documento.pdf"
10733
+ * x-title: Éxito
10734
+ * x-message: Descarga completada exitosamente
10735
+ *
10736
+ * [bytes del archivo]
10737
+ * ```
10738
+ *
10739
+ * 2. **JSON Serializado con ServiceResult**:
10740
+ * - El backend retorna un JSON con estructura ServiceResult<FileContentResult>.
10741
+ * - El archivo viene codificado en Base64 o como array de bytes.
10742
+ * - Soporta campos en PascalCase o camelCase.
10743
+ * - Ejemplo:
10744
+ * ```json
10745
+ * {
10746
+ * "isSuccess": true,
10747
+ * "title": "Éxito",
10748
+ * "message": "Archivo generado correctamente",
10749
+ * "data": {
10750
+ * "fileContents": "base64EncodedString...",
10751
+ * "fileDownloadName": "reporte.xlsx",
10752
+ * "contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
10753
+ * }
10754
+ * }
10755
+ * ```
10756
+ *
10757
+ * 3. **Error del Backend**:
10758
+ * - Retorna un ServiceResult con isSuccess: false.
10759
+ * - Se extrae el título y mensaje de error.
10760
+ * - Ejemplo:
10761
+ * ```json
10762
+ * {
10763
+ * "isSuccess": false,
10764
+ * "title": "Error de Validación",
10765
+ * "message": "El ID del documento no existe"
10766
+ * }
10767
+ * ```
10768
+ *
10769
+ * 4. **Error de Red/HTTP**:
10770
+ * - Captura errores de conexión, timeout, etc.
10771
+ * - Retorna un ServiceResult con isSuccess: false y mensaje genérico.
10772
+ *
10773
+ * 🎯 **Método de Uso Recomendado:**
10774
+ * ```typescript
10775
+ * // Opción 1: Descarga automática
10776
+ * this.resultFileService.getDownloadableFile('/api/documentos/123')
10777
+ * .subscribe(result => {
10778
+ * this.resultFileService.processFileResult(result, false);
10779
+ * });
10780
+ *
10781
+ * // Opción 2: Visualización de PDF
10782
+ * this.resultFileService.getDownloadableFile('/api/documentos/123')
10783
+ * .subscribe(result => {
10784
+ * this.resultFileService.processFileResult(result, true);
10785
+ * });
10786
+ *
10787
+ * // Opción 3: Procesamiento personalizado
10788
+ * this.resultFileService.getDownloadableFile('/api/documentos/123')
10789
+ * .subscribe({
10790
+ * next: (result) => {
10791
+ * if (result.isSuccess && result.data?.blob) {
10792
+ * // Procesar el blob manualmente
10793
+ * const { blob, fileName } = result.data;
10794
+ * // Hacer algo con el archivo
10795
+ * } else {
10796
+ * // Manejar error
10797
+ * console.error(result.title, result.message);
10798
+ * }
10799
+ * },
10800
+ * error: (error) => {
10801
+ * console.error('Error de red:', error);
10802
+ * }
10803
+ * });
10804
+ * ```
10805
+ *
10806
+ * 📝 **Parámetros:**
10807
+ * @param url - URL del endpoint de descarga.
10808
+ * @param options - Opciones adicionales para la solicitud.
10809
+ * @param options.params - Parámetros de consulta (Query params). Soporta:
10810
+ * - `string`: valor simple
10811
+ * - `string[]`: múltiples valores para el mismo parámetro
10812
+ * - `Date`: se convierte automáticamente a ISO string
10813
+ * - `number`: se convierte a string
10814
+ * - `HttpParams`: instancia directamente
10815
+ * @param options.headers - Headers HTTP personalizados (HttpHeaders).
10816
+ * @param options.withCredentials - Enviar cookies/credenciales en la solicitud.
10817
+ *
10818
+ * 🔍 **Estructura de Retorno:**
10819
+ * ```typescript
10820
+ * Observable<ServiceResult<FileData>>
10821
+ * ```
10822
+ *
10823
+ * Donde `ServiceResult<FileData>` tiene:
10824
+ * ```typescript
10825
+ * {
10826
+ * data: {
10827
+ * blob: Blob, // El archivo como Blob
10828
+ * fileName: string // Nombre del archivo (con extensión)
10829
+ * } | null,
10830
+ * isSuccess: boolean, // Indica si la operación fue exitosa
10831
+ * title: string, // Título del resultado (éxito o error)
10832
+ * message: string // Mensaje descriptivo
10833
+ * }
10834
+ * ```
10835
+ *
10836
+ * ⚠️ **Notas Importantes:**
10837
+ * - El método siempre retorna un Observable que emite un ServiceResult.
10838
+ * - Nunca emite error en el Observable (los errores se convierten a ServiceResult con isSuccess: false).
10839
+ * - Para descarga automática usar `processFileResult(result, false)`.
10840
+ * - Para visualización de PDF usar `processFileResult(result, true)`.
10841
+ * - El nombre del archivo se determina en este orden de prioridad:
10842
+ * 1. `Content-Disposition` header (si existe)
10843
+ * 2. `fileDownloadName` del JSON (si es respuesta JSON)
10844
+ * 3. `fileName` del JSON (si es respuesta JSON)
10845
+ * 4. Valor por defecto: 'document'
10846
+ *
10847
+ * 💡 **Ejemplos de Uso Adicionales:**
10848
+ *
10849
+ * ```typescript
10850
+ * // Con parámetros de consulta
10851
+ * const options = {
10852
+ * params: {
10853
+ * id: 123,
10854
+ * formato: 'pdf',
10855
+ * fechas: ['2024-01-01', '2024-12-31']
10856
+ * }
10857
+ * };
10858
+ * this.resultFileService.getDownloadableFile('/api/reportes', options)
10859
+ * .subscribe(result => {
10860
+ * this.resultFileService.processFileResult(result);
10861
+ * });
10862
+ *
10863
+ * // Con headers personalizados (ej: autenticación)
10864
+ * const options = {
10865
+ * headers: new HttpHeaders({
10866
+ * 'Authorization': 'Bearer ' + token,
10867
+ * 'X-Custom-Header': 'valor'
10868
+ * })
10869
+ * };
10870
+ * this.resultFileService.getDownloadableFile('/api/documentos', options)
10871
+ * .subscribe(result => {
10872
+ * this.resultFileService.processFileResult(result);
10873
+ * });
10874
+ *
10875
+ * // Con credenciales
10876
+ * const options = {
10877
+ * withCredentials: true
10878
+ * };
10879
+ * this.resultFileService.getDownloadableFile('/api/documentos', options)
10880
+ * .subscribe(result => {
10881
+ * this.resultFileService.processFileResult(result);
10882
+ * });
10883
+ * ```
10884
+ *
10885
+ * 🚀 **Flujo de Trabajo Típico:**
10886
+ * 1. Llamar a `getDownloadableFile(url, options)` con la URL del endpoint.
10887
+ * 2. Suscribirse al Observable.
10888
+ * 3. En el `next`, recibir el `ServiceResult<FileData>`.
10889
+ * 4. Usar `processFileResult(result, visualizar)` para:
10890
+ * - Descargar automáticamente (visualizar: false)
10891
+ * - Visualizar PDF en nueva pestaña (visualizar: true)
10892
+ * 5. Alternativamente, procesar el blob manualmente si se necesita.
9850
10893
  *
9851
- * Nota de integración backend:
9852
- * - Para nombre de archivo en binario directo, enviar Content-Disposition.
9853
- * - Para mensajes de éxito, enviar x-title/x-message o title/message en JSON.
9854
- * - Si se envía JSON serializado, incluir los campos de FileContentResult.
10894
+ * @see processFileResult - Para descargar o visualizar el archivo obtenido.
10895
+ * @see FileData - Estructura de datos del archivo.
10896
+ * @see BlobRequestOptions - Opciones de configuración para la solicitud.
10897
+ * @see ServiceResult - Estructura de resultado estandarizada.
9855
10898
  */
9856
10899
  getDownloadableFile(url, options = {}) {
9857
10900
  return this.http
@@ -10056,25 +11099,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10056
11099
  }] });
10057
11100
 
10058
11101
  class SweetAlert2DialogService {
10059
- /**
10060
- * Configuración del entorno inyectada (opcional).
10061
- * Se usa para obtener el tema global de SweetAlert2 desde el environment.
10062
- */
10063
11102
  environment = inject(ENVIRONMENT, { optional: true });
10064
- /**
10065
- * Tema visual por defecto para todas las alertas SweetAlert2.
10066
- * Se inicializa desde el environment si está configurado, o puede establecerse
10067
- * manualmente llamando a setDefaultTheme().
10068
- *
10069
- * **Prioridad de temas:**
10070
- * 1. Tema específico pasado en options de cada alerta
10071
- * 2. Tema establecido con setDefaultTheme()
10072
- * 3. Tema configurado en environment.sweetAlertTheme
10073
- * 4. Tema por defecto de SweetAlert2
10074
- */
10075
11103
  defaultTheme = undefined;
11104
+ NATIVE_ICONS = [
11105
+ 'success',
11106
+ 'error',
11107
+ 'warning',
11108
+ 'info',
11109
+ 'question',
11110
+ ];
10076
11111
  constructor() {
10077
- // Inicializar el tema desde el environment si está disponible
10078
11112
  if (this.environment?.sweetAlertTheme) {
10079
11113
  const allowed = SWEET_ALERT_THEMES;
10080
11114
  const themeValue = this.environment.sweetAlertTheme;
@@ -10094,59 +11128,102 @@ class SweetAlert2DialogService {
10094
11128
  img.onerror = (err) => reject(err);
10095
11129
  });
10096
11130
  }
10097
- // 4. Implementación real del método
10098
- // 5. Implementación unificada final
10099
- Dialog(firstParam, text, icoOrIcono, options = {}) {
11131
+ // Implementación unificada
11132
+ Dialog(firstParam, text, secondOrThird, thirdOrOptions) {
10100
11133
  let title = '';
10101
- let finalOptions = { ...options };
10102
- let finalIcoOrIcono = icoOrIcono;
10103
- // DETECTAR SI EL PRIMER PARÁMETRO ES EL OBJETO COMPLETO
11134
+ let messageContent = '';
11135
+ let options = {};
11136
+ let iconFromParam = null;
11137
+ let imageFromOptions = null;
11138
+ // === PROCESAR PARÁMETROS ===
10104
11139
  if (typeof firstParam === 'object' && firstParam !== null) {
10105
- const { title: cTitle, message: cMessage, icon, icono, ...cOptions } = firstParam;
10106
- title = cTitle;
10107
- text = cMessage;
10108
- finalIcoOrIcono = icon || icono || '';
10109
- finalOptions = { ...cOptions };
11140
+ // Caso 1: ActionMessageConfig
11141
+ const config = firstParam;
11142
+ title = config.title;
11143
+ messageContent = config.message;
11144
+ // Extraer opciones
11145
+ const { title: _title, message: _message, icon: _icon, icono: _icono, html: _html, footer: _footer, ...restOptions } = config;
11146
+ options = { ...restOptions };
11147
+ if (_html)
11148
+ options.html = _html;
11149
+ if (_footer)
11150
+ options.footer = _footer;
11151
+ // ✅ Si hay icono en el config, es el parámetro
11152
+ if (config.icon) {
11153
+ iconFromParam = config.icon;
11154
+ }
11155
+ // ✅ Si hay icono personalizado en el config
11156
+ if (config.icono) {
11157
+ imageFromOptions = config.icono;
11158
+ }
10110
11159
  }
10111
11160
  else {
11161
+ // Caso 2, 3, 4: Parámetros tradicionales
10112
11162
  title = firstParam;
10113
- }
10114
- // A partir de aquí, tu lógica interna se mantiene exactamente igual usando las variables normalizadas
10115
- let ico = null;
10116
- let iconoImg = null;
10117
- if (finalIcoOrIcono) {
10118
- const nativeIcons = [
10119
- 'success',
10120
- 'error',
10121
- 'warning',
10122
- 'info',
10123
- 'question',
10124
- ];
10125
- if (nativeIcons.includes(finalIcoOrIcono)) {
10126
- ico = finalIcoOrIcono;
11163
+ messageContent = text || '';
11164
+ // Determinar qué tipo de parámetros tenemos
11165
+ if (secondOrThird !== undefined && secondOrThird !== null) {
11166
+ if (typeof secondOrThird === 'string') {
11167
+ // ✅ Es un icono nativo (Caso 4)
11168
+ if (this.NATIVE_ICONS.includes(secondOrThird)) {
11169
+ iconFromParam = secondOrThird;
11170
+ }
11171
+ options = thirdOrOptions || {};
11172
+ }
11173
+ else if (typeof secondOrThird === 'object') {
11174
+ // ✅ Son opciones (Caso 3)
11175
+ options = secondOrThird;
11176
+ // Si hay icono personalizado en options
11177
+ if (options.icono) {
11178
+ imageFromOptions = options.icono;
11179
+ }
11180
+ }
10127
11181
  }
10128
11182
  else {
10129
- iconoImg = finalIcoOrIcono;
11183
+ // Solo título y texto (Caso 2)
11184
+ options = thirdOrOptions || {};
11185
+ if (options.icono) {
11186
+ imageFromOptions = options.icono;
11187
+ }
10130
11188
  }
10131
11189
  }
10132
- // Configuración por defecto de botones y estilos
10133
- const { confirmButtonText = `Aceptar!`, cancelButtonText = `Cancelar`, showCloseButton = true, showCancelButton = true, imageWidth = 150, imageHeight = 150, theme, ...rest } = finalOptions;
11190
+ // PRIORIDAD: imageFromOptions (imagen) > iconFromParam (icono nativo)
11191
+ let ico = null;
11192
+ let iconoImg = null;
11193
+ // 1. Si hay imagen en options, usarla (máxima prioridad)
11194
+ if (imageFromOptions) {
11195
+ iconoImg = imageFromOptions;
11196
+ }
11197
+ // 2. Si hay icono en el parámetro, usarlo
11198
+ else if (iconFromParam) {
11199
+ ico = iconFromParam;
11200
+ }
11201
+ // === CONFIGURACIÓN DE LA ALERTA ===
11202
+ const { confirmButtonText = 'Aceptar', cancelButtonText = 'Cancelar', showCloseButton = true, showCancelButton = true, imageWidth = 150, imageHeight = 150, theme, ...rest } = options;
10134
11203
  const alertTheme = theme !== undefined ? theme : this.defaultTheme;
11204
+ // === CONSTRUIR FOOTER ===
10135
11205
  const year = new Date().getFullYear();
10136
- const baseFooter = this.environment?.sweetAlertFooter ?? 'DEVSOFTXela';
10137
- const footer = `${baseFooter} <strong>${year}</strong>`;
11206
+ const footerText = options.footer ?? this.environment?.sweetAlertFooter ?? 'DEVSOFTXela';
11207
+ const footer = `${footerText} <strong>${year}</strong>`;
11208
+ // === CONSTRUIR CONFIGURACIÓN DE SWEETALERT ===
10138
11209
  const alertConfig = {
10139
11210
  title,
10140
- html: text,
10141
11211
  footer,
10142
11212
  showCloseButton,
10143
11213
  showCancelButton,
10144
11214
  confirmButtonText,
10145
11215
  cancelButtonText,
10146
- //reverseButtons: true,
10147
11216
  ...(alertTheme !== undefined && { theme: alertTheme }),
10148
11217
  ...rest,
10149
11218
  };
11219
+ // === PRIORIDAD DEL CONTENIDO HTML ===
11220
+ if (options.html) {
11221
+ alertConfig.html = options.html;
11222
+ }
11223
+ else if (messageContent) {
11224
+ alertConfig.html = messageContent;
11225
+ }
11226
+ // === IMAGEN O ICONO ===
10150
11227
  if (iconoImg) {
10151
11228
  alertConfig.imageUrl = `/${iconoImg}`;
10152
11229
  alertConfig.imageWidth = imageWidth;
@@ -10170,6 +11247,320 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10170
11247
  }]
10171
11248
  }], ctorParameters: () => [] });
10172
11249
 
11250
+ class DsxMessagesService {
11251
+ AlertaService = inject(AlertaService);
11252
+ SweetDialog = inject(SweetAlert2DialogService);
11253
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11254
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxMessagesService, providedIn: 'root' });
11255
+ }
11256
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxMessagesService, decorators: [{
11257
+ type: Injectable,
11258
+ args: [{
11259
+ providedIn: 'root',
11260
+ }]
11261
+ }] });
11262
+
11263
+ /**
11264
+ * Obtiene los datos de un FormGroup excluyendo TODAS las propiedades de tipo File
11265
+ *
11266
+ * @param form - FormGroup del cual extraer los datos
11267
+ * @returns Objeto con los datos (sin archivos) y la lista de archivos encontrados
11268
+ *
11269
+ * @internal
11270
+ */
11271
+ function getFormDataExcludingFiles(form) {
11272
+ const rawValue = form.getRawValue();
11273
+ const entries = Object.entries(rawValue);
11274
+ const files = [];
11275
+ const data = {};
11276
+ for (const [key, value] of entries) {
11277
+ if (value instanceof File) {
11278
+ files.push(value);
11279
+ }
11280
+ else {
11281
+ data[key] = value;
11282
+ }
11283
+ }
11284
+ return { data: data, files };
11285
+ }
11286
+ /**
11287
+ * Servicio para guardar entidades y subir archivos automáticamente.
11288
+ *
11289
+ * Este servicio centraliza el flujo común de:
11290
+ * 1. Extraer automáticamente el archivo del FormGroup (por tipo `File`)
11291
+ * 2. Guardar la entidad (sin el archivo) en la base de datos
11292
+ * 3. Si el guardado es exitoso y hay archivo, subirlo
11293
+ * 4. Manejar errores de forma consistente
11294
+ * 5. Resetear el formulario automáticamente en caso de éxito
11295
+ *
11296
+ * @example
11297
+ * // En tu componente:
11298
+ *
11299
+ * // Guardar sin archivo
11300
+ * this._saveEntityPostFile.saveOnly(
11301
+ * this.form,
11302
+ * this._baseService.save.bind(this._baseService),
11303
+ * (response) => {
11304
+ * this.formState.emit(response);
11305
+ * }
11306
+ * );
11307
+ *
11308
+ * // Guardar con archivo (se extrae automáticamente)
11309
+ * this._saveEntityPostFile.saveWithFile(
11310
+ * this.form,
11311
+ * this._baseService.save.bind(this._baseService),
11312
+ * this._baseService.AdjuntarSoporte.bind(this._baseService),
11313
+ * (response) => {
11314
+ * this.formState.emit(response);
11315
+ * }
11316
+ * );
11317
+ */
11318
+ class SaveEntityPostFileService {
11319
+ /** Servicio para mostrar mensajes al usuario */
11320
+ _dsxMessageService = inject(DsxMessagesService);
11321
+ /** Configuración del entorno (para controlar debug) */
11322
+ environment = inject(ENVIRONMENT);
11323
+ /**
11324
+ * Determina si el debug está activo
11325
+ *
11326
+ * @param debug - Solicitud explícita de debug
11327
+ * @returns `true` solo si `debug=true` Y no estamos en producción
11328
+ *
11329
+ * @internal
11330
+ */
11331
+ shouldLog(debug) {
11332
+ return debug && !this.environment.production;
11333
+ }
11334
+ /**
11335
+ * Guarda una entidad Y sube automáticamente el archivo extraído del FormGroup.
11336
+ *
11337
+ * El servicio extrae automáticamente el archivo del FormGroup (por tipo `File`),
11338
+ * lo excluye del objeto que se envía al backend, y después de guardar
11339
+ * exitosamente, procede a subir el archivo usando el ID generado.
11340
+ *
11341
+ * @param form - FormGroup que contiene los datos del formulario
11342
+ * @param saveMethod - Método de save que recibe los datos (sin archivo)
11343
+ * @param uploadMethod - Método de upload que recibe (id, file) y retorna Observable
11344
+ * @param onSuccess - Callback de éxito (opcional)
11345
+ * @param onError - Callback de error (opcional)
11346
+ * @param resetForm - Función para resetear el formulario (opcional)
11347
+ * @param debug - Activa logs en consola (solo funciona en desarrollo). Por defecto `false`.
11348
+ *
11349
+ * @example
11350
+ * // Uso básico
11351
+ * this._saveEntityPostFile.saveWithFile(
11352
+ * this.form,
11353
+ * this._baseService.save.bind(this._baseService),
11354
+ * this._baseService.AdjuntarSoporte.bind(this._baseService),
11355
+ * (response) => {
11356
+ * this.formState.emit(response);
11357
+ * }
11358
+ * );
11359
+ *
11360
+ * @example
11361
+ * // Con reset personalizado
11362
+ * this._saveEntityPostFile.saveWithFile(
11363
+ * this.form,
11364
+ * this._baseService.save.bind(this._baseService),
11365
+ * this._baseService.AdjuntarSoporte.bind(this._baseService),
11366
+ * (response) => {
11367
+ * this.formState.emit(response);
11368
+ * },
11369
+ * (error) => {
11370
+ * console.error('Error:', error);
11371
+ * },
11372
+ * () => {
11373
+ * this.form.reset();
11374
+ * this.getForm();
11375
+ * this.cleanFileInput();
11376
+ * }
11377
+ * );
11378
+ */
11379
+ saveWithFile(form, saveMethod, uploadMethod, deleteMethod, onSuccess, onError, resetForm, debug = false) {
11380
+ this.execute(form, saveMethod, uploadMethod, deleteMethod, onSuccess, onError, resetForm, debug);
11381
+ }
11382
+ /**
11383
+ * Ejecuta el flujo completo de guardado y subida de archivo.
11384
+ *
11385
+ * Este método es privado y contiene la lógica central del servicio.
11386
+ *
11387
+ * ## Flujo de ejecución:
11388
+ * 1. Extrae el archivo del FormGroup (por tipo `File`)
11389
+ * 2. Construye el objeto de datos sin el archivo
11390
+ * 3. Ejecuta el `saveMethod` con los datos limpios
11391
+ * 4. Si el save es exitoso y hay archivo, ejecuta el `uploadMethod`
11392
+ * 5. Si todo es exitoso, ejecuta `resetForm` y `onSuccess`
11393
+ * 6. Si hay error en cualquier paso, ejecuta `onError`
11394
+ *
11395
+ * @param form - FormGroup que contiene los datos del formulario
11396
+ * @param saveMethod - Método de save que recibe los datos (sin archivo)
11397
+ * @param uploadMethod - Método de upload que recibe (id, file) (opcional)
11398
+ * @param onSuccess - Callback de éxito (opcional)
11399
+ * @param onError - Callback de error (opcional)
11400
+ * @param resetForm - Función para resetear el formulario (opcional)
11401
+ * @param debug - Activa logs en consola (solo funciona en desarrollo)
11402
+ *
11403
+ * @internal
11404
+ */
11405
+ execute(form, saveMethod, uploadMethod, deleteMethod, onSuccess, onError, resetForm, debug = false) {
11406
+ const log = this.shouldLog(debug);
11407
+ // 1️⃣ Extraer archivo y datos del FormGroup
11408
+ const { data: saveData, files } = getFormDataExcludingFiles(form);
11409
+ const fileUpload = files.length > 0 ? files[0] : null;
11410
+ if (log) {
11411
+ console.group('📤 [SaveEntityPostFile] Preparando datos');
11412
+ console.log('📄 Datos a guardar (sin archivo):', saveData);
11413
+ console.log('📎 Archivo encontrado:', fileUpload?.name || 'Ninguno');
11414
+ console.groupEnd();
11415
+ }
11416
+ // 2️⃣ Crear el observable del save con los datos sin archivo
11417
+ const save$ = saveMethod(saveData);
11418
+ // 3️⃣ Ejecutar el flujo completo
11419
+ save$.subscribe({
11420
+ next: (saveResult) => {
11421
+ // 📥 Log del save
11422
+ if (log) {
11423
+ console.group('📥 [SaveEntityPostFile] Respuesta del save');
11424
+ console.log('Resultado:', saveResult);
11425
+ console.groupEnd();
11426
+ }
11427
+ // ❌ Si el guardado falla
11428
+ if (!saveResult.isSuccess) {
11429
+ this._dsxMessageService.SweetDialog.Dialog(saveResult.title || 'Error', saveResult.message || 'Error al guardar los datos', 'error', { showConfirmButton: true, showCancelButton: false });
11430
+ onError?.(saveResult);
11431
+ return;
11432
+ }
11433
+ // ✅ Si el guardado es exitoso y hay archivo
11434
+ if (uploadMethod && fileUpload && saveResult.data) {
11435
+ if (log)
11436
+ console.log('📤 [SaveEntityPostFile] Subiendo archivo...');
11437
+ uploadMethod(saveResult.data, fileUpload).subscribe({
11438
+ next: (uploadResult) => {
11439
+ // 📥 Log del upload
11440
+ if (log) {
11441
+ console.group('📥 [SaveEntityPostFile] Respuesta del upload');
11442
+ console.log('Resultado:', uploadResult);
11443
+ console.groupEnd();
11444
+ }
11445
+ if (uploadResult.isSuccess) {
11446
+ // ✅ Éxito: guardado + archivo
11447
+ if (log)
11448
+ console.log('✅ [SaveEntityPostFile] Subida exitosa');
11449
+ resetForm?.();
11450
+ onSuccess?.(uploadResult);
11451
+ }
11452
+ else {
11453
+ // ❌ Error en la subida
11454
+ if (log)
11455
+ console.warn('⚠️ [SaveEntityPostFile] Subida falló');
11456
+ const htmlContent = `
11457
+ <div style="text-align: left; font-family: -apple-system, system-ui, sans-serif;">
11458
+
11459
+ <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;">
11460
+ <span style="font-size: 22px;">⚠️</span>
11461
+ <span style="font-size: 17px; font-weight: 600; color: #1a1a1a;">Archivo no subido</span>
11462
+ </div>
11463
+
11464
+ <p style="color: #555; font-size: 14px; line-height: 1.5; margin: 0 0 8px 0;">
11465
+ El registro se guardó, pero el archivo falló.
11466
+ </p>
11467
+
11468
+ <div style="background: #f8f4ec; padding: 6px 12px; border-radius: 4px; margin: 6px 0 14px 0;">
11469
+ <span style="color: #7a6b4a; font-size: 13px;">
11470
+ <strong>Motivo:</strong> ${uploadResult.message}
11471
+ </span>
11472
+ </div>
11473
+
11474
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #eee;">
11475
+ <p style="font-weight: 500; color: #1a1a1a; font-size: 13px; margin: 0 0 6px 0;">
11476
+ ¿Qué deseas hacer?
11477
+ </p>
11478
+ <div style="font-size: 13px; color: #555; line-height: 1.7;">
11479
+ <div>📁 Guardar sin archivo <span style="color: #999; font-size: 12px;">(subir después)</span></div>
11480
+ <div>🗑️ Eliminar registro <span style="color: #999; font-size: 12px;">(corregir y reintentar)</span></div>
11481
+ </div>
11482
+ </div>
11483
+
11484
+ </div>`;
11485
+ this._dsxMessageService.SweetDialog.Dialog(uploadResult.title || 'Error', '', 'error', {
11486
+ html: htmlContent,
11487
+ showConfirmButton: true,
11488
+ confirmButtonText: 'Guardar',
11489
+ showCancelButton: true,
11490
+ cancelButtonText: 'Eliminar',
11491
+ }).then((isConfirmed) => {
11492
+ if (isConfirmed) {
11493
+ if (log)
11494
+ console.warn('⚠️ [SaveEntityPostFile] Se conservo el registro');
11495
+ resetForm?.();
11496
+ onSuccess?.(saveResult);
11497
+ }
11498
+ else {
11499
+ if (log)
11500
+ console.warn('❌ [SaveEntityPostFile] Se elimino el registro');
11501
+ // 2️⃣ Crear el observable del save con los datos sin archivo
11502
+ const delete$ = deleteMethod(saveResult.data, false);
11503
+ delete$.subscribe({
11504
+ next: (deleteResult) => {
11505
+ // 📥 Log del save
11506
+ if (log) {
11507
+ console.group('📥 [SaveEntityPostFile] Respuesta del delete');
11508
+ console.log('Resultado:', deleteResult);
11509
+ console.groupEnd();
11510
+ }
11511
+ if (deleteResult.isSuccess) {
11512
+ if (log)
11513
+ console.log('✅ [SaveEntityPostFile] Registro eliminado');
11514
+ onSuccess?.(deleteResult);
11515
+ }
11516
+ else {
11517
+ this._dsxMessageService.SweetDialog.Dialog('❌ Error al eliminar', deleteResult.message ||
11518
+ 'No se pudo eliminar el registro.', 'error', {
11519
+ showConfirmButton: true,
11520
+ showCancelButton: false,
11521
+ });
11522
+ onError?.(deleteResult);
11523
+ }
11524
+ },
11525
+ error: (err) => console.warn(err),
11526
+ complete: () => '',
11527
+ });
11528
+ }
11529
+ });
11530
+ onError?.(uploadResult);
11531
+ }
11532
+ },
11533
+ error: (uploadError) => {
11534
+ // ❌ Error en la subida (excepción)
11535
+ console.warn('Error no controlado en upload:', uploadError);
11536
+ onError?.(uploadError);
11537
+ },
11538
+ });
11539
+ return;
11540
+ }
11541
+ // ✅ Éxito sin archivo
11542
+ if (log)
11543
+ console.log('✅ [SaveEntityPostFile] Proceso completado (sin archivo)');
11544
+ resetForm?.();
11545
+ onSuccess?.(saveResult);
11546
+ },
11547
+ error: (error) => {
11548
+ // ❌ Error en el save (excepción)
11549
+ console.warn('Error no controlado en save:', error);
11550
+ onError?.(error);
11551
+ },
11552
+ });
11553
+ }
11554
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveEntityPostFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11555
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveEntityPostFileService, providedIn: 'root' });
11556
+ }
11557
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveEntityPostFileService, decorators: [{
11558
+ type: Injectable,
11559
+ args: [{
11560
+ providedIn: 'root',
11561
+ }]
11562
+ }] });
11563
+
10173
11564
  class NextToastNotifyService {
10174
11565
  // Configuraciones iniciales por defecto (ambos activados como en tus switches)
10175
11566
  defaultOptions = {
@@ -10213,8 +11604,10 @@ class ResultResponseService {
10213
11604
  else {
10214
11605
  const guideHtml = this.buildAssociatedGuideHtml(response?.data);
10215
11606
  const mainMessage = this.formatPrimaryAlertMessage(response.message);
10216
- this._sweetAlert2DialogService.Dialog(response.title, `${mainMessage}${guideHtml}`, // Se concatena limpiamente sin alterar la lógica
10217
- 'icon/sign.png', {
11607
+ this._sweetAlert2DialogService.Dialog(response.title,
11608
+ // Se concatena limpiamente sin alterar la lógica
11609
+ `${mainMessage}${guideHtml}`, {
11610
+ icono: 'icon/sign.png',
10218
11611
  showCancelButton: false,
10219
11612
  showConfirmButton: true,
10220
11613
  });
@@ -10260,10 +11653,407 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10260
11653
  }]
10261
11654
  }] });
10262
11655
 
11656
+ /**
11657
+ * Servicio especializado para extraer y procesar archivos (Blob) desde respuestas del backend.
11658
+ *
11659
+ * 📋 **Propósito:**
11660
+ * Este servicio actúa como un puente entre la respuesta del backend (normalizada como ServiceResult<FileData>)
11661
+ * y el consumidor final, extrayendo el archivo (Blob) y su nombre de forma segura y validada.
11662
+ *
11663
+ * 🎯 **Cuándo usar este servicio:**
11664
+ * - Cuando necesitas obtener el Blob y el nombre de archivo de un Observable<ServiceResult<FileData>>
11665
+ * - Cuando quieres validar que el archivo existe y no está vacío antes de procesarlo
11666
+ * - Cuando necesitas asegurar que el nombre del archivo tenga la extensión correcta
11667
+ * - Cuando quieres un manejo de errores detallado y estructurado
11668
+ *
11669
+ * 🔄 **Flujo de Trabajo:**
11670
+ * 1. El backend retorna un ServiceResult<FileData> (normalizado por getDownloadableFile)
11671
+ * 2. Este servicio extrae el Blob y el fileName
11672
+ * 3. Valida que el archivo exista y tenga contenido
11673
+ * 4. Asegura que el nombre tenga la extensión correcta según el MIME type
11674
+ * 5. Retorna un objeto con { blob, fileName } o un error estructurado
11675
+ *
11676
+ * 🔗 **Relación con otros servicios:**
11677
+ * - **ResultFileService**: Proporciona el Observable<ServiceResult<FileData>> de entrada
11678
+ * - **AlertaService**: Para mostrar mensajes al usuario
11679
+ *
11680
+ * @see ResultFileService - Servicio que realiza la petición HTTP
11681
+ * @see FileData - Estructura de datos del archivo
11682
+ * @see ServiceResult - Estructura de resultado estandarizada
11683
+ * @see Blob - API nativa de Blob de JavaScript
11684
+ */
11685
+ class ResultFileReturnBlobNameService {
11686
+ /** Configuración del entorno (para controlar debug) */
11687
+ environment = inject(ENVIRONMENT);
11688
+ /**
11689
+ * Extrae el Blob y el nombre de archivo de un Observable<ServiceResult<FileData>>,
11690
+ * realizando validaciones exhaustivas y manejo de errores detallado.
11691
+ *
11692
+ * 📝 **Descripción:**
11693
+ * Este método toma el Observable resultante de `getDownloadableFile` y extrae
11694
+ * el archivo (Blob) y su nombre, validando que todo sea correcto antes de
11695
+ * entregar el resultado al consumidor.
11696
+ *
11697
+ * 🔍 **Validaciones Realizadas:**
11698
+ * 1. ✅ Resultado nulo o indefinido → Error: "El resultado es nulo o indefinido"
11699
+ * 2. ✅ isSuccess = false → Error con title y message del backend
11700
+ * 3. ✅ data es null → Error: "El resultado no contiene datos"
11701
+ * 4. ✅ blob es null → Error: "El blob del archivo no está presente"
11702
+ * 5. ✅ blob.size = 0 → Error: "El archivo descargado está vacío (0 bytes)"
11703
+ * 6. ✅ Error de conexión → Error: "No se pudo obtener el archivo del servidor"
11704
+ *
11705
+ * 📁 **Gestión de Extensiones:**
11706
+ * El método automáticamente asegura que el nombre del archivo tenga la extensión
11707
+ * correcta basada en el MIME type. Si el nombre ya tiene extensión, la mantiene.
11708
+ *
11709
+ * 💡 **Ejemplos de Uso:**
11710
+ *
11711
+ * ```typescript
11712
+ * // EJEMPLO 1: Uso básico con descarga automática
11713
+ * const fileObservable = this.resultFileService.getDownloadableFile('/api/documentos/123');
11714
+ *
11715
+ * this.resultFileReturnBlobNameService.extractFileDataFromServiceResultEnhanced(fileObservable)
11716
+ * .subscribe({
11717
+ * next: ({ blob, fileName }) => {
11718
+ * // Descargar el archivo automáticamente
11719
+ * this.resultFileService.descargarBlob(blob, fileName);
11720
+ * },
11721
+ * error: (error) => {
11722
+ * // Error estructurado
11723
+ * this.alertaService.mostrarError(error.title, error.message);
11724
+ * }
11725
+ * });
11726
+ *
11727
+ * // EJEMPLO 2: Visualizar PDF
11728
+ * const fileObservable = this.resultFileService.getDownloadableFile('/api/documentos/123');
11729
+ *
11730
+ * this.resultFileReturnBlobNameService.extractFileDataFromServiceResultEnhanced(fileObservable)
11731
+ * .subscribe({
11732
+ * next: ({ blob, fileName }) => {
11733
+ * // Crear URL para visualizar
11734
+ * const url = URL.createObjectURL(blob);
11735
+ * window.open(url, '_blank');
11736
+ * },
11737
+ * error: (error) => {
11738
+ * console.error('Error al visualizar:', error);
11739
+ * }
11740
+ * });
11741
+ *
11742
+ * // EJEMPLO 3: Procesamiento personalizado con validación
11743
+ * const fileObservable = this.resultFileService.getDownloadableFile('/api/reportes', {
11744
+ * params: { formato: 'excel' }
11745
+ * });
11746
+ *
11747
+ * this.resultFileReturnBlobNameService.extractFileDataFromServiceResultEnhanced(
11748
+ * fileObservable,
11749
+ * 'reporte_generado' // fallbackFileName
11750
+ * ).subscribe({
11751
+ * next: ({ blob, fileName }) => {
11752
+ * // Procesar el blob según necesidades
11753
+ * if (fileName.endsWith('.xlsx')) {
11754
+ * this.procesarExcel(blob);
11755
+ * } else {
11756
+ * this.descargarArchivo(blob, fileName);
11757
+ * }
11758
+ * },
11759
+ * error: (error) => {
11760
+ * // Manejo detallado de errores
11761
+ * if (error.title === 'Archivo vacío') {
11762
+ * this.alertaService.advertencia('El archivo está vacío');
11763
+ * } else if (error.title === 'Error de conexión') {
11764
+ * this.alertaService.error('Error de red', 'Verifica tu conexión');
11765
+ * } else {
11766
+ * this.alertaService.error(error.title, error.message);
11767
+ * }
11768
+ * }
11769
+ * });
11770
+ *
11771
+ * // EJEMPLO 4: Uso con loading state
11772
+ * cargando = false;
11773
+ *
11774
+ * descargarDocumento(id: number): void {
11775
+ * this.cargando = true;
11776
+ * const fileObservable = this.resultFileService.getDownloadableFile(`/api/documentos/${id}`);
11777
+ *
11778
+ * this.resultFileReturnBlobNameService.extractFileDataFromServiceResultEnhanced(
11779
+ * fileObservable,
11780
+ * `documento_${id}`
11781
+ * ).subscribe({
11782
+ * next: ({ blob, fileName }) => {
11783
+ * this.cargando = false;
11784
+ * this.resultFileService.descargarBlob(blob, fileName);
11785
+ * this.alertaService.exito('Descarga completada');
11786
+ * },
11787
+ * error: (error) => {
11788
+ * this.cargando = false;
11789
+ * this.alertaService.error('Error', error.message);
11790
+ * }
11791
+ * });
11792
+ * }
11793
+ *
11794
+ * // EJEMPLO 5: Combinación con otros operadores RxJS
11795
+ * const fileObservable = this.resultFileService.getDownloadableFile('/api/documentos/123');
11796
+ *
11797
+ * this.resultFileReturnBlobNameService.extractFileDataFromServiceResultEnhanced(fileObservable)
11798
+ * .pipe(
11799
+ * tap(({ blob, fileName }) => {
11800
+ * console.log(`Archivo: ${fileName}, Tamaño: ${blob.size} bytes`);
11801
+ * }),
11802
+ * delay(1000), // Simular procesamiento
11803
+ * map(({ blob, fileName }) => ({
11804
+ * blob,
11805
+ * fileName,
11806
+ * tipo: this.obtenerTipoArchivo(fileName)
11807
+ * }))
11808
+ * )
11809
+ * .subscribe(({ blob, fileName, tipo }) => {
11810
+ * console.log(`Tipo de archivo: ${tipo}`);
11811
+ * });
11812
+ * ```
11813
+ *
11814
+ * @param fileObservable - Observable que emite un ServiceResult<FileData>
11815
+ * @param fallbackFileName - Nombre alternativo si no se puede determinar el nombre (por defecto: 'archivo sin nombre')
11816
+ *
11817
+ * @returns Observable que emite un objeto con:
11818
+ * - blob: Blob - El archivo extraído
11819
+ * - fileName: string - Nombre del archivo con extensión
11820
+ * - headers?: HttpHeaders - Headers de la respuesta (opcional, actualmente no implementado)
11821
+ *
11822
+ * @throws {Object} En caso de error, emite un objeto con:
11823
+ * - success: false
11824
+ * - title: string - Título del error
11825
+ * - message: string - Mensaje descriptivo
11826
+ * - error?: any - Error original (en caso de error de conexión)
11827
+ *
11828
+ * @see ResultFileService.getDownloadableFile - Método que genera el Observable de entrada
11829
+ * @see ResultFileService.descargarBlob - Para descargar el archivo extraído
11830
+ * @see ResultFileService.PdfView - Para visualizar PDFs
11831
+ */
11832
+ extractFileDataFromServiceResultEnhanced(fileObservable, fallbackFileName = 'archivo sin nombre') {
11833
+ return new Observable((observer) => {
11834
+ fileObservable.subscribe({
11835
+ next: (result) => {
11836
+ // Validación completa del resultado
11837
+ if (!result) {
11838
+ observer.error({
11839
+ success: false,
11840
+ title: 'Resultado nulo',
11841
+ message: 'El resultado es nulo o indefinido',
11842
+ });
11843
+ return;
11844
+ }
11845
+ if (!result.isSuccess) {
11846
+ observer.error({
11847
+ success: false,
11848
+ title: result.title || 'Error en la operación',
11849
+ message: result.message || 'El servidor reportó un error',
11850
+ });
11851
+ return;
11852
+ }
11853
+ if (!result.data) {
11854
+ observer.error({
11855
+ success: false,
11856
+ title: 'Datos no disponibles',
11857
+ message: 'El resultado no contiene datos',
11858
+ });
11859
+ return;
11860
+ }
11861
+ const blob = result.data.blob;
11862
+ if (!blob) {
11863
+ observer.error({
11864
+ success: false,
11865
+ title: 'Archivo no disponible',
11866
+ message: 'El blob del archivo no está presente',
11867
+ });
11868
+ return;
11869
+ }
11870
+ // Validar que el blob tenga tamaño
11871
+ if (blob.size === 0) {
11872
+ observer.error({
11873
+ success: false,
11874
+ title: 'Archivo vacío',
11875
+ message: 'El archivo descargado está vacío (0 bytes)',
11876
+ });
11877
+ return;
11878
+ }
11879
+ // Procesar nombre de archivo
11880
+ let fileName = result.data.fileName || fallbackFileName;
11881
+ fileName = this.ensureFileNameWithExtension(fileName, blob.type);
11882
+ // Log para debugging
11883
+ this.logIfNotProduction('✅ Archivo extraído exitosamente:', {
11884
+ tamaño: this.formatFileSize(blob.size),
11885
+ tipo: blob.type,
11886
+ nombre: fileName,
11887
+ });
11888
+ observer.next({
11889
+ blob: blob,
11890
+ fileName: fileName,
11891
+ });
11892
+ observer.complete();
11893
+ },
11894
+ error: (err) => {
11895
+ this.logIfNotProduction('❌ Error extrayendo archivo:', err);
11896
+ observer.error({
11897
+ success: false,
11898
+ title: 'Error de conexión',
11899
+ message: 'No se pudo obtener el archivo del servidor',
11900
+ error: err,
11901
+ });
11902
+ },
11903
+ });
11904
+ });
11905
+ }
11906
+ /**
11907
+ * Asegura que el nombre de archivo tenga una extensión válida.
11908
+ *
11909
+ * 📋 **Funcionamiento:**
11910
+ * 1. Si el nombre ya tiene una extensión (contiene un punto), lo retorna sin cambios.
11911
+ * 2. Si no tiene extensión, intenta obtenerla del MIME type.
11912
+ * 3. Si se puede determinar la extensión, la agrega al nombre.
11913
+ * 4. Si no se puede determinar, retorna el nombre sin cambios.
11914
+ *
11915
+ * 💡 **Ejemplos:**
11916
+ * - `ensureFileNameWithExtension('documento', 'application/pdf')` → `'documento.pdf'`
11917
+ * - `ensureFileNameWithExtension('foto', 'image/jpeg')` → `'foto.jpg'`
11918
+ * - `ensureFileNameWithExtension('archivo.txt', 'text/plain')` → `'archivo.txt'` (ya tiene extensión)
11919
+ * - `ensureFileNameWithExtension('reporte', 'application/unknown')` → `'reporte'`
11920
+ *
11921
+ * @param fileName - Nombre del archivo (puede o no tener extensión)
11922
+ * @param mimeType - Tipo MIME del archivo
11923
+ * @returns Nombre del archivo con extensión (si fue posible determinarla)
11924
+ */
11925
+ ensureFileNameWithExtension(fileName, mimeType) {
11926
+ // Si ya tiene extensión, retornarlo
11927
+ if (fileName.includes('.')) {
11928
+ return fileName;
11929
+ }
11930
+ // Obtener extensión del MIME type
11931
+ const extension = this.getExtensionFromMimeType(mimeType);
11932
+ return extension ? `${fileName}.${extension}` : fileName;
11933
+ }
11934
+ /**
11935
+ * Obtiene la extensión del archivo basado en el MIME type.
11936
+ *
11937
+ * 📁 **Mapa de MIME Types Soportados:**
11938
+ *
11939
+ * | MIME Type | Extensión |
11940
+ * |-----------|-----------|
11941
+ * | application/pdf | pdf |
11942
+ * | application/msword | doc |
11943
+ * | application/vnd.openxmlformats-officedocument.wordprocessingml.document | docx |
11944
+ * | application/vnd.ms-excel | xls |
11945
+ * | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | xlsx |
11946
+ * | application/vnd.ms-powerpoint | ppt |
11947
+ * | application/vnd.openxmlformats-officedocument.presentationml.presentation | pptx |
11948
+ * | image/jpeg | jpg |
11949
+ * | image/png | png |
11950
+ * | image/gif | gif |
11951
+ * | text/plain | txt |
11952
+ * | application/json | json |
11953
+ * | application/zip | zip |
11954
+ * | application/octet-stream | bin |
11955
+ *
11956
+ * 🔄 **Comportamiento:**
11957
+ * 1. Busca el MIME type en el mapa predefinido.
11958
+ * 2. Si lo encuentra, retorna la extensión correspondiente.
11959
+ * 3. Si no lo encuentra, intenta extraer el subtipo del MIME (ej: 'pdf' de 'application/pdf').
11960
+ * 4. Si el subtipo es 'octet-stream' o no se puede determinar, retorna 'file'.
11961
+ *
11962
+ * @param mimeType - Tipo MIME del archivo
11963
+ * @returns Extensión del archivo (sin el punto) o 'file' si no se puede determinar
11964
+ */
11965
+ getExtensionFromMimeType(mimeType) {
11966
+ const mimeMap = {
11967
+ 'application/pdf': 'pdf',
11968
+ 'application/msword': 'doc',
11969
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
11970
+ 'application/vnd.ms-excel': 'xls',
11971
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
11972
+ 'application/vnd.ms-powerpoint': 'ppt',
11973
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
11974
+ 'image/jpeg': 'jpg',
11975
+ 'image/png': 'png',
11976
+ 'image/gif': 'gif',
11977
+ 'text/plain': 'txt',
11978
+ 'application/json': 'json',
11979
+ 'application/zip': 'zip',
11980
+ 'application/octet-stream': 'bin',
11981
+ };
11982
+ // Buscar extensión en el mapa
11983
+ const extension = mimeMap[mimeType];
11984
+ if (extension) {
11985
+ return extension;
11986
+ }
11987
+ // Si no está en el mapa, intentar extraer del MIME type
11988
+ const mimeParts = mimeType.split('/');
11989
+ if (mimeParts.length === 2) {
11990
+ const type = mimeParts[1];
11991
+ if (type !== 'octet-stream') {
11992
+ return type;
11993
+ }
11994
+ }
11995
+ // Si no se puede determinar, devolver 'file'
11996
+ return 'file';
11997
+ }
11998
+ /**
11999
+ * Formatea el tamaño del archivo para logging.
12000
+ *
12001
+ * 📊 **Formato de Salida:**
12002
+ * - Bytes: `"123 B"`
12003
+ * - Kilobytes: `"1.5 KB"`
12004
+ * - Megabytes: `"2.3 MB"`
12005
+ * - Gigabytes: `"1.2 GB"`
12006
+ *
12007
+ * 💡 **Ejemplos:**
12008
+ * - `formatFileSize(0)` → `"0 B"`
12009
+ * - `formatFileSize(1024)` → `"1.00 KB"`
12010
+ * - `formatFileSize(1536)` → `"1.50 KB"`
12011
+ * - `formatFileSize(1048576)` → `"1.00 MB"`
12012
+ *
12013
+ * @param bytes - Tamaño en bytes
12014
+ * @returns String formateado del tamaño
12015
+ */
12016
+ formatFileSize(bytes) {
12017
+ if (bytes === 0)
12018
+ return '0 B';
12019
+ const k = 1024;
12020
+ const sizes = ['B', 'KB', 'MB', 'GB'];
12021
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
12022
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
12023
+ }
12024
+ /**
12025
+ * Registra mensajes de log solo en entornos de desarrollo.
12026
+ *
12027
+ * 🛠️ **Comportamiento:**
12028
+ * - En desarrollo (production = false): Muestra los logs en consola
12029
+ * - En producción (production = true): No muestra nada
12030
+ *
12031
+ * 📝 **Uso Interno:**
12032
+ * Se utiliza para depuración sin afectar el rendimiento en producción.
12033
+ *
12034
+ * @param args - Argumentos para mostrar en el log
12035
+ */
12036
+ logIfNotProduction(...args) {
12037
+ if (!this.environment.production) {
12038
+ console.log('[ResultFileService]', ...args);
12039
+ }
12040
+ }
12041
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ResultFileReturnBlobNameService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
12042
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ResultFileReturnBlobNameService, providedIn: 'root' });
12043
+ }
12044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ResultFileReturnBlobNameService, decorators: [{
12045
+ type: Injectable,
12046
+ args: [{
12047
+ providedIn: 'root',
12048
+ }]
12049
+ }] });
12050
+
10263
12051
  class HelpersService {
10264
12052
  Array = inject(ArrayHelpersService);
12053
+ ServiceEntityPostFile = inject(SaveEntityPostFileService);
10265
12054
  ServiceResultFile = inject(ResultFileService);
10266
12055
  ServiceResultResponse = inject(ResultResponseService);
12056
+ ServiceResultFileReturnBlobName = inject(ResultFileReturnBlobNameService);
10267
12057
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: HelpersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
10268
12058
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: HelpersService, providedIn: 'root' });
10269
12059
  }
@@ -10721,19 +12511,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
10721
12511
  }]
10722
12512
  }] });
10723
12513
 
10724
- class DsxMessagesService {
10725
- AlertaService = inject(AlertaService);
10726
- SweetDialog = inject(SweetAlert2DialogService);
10727
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
10728
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxMessagesService, providedIn: 'root' });
10729
- }
10730
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DsxMessagesService, decorators: [{
10731
- type: Injectable,
10732
- args: [{
10733
- providedIn: 'root',
10734
- }]
10735
- }] });
10736
-
10737
12514
  // form-preview.service.ts
10738
12515
  class FormPreviewService {
10739
12516
  environment;
@@ -11980,5 +13757,5 @@ function sorensenDiceValidator(dataSource, key, umbral = 0.7) {
11980
13757
  * Generated bundle index. Do not edit.
11981
13758
  */
11982
13759
 
11983
- export { ACTION_TYPES, AlertaService, AppMessageErrorComponent, AppMessageHelpComponent, ArrowNavigationDirective, AuthorizeService, AutoScrollHeightDirective, BaseCRUDService, CACHE_KEYS, CacheService, CssV2Component, DSX_PALETTE, DateIndicator, DocxPreviewComponent, DsxAddToolsModule, DsxAutocomplete, DsxButtonComponent, DsxEnableDisable, DsxInputCurrency, DsxMessagesService, DsxSelect, DsxStatusToggle, DsxdateShort, DteService, ENVIRONMENT, EndpointService, ErrorHandlerService, FileComponent, FormPreviewService, GTQFormatter, HeaderDsx, HelpersService, HttpHelpersService, INITIAL_PARAMETERS, IcoLabel, IconDsxComponent, JoinByPipe, JsonHighlightPipe, JsonValuesDebujComponent, JsonViewerComponent, KpicardComponent, LoadingComponent, LoadingLottieComponent, LogoDsxComponent, MasterDetailChangeService, NavbarDsxComponent, NetworkStatusComponent, OnlyRangoPatternDirective, ParameterValuesService, PdfPreviewComponent, PrimeNgModule, QrGenerator, ResultFileService, SWEET_ALERT_THEMES, ScreenInspector, SecurityService, SelectAllOnFocusDirective, SpinnerLoadingService, SweetAlert2DialogService, TablePreviewService, TemplateHighlight, TokenPurposeLogin, TruncatePipe, UtilityAddService, asyncExistsValidator, atLeastOneFieldRequiredValidator, chainControlGroups, createCurrencyFormatter, createInitialCache, createTypedCacheProvider, cuiValidator, dateMinMaxValidator, dateRangeValidator, dateRangeValidatorFromTo, developmentEnvironment, getActionMessageConfig, getZeroBasedRolIndex, guardTokenPurposeGuard, httpAuthorizeInterceptor, minimumAgeValidator, nitValidator, productionEnvironment, provideEnvironment, sorensenDiceValidator, templateStructureValidator, templateVariablesValidator, validateEnvironmentConfig };
13760
+ export { ACTION_TYPES, AlertaService, AppMessageErrorComponent, AppMessageHelpComponent, ArrowNavigationDirective, AuthorizeService, AutoScrollHeightDirective, BaseCRUDService, CACHE_KEYS, CacheService, CssV2Component, DSX_PALETTE, DateIndicator, DocxPreviewComponent, DsxAddToolsModule, DsxAutocomplete, DsxButtonComponent, DsxEnableDisable, DsxInputCurrency, DsxMessagesService, DsxSelect, DsxStatusToggle, DsxdateShort, DsxdateTime, DteService, ENVIRONMENT, EndpointService, ErrorHandlerService, FileComponent, FormPreviewService, GTQFormatter, HeaderDsx, HelpersService, HttpHelpersService, INITIAL_PARAMETERS, IcoLabel, IconDsxComponent, JoinByPipe, JsonHighlightPipe, JsonValuesDebujComponent, JsonViewerComponent, KpicardComponent, LoadingComponent, LoadingLottieComponent, LogoDsxComponent, MasterDetailChangeService, NavbarDsxComponent, NetworkStatusComponent, OnlyRangoPatternDirective, ParameterValuesService, PdfPreviewComponent, PrimeNgModule, QrGenerator, ResultFileService, SWEET_ALERT_THEMES, ScreenInspector, SecurityService, SelectAllOnFocusDirective, SpinnerLoadingService, SweetAlert2DialogService, TablePreviewService, TemplateHighlight, TokenPurposeLogin, TruncatePipe, UtilityAddService, asyncExistsValidator, atLeastOneFieldRequiredValidator, chainControlGroups, createCurrencyFormatter, createInitialCache, createTypedCacheProvider, cuiValidator, dateMinMaxValidator, dateRangeValidator, dateRangeValidatorFromTo, developmentEnvironment, getActionMessageConfig, getZeroBasedRolIndex, guardTokenPurposeGuard, httpAuthorizeInterceptor, minimumAgeValidator, nitValidator, productionEnvironment, provideEnvironment, sorensenDiceValidator, templateStructureValidator, templateVariablesValidator, validateEnvironmentConfig };
11984
13761
  //# sourceMappingURL=ngx-dsxlibrary.mjs.map