@meetelise/chat 1.54.1 → 1.55.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.d.ts +3 -0
  2. package/dist/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.d.ts +4 -1
  3. package/dist/src/WebComponent/FeeCalculator/fee-calculator.d.ts +3 -0
  4. package/dist/src/WebComponent/FeeCalculator/model/marketable-fee.d.ts +2 -0
  5. package/dist/src/WebComponent/FeeCalculator/utils/pet-limit-utils.d.ts +10 -0
  6. package/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +21 -0
  7. package/dist/src/WebComponent/actions/email-us-window.d.ts +1 -7
  8. package/dist/src/WebComponent/brokerReferral.d.ts +17 -0
  9. package/dist/src/services/fees/calculateQuote.d.ts +14 -1
  10. package/package.json +1 -1
  11. package/public/dist/index.js +505 -284
  12. package/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.ts +44 -10
  13. package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout-styles.ts +7 -0
  14. package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.ts +32 -1
  15. package/src/WebComponent/FeeCalculator/fee-calculator.ts +26 -11
  16. package/src/WebComponent/FeeCalculator/model/marketable-fee.ts +3 -0
  17. package/src/WebComponent/FeeCalculator/utils/pet-limit-utils.ts +27 -0
  18. package/src/WebComponent/Scheduler/tour-scheduler.ts +253 -32
  19. package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +67 -1
  20. package/src/WebComponent/actions/email-us-window.ts +72 -51
  21. package/src/WebComponent/brokerReferral.ts +55 -0
  22. package/src/WebComponent/launcher/Launcher.ts +1 -0
  23. package/src/brokerReferralSubmission.test.ts +149 -0
  24. package/src/feeCalculatorPetLimit.test.ts +346 -0
  25. package/src/services/fees/calculateQuote.ts +38 -3
@@ -44,6 +44,13 @@ import {
44
44
  import { pushGtmEvent } from "../../gtm";
45
45
  import formDisclaimer from "../../disclaimers";
46
46
  import { tourSchedulerStyles } from "./tourSchedulerStyles";
47
+ import type { BrokerReferral } from "../brokerReferral";
48
+ import {
49
+ brokerEmailError,
50
+ brokerNameIsMissing,
51
+ brokerRequestFields,
52
+ toBrokerReferral,
53
+ } from "../brokerReferral";
47
54
  import { defaultPrimaryColor } from "../../themes";
48
55
  import { LogType, sendLoggingEvent } from "../../analytics";
49
56
  import LeadSourceClient, {
@@ -128,6 +135,9 @@ export class TourScheduler extends LitElement {
128
135
  hasDynamicSchedulingEnabled = false;
129
136
  @property({ attribute: true })
130
137
  bedroomPreferenceRequired = false;
138
+ /** Broker intake is on for the building: offer the broker referral fields. */
139
+ @property({ attribute: false })
140
+ brokerIntakeEnabled = false;
131
141
 
132
142
  @property({ attribute: true })
133
143
  private leadSourceClient: LeadSourceClient | null = null;
@@ -186,6 +196,16 @@ export class TourScheduler extends LitElement {
186
196
  private promptForReschedule = false;
187
197
  @state()
188
198
  private canceledReschedule = false;
199
+ @state()
200
+ isBrokerReferral = false;
201
+ @state()
202
+ brokerFirstName = "";
203
+ @state()
204
+ brokerLastName = "";
205
+ @state()
206
+ brokerEmail = "";
207
+ @state()
208
+ brokerFirmName = "";
189
209
 
190
210
  @property({ attribute: false })
191
211
  leadSources: string[] = [];
@@ -649,6 +669,51 @@ export class TourScheduler extends LitElement {
649
669
  this.email = (e.target as HTMLInputElement).value;
650
670
  };
651
671
 
672
+ /**
673
+ * A broker usually starts typing their own details before ticking the box, so ticking it moves
674
+ * the name and email into the broker's empty details and leaves the client's fields empty. Once
675
+ * the broker's details are filled, re-ticking leaves both sections alone, as on the email form.
676
+ */
677
+ onToggleBrokerReferral = (e: Event): void => {
678
+ const isBrokerReferral = (e.target as HTMLInputElement).checked;
679
+ const brokerIsEmpty =
680
+ !this.brokerFirstName && !this.brokerLastName && !this.brokerEmail;
681
+ const clientIsEmpty =
682
+ !this.firstNameInputValue && !this.lastNameInputValue && !this.email;
683
+ if (isBrokerReferral && brokerIsEmpty) {
684
+ this.brokerFirstName = this.firstNameInputValue;
685
+ this.brokerLastName = this.lastNameInputValue;
686
+ this.brokerEmail = this.email;
687
+ this.firstNameInputValue = "";
688
+ this.lastNameInputValue = "";
689
+ this.email = "";
690
+ // There's no broker phone field: what was typed is the broker's, not the client's.
691
+ this.phoneNumber = "";
692
+ this.phoneNumberE164 = null;
693
+ this.phoneNumberIsValid = false;
694
+ this.phoneNumberHasBlurred = false;
695
+ } else if (!isBrokerReferral && clientIsEmpty) {
696
+ this.firstNameInputValue = this.brokerFirstName;
697
+ this.lastNameInputValue = this.brokerLastName;
698
+ this.email = this.brokerEmail;
699
+ this.brokerFirstName = "";
700
+ this.brokerLastName = "";
701
+ this.brokerEmail = "";
702
+ }
703
+ this.isBrokerReferral = isBrokerReferral;
704
+ };
705
+
706
+ /** The broker's details to send, only when the submitter says they're referring a client. */
707
+ brokerReferral = (): BrokerReferral | null =>
708
+ this.brokerIntakeEnabled && this.isBrokerReferral
709
+ ? toBrokerReferral(
710
+ this.brokerFirstName,
711
+ this.brokerLastName,
712
+ this.brokerEmail,
713
+ this.brokerFirmName
714
+ )
715
+ : null;
716
+
652
717
  validators = {
653
718
  tourType: (): boolean => this.tourType !== null,
654
719
  dateAndTime: (): boolean => !!this.selectedDate && !!this.selectedTime,
@@ -662,6 +727,13 @@ export class TourScheduler extends LitElement {
662
727
  },
663
728
  layout: (): boolean =>
664
729
  !this.bedroomPreferenceRequired || !!this.selectedLayoutValue,
730
+ broker: (): boolean => {
731
+ const broker = this.brokerReferral();
732
+ return (
733
+ !broker ||
734
+ (!brokerNameIsMissing(broker) && !brokerEmailError(broker, this.email))
735
+ );
736
+ },
665
737
  };
666
738
 
667
739
  formIsValidForSubmission = (): boolean => {
@@ -669,7 +741,8 @@ export class TourScheduler extends LitElement {
669
741
  this.validators.tourType() &&
670
742
  this.validators.dateAndTime() &&
671
743
  this.validators.leadInfo() &&
672
- this.validators.layout();
744
+ this.validators.layout() &&
745
+ this.validators.broker();
673
746
  return isValid;
674
747
  };
675
748
 
@@ -696,6 +769,10 @@ export class TourScheduler extends LitElement {
696
769
  this.phoneNumberIsValid = false;
697
770
  return;
698
771
  }
772
+ if (!this.validators.broker()) {
773
+ return;
774
+ }
775
+ const broker = this.brokerReferral();
699
776
  const queryParams = new URLSearchParams(window.location.search);
700
777
 
701
778
  let parsedLeadSource = null;
@@ -744,6 +821,8 @@ export class TourScheduler extends LitElement {
744
821
  layout: selectedLayout,
745
822
  unit: selectedUnit,
746
823
  conversationTrackingId: this.leadSourceClient?.chatId ?? null,
824
+ // A flag only: the broker's own details stay out of the data layer.
825
+ ...(broker ? { isBrokerReferral: true } : {}),
747
826
  });
748
827
  const googleReferral = readGoogleReserveReferral();
749
828
  const data = {
@@ -781,6 +860,7 @@ export class TourScheduler extends LitElement {
781
860
  google_rwg_token: googleReferral?.rwgToken,
782
861
  google_referral_building_id: googleReferral?.buildingId,
783
862
  google_environment: googleReferral?.environment,
863
+ ...brokerRequestFields(broker),
784
864
  };
785
865
 
786
866
  const url = `https://app.meetelise.com/platformApi/state/create/v2/scheduleMe`;
@@ -1216,7 +1296,8 @@ export class TourScheduler extends LitElement {
1216
1296
  },
1217
1297
  },
1218
1298
  {
1219
- validate: (): boolean => this.validators.leadInfo(),
1299
+ validate: (): boolean =>
1300
+ this.validators.leadInfo() && this.validators.broker(),
1220
1301
  // last page gets <action-confirm-button> instead of the regular button
1221
1302
  nextButtonText: null,
1222
1303
  renderNextButton: (): TemplateResult => html` <action-confirm-button
@@ -1235,10 +1316,105 @@ export class TourScheduler extends LitElement {
1235
1316
  },
1236
1317
  ];
1237
1318
 
1319
+ renderBrokerToggle = (): TemplateResult => html`
1320
+ <label class="broker-toggle">
1321
+ <span>Are you an agent or locator?</span>
1322
+ <input
1323
+ type="checkbox"
1324
+ role="switch"
1325
+ id="broker-referral-toggle"
1326
+ class="broker-switch"
1327
+ .checked=${this.isBrokerReferral}
1328
+ @change=${this.onToggleBrokerReferral}
1329
+ />
1330
+ </label>
1331
+ `;
1332
+
1333
+ renderBrokerSection(): TemplateResult {
1334
+ const inputClasses = (extra?: string) =>
1335
+ classMap({
1336
+ ["webchat-input"]: true,
1337
+ ...(extra ? { [extra]: true } : {}),
1338
+ ["webchat-font__desktop"]: !isMobile(),
1339
+ ["webchat-font__mobile"]: isMobile(),
1340
+ });
1341
+ const broker = this.brokerReferral();
1342
+ // Only once the broker has typed an email, so the field isn't flagged before they start.
1343
+ const emailError =
1344
+ broker && this.brokerEmail ? brokerEmailError(broker, this.email) : null;
1345
+ return html`
1346
+ <div class="section-label">Your details</div>
1347
+ <div id="brokerNamesWrapper" class="namesWrapper">
1348
+ <div class="nameContainer">
1349
+ <input
1350
+ class=${inputClasses("nameInput")}
1351
+ type="text"
1352
+ placeholder="Your first name"
1353
+ name="brokerFirstName"
1354
+ autocomplete="given-name"
1355
+ .value=${this.brokerFirstName}
1356
+ @input=${(e: Event) => {
1357
+ this.brokerFirstName = (e.target as HTMLInputElement).value;
1358
+ }}
1359
+ />
1360
+ </div>
1361
+ <div class="nameContainer">
1362
+ <input
1363
+ class=${inputClasses("nameInput")}
1364
+ type="text"
1365
+ placeholder="Your last name"
1366
+ name="brokerLastName"
1367
+ autocomplete="family-name"
1368
+ .value=${this.brokerLastName}
1369
+ @input=${(e: Event) => {
1370
+ this.brokerLastName = (e.target as HTMLInputElement).value;
1371
+ }}
1372
+ />
1373
+ </div>
1374
+ </div>
1375
+ <div class="inputContainer">
1376
+ <input
1377
+ class=${classMap({
1378
+ ["webchat-input"]: true,
1379
+ ["webchat-font__desktop"]: !isMobile(),
1380
+ ["webchat-font__mobile"]: isMobile(),
1381
+ ["webchat-input__error"]: !!emailError,
1382
+ })}
1383
+ type="email"
1384
+ inputmode="email"
1385
+ placeholder="Your email"
1386
+ name="brokerEmail"
1387
+ autocomplete="email"
1388
+ .value=${this.brokerEmail}
1389
+ @input=${(e: Event) => {
1390
+ this.brokerEmail = (e.target as HTMLInputElement).value;
1391
+ }}
1392
+ />
1393
+ ${emailError ? html`<p class="error-message">${emailError}</p>` : ""}
1394
+ </div>
1395
+ <div class="inputContainer">
1396
+ <input
1397
+ class=${inputClasses()}
1398
+ type="text"
1399
+ placeholder="Brokerage (optional)"
1400
+ name="brokerFirmName"
1401
+ autocomplete="organization"
1402
+ .value=${this.brokerFirmName}
1403
+ @input=${(e: Event) => {
1404
+ this.brokerFirmName = (e.target as HTMLInputElement).value;
1405
+ }}
1406
+ />
1407
+ </div>
1408
+ <div class="section-label">Your client</div>
1409
+ `;
1410
+ }
1411
+
1238
1412
  userInfoAndLayoutMenu(): TemplateResult {
1413
+ const isReferral = this.brokerIntakeEnabled && this.isBrokerReferral;
1239
1414
  return html`<h2 class="journey-header">Your Information</h2>
1240
1415
  <div id="yourInformationMenu">
1241
- <div id="namesWrapper">
1416
+ ${isReferral ? this.renderBrokerSection() : ""}
1417
+ <div id="namesWrapper" class="namesWrapper">
1242
1418
  <div class="nameContainer" id="firstName">
1243
1419
  <input
1244
1420
  class=${classMap({
@@ -1248,10 +1424,13 @@ export class TourScheduler extends LitElement {
1248
1424
  ["webchat-font__mobile"]: isMobile(),
1249
1425
  })}
1250
1426
  type="text"
1251
- placeholder="First name"
1427
+ placeholder=${isReferral ? "Client first name" : "First name"}
1252
1428
  name="firstName"
1253
- autocomplete="given-name"
1254
- @input=${() => this.requestUpdate()}
1429
+ autocomplete=${isReferral ? "off" : "given-name"}
1430
+ .value=${this.firstNameInputValue}
1431
+ @input=${(e: Event) => {
1432
+ this.firstNameInputValue = (e.target as HTMLInputElement).value;
1433
+ }}
1255
1434
  />
1256
1435
  </div>
1257
1436
  <div class="nameContainer" id="lastName">
@@ -1263,10 +1442,13 @@ export class TourScheduler extends LitElement {
1263
1442
  ["webchat-font__mobile"]: isMobile(),
1264
1443
  })}
1265
1444
  type="text"
1266
- placeholder="Last name"
1445
+ placeholder=${isReferral ? "Client last name" : "Last name"}
1267
1446
  name="lastName"
1268
- autocomplete="family-name"
1269
- @input=${() => this.requestUpdate()}
1447
+ autocomplete=${isReferral ? "off" : "family-name"}
1448
+ .value=${this.lastNameInputValue}
1449
+ @input=${(e: Event) => {
1450
+ this.lastNameInputValue = (e.target as HTMLInputElement).value;
1451
+ }}
1270
1452
  />
1271
1453
  </div>
1272
1454
  </div>
@@ -1282,9 +1464,9 @@ export class TourScheduler extends LitElement {
1282
1464
  })}
1283
1465
  type="email"
1284
1466
  inputmode="email"
1285
- placeholder="Email"
1467
+ placeholder=${isReferral ? "Client email" : "Email"}
1286
1468
  name="email"
1287
- autocomplete="email"
1469
+ autocomplete=${isReferral ? "off" : "email"}
1288
1470
  .value=${this.email}
1289
1471
  @input=${this.onChangeEmail}
1290
1472
  />
@@ -1306,6 +1488,7 @@ export class TourScheduler extends LitElement {
1306
1488
  ? html`<p class="error-message">Invalid phone number</p>`
1307
1489
  : ""}
1308
1490
  </div>
1491
+ ${this.brokerIntakeEnabled ? this.renderBrokerToggle() : ""}
1309
1492
  ${this.layoutOptions.length > 0
1310
1493
  ? html` <me-select
1311
1494
  id="layout"
@@ -1438,27 +1621,56 @@ export class TourScheduler extends LitElement {
1438
1621
  fill="#202020"
1439
1622
  />
1440
1623
  </svg>
1441
- <p>Thank you!</p>
1442
- <p>
1443
- Your
1444
- ${{
1445
- [TourType.Guided]: "guided",
1446
- [TourType.Self]: "self-guided",
1447
- [TourType.Virtual]: "virtual",
1448
- }[this.tourType]}
1449
- tour is
1450
- ${this.promptForReschedule ? html`rescheduled` : html`scheduled`} for
1451
- ${readableDateAndTime}
1452
- ${getTimezoneAbbreviation(this.calendarTimeZone)}.
1453
- </p>
1454
- <p>
1455
- Look for an email confirmation along with instructions and directions.
1456
- </p>
1457
- <p>You can make changes at any time, just reply to the email.</p>
1624
+ ${this.brokerIntakeEnabled && this.isBrokerReferral
1625
+ ? this.referralConfirmation(readableDateAndTime)
1626
+ : html`<p>Thank you!</p>
1627
+ <p>
1628
+ Your
1629
+ ${{
1630
+ [TourType.Guided]: "guided",
1631
+ [TourType.Self]: "self-guided",
1632
+ [TourType.Virtual]: "virtual",
1633
+ }[this.tourType]}
1634
+ tour is
1635
+ ${this.promptForReschedule
1636
+ ? html`rescheduled`
1637
+ : html`scheduled`}
1638
+ for ${readableDateAndTime}
1639
+ ${getTimezoneAbbreviation(this.calendarTimeZone)}.
1640
+ </p>
1641
+ <p>
1642
+ Look for an email confirmation along with instructions and
1643
+ directions.
1644
+ </p>
1645
+ <p>
1646
+ You can make changes at any time, just reply to the email.
1647
+ </p>`}
1458
1648
  </div>
1459
1649
  `;
1460
1650
  }
1461
1651
 
1652
+ /** The broker isn't the one touring: the client gets the confirmation email. */
1653
+ private referralConfirmation(readableDateAndTime: string): TemplateResult {
1654
+ if (this.tourType === null) return html``;
1655
+ return html`<p>Thank you for the referral!</p>
1656
+ <p>
1657
+ Your client's
1658
+ ${{
1659
+ [TourType.Guided]: "guided",
1660
+ [TourType.Self]: "self-guided",
1661
+ [TourType.Virtual]: "virtual",
1662
+ }[this.tourType]}
1663
+ tour is
1664
+ ${this.promptForReschedule ? html`rescheduled` : html`scheduled`} for
1665
+ ${readableDateAndTime}
1666
+ ${getTimezoneAbbreviation(this.calendarTimeZone)}.
1667
+ </p>
1668
+ <p>
1669
+ We'll email your client a confirmation along with instructions and
1670
+ directions.
1671
+ </p>`;
1672
+ }
1673
+
1462
1674
  loadingIcon(size = 21): TemplateResult {
1463
1675
  return html` <svg
1464
1676
  aria-hidden="true"
@@ -1678,7 +1890,13 @@ export class TourScheduler extends LitElement {
1678
1890
  </div>
1679
1891
  `
1680
1892
  : html`
1681
- <div id="scheduler-container">
1893
+ <div
1894
+ id="scheduler-container"
1895
+ class=${classMap({
1896
+ ["with-broker"]:
1897
+ this.brokerIntakeEnabled && this.isBrokerReferral,
1898
+ })}
1899
+ >
1682
1900
  <div id="book-tour-journey-items">
1683
1901
  <div id="tour-type-menu-outer-container">
1684
1902
  ${this.tourTypeMenu()}
@@ -1694,9 +1912,12 @@ export class TourScheduler extends LitElement {
1694
1912
  </div>
1695
1913
  <div id="tour-scheduler-footer">
1696
1914
  <div class="explanation">
1697
- We'll send a confirmation and any follow-ups to your email
1698
- address, please reply to the email to make any appointment
1699
- changes.
1915
+ ${this.brokerIntakeEnabled && this.isBrokerReferral
1916
+ ? html`We'll send a confirmation and any follow-ups to
1917
+ your client's email address.`
1918
+ : html`We'll send a confirmation and any follow-ups to
1919
+ your email address, please reply to the email to make
1920
+ any appointment changes.`}
1700
1921
  ${formDisclaimer({
1701
1922
  buildingName: this.buildingName,
1702
1923
  phoneNumberInput: this.phoneNumber,
@@ -271,6 +271,46 @@ otherwise there's some empty space at the bottom of the button, which interferes
271
271
  height: 49px;
272
272
  }
273
273
 
274
+ #yourInformationMenu .broker-switch {
275
+ appearance: none;
276
+ -webkit-appearance: none;
277
+ flex-shrink: 0;
278
+ position: relative;
279
+ width: 40px;
280
+ height: 24px;
281
+ margin: 0;
282
+ border-radius: 100px;
283
+ background: #bdbdbd;
284
+ cursor: pointer;
285
+ transition: background 0.15s;
286
+ }
287
+
288
+ #yourInformationMenu .broker-switch::before {
289
+ content: "";
290
+ position: absolute;
291
+ top: 2px;
292
+ left: 2px;
293
+ width: 20px;
294
+ height: 20px;
295
+ border-radius: 50%;
296
+ background: #ffffff;
297
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
298
+ transition: transform 0.15s;
299
+ }
300
+
301
+ #yourInformationMenu .broker-switch:checked {
302
+ background: #202020;
303
+ }
304
+
305
+ #yourInformationMenu .broker-switch:checked::before {
306
+ transform: translateX(16px);
307
+ }
308
+
309
+ #yourInformationMenu .broker-switch:focus-visible {
310
+ outline: 2px solid #202020;
311
+ outline-offset: 2px;
312
+ }
313
+
274
314
  hr {
275
315
  grid-row: 6;
276
316
  grid-column: 1 / 5;
@@ -403,9 +443,35 @@ otherwise there's some empty space at the bottom of the button, which interferes
403
443
  display: none;
404
444
  }
405
445
 
406
- #namesWrapper {
446
+ #namesWrapper,
447
+ .namesWrapper {
448
+ display: flex;
449
+ justify-content: space-between;
450
+ }
451
+
452
+ /* A broker referral adds the broker's rows, so the form may grow taller before it scrolls. */
453
+ #scheduler-container.with-broker {
454
+ max-height: min(720px, calc(100vh - 180px));
455
+ }
456
+
457
+ .section-label {
458
+ font-size: 12px;
459
+ font-weight: 600;
460
+ color: #5c5c5c;
461
+ }
462
+
463
+ /* The question lines up with the fields' text, its switch with their right edge. */
464
+ .broker-toggle {
407
465
  display: flex;
466
+ align-items: center;
408
467
  justify-content: space-between;
468
+ gap: 12px;
469
+ min-height: 32px;
470
+ padding: 0 17px;
471
+ font-family: "Helvetica Neue", Arial;
472
+ font-size: 14px;
473
+ color: #202020;
474
+ cursor: pointer;
409
475
  }
410
476
 
411
477
  .nameContainer {