valtech-components 4.0.230 → 4.0.233

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.
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.230';
59
+ const VERSION = '4.0.233';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -61830,12 +61830,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
61830
61830
  }] } });
61831
61831
 
61832
61832
  /**
61833
- * Servicio para generación de PDFs via el PDF Lambda (/v2/pdf/generate).
61833
+ * Servicio de PDF de plataforma (ADR-042). Reutilizable por cualquier app.
61834
61834
  *
61835
61835
  * El JWT del usuario se inyecta automáticamente por el authInterceptor
61836
61836
  * (configurado vía provideValtechAuth). El app-id se toma de ValtechAuthConfig.
61837
61837
  *
61838
- * Modos de uso:
61838
+ * Tres modos:
61839
+ * - generate() → HTTP sync: genera y devuelve la URL presignada.
61840
+ * - createJob() → async: encola un job (delivery.mode='async') y devuelve
61841
+ * { jobId, status:'queued' }. El PDF se genera fuera de banda.
61842
+ * - watchJob() → observa el estado del job en tiempo real (Firestore),
61843
+ * proyectado por el backend al completarse.
61844
+ *
61845
+ * Fuentes (en los 3 modos):
61839
61846
  * - source.template → renderiza una plantilla desde DynamoDB
61840
61847
  * - source.html → HTML arbitrario provisto por el caller
61841
61848
  * - source.url → URL pública (Puppeteer la carga)
@@ -61844,6 +61851,8 @@ class PdfService {
61844
61851
  constructor(config) {
61845
61852
  this.config = config;
61846
61853
  this.http = inject(HttpClient);
61854
+ this.auth = inject(AuthService);
61855
+ this.collections = inject(FirestoreCollectionFactory);
61847
61856
  }
61848
61857
  get baseUrl() {
61849
61858
  return `${this.config?.apiUrl ?? ''}/v2/pdf`;
@@ -61860,6 +61869,35 @@ class PdfService {
61860
61869
  generateAndDownload(req, filename = 'documento.pdf') {
61861
61870
  return this.generate(req).pipe(switchMap$1(result => from(this._download(result.url, filename))));
61862
61871
  }
61872
+ /**
61873
+ * Encola un job PDF async. El backend publica `pdf.generate.requested`; el PDF
61874
+ * Lambda lo genera fuera de banda y, al completar, proyecta el estado a
61875
+ * Firestore. Usar watchJob(jobId) para observar el resultado en tiempo real.
61876
+ */
61877
+ createJob(req) {
61878
+ const body = {
61879
+ ...req,
61880
+ delivery: { ...(req.delivery ?? {}), mode: 'async' },
61881
+ };
61882
+ return this.http.post(`${this.baseUrl}/generate`, body, {
61883
+ headers: this.appIdHeader,
61884
+ });
61885
+ }
61886
+ /**
61887
+ * Observa el estado de un job PDF async en tiempo real (onSnapshot del doc
61888
+ * `apps/{appId}/orgs/{orgId}/pdf-jobs/{jobId}`). Emite null si no hay org
61889
+ * activa o hasta que el doc exista. Cuando el job completa/falla, el doc trae
61890
+ * status + url. Cierra la suscripción al completar tu propia lógica.
61891
+ */
61892
+ watchJob(jobId) {
61893
+ const orgId = this.auth.user()?.activeOrg ?? '';
61894
+ if (!orgId || !jobId)
61895
+ return of(null);
61896
+ const coll = this.collections.create(`orgs/${orgId}/pdf-jobs`, {
61897
+ timestamps: false,
61898
+ });
61899
+ return coll.watch(jobId);
61900
+ }
61863
61901
  async _download(url, _filename) {
61864
61902
  window.open(url, '_blank', 'noopener');
61865
61903
  }
@@ -72822,14 +72860,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72822
72860
  type: Output
72823
72861
  }] } });
72824
72862
 
72825
- addIcons({ addOutline, checkmarkOutline, closeOutline, documentOutline, micOutline, arrowUpOutline, trashOutline });
72863
+ addIcons({ addOutline, closeOutline, documentOutline, micOutline, arrowUpOutline, trashOutline });
72826
72864
  const CHAT_COMPOSER_I18N = {
72827
72865
  es: {
72828
72866
  placeholder: 'Escribe un mensaje',
72829
72867
  send: 'Enviar',
72830
72868
  attach: 'Adjuntar',
72831
72869
  voice: 'Mensaje de voz',
72832
- done: 'Listo',
72833
72870
  replyingTo: 'Respondiendo a',
72834
72871
  cancel: 'Cancelar',
72835
72872
  remove: 'Quitar',
@@ -72839,7 +72876,6 @@ const CHAT_COMPOSER_I18N = {
72839
72876
  send: 'Send',
72840
72877
  attach: 'Attach',
72841
72878
  voice: 'Voice message',
72842
- done: 'Done',
72843
72879
  replyingTo: 'Replying to',
72844
72880
  cancel: 'Cancel',
72845
72881
  remove: 'Remove',
@@ -72865,6 +72901,7 @@ class ChatComposerComponent {
72865
72901
  this.showMic = input(true);
72866
72902
  this.send = output();
72867
72903
  this.typing = output();
72904
+ this.attach = output();
72868
72905
  this.voice = output();
72869
72906
  this.cancelReply = output();
72870
72907
  this.body = signal('');
@@ -72907,16 +72944,19 @@ class ChatComposerComponent {
72907
72944
  onSend() {
72908
72945
  if (!this.canSend())
72909
72946
  return;
72910
- // Un solo evento: texto (caption) + todos los adjuntos staged. El consumer
72911
- // los sube y manda como UN mensaje con body + attachments.
72912
- const files = this.pending().map(p => p.file);
72913
- const text = this.body().trim();
72914
- this.send.emit({ text, files });
72915
- for (const att of this.pending()) {
72947
+ // Primero los adjuntos pendientes (cada uno se sube y manda en el consumer),
72948
+ // luego el texto como mensaje aparte si lo hay.
72949
+ const files = this.pending();
72950
+ for (const att of files) {
72951
+ this.attach.emit(att.file);
72916
72952
  if (att.url)
72917
72953
  URL.revokeObjectURL(att.url);
72918
72954
  }
72919
72955
  this.pending.set([]);
72956
+ const trimmed = this.body().trim();
72957
+ if (trimmed && trimmed.length <= this.maxLength()) {
72958
+ this.send.emit(trimmed);
72959
+ }
72920
72960
  this.body.set('');
72921
72961
  }
72922
72962
  onFile(event) {
@@ -72943,13 +72983,9 @@ class ChatComposerComponent {
72943
72983
  }
72944
72984
  }
72945
72985
  addPending(file) {
72946
- const kind = file.type.startsWith('image/')
72947
- ? 'image'
72948
- : file.type.startsWith('audio/')
72949
- ? 'audio'
72950
- : 'file';
72951
- const url = kind === 'file' ? undefined : URL.createObjectURL(file);
72952
- this.pending.update(arr => [...arr, { id: this.pendingId++, file, url, kind }]);
72986
+ const isImage = file.type.startsWith('image/');
72987
+ const url = isImage ? URL.createObjectURL(file) : undefined;
72988
+ this.pending.update(arr => [...arr, { id: this.pendingId++, file, url, isImage }]);
72953
72989
  }
72954
72990
  removePending(id) {
72955
72991
  this.pending.update(arr => {
@@ -73004,8 +73040,7 @@ class ChatComposerComponent {
73004
73040
  this.cleanupRecording();
73005
73041
  }
73006
73042
  }
73007
- /** Para la grabación y la stagea como adjunto pendiente (con preview de audio). */
73008
- finishRecording() {
73043
+ stopAndSend() {
73009
73044
  const rec = this.recorder;
73010
73045
  if (!rec) {
73011
73046
  this.cleanupRecording();
@@ -73017,7 +73052,7 @@ class ChatComposerComponent {
73017
73052
  const ext = type.includes('mp4') ? 'm4a' : 'webm';
73018
73053
  const file = new File([blob], `audio-${this.recSeconds()}s.${ext}`, { type });
73019
73054
  if (blob.size > 0)
73020
- this.addPending(file);
73055
+ this.attach.emit(file);
73021
73056
  this.cleanupRecording();
73022
73057
  };
73023
73058
  rec.stop();
@@ -73057,7 +73092,7 @@ class ChatComposerComponent {
73057
73092
  return this.i18n.t(key, 'ChatComposer');
73058
73093
  }
73059
73094
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73060
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatComposerComponent, isStandalone: true, selector: "val-chat-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, replyingTo: { classPropertyName: "replyingTo", publicName: "replyingTo", isSignal: true, isRequired: false, transformFunction: null }, showAttach: { classPropertyName: "showAttach", publicName: "showAttach", isSignal: true, isRequired: false, transformFunction: null }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", voice: "voice", cancelReply: "cancelReply" }, ngImport: i0, template: `
73095
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatComposerComponent, isStandalone: true, selector: "val-chat-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, replyingTo: { classPropertyName: "replyingTo", publicName: "replyingTo", isSignal: true, isRequired: false, transformFunction: null }, showAttach: { classPropertyName: "showAttach", publicName: "showAttach", isSignal: true, isRequired: false, transformFunction: null }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", attach: "attach", voice: "voice", cancelReply: "cancelReply" }, ngImport: i0, template: `
73061
73096
  <div class="composer">
73062
73097
  @if (replyingTo()) {
73063
73098
  <div class="reply-bar">
@@ -73074,26 +73109,17 @@ class ChatComposerComponent {
73074
73109
  @if (pending().length > 0) {
73075
73110
  <div class="pending-row">
73076
73111
  @for (att of pending(); track att.id) {
73077
- @if (att.kind === 'audio') {
73078
- <div class="audio-chip">
73079
- <audio controls preload="metadata" [src]="att.url"></audio>
73080
- <button class="thumb-remove inline" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73081
- <ion-icon name="close-outline" aria-hidden="true" />
73082
- </button>
73083
- </div>
73084
- } @else {
73085
- <div class="thumb" [class.file]="att.kind === 'file'">
73086
- @if (att.kind === 'image') {
73087
- <img [src]="att.url" [alt]="att.file.name" />
73088
- } @else {
73089
- <ion-icon name="document-outline" aria-hidden="true" />
73090
- <span class="thumb-name">{{ att.file.name }}</span>
73091
- }
73092
- <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73093
- <ion-icon name="close-outline" aria-hidden="true" />
73094
- </button>
73095
- </div>
73096
- }
73112
+ <div class="thumb" [class.file]="!att.isImage">
73113
+ @if (att.isImage) {
73114
+ <img [src]="att.url" [alt]="att.file.name" />
73115
+ } @else {
73116
+ <ion-icon name="document-outline" aria-hidden="true" />
73117
+ <span class="thumb-name">{{ att.file.name }}</span>
73118
+ }
73119
+ <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73120
+ <ion-icon name="close-outline" aria-hidden="true" />
73121
+ </button>
73122
+ </div>
73097
73123
  }
73098
73124
  </div>
73099
73125
  }
@@ -73106,8 +73132,8 @@ class ChatComposerComponent {
73106
73132
  <span class="rec-dot"></span>
73107
73133
  <span class="rec-time">{{ recTimeLabel() }}</span>
73108
73134
  <span class="rec-spacer"></span>
73109
- <button class="send-btn" [attr.aria-label]="t('done')" (click)="finishRecording()">
73110
- <ion-icon name="checkmark-outline" aria-hidden="true" />
73135
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
73136
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73111
73137
  </button>
73112
73138
  </div>
73113
73139
  } @else {
@@ -73148,7 +73174,7 @@ class ChatComposerComponent {
73148
73174
  </div>
73149
73175
  }
73150
73176
  </div>
73151
- `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
73177
+ `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
73152
73178
  }
73153
73179
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
73154
73180
  type: Component,
@@ -73169,26 +73195,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73169
73195
  @if (pending().length > 0) {
73170
73196
  <div class="pending-row">
73171
73197
  @for (att of pending(); track att.id) {
73172
- @if (att.kind === 'audio') {
73173
- <div class="audio-chip">
73174
- <audio controls preload="metadata" [src]="att.url"></audio>
73175
- <button class="thumb-remove inline" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73176
- <ion-icon name="close-outline" aria-hidden="true" />
73177
- </button>
73178
- </div>
73179
- } @else {
73180
- <div class="thumb" [class.file]="att.kind === 'file'">
73181
- @if (att.kind === 'image') {
73182
- <img [src]="att.url" [alt]="att.file.name" />
73183
- } @else {
73184
- <ion-icon name="document-outline" aria-hidden="true" />
73185
- <span class="thumb-name">{{ att.file.name }}</span>
73186
- }
73187
- <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73188
- <ion-icon name="close-outline" aria-hidden="true" />
73189
- </button>
73190
- </div>
73191
- }
73198
+ <div class="thumb" [class.file]="!att.isImage">
73199
+ @if (att.isImage) {
73200
+ <img [src]="att.url" [alt]="att.file.name" />
73201
+ } @else {
73202
+ <ion-icon name="document-outline" aria-hidden="true" />
73203
+ <span class="thumb-name">{{ att.file.name }}</span>
73204
+ }
73205
+ <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73206
+ <ion-icon name="close-outline" aria-hidden="true" />
73207
+ </button>
73208
+ </div>
73192
73209
  }
73193
73210
  </div>
73194
73211
  }
@@ -73201,8 +73218,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73201
73218
  <span class="rec-dot"></span>
73202
73219
  <span class="rec-time">{{ recTimeLabel() }}</span>
73203
73220
  <span class="rec-spacer"></span>
73204
- <button class="send-btn" [attr.aria-label]="t('done')" (click)="finishRecording()">
73205
- <ion-icon name="checkmark-outline" aria-hidden="true" />
73221
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
73222
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73206
73223
  </button>
73207
73224
  </div>
73208
73225
  } @else {
@@ -73243,7 +73260,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73243
73260
  </div>
73244
73261
  }
73245
73262
  </div>
73246
- `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"] }]
73263
+ `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"] }]
73247
73264
  }], ctorParameters: () => [] });
73248
73265
 
73249
73266
  /**
@@ -73463,6 +73480,7 @@ class ChatWindowComponent {
73463
73480
  this.replyTo = output();
73464
73481
  this.editMessage = output();
73465
73482
  this.typing = output();
73483
+ this.attach = output();
73466
73484
  this.voice = output();
73467
73485
  this.msgsEl = viewChild('msgs');
73468
73486
  this.replyingTo = signal(null);
@@ -73546,8 +73564,8 @@ class ChatWindowComponent {
73546
73564
  this.atBottom.set(true);
73547
73565
  this.hasNew.set(false);
73548
73566
  }
73549
- onSend(event) {
73550
- this.sendMessage.emit(event);
73567
+ onSend(body) {
73568
+ this.sendMessage.emit(body);
73551
73569
  this.replyingTo.set(null);
73552
73570
  }
73553
73571
  onAction(event) {
@@ -73575,7 +73593,7 @@ class ChatWindowComponent {
73575
73593
  return this.i18n.t(key, 'ChatWindow');
73576
73594
  }
73577
73595
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73578
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatWindowComponent, isStandalone: true, selector: "val-chat-window", inputs: { convId: { classPropertyName: "convId", publicName: "convId", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, currentUserId: { classPropertyName: "currentUserId", publicName: "currentUserId", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, typingUsers: { classPropertyName: "typingUsers", publicName: "typingUsers", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sendMessage: "sendMessage", loadMore: "loadMore", reactionClick: "reactionClick", deleteMessage: "deleteMessage", replyTo: "replyTo", editMessage: "editMessage", typing: "typing", voice: "voice" }, viewQueries: [{ propertyName: "msgsEl", first: true, predicate: ["msgs"], descendants: true, isSignal: true }], ngImport: i0, template: `
73596
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatWindowComponent, isStandalone: true, selector: "val-chat-window", inputs: { convId: { classPropertyName: "convId", publicName: "convId", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, currentUserId: { classPropertyName: "currentUserId", publicName: "currentUserId", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, typingUsers: { classPropertyName: "typingUsers", publicName: "typingUsers", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sendMessage: "sendMessage", loadMore: "loadMore", reactionClick: "reactionClick", deleteMessage: "deleteMessage", replyTo: "replyTo", editMessage: "editMessage", typing: "typing", attach: "attach", voice: "voice" }, viewQueries: [{ propertyName: "msgsEl", first: true, predicate: ["msgs"], descendants: true, isSignal: true }], ngImport: i0, template: `
73579
73597
  <div class="chat">
73580
73598
  <div #msgs class="messages" (scroll)="onScroll()">
73581
73599
  @if (isLoading()) {
@@ -73622,6 +73640,7 @@ class ChatWindowComponent {
73622
73640
  [replyingTo]="replyingTo()"
73623
73641
  (send)="onSend($event)"
73624
73642
  (typing)="typing.emit()"
73643
+ (attach)="attach.emit($event)"
73625
73644
  (voice)="voice.emit()"
73626
73645
  (cancelReply)="replyingTo.set(null)"
73627
73646
  />
@@ -73629,7 +73648,7 @@ class ChatWindowComponent {
73629
73648
  <div class="closed-banner">{{ t('conversationClosed') }}</div>
73630
73649
  }
73631
73650
  </div>
73632
- `, isInline: true, styles: [":host{display:block;height:100%}.chat{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}.messages{flex:1;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}.state{margin:auto;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ion-color-medium, #92949c);font-size:.9375rem;text-align:center;padding:24px}.state.empty ion-icon{font-size:2.5rem;opacity:.5}.date-sep{position:sticky;top:4px;z-index:1;display:flex;justify-content:center;margin:8px 0}.date-sep span{font-size:.75rem;color:var(--ion-color-medium, #92949c);background:var(--ion-color-step-100, rgba(127, 127, 127, .14));padding:3px 12px;border-radius:999px;backdrop-filter:blur(4px)}.scroll-down{position:absolute;right:14px;bottom:84px;width:40px;height:40px;border-radius:50%;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-color-medium, #92949c);box-shadow:0 2px 10px #00000026;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;z-index:3}.scroll-down .dot{position:absolute;top:0;right:0;width:12px;height:12px;border-radius:50%;background:var(--ion-color-primary);border:2px solid var(--ion-card-background, #fff)}.closed-banner{text-align:center;padding:14px;color:var(--ion-color-medium, #92949c);font-size:.875rem;border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: MessageBubbleComponent, selector: "val-message-bubble", inputs: ["msg", "showAvatar", "showName", "tail", "locale"], outputs: ["action"] }, { kind: "component", type: ChatComposerComponent, selector: "val-chat-composer", inputs: ["placeholder", "disabled", "maxLength", "replyingTo", "showAttach", "showMic"], outputs: ["send", "typing", "voice", "cancelReply"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
73651
+ `, isInline: true, styles: [":host{display:block;height:100%}.chat{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}.messages{flex:1;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}.state{margin:auto;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ion-color-medium, #92949c);font-size:.9375rem;text-align:center;padding:24px}.state.empty ion-icon{font-size:2.5rem;opacity:.5}.date-sep{position:sticky;top:4px;z-index:1;display:flex;justify-content:center;margin:8px 0}.date-sep span{font-size:.75rem;color:var(--ion-color-medium, #92949c);background:var(--ion-color-step-100, rgba(127, 127, 127, .14));padding:3px 12px;border-radius:999px;backdrop-filter:blur(4px)}.scroll-down{position:absolute;right:14px;bottom:84px;width:40px;height:40px;border-radius:50%;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-color-medium, #92949c);box-shadow:0 2px 10px #00000026;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;z-index:3}.scroll-down .dot{position:absolute;top:0;right:0;width:12px;height:12px;border-radius:50%;background:var(--ion-color-primary);border:2px solid var(--ion-card-background, #fff)}.closed-banner{text-align:center;padding:14px;color:var(--ion-color-medium, #92949c);font-size:.875rem;border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: MessageBubbleComponent, selector: "val-message-bubble", inputs: ["msg", "showAvatar", "showName", "tail", "locale"], outputs: ["action"] }, { kind: "component", type: ChatComposerComponent, selector: "val-chat-composer", inputs: ["placeholder", "disabled", "maxLength", "replyingTo", "showAttach", "showMic"], outputs: ["send", "typing", "attach", "voice", "cancelReply"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
73633
73652
  }
73634
73653
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
73635
73654
  type: Component,
@@ -73680,6 +73699,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73680
73699
  [replyingTo]="replyingTo()"
73681
73700
  (send)="onSend($event)"
73682
73701
  (typing)="typing.emit()"
73702
+ (attach)="attach.emit($event)"
73683
73703
  (voice)="voice.emit()"
73684
73704
  (cancelReply)="replyingTo.set(null)"
73685
73705
  />
@@ -73793,13 +73813,11 @@ class ThreadPanelComponent {
73793
73813
  this.conversationSvc.stopTyping(conv.convId, orgId, appId);
73794
73814
  }
73795
73815
  }
73796
- onSend(event) {
73816
+ onSend(body) {
73797
73817
  const conv = this.conv();
73798
73818
  if (!conv || !this.conversationSvc)
73799
73819
  return;
73800
- // thread-panel no tiene pipeline de subida de adjuntos; solo envía el texto.
73801
- if (event.text)
73802
- this.conversationSvc.sendMessage(conv.convId, event.text).subscribe();
73820
+ this.conversationSvc.sendMessage(conv.convId, body).subscribe();
73803
73821
  }
73804
73822
  onReactionClick(event) {
73805
73823
  // Reacciones se implementan a nivel de app consumer
@@ -73848,7 +73866,7 @@ class ThreadPanelComponent {
73848
73866
  </div>
73849
73867
  }
73850
73868
  </div>
73851
- `, isInline: true, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ChatWindowComponent, selector: "val-chat-window", inputs: ["convId", "messages", "currentUserId", "isOpen", "typingUsers", "isLoading"], outputs: ["sendMessage", "loadMore", "reactionClick", "deleteMessage", "replyTo", "editMessage", "typing", "voice"] }] }); }
73869
+ `, isInline: true, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ChatWindowComponent, selector: "val-chat-window", inputs: ["convId", "messages", "currentUserId", "isOpen", "typingUsers", "isLoading"], outputs: ["sendMessage", "loadMore", "reactionClick", "deleteMessage", "replyTo", "editMessage", "typing", "attach", "voice"] }] }); }
73852
73870
  }
73853
73871
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73854
73872
  type: Component,