@forgeroute/ui 0.4.71 → 0.4.73

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.
@@ -4,8 +4,8 @@ import * as i1 from "@angular/forms";
4
4
  import { FormsModule } from "@angular/forms";
5
5
  import { HttpClient, HttpErrorResponse } from "@angular/common/http";
6
6
  import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
7
- import { AlertComponent, ButtonComponent, CheckboxComponent, ChipComponent, ComboboxComponent, ContactPickerComponent, DatetimeInputComponent, ErrorComponent, FORGEROUTE_CONFIG, FormRowComponent, FormSectionComponent, HintComponent, PillTabsComponent, RadioGroupComponent, RichTextEditorComponent, SelectComponent, SidePanelComponent, TextInputComponent, TextareaInputComponent, ToastService, ToggleBadgeComponent, UiFieldValidity, createFieldReveal, getErrorMessage, requiredIssue } from "@forgeroute/ui";
8
- import { Subject, catchError, debounceTime, defer, distinctUntilChanged, map, of, switchMap, take } from "rxjs";
7
+ import { AlertComponent, BadgeComponent, ButtonComponent, CheckboxComponent, ChipComponent, ComboboxComponent, ContactPickerComponent, DatetimeInputComponent, ErrorComponent, FORGEROUTE_CONFIG, FormRowComponent, FormSectionComponent, HintComponent, PillTabsComponent, RadioGroupComponent, RichTextEditorComponent, SelectComponent, SidePanelComponent, TextInputComponent, TextareaInputComponent, ToastService, ToggleBadgeComponent, UiFieldValidity, createFieldReveal, getErrorMessage, requiredIssue } from "@forgeroute/ui";
8
+ import { Subject, catchError, debounceTime, defer, distinctUntilChanged, map, of, switchMap, take, throwError } from "rxjs";
9
9
  let nextVideoPickerId = 0;
10
10
  var VideoPickerComponent = class VideoPickerComponent {
11
11
  optionId(i) {
@@ -1958,6 +1958,8 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
1958
1958
  inputs: [
1959
1959
  "label",
1960
1960
  "ariaLabel",
1961
+ "labelHidden",
1962
+ "inputName",
1961
1963
  "placeholder",
1962
1964
  "options",
1963
1965
  "valueType",
@@ -2080,8 +2082,10 @@ var DocumentSendOptionsComponent = class DocumentSendOptionsComponent {
2080
2082
  "label",
2081
2083
  "ariaLabel",
2082
2084
  "inputTestId",
2085
+ "inputName",
2083
2086
  "placeholder",
2084
2087
  "prefix",
2088
+ "leadingIcon",
2085
2089
  "revealable",
2086
2090
  "passwordRequirements",
2087
2091
  "passwordRules",
@@ -3287,6 +3291,8 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3287
3291
  inputs: [
3288
3292
  "label",
3289
3293
  "ariaLabel",
3294
+ "labelHidden",
3295
+ "inputName",
3290
3296
  "placeholder",
3291
3297
  "options",
3292
3298
  "valueType",
@@ -3308,8 +3314,10 @@ var VideoLinkSettingsComponent = class VideoLinkSettingsComponent {
3308
3314
  "label",
3309
3315
  "ariaLabel",
3310
3316
  "inputTestId",
3317
+ "inputName",
3311
3318
  "placeholder",
3312
3319
  "prefix",
3320
+ "leadingIcon",
3313
3321
  "revealable",
3314
3322
  "passwordRequirements",
3315
3323
  "passwordRules",
@@ -3526,8 +3534,18 @@ function applyMergeTags(text, values) {
3526
3534
  return v != null && String(v).trim() !== "" ? String(v) : whole;
3527
3535
  });
3528
3536
  }
3529
- function hasLeftoverTags(text) {
3530
- return ANY_TAG_RE.test(text ?? "");
3537
+ const PERSONAL_TAG_KEYS = [
3538
+ "first_name",
3539
+ "last_name",
3540
+ "full_name",
3541
+ "name",
3542
+ "email",
3543
+ "phone"
3544
+ ];
3545
+ function hasLeftoverTags(text, ignoreKeys = []) {
3546
+ const t = text ?? "";
3547
+ if (ignoreKeys.length === 0) return ANY_TAG_RE.test(t);
3548
+ return ANY_TAG_RE.test(t.replace(TAG_RE, (whole, k) => ignoreKeys.includes(k) ? "" : whole));
3531
3549
  }
3532
3550
  function nonBlankValues(...sources) {
3533
3551
  const out = {};
@@ -3561,6 +3579,7 @@ function marketVarsFrom(resp) {
3561
3579
  function formatVarLabel(key) {
3562
3580
  return key.replace(/[_.]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3563
3581
  }
3582
+ const BULK_NO_SEND_MESSAGE = "Sending to several recipients is not set up for this page, so nothing was sent.";
3564
3583
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3565
3584
  function messagePreviewHtml(body) {
3566
3585
  if (!body) return "";
@@ -3569,6 +3588,9 @@ function messagePreviewHtml(body) {
3569
3588
  }
3570
3589
  const NO_DOCUMENTS$1 = [];
3571
3590
  var SendEmailComponent = class SendEmailComponent {
3591
+ get deferredKeys() {
3592
+ return this.bulk() ? PERSONAL_TAG_KEYS : [];
3593
+ }
3572
3594
  ngOnInit() {
3573
3595
  this.includeReceipt = this.receiptDefault();
3574
3596
  this.http.get(`${this.apiBase}/service-areas?size=100`).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (resp) => {
@@ -3586,6 +3608,8 @@ var SendEmailComponent = class SendEmailComponent {
3586
3608
  this.contactEmail = input("", ...ngDevMode ? [{ debugName: "contactEmail" }] : /* istanbul ignore next */ []);
3587
3609
  this.contactPhone = input("", ...ngDevMode ? [{ debugName: "contactPhone" }] : /* istanbul ignore next */ []);
3588
3610
  this.contactId = input("", ...ngDevMode ? [{ debugName: "contactId" }] : /* istanbul ignore next */ []);
3611
+ this.bulkRecipients = input(null, ...ngDevMode ? [{ debugName: "bulkRecipients" }] : /* istanbul ignore next */ []);
3612
+ this.bulk = computed(() => this.bulkRecipients() !== null, ...ngDevMode ? [{ debugName: "bulk" }] : /* istanbul ignore next */ []);
3589
3613
  this.defaultSubject = input("", ...ngDevMode ? [{ debugName: "defaultSubject" }] : /* istanbul ignore next */ []);
3590
3614
  this.defaultBody = input("", ...ngDevMode ? [{ debugName: "defaultBody" }] : /* istanbul ignore next */ []);
3591
3615
  this.context = input({}, ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
@@ -3626,6 +3650,8 @@ var SendEmailComponent = class SendEmailComponent {
3626
3650
  this.showTemplates = input(true, ...ngDevMode ? [{ debugName: "showTemplates" }] : /* istanbul ignore next */ []);
3627
3651
  this.showVideo = input(true, ...ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []);
3628
3652
  this.sent = output();
3653
+ this.bulkResult = output();
3654
+ this.bulkSending = output();
3629
3655
  this.emailAdded = output();
3630
3656
  this.validChange = output();
3631
3657
  this.rejected = output();
@@ -3684,6 +3710,7 @@ var SendEmailComponent = class SendEmailComponent {
3684
3710
  this.contactEmail();
3685
3711
  this.contactId();
3686
3712
  this.context();
3713
+ this.bulkRecipients();
3687
3714
  untracked(() => this.notifyValid());
3688
3715
  });
3689
3716
  effect(() => {
@@ -3823,7 +3850,7 @@ var SendEmailComponent = class SendEmailComponent {
3823
3850
  if (this.ccError || this.documents() && this.documentsInvalid) return false;
3824
3851
  if (this.signersActive && (this.signersInvalid || !this.signersValue)) return false;
3825
3852
  if (this.viaDocuSign && !this.contactId()) return false;
3826
- const hasContent = (this.viaDocuSign || (!this.showSubject() || !!this.subject.trim()) && !!this.body.trim()) && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedSubject) && !hasLeftoverTags(this.resolvedBody);
3853
+ const hasContent = (this.viaDocuSign || (!this.showSubject() || !!this.subject.trim()) && !!this.body.trim()) && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedSubject, this.deferredKeys) && !hasLeftoverTags(this.resolvedBody, this.deferredKeys);
3827
3854
  if (this.showSchedule) return hasContent && !!this.scheduledAt && new Date(this.scheduledAt).getTime() > Date.now();
3828
3855
  return hasContent;
3829
3856
  }
@@ -3833,7 +3860,7 @@ var SendEmailComponent = class SendEmailComponent {
3833
3860
  get knownVars() {
3834
3861
  const name = this.contactName().trim();
3835
3862
  const parts = name.split(/\s+/);
3836
- return nonBlankValues({
3863
+ return nonBlankValues(this.bulk() ? {} : {
3837
3864
  first_name: parts[0] ?? "",
3838
3865
  last_name: parts.slice(1).join(" "),
3839
3866
  full_name: name,
@@ -3870,11 +3897,11 @@ var SendEmailComponent = class SendEmailComponent {
3870
3897
  }
3871
3898
  get missingVars() {
3872
3899
  const known = this.knownVars;
3873
- return mergeTagKeys(this.subject + " " + this.body).filter((k) => !known[k]);
3900
+ return mergeTagKeys(this.subject + " " + this.body).filter((k) => !known[k] && !this.deferredKeys.includes(k));
3874
3901
  }
3875
3902
  get composeTagError() {
3876
3903
  if (this.mode !== "compose") return "";
3877
- return hasLeftoverTags(this.resolvedSubject) || hasLeftoverTags(this.resolvedBody) ? "Replace every {{…}} field with text before sending." : "";
3904
+ return hasLeftoverTags(this.resolvedSubject, this.deferredKeys) || hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? "Replace every {{…}} field with text before sending." : "";
3878
3905
  }
3879
3906
  get unresolvedVars() {
3880
3907
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -3916,7 +3943,7 @@ var SendEmailComponent = class SendEmailComponent {
3916
3943
  this.cdr.markForCheck();
3917
3944
  }
3918
3945
  notifyValid() {
3919
- this.validChange.emit(!!this.contactEmail() && this.canSend);
3946
+ this.validChange.emit((this.bulk() || !!this.contactEmail()) && this.canSend);
3920
3947
  this.cdr.markForCheck();
3921
3948
  }
3922
3949
  setMode(key) {
@@ -4036,7 +4063,11 @@ var SendEmailComponent = class SendEmailComponent {
4036
4063
  this.videoPickerKey++;
4037
4064
  }
4038
4065
  submit() {
4039
- if (!this.canSend || this.loading || !this.contactEmail()) return;
4066
+ if (!this.canSend || this.loading || !this.bulk() && !this.contactEmail()) return;
4067
+ if (this.bulk() && !this.sendFn()) {
4068
+ this.toast.error(BULK_NO_SEND_MESSAGE);
4069
+ return;
4070
+ }
4040
4071
  if (this.mode === "template") this.applyPendingVars();
4041
4072
  this.loading = true;
4042
4073
  this.cdr.markForCheck();
@@ -4054,6 +4085,10 @@ var SendEmailComponent = class SendEmailComponent {
4054
4085
  scheduledAt: this.showSchedule && this.scheduledAt ? new Date(this.scheduledAt).toISOString() : void 0,
4055
4086
  attachments: attachments.length ? attachments : void 0
4056
4087
  };
4088
+ if (this.bulk()) {
4089
+ payload.recipients = [...this.bulkRecipients()];
4090
+ this.bulkSending.emit(payload.recipients);
4091
+ }
4057
4092
  if (this.showReceiptOption() === true) payload.includeReceipt = this.includeReceipt;
4058
4093
  if (this.mode === "template" && this.templateId) payload.templateId = this.templateId;
4059
4094
  const cc = this.ccList;
@@ -4069,7 +4104,14 @@ var SendEmailComponent = class SendEmailComponent {
4069
4104
  payload.scheduledAt = void 0;
4070
4105
  }
4071
4106
  const custom = this.sendFn();
4072
- (custom ? custom(payload) : docs?.provider === "DOCUSIGN" ? sendDocuSignRequest(this.http, this.apiBase, this.docuSignBody(docs, payload)) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
4107
+ const safeHostCall = () => {
4108
+ try {
4109
+ return custom(payload);
4110
+ } catch (e) {
4111
+ return throwError(() => e);
4112
+ }
4113
+ };
4114
+ const req$ = custom && this.bulk() ? safeHostCall() : custom ? custom(payload) : docs?.provider === "DOCUSIGN" ? sendDocuSignRequest(this.http, this.apiBase, this.docuSignBody(docs, payload)) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
4073
4115
  subject: payload.subject || "Document Signature Required",
4074
4116
  message: payload.body,
4075
4117
  customerId: payload.contactId,
@@ -4093,8 +4135,22 @@ var SendEmailComponent = class SendEmailComponent {
4093
4135
  ...cc.length ? { ccEmails: cc } : {},
4094
4136
  ...signingBodyFields(payload.documents),
4095
4137
  ...signersContextBodyFields(payload.documents)
4096
- }, payload.templateId) : this.http.post(`${this.apiBase}/email-messages`, payload)).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4097
- next: () => {
4138
+ }, payload.templateId) : this.http.post(`${this.apiBase}/email-messages`, payload);
4139
+ let destroyed = false;
4140
+ this.destroyRef.onDestroy(() => {
4141
+ destroyed = true;
4142
+ });
4143
+ const onError = (err) => {
4144
+ if (this.bulk() && (destroyed || !this.loading)) return;
4145
+ this.toast.error(custom || docs ? getErrorMessage(err, "email", "send") : serverMessage(err) || "Failed to send email");
4146
+ this.loading = false;
4147
+ this.cdr.markForCheck();
4148
+ if (this.bulk()) this.bulkSending.emit(null);
4149
+ const status = err?.status;
4150
+ if (docs && (status === 404 || status === 409)) this.rejected.emit();
4151
+ };
4152
+ req$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4153
+ next: (result) => {
4098
4154
  const sentText = docs ? this.documentSentMessage()?.({
4099
4155
  titles: docs.documents.map((d) => d.title),
4100
4156
  envelopeName: this.documentEnvelope()?.name,
@@ -4106,15 +4162,16 @@ var SendEmailComponent = class SendEmailComponent {
4106
4162
  this.loading = false;
4107
4163
  this.attachments = [];
4108
4164
  this.includeReceipt = this.receiptDefault();
4165
+ if (this.bulk()) this.bulkResult.emit({
4166
+ result,
4167
+ recipients: payload.recipients ?? []
4168
+ });
4109
4169
  this.sent.emit();
4110
4170
  this.cdr.markForCheck();
4111
4171
  },
4112
- error: (err) => {
4113
- this.toast.error(custom || docs ? getErrorMessage(err, "email", "send") : serverMessage(err) || "Failed to send email");
4114
- this.loading = false;
4115
- this.cdr.markForCheck();
4116
- const status = err?.status;
4117
- if (docs && (status === 404 || status === 409)) this.rejected.emit();
4172
+ error: onError,
4173
+ complete: () => {
4174
+ if (!destroyed && this.bulk() && this.loading) onError(null);
4118
4175
  }
4119
4176
  });
4120
4177
  }
@@ -4164,6 +4221,13 @@ var SendEmailComponent = class SendEmailComponent {
4164
4221
  isRequired: false,
4165
4222
  transformFunction: null
4166
4223
  },
4224
+ bulkRecipients: {
4225
+ classPropertyName: "bulkRecipients",
4226
+ publicName: "bulkRecipients",
4227
+ isSignal: true,
4228
+ isRequired: false,
4229
+ transformFunction: null
4230
+ },
4167
4231
  defaultSubject: {
4168
4232
  classPropertyName: "defaultSubject",
4169
4233
  publicName: "defaultSubject",
@@ -4349,6 +4413,8 @@ var SendEmailComponent = class SendEmailComponent {
4349
4413
  },
4350
4414
  outputs: {
4351
4415
  sent: "sent",
4416
+ bulkResult: "bulkResult",
4417
+ bulkSending: "bulkSending",
4352
4418
  emailAdded: "emailAdded",
4353
4419
  validChange: "validChange",
4354
4420
  rejected: "rejected",
@@ -4357,7 +4423,7 @@ var SendEmailComponent = class SendEmailComponent {
4357
4423
  ngImport: i0,
4358
4424
  template: `
4359
4425
  <!-- Missing email guard -->
4360
- @if (!contactEmail()) {
4426
+ @if (!bulk() && !contactEmail()) {
4361
4427
  <div class="sm-missing-banner">
4362
4428
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
4363
4429
  No email address on file for this contact.
@@ -4374,7 +4440,8 @@ var SendEmailComponent = class SendEmailComponent {
4374
4440
  Save &amp; Continue
4375
4441
  </ui-button>
4376
4442
  } @else {
4377
- <!-- To -->
4443
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
4444
+ @if (!bulk()) {
4378
4445
  <div class="sm-group">
4379
4446
  <span class="sm-group-label">To</span>
4380
4447
  <div class="sm-to-card">
@@ -4382,6 +4449,7 @@ var SendEmailComponent = class SendEmailComponent {
4382
4449
  <span class="sm-to-detail">{{ contactEmail() }}</span>
4383
4450
  </div>
4384
4451
  </div>
4452
+ }
4385
4453
 
4386
4454
  <ui-error errorId="sm-email-disabled" [message]="emailEnabled() ? '' : EMAIL_DISABLED_MESSAGE" />
4387
4455
 
@@ -4608,7 +4676,7 @@ var SendEmailComponent = class SendEmailComponent {
4608
4676
  <div class="sp-email-meta">
4609
4677
  <div class="sp-meta-row">
4610
4678
  <span class="sp-meta-label">To</span>
4611
- <span class="sp-meta-value">{{ contactName() }} &lt;{{ contactEmail() }}&gt;</span>
4679
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
4612
4680
  </div>
4613
4681
  <div class="sp-meta-row">
4614
4682
  <span class="sp-meta-label">Subject</span>
@@ -4728,7 +4796,8 @@ var SendEmailComponent = class SendEmailComponent {
4728
4796
  "ariaLabel",
4729
4797
  "name",
4730
4798
  "noWrap",
4731
- "orientation"
4799
+ "orientation",
4800
+ "controlsPanels"
4732
4801
  ],
4733
4802
  outputs: ["tabChange"]
4734
4803
  },
@@ -4768,8 +4837,10 @@ var SendEmailComponent = class SendEmailComponent {
4768
4837
  "label",
4769
4838
  "ariaLabel",
4770
4839
  "inputTestId",
4840
+ "inputName",
4771
4841
  "placeholder",
4772
4842
  "prefix",
4843
+ "leadingIcon",
4773
4844
  "revealable",
4774
4845
  "passwordRequirements",
4775
4846
  "passwordRules",
@@ -4965,7 +5036,7 @@ i0.ɵɵngDeclareClassMetadata({
4965
5036
  ],
4966
5037
  template: `
4967
5038
  <!-- Missing email guard -->
4968
- @if (!contactEmail()) {
5039
+ @if (!bulk() && !contactEmail()) {
4969
5040
  <div class="sm-missing-banner">
4970
5041
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
4971
5042
  No email address on file for this contact.
@@ -4982,7 +5053,8 @@ i0.ɵɵngDeclareClassMetadata({
4982
5053
  Save &amp; Continue
4983
5054
  </ui-button>
4984
5055
  } @else {
4985
- <!-- To -->
5056
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
5057
+ @if (!bulk()) {
4986
5058
  <div class="sm-group">
4987
5059
  <span class="sm-group-label">To</span>
4988
5060
  <div class="sm-to-card">
@@ -4990,6 +5062,7 @@ i0.ɵɵngDeclareClassMetadata({
4990
5062
  <span class="sm-to-detail">{{ contactEmail() }}</span>
4991
5063
  </div>
4992
5064
  </div>
5065
+ }
4993
5066
 
4994
5067
  <ui-error errorId="sm-email-disabled" [message]="emailEnabled() ? '' : EMAIL_DISABLED_MESSAGE" />
4995
5068
 
@@ -5216,7 +5289,7 @@ i0.ɵɵngDeclareClassMetadata({
5216
5289
  <div class="sp-email-meta">
5217
5290
  <div class="sp-meta-row">
5218
5291
  <span class="sp-meta-label">To</span>
5219
- <span class="sp-meta-value">{{ contactName() }} &lt;{{ contactEmail() }}&gt;</span>
5292
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} &lt;{{ contactEmail() }}&gt; }</span>
5220
5293
  </div>
5221
5294
  <div class="sp-meta-row">
5222
5295
  <span class="sp-meta-label">Subject</span>
@@ -5265,6 +5338,14 @@ i0.ɵɵngDeclareClassMetadata({
5265
5338
  required: false
5266
5339
  }]
5267
5340
  }],
5341
+ bulkRecipients: [{
5342
+ type: i0.Input,
5343
+ args: [{
5344
+ isSignal: true,
5345
+ alias: "bulkRecipients",
5346
+ required: false
5347
+ }]
5348
+ }],
5268
5349
  defaultSubject: [{
5269
5350
  type: i0.Input,
5270
5351
  args: [{
@@ -5477,6 +5558,14 @@ i0.ɵɵngDeclareClassMetadata({
5477
5558
  type: i0.Output,
5478
5559
  args: ["sent"]
5479
5560
  }],
5561
+ bulkResult: [{
5562
+ type: i0.Output,
5563
+ args: ["bulkResult"]
5564
+ }],
5565
+ bulkSending: [{
5566
+ type: i0.Output,
5567
+ args: ["bulkSending"]
5568
+ }],
5480
5569
  emailAdded: [{
5481
5570
  type: i0.Output,
5482
5571
  args: ["emailAdded"]
@@ -5798,8 +5887,10 @@ var SendInAppComponent = class SendInAppComponent {
5798
5887
  "label",
5799
5888
  "ariaLabel",
5800
5889
  "inputTestId",
5890
+ "inputName",
5801
5891
  "placeholder",
5802
5892
  "prefix",
5893
+ "leadingIcon",
5803
5894
  "revealable",
5804
5895
  "passwordRequirements",
5805
5896
  "passwordRules",
@@ -6063,6 +6154,9 @@ function formatPhone(raw) {
6063
6154
  }
6064
6155
  const NO_DOCUMENTS = [];
6065
6156
  var SendTextComponent = class SendTextComponent {
6157
+ get deferredKeys() {
6158
+ return this.bulk() ? PERSONAL_TAG_KEYS : [];
6159
+ }
6066
6160
  ngOnInit() {
6067
6161
  this.http.get(`${this.apiBase}/service-areas?size=100`).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (resp) => {
6068
6162
  this.marketVars = marketVarsFrom(resp);
@@ -6080,6 +6174,8 @@ var SendTextComponent = class SendTextComponent {
6080
6174
  this.contactPhone = input("", ...ngDevMode ? [{ debugName: "contactPhone" }] : /* istanbul ignore next */ []);
6081
6175
  this.contactEmail = input("", ...ngDevMode ? [{ debugName: "contactEmail" }] : /* istanbul ignore next */ []);
6082
6176
  this.contactId = input("", ...ngDevMode ? [{ debugName: "contactId" }] : /* istanbul ignore next */ []);
6177
+ this.bulkRecipients = input(null, ...ngDevMode ? [{ debugName: "bulkRecipients" }] : /* istanbul ignore next */ []);
6178
+ this.bulk = computed(() => this.bulkRecipients() !== null, ...ngDevMode ? [{ debugName: "bulk" }] : /* istanbul ignore next */ []);
6083
6179
  this.defaultBody = input("", ...ngDevMode ? [{ debugName: "defaultBody" }] : /* istanbul ignore next */ []);
6084
6180
  this.context = input({}, ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
6085
6181
  this.sendFn = input(null, ...ngDevMode ? [{ debugName: "sendFn" }] : /* istanbul ignore next */ []);
@@ -6112,6 +6208,8 @@ var SendTextComponent = class SendTextComponent {
6112
6208
  }, ...ngDevMode ? [{ debugName: "previewContact" }] : /* istanbul ignore next */ []);
6113
6209
  this.videoDefaults = input(null, ...ngDevMode ? [{ debugName: "videoDefaults" }] : /* istanbul ignore next */ []);
6114
6210
  this.sent = output();
6211
+ this.bulkResult = output();
6212
+ this.bulkSending = output();
6115
6213
  this.phoneAdded = output();
6116
6214
  this.validChange = output();
6117
6215
  this.rejected = output();
@@ -6158,6 +6256,7 @@ var SendTextComponent = class SendTextComponent {
6158
6256
  this.contactPhone();
6159
6257
  this.contactId();
6160
6258
  this.context();
6259
+ this.bulkRecipients();
6161
6260
  untracked(() => this.notifyValid());
6162
6261
  });
6163
6262
  effect(() => {
@@ -6218,7 +6317,7 @@ var SendTextComponent = class SendTextComponent {
6218
6317
  get canSend() {
6219
6318
  if (this.documents() && this.documentsInvalid) return false;
6220
6319
  if (this._videoLine && this.body.length > this.MAX_LENGTH) return false;
6221
- const hasBody = this.body.trim().length > 0 && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedBody);
6320
+ const hasBody = this.body.trim().length > 0 && this.unresolvedVars.length === 0 && !hasLeftoverTags(this.resolvedBody, this.deferredKeys);
6222
6321
  if (this.showSchedule) return hasBody && !!this.scheduledAt && new Date(this.scheduledAt).getTime() > Date.now();
6223
6322
  return hasBody;
6224
6323
  }
@@ -6228,7 +6327,7 @@ var SendTextComponent = class SendTextComponent {
6228
6327
  get knownVars() {
6229
6328
  const name = this.contactName().trim();
6230
6329
  const parts = name.split(/\s+/);
6231
- return nonBlankValues({
6330
+ return nonBlankValues(this.bulk() ? {} : {
6232
6331
  first_name: parts[0] ?? "",
6233
6332
  last_name: parts.slice(1).join(" "),
6234
6333
  full_name: name,
@@ -6253,13 +6352,13 @@ var SendTextComponent = class SendTextComponent {
6253
6352
  }
6254
6353
  get missingVars() {
6255
6354
  const known = this.knownVars;
6256
- return mergeTagKeys(this.body).filter((k) => !known[k]);
6355
+ return mergeTagKeys(this.body).filter((k) => !known[k] && !this.deferredKeys.includes(k));
6257
6356
  }
6258
6357
  get videoLengthError() {
6259
6358
  return this._videoLine && this.body.length > this.MAX_LENGTH ? `The video link makes this text longer than ${this.MAX_LENGTH} characters. Shorten the message or remove the video.` : "";
6260
6359
  }
6261
6360
  get composeTagError() {
6262
- return this.mode === "compose" && hasLeftoverTags(this.resolvedBody) ? "Replace every {{…}} field with text before sending." : "";
6361
+ return this.mode === "compose" && hasLeftoverTags(this.resolvedBody, this.deferredKeys) ? "Replace every {{…}} field with text before sending." : "";
6263
6362
  }
6264
6363
  get unresolvedVars() {
6265
6364
  return this.missingVars.filter((k) => !(this.pendingInputs[k] ?? "").trim());
@@ -6303,7 +6402,7 @@ var SendTextComponent = class SendTextComponent {
6303
6402
  this.notifyValid();
6304
6403
  }
6305
6404
  notifyValid() {
6306
- this.validChange.emit(!!this.contactPhone() && this.canSend);
6405
+ this.validChange.emit((this.bulk() || !!this.contactPhone()) && this.canSend);
6307
6406
  this.cdr.detectChanges();
6308
6407
  }
6309
6408
  setMode(key) {
@@ -6404,7 +6503,11 @@ var SendTextComponent = class SendTextComponent {
6404
6503
  this.videoPickerKey++;
6405
6504
  }
6406
6505
  submit() {
6407
- if (!this.canSend || this.loading || !this.contactPhone()) return;
6506
+ if (!this.canSend || this.loading || !this.bulk() && !this.contactPhone()) return;
6507
+ if (this.bulk() && !this.sendFn()) {
6508
+ this.toast.error(BULK_NO_SEND_MESSAGE);
6509
+ return;
6510
+ }
6408
6511
  if (this.mode === "template") this.applyPendingVars();
6409
6512
  this.loading = true;
6410
6513
  this.cdr.detectChanges();
@@ -6419,6 +6522,10 @@ var SendTextComponent = class SendTextComponent {
6419
6522
  mediaType: media?.mediaType ?? void 0,
6420
6523
  mediaFileName: media?.mediaFileName ?? void 0
6421
6524
  };
6525
+ if (this.bulk()) {
6526
+ payload.recipients = [...this.bulkRecipients()];
6527
+ this.bulkSending.emit(payload.recipients);
6528
+ }
6422
6529
  if (this.mode === "template" && this.templateId) payload.templateId = this.templateId;
6423
6530
  const docs = this.hasDocuments ? this.documentsValue : null;
6424
6531
  if (docs) {
@@ -6432,7 +6539,14 @@ var SendTextComponent = class SendTextComponent {
6432
6539
  payload.scheduledAt = void 0;
6433
6540
  }
6434
6541
  const custom = this.sendFn();
6435
- (custom ? custom(payload) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
6542
+ const safeHostCall = () => {
6543
+ try {
6544
+ return custom(payload);
6545
+ } catch (e) {
6546
+ return throwError(() => e);
6547
+ }
6548
+ };
6549
+ const req$ = custom && this.bulk() ? safeHostCall() : custom ? custom(payload) : docs && this.documentEnvelope() ? sendExistingEnvelope(this.http, this.apiBase, this.documentEnvelope().id, {
6436
6550
  subject: DEFAULT_DOCUMENT_SUBJECT,
6437
6551
  message: payload.body,
6438
6552
  customerId: payload.contactId,
@@ -6456,8 +6570,22 @@ var SendTextComponent = class SendTextComponent {
6456
6570
  delivery: "TEXT",
6457
6571
  ...signingBodyFields(payload.documents),
6458
6572
  ...signersContextBodyFields(payload.documents)
6459
- }, payload.templateId) : this.http.post(`${this.apiBase}/text-messages`, payload)).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
6460
- next: () => {
6573
+ }, payload.templateId) : this.http.post(`${this.apiBase}/text-messages`, payload);
6574
+ let destroyed = false;
6575
+ this.destroyRef.onDestroy(() => {
6576
+ destroyed = true;
6577
+ });
6578
+ const onError = (err) => {
6579
+ if (this.bulk() && (destroyed || !this.loading)) return;
6580
+ this.toast.error(custom || docs ? getErrorMessage(err, "text message", "send") : serverMessage(err) || "Failed to send text message");
6581
+ this.loading = false;
6582
+ this.cdr.detectChanges();
6583
+ if (this.bulk()) this.bulkSending.emit(null);
6584
+ const status = err?.status;
6585
+ if (docs && (status === 404 || status === 409)) this.rejected.emit();
6586
+ };
6587
+ req$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
6588
+ next: (result) => {
6461
6589
  const sentText = docs ? this.documentSentMessage()?.({
6462
6590
  titles: docs.documents.map((d) => d.title),
6463
6591
  envelopeName: this.documentEnvelope()?.name,
@@ -6467,15 +6595,16 @@ var SendTextComponent = class SendTextComponent {
6467
6595
  this.toast.show(sentText ?? (docs && this.documentEnvelope() ? `Envelope "${this.documentEnvelope().name}" sent` : this.showSchedule ? "Message scheduled" : "Text message sent"));
6468
6596
  this.loading = false;
6469
6597
  this.mediaAttachment.set(null);
6598
+ if (this.bulk()) this.bulkResult.emit({
6599
+ result,
6600
+ recipients: payload.recipients ?? []
6601
+ });
6470
6602
  this.sent.emit();
6471
6603
  this.cdr.detectChanges();
6472
6604
  },
6473
- error: (err) => {
6474
- this.toast.error(custom || docs ? getErrorMessage(err, "text message", "send") : serverMessage(err) || "Failed to send text message");
6475
- this.loading = false;
6476
- this.cdr.detectChanges();
6477
- const status = err?.status;
6478
- if (docs && (status === 404 || status === 409)) this.rejected.emit();
6605
+ error: onError,
6606
+ complete: () => {
6607
+ if (!destroyed && this.bulk() && this.loading) onError(null);
6479
6608
  }
6480
6609
  });
6481
6610
  }
@@ -6525,6 +6654,13 @@ var SendTextComponent = class SendTextComponent {
6525
6654
  isRequired: false,
6526
6655
  transformFunction: null
6527
6656
  },
6657
+ bulkRecipients: {
6658
+ classPropertyName: "bulkRecipients",
6659
+ publicName: "bulkRecipients",
6660
+ isSignal: true,
6661
+ isRequired: false,
6662
+ transformFunction: null
6663
+ },
6528
6664
  defaultBody: {
6529
6665
  classPropertyName: "defaultBody",
6530
6666
  publicName: "defaultBody",
@@ -6654,6 +6790,8 @@ var SendTextComponent = class SendTextComponent {
6654
6790
  },
6655
6791
  outputs: {
6656
6792
  sent: "sent",
6793
+ bulkResult: "bulkResult",
6794
+ bulkSending: "bulkSending",
6657
6795
  phoneAdded: "phoneAdded",
6658
6796
  validChange: "validChange",
6659
6797
  rejected: "rejected"
@@ -6661,7 +6799,7 @@ var SendTextComponent = class SendTextComponent {
6661
6799
  ngImport: i0,
6662
6800
  template: `
6663
6801
  <!-- Missing phone guard -->
6664
- @if (!contactPhone()) {
6802
+ @if (!bulk() && !contactPhone()) {
6665
6803
  <div class="sm-missing-banner">
6666
6804
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
6667
6805
  No phone number on file for this contact.
@@ -6678,7 +6816,8 @@ var SendTextComponent = class SendTextComponent {
6678
6816
  Save &amp; Continue
6679
6817
  </ui-button>
6680
6818
  } @else {
6681
- <!-- To -->
6819
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
6820
+ @if (!bulk()) {
6682
6821
  <div class="sm-group">
6683
6822
  <span class="sm-group-label">To</span>
6684
6823
  <div class="sm-to-card">
@@ -6686,6 +6825,7 @@ var SendTextComponent = class SendTextComponent {
6686
6825
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
6687
6826
  </div>
6688
6827
  </div>
6828
+ }
6689
6829
 
6690
6830
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
6691
6831
  @if (showTemplates()) {
@@ -6869,7 +7009,7 @@ var SendTextComponent = class SendTextComponent {
6869
7009
  <div class="sp-text-meta">
6870
7010
  <div class="sp-meta-row">
6871
7011
  <span class="sp-meta-label">To</span>
6872
- <span class="sp-meta-value">{{ contactName() }} {{ formatPhone(contactPhone()) }}</span>
7012
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
6873
7013
  </div>
6874
7014
  </div>
6875
7015
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7003,7 +7143,8 @@ var SendTextComponent = class SendTextComponent {
7003
7143
  "ariaLabel",
7004
7144
  "name",
7005
7145
  "noWrap",
7006
- "orientation"
7146
+ "orientation",
7147
+ "controlsPanels"
7007
7148
  ],
7008
7149
  outputs: ["tabChange"]
7009
7150
  },
@@ -7043,8 +7184,10 @@ var SendTextComponent = class SendTextComponent {
7043
7184
  "label",
7044
7185
  "ariaLabel",
7045
7186
  "inputTestId",
7187
+ "inputName",
7046
7188
  "placeholder",
7047
7189
  "prefix",
7190
+ "leadingIcon",
7048
7191
  "revealable",
7049
7192
  "passwordRequirements",
7050
7193
  "passwordRules",
@@ -7215,7 +7358,7 @@ i0.ɵɵngDeclareClassMetadata({
7215
7358
  ],
7216
7359
  template: `
7217
7360
  <!-- Missing phone guard -->
7218
- @if (!contactPhone()) {
7361
+ @if (!bulk() && !contactPhone()) {
7219
7362
  <div class="sm-missing-banner">
7220
7363
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
7221
7364
  No phone number on file for this contact.
@@ -7232,7 +7375,8 @@ i0.ɵɵngDeclareClassMetadata({
7232
7375
  Save &amp; Continue
7233
7376
  </ui-button>
7234
7377
  } @else {
7235
- <!-- To -->
7378
+ <!-- To (bulk mode: the panel shows the recipient summary instead) -->
7379
+ @if (!bulk()) {
7236
7380
  <div class="sm-group">
7237
7381
  <span class="sm-group-label">To</span>
7238
7382
  <div class="sm-to-card">
@@ -7240,6 +7384,7 @@ i0.ɵɵngDeclareClassMetadata({
7240
7384
  <span class="sm-to-detail">{{ formatPhone(contactPhone()) }}</span>
7241
7385
  </div>
7242
7386
  </div>
7387
+ }
7243
7388
 
7244
7389
  <!-- Mode (hidden with [showTemplates]="false": the form is Create Message only) -->
7245
7390
  @if (showTemplates()) {
@@ -7423,7 +7568,7 @@ i0.ɵɵngDeclareClassMetadata({
7423
7568
  <div class="sp-text-meta">
7424
7569
  <div class="sp-meta-row">
7425
7570
  <span class="sp-meta-label">To</span>
7426
- <span class="sp-meta-value">{{ contactName() }} {{ formatPhone(contactPhone()) }}</span>
7571
+ <span class="sp-meta-value">@if (bulk()) { {{ bulkRecipients()!.length }} recipient{{ bulkRecipients()!.length === 1 ? '' : 's' }} } @else { {{ contactName() }} {{ formatPhone(contactPhone()) }} }</span>
7427
7572
  </div>
7428
7573
  </div>
7429
7574
  <div class="sp-text-body">{{ resolvedBody }}</div>
@@ -7486,6 +7631,14 @@ i0.ɵɵngDeclareClassMetadata({
7486
7631
  required: false
7487
7632
  }]
7488
7633
  }],
7634
+ bulkRecipients: [{
7635
+ type: i0.Input,
7636
+ args: [{
7637
+ isSignal: true,
7638
+ alias: "bulkRecipients",
7639
+ required: false
7640
+ }]
7641
+ }],
7489
7642
  defaultBody: [{
7490
7643
  type: i0.Input,
7491
7644
  args: [{
@@ -7634,6 +7787,14 @@ i0.ɵɵngDeclareClassMetadata({
7634
7787
  type: i0.Output,
7635
7788
  args: ["sent"]
7636
7789
  }],
7790
+ bulkResult: [{
7791
+ type: i0.Output,
7792
+ args: ["bulkResult"]
7793
+ }],
7794
+ bulkSending: [{
7795
+ type: i0.Output,
7796
+ args: ["bulkSending"]
7797
+ }],
7637
7798
  phoneAdded: [{
7638
7799
  type: i0.Output,
7639
7800
  args: ["phoneAdded"]
@@ -7648,7 +7809,46 @@ i0.ɵɵngDeclareClassMetadata({
7648
7809
  }]
7649
7810
  }
7650
7811
  });
7812
+ const BULK_MERGE_TAG_HINT = "Personal details such as {{first_name}} are filled in for each recipient when sent.";
7813
+ const SKIPPED_NAMES_SHOWN = 5;
7651
7814
  var SendMessagePanelComponent = class SendMessagePanelComponent {
7815
+ skippedReason() {
7816
+ return this.channel() === "email" ? "no email address" : "no phone number or no opt-in to texts";
7817
+ }
7818
+ skippedNames() {
7819
+ const names = this.skippedRecipients().map((r) => r.name || "Unnamed");
7820
+ const shown = names.slice(0, SKIPPED_NAMES_SHOWN).join(", ");
7821
+ return names.length > SKIPPED_NAMES_SHOWN ? `${shown} and ${names.length - SKIPPED_NAMES_SHOWN} more` : shown;
7822
+ }
7823
+ bulkRecipientsFor(ch) {
7824
+ if (!this.bulkMode()) return null;
7825
+ return ch === "email" ? this.emailRecipients() : this.textRecipients();
7826
+ }
7827
+ onBulkSending(sent) {
7828
+ if (sent) {
7829
+ this.submitSnapshot = {
7830
+ channel: this.channel(),
7831
+ selected: [...this.bulkList()],
7832
+ sent: [...sent]
7833
+ };
7834
+ this.sending.set(true);
7835
+ } else {
7836
+ this.submitSnapshot = null;
7837
+ this.sending.set(false);
7838
+ }
7839
+ }
7840
+ resetBulkSend() {
7841
+ this.submitSnapshot = null;
7842
+ this.lastBulkResult = void 0;
7843
+ this.sending.set(false);
7844
+ }
7845
+ onBulkResult(e) {
7846
+ this.lastBulkResult = e.result;
7847
+ if (this.submitSnapshot) this.submitSnapshot = {
7848
+ ...this.submitSnapshot,
7849
+ sent: [...e.recipients]
7850
+ };
7851
+ }
7652
7852
  onJobPick(o) {
7653
7853
  const job = o ? {
7654
7854
  id: o.value,
@@ -7669,6 +7869,10 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7669
7869
  this.open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
7670
7870
  this.contact = input(null, ...ngDevMode ? [{ debugName: "contact" }] : /* istanbul ignore next */ []);
7671
7871
  this.loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
7872
+ this.recipients = input(null, ...ngDevMode ? [{ debugName: "recipients" }] : /* istanbul ignore next */ []);
7873
+ this.initialChannel = input(null, ...ngDevMode ? [{ debugName: "initialChannel" }] : /* istanbul ignore next */ []);
7874
+ this.recipientNoun = input("recipients", ...ngDevMode ? [{ debugName: "recipientNoun" }] : /* istanbul ignore next */ []);
7875
+ this.recipientNounSingular = input("", ...ngDevMode ? [{ debugName: "recipientNounSingular" }] : /* istanbul ignore next */ []);
7672
7876
  this.textEnabled = input(false, ...ngDevMode ? [{ debugName: "textEnabled" }] : /* istanbul ignore next */ []);
7673
7877
  this.title = input("Send Message", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
7674
7878
  this.defaultSubject = input("", ...ngDevMode ? [{ debugName: "defaultSubject" }] : /* istanbul ignore next */ []);
@@ -7691,7 +7895,9 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7691
7895
  this.showSubject = input(true, ...ngDevMode ? [{ debugName: "showSubject" }] : /* istanbul ignore next */ []);
7692
7896
  this.showTemplates = input(true, ...ngDevMode ? [{ debugName: "showTemplates" }] : /* istanbul ignore next */ []);
7693
7897
  this.showVideo = input(true, ...ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []);
7694
- this.inApp = computed(() => this.recipientMode() === "in-app", ...ngDevMode ? [{ debugName: "inApp" }] : /* istanbul ignore next */ []);
7898
+ this.inApp = computed(() => this.recipientMode() === "in-app" && !this.bulkMode(), ...ngDevMode ? [{ debugName: "inApp" }] : /* istanbul ignore next */ []);
7899
+ this.BULK_NO_SEND_MESSAGE = BULK_NO_SEND_MESSAGE;
7900
+ this.BULK_MERGE_TAG_HINT = BULK_MERGE_TAG_HINT;
7695
7901
  this.attachmentsAndScheduling = input(true, ...ngDevMode ? [{ debugName: "attachmentsAndScheduling" }] : /* istanbul ignore next */ []);
7696
7902
  this.documentPurposes = input([
7697
7903
  "ESIGN",
@@ -7712,7 +7918,50 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7712
7918
  this.docuSignConnected = input(false, ...ngDevMode ? [{ debugName: "docuSignConnected" }] : /* istanbul ignore next */ []);
7713
7919
  this.documentPreview = input(null, ...ngDevMode ? [{ debugName: "documentPreview" }] : /* istanbul ignore next */ []);
7714
7920
  this.videoDefaults = input(null, ...ngDevMode ? [{ debugName: "videoDefaults" }] : /* istanbul ignore next */ []);
7715
- this.documentsOn = computed(() => (this.documents() || !!this.documentEnvelope()) && this.documentsEnabled(), ...ngDevMode ? [{ debugName: "documentsOn" }] : /* istanbul ignore next */ []);
7921
+ this.documentsOn = computed(() => (this.documents() || !!this.documentEnvelope()) && this.documentsEnabled() && !this.bulkMode(), ...ngDevMode ? [{ debugName: "documentsOn" }] : /* istanbul ignore next */ []);
7922
+ this.bulkList = computed(() => {
7923
+ const seen = /* @__PURE__ */ new Set();
7924
+ return (this.recipients() ?? []).filter((r) => !seen.has(r.id) && !!seen.add(r.id));
7925
+ }, ...ngDevMode ? [{ debugName: "bulkList" }] : /* istanbul ignore next */ []);
7926
+ this.bulkMode = computed(() => this.bulkList().length > 0, ...ngDevMode ? [{ debugName: "bulkMode" }] : /* istanbul ignore next */ []);
7927
+ this.emailRecipients = computed(() => this.bulkList().filter((r) => !!r.email?.trim()), ...ngDevMode ? [{ debugName: "emailRecipients" }] : /* istanbul ignore next */ []);
7928
+ this.textRecipients = computed(() => this.textEnabled() ? this.bulkList().filter((r) => !!r.phone?.trim() && r.smsOptedIn === true) : [], ...ngDevMode ? [{ debugName: "textRecipients" }] : /* istanbul ignore next */ []);
7929
+ this.activeRecipients = computed(() => this.channel() === "email" ? this.emailRecipients() : this.textRecipients(), ...ngDevMode ? [{ debugName: "activeRecipients" }] : /* istanbul ignore next */ []);
7930
+ this.skippedRecipients = computed(() => {
7931
+ const ok = new Set(this.activeRecipients());
7932
+ return this.bulkList().filter((r) => !ok.has(r));
7933
+ }, ...ngDevMode ? [{ debugName: "skippedRecipients" }] : /* istanbul ignore next */ []);
7934
+ this.bulkNoneEligible = computed(() => this.bulkMode() && this.activeRecipients().length === 0, ...ngDevMode ? [{ debugName: "bulkNoneEligible" }] : /* istanbul ignore next */ []);
7935
+ this.bulkMissingSend = computed(() => this.bulkMode() && !(this.channel() === "email" ? this.sendEmail() : this.sendText()), ...ngDevMode ? [{ debugName: "bulkMissingSend" }] : /* istanbul ignore next */ []);
7936
+ this.bulkBlocked = computed(() => this.bulkMode() && (this.bulkNoneEligible() || this.bulkMissingSend()), ...ngDevMode ? [{ debugName: "bulkBlocked" }] : /* istanbul ignore next */ []);
7937
+ this.hasTarget = computed(() => this.bulkMode() || !!this.activeContact(), ...ngDevMode ? [{ debugName: "hasTarget" }] : /* istanbul ignore next */ []);
7938
+ this.sending = signal(false, ...ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []);
7939
+ this.submitSnapshot = null;
7940
+ this.recipientWord = computed(() => {
7941
+ const n = this.bulkList().length;
7942
+ const plural = this.recipientNoun() || "recipients";
7943
+ if (n !== 1) return plural;
7944
+ return this.recipientNounSingular() || (plural.endsWith("s") ? plural.slice(0, -1) : plural);
7945
+ }, ...ngDevMode ? [{ debugName: "recipientWord" }] : /* istanbul ignore next */ []);
7946
+ this.skippedText = computed(() => {
7947
+ const n = this.skippedRecipients().length;
7948
+ if (n === 0) return "";
7949
+ const what = this.channel() === "email" ? "email" : "text";
7950
+ return `${n} ${n === 1 ? "has" : "have"} ${this.skippedReason()} and will be skipped (${this.skippedNames()}). Only the others get this ${what}.`;
7951
+ }, ...ngDevMode ? [{ debugName: "skippedText" }] : /* istanbul ignore next */ []);
7952
+ this.noneEligibleText = computed(() => {
7953
+ return `None of the ${this.bulkList().length} selected can receive this ${this.channel() === "email" ? "email" : "text message"} (${this.skippedReason()}), so there is nothing to send.`;
7954
+ }, ...ngDevMode ? [{ debugName: "noneEligibleText" }] : /* istanbul ignore next */ []);
7955
+ this.bulkAnnouncement = computed(() => {
7956
+ if (!this.bulkMode()) return "";
7957
+ const n = this.bulkList().length;
7958
+ const ok = this.activeRecipients().length;
7959
+ const ch = this.channel() === "email" ? "email" : "text";
7960
+ if (ok === 0) return `${n} ${this.recipientWord()} selected. None can receive the ${ch}.`;
7961
+ const skipped = this.skippedRecipients().length;
7962
+ return `${n} ${this.recipientWord()} selected. ${ok} can receive the ${ch}.${skipped ? ` ${skipped} will be skipped.` : ""}`;
7963
+ }, ...ngDevMode ? [{ debugName: "bulkAnnouncement" }] : /* istanbul ignore next */ []);
7964
+ this.lastBulkResult = void 0;
7716
7965
  this.showJobPicker = input(false, ...ngDevMode ? [{ debugName: "showJobPicker" }] : /* istanbul ignore next */ []);
7717
7966
  this.jobLabel = input("Job", ...ngDevMode ? [{ debugName: "jobLabel" }] : /* istanbul ignore next */ []);
7718
7967
  this.jobHint = input("", ...ngDevMode ? [{ debugName: "jobHint" }] : /* istanbul ignore next */ []);
@@ -7720,6 +7969,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7720
7969
  this.jobSelected = output();
7721
7970
  this.closed = output();
7722
7971
  this.sent = output();
7972
+ this.sentBulk = output();
7723
7973
  this.emailAdded = output();
7724
7974
  this.rejected = output();
7725
7975
  this.mapFieldsClick = output();
@@ -7777,6 +8027,31 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7777
8027
  const loadingVal = this.loading();
7778
8028
  const openedNow = openVal && !this._prevOpen;
7779
8029
  this._prevOpen = openVal;
8030
+ if (this.bulkMode()) {
8031
+ const emailCount = this.emailRecipients().length;
8032
+ const textCount = this.textRecipients().length;
8033
+ untracked(() => {
8034
+ const wasSingle = !!this.activeContact();
8035
+ const ch = this.channel();
8036
+ const activeCount = ch === "email" ? emailCount : textCount;
8037
+ const otherCount = ch === "email" ? textCount : emailCount;
8038
+ const pref = this.initialChannel();
8039
+ const defaultCh = pref === "text" && textCount > 0 ? "text" : pref === "email" && emailCount > 0 ? "email" : emailCount > 0 || textCount === 0 ? "email" : "text";
8040
+ if (openedNow || wasSingle) {
8041
+ this.activeContact.set(null);
8042
+ this.channel.set(defaultCh);
8043
+ } else if (openVal && activeCount === 0 && otherCount > 0 && !this.sending()) this.channel.set(ch === "email" ? "text" : "email");
8044
+ else return;
8045
+ this.resetBulkSend();
8046
+ this.valid.set(false);
8047
+ this.resetJob();
8048
+ this.formEpoch.update((n) => n + 1);
8049
+ });
8050
+ return;
8051
+ }
8052
+ untracked(() => {
8053
+ if (openedNow || this.sending() || this.submitSnapshot) this.resetBulkSend();
8054
+ });
7780
8055
  if (!openedNow && !(openVal && !loadingVal && !!contactVal)) return;
7781
8056
  const c = contactVal ?? null;
7782
8057
  const active = untracked(() => this.activeContact());
@@ -7800,6 +8075,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7800
8075
  this.formEpoch.update((n) => n + 1);
7801
8076
  }
7802
8077
  onChannelChange(ch) {
8078
+ if (this.sending()) return;
7803
8079
  this.channel.set(ch);
7804
8080
  this.valid.set(false);
7805
8081
  }
@@ -7820,12 +8096,29 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7820
8096
  this.phoneAdded.emit(phone);
7821
8097
  }
7822
8098
  submit() {
8099
+ if (this.bulkBlocked()) return;
7823
8100
  if (this.inApp()) this.sendInAppRef()?.submit();
7824
8101
  else if (this.channel() === "email") this.sendEmailRef()?.submit();
7825
8102
  else this.sendTextRef()?.submit();
7826
8103
  }
7827
8104
  onSent() {
8105
+ const snap = this.submitSnapshot;
8106
+ const bulk = !!snap || this.bulkMode();
8107
+ const channel = snap?.channel ?? this.channel();
8108
+ const selected = snap?.selected ?? [...this.bulkList()];
8109
+ const sentList = snap?.sent ?? [...this.activeRecipients()];
8110
+ const sentIds = new Set(sentList.map((r) => r.id));
8111
+ const event = bulk ? {
8112
+ channel,
8113
+ recipients: sentList,
8114
+ skipped: selected.filter((r) => !sentIds.has(r.id)),
8115
+ result: this.lastBulkResult
8116
+ } : null;
8117
+ this.lastBulkResult = void 0;
8118
+ this.submitSnapshot = null;
8119
+ this.sending.set(false);
7828
8120
  this.sent.emit();
8121
+ if (event) this.sentBulk.emit(event);
7829
8122
  this.closed.emit();
7830
8123
  }
7831
8124
  onClose() {
@@ -7870,6 +8163,34 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
7870
8163
  isRequired: false,
7871
8164
  transformFunction: null
7872
8165
  },
8166
+ recipients: {
8167
+ classPropertyName: "recipients",
8168
+ publicName: "recipients",
8169
+ isSignal: true,
8170
+ isRequired: false,
8171
+ transformFunction: null
8172
+ },
8173
+ initialChannel: {
8174
+ classPropertyName: "initialChannel",
8175
+ publicName: "initialChannel",
8176
+ isSignal: true,
8177
+ isRequired: false,
8178
+ transformFunction: null
8179
+ },
8180
+ recipientNoun: {
8181
+ classPropertyName: "recipientNoun",
8182
+ publicName: "recipientNoun",
8183
+ isSignal: true,
8184
+ isRequired: false,
8185
+ transformFunction: null
8186
+ },
8187
+ recipientNounSingular: {
8188
+ classPropertyName: "recipientNounSingular",
8189
+ publicName: "recipientNounSingular",
8190
+ isSignal: true,
8191
+ isRequired: false,
8192
+ transformFunction: null
8193
+ },
7873
8194
  textEnabled: {
7874
8195
  classPropertyName: "textEnabled",
7875
8196
  publicName: "textEnabled",
@@ -8169,6 +8490,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8169
8490
  jobSelected: "jobSelected",
8170
8491
  closed: "closed",
8171
8492
  sent: "sent",
8493
+ sentBulk: "sentBulk",
8172
8494
  emailAdded: "emailAdded",
8173
8495
  rejected: "rejected",
8174
8496
  mapFieldsClick: "mapFieldsClick",
@@ -8204,9 +8526,9 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8204
8526
  [title]="title()"
8205
8527
  [wide]="wide()"
8206
8528
  cancelLabel="Cancel"
8207
- [primaryLabel]="!loading() && activeContact() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8529
+ [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8208
8530
  primaryVariant="warning"
8209
- [primaryDisabled]="!valid()"
8531
+ [primaryDisabled]="!valid() || bulkBlocked()"
8210
8532
  (closed)="onClose()"
8211
8533
  (primaryClick)="submit()">
8212
8534
 
@@ -8218,7 +8540,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8218
8540
  </div>
8219
8541
  }
8220
8542
 
8221
- @if (!loading() && !activeContact() && !inApp()) {
8543
+ @if (!loading() && !hasTarget() && !inApp()) {
8222
8544
  <div class="msg-step">
8223
8545
  <span class="msg-step-badge">1</span>
8224
8546
  <span class="msg-step-text">Who would you like to message?</span>
@@ -8228,8 +8550,60 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8228
8550
  (contactSelected)="onContactSelected($event)" />
8229
8551
  }
8230
8552
 
8231
- @if (!loading() && activeContact()) {
8232
- @if (!inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8553
+ @if (!loading() && hasTarget()) {
8554
+ @if (bulkMode()) {
8555
+ <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
8556
+ <p class="msg-bulk-to" data-testid="bulk-to">To: {{ bulkList().length }} {{ recipientWord() }}</p>
8557
+ <div class="msg-bulk-counts">
8558
+ <ui-badge [label]="emailRecipients().length + ' can receive email'" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
8559
+ @if (textEnabled()) {
8560
+ <ui-badge [label]="textRecipients().length + ' can receive text'" [variant]="textRecipients().length ? 'blue' : 'gray'" />
8561
+ }
8562
+ </div>
8563
+ @if (!textEnabled()) {
8564
+ <ui-hint>Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.</ui-hint>
8565
+ }
8566
+ <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
8567
+ @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
8568
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-skipped">{{ skippedText() }}</ui-alert>
8569
+ }
8570
+ @if (bulkNoneEligible()) {
8571
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
8572
+ }
8573
+ @if (bulkMissingSend()) {
8574
+ <ui-alert variant="danger" data-testid="bulk-no-send">{{ BULK_NO_SEND_MESSAGE }}</ui-alert>
8575
+ }
8576
+ @if (!bulkNoneEligible()) {
8577
+ <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
8578
+ }
8579
+ </div>
8580
+ }
8581
+
8582
+ @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
8583
+ <div class="msg-channel-row" data-testid="bulk-channel-row">
8584
+ <label class="form-label msg-via-label">Send via</label>
8585
+ @if (sending()) {
8586
+ <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
8587
+ <div class="msg-via-tabs">
8588
+ <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
8589
+ <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">Channel locked while sending.</span>
8590
+ </div>
8591
+ } @else {
8592
+ <div class="msg-via-tabs">
8593
+ <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
8594
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
8595
+ Email
8596
+ </ui-toggle-badge>
8597
+ <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
8598
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
8599
+ Text
8600
+ </ui-toggle-badge>
8601
+ </div>
8602
+ }
8603
+ </div>
8604
+ }
8605
+
8606
+ @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8233
8607
  <div class="msg-channel-row">
8234
8608
  <label class="form-label msg-via-label">Send via</label>
8235
8609
  <div class="msg-via-tabs">
@@ -8253,7 +8627,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8253
8627
  }
8254
8628
 
8255
8629
  <!-- Optional job link (hidden unless the host turns it on). -->
8256
- @if (showJobPicker()) {
8630
+ @if (showJobPicker() && !bulkMode()) {
8257
8631
  @for (epoch of [formEpoch()]; track epoch) {
8258
8632
  <div class="msg-job">
8259
8633
  <ui-combobox
@@ -8266,13 +8640,14 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8266
8640
  }
8267
8641
 
8268
8642
  <!-- Keyed on formEpoch: a fresh open or another contact gets brand-new forms (no stale draft / template). -->
8643
+ <div [hidden]="bulkNoneEligible()" data-testid="msg-forms">
8269
8644
  @for (epoch of [formEpoch()]; track epoch) {
8270
8645
  @if (inApp()) {
8271
8646
  <ui-send-in-app
8272
8647
  #sendInAppRef
8273
- [contactName]="activeContact()!.name"
8274
- [contactId]="activeContact()!.id"
8275
- [contactEmail]="activeContact()!.email ?? ''"
8648
+ [contactName]="(activeContact()?.name ?? '')"
8649
+ [contactId]="(activeContact()?.id ?? '')"
8650
+ [contactEmail]="activeContact()?.email ?? ''"
8276
8651
  [allowAttachments]="inAppAttachments()"
8277
8652
  [richText]="inAppRichText()"
8278
8653
  [defaultSubject]="defaultSubject()"
@@ -8285,10 +8660,11 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8285
8660
  } @else if (channel() === 'email') {
8286
8661
  <ui-send-email
8287
8662
  #sendEmailRef
8288
- [contactName]="activeContact()!.name"
8289
- [contactEmail]="activeContact()!.email ?? ''"
8290
- [contactPhone]="activeContact()!.phone ?? ''"
8291
- [contactId]="activeContact()!.id"
8663
+ [contactName]="(activeContact()?.name ?? '')"
8664
+ [contactEmail]="activeContact()?.email ?? ''"
8665
+ [contactPhone]="activeContact()?.phone ?? ''"
8666
+ [contactId]="(activeContact()?.id ?? '')"
8667
+ [bulkRecipients]="bulkRecipientsFor('email')"
8292
8668
  [defaultSubject]="defaultSubject()"
8293
8669
  [defaultBody]="defaultBody()"
8294
8670
  [context]="context()"
@@ -8298,13 +8674,13 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8298
8674
  [receiptDefault]="receiptDefault()"
8299
8675
  [documents]="documentsOn()"
8300
8676
  [documentPurposes]="documentPurposes()"
8301
- [initialDocuments]="initialDocuments()"
8677
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8302
8678
  [documentSigningOptions]="documentSigningOptions()"
8303
- [documentSigners]="documentSigners()"
8679
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8304
8680
  [sendContext]="sendContext()"
8305
8681
  [documentContextTypes]="documentContextTypes()"
8306
8682
  [documentMergeCheck]="documentMergeCheck()"
8307
- [documentEnvelope]="documentEnvelope()"
8683
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8308
8684
  [emailEnabled]="emailEnabled()"
8309
8685
  [documentSentMessage]="documentSentMessage()"
8310
8686
  (rejected)="rejected.emit()"
@@ -8317,46 +8693,52 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8317
8693
  [showSubject]="showSubject()"
8318
8694
  [showTemplates]="showTemplates()"
8319
8695
  [showVideo]="showVideo()"
8696
+ (bulkSending)="onBulkSending($event)"
8697
+ (bulkResult)="onBulkResult($event)"
8320
8698
  (sent)="onSent()"
8321
8699
  (emailAdded)="onEmailAdded($event)"
8322
8700
  (validChange)="valid.set($event)" />
8323
8701
  } @else {
8324
8702
  <ui-send-text
8325
8703
  #sendTextRef
8326
- [contactName]="activeContact()!.name"
8327
- [contactPhone]="activeContact()!.phone ?? ''"
8328
- [contactEmail]="activeContact()!.email ?? ''"
8329
- [contactId]="activeContact()!.id"
8704
+ [contactName]="(activeContact()?.name ?? '')"
8705
+ [contactPhone]="activeContact()?.phone ?? ''"
8706
+ [contactEmail]="activeContact()?.email ?? ''"
8707
+ [contactId]="(activeContact()?.id ?? '')"
8708
+ [bulkRecipients]="bulkRecipientsFor('text')"
8330
8709
  [defaultBody]="defaultMessage()"
8331
8710
  [context]="context()"
8332
8711
  [sendFn]="sendText()"
8333
8712
  [attachmentsAndScheduling]="attachmentsAndScheduling()"
8334
8713
  [documents]="documentsOn()"
8335
8714
  [documentPurposes]="documentPurposes()"
8336
- [initialDocuments]="initialDocuments()"
8715
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8337
8716
  [documentSigningOptions]="documentSigningOptions()"
8338
- [documentSigners]="documentSigners()"
8717
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8339
8718
  [sendContext]="sendContext()"
8340
8719
  [documentContextTypes]="documentContextTypes()"
8341
8720
  [documentMergeCheck]="documentMergeCheck()"
8342
- [documentEnvelope]="documentEnvelope()"
8721
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8343
8722
  [documentSentMessage]="documentSentMessage()"
8344
8723
  (rejected)="rejected.emit()"
8345
8724
  [documentPreview]="documentPreview()"
8346
8725
  [videoDefaults]="videoDefaults()"
8347
8726
  [showTemplates]="showTemplates()"
8348
8727
  [showVideo]="showVideo()"
8728
+ (bulkSending)="onBulkSending($event)"
8729
+ (bulkResult)="onBulkResult($event)"
8349
8730
  (sent)="onSent()"
8350
8731
  (phoneAdded)="onPhoneAdded($event)"
8351
8732
  (validChange)="valid.set($event)" />
8352
8733
  }
8353
8734
  }
8735
+ </div>
8354
8736
  }
8355
8737
 
8356
8738
  </ui-side-panel>
8357
8739
  `,
8358
8740
  isInline: true,
8359
- styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-via-tabs{display:flex;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"],
8741
+ styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-locked-note{font-size:.75rem;color:#64748b}.msg-bulk{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.msg-bulk-to{margin:0;font-size:.9375rem;font-weight:600;color:var(--shell-text, #1e293b)}.msg-bulk-counts{display:flex;flex-wrap:wrap;gap:6px}.msg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.msg-via-tabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"],
8360
8742
  dependencies: [
8361
8743
  {
8362
8744
  kind: "component",
@@ -8400,6 +8782,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8400
8782
  "contactEmail",
8401
8783
  "contactPhone",
8402
8784
  "contactId",
8785
+ "bulkRecipients",
8403
8786
  "defaultSubject",
8404
8787
  "defaultBody",
8405
8788
  "context",
@@ -8429,6 +8812,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8429
8812
  ],
8430
8813
  outputs: [
8431
8814
  "sent",
8815
+ "bulkResult",
8816
+ "bulkSending",
8432
8817
  "emailAdded",
8433
8818
  "validChange",
8434
8819
  "rejected",
@@ -8462,6 +8847,7 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8462
8847
  "contactPhone",
8463
8848
  "contactEmail",
8464
8849
  "contactId",
8850
+ "bulkRecipients",
8465
8851
  "defaultBody",
8466
8852
  "context",
8467
8853
  "sendFn",
@@ -8483,6 +8869,8 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8483
8869
  ],
8484
8870
  outputs: [
8485
8871
  "sent",
8872
+ "bulkResult",
8873
+ "bulkSending",
8486
8874
  "phoneAdded",
8487
8875
  "validChange",
8488
8876
  "rejected"
@@ -8540,6 +8928,33 @@ var SendMessagePanelComponent = class SendMessagePanelComponent {
8540
8928
  "focused",
8541
8929
  "blurred"
8542
8930
  ]
8931
+ },
8932
+ {
8933
+ kind: "component",
8934
+ type: AlertComponent,
8935
+ selector: "ui-alert",
8936
+ inputs: [
8937
+ "variant",
8938
+ "title",
8939
+ "dismissible",
8940
+ "dismissAriaLabel",
8941
+ "quiet",
8942
+ "compact",
8943
+ "inline",
8944
+ "fill"
8945
+ ],
8946
+ outputs: ["dismissed"]
8947
+ },
8948
+ {
8949
+ kind: "component",
8950
+ type: BadgeComponent,
8951
+ selector: "ui-badge",
8952
+ inputs: ["label", "variant"]
8953
+ },
8954
+ {
8955
+ kind: "component",
8956
+ type: HintComponent,
8957
+ selector: "ui-hint"
8543
8958
  }
8544
8959
  ],
8545
8960
  changeDetection: i0.ChangeDetectionStrategy.OnPush
@@ -8564,7 +8979,10 @@ i0.ɵɵngDeclareClassMetadata({
8564
8979
  SendInAppComponent,
8565
8980
  SendTextComponent,
8566
8981
  ContactPickerComponent,
8567
- ComboboxComponent
8982
+ ComboboxComponent,
8983
+ AlertComponent,
8984
+ BadgeComponent,
8985
+ HintComponent
8568
8986
  ],
8569
8987
  template: `
8570
8988
  <ui-side-panel
@@ -8572,9 +8990,9 @@ i0.ɵɵngDeclareClassMetadata({
8572
8990
  [title]="title()"
8573
8991
  [wide]="wide()"
8574
8992
  cancelLabel="Cancel"
8575
- [primaryLabel]="!loading() && activeContact() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8993
+ [primaryLabel]="!loading() && hasTarget() ? (sendLabel() || (inApp() ? 'Send Message' : channel() === 'email' ? 'Send Email' : 'Send Text')) : ''"
8576
8994
  primaryVariant="warning"
8577
- [primaryDisabled]="!valid()"
8995
+ [primaryDisabled]="!valid() || bulkBlocked()"
8578
8996
  (closed)="onClose()"
8579
8997
  (primaryClick)="submit()">
8580
8998
 
@@ -8586,7 +9004,7 @@ i0.ɵɵngDeclareClassMetadata({
8586
9004
  </div>
8587
9005
  }
8588
9006
 
8589
- @if (!loading() && !activeContact() && !inApp()) {
9007
+ @if (!loading() && !hasTarget() && !inApp()) {
8590
9008
  <div class="msg-step">
8591
9009
  <span class="msg-step-badge">1</span>
8592
9010
  <span class="msg-step-text">Who would you like to message?</span>
@@ -8596,8 +9014,60 @@ i0.ɵɵngDeclareClassMetadata({
8596
9014
  (contactSelected)="onContactSelected($event)" />
8597
9015
  }
8598
9016
 
8599
- @if (!loading() && activeContact()) {
8600
- @if (!inApp() && textEnabled() && activeContact()?.smsOptedIn) {
9017
+ @if (!loading() && hasTarget()) {
9018
+ @if (bulkMode()) {
9019
+ <div class="msg-bulk" role="group" aria-label="Recipients" data-testid="bulk-summary">
9020
+ <p class="msg-bulk-to" data-testid="bulk-to">To: {{ bulkList().length }} {{ recipientWord() }}</p>
9021
+ <div class="msg-bulk-counts">
9022
+ <ui-badge [label]="emailRecipients().length + ' can receive email'" [variant]="emailRecipients().length ? 'blue' : 'gray'" />
9023
+ @if (textEnabled()) {
9024
+ <ui-badge [label]="textRecipients().length + ' can receive text'" [variant]="textRecipients().length ? 'blue' : 'gray'" />
9025
+ }
9026
+ </div>
9027
+ @if (!textEnabled()) {
9028
+ <ui-hint>Text messaging is not enabled. Connect Twilio in Comm Settings to send texts.</ui-hint>
9029
+ }
9030
+ <span class="msg-sr" role="status" data-testid="bulk-live">{{ bulkAnnouncement() }}</span>
9031
+ @if (skippedRecipients().length > 0 && !bulkNoneEligible()) {
9032
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-skipped">{{ skippedText() }}</ui-alert>
9033
+ }
9034
+ @if (bulkNoneEligible()) {
9035
+ <ui-alert variant="warning" [quiet]="true" data-testid="bulk-none-eligible">{{ noneEligibleText() }}</ui-alert>
9036
+ }
9037
+ @if (bulkMissingSend()) {
9038
+ <ui-alert variant="danger" data-testid="bulk-no-send">{{ BULK_NO_SEND_MESSAGE }}</ui-alert>
9039
+ }
9040
+ @if (!bulkNoneEligible()) {
9041
+ <ui-hint data-testid="bulk-merge-hint">{{ BULK_MERGE_TAG_HINT }}</ui-hint>
9042
+ }
9043
+ </div>
9044
+ }
9045
+
9046
+ @if (bulkMode() && textEnabled() && emailRecipients().length > 0 && textRecipients().length > 0) {
9047
+ <div class="msg-channel-row" data-testid="bulk-channel-row">
9048
+ <label class="form-label msg-via-label">Send via</label>
9049
+ @if (sending()) {
9050
+ <!-- A send is in flight: the toggle is replaced by plain text (ui-toggle-badge has no disabled state), so nothing pressable remains. -->
9051
+ <div class="msg-via-tabs">
9052
+ <ui-badge [label]="channel() === 'email' ? 'Email' : 'Text'" variant="blue" />
9053
+ <span class="msg-locked-note" role="status" data-testid="bulk-channel-locked">Channel locked while sending.</span>
9054
+ </div>
9055
+ } @else {
9056
+ <div class="msg-via-tabs">
9057
+ <ui-toggle-badge [active]="channel() === 'email'" (toggle)="onChannelChange('email')">
9058
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
9059
+ Email
9060
+ </ui-toggle-badge>
9061
+ <ui-toggle-badge [active]="channel() === 'text'" (toggle)="onChannelChange('text')">
9062
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
9063
+ Text
9064
+ </ui-toggle-badge>
9065
+ </div>
9066
+ }
9067
+ </div>
9068
+ }
9069
+
9070
+ @if (!bulkMode() && !inApp() && textEnabled() && activeContact()?.smsOptedIn) {
8601
9071
  <div class="msg-channel-row">
8602
9072
  <label class="form-label msg-via-label">Send via</label>
8603
9073
  <div class="msg-via-tabs">
@@ -8621,7 +9091,7 @@ i0.ɵɵngDeclareClassMetadata({
8621
9091
  }
8622
9092
 
8623
9093
  <!-- Optional job link (hidden unless the host turns it on). -->
8624
- @if (showJobPicker()) {
9094
+ @if (showJobPicker() && !bulkMode()) {
8625
9095
  @for (epoch of [formEpoch()]; track epoch) {
8626
9096
  <div class="msg-job">
8627
9097
  <ui-combobox
@@ -8634,13 +9104,14 @@ i0.ɵɵngDeclareClassMetadata({
8634
9104
  }
8635
9105
 
8636
9106
  <!-- Keyed on formEpoch: a fresh open or another contact gets brand-new forms (no stale draft / template). -->
9107
+ <div [hidden]="bulkNoneEligible()" data-testid="msg-forms">
8637
9108
  @for (epoch of [formEpoch()]; track epoch) {
8638
9109
  @if (inApp()) {
8639
9110
  <ui-send-in-app
8640
9111
  #sendInAppRef
8641
- [contactName]="activeContact()!.name"
8642
- [contactId]="activeContact()!.id"
8643
- [contactEmail]="activeContact()!.email ?? ''"
9112
+ [contactName]="(activeContact()?.name ?? '')"
9113
+ [contactId]="(activeContact()?.id ?? '')"
9114
+ [contactEmail]="activeContact()?.email ?? ''"
8644
9115
  [allowAttachments]="inAppAttachments()"
8645
9116
  [richText]="inAppRichText()"
8646
9117
  [defaultSubject]="defaultSubject()"
@@ -8653,10 +9124,11 @@ i0.ɵɵngDeclareClassMetadata({
8653
9124
  } @else if (channel() === 'email') {
8654
9125
  <ui-send-email
8655
9126
  #sendEmailRef
8656
- [contactName]="activeContact()!.name"
8657
- [contactEmail]="activeContact()!.email ?? ''"
8658
- [contactPhone]="activeContact()!.phone ?? ''"
8659
- [contactId]="activeContact()!.id"
9127
+ [contactName]="(activeContact()?.name ?? '')"
9128
+ [contactEmail]="activeContact()?.email ?? ''"
9129
+ [contactPhone]="activeContact()?.phone ?? ''"
9130
+ [contactId]="(activeContact()?.id ?? '')"
9131
+ [bulkRecipients]="bulkRecipientsFor('email')"
8660
9132
  [defaultSubject]="defaultSubject()"
8661
9133
  [defaultBody]="defaultBody()"
8662
9134
  [context]="context()"
@@ -8666,13 +9138,13 @@ i0.ɵɵngDeclareClassMetadata({
8666
9138
  [receiptDefault]="receiptDefault()"
8667
9139
  [documents]="documentsOn()"
8668
9140
  [documentPurposes]="documentPurposes()"
8669
- [initialDocuments]="initialDocuments()"
9141
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8670
9142
  [documentSigningOptions]="documentSigningOptions()"
8671
- [documentSigners]="documentSigners()"
9143
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8672
9144
  [sendContext]="sendContext()"
8673
9145
  [documentContextTypes]="documentContextTypes()"
8674
9146
  [documentMergeCheck]="documentMergeCheck()"
8675
- [documentEnvelope]="documentEnvelope()"
9147
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8676
9148
  [emailEnabled]="emailEnabled()"
8677
9149
  [documentSentMessage]="documentSentMessage()"
8678
9150
  (rejected)="rejected.emit()"
@@ -8685,45 +9157,51 @@ i0.ɵɵngDeclareClassMetadata({
8685
9157
  [showSubject]="showSubject()"
8686
9158
  [showTemplates]="showTemplates()"
8687
9159
  [showVideo]="showVideo()"
9160
+ (bulkSending)="onBulkSending($event)"
9161
+ (bulkResult)="onBulkResult($event)"
8688
9162
  (sent)="onSent()"
8689
9163
  (emailAdded)="onEmailAdded($event)"
8690
9164
  (validChange)="valid.set($event)" />
8691
9165
  } @else {
8692
9166
  <ui-send-text
8693
9167
  #sendTextRef
8694
- [contactName]="activeContact()!.name"
8695
- [contactPhone]="activeContact()!.phone ?? ''"
8696
- [contactEmail]="activeContact()!.email ?? ''"
8697
- [contactId]="activeContact()!.id"
9168
+ [contactName]="(activeContact()?.name ?? '')"
9169
+ [contactPhone]="activeContact()?.phone ?? ''"
9170
+ [contactEmail]="activeContact()?.email ?? ''"
9171
+ [contactId]="(activeContact()?.id ?? '')"
9172
+ [bulkRecipients]="bulkRecipientsFor('text')"
8698
9173
  [defaultBody]="defaultMessage()"
8699
9174
  [context]="context()"
8700
9175
  [sendFn]="sendText()"
8701
9176
  [attachmentsAndScheduling]="attachmentsAndScheduling()"
8702
9177
  [documents]="documentsOn()"
8703
9178
  [documentPurposes]="documentPurposes()"
8704
- [initialDocuments]="initialDocuments()"
9179
+ [initialDocuments]="bulkMode() ? [] : initialDocuments()"
8705
9180
  [documentSigningOptions]="documentSigningOptions()"
8706
- [documentSigners]="documentSigners()"
9181
+ [documentSigners]="bulkMode() ? null : documentSigners()"
8707
9182
  [sendContext]="sendContext()"
8708
9183
  [documentContextTypes]="documentContextTypes()"
8709
9184
  [documentMergeCheck]="documentMergeCheck()"
8710
- [documentEnvelope]="documentEnvelope()"
9185
+ [documentEnvelope]="bulkMode() ? null : documentEnvelope()"
8711
9186
  [documentSentMessage]="documentSentMessage()"
8712
9187
  (rejected)="rejected.emit()"
8713
9188
  [documentPreview]="documentPreview()"
8714
9189
  [videoDefaults]="videoDefaults()"
8715
9190
  [showTemplates]="showTemplates()"
8716
9191
  [showVideo]="showVideo()"
9192
+ (bulkSending)="onBulkSending($event)"
9193
+ (bulkResult)="onBulkResult($event)"
8717
9194
  (sent)="onSent()"
8718
9195
  (phoneAdded)="onPhoneAdded($event)"
8719
9196
  (validChange)="valid.set($event)" />
8720
9197
  }
8721
9198
  }
9199
+ </div>
8722
9200
  }
8723
9201
 
8724
9202
  </ui-side-panel>
8725
9203
  `,
8726
- styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-via-tabs{display:flex;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"]
9204
+ styles: [".msg-step{display:flex;align-items:center;gap:10px;margin-bottom:14px}.msg-step-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;flex-shrink:0;border-radius:50%;background:#1e293b;color:#fff;font-size:.75rem;font-weight:700}.msg-step-text{font-size:.9375rem;font-weight:600;color:#1e293b}.msg-channel-row{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}.msg-sms-notice{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;font-size:.8125rem;color:#64748b}.msg-job{margin-bottom:16px}.msg-locked-note{font-size:.75rem;color:#64748b}.msg-bulk{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.msg-bulk-to{margin:0;font-size:.9375rem;font-weight:600;color:var(--shell-text, #1e293b)}.msg-bulk-counts{display:flex;flex-wrap:wrap;gap:6px}.msg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.msg-via-tabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.msg-skeleton{display:flex;flex-direction:column;gap:12px;padding:4px 0}.skeleton-line{height:14px;border-radius:6px;background:linear-gradient(90deg,#e2e8f0 25%,#f1f5f9,#e2e8f0 75%);background-size:200% 100%;animation:skeleton-shimmer 1.4s infinite}.skeleton-line--wide{width:100%}.skeleton-line--medium{width:70%}.skeleton-line--narrow{width:45%}@keyframes skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"]
8727
9205
  }]
8728
9206
  }],
8729
9207
  ctorParameters: () => [],
@@ -8752,6 +9230,38 @@ i0.ɵɵngDeclareClassMetadata({
8752
9230
  required: false
8753
9231
  }]
8754
9232
  }],
9233
+ recipients: [{
9234
+ type: i0.Input,
9235
+ args: [{
9236
+ isSignal: true,
9237
+ alias: "recipients",
9238
+ required: false
9239
+ }]
9240
+ }],
9241
+ initialChannel: [{
9242
+ type: i0.Input,
9243
+ args: [{
9244
+ isSignal: true,
9245
+ alias: "initialChannel",
9246
+ required: false
9247
+ }]
9248
+ }],
9249
+ recipientNoun: [{
9250
+ type: i0.Input,
9251
+ args: [{
9252
+ isSignal: true,
9253
+ alias: "recipientNoun",
9254
+ required: false
9255
+ }]
9256
+ }],
9257
+ recipientNounSingular: [{
9258
+ type: i0.Input,
9259
+ args: [{
9260
+ isSignal: true,
9261
+ alias: "recipientNounSingular",
9262
+ required: false
9263
+ }]
9264
+ }],
8755
9265
  textEnabled: [{
8756
9266
  type: i0.Input,
8757
9267
  args: [{
@@ -9100,6 +9610,10 @@ i0.ɵɵngDeclareClassMetadata({
9100
9610
  type: i0.Output,
9101
9611
  args: ["sent"]
9102
9612
  }],
9613
+ sentBulk: [{
9614
+ type: i0.Output,
9615
+ args: ["sentBulk"]
9616
+ }],
9103
9617
  emailAdded: [{
9104
9618
  type: i0.Output,
9105
9619
  args: ["emailAdded"]
@@ -9130,6 +9644,6 @@ i0.ɵɵngDeclareClassMetadata({
9130
9644
  }]
9131
9645
  }
9132
9646
  });
9133
- export { DEFAULT_DOCUMENT_SUBJECT, DEFAULT_SIGNING_OPTIONS, DOCUMENT_PROVIDER_OPTIONS, DOCUMENT_PURPOSE_OPTIONS, DocumentMergeCheckComponent, DocumentSendOptionsComponent, DocumentSignersComponent, MAX_CC, MERGE_CHECK_DEBOUNCE_MS, SIGNER_ROLES, SIGNER_ROLE_LABEL_DEFAULT, SIGNING_LIMITS, SendMessagePanelComponent, TemplatePickerComponent, VideoPickerComponent, appendVideoLink, applyVideoLine, buildSigners, buildVideoLink, countersignLine, countersignPlaceLabel, describeDocumentSigners, effectiveRoles, forwardContext, hasMappedDataFields, mergeFieldText, mergeWarningLines, messagePreviewHtml, normalizeRoles, parseCcList, roleNumber, sendExistingEnvelope, signerProblems, signerRoleLabel, stripVideoLink, suggestDocumentSubject, videoMessageLine };
9647
+ export { BULK_MERGE_TAG_HINT, BULK_NO_SEND_MESSAGE, DEFAULT_DOCUMENT_SUBJECT, DEFAULT_SIGNING_OPTIONS, DOCUMENT_PROVIDER_OPTIONS, DOCUMENT_PURPOSE_OPTIONS, DocumentMergeCheckComponent, DocumentSendOptionsComponent, DocumentSignersComponent, MAX_CC, MERGE_CHECK_DEBOUNCE_MS, PERSONAL_TAG_KEYS, SIGNER_ROLES, SIGNER_ROLE_LABEL_DEFAULT, SIGNING_LIMITS, SendMessagePanelComponent, TemplatePickerComponent, VideoPickerComponent, appendVideoLink, applyVideoLine, buildSigners, buildVideoLink, countersignLine, countersignPlaceLabel, describeDocumentSigners, effectiveRoles, forwardContext, hasMappedDataFields, mergeFieldText, mergeWarningLines, messagePreviewHtml, normalizeRoles, parseCcList, roleNumber, sendExistingEnvelope, signerProblems, signerRoleLabel, stripVideoLink, suggestDocumentSubject, videoMessageLine };
9134
9648
 
9135
9649
  //# sourceMappingURL=forgeroute-ui-messaging.mjs.map