valtech-components 4.0.988 → 4.0.990

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/esm2022/lib/components/organisms/attachment-uploader/attachment-uploader.component.mjs +41 -2
  2. package/esm2022/lib/components/organisms/attachment-uploader/types.mjs +1 -1
  3. package/esm2022/lib/components/organisms/request-review-panel/request-review-panel.component.mjs +316 -481
  4. package/esm2022/lib/components/organisms/request-review-panel/request-review-panel.i18n.mjs +21 -0
  5. package/esm2022/lib/components/organisms/request-review-panel/types.mjs +2 -0
  6. package/esm2022/lib/components/organisms/survey-builder/survey-builder.component.mjs +24 -5
  7. package/esm2022/lib/components/organisms/survey-builder/types.mjs +1 -1
  8. package/esm2022/lib/services/payments/config.mjs +23 -0
  9. package/esm2022/lib/services/payments/index.mjs +4 -0
  10. package/esm2022/lib/services/payments/payments-gateway.service.mjs +69 -0
  11. package/esm2022/lib/services/payments/types.mjs +8 -0
  12. package/esm2022/lib/version.mjs +2 -2
  13. package/esm2022/public-api.mjs +4 -2
  14. package/fesm2022/valtech-components.mjs +793 -784
  15. package/fesm2022/valtech-components.mjs.map +1 -1
  16. package/lib/components/organisms/attachment-uploader/attachment-uploader.component.d.ts +14 -0
  17. package/lib/components/organisms/attachment-uploader/types.d.ts +30 -0
  18. package/lib/components/organisms/request-review-panel/request-review-panel.component.d.ts +72 -76
  19. package/lib/components/organisms/request-review-panel/request-review-panel.i18n.d.ts +2 -0
  20. package/lib/components/organisms/request-review-panel/types.d.ts +73 -0
  21. package/lib/components/organisms/survey-builder/survey-builder.component.d.ts +9 -2
  22. package/lib/components/organisms/survey-builder/types.d.ts +28 -1
  23. package/lib/services/payments/config.d.ts +14 -0
  24. package/lib/services/payments/index.d.ts +3 -0
  25. package/lib/services/payments/payments-gateway.service.d.ts +53 -0
  26. package/lib/services/payments/types.d.ts +33 -0
  27. package/lib/version.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/public-api.d.ts +3 -1
@@ -70,7 +70,7 @@ import fixWebmDuration from 'fix-webm-duration';
70
70
  * Current version of valtech-components.
71
71
  * This is automatically updated during the publish process.
72
72
  */
73
- const VERSION = '4.0.988';
73
+ const VERSION = '4.0.990';
74
74
 
75
75
  function evaluateValtechAccess(rule, context, features = {}, visitedFeatures = new Set()) {
76
76
  if (rule == null)
@@ -37047,6 +37047,260 @@ const DEFAULT_FEEDBACK_TYPE_OPTIONS = [
37047
37047
  */
37048
37048
  // Configuration
37049
37049
 
37050
+ /**
37051
+ * Default values for image processing
37052
+ */
37053
+ const IMAGE_DEFAULTS = {
37054
+ maxWidth: 800,
37055
+ maxHeight: 800,
37056
+ quality: 0.8,
37057
+ mimeType: 'image/jpeg',
37058
+ maxSize: 10 * 1024 * 1024, // 10MB
37059
+ allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
37060
+ thumbnailSize: 150,
37061
+ };
37062
+
37063
+ /**
37064
+ * ImageService
37065
+ *
37066
+ * Service for image processing including compression, thumbnails, cropping and validation.
37067
+ * Uses HTML Canvas for all operations - no external dependencies.
37068
+ *
37069
+ * @example
37070
+ * ```typescript
37071
+ * const imageService = inject(ImageService);
37072
+ *
37073
+ * // Compress an image
37074
+ * const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
37075
+ *
37076
+ * // Generate thumbnail
37077
+ * const thumb = await imageService.thumbnail(file, 150);
37078
+ *
37079
+ * // Validate before processing
37080
+ * const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
37081
+ * if (!validation.valid) {
37082
+ * console.error(validation.message);
37083
+ * }
37084
+ * ```
37085
+ */
37086
+ class ImageService {
37087
+ /**
37088
+ * Compress an image maintaining aspect ratio
37089
+ * @param file - File or Blob to compress
37090
+ * @param options - Compression options
37091
+ * @returns Promise with processed image data
37092
+ */
37093
+ async compress(file, options) {
37094
+ const opts = {
37095
+ maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
37096
+ maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
37097
+ quality: options?.quality ?? IMAGE_DEFAULTS.quality,
37098
+ mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
37099
+ };
37100
+ const img = await this.loadImage(file);
37101
+ const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
37102
+ const canvas = document.createElement('canvas');
37103
+ canvas.width = width;
37104
+ canvas.height = height;
37105
+ const ctx = canvas.getContext('2d');
37106
+ ctx.drawImage(img, 0, 0, width, height);
37107
+ const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
37108
+ const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
37109
+ return {
37110
+ blob,
37111
+ dataUrl,
37112
+ width,
37113
+ height,
37114
+ size: blob.size,
37115
+ };
37116
+ }
37117
+ /**
37118
+ * Generate a square thumbnail from an image
37119
+ * @param file - File or Blob to process
37120
+ * @param size - Thumbnail size in pixels (default: 150)
37121
+ * @returns Promise with processed thumbnail
37122
+ */
37123
+ async thumbnail(file, size) {
37124
+ const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
37125
+ const img = await this.loadImage(file);
37126
+ // Calculate square crop from center
37127
+ const minDim = Math.min(img.width, img.height);
37128
+ const cropX = (img.width - minDim) / 2;
37129
+ const cropY = (img.height - minDim) / 2;
37130
+ const canvas = document.createElement('canvas');
37131
+ canvas.width = thumbSize;
37132
+ canvas.height = thumbSize;
37133
+ const ctx = canvas.getContext('2d');
37134
+ ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
37135
+ const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
37136
+ );
37137
+ const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
37138
+ return {
37139
+ blob,
37140
+ dataUrl,
37141
+ width: thumbSize,
37142
+ height: thumbSize,
37143
+ size: blob.size,
37144
+ };
37145
+ }
37146
+ /**
37147
+ * Crop an image with specific coordinates
37148
+ * @param file - File or Blob to crop
37149
+ * @param cropData - Crop coordinates and dimensions
37150
+ * @param options - Optional compression options for output
37151
+ * @returns Promise with cropped image
37152
+ */
37153
+ async crop(file, cropData, options) {
37154
+ const img = await this.loadImage(file);
37155
+ const opts = {
37156
+ quality: options?.quality ?? IMAGE_DEFAULTS.quality,
37157
+ mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
37158
+ };
37159
+ const canvas = document.createElement('canvas');
37160
+ canvas.width = cropData.width;
37161
+ canvas.height = cropData.height;
37162
+ const ctx = canvas.getContext('2d');
37163
+ ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
37164
+ // Apply max dimensions if specified
37165
+ if (options?.maxWidth || options?.maxHeight) {
37166
+ return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
37167
+ }
37168
+ const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
37169
+ const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
37170
+ return {
37171
+ blob,
37172
+ dataUrl,
37173
+ width: cropData.width,
37174
+ height: cropData.height,
37175
+ size: blob.size,
37176
+ };
37177
+ }
37178
+ /**
37179
+ * Validate an image file before processing
37180
+ * @param file - File to validate
37181
+ * @param options - Validation options
37182
+ * @returns Validation result with error details if invalid
37183
+ */
37184
+ validate(file, options) {
37185
+ const opts = {
37186
+ maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
37187
+ allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
37188
+ };
37189
+ // Check file type
37190
+ if (!opts.allowedTypes.includes(file.type)) {
37191
+ return {
37192
+ valid: false,
37193
+ error: 'invalidType',
37194
+ message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
37195
+ };
37196
+ }
37197
+ // Check file size
37198
+ if (file.size > opts.maxSize) {
37199
+ const maxMB = Math.round(opts.maxSize / (1024 * 1024));
37200
+ return {
37201
+ valid: false,
37202
+ error: 'fileTooLarge',
37203
+ message: `La imagen es muy grande. Máximo ${maxMB}MB`,
37204
+ };
37205
+ }
37206
+ return { valid: true };
37207
+ }
37208
+ /**
37209
+ * Validate image dimensions (async - requires loading image)
37210
+ * @param file - File to validate
37211
+ * @param options - Validation options with minWidth/minHeight
37212
+ * @returns Promise with validation result
37213
+ */
37214
+ async validateDimensions(file, options) {
37215
+ const img = await this.loadImage(file);
37216
+ if (options.minWidth && img.width < options.minWidth) {
37217
+ return {
37218
+ valid: false,
37219
+ error: 'imageTooSmall',
37220
+ message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
37221
+ };
37222
+ }
37223
+ if (options.minHeight && img.height < options.minHeight) {
37224
+ return {
37225
+ valid: false,
37226
+ error: 'imageTooSmall',
37227
+ message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
37228
+ };
37229
+ }
37230
+ return { valid: true };
37231
+ }
37232
+ /**
37233
+ * Convert a Blob/File to a data URL
37234
+ */
37235
+ async toDataUrl(file) {
37236
+ return new Promise((resolve, reject) => {
37237
+ const reader = new FileReader();
37238
+ reader.onload = () => resolve(reader.result);
37239
+ reader.onerror = reject;
37240
+ reader.readAsDataURL(file);
37241
+ });
37242
+ }
37243
+ /**
37244
+ * Convert a data URL to a Blob
37245
+ */
37246
+ dataUrlToBlob(dataUrl) {
37247
+ const arr = dataUrl.split(',');
37248
+ const mime = arr[0].match(/:(.*?);/)[1];
37249
+ const bstr = atob(arr[1]);
37250
+ let n = bstr.length;
37251
+ const u8arr = new Uint8Array(n);
37252
+ while (n--) {
37253
+ u8arr[n] = bstr.charCodeAt(n);
37254
+ }
37255
+ return new Blob([u8arr], { type: mime });
37256
+ }
37257
+ // ============== Private Helpers ==============
37258
+ loadImage(file) {
37259
+ return new Promise((resolve, reject) => {
37260
+ const img = new Image();
37261
+ img.onload = () => {
37262
+ URL.revokeObjectURL(img.src);
37263
+ resolve(img);
37264
+ };
37265
+ img.onerror = reject;
37266
+ img.src = URL.createObjectURL(file);
37267
+ });
37268
+ }
37269
+ calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
37270
+ let width = originalWidth;
37271
+ let height = originalHeight;
37272
+ // Scale down if necessary, maintaining aspect ratio
37273
+ if (width > maxWidth) {
37274
+ height = (height * maxWidth) / width;
37275
+ width = maxWidth;
37276
+ }
37277
+ if (height > maxHeight) {
37278
+ width = (width * maxHeight) / height;
37279
+ height = maxHeight;
37280
+ }
37281
+ return {
37282
+ width: Math.round(width),
37283
+ height: Math.round(height),
37284
+ };
37285
+ }
37286
+ canvasToBlob(canvas, mimeType, quality) {
37287
+ return new Promise((resolve, reject) => {
37288
+ canvas.toBlob((blob) => {
37289
+ if (blob)
37290
+ resolve(blob);
37291
+ else
37292
+ reject(new Error('Failed to create blob from canvas'));
37293
+ }, mimeType, quality);
37294
+ });
37295
+ }
37296
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
37297
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
37298
+ }
37299
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
37300
+ type: Injectable,
37301
+ args: [{ providedIn: 'root' }]
37302
+ }] });
37303
+
37050
37304
  class AttachmentUploaderComponent {
37051
37305
  get readyUrls() {
37052
37306
  return this.attachments()
@@ -37069,6 +37323,7 @@ class AttachmentUploaderComponent {
37069
37323
  this.attachmentsChange = output();
37070
37324
  this.i18n = inject(I18nService);
37071
37325
  this.feedbackService = inject(FeedbackService, { optional: true });
37326
+ this.imageService = inject(ImageService);
37072
37327
  this.attachments = signal([]);
37073
37328
  this.showCameraOverlay = signal(false);
37074
37329
  this.cameraStream = null;
@@ -37170,6 +37425,35 @@ class AttachmentUploaderComponent {
37170
37425
  });
37171
37426
  });
37172
37427
  }
37428
+ /**
37429
+ * Genera y sube la miniatura de una imagen ya comprimida.
37430
+ *
37431
+ * Recibe el archivo COMPRIMIDO a propósito: `compressorjs` ya normalizó la
37432
+ * orientación EXIF, y `ImageService` trabaja sobre canvas sin leer EXIF — si
37433
+ * se le pasara el original, las fotos verticales de teléfono saldrían
37434
+ * rotadas. Ver la nota en `generateThumbnail` (types.ts).
37435
+ *
37436
+ * Nunca es fatal: si falla, el adjunto queda subido sin miniatura y las
37437
+ * listas caen al archivo grande. Perder la miniatura no justifica perder la
37438
+ * foto que el usuario acaba de subir.
37439
+ */
37440
+ async uploadThumbnail(compressed) {
37441
+ try {
37442
+ const size = this.props().thumbnailSize ?? 320;
37443
+ const thumb = await this.imageService.thumbnail(compressed, size);
37444
+ const baseName = compressed.name.replace(/\.[^.]+$/, '');
37445
+ const thumbFile = new File([thumb.blob], `${baseName}-thumb.jpg`, { type: 'image/jpeg' });
37446
+ const thumbUploadFn = this.props().thumbnailUploadFn ??
37447
+ this.props().uploadFn ??
37448
+ this.feedbackService?.uploadAttachment.bind(this.feedbackService);
37449
+ if (!thumbUploadFn)
37450
+ return undefined;
37451
+ return await thumbUploadFn(thumbFile);
37452
+ }
37453
+ catch {
37454
+ return undefined;
37455
+ }
37456
+ }
37173
37457
  async uploadFile(id, file) {
37174
37458
  try {
37175
37459
  const shouldCompress = this.props().compressImages !== false && file.type.startsWith('image/');
@@ -37178,7 +37462,15 @@ class AttachmentUploaderComponent {
37178
37462
  if (!uploadFn)
37179
37463
  throw new Error('No upload function configured');
37180
37464
  const url = await uploadFn(fileToUpload);
37181
- this.attachments.update(list => list.map(a => (a.id === id ? { ...a, status: 'ready', url } : a)));
37465
+ // La miniatura SIEMPRE sale de un archivo pasado por compressorjs, que es
37466
+ // lo que endereza la orientación EXIF. Si el consumer desactivó la
37467
+ // compresión, se comprime igual solo para generar la miniatura — el
37468
+ // archivo que se sube como principal sigue siendo el original.
37469
+ const wantsThumb = this.props().generateThumbnail === true && file.type.startsWith('image/');
37470
+ const thumbnailUrl = wantsThumb
37471
+ ? await this.uploadThumbnail(shouldCompress ? fileToUpload : await this.compressImage(file))
37472
+ : undefined;
37473
+ this.attachments.update(list => list.map(a => (a.id === id ? { ...a, status: 'ready', url, thumbnailUrl } : a)));
37182
37474
  }
37183
37475
  catch {
37184
37476
  const error = this.i18n.t('attachUploadFailed');
@@ -38719,7 +39011,7 @@ const SURVEY_RESPONSE_I18N = {
38719
39011
  },
38720
39012
  };
38721
39013
 
38722
- const NAMESPACE$3 = 'SurveyResponse';
39014
+ const NAMESPACE$4 = 'SurveyResponse';
38723
39015
  /** Code del backend cuando la invitación de esa persona ya se respondió. */
38724
39016
  const INVITE_USED_CODE = 'SURVEY_INVITE_ALREADY_USED';
38725
39017
  /** Tipos cuya respuesta es un booleano, no un texto. */
@@ -38798,8 +39090,8 @@ class SurveyResponseComponent {
38798
39090
  this.i18n.lang();
38799
39091
  return { variant: 'error', title: this.t('loginRequiredTitle'), description: this.t('loginRequiredBody') };
38800
39092
  });
38801
- if (!this.i18n.hasNamespace(NAMESPACE$3)) {
38802
- this.i18n.registerDefaults(NAMESPACE$3, SURVEY_RESPONSE_I18N);
39093
+ if (!this.i18n.hasNamespace(NAMESPACE$4)) {
39094
+ this.i18n.registerDefaults(NAMESPACE$4, SURVEY_RESPONSE_I18N);
38803
39095
  }
38804
39096
  }
38805
39097
  async ngOnInit() {
@@ -38989,12 +39281,12 @@ class SurveyResponseComponent {
38989
39281
  this.errors.handle(err, {
38990
39282
  context: 'surveyResponse.submit',
38991
39283
  fallbackKey: 'sendError',
38992
- i18nNamespace: NAMESPACE$3,
39284
+ i18nNamespace: NAMESPACE$4,
38993
39285
  });
38994
39286
  }
38995
39287
  }
38996
39288
  t(key) {
38997
- return this.i18n.t(key, NAMESPACE$3);
39289
+ return this.i18n.t(key, NAMESPACE$4);
38998
39290
  }
38999
39291
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SurveyResponseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
39000
39292
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SurveyResponseComponent, isStandalone: true, selector: "val-survey-response", inputs: { props: "props" }, outputs: { submitted: "submitted" }, ngImport: i0, template: `
@@ -39603,7 +39895,7 @@ const FIELD_SCHEMA_EDITOR_I18N = {
39603
39895
  },
39604
39896
  };
39605
39897
 
39606
- const NAMESPACE$2 = 'FieldSchemaEditor';
39898
+ const NAMESPACE$3 = 'FieldSchemaEditor';
39607
39899
  /**
39608
39900
  * Tipos cuyo valor sale de una lista cerrada que define quien arma el
39609
39901
  * formulario — sin al menos una opción, el campo no se puede responder.
@@ -39688,8 +39980,8 @@ class FieldSchemaEditorComponent {
39688
39980
  options: this.options(),
39689
39981
  };
39690
39982
  });
39691
- if (!this.i18n.hasNamespace(NAMESPACE$2)) {
39692
- this.i18n.registerDefaults(NAMESPACE$2, FIELD_SCHEMA_EDITOR_I18N);
39983
+ if (!this.i18n.hasNamespace(NAMESPACE$3)) {
39984
+ this.i18n.registerDefaults(NAMESPACE$3, FIELD_SCHEMA_EDITOR_I18N);
39693
39985
  }
39694
39986
  this.typeControl.valueChanges.subscribe(value => {
39695
39987
  this.currentType.set(value || 'TEXT');
@@ -39726,7 +40018,7 @@ class FieldSchemaEditorComponent {
39726
40018
  }
39727
40019
  t(key) {
39728
40020
  this.i18n.lang();
39729
- return this.i18n.t(key, NAMESPACE$2);
40021
+ return this.i18n.t(key, NAMESPACE$3);
39730
40022
  }
39731
40023
  state() {
39732
40024
  return this.props.state ?? ComponentStates.ENABLED;
@@ -39953,6 +40245,379 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
39953
40245
  type: Output
39954
40246
  }] } });
39955
40247
 
40248
+ const REQUEST_REVIEW_PANEL_I18N = {
40249
+ es: {
40250
+ approve: 'Aprobar',
40251
+ reject: 'Rechazar',
40252
+ cancel: 'Cancelar',
40253
+ confirmReject: 'Confirmar rechazo',
40254
+ rejectReason: 'Motivo del rechazo',
40255
+ rejectReasonPlaceholder: 'Cuéntale qué le faltó o qué debe corregir',
40256
+ rejectReasonHint: 'Esto le llega a quien envió la solicitud.',
40257
+ },
40258
+ en: {
40259
+ approve: 'Approve',
40260
+ reject: 'Reject',
40261
+ cancel: 'Cancel',
40262
+ confirmReject: 'Confirm rejection',
40263
+ rejectReason: 'Reason for rejection',
40264
+ rejectReasonPlaceholder: 'Tell them what was missing or needs fixing',
40265
+ rejectReasonHint: 'This is sent to whoever submitted the request.',
40266
+ },
40267
+ };
40268
+
40269
+ const NAMESPACE$2 = 'RequestReviewPanel';
40270
+ /**
40271
+ * val-request-review-panel — cola de revisión del factory (ADR-062).
40272
+ *
40273
+ * Es **presentacional**: no carga datos, no conoce el backend ni el dominio. El
40274
+ * consumer le pasa la cola ya normalizada y escucha `(decision)`.
40275
+ *
40276
+ * ## Por qué el cuerpo va proyectado y no configurado
40277
+ *
40278
+ * El ADR original proponía describir el cuerpo con arrays de nombres de campo
40279
+ * (`fields.display: ['taxId', 'tagsRequested']`). Al implementarlo con los dos
40280
+ * consumidores reales delante quedó claro que eso reinventa un motor de
40281
+ * templating peor que Angular: Chesed necesita una lista de documentos con
40282
+ * iconos y `target="_blank"`, Okhelia un enlace a la receta y un input de
40283
+ * comentario. Ninguna lista de strings expresa eso sin agregarle un flag al
40284
+ * componente por cada caso nuevo.
40285
+ *
40286
+ * Entonces el panel es dueño de lo que **de verdad** se repite — la cáscara, el
40287
+ * chrome de la card, y la mecánica de decisión — y el cuerpo de cada ítem lo
40288
+ * escribe cada vertical en un `ng-template` con su propio HTML.
40289
+ *
40290
+ * ```html
40291
+ * <val-request-review-panel [props]="panelProps()" [itemBody]="body"
40292
+ * (decision)="onDecision($event)" />
40293
+ *
40294
+ * <ng-template #body let-item>
40295
+ * <a [routerLink]="['/app/recetas', recipeIdOf(item)]">Ver receta</a>
40296
+ * </ng-template>
40297
+ * ```
40298
+ *
40299
+ * ## Rechazo con motivo
40300
+ *
40301
+ * El flujo de dos pasos (Rechazar → escribir motivo → Confirmar) es del panel,
40302
+ * no del consumer, y está activo por defecto. Rechazar de un click deja al
40303
+ * solicitante sin saber qué corregir; era el único de los dos paneles que lo
40304
+ * tenía bien resuelto y ahora lo heredan los dos.
40305
+ */
40306
+ class RequestReviewPanelComponent {
40307
+ constructor() {
40308
+ this.i18n = inject(I18nService);
40309
+ this.props = input.required();
40310
+ this.decision = new EventEmitter();
40311
+ /** ID del ítem cuyo formulario de rechazo está abierto. */
40312
+ this._rejectingId = signal('');
40313
+ this.rejectingId = this._rejectingId.asReadonly();
40314
+ this._reason = signal('');
40315
+ this.reason = this._reason.asReadonly();
40316
+ this.cfg = computed(() => this.props());
40317
+ this.skeletonRows = computed(() => Array.from({ length: this.cfg().skeletonRows ?? 3 }, (_, i) => i));
40318
+ /** Hay una decisión en vuelo: toda la cola se congela. */
40319
+ this.isBusy = computed(() => !!this.cfg().busyItemId);
40320
+ if (!this.i18n.hasNamespace(NAMESPACE$2)) {
40321
+ this.i18n.registerContent(NAMESPACE$2, REQUEST_REVIEW_PANEL_I18N);
40322
+ }
40323
+ }
40324
+ t(key) {
40325
+ return this.i18n.t(key, NAMESPACE$2);
40326
+ }
40327
+ /** El ítem en vuelo muestra WORKING; los demás quedan deshabilitados. */
40328
+ stateFor(item) {
40329
+ const busyId = this.cfg().busyItemId;
40330
+ if (busyId === item.id)
40331
+ return 'WORKING';
40332
+ return busyId ? 'DISABLED' : 'ENABLED';
40333
+ }
40334
+ approveState(item) {
40335
+ return this.stateFor(item);
40336
+ }
40337
+ /**
40338
+ * Confirmar el rechazo exige motivo cuando `requireRejectReason` está activo
40339
+ * (el default). Es el freno que evita la notificación de rechazo vacía.
40340
+ */
40341
+ confirmRejectState(item) {
40342
+ const state = this.stateFor(item);
40343
+ if (state !== 'ENABLED')
40344
+ return state;
40345
+ const needsReason = this.cfg().requireRejectReason !== false;
40346
+ return needsReason && this._reason().trim().length === 0 ? 'DISABLED' : 'ENABLED';
40347
+ }
40348
+ onReasonInput(event) {
40349
+ this._reason.set(event.target.value);
40350
+ }
40351
+ startReject(item) {
40352
+ // Sin motivo obligatorio el rechazo es directo: abrir un formulario que no
40353
+ // exige nada solo agrega un paso.
40354
+ if (this.cfg().requireRejectReason === false) {
40355
+ this.decision.emit({ itemId: item.id, approved: false });
40356
+ return;
40357
+ }
40358
+ this._reason.set('');
40359
+ this._rejectingId.set(item.id);
40360
+ }
40361
+ cancelReject() {
40362
+ this._rejectingId.set('');
40363
+ this._reason.set('');
40364
+ }
40365
+ confirmReject(item) {
40366
+ const reason = this._reason().trim();
40367
+ if (this.cfg().requireRejectReason !== false && reason.length === 0)
40368
+ return;
40369
+ this.decision.emit({ itemId: item.id, approved: false, reason: reason || undefined });
40370
+ this.cancelReject();
40371
+ }
40372
+ approve(item) {
40373
+ this.decision.emit({ itemId: item.id, approved: true });
40374
+ }
40375
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestReviewPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
40376
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestReviewPanelComponent, isStandalone: true, selector: "val-request-review-panel", inputs: { props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: true, transformFunction: null }, itemBody: { classPropertyName: "itemBody", publicName: "itemBody", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { decision: "decision" }, ngImport: i0, template: `
40377
+ <div class="rrp">
40378
+ @if (cfg().loading) {
40379
+ <div class="rrp__skel" aria-hidden="true">
40380
+ @for (row of skeletonRows(); track row) {
40381
+ <val-skeleton [props]="{ width: '100%', height: '120px', borderRadius: '14px' }" />
40382
+ }
40383
+ </div>
40384
+ } @else if (cfg().errorState) {
40385
+ <val-empty-state [props]="cfg().errorState!" />
40386
+ } @else if (cfg().items.length === 0) {
40387
+ @if (cfg().emptyState; as empty) {
40388
+ <val-empty-state [props]="empty" />
40389
+ }
40390
+ } @else {
40391
+ @if (cfg().countLabel; as count) {
40392
+ <p class="rrp__count">{{ count }}</p>
40393
+ }
40394
+
40395
+ <ul class="rrp__queue">
40396
+ @for (item of cfg().items; track item.id) {
40397
+ <li class="rrp__item">
40398
+ <h3 class="rrp__title">{{ item.title }}</h3>
40399
+
40400
+ @for (line of item.meta ?? []; track line) {
40401
+ <p class="rrp__meta">{{ line }}</p>
40402
+ }
40403
+
40404
+ @if (itemBody) {
40405
+ <div class="rrp__body">
40406
+ <ng-container
40407
+ [ngTemplateOutlet]="itemBody"
40408
+ [ngTemplateOutletContext]="{ $implicit: item, item: item }"
40409
+ />
40410
+ </div>
40411
+ }
40412
+
40413
+ @if (rejectingId() === item.id) {
40414
+ <div class="rrp__reject">
40415
+ <label class="rrp__reject-label" [attr.for]="'rrp-note-' + item.id">
40416
+ {{ cfg().rejectReasonLabel ?? t('rejectReason') }}
40417
+ </label>
40418
+ <textarea
40419
+ class="rrp__reject-input"
40420
+ [id]="'rrp-note-' + item.id"
40421
+ rows="3"
40422
+ [placeholder]="cfg().rejectReasonPlaceholder ?? t('rejectReasonPlaceholder')"
40423
+ [value]="reason()"
40424
+ (input)="onReasonInput($event)"
40425
+ ></textarea>
40426
+ <p class="rrp__reject-hint">
40427
+ {{ cfg().rejectReasonHint ?? t('rejectReasonHint') }}
40428
+ </p>
40429
+
40430
+ <div class="rrp__actions">
40431
+ <val-button
40432
+ [props]="{
40433
+ token: 'rrp-cancel-' + item.id,
40434
+ text: cfg().cancelLabel ?? t('cancel'),
40435
+ color: 'dark',
40436
+ fill: 'clear',
40437
+ size: 'small',
40438
+ type: 'button',
40439
+ state: isBusy() ? 'DISABLED' : 'ENABLED',
40440
+ }"
40441
+ (onClick)="cancelReject()"
40442
+ />
40443
+ <val-button
40444
+ [props]="{
40445
+ token: 'rrp-confirm-reject-' + item.id,
40446
+ text: cfg().confirmRejectLabel ?? t('confirmReject'),
40447
+ color: 'danger',
40448
+ fill: 'solid',
40449
+ shape: 'round',
40450
+ size: 'small',
40451
+ type: 'button',
40452
+ state: confirmRejectState(item),
40453
+ }"
40454
+ (onClick)="confirmReject(item)"
40455
+ />
40456
+ </div>
40457
+ </div>
40458
+ } @else {
40459
+ <div class="rrp__actions">
40460
+ <val-button
40461
+ [props]="{
40462
+ token: 'rrp-reject-' + item.id,
40463
+ text: cfg().rejectLabel ?? t('reject'),
40464
+ color: 'dark',
40465
+ fill: 'outline',
40466
+ shape: 'round',
40467
+ size: 'small',
40468
+ type: 'button',
40469
+ state: isBusy() ? 'DISABLED' : 'ENABLED',
40470
+ }"
40471
+ (onClick)="startReject(item)"
40472
+ />
40473
+ <val-button
40474
+ [props]="{
40475
+ token: 'rrp-approve-' + item.id,
40476
+ text: cfg().approveLabel ?? t('approve'),
40477
+ color: 'primary',
40478
+ fill: 'solid',
40479
+ shape: 'round',
40480
+ size: 'small',
40481
+ type: 'button',
40482
+ state: approveState(item),
40483
+ }"
40484
+ (onClick)="approve(item)"
40485
+ />
40486
+ </div>
40487
+ }
40488
+ </li>
40489
+ }
40490
+ </ul>
40491
+ }
40492
+ </div>
40493
+ `, isInline: true, styles: [":host{display:block}.rrp{display:flex;flex-direction:column;gap:12px}.rrp__skel{display:flex;flex-direction:column;gap:10px}.rrp__count{margin:0;font-size:.8125rem;font-weight:700;color:var(--ion-color-medium, #92949c);font-variant-numeric:tabular-nums}.rrp__queue{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.rrp__item{display:flex;flex-direction:column;gap:8px;padding:14px 16px;border-radius:14px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1))}.rrp__title{margin:0;font-size:1rem;font-weight:700;color:var(--ion-text-color, #000)}.rrp__meta{margin:0;font-size:.8125rem;color:var(--ion-color-medium, #92949c)}.rrp__body{display:flex;flex-direction:column;gap:8px}.rrp__reject{display:flex;flex-direction:column;gap:6px}.rrp__reject-label{font-size:.8125rem;font-weight:600;color:var(--ion-text-color, #000)}.rrp__reject-input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:10px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-background-color, #fff);color:var(--ion-text-color, #000);font-family:inherit;font-size:.875rem;resize:vertical}.rrp__reject-input:focus{outline:none;border-color:var(--ion-color-primary)}.rrp__reject-hint{margin:0;font-size:.75rem;color:var(--ion-color-medium, #92949c)}.rrp__actions{display:flex;flex-direction:column;gap:8px;margin-top:4px}@media (min-width: 576px){.rrp__actions{flex-direction:row;justify-content:flex-end;align-items:center;flex-wrap:wrap}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
40494
+ }
40495
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestReviewPanelComponent, decorators: [{
40496
+ type: Component,
40497
+ args: [{ selector: 'val-request-review-panel', standalone: true, imports: [CommonModule, ButtonComponent, EmptyStateComponent, SkeletonComponent], template: `
40498
+ <div class="rrp">
40499
+ @if (cfg().loading) {
40500
+ <div class="rrp__skel" aria-hidden="true">
40501
+ @for (row of skeletonRows(); track row) {
40502
+ <val-skeleton [props]="{ width: '100%', height: '120px', borderRadius: '14px' }" />
40503
+ }
40504
+ </div>
40505
+ } @else if (cfg().errorState) {
40506
+ <val-empty-state [props]="cfg().errorState!" />
40507
+ } @else if (cfg().items.length === 0) {
40508
+ @if (cfg().emptyState; as empty) {
40509
+ <val-empty-state [props]="empty" />
40510
+ }
40511
+ } @else {
40512
+ @if (cfg().countLabel; as count) {
40513
+ <p class="rrp__count">{{ count }}</p>
40514
+ }
40515
+
40516
+ <ul class="rrp__queue">
40517
+ @for (item of cfg().items; track item.id) {
40518
+ <li class="rrp__item">
40519
+ <h3 class="rrp__title">{{ item.title }}</h3>
40520
+
40521
+ @for (line of item.meta ?? []; track line) {
40522
+ <p class="rrp__meta">{{ line }}</p>
40523
+ }
40524
+
40525
+ @if (itemBody) {
40526
+ <div class="rrp__body">
40527
+ <ng-container
40528
+ [ngTemplateOutlet]="itemBody"
40529
+ [ngTemplateOutletContext]="{ $implicit: item, item: item }"
40530
+ />
40531
+ </div>
40532
+ }
40533
+
40534
+ @if (rejectingId() === item.id) {
40535
+ <div class="rrp__reject">
40536
+ <label class="rrp__reject-label" [attr.for]="'rrp-note-' + item.id">
40537
+ {{ cfg().rejectReasonLabel ?? t('rejectReason') }}
40538
+ </label>
40539
+ <textarea
40540
+ class="rrp__reject-input"
40541
+ [id]="'rrp-note-' + item.id"
40542
+ rows="3"
40543
+ [placeholder]="cfg().rejectReasonPlaceholder ?? t('rejectReasonPlaceholder')"
40544
+ [value]="reason()"
40545
+ (input)="onReasonInput($event)"
40546
+ ></textarea>
40547
+ <p class="rrp__reject-hint">
40548
+ {{ cfg().rejectReasonHint ?? t('rejectReasonHint') }}
40549
+ </p>
40550
+
40551
+ <div class="rrp__actions">
40552
+ <val-button
40553
+ [props]="{
40554
+ token: 'rrp-cancel-' + item.id,
40555
+ text: cfg().cancelLabel ?? t('cancel'),
40556
+ color: 'dark',
40557
+ fill: 'clear',
40558
+ size: 'small',
40559
+ type: 'button',
40560
+ state: isBusy() ? 'DISABLED' : 'ENABLED',
40561
+ }"
40562
+ (onClick)="cancelReject()"
40563
+ />
40564
+ <val-button
40565
+ [props]="{
40566
+ token: 'rrp-confirm-reject-' + item.id,
40567
+ text: cfg().confirmRejectLabel ?? t('confirmReject'),
40568
+ color: 'danger',
40569
+ fill: 'solid',
40570
+ shape: 'round',
40571
+ size: 'small',
40572
+ type: 'button',
40573
+ state: confirmRejectState(item),
40574
+ }"
40575
+ (onClick)="confirmReject(item)"
40576
+ />
40577
+ </div>
40578
+ </div>
40579
+ } @else {
40580
+ <div class="rrp__actions">
40581
+ <val-button
40582
+ [props]="{
40583
+ token: 'rrp-reject-' + item.id,
40584
+ text: cfg().rejectLabel ?? t('reject'),
40585
+ color: 'dark',
40586
+ fill: 'outline',
40587
+ shape: 'round',
40588
+ size: 'small',
40589
+ type: 'button',
40590
+ state: isBusy() ? 'DISABLED' : 'ENABLED',
40591
+ }"
40592
+ (onClick)="startReject(item)"
40593
+ />
40594
+ <val-button
40595
+ [props]="{
40596
+ token: 'rrp-approve-' + item.id,
40597
+ text: cfg().approveLabel ?? t('approve'),
40598
+ color: 'primary',
40599
+ fill: 'solid',
40600
+ shape: 'round',
40601
+ size: 'small',
40602
+ type: 'button',
40603
+ state: approveState(item),
40604
+ }"
40605
+ (onClick)="approve(item)"
40606
+ />
40607
+ </div>
40608
+ }
40609
+ </li>
40610
+ }
40611
+ </ul>
40612
+ }
40613
+ </div>
40614
+ `, styles: [":host{display:block}.rrp{display:flex;flex-direction:column;gap:12px}.rrp__skel{display:flex;flex-direction:column;gap:10px}.rrp__count{margin:0;font-size:.8125rem;font-weight:700;color:var(--ion-color-medium, #92949c);font-variant-numeric:tabular-nums}.rrp__queue{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.rrp__item{display:flex;flex-direction:column;gap:8px;padding:14px 16px;border-radius:14px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1))}.rrp__title{margin:0;font-size:1rem;font-weight:700;color:var(--ion-text-color, #000)}.rrp__meta{margin:0;font-size:.8125rem;color:var(--ion-color-medium, #92949c)}.rrp__body{display:flex;flex-direction:column;gap:8px}.rrp__reject{display:flex;flex-direction:column;gap:6px}.rrp__reject-label{font-size:.8125rem;font-weight:600;color:var(--ion-text-color, #000)}.rrp__reject-input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:10px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-background-color, #fff);color:var(--ion-text-color, #000);font-family:inherit;font-size:.875rem;resize:vertical}.rrp__reject-input:focus{outline:none;border-color:var(--ion-color-primary)}.rrp__reject-hint{margin:0;font-size:.75rem;color:var(--ion-color-medium, #92949c)}.rrp__actions{display:flex;flex-direction:column;gap:8px;margin-top:4px}@media (min-width: 576px){.rrp__actions{flex-direction:row;justify-content:flex-end;align-items:center;flex-wrap:wrap}}\n"] }]
40615
+ }], ctorParameters: () => [], propDecorators: { itemBody: [{
40616
+ type: Input
40617
+ }], decision: [{
40618
+ type: Output
40619
+ }] } });
40620
+
39956
40621
  class RequestFormBuilderService extends FormSchemaBuilderService {
39957
40622
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestFormBuilderService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
39958
40623
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestFormBuilderService, providedIn: 'root' }); }
@@ -40074,6 +40739,13 @@ class SurveyBuilderComponent {
40074
40739
  constructor() {
40075
40740
  this.props = {};
40076
40741
  this.saved = new EventEmitter();
40742
+ /**
40743
+ * Emite lo escrito, ya validado, SIN guardarlo. Solo con `props.persist:
40744
+ * false` — ver el comentario de ese campo: cuando la app tiene endpoint
40745
+ * propio, guardar desde acá se salta la validación de la entidad dueña y el
40746
+ * permiso de la vertical.
40747
+ */
40748
+ this.draft = new EventEmitter();
40077
40749
  this.i18n = inject(I18nService);
40078
40750
  this.requests = inject(RequestService);
40079
40751
  this.formBuilder = inject(RequestFormBuilderService);
@@ -40235,7 +40907,6 @@ class SurveyBuilderComponent {
40235
40907
  this.validationError.set(this.t('missingOptions'));
40236
40908
  return;
40237
40909
  }
40238
- this.saving.set(true);
40239
40910
  const fieldSchema = [];
40240
40911
  rows.forEach((row, index) => {
40241
40912
  const name = this.formBuilder.uniqueFieldName(row.name || row.label, fieldSchema);
@@ -40249,8 +40920,19 @@ class SurveyBuilderComponent {
40249
40920
  options: row.options,
40250
40921
  }, index));
40251
40922
  });
40252
- const typeId = this.props.typeId ?? `${this.props.typePrefix ?? DEFAULT_TYPE_PREFIX}:${this.formBuilder.toToken(title)}`;
40253
40923
  const subtitle = this.subtitleControl.value.trim();
40924
+ // La app guarda: acá solo se valida y se entrega lo escrito.
40925
+ if (this.props.persist === false) {
40926
+ this.draft.emit({ label: title, subtitle, questions: fieldSchema });
40927
+ return;
40928
+ }
40929
+ this.saving.set(true);
40930
+ // `typeId` derivado del título: sirve para un tipo de solicitud de
40931
+ // plataforma, donde el título ES la identidad. Para una encuesta atada a
40932
+ // una entidad NO alcanza — dos eventos con el mismo título colisionarían en
40933
+ // el mismo tipo y compartirían respuestas. Por eso una vertical usa su
40934
+ // endpoint (`persist: false`) y arma el id con la entidad adentro.
40935
+ const typeId = this.props.typeId ?? `${this.props.typePrefix ?? DEFAULT_TYPE_PREFIX}:${this.formBuilder.toToken(title)}`;
40254
40936
  const payload = {
40255
40937
  label: title,
40256
40938
  subtitle,
@@ -40291,7 +40973,7 @@ class SurveyBuilderComponent {
40291
40973
  };
40292
40974
  }
40293
40975
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SurveyBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
40294
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SurveyBuilderComponent, isStandalone: true, selector: "val-survey-builder", inputs: { props: "props" }, outputs: { saved: "saved" }, ngImport: i0, template: `
40976
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SurveyBuilderComponent, isStandalone: true, selector: "val-survey-builder", inputs: { props: "props" }, outputs: { saved: "saved", draft: "draft" }, ngImport: i0, template: `
40295
40977
  <div class="survey-builder">
40296
40978
  <val-form-field [label]="t('titleLabel')">
40297
40979
  <val-text-input [props]="{ control: titleControl, placeholder: t('titlePlaceholder') }" />
@@ -40523,6 +41205,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40523
41205
  type: Input
40524
41206
  }], saved: [{
40525
41207
  type: Output
41208
+ }], draft: [{
41209
+ type: Output
40526
41210
  }] } });
40527
41211
 
40528
41212
  /**
@@ -44999,521 +45683,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
44999
45683
  type: Output
45000
45684
  }] } });
45001
45685
 
45002
- const STATUS_STYLE = {
45003
- approved: { bg: '#22c55e', color: '#fff' },
45004
- rejected: { bg: '#ef4444', color: '#fff' },
45005
- in_review: { bg: '#fef3c7', color: '#92400e' },
45006
- pending: { bg: 'var(--ion-color-light, #f4f5f8)', color: 'var(--ion-color-dark, #222)' },
45007
- cancelled: { bg: 'var(--ion-color-light, #f4f5f8)', color: 'var(--ion-color-medium, #636469)' },
45008
- closed: { bg: 'var(--ion-color-light, #f4f5f8)', color: 'var(--ion-color-medium, #636469)' },
45009
- };
45010
- /**
45011
- * Panel de revisión de una Request genérica (ADR-061/062): status + acciones
45012
- * approve/reject + fields del formulario + comentarios. Config-driven, sin
45013
- * conocimiento del dominio (adoption, recipe_verification, etc.) — cada
45014
- * consumer inyecta su `RequestReviewPanelConfig`.
45015
- */
45016
- class RequestReviewPanelComponent {
45017
- constructor() {
45018
- this.requests = inject(RequestService);
45019
- this.i18n = inject(I18nService);
45020
- this.errors = inject(ValtechErrorService);
45021
- this.toast = inject(ToastService);
45022
- this.destroyRef = inject(DestroyRef);
45023
- this.requestId = input.required();
45024
- this.config = input.required();
45025
- this.loaded = output();
45026
- this._req = signal(null);
45027
- this._loading = signal(true);
45028
- this._loadError = signal(null);
45029
- this._comments = signal([]);
45030
- this._loadingComments = signal(false);
45031
- this._transitioning = signal(false);
45032
- this.req = this._req.asReadonly();
45033
- this.loading = this._loading.asReadonly();
45034
- this.loadError = this._loadError.asReadonly();
45035
- this.comments = this._comments.asReadonly();
45036
- this.loadingComments = this._loadingComments.asReadonly();
45037
- this.transitioning = this._transitioning.asReadonly();
45038
- this.heroImgFailed = signal(false);
45039
- this.heroConfig = computed(() => this.config().entityHero);
45040
- this.statusStyle = computed(() => STATUS_STYLE[this._req()?.status ?? ''] ?? STATUS_STYLE['pending']);
45041
- this.heroName = computed(() => {
45042
- const hero = this.heroConfig();
45043
- if (!hero)
45044
- return undefined;
45045
- return this._req()?.metadata?.[hero.nameField];
45046
- });
45047
- this.heroImage = computed(() => {
45048
- const hero = this.heroConfig();
45049
- if (!hero?.imageField)
45050
- return undefined;
45051
- return this._req()?.metadata?.[hero.imageField];
45052
- });
45053
- this.canNavigateHero = computed(() => {
45054
- const hero = this.heroConfig();
45055
- const metadata = this._req()?.metadata;
45056
- if (!hero || !metadata)
45057
- return false;
45058
- return hero.canNavigate(metadata);
45059
- });
45060
- this.submitterInitial = computed(() => {
45061
- const s = this._req()?.submitter;
45062
- if (!s)
45063
- return '?';
45064
- return ((s.name || s.email) ?? '?').charAt(0).toUpperCase();
45065
- });
45066
- this.fieldEntries = computed(() => {
45067
- const fields = this._req()?.fields ?? {};
45068
- return Object.entries(fields)
45069
- .filter(([, v]) => v != null && v !== '')
45070
- .map(([k, v]) => ({ key: k, value: String(v) }));
45071
- });
45072
- this.errorState = computed(() => {
45073
- this.i18n.lang();
45074
- const err = this._loadError();
45075
- const msg = (err instanceof Error ? err.message : String(err ?? '')).toLowerCase();
45076
- const isOffline = msg.includes('network') || msg.includes('offline');
45077
- return {
45078
- variant: 'error',
45079
- title: isOffline ? this.t('offlineTitle') : this.t('errorTitle'),
45080
- description: isOffline ? this.t('offlineHint') : '',
45081
- cta: { label: this.t('retry'), handler: () => void this.load() },
45082
- };
45083
- });
45084
- }
45085
- ngOnInit() {
45086
- void this.load();
45087
- }
45088
- async load() {
45089
- const id = this.requestId();
45090
- this._loading.set(true);
45091
- this._loadError.set(null);
45092
- try {
45093
- const req = await firstValueFrom(this.requests.getRequest(id));
45094
- this._req.set(req);
45095
- this.loaded.emit(req);
45096
- this.subscribeComments(req.appId, req.orgId, id);
45097
- }
45098
- catch (err) {
45099
- this._loadError.set(err);
45100
- this.errors.handle(err, {
45101
- context: 'request-review-panel.load',
45102
- fallbackKey: 'errorTitle',
45103
- i18nNamespace: this.config().i18nNamespace,
45104
- });
45105
- }
45106
- finally {
45107
- this._loading.set(false);
45108
- }
45109
- }
45110
- subscribeComments(appId, orgId, requestId) {
45111
- this._loadingComments.set(true);
45112
- this.requests
45113
- .watchComments(appId, orgId, requestId)
45114
- .pipe(takeUntilDestroyed(this.destroyRef))
45115
- .subscribe({
45116
- next: comments => {
45117
- this._comments.set(comments);
45118
- this._loadingComments.set(false);
45119
- },
45120
- error: () => {
45121
- // comentarios son best-effort; si el listener falla, silenciar y ocultar spinner
45122
- this._loadingComments.set(false);
45123
- },
45124
- });
45125
- }
45126
- async transition(status) {
45127
- const req = this._req();
45128
- if (!req || this._transitioning())
45129
- return;
45130
- this._transitioning.set(true);
45131
- try {
45132
- await firstValueFrom(this.requests.transition(req.id, { status }));
45133
- await this.load();
45134
- this.toast.show({ message: this.t('statusUpdated'), duration: 2500, color: 'dark', position: 'top' });
45135
- }
45136
- catch (err) {
45137
- this.errors.handle(err, {
45138
- context: 'request-review-panel.transition',
45139
- fallbackKey: 'statusError',
45140
- i18nNamespace: this.config().i18nNamespace,
45141
- });
45142
- }
45143
- finally {
45144
- this._transitioning.set(false);
45145
- }
45146
- }
45147
- onHeroClick() {
45148
- const hero = this.heroConfig();
45149
- const metadata = this._req()?.metadata;
45150
- if (hero && metadata)
45151
- hero.navigate(metadata);
45152
- }
45153
- formatDate(iso) {
45154
- if (!iso)
45155
- return '';
45156
- try {
45157
- return new Date(iso).toLocaleDateString(this.i18n.lang() === 'es' ? 'es-CL' : 'en-US', {
45158
- day: 'numeric',
45159
- month: 'short',
45160
- year: 'numeric',
45161
- });
45162
- }
45163
- catch {
45164
- return iso;
45165
- }
45166
- }
45167
- authorInitial(c) {
45168
- return ((c.author.name || c.author.email) ?? '?').charAt(0).toUpperCase();
45169
- }
45170
- relativeTime(iso) {
45171
- if (!iso)
45172
- return '';
45173
- const diffMs = Date.now() - new Date(iso).getTime();
45174
- if (Number.isNaN(diffMs))
45175
- return iso;
45176
- const lang = this.i18n.lang();
45177
- const mins = Math.floor(diffMs / 60000);
45178
- if (mins < 1)
45179
- return lang === 'es' ? 'hace un momento' : 'just now';
45180
- if (mins < 60)
45181
- return lang === 'es' ? `hace ${mins} min` : `${mins}m ago`;
45182
- const hours = Math.floor(mins / 60);
45183
- if (hours < 24)
45184
- return lang === 'es' ? `hace ${hours} h` : `${hours}h ago`;
45185
- const days = Math.floor(hours / 24);
45186
- if (days < 7)
45187
- return lang === 'es' ? `hace ${days} dia${days > 1 ? 's' : ''}` : `${days}d ago`;
45188
- return this.formatDate(iso);
45189
- }
45190
- t(key) {
45191
- return this.i18n.t(key, this.config().i18nNamespace);
45192
- }
45193
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestReviewPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
45194
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestReviewPanelComponent, isStandalone: true, selector: "val-request-review-panel", inputs: { requestId: { classPropertyName: "requestId", publicName: "requestId", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { loaded: "loaded" }, ngImport: i0, template: `
45195
- <div class="page">
45196
- @if (loading()) {
45197
- <div class="skel" aria-hidden="true">
45198
- <val-skeleton [props]="{ width: '100%', height: '88px', borderRadius: '14px' }" />
45199
- <val-skeleton [props]="{ width: '100%', height: '64px', borderRadius: '12px' }" />
45200
- <val-skeleton [props]="{ width: '100%', height: '80px', borderRadius: '12px' }" />
45201
- <val-skeleton [props]="{ width: '100%', height: '120px', borderRadius: '12px' }" />
45202
- </div>
45203
- } @else if (loadError()) {
45204
- <val-empty-state [props]="errorState()" />
45205
- } @else if (req()) {
45206
- <div class="detail">
45207
- @if (heroConfig() && heroName()) {
45208
- <button class="entity-hero" (click)="onHeroClick()" [disabled]="!canNavigateHero()">
45209
- <div class="entity-hero__media">
45210
- @if (heroImage() && !heroImgFailed()) {
45211
- <img
45212
- [src]="heroImage()"
45213
- [alt]="heroName()!"
45214
- loading="lazy"
45215
- (error)="heroImgFailed.set(true)"
45216
- class="entity-hero__img"
45217
- />
45218
- } @else {
45219
- <span class="entity-hero__placeholder" aria-hidden="true">{{
45220
- heroConfig()!.placeholderIcon || '📄'
45221
- }}</span>
45222
- }
45223
- </div>
45224
- <div class="entity-hero__body">
45225
- <span class="entity-hero__label">{{ t(heroConfig()!.labelKey) }}</span>
45226
- <span class="entity-hero__name">{{ heroName() }}</span>
45227
- </div>
45228
- @if (canNavigateHero()) {
45229
- <span class="entity-hero__chevron" aria-hidden="true">›</span>
45230
- }
45231
- </button>
45232
- }
45233
-
45234
- <div class="status-card" [style.border-left-color]="statusStyle().bg">
45235
- <div class="status-card__top">
45236
- <span class="status-badge" [style.background]="statusStyle().bg" [style.color]="statusStyle().color">
45237
- {{ t('status_' + req()!.status) }}
45238
- </span>
45239
- <span class="submitted-at">{{ t('submittedAt') }}: {{ formatDate(req()!.createdAt) }}</span>
45240
- </div>
45241
-
45242
- @if (config().canReview() && (req()!.status === 'pending' || req()!.status === 'in_review')) {
45243
- <div class="actions-row">
45244
- <val-button
45245
- [props]="{
45246
- token: 'approve',
45247
- text: t('approve'),
45248
- color: 'primary',
45249
- fill: 'solid',
45250
- shape: 'round',
45251
- size: 'small',
45252
- type: 'button',
45253
- state: transitioning() ? 'WORKING' : 'ENABLED',
45254
- handler: transition.bind(this, 'approved'),
45255
- }"
45256
- />
45257
- <val-button
45258
- [props]="{
45259
- token: 'reject',
45260
- text: t('reject'),
45261
- color: 'medium',
45262
- fill: 'outline',
45263
- shape: 'round',
45264
- size: 'small',
45265
- type: 'button',
45266
- state: transitioning() ? 'WORKING' : 'ENABLED',
45267
- handler: transition.bind(this, 'rejected'),
45268
- }"
45269
- />
45270
- </div>
45271
- }
45272
- </div>
45273
-
45274
- @if (req()!.submitter) {
45275
- <div class="section">
45276
- <p class="section__title">{{ t('submitter') }}</p>
45277
- <div class="submitter-row">
45278
- <div class="submitter-avatar" aria-hidden="true">{{ submitterInitial() }}</div>
45279
- <div class="submitter-info">
45280
- @if (req()!.submitter!.name) {
45281
- <span class="submitter-name">{{ req()!.submitter!.name }}</span>
45282
- }
45283
- @if (req()!.submitter!.email) {
45284
- <span class="submitter-email">{{ req()!.submitter!.email }}</span>
45285
- }
45286
- @if (req()!.submitter!.phone) {
45287
- <span class="submitter-email">{{ req()!.submitter!.phone }}</span>
45288
- }
45289
- </div>
45290
- </div>
45291
- </div>
45292
- }
45293
-
45294
- @if (fieldEntries().length) {
45295
- <div class="section">
45296
- <p class="section__title">{{ t('fields') }}</p>
45297
- @for (entry of fieldEntries(); track entry.key) {
45298
- <div class="field-row">
45299
- <span class="field-row__label">{{ entry.key }}</span>
45300
- <span class="field-row__value">{{ entry.value }}</span>
45301
- </div>
45302
- }
45303
- </div>
45304
- }
45305
-
45306
- @if (
45307
- !config().canReview() && req()!.status === 'approved' && canNavigateHero() && config().viewEntityCtaLabelKey
45308
- ) {
45309
- <val-button
45310
- [props]="{
45311
- token: 'view-entity',
45312
- text: t(config().viewEntityCtaLabelKey!),
45313
- color: 'primary',
45314
- fill: 'solid',
45315
- shape: 'round',
45316
- size: 'default',
45317
- type: 'button',
45318
- state: 'ENABLED',
45319
- handler: onHeroClick.bind(this),
45320
- }"
45321
- />
45322
- }
45323
-
45324
- <div class="section">
45325
- <p class="section__title">{{ t('commentsTitle') }}</p>
45326
- @if (loadingComments()) {
45327
- <div class="section-pad">
45328
- <val-skeleton [props]="{ width: '100%', height: '48px', borderRadius: '8px' }" />
45329
- </div>
45330
- } @else if (comments().length === 0) {
45331
- <p class="no-comments">{{ t('noComments') }}</p>
45332
- } @else {
45333
- <ul class="comments">
45334
- @for (c of comments(); track c.commentId) {
45335
- <li class="comment">
45336
- <div class="comment__avatar" aria-hidden="true">{{ authorInitial(c) }}</div>
45337
- <div class="comment__content">
45338
- <div class="comment__header">
45339
- <span class="comment__author">{{ c.author.name || c.author.email }}</span>
45340
- <span class="comment__date">{{ relativeTime(c.createdAt) }}</span>
45341
- </div>
45342
- <p class="comment__body">{{ c.body }}</p>
45343
- </div>
45344
- </li>
45345
- }
45346
- </ul>
45347
- }
45348
- </div>
45349
- </div>
45350
- }
45351
- </div>
45352
- `, isInline: true, styles: [".page{display:flex;flex-direction:column;gap:14px}.skel{display:flex;flex-direction:column;gap:12px}.skel val-skeleton{display:block}.detail{display:flex;flex-direction:column;gap:14px}.entity-hero{display:flex;align-items:center;gap:14px;padding:14px 16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border-radius:14px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));width:100%;text-align:left;cursor:pointer}.entity-hero:disabled{cursor:default}.entity-hero__media{width:56px;height:56px;border-radius:12px;overflow:hidden;flex-shrink:0;background:var(--ion-color-light, #f4f5f8);display:flex;align-items:center;justify-content:center}.entity-hero__img{width:100%;height:100%;object-fit:cover}.entity-hero__placeholder{font-size:1.625rem}.entity-hero__body{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}.entity-hero__label{font-size:.75rem;color:var(--ion-color-dark, #636469)}.entity-hero__name{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.entity-hero__chevron{font-size:1.5rem;color:var(--ion-color-dark, #92949c);line-height:1}.status-card{background:var(--ion-card-background, var(--ion-background-color, #fff));border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-left:4px solid;padding:14px 16px;display:flex;flex-direction:column;gap:12px}.status-card__top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.status-badge{display:inline-block;padding:4px 12px;border-radius:100px;font-size:.8125rem;font-weight:700}.submitted-at{font-size:.8125rem;color:var(--ion-color-dark-shade, #1e2023)}.actions-row{display:flex;gap:10px;flex-wrap:wrap}.section{background:var(--ion-card-background, var(--ion-background-color, #fff));border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));overflow:hidden}.section__title{margin:0;font-size:.8125rem;font-weight:600;color:var(--ion-color-dark-shade, #1e2023);padding:12px 16px 0}.section-pad{padding:12px 16px}.submitter-row{display:flex;align-items:center;gap:12px;padding:12px 16px}.submitter-avatar{width:40px;height:40px;border-radius:50%;background:var(--ion-color-primary, #313131);color:var(--ion-color-primary-contrast, #fff);display:flex;align-items:center;justify-content:center;font-size:1rem;font-weight:700;flex-shrink:0}.submitter-info{display:flex;flex-direction:column;gap:2px;min-width:0}.submitter-name{font-size:.9375rem;font-weight:600;color:var(--ion-text-color, #000)}.submitter-email{font-size:.8125rem;color:var(--ion-color-dark, #636469);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.field-row{display:flex;flex-direction:column;gap:3px;padding:11px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .06))}.field-row:last-child{border-bottom:none}.field-row__label{font-size:.75rem;color:var(--ion-color-dark, #636469);text-transform:capitalize}.field-row__value{font-size:.9375rem;color:var(--ion-text-color, #000);font-weight:500;word-break:break-word;white-space:pre-line}.no-comments{margin:0;padding:12px 16px;font-size:.875rem;color:var(--ion-color-dark, #92949c)}.comments{list-style:none;margin:0;padding:0}.comment{display:flex;align-items:flex-start;gap:10px;padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .06))}.comment:last-child{border-bottom:none}.comment__avatar{width:32px;height:32px;border-radius:50%;background:var(--ion-color-primary, #313131);color:var(--ion-color-primary-contrast, #fff);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0}.comment__content{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.comment__header{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.comment__author{font-size:.875rem;font-weight:600;color:var(--ion-text-color, #000)}.comment__date{font-size:.75rem;color:var(--ion-color-dark, #92949c);white-space:nowrap}.comment__body{margin:0;font-size:.875rem;color:var(--ion-text-color, #000);line-height:1.5;white-space:pre-line}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
45353
- }
45354
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestReviewPanelComponent, decorators: [{
45355
- type: Component,
45356
- args: [{ selector: 'val-request-review-panel', standalone: true, imports: [ButtonComponent, EmptyStateComponent, SkeletonComponent], template: `
45357
- <div class="page">
45358
- @if (loading()) {
45359
- <div class="skel" aria-hidden="true">
45360
- <val-skeleton [props]="{ width: '100%', height: '88px', borderRadius: '14px' }" />
45361
- <val-skeleton [props]="{ width: '100%', height: '64px', borderRadius: '12px' }" />
45362
- <val-skeleton [props]="{ width: '100%', height: '80px', borderRadius: '12px' }" />
45363
- <val-skeleton [props]="{ width: '100%', height: '120px', borderRadius: '12px' }" />
45364
- </div>
45365
- } @else if (loadError()) {
45366
- <val-empty-state [props]="errorState()" />
45367
- } @else if (req()) {
45368
- <div class="detail">
45369
- @if (heroConfig() && heroName()) {
45370
- <button class="entity-hero" (click)="onHeroClick()" [disabled]="!canNavigateHero()">
45371
- <div class="entity-hero__media">
45372
- @if (heroImage() && !heroImgFailed()) {
45373
- <img
45374
- [src]="heroImage()"
45375
- [alt]="heroName()!"
45376
- loading="lazy"
45377
- (error)="heroImgFailed.set(true)"
45378
- class="entity-hero__img"
45379
- />
45380
- } @else {
45381
- <span class="entity-hero__placeholder" aria-hidden="true">{{
45382
- heroConfig()!.placeholderIcon || '📄'
45383
- }}</span>
45384
- }
45385
- </div>
45386
- <div class="entity-hero__body">
45387
- <span class="entity-hero__label">{{ t(heroConfig()!.labelKey) }}</span>
45388
- <span class="entity-hero__name">{{ heroName() }}</span>
45389
- </div>
45390
- @if (canNavigateHero()) {
45391
- <span class="entity-hero__chevron" aria-hidden="true">›</span>
45392
- }
45393
- </button>
45394
- }
45395
-
45396
- <div class="status-card" [style.border-left-color]="statusStyle().bg">
45397
- <div class="status-card__top">
45398
- <span class="status-badge" [style.background]="statusStyle().bg" [style.color]="statusStyle().color">
45399
- {{ t('status_' + req()!.status) }}
45400
- </span>
45401
- <span class="submitted-at">{{ t('submittedAt') }}: {{ formatDate(req()!.createdAt) }}</span>
45402
- </div>
45403
-
45404
- @if (config().canReview() && (req()!.status === 'pending' || req()!.status === 'in_review')) {
45405
- <div class="actions-row">
45406
- <val-button
45407
- [props]="{
45408
- token: 'approve',
45409
- text: t('approve'),
45410
- color: 'primary',
45411
- fill: 'solid',
45412
- shape: 'round',
45413
- size: 'small',
45414
- type: 'button',
45415
- state: transitioning() ? 'WORKING' : 'ENABLED',
45416
- handler: transition.bind(this, 'approved'),
45417
- }"
45418
- />
45419
- <val-button
45420
- [props]="{
45421
- token: 'reject',
45422
- text: t('reject'),
45423
- color: 'medium',
45424
- fill: 'outline',
45425
- shape: 'round',
45426
- size: 'small',
45427
- type: 'button',
45428
- state: transitioning() ? 'WORKING' : 'ENABLED',
45429
- handler: transition.bind(this, 'rejected'),
45430
- }"
45431
- />
45432
- </div>
45433
- }
45434
- </div>
45435
-
45436
- @if (req()!.submitter) {
45437
- <div class="section">
45438
- <p class="section__title">{{ t('submitter') }}</p>
45439
- <div class="submitter-row">
45440
- <div class="submitter-avatar" aria-hidden="true">{{ submitterInitial() }}</div>
45441
- <div class="submitter-info">
45442
- @if (req()!.submitter!.name) {
45443
- <span class="submitter-name">{{ req()!.submitter!.name }}</span>
45444
- }
45445
- @if (req()!.submitter!.email) {
45446
- <span class="submitter-email">{{ req()!.submitter!.email }}</span>
45447
- }
45448
- @if (req()!.submitter!.phone) {
45449
- <span class="submitter-email">{{ req()!.submitter!.phone }}</span>
45450
- }
45451
- </div>
45452
- </div>
45453
- </div>
45454
- }
45455
-
45456
- @if (fieldEntries().length) {
45457
- <div class="section">
45458
- <p class="section__title">{{ t('fields') }}</p>
45459
- @for (entry of fieldEntries(); track entry.key) {
45460
- <div class="field-row">
45461
- <span class="field-row__label">{{ entry.key }}</span>
45462
- <span class="field-row__value">{{ entry.value }}</span>
45463
- </div>
45464
- }
45465
- </div>
45466
- }
45467
-
45468
- @if (
45469
- !config().canReview() && req()!.status === 'approved' && canNavigateHero() && config().viewEntityCtaLabelKey
45470
- ) {
45471
- <val-button
45472
- [props]="{
45473
- token: 'view-entity',
45474
- text: t(config().viewEntityCtaLabelKey!),
45475
- color: 'primary',
45476
- fill: 'solid',
45477
- shape: 'round',
45478
- size: 'default',
45479
- type: 'button',
45480
- state: 'ENABLED',
45481
- handler: onHeroClick.bind(this),
45482
- }"
45483
- />
45484
- }
45485
-
45486
- <div class="section">
45487
- <p class="section__title">{{ t('commentsTitle') }}</p>
45488
- @if (loadingComments()) {
45489
- <div class="section-pad">
45490
- <val-skeleton [props]="{ width: '100%', height: '48px', borderRadius: '8px' }" />
45491
- </div>
45492
- } @else if (comments().length === 0) {
45493
- <p class="no-comments">{{ t('noComments') }}</p>
45494
- } @else {
45495
- <ul class="comments">
45496
- @for (c of comments(); track c.commentId) {
45497
- <li class="comment">
45498
- <div class="comment__avatar" aria-hidden="true">{{ authorInitial(c) }}</div>
45499
- <div class="comment__content">
45500
- <div class="comment__header">
45501
- <span class="comment__author">{{ c.author.name || c.author.email }}</span>
45502
- <span class="comment__date">{{ relativeTime(c.createdAt) }}</span>
45503
- </div>
45504
- <p class="comment__body">{{ c.body }}</p>
45505
- </div>
45506
- </li>
45507
- }
45508
- </ul>
45509
- }
45510
- </div>
45511
- </div>
45512
- }
45513
- </div>
45514
- `, styles: [".page{display:flex;flex-direction:column;gap:14px}.skel{display:flex;flex-direction:column;gap:12px}.skel val-skeleton{display:block}.detail{display:flex;flex-direction:column;gap:14px}.entity-hero{display:flex;align-items:center;gap:14px;padding:14px 16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border-radius:14px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));width:100%;text-align:left;cursor:pointer}.entity-hero:disabled{cursor:default}.entity-hero__media{width:56px;height:56px;border-radius:12px;overflow:hidden;flex-shrink:0;background:var(--ion-color-light, #f4f5f8);display:flex;align-items:center;justify-content:center}.entity-hero__img{width:100%;height:100%;object-fit:cover}.entity-hero__placeholder{font-size:1.625rem}.entity-hero__body{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}.entity-hero__label{font-size:.75rem;color:var(--ion-color-dark, #636469)}.entity-hero__name{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.entity-hero__chevron{font-size:1.5rem;color:var(--ion-color-dark, #92949c);line-height:1}.status-card{background:var(--ion-card-background, var(--ion-background-color, #fff));border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-left:4px solid;padding:14px 16px;display:flex;flex-direction:column;gap:12px}.status-card__top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.status-badge{display:inline-block;padding:4px 12px;border-radius:100px;font-size:.8125rem;font-weight:700}.submitted-at{font-size:.8125rem;color:var(--ion-color-dark-shade, #1e2023)}.actions-row{display:flex;gap:10px;flex-wrap:wrap}.section{background:var(--ion-card-background, var(--ion-background-color, #fff));border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));overflow:hidden}.section__title{margin:0;font-size:.8125rem;font-weight:600;color:var(--ion-color-dark-shade, #1e2023);padding:12px 16px 0}.section-pad{padding:12px 16px}.submitter-row{display:flex;align-items:center;gap:12px;padding:12px 16px}.submitter-avatar{width:40px;height:40px;border-radius:50%;background:var(--ion-color-primary, #313131);color:var(--ion-color-primary-contrast, #fff);display:flex;align-items:center;justify-content:center;font-size:1rem;font-weight:700;flex-shrink:0}.submitter-info{display:flex;flex-direction:column;gap:2px;min-width:0}.submitter-name{font-size:.9375rem;font-weight:600;color:var(--ion-text-color, #000)}.submitter-email{font-size:.8125rem;color:var(--ion-color-dark, #636469);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.field-row{display:flex;flex-direction:column;gap:3px;padding:11px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .06))}.field-row:last-child{border-bottom:none}.field-row__label{font-size:.75rem;color:var(--ion-color-dark, #636469);text-transform:capitalize}.field-row__value{font-size:.9375rem;color:var(--ion-text-color, #000);font-weight:500;word-break:break-word;white-space:pre-line}.no-comments{margin:0;padding:12px 16px;font-size:.875rem;color:var(--ion-color-dark, #92949c)}.comments{list-style:none;margin:0;padding:0}.comment{display:flex;align-items:flex-start;gap:10px;padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .06))}.comment:last-child{border-bottom:none}.comment__avatar{width:32px;height:32px;border-radius:50%;background:var(--ion-color-primary, #313131);color:var(--ion-color-primary-contrast, #fff);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0}.comment__content{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.comment__header{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.comment__author{font-size:.875rem;font-weight:600;color:var(--ion-text-color, #000)}.comment__date{font-size:.75rem;color:var(--ion-color-dark, #92949c);white-space:nowrap}.comment__body{margin:0;font-size:.875rem;color:var(--ion-text-color, #000);line-height:1.5;white-space:pre-line}\n"] }]
45515
- }], ctorParameters: () => [] });
45516
-
45517
45686
  /**
45518
45687
  * `val-cookie-banner` — bottom/top fixed banner asking the user to choose
45519
45688
  * a cookie consent option. Presentational only: emits events on each
@@ -54835,260 +55004,6 @@ function buildSideNavItemsFromBottomNav(config, options = {}) {
54835
55004
  }));
54836
55005
  }
54837
55006
 
54838
- /**
54839
- * Default values for image processing
54840
- */
54841
- const IMAGE_DEFAULTS = {
54842
- maxWidth: 800,
54843
- maxHeight: 800,
54844
- quality: 0.8,
54845
- mimeType: 'image/jpeg',
54846
- maxSize: 10 * 1024 * 1024, // 10MB
54847
- allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
54848
- thumbnailSize: 150,
54849
- };
54850
-
54851
- /**
54852
- * ImageService
54853
- *
54854
- * Service for image processing including compression, thumbnails, cropping and validation.
54855
- * Uses HTML Canvas for all operations - no external dependencies.
54856
- *
54857
- * @example
54858
- * ```typescript
54859
- * const imageService = inject(ImageService);
54860
- *
54861
- * // Compress an image
54862
- * const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
54863
- *
54864
- * // Generate thumbnail
54865
- * const thumb = await imageService.thumbnail(file, 150);
54866
- *
54867
- * // Validate before processing
54868
- * const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
54869
- * if (!validation.valid) {
54870
- * console.error(validation.message);
54871
- * }
54872
- * ```
54873
- */
54874
- class ImageService {
54875
- /**
54876
- * Compress an image maintaining aspect ratio
54877
- * @param file - File or Blob to compress
54878
- * @param options - Compression options
54879
- * @returns Promise with processed image data
54880
- */
54881
- async compress(file, options) {
54882
- const opts = {
54883
- maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
54884
- maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
54885
- quality: options?.quality ?? IMAGE_DEFAULTS.quality,
54886
- mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
54887
- };
54888
- const img = await this.loadImage(file);
54889
- const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
54890
- const canvas = document.createElement('canvas');
54891
- canvas.width = width;
54892
- canvas.height = height;
54893
- const ctx = canvas.getContext('2d');
54894
- ctx.drawImage(img, 0, 0, width, height);
54895
- const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
54896
- const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
54897
- return {
54898
- blob,
54899
- dataUrl,
54900
- width,
54901
- height,
54902
- size: blob.size,
54903
- };
54904
- }
54905
- /**
54906
- * Generate a square thumbnail from an image
54907
- * @param file - File or Blob to process
54908
- * @param size - Thumbnail size in pixels (default: 150)
54909
- * @returns Promise with processed thumbnail
54910
- */
54911
- async thumbnail(file, size) {
54912
- const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
54913
- const img = await this.loadImage(file);
54914
- // Calculate square crop from center
54915
- const minDim = Math.min(img.width, img.height);
54916
- const cropX = (img.width - minDim) / 2;
54917
- const cropY = (img.height - minDim) / 2;
54918
- const canvas = document.createElement('canvas');
54919
- canvas.width = thumbSize;
54920
- canvas.height = thumbSize;
54921
- const ctx = canvas.getContext('2d');
54922
- ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
54923
- const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
54924
- );
54925
- const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
54926
- return {
54927
- blob,
54928
- dataUrl,
54929
- width: thumbSize,
54930
- height: thumbSize,
54931
- size: blob.size,
54932
- };
54933
- }
54934
- /**
54935
- * Crop an image with specific coordinates
54936
- * @param file - File or Blob to crop
54937
- * @param cropData - Crop coordinates and dimensions
54938
- * @param options - Optional compression options for output
54939
- * @returns Promise with cropped image
54940
- */
54941
- async crop(file, cropData, options) {
54942
- const img = await this.loadImage(file);
54943
- const opts = {
54944
- quality: options?.quality ?? IMAGE_DEFAULTS.quality,
54945
- mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
54946
- };
54947
- const canvas = document.createElement('canvas');
54948
- canvas.width = cropData.width;
54949
- canvas.height = cropData.height;
54950
- const ctx = canvas.getContext('2d');
54951
- ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
54952
- // Apply max dimensions if specified
54953
- if (options?.maxWidth || options?.maxHeight) {
54954
- return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
54955
- }
54956
- const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
54957
- const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
54958
- return {
54959
- blob,
54960
- dataUrl,
54961
- width: cropData.width,
54962
- height: cropData.height,
54963
- size: blob.size,
54964
- };
54965
- }
54966
- /**
54967
- * Validate an image file before processing
54968
- * @param file - File to validate
54969
- * @param options - Validation options
54970
- * @returns Validation result with error details if invalid
54971
- */
54972
- validate(file, options) {
54973
- const opts = {
54974
- maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
54975
- allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
54976
- };
54977
- // Check file type
54978
- if (!opts.allowedTypes.includes(file.type)) {
54979
- return {
54980
- valid: false,
54981
- error: 'invalidType',
54982
- message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
54983
- };
54984
- }
54985
- // Check file size
54986
- if (file.size > opts.maxSize) {
54987
- const maxMB = Math.round(opts.maxSize / (1024 * 1024));
54988
- return {
54989
- valid: false,
54990
- error: 'fileTooLarge',
54991
- message: `La imagen es muy grande. Máximo ${maxMB}MB`,
54992
- };
54993
- }
54994
- return { valid: true };
54995
- }
54996
- /**
54997
- * Validate image dimensions (async - requires loading image)
54998
- * @param file - File to validate
54999
- * @param options - Validation options with minWidth/minHeight
55000
- * @returns Promise with validation result
55001
- */
55002
- async validateDimensions(file, options) {
55003
- const img = await this.loadImage(file);
55004
- if (options.minWidth && img.width < options.minWidth) {
55005
- return {
55006
- valid: false,
55007
- error: 'imageTooSmall',
55008
- message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
55009
- };
55010
- }
55011
- if (options.minHeight && img.height < options.minHeight) {
55012
- return {
55013
- valid: false,
55014
- error: 'imageTooSmall',
55015
- message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
55016
- };
55017
- }
55018
- return { valid: true };
55019
- }
55020
- /**
55021
- * Convert a Blob/File to a data URL
55022
- */
55023
- async toDataUrl(file) {
55024
- return new Promise((resolve, reject) => {
55025
- const reader = new FileReader();
55026
- reader.onload = () => resolve(reader.result);
55027
- reader.onerror = reject;
55028
- reader.readAsDataURL(file);
55029
- });
55030
- }
55031
- /**
55032
- * Convert a data URL to a Blob
55033
- */
55034
- dataUrlToBlob(dataUrl) {
55035
- const arr = dataUrl.split(',');
55036
- const mime = arr[0].match(/:(.*?);/)[1];
55037
- const bstr = atob(arr[1]);
55038
- let n = bstr.length;
55039
- const u8arr = new Uint8Array(n);
55040
- while (n--) {
55041
- u8arr[n] = bstr.charCodeAt(n);
55042
- }
55043
- return new Blob([u8arr], { type: mime });
55044
- }
55045
- // ============== Private Helpers ==============
55046
- loadImage(file) {
55047
- return new Promise((resolve, reject) => {
55048
- const img = new Image();
55049
- img.onload = () => {
55050
- URL.revokeObjectURL(img.src);
55051
- resolve(img);
55052
- };
55053
- img.onerror = reject;
55054
- img.src = URL.createObjectURL(file);
55055
- });
55056
- }
55057
- calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
55058
- let width = originalWidth;
55059
- let height = originalHeight;
55060
- // Scale down if necessary, maintaining aspect ratio
55061
- if (width > maxWidth) {
55062
- height = (height * maxWidth) / width;
55063
- width = maxWidth;
55064
- }
55065
- if (height > maxHeight) {
55066
- width = (width * maxHeight) / height;
55067
- height = maxHeight;
55068
- }
55069
- return {
55070
- width: Math.round(width),
55071
- height: Math.round(height),
55072
- };
55073
- }
55074
- canvasToBlob(canvas, mimeType, quality) {
55075
- return new Promise((resolve, reject) => {
55076
- canvas.toBlob((blob) => {
55077
- if (blob)
55078
- resolve(blob);
55079
- else
55080
- reject(new Error('Failed to create blob from canvas'));
55081
- }, mimeType, quality);
55082
- });
55083
- }
55084
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
55085
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
55086
- }
55087
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
55088
- type: Injectable,
55089
- args: [{ providedIn: 'root' }]
55090
- }] });
55091
-
55092
55007
  /**
55093
55008
  * Default values
55094
55009
  */
@@ -83441,6 +83356,100 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
83441
83356
  * `markdown-article-parser` existente.
83442
83357
  */
83443
83358
 
83359
+ /**
83360
+ * Contratos del onboarding de pasarelas de pago (ADR-028).
83361
+ *
83362
+ * Espejo reducido del backend: solo lo que una app necesita para conectar la
83363
+ * cuenta de una organización y saber si puede cobrar.
83364
+ */
83365
+
83366
+ /** Token de inyección de la configuración de pagos. */
83367
+ const VALTECH_PAYMENTS_CONFIG = new InjectionToken('ValtechPaymentsConfig');
83368
+ const DEFAULT_PAYMENTS_CONFIG = {
83369
+ paymentsPrefix: '/v1/payments',
83370
+ };
83371
+ /**
83372
+ * Provee el cliente de pasarelas de pago.
83373
+ *
83374
+ * @example
83375
+ * ```typescript
83376
+ * providePaymentsGateway({ apiUrl: environment.apiUrl }),
83377
+ * ```
83378
+ */
83379
+ function providePaymentsGateway(config) {
83380
+ return makeEnvironmentProviders([
83381
+ {
83382
+ provide: VALTECH_PAYMENTS_CONFIG,
83383
+ useValue: { ...DEFAULT_PAYMENTS_CONFIG, ...config },
83384
+ },
83385
+ ]);
83386
+ }
83387
+
83388
+ /**
83389
+ * Cliente del onboarding de pasarelas de pago (ADR-028).
83390
+ *
83391
+ * Conecta la cuenta de UNA ORGANIZACIÓN con una pasarela, para que el dinero
83392
+ * le llegue directo. Ese es el punto: si el dinero pasara por una cuenta de
83393
+ * Valtech, Valtech sería un PSP regulado por la CMF (ver ADR-028). Por eso
83394
+ * todas las operaciones son por `orgId`, y no existe acá un modo "cuenta de la
83395
+ * plataforma".
83396
+ *
83397
+ * Vive en la lib y no en una app porque el flujo es idéntico en todas: Bingo
83398
+ * lo usa para que cada colegio cobre sus cartones, y Chesed para que cada
83399
+ * refugio reciba aportes. Lo que cambia entre apps es qué se cobra, no cómo se
83400
+ * conecta la cuenta.
83401
+ *
83402
+ * Autenticación: no agrega el Bearer a mano — lo pone el interceptor de
83403
+ * `provideValtechAuth`. Errores: no se capturan acá; los maneja la pantalla.
83404
+ */
83405
+ class PaymentsGatewayService {
83406
+ constructor() {
83407
+ this.http = inject(HttpClient);
83408
+ this.config = inject(VALTECH_PAYMENTS_CONFIG, { optional: true });
83409
+ }
83410
+ get base() {
83411
+ const apiUrl = this.config?.apiUrl ?? '';
83412
+ const prefix = this.config?.paymentsPrefix ?? '/v1/payments';
83413
+ return `${apiUrl}${prefix}`;
83414
+ }
83415
+ /**
83416
+ * Estado de conexión de la organización con una pasarela.
83417
+ *
83418
+ * Es lo que decide si la app puede ofrecer el cobro: `connected` habilita,
83419
+ * `expired` y `revoked` NO. Mostrar un botón de pago con un token revocado
83420
+ * lleva al usuario a un cobro que falla después de que ya decidió pagar.
83421
+ */
83422
+ getConnectionStatus(orgId, gateway) {
83423
+ return this.http.get(`${this.base}/credentials/orgs/${orgId}/${gateway}/connection-status`);
83424
+ }
83425
+ /**
83426
+ * URL de autorización OAuth, con su `state` CSRF.
83427
+ *
83428
+ * El caller guarda el `state` antes de redirigir y lo compara al volver: sin
83429
+ * eso, cualquiera podría completar la conexión de otra organización.
83430
+ */
83431
+ getOAuthAuthorizeURL(orgId, gateway) {
83432
+ return this.http.get(`${this.base}/credentials/orgs/${orgId}/${gateway}/oauth/authorize-url`);
83433
+ }
83434
+ /** Desconecta la credencial de la organización para esa pasarela. */
83435
+ disconnect(orgId, gateway) {
83436
+ return this.http.delete(`${this.base}/credentials/orgs/${orgId}/${gateway}`);
83437
+ }
83438
+ /**
83439
+ * Guarda una credencial de acceso directo (pasarelas sin OAuth, ej. Khipu).
83440
+ * El token es el secreto del cobrador.
83441
+ */
83442
+ setCredential(orgId, gateway, accessToken) {
83443
+ return this.http.put(`${this.base}/credentials/orgs/${orgId}/${gateway}`, { accessToken });
83444
+ }
83445
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaymentsGatewayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
83446
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaymentsGatewayService, providedIn: 'root' }); }
83447
+ }
83448
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaymentsGatewayService, decorators: [{
83449
+ type: Injectable,
83450
+ args: [{ providedIn: 'root' }]
83451
+ }] });
83452
+
83444
83453
  const VALTECH_WHATSAPP_CONFIG = new InjectionToken('ValtechWhatsappConfig');
83445
83454
  function provideValtechWhatsapp(config) {
83446
83455
  return makeEnvironmentProviders([{ provide: VALTECH_WHATSAPP_CONFIG, useValue: config }]);
@@ -92953,5 +92962,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
92953
92962
  * Generated bundle index. Do not edit.
92954
92963
  */
92955
92964
 
92956
- export { ACCESS_REQUEST_TYPE, ACCESS_REQUEST_VIEW_I18N, ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccessControlService, AccessRequestViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimalCardComponent, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BackButtonComponent, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, BubbleBlitzGameComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CanAccessDirective, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, ChatComposerComponent, ChatInputComponent, ChatWindowComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ChipSelectComponent, ClientTelemetryService, CodeDisplayComponent, CodeValidatorComponent, CollectionsService, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CommsPreferencesService, CommsPreferencesSettingsComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ConfirmationDialogV2Component, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentReactionModalComponent, ContentService, ContentTransformer, ConversationListItemComponent, ConversationService, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CANONICAL_FIELD_ALIASES, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMOJI_RATING_FACES, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DangerSectionComponent, DataTableComponent, DatasetPaginationService, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EXPECTED_NOT_FOUND, EditOrgModalComponent, EmojiRatingComponent, EmptyStateComponent, EntityCardComponent, EntityFeedService, EntradaSerializada, ExpandableTextComponent, ExternalBrowserService, FEATURES_LIST_DEFAULTS, FIELD_TYPES_WITH_OPTIONS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeatureControlService, FeatureGuardDirective, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FieldOptionsEditorComponent, FieldSchemaEditorComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FolderTabsComponent, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSchemaBuilderService, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GAME_AVATAR_CATALOG_SIZE_PER_STYLE, GameAvatarComponent, GameProfileService, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, GroupMembersComponent, GroupPickerComponent, GroupsService, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandleService, HandoffService, HapticsService, HasPermissionDirective, HeaderActionsService, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, KNOWN_ROUTES, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LightRippleDirective, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LiveReadFallbackService, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METADATA_LIST_DEFAULTS, META_SCHEMA_VERSION, METRIC_CARD_DEFAULTS, MINI_GAMES_I18N, MINI_GAME_PLAYER_AVATARS, MINI_GAME_PLAYER_COLORS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MediaViewerModalComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MemoryGameComponent, MenuComponent, MessageBubbleComponent, MessagingService, MetaService, MetadataListComponent, MeteringService, MetricCardComponent, MfaModalComponent, MiniGameCalloutComponent, MiniGamePlayerProfileService, MiniGameScorePopComponent, MiniGamesMenuComponent, ModalLayoutComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NoticeComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberPickerComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OperationReferenceComponent, OptionCardsComponent, OptionSheetComponent, OptionSheetService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PERSONA_CONFIG, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageBlockComponent, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PersonaService, PhoneDisplayComponent, PhoneFormatService, PhoneInputComponent, PickerV2Component, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PricingTableComponent, ProcessLinksPipe, ProfileCardComponent, ProfileContentComponent, ProfileModalComponent, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrBrandValidationError, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RbacService, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestReviewPanelComponent, RequestService, RetroAudioService, RichEditorComponent, RightsFooterComponent, RoleManagerComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SURVEY_QUESTION_TYPES, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SelectSearchPickerModalComponent, SessionListModalComponent, SessionService, SessionTransitionOverlayComponent, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimonGameComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SupportTicketCtaComponent, SurveyBuilderComponent, SurveyResponseComponent, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeSelectorComponent, ThemeService, ThreadPanelComponent, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, TypingIndicatorComponent, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UsageMetersComponent, UsageService, UserAvatarComponent, UsernameInputComponent, VALTECH_ACCESS_FEATURES, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_CHAT_CONFIG, VALTECH_COLLECTIONS_CONFIG, VALTECH_COMMS_PREFERENCES_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_COPYRIGHT_TEMPLATE, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DIAGRAMS, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_LEGAL_ENTITY, VALTECH_MENU_I18N, VALTECH_NETWORK_ERROR_KEY, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VAL_REGISTERED_ICONS, VERSION, ValCommentThreadComponent, ValQuotaWarningComponent, ValtechErrorService, VerifyViewComponent, VideoPlayerComponent, VideoUploadService, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, WorkflowService, accessGuard, accessGuardFromRoute, applyDefaultValueToControl, articleToTiptapDoc, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildPlatformMenu, buildSettingsCards, buildSideNavItemsFromBottomNav, button, canSubmitRequestType, classifyChip, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGameAvatarCatalog, createGameAvatarProps, createGlowCardProps, createInitialDatasetState, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, datasetPageFromLegacyCursor, defaultQrBrand, docs, errorLoggingInterceptor, evaluateValtechAccess, extractPathParams, firmaDeSesionDeCustomToken, formatClockTime, formatDateSeparator, formatRelativeTime, gameAvatarCatalogEntryToMetadata, gameAvatarDataUri, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isKnownRoute, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, providePersona, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccessFeatures, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechChat, provideValtechCollections, provideValtechCommsPreferences, provideValtechContent, provideValtechDebugConsole, provideValtechDiagrams, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationClickActions, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, qrContrastRatio, qrErrorCorrectionFor, query, rbacGuard, renderGameAvatarSvg, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveCopyrightTemplate, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, roleOf, selectableRequestTypes, storagePaths, suggestEmailFix, superAdminGuard, supportedGameAvatarStyles, tiptapDocToArticle, tiptapDocToArticleElements, toArticle, validateQrBrand, validateRoutes };
92965
+ export { ACCESS_REQUEST_TYPE, ACCESS_REQUEST_VIEW_I18N, ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccessControlService, AccessRequestViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimalCardComponent, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BackButtonComponent, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, BubbleBlitzGameComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CanAccessDirective, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, ChatComposerComponent, ChatInputComponent, ChatWindowComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ChipSelectComponent, ClientTelemetryService, CodeDisplayComponent, CodeValidatorComponent, CollectionsService, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CommsPreferencesService, CommsPreferencesSettingsComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ConfirmationDialogV2Component, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentReactionModalComponent, ContentService, ContentTransformer, ConversationListItemComponent, ConversationService, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CANONICAL_FIELD_ALIASES, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMOJI_RATING_FACES, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENTS_CONFIG, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DangerSectionComponent, DataTableComponent, DatasetPaginationService, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EXPECTED_NOT_FOUND, EditOrgModalComponent, EmojiRatingComponent, EmptyStateComponent, EntityCardComponent, EntityFeedService, EntradaSerializada, ExpandableTextComponent, ExternalBrowserService, FEATURES_LIST_DEFAULTS, FIELD_TYPES_WITH_OPTIONS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeatureControlService, FeatureGuardDirective, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FieldOptionsEditorComponent, FieldSchemaEditorComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FolderTabsComponent, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSchemaBuilderService, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GAME_AVATAR_CATALOG_SIZE_PER_STYLE, GameAvatarComponent, GameProfileService, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, GroupMembersComponent, GroupPickerComponent, GroupsService, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandleService, HandoffService, HapticsService, HasPermissionDirective, HeaderActionsService, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, KNOWN_ROUTES, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LightRippleDirective, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LiveReadFallbackService, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METADATA_LIST_DEFAULTS, META_SCHEMA_VERSION, METRIC_CARD_DEFAULTS, MINI_GAMES_I18N, MINI_GAME_PLAYER_AVATARS, MINI_GAME_PLAYER_COLORS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MediaViewerModalComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MemoryGameComponent, MenuComponent, MessageBubbleComponent, MessagingService, MetaService, MetadataListComponent, MeteringService, MetricCardComponent, MfaModalComponent, MiniGameCalloutComponent, MiniGamePlayerProfileService, MiniGameScorePopComponent, MiniGamesMenuComponent, ModalLayoutComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NoticeComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberPickerComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OperationReferenceComponent, OptionCardsComponent, OptionSheetComponent, OptionSheetService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PERSONA_CONFIG, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageBlockComponent, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PaymentsGatewayService, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PersonaService, PhoneDisplayComponent, PhoneFormatService, PhoneInputComponent, PickerV2Component, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PricingTableComponent, ProcessLinksPipe, ProfileCardComponent, ProfileContentComponent, ProfileModalComponent, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrBrandValidationError, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RbacService, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestReviewPanelComponent, RequestService, RetroAudioService, RichEditorComponent, RightsFooterComponent, RoleManagerComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SURVEY_QUESTION_TYPES, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SelectSearchPickerModalComponent, SessionListModalComponent, SessionService, SessionTransitionOverlayComponent, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimonGameComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SupportTicketCtaComponent, SurveyBuilderComponent, SurveyResponseComponent, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeSelectorComponent, ThemeService, ThreadPanelComponent, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, TypingIndicatorComponent, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UsageMetersComponent, UsageService, UserAvatarComponent, UsernameInputComponent, VALTECH_ACCESS_FEATURES, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_CHAT_CONFIG, VALTECH_COLLECTIONS_CONFIG, VALTECH_COMMS_PREFERENCES_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_COPYRIGHT_TEMPLATE, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DIAGRAMS, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_LEGAL_ENTITY, VALTECH_MENU_I18N, VALTECH_NETWORK_ERROR_KEY, VALTECH_PAYMENTS_CONFIG, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VAL_REGISTERED_ICONS, VERSION, ValCommentThreadComponent, ValQuotaWarningComponent, ValtechErrorService, VerifyViewComponent, VideoPlayerComponent, VideoUploadService, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, WorkflowService, accessGuard, accessGuardFromRoute, applyDefaultValueToControl, articleToTiptapDoc, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildPlatformMenu, buildSettingsCards, buildSideNavItemsFromBottomNav, button, canSubmitRequestType, classifyChip, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGameAvatarCatalog, createGameAvatarProps, createGlowCardProps, createInitialDatasetState, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, datasetPageFromLegacyCursor, defaultQrBrand, docs, errorLoggingInterceptor, evaluateValtechAccess, extractPathParams, firmaDeSesionDeCustomToken, formatClockTime, formatDateSeparator, formatRelativeTime, gameAvatarCatalogEntryToMetadata, gameAvatarDataUri, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isKnownRoute, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, providePaymentsGateway, providePersona, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccessFeatures, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechChat, provideValtechCollections, provideValtechCommsPreferences, provideValtechContent, provideValtechDebugConsole, provideValtechDiagrams, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationClickActions, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, qrContrastRatio, qrErrorCorrectionFor, query, rbacGuard, renderGameAvatarSvg, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveCopyrightTemplate, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, roleOf, selectableRequestTypes, storagePaths, suggestEmailFix, superAdminGuard, supportedGameAvatarStyles, tiptapDocToArticle, tiptapDocToArticleElements, toArticle, validateQrBrand, validateRoutes };
92957
92966
  //# sourceMappingURL=valtech-components.mjs.map