valtech-components 4.0.987 → 4.0.989

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 (24) 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-response/survey-response.component.mjs +21 -1
  7. package/esm2022/lib/components/organisms/survey-response/survey-response.i18n.mjs +5 -1
  8. package/esm2022/lib/components/organisms/survey-response/types.mjs +1 -1
  9. package/esm2022/lib/services/requests/types.mjs +1 -1
  10. package/esm2022/lib/version.mjs +2 -2
  11. package/esm2022/public-api.mjs +3 -2
  12. package/fesm2022/valtech-components.mjs +699 -780
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/organisms/attachment-uploader/attachment-uploader.component.d.ts +14 -0
  15. package/lib/components/organisms/attachment-uploader/types.d.ts +30 -0
  16. package/lib/components/organisms/request-review-panel/request-review-panel.component.d.ts +72 -76
  17. package/lib/components/organisms/request-review-panel/request-review-panel.i18n.d.ts +2 -0
  18. package/lib/components/organisms/request-review-panel/types.d.ts +73 -0
  19. package/lib/components/organisms/survey-response/survey-response.component.d.ts +2 -0
  20. package/lib/components/organisms/survey-response/types.d.ts +6 -0
  21. package/lib/services/requests/types.d.ts +6 -0
  22. package/lib/version.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/public-api.d.ts +2 -1
@@ -9,6 +9,7 @@ export declare class AttachmentUploaderComponent implements OnInit {
9
9
  attachmentsChange: import("@angular/core").OutputEmitterRef<AttachmentItem[]>;
10
10
  protected i18n: I18nService;
11
11
  private feedbackService;
12
+ private imageService;
12
13
  attachments: import("@angular/core").WritableSignal<AttachmentItem[]>;
13
14
  showCameraOverlay: import("@angular/core").WritableSignal<boolean>;
14
15
  private cameraStream;
@@ -26,6 +27,19 @@ export declare class AttachmentUploaderComponent implements OnInit {
26
27
  onFilesSelected(event: Event): Promise<void>;
27
28
  private processFiles;
28
29
  private compressImage;
30
+ /**
31
+ * Genera y sube la miniatura de una imagen ya comprimida.
32
+ *
33
+ * Recibe el archivo COMPRIMIDO a propósito: `compressorjs` ya normalizó la
34
+ * orientación EXIF, y `ImageService` trabaja sobre canvas sin leer EXIF — si
35
+ * se le pasara el original, las fotos verticales de teléfono saldrían
36
+ * rotadas. Ver la nota en `generateThumbnail` (types.ts).
37
+ *
38
+ * Nunca es fatal: si falla, el adjunto queda subido sin miniatura y las
39
+ * listas caen al archivo grande. Perder la miniatura no justifica perder la
40
+ * foto que el usuario acaba de subir.
41
+ */
42
+ private uploadThumbnail;
29
43
  private uploadFile;
30
44
  remove(id: string): void;
31
45
  thumbFailed(id: string): void;
@@ -3,6 +3,13 @@ export interface AttachmentItem {
3
3
  file?: File;
4
4
  status: 'uploading' | 'ready' | 'error';
5
5
  url?: string;
6
+ /**
7
+ * URL de la miniatura, presente solo si se pidió `generateThumbnail` y el
8
+ * archivo es una imagen. Es la que deben usar las LISTAS: `url` apunta al
9
+ * archivo de hasta 1920px, y pedirlo para pintar un recuadro de 84px es la
10
+ * diferencia entre 240KB y 15KB por fila. Ver `generateThumbnail`.
11
+ */
12
+ thumbnailUrl?: string;
6
13
  error?: string;
7
14
  isExternal?: boolean;
8
15
  }
@@ -16,6 +23,29 @@ export interface AttachmentUploaderMetadata {
16
23
  disabled?: boolean;
17
24
  /** Compress images before upload (default: true). PDFs are never compressed. */
18
25
  compressImages?: boolean;
26
+ /**
27
+ * Genera y sube además una miniatura cuadrada por cada imagen, y la expone en
28
+ * `AttachmentItem.thumbnailUrl` (default: false).
29
+ *
30
+ * Por qué es opt-in y no el default: sube DOS archivos por imagen en vez de
31
+ * uno, y no todo consumer quiere pagar ese costo (ni tener el archivo extra
32
+ * en su bucket). Actívalo cuando las fotos se muestren en una lista o grilla,
33
+ * que es donde el ahorro es real.
34
+ *
35
+ * La miniatura se genera a partir del archivo YA comprimido, no del original:
36
+ * `compressorjs` normaliza la orientación EXIF al comprimir, así que a esa
37
+ * altura la imagen ya está derecha. Generarla del original la sacaría rotada
38
+ * en las fotos verticales de teléfono (ImageService usa canvas y no lee EXIF).
39
+ */
40
+ generateThumbnail?: boolean;
41
+ /** Lado de la miniatura en píxeles (default: 320). Cuadrada, recorte centrado. */
42
+ thumbnailSize?: number;
43
+ /**
44
+ * Función de subida de la miniatura. Si se omite, se usa `uploadFn` con un
45
+ * File renombrado a `<nombre>-thumb.jpg` — así el consumer que deriva la ruta
46
+ * del nombre del archivo no necesita hacer nada.
47
+ */
48
+ thumbnailUploadFn?: (file: File) => Promise<string>;
19
49
  /** Upload function. If omitted, falls back to FeedbackService.uploadAttachment. */
20
50
  uploadFn?: (file: File) => Promise<string>;
21
51
  /** URLs de items ya subidos — se pre-seedean como items 'ready' al montar. */
@@ -1,83 +1,79 @@
1
- import { OnInit } from '@angular/core';
2
- import { AppRequest, RequestComment } from '../../../services/requests/types';
3
- import { EmptyStateMetadata } from '../../molecules/empty-state/types';
1
+ import { EventEmitter, TemplateRef } from '@angular/core';
2
+ import { ComponentState } from '../../types';
3
+ import { RequestReviewDecision, RequestReviewItem, RequestReviewPanelMetadata } from './types';
4
4
  import * as i0 from "@angular/core";
5
- interface StatusStyle {
6
- bg: string;
7
- color: string;
8
- }
9
- /**
10
- * Config del "entity hero" (bloque opcional arriba del status: ej. animal en
11
- * Chesed, receta en Mealplan). Agnóstico al dominio — cada app declara de qué
12
- * campos de `metadata` sacar nombre/imagen y a dónde navegar.
13
- */
14
- export interface RequestReviewEntityHeroConfig {
15
- labelKey: string;
16
- nameField: string;
17
- imageField?: string;
18
- placeholderIcon?: string;
19
- canNavigate: (metadata: Record<string, any>) => boolean;
20
- navigate: (metadata: Record<string, any>) => void;
21
- }
22
- export interface RequestReviewPanelConfig {
23
- /** Namespace i18n donde viven las keys (status_*, approve, reject, submitter, fields, commentsTitle, noComments, submittedAt, statusUpdated, statusError, errorTitle, offlineTitle, offlineHint, retry, viewAnimal). */
24
- i18nNamespace: string;
25
- entityHero?: RequestReviewEntityHeroConfig;
26
- /** Si el usuario actual puede aprobar/rechazar (ej. persona.isPublisher()). */
27
- canReview: () => boolean;
28
- /** CTA post-aprobación para quien NO revisa (ej. "ver receta"/"ver animal"). */
29
- viewEntityCtaLabelKey?: string;
30
- }
31
5
  /**
32
- * Panel de revisión de una Request genérica (ADR-061/062): status + acciones
33
- * approve/reject + fields del formulario + comentarios. Config-driven, sin
34
- * conocimiento del dominio (adoption, recipe_verification, etc.) — cada
35
- * consumer inyecta su `RequestReviewPanelConfig`.
6
+ * val-request-review-panel cola de revisión del factory (ADR-062).
7
+ *
8
+ * Es **presentacional**: no carga datos, no conoce el backend ni el dominio. El
9
+ * consumer le pasa la cola ya normalizada y escucha `(decision)`.
10
+ *
11
+ * ## Por qué el cuerpo va proyectado y no configurado
12
+ *
13
+ * El ADR original proponía describir el cuerpo con arrays de nombres de campo
14
+ * (`fields.display: ['taxId', 'tagsRequested']`). Al implementarlo con los dos
15
+ * consumidores reales delante quedó claro que eso reinventa un motor de
16
+ * templating peor que Angular: Chesed necesita una lista de documentos con
17
+ * iconos y `target="_blank"`, Okhelia un enlace a la receta y un input de
18
+ * comentario. Ninguna lista de strings expresa eso sin agregarle un flag al
19
+ * componente por cada caso nuevo.
20
+ *
21
+ * Entonces el panel es dueño de lo que **de verdad** se repite — la cáscara, el
22
+ * chrome de la card, y la mecánica de decisión — y el cuerpo de cada ítem lo
23
+ * escribe cada vertical en un `ng-template` con su propio HTML.
24
+ *
25
+ * ```html
26
+ * <val-request-review-panel [props]="panelProps()" [itemBody]="body"
27
+ * (decision)="onDecision($event)" />
28
+ *
29
+ * <ng-template #body let-item>
30
+ * <a [routerLink]="['/app/recetas', recipeIdOf(item)]">Ver receta</a>
31
+ * </ng-template>
32
+ * ```
33
+ *
34
+ * ## Rechazo con motivo
35
+ *
36
+ * El flujo de dos pasos (Rechazar → escribir motivo → Confirmar) es del panel,
37
+ * no del consumer, y está activo por defecto. Rechazar de un click deja al
38
+ * solicitante sin saber qué corregir; era el único de los dos paneles que lo
39
+ * tenía bien resuelto y ahora lo heredan los dos.
36
40
  */
37
- export declare class RequestReviewPanelComponent implements OnInit {
38
- private requests;
41
+ export declare class RequestReviewPanelComponent {
39
42
  private i18n;
40
- private errors;
41
- private toast;
42
- private destroyRef;
43
- readonly requestId: import("@angular/core").InputSignal<string>;
44
- readonly config: import("@angular/core").InputSignal<RequestReviewPanelConfig>;
45
- readonly loaded: import("@angular/core").OutputEmitterRef<AppRequest>;
46
- private readonly _req;
47
- private readonly _loading;
48
- private readonly _loadError;
49
- private readonly _comments;
50
- private readonly _loadingComments;
51
- private readonly _transitioning;
52
- readonly req: import("@angular/core").Signal<AppRequest>;
53
- readonly loading: import("@angular/core").Signal<boolean>;
54
- readonly loadError: import("@angular/core").Signal<unknown>;
55
- readonly comments: import("@angular/core").Signal<RequestComment[]>;
56
- readonly loadingComments: import("@angular/core").Signal<boolean>;
57
- readonly transitioning: import("@angular/core").Signal<boolean>;
58
- readonly heroImgFailed: import("@angular/core").WritableSignal<boolean>;
59
- readonly heroConfig: import("@angular/core").Signal<RequestReviewEntityHeroConfig>;
60
- readonly statusStyle: import("@angular/core").Signal<StatusStyle>;
61
- readonly heroName: import("@angular/core").Signal<string>;
62
- readonly heroImage: import("@angular/core").Signal<string>;
63
- readonly canNavigateHero: import("@angular/core").Signal<boolean>;
64
- readonly submitterInitial: import("@angular/core").Signal<string>;
65
- readonly fieldEntries: import("@angular/core").Signal<{
66
- key: string;
67
- value: string;
68
- }[]>;
69
- readonly errorState: import("@angular/core").Signal<EmptyStateMetadata>;
43
+ readonly props: import("@angular/core").InputSignal<RequestReviewPanelMetadata>;
44
+ /**
45
+ * Cuerpo de cada card. El contexto trae el ítem como `$implicit` y también
46
+ * como `item`, para que el consumer pueda usar `let-item` o `let-x="item"`.
47
+ */
48
+ itemBody?: TemplateRef<{
49
+ $implicit: RequestReviewItem;
50
+ item: RequestReviewItem;
51
+ }>;
52
+ decision: EventEmitter<RequestReviewDecision>;
53
+ /** ID del ítem cuyo formulario de rechazo está abierto. */
54
+ private readonly _rejectingId;
55
+ readonly rejectingId: import("@angular/core").Signal<string>;
56
+ private readonly _reason;
57
+ readonly reason: import("@angular/core").Signal<string>;
58
+ readonly cfg: import("@angular/core").Signal<RequestReviewPanelMetadata>;
59
+ readonly skeletonRows: import("@angular/core").Signal<number[]>;
60
+ /** Hay una decisión en vuelo: toda la cola se congela. */
61
+ readonly isBusy: import("@angular/core").Signal<boolean>;
70
62
  constructor();
71
- ngOnInit(): void;
72
- load(): Promise<void>;
73
- private subscribeComments;
74
- transition(status: 'approved' | 'rejected'): Promise<void>;
75
- onHeroClick(): void;
76
- formatDate(iso: string): string;
77
- authorInitial(c: RequestComment): string;
78
- relativeTime(iso: string): string;
79
- t(key: string): string;
63
+ protected t(key: string): string;
64
+ /** El ítem en vuelo muestra WORKING; los demás quedan deshabilitados. */
65
+ private stateFor;
66
+ protected approveState(item: RequestReviewItem): ComponentState;
67
+ /**
68
+ * Confirmar el rechazo exige motivo cuando `requireRejectReason` está activo
69
+ * (el default). Es el freno que evita la notificación de rechazo vacía.
70
+ */
71
+ protected confirmRejectState(item: RequestReviewItem): ComponentState;
72
+ protected onReasonInput(event: Event): void;
73
+ protected startReject(item: RequestReviewItem): void;
74
+ protected cancelReject(): void;
75
+ protected confirmReject(item: RequestReviewItem): void;
76
+ protected approve(item: RequestReviewItem): void;
80
77
  static ɵfac: i0.ɵɵFactoryDeclaration<RequestReviewPanelComponent, never>;
81
- static ɵcmp: i0.ɵɵComponentDeclaration<RequestReviewPanelComponent, "val-request-review-panel", never, { "requestId": { "alias": "requestId"; "required": true; "isSignal": true; }; "config": { "alias": "config"; "required": true; "isSignal": true; }; }, { "loaded": "loaded"; }, never, never, true, never>;
78
+ static ɵcmp: i0.ɵɵComponentDeclaration<RequestReviewPanelComponent, "val-request-review-panel", never, { "props": { "alias": "props"; "required": true; "isSignal": true; }; "itemBody": { "alias": "itemBody"; "required": false; }; }, { "decision": "decision"; }, never, never, true, never>;
82
79
  }
83
- export {};
@@ -0,0 +1,2 @@
1
+ import { LanguagesContent } from '../../../services/i18n';
2
+ export declare const REQUEST_REVIEW_PANEL_I18N: LanguagesContent;
@@ -0,0 +1,73 @@
1
+ import { EmptyStateMetadata } from '../../molecules/empty-state/types';
2
+ /**
3
+ * Un ítem de la cola de revisión, ya normalizado por el consumer.
4
+ *
5
+ * Deliberadamente pobre: id, título y unas líneas de metadata. Todo lo que sea
6
+ * del dominio (el RUT del criadero, los tags de la receta, los documentos
7
+ * adjuntos) va en el cuerpo proyectado, no acá — la lib no aprende qué es una
8
+ * receta ni un criadero.
9
+ */
10
+ export interface RequestReviewItem {
11
+ /** Identidad del ítem. Es lo que viaja en la decisión. */
12
+ id: string;
13
+ /** Encabezado de la card. */
14
+ title: string;
15
+ /**
16
+ * Líneas bajo el título (fecha de envío, identificador, estado). Texto ya
17
+ * resuelto y formateado por el consumer: la lib no traduce ni formatea fechas.
18
+ */
19
+ meta?: string[];
20
+ }
21
+ /** Lo que emite el panel cuando el revisor decide. */
22
+ export interface RequestReviewDecision {
23
+ itemId: string;
24
+ approved: boolean;
25
+ /**
26
+ * Motivo del rechazo. Presente solo al rechazar con `requireRejectReason`
27
+ * activo (el default), donde el panel garantiza que no viene vacío.
28
+ */
29
+ reason?: string;
30
+ }
31
+ export interface RequestReviewPanelMetadata {
32
+ /** La cola. Vacía + `loading: false` muestra el estado vacío. */
33
+ items: RequestReviewItem[];
34
+ /** Muestra el skeleton en vez de la lista. */
35
+ loading?: boolean;
36
+ /** Filas del skeleton (default: 3). */
37
+ skeletonRows?: number;
38
+ /**
39
+ * Estado de error de la carga. Tiene prioridad sobre todo lo demás — un panel
40
+ * que no pudo cargar no debe mostrar "no hay nada pendiente", que es una
41
+ * afirmación distinta y falsa.
42
+ */
43
+ errorState?: EmptyStateMetadata | null;
44
+ /** Estado cuando la cola está vacía de verdad. */
45
+ emptyState?: EmptyStateMetadata;
46
+ /**
47
+ * Línea de conteo sobre la lista, ya compuesta por el consumer
48
+ * ("3 pendientes"). Sin ella no se muestra nada.
49
+ */
50
+ countLabel?: string;
51
+ /**
52
+ * ID del ítem con una decisión en vuelo. Ese ítem muestra su acción en
53
+ * `WORKING` y el resto queda deshabilitado: dos decisiones simultáneas sobre
54
+ * la misma cola es justo lo que no se quiere.
55
+ */
56
+ busyItemId?: string;
57
+ /**
58
+ * Exigir un motivo escrito para rechazar (default: **true**).
59
+ *
60
+ * Es el default a propósito. Rechazar sin explicación deja al solicitante sin
61
+ * saber qué corregir, y la notificación de decisión sale vacía. Ponerlo en
62
+ * `false` es una decisión explícita del consumer, no un olvido.
63
+ */
64
+ requireRejectReason?: boolean;
65
+ /** Etiquetas. Todas opcionales — la lib trae defaults es/en. */
66
+ approveLabel?: string;
67
+ rejectLabel?: string;
68
+ cancelLabel?: string;
69
+ confirmRejectLabel?: string;
70
+ rejectReasonLabel?: string;
71
+ rejectReasonPlaceholder?: string;
72
+ rejectReasonHint?: string;
73
+ }
@@ -41,6 +41,7 @@ export declare class SurveyResponseComponent implements OnInit {
41
41
  readonly sending: import("@angular/core").WritableSignal<boolean>;
42
42
  readonly typeConfig: import("@angular/core").WritableSignal<RequestTypeConfig>;
43
43
  readonly multiSelectError: import("@angular/core").WritableSignal<boolean>;
44
+ readonly alreadyAnswered: import("@angular/core").WritableSignal<boolean>;
44
45
  readonly nameControl: FormControl<string>;
45
46
  readonly emailControl: FormControl<string>;
46
47
  private form;
@@ -106,6 +107,7 @@ export declare class SurveyResponseComponent implements OnInit {
106
107
  needsIdentity(): boolean;
107
108
  readonly submitState: import("@angular/core").Signal<"ENABLED" | "WORKING">;
108
109
  submit(): Promise<void>;
110
+ readonly alreadyAnsweredState: import("@angular/core").Signal<EmptyStateMetadata>;
109
111
  readonly sentState: import("@angular/core").Signal<EmptyStateMetadata>;
110
112
  readonly loadErrorState: import("@angular/core").Signal<EmptyStateMetadata>;
111
113
  readonly loginRequiredState: import("@angular/core").Signal<EmptyStateMetadata>;
@@ -1,6 +1,12 @@
1
1
  export interface SurveyResponseMetadata {
2
2
  /** El `TypeID` del RequestTypeConfig (convención: `"survey:<slug>"`). */
3
3
  typeId: string;
4
+ /**
5
+ * Token de invitación (`?i=` del link del correo). Lo pasa el consumer desde
6
+ * la query de la ruta; el componente solo lo reenvía al backend, que decide
7
+ * si esa invitación sigue disponible.
8
+ */
9
+ inviteToken?: string;
4
10
  }
5
11
  export interface SurveyResponseSubmitEvent {
6
12
  requestId: string;
@@ -95,6 +95,12 @@ export interface CreateRequestPayload {
95
95
  priority?: string;
96
96
  /** Para solicitudes anónimas */
97
97
  submitter?: RequestActor;
98
+ /**
99
+ * El `?i=` del link que le llegó por correo a quien responde. Con token, la
100
+ * invitación se consume y no admite una segunda respuesta; sin token (el QR
101
+ * abierto de un evento) se responde sin límite.
102
+ */
103
+ inviteToken?: string;
98
104
  dueAt?: string;
99
105
  }
100
106
  export interface UpdateRequestPayload {
package/lib/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Current version of valtech-components.
3
3
  * This is automatically updated during the publish process.
4
4
  */
5
- export declare const VERSION = "4.0.987";
5
+ export declare const VERSION = "4.0.989";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valtech-components",
3
- "version": "4.0.987",
3
+ "version": "4.0.989",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "valtech-firebase-config": "./src/lib/services/firebase/scripts/generate-sw-config.js"
package/public-api.d.ts CHANGED
@@ -242,6 +242,8 @@ export * from './lib/components/organisms/survey-response/survey-response.compon
242
242
  export * from './lib/components/organisms/survey-response/types';
243
243
  export * from './lib/components/organisms/field-schema-editor/field-schema-editor.component';
244
244
  export * from './lib/components/organisms/field-schema-editor/types';
245
+ export * from './lib/components/organisms/request-review-panel/request-review-panel.component';
246
+ export * from './lib/components/organisms/request-review-panel/types';
245
247
  export * from './lib/components/organisms/survey-builder/survey-builder.component';
246
248
  export * from './lib/components/organisms/survey-builder/types';
247
249
  export * from './lib/components/molecules/empty-state/empty-state.component';
@@ -269,7 +271,6 @@ export * from './lib/components/organisms/share-profile-modal/share-profile-moda
269
271
  export * from './lib/components/organisms/share-profile-modal/types';
270
272
  export * from './lib/components/organisms/fun-modal/fun-modal.component';
271
273
  export * from './lib/components/organisms/request-modal/request-modal.component';
272
- export * from './lib/components/organisms/request-review-panel/request-review-panel.component';
273
274
  export * from './lib/components/organisms/fun-modal/types';
274
275
  export * from './lib/components/organisms/cookie-banner/cookie-banner.component';
275
276
  export * from './lib/components/organisms/cookie-banner/types';