@meetelise/chat 1.44.0 → 1.46.0
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.
- package/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +17 -0
- package/dist/src/fetchBuildingUnitsSummary.d.ts +10 -0
- package/dist/src/fetchCentralConvoBuildings.d.ts +14 -0
- package/dist/src/types/rest-sdk.types.d.ts +4 -1
- package/package.json +1 -1
- package/public/dist/index.js +224 -186
- package/src/WebComponent/Scheduler/tour-scheduler.ts +213 -2
- package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +16 -0
- package/src/fetchBuildingUnitsSummary.ts +37 -0
- package/src/fetchCentralConvoBuildings.ts +32 -0
- package/src/types/rest-sdk.types.ts +5 -1
|
@@ -57,11 +57,18 @@ import { TourVirtuallyIcon } from "../icons/TourVirtuallyIcon";
|
|
|
57
57
|
import { TourSelfGuidedIcon } from "../icons/TourSelfGuidedIcon";
|
|
58
58
|
import { TourWithAgentIcon } from "../icons/TourWithAgentIcon";
|
|
59
59
|
import { shouldOpenTourLink } from "../../fetchBuildingWebchatView";
|
|
60
|
+
import fetchBuildingWebchatView from "../../fetchBuildingWebchatView";
|
|
61
|
+
import fetchCentralConvoBuildings, {
|
|
62
|
+
CentralConvoBuilding,
|
|
63
|
+
} from "../../fetchCentralConvoBuildings";
|
|
60
64
|
import formatInTimeZone from "date-fns-tz/formatInTimeZone";
|
|
61
65
|
import { getTimezoneAbbreviation } from "../../getTimezoneString";
|
|
62
66
|
import startOfDay from "date-fns/startOfDay";
|
|
63
67
|
import isSameDay from "date-fns/isSameDay";
|
|
64
68
|
import { LeadSourceMultitouchClient } from "../LeadSourceMultitouchClient";
|
|
69
|
+
import fetchBuildingUnitsSummary, {
|
|
70
|
+
UnitSummary,
|
|
71
|
+
} from "../../fetchBuildingUnitsSummary";
|
|
65
72
|
|
|
66
73
|
@customElement("tour-scheduler")
|
|
67
74
|
export class TourScheduler extends LitElement {
|
|
@@ -196,6 +203,8 @@ export class TourScheduler extends LitElement {
|
|
|
196
203
|
selectedLeadSource!: MESelect;
|
|
197
204
|
@query("me-select#layout")
|
|
198
205
|
selectedLayoutEl?: MESelect;
|
|
206
|
+
@query("me-select#unit")
|
|
207
|
+
selectedUnitEl?: MESelect;
|
|
199
208
|
|
|
200
209
|
@state()
|
|
201
210
|
firstNameInputValue = "";
|
|
@@ -205,10 +214,23 @@ export class TourScheduler extends LitElement {
|
|
|
205
214
|
leadSourceInputValue = "";
|
|
206
215
|
@state()
|
|
207
216
|
selectedLayoutValue = "";
|
|
217
|
+
@state()
|
|
218
|
+
selectedUnitValue = "";
|
|
219
|
+
@state()
|
|
220
|
+
private units: UnitSummary[] = [];
|
|
221
|
+
@state()
|
|
222
|
+
private allowOccupiedUnitTours = false;
|
|
208
223
|
|
|
209
224
|
@state()
|
|
210
225
|
errorGettingAvailabilities = false;
|
|
211
226
|
|
|
227
|
+
@state()
|
|
228
|
+
private centralConvoBuildings: CentralConvoBuilding[] = [];
|
|
229
|
+
@query("me-select#building")
|
|
230
|
+
selectedBuildingEl?: MESelect;
|
|
231
|
+
@state()
|
|
232
|
+
private statusMessage = "";
|
|
233
|
+
|
|
212
234
|
_setAvailabilities = async (): Promise<void> => {
|
|
213
235
|
try {
|
|
214
236
|
const [allowScheduling, availabilitiesExistForTourType] =
|
|
@@ -410,10 +432,151 @@ export class TourScheduler extends LitElement {
|
|
|
410
432
|
return null;
|
|
411
433
|
};
|
|
412
434
|
|
|
435
|
+
_getUnitOptions = (): { label: string; value: string }[] => {
|
|
436
|
+
const visible = this.selectedLayoutValue
|
|
437
|
+
? this.units.filter(
|
|
438
|
+
(u) =>
|
|
439
|
+
layoutValueToCanonical(u.numberOfBedrooms * 10) ===
|
|
440
|
+
this.selectedLayoutValue
|
|
441
|
+
)
|
|
442
|
+
: this.units;
|
|
443
|
+
const unitOptions = visible.map((u) => ({
|
|
444
|
+
label: u.unitNumber,
|
|
445
|
+
value: u.unitNumber,
|
|
446
|
+
}));
|
|
447
|
+
return unitOptions;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
_fetchUnits = async (): Promise<void> => {
|
|
451
|
+
if (!this.buildingSlug) return;
|
|
452
|
+
try {
|
|
453
|
+
this.units = await fetchBuildingUnitsSummary({
|
|
454
|
+
buildingSlug: this.buildingSlug,
|
|
455
|
+
});
|
|
456
|
+
} catch (e) {
|
|
457
|
+
this.units = [];
|
|
458
|
+
sendLoggingEvent({
|
|
459
|
+
logTitle: "ERROR_LOADING_UNITS_FOR_TOUR_SCHEDULER",
|
|
460
|
+
logData: { error: e },
|
|
461
|
+
logType: LogType.error,
|
|
462
|
+
buildingSlug: this.buildingSlug,
|
|
463
|
+
orgSlug: this.orgSlug,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
413
468
|
firstUpdated = async (): Promise<void> => {
|
|
414
|
-
|
|
469
|
+
this._fetchCentralConvoBuildings();
|
|
470
|
+
await this._loadForCurrentBuilding();
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
private _loadForCurrentBuilding = async (): Promise<void> => {
|
|
474
|
+
try {
|
|
475
|
+
await getRawAvailabilities(this.buildingId);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
// noop
|
|
478
|
+
}
|
|
479
|
+
await Promise.all([
|
|
480
|
+
this._setAvailabilities().then(async () => {
|
|
481
|
+
try {
|
|
482
|
+
const rawAvailabilities = await getRawAvailabilities(this.buildingId);
|
|
483
|
+
this.allowOccupiedUnitTours =
|
|
484
|
+
!!rawAvailabilities.allowOccupiedUnitTours;
|
|
485
|
+
} catch (e) {
|
|
486
|
+
this.allowOccupiedUnitTours = false;
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
489
|
+
this._fetchUnits(),
|
|
490
|
+
]);
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
private _fetchCentralConvoBuildings = async (): Promise<void> => {
|
|
494
|
+
if (!this.orgSlug || !this.buildingSlug) return;
|
|
495
|
+
try {
|
|
496
|
+
this.centralConvoBuildings = await fetchCentralConvoBuildings(
|
|
497
|
+
this.orgSlug,
|
|
498
|
+
this.buildingSlug
|
|
499
|
+
);
|
|
500
|
+
} catch (e) {
|
|
501
|
+
this.centralConvoBuildings = [];
|
|
502
|
+
}
|
|
415
503
|
};
|
|
416
504
|
|
|
505
|
+
private _onBuildingChange = async (): Promise<void> => {
|
|
506
|
+
const slug = this.selectedBuildingEl?.value;
|
|
507
|
+
if (!slug || slug === this.buildingSlug) return;
|
|
508
|
+
const building = this.centralConvoBuildings.find((b) => b.slug === slug);
|
|
509
|
+
if (!building) return;
|
|
510
|
+
|
|
511
|
+
this.waitingForAvailabilities = true;
|
|
512
|
+
this.errorGettingAvailabilities = false;
|
|
513
|
+
this.tourType = null;
|
|
514
|
+
this.selectedDate = undefined;
|
|
515
|
+
this.selectedTime = undefined;
|
|
516
|
+
this.availabilitiesGroupedByDay = {};
|
|
517
|
+
this.mobilePageIndex = 0;
|
|
518
|
+
this.units = [];
|
|
519
|
+
this.selectedLayoutValue = "";
|
|
520
|
+
this.selectedUnitValue = "";
|
|
521
|
+
|
|
522
|
+
this.buildingId = building.building_id;
|
|
523
|
+
this.buildingSlug = building.slug;
|
|
524
|
+
this.buildingName = building.name;
|
|
525
|
+
this.statusMessage = `Loading tours for ${building.name}…`;
|
|
526
|
+
|
|
527
|
+
pushGtmEvent("tourBuildingSelected", {
|
|
528
|
+
buildingId: building.building_id,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const view = await fetchBuildingWebchatView(this.orgSlug, building.slug);
|
|
533
|
+
this.sgtUrl = view.sgtUrl;
|
|
534
|
+
this.selfGuidedToursTypeOffered = view.selfGuidedToursTypeOffered;
|
|
535
|
+
this.selfGuidedTourEnabled = view.isSelfGuidedTourEnabled;
|
|
536
|
+
this.escortedToursLink = view.escortedToursLink;
|
|
537
|
+
this.escortedToursTypeOffered = view.escortedToursTypeOffered;
|
|
538
|
+
this.virtualToursLink = view.virtualToursLink;
|
|
539
|
+
this.virtualToursTypeOffered = view.virtualToursTypeOffered;
|
|
540
|
+
this.tourTypeOptions = view.tourTypeOptions.map((o) => ({
|
|
541
|
+
label: o.label,
|
|
542
|
+
value: o.value,
|
|
543
|
+
}));
|
|
544
|
+
this.hasDynamicSchedulingEnabled = view.usesDynamicScheduling;
|
|
545
|
+
} catch (e) {
|
|
546
|
+
// eslint-disable-next-line no-console
|
|
547
|
+
console.error("Failed to load building config for selection", e);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
await this._loadForCurrentBuilding();
|
|
551
|
+
|
|
552
|
+
if (!this.shouldAllowScheduling || this.errorGettingAvailabilities) {
|
|
553
|
+
this.statusMessage = `No tour availabilities for ${building.name}.`;
|
|
554
|
+
} else {
|
|
555
|
+
const count = Object.values(this.shouldShowTourType).filter(
|
|
556
|
+
Boolean
|
|
557
|
+
).length;
|
|
558
|
+
this.statusMessage = `Showing tours for ${
|
|
559
|
+
building.name
|
|
560
|
+
}. ${count} tour type${count === 1 ? "" : "s"} available.`;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
buildingSelector(): TemplateResult | string {
|
|
565
|
+
if (this.centralConvoBuildings.length <= 1) return "";
|
|
566
|
+
return html`<h2 class="journey-header">Building</h2>
|
|
567
|
+
<me-select
|
|
568
|
+
id="building"
|
|
569
|
+
placeholder="Select a property"
|
|
570
|
+
.value=${this.buildingSlug}
|
|
571
|
+
.options="${this.centralConvoBuildings.map((b) => ({
|
|
572
|
+
label: b.name,
|
|
573
|
+
value: b.slug,
|
|
574
|
+
}))}"
|
|
575
|
+
@change=${this._onBuildingChange}
|
|
576
|
+
>
|
|
577
|
+
</me-select>`;
|
|
578
|
+
}
|
|
579
|
+
|
|
417
580
|
protected willUpdate = async (
|
|
418
581
|
_changedProperties:
|
|
419
582
|
| PropertyValueMap<{ tourType: TourType }>
|
|
@@ -652,6 +815,8 @@ export class TourScheduler extends LitElement {
|
|
|
652
815
|
this.leadSourceInputValue = parsedLeadSource;
|
|
653
816
|
const selectedLayout =
|
|
654
817
|
this.selectedLayoutEl?.value || this.selectedLayoutValue || null;
|
|
818
|
+
const selectedUnit =
|
|
819
|
+
this.selectedUnitEl?.value || this.selectedUnitValue || null;
|
|
655
820
|
pushGtmEvent("scheduleTourSubmitted", {
|
|
656
821
|
email: this.email,
|
|
657
822
|
phone: `+1${this.phoneNumber.match(/\d/g)?.join("")}`,
|
|
@@ -662,6 +827,7 @@ export class TourScheduler extends LitElement {
|
|
|
662
827
|
originatingSource:
|
|
663
828
|
leadSources.find((i) => i !== "property-website") || null,
|
|
664
829
|
layout: selectedLayout,
|
|
830
|
+
unit: selectedUnit,
|
|
665
831
|
});
|
|
666
832
|
const data = {
|
|
667
833
|
referrer: document.referrer,
|
|
@@ -683,6 +849,7 @@ export class TourScheduler extends LitElement {
|
|
|
683
849
|
query_params: Object.fromEntries(queryParams.entries()),
|
|
684
850
|
conversation_tracking_id: this.leadSourceClient?.chatId,
|
|
685
851
|
layouts: selectedLayout ? [selectedLayout] : undefined,
|
|
852
|
+
unit_numbers: selectedUnit ? [selectedUnit] : undefined,
|
|
686
853
|
lead_sources_with_timestamps: (
|
|
687
854
|
this.leadSourceMultitouchClient?.getSafeLeadSourceTouchpointsWithDefault(
|
|
688
855
|
{
|
|
@@ -773,7 +940,8 @@ export class TourScheduler extends LitElement {
|
|
|
773
940
|
static styles = [tourSchedulerStyles, InputStyles];
|
|
774
941
|
|
|
775
942
|
tourTypeMenu(): TemplateResult {
|
|
776
|
-
return html
|
|
943
|
+
return html`${this.buildingSelector()}
|
|
944
|
+
<h2 id="tour-type-heading" class="journey-header">Tour Type</h2>
|
|
777
945
|
<div
|
|
778
946
|
id="tour-type-menu"
|
|
779
947
|
role="radiogroup"
|
|
@@ -1247,6 +1415,21 @@ export class TourScheduler extends LitElement {
|
|
|
1247
1415
|
@change=${() => {
|
|
1248
1416
|
const v = this.selectedLayoutEl?.value || "";
|
|
1249
1417
|
this.selectedLayoutValue = v;
|
|
1418
|
+
// Clear unit selection if it no longer matches the new layout.
|
|
1419
|
+
if (v && this.selectedUnitValue) {
|
|
1420
|
+
const unit = this.units.find(
|
|
1421
|
+
(u) => u.unitNumber === this.selectedUnitValue
|
|
1422
|
+
);
|
|
1423
|
+
if (
|
|
1424
|
+
unit &&
|
|
1425
|
+
layoutValueToCanonical(unit.numberOfBedrooms * 10) !== v
|
|
1426
|
+
) {
|
|
1427
|
+
this.selectedUnitValue = "";
|
|
1428
|
+
if (this.selectedUnitEl) {
|
|
1429
|
+
this.selectedUnitEl.value = undefined;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1250
1433
|
if (v) {
|
|
1251
1434
|
pushGtmEvent("tourLayoutSelected", {
|
|
1252
1435
|
layout: v,
|
|
@@ -1257,6 +1440,25 @@ export class TourScheduler extends LitElement {
|
|
|
1257
1440
|
>
|
|
1258
1441
|
</me-select>`
|
|
1259
1442
|
: ""}
|
|
1443
|
+
${this.allowOccupiedUnitTours && this._getUnitOptions().length > 0
|
|
1444
|
+
? html` <me-select
|
|
1445
|
+
id="unit"
|
|
1446
|
+
placeholder="Unit preference (optional)"
|
|
1447
|
+
.value=${this.selectedUnitValue}
|
|
1448
|
+
.options="${this._getUnitOptions()}"
|
|
1449
|
+
@change=${() => {
|
|
1450
|
+
const v = this.selectedUnitEl?.value || "";
|
|
1451
|
+
this.selectedUnitValue = v;
|
|
1452
|
+
if (v) {
|
|
1453
|
+
pushGtmEvent("tourUnitSelected", {
|
|
1454
|
+
unit: v,
|
|
1455
|
+
buildingId: this.buildingId,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}}
|
|
1459
|
+
>
|
|
1460
|
+
</me-select>`
|
|
1461
|
+
: ""}
|
|
1260
1462
|
${this.leadSources.length > 0 &&
|
|
1261
1463
|
(this.featureFlagShowDropdown === FeatureFlagsShowDropdown.always ||
|
|
1262
1464
|
(this.featureFlagShowDropdown ===
|
|
@@ -1368,6 +1570,15 @@ export class TourScheduler extends LitElement {
|
|
|
1368
1570
|
}
|
|
1369
1571
|
|
|
1370
1572
|
render(): TemplateResult {
|
|
1573
|
+
return html`
|
|
1574
|
+
<div class="sr-only" role="status" aria-live="polite">
|
|
1575
|
+
${this.statusMessage}
|
|
1576
|
+
</div>
|
|
1577
|
+
${this.renderBody()}
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private renderBody(): TemplateResult {
|
|
1371
1582
|
const isLoading =
|
|
1372
1583
|
this.waitingForAvailabilities || this.shouldAllowScheduleLoading;
|
|
1373
1584
|
if (!this.shouldAllowScheduling && !isLoading) {
|
|
@@ -111,6 +111,17 @@ export const tourSchedulerStyles = css`
|
|
|
111
111
|
font-size: 14px;
|
|
112
112
|
font-weight: 600;
|
|
113
113
|
}
|
|
114
|
+
.sr-only {
|
|
115
|
+
position: absolute;
|
|
116
|
+
width: 1px;
|
|
117
|
+
height: 1px;
|
|
118
|
+
padding: 0;
|
|
119
|
+
margin: -1px;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
clip: rect(0, 0, 0, 0);
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
border: 0;
|
|
124
|
+
}
|
|
114
125
|
#close-button {
|
|
115
126
|
background: none;
|
|
116
127
|
border: none;
|
|
@@ -138,6 +149,11 @@ export const tourSchedulerStyles = css`
|
|
|
138
149
|
padding-bottom: 12px;
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
me-select#building {
|
|
153
|
+
display: block;
|
|
154
|
+
margin-bottom: 20px;
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
#tour-type-menu-outer-container {
|
|
142
158
|
width: 220px;
|
|
143
159
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { LogType, sendLoggingEvent } from "./analytics";
|
|
3
|
+
import { BASE_DOMAIN } from "./globals";
|
|
4
|
+
import { camelize } from "./utils";
|
|
5
|
+
|
|
6
|
+
export type UnitSummary = {
|
|
7
|
+
id: string;
|
|
8
|
+
unitNumber: string;
|
|
9
|
+
numberOfBedrooms: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type FetchBuildingUnitsSummaryParams = {
|
|
13
|
+
buildingSlug: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const fetchBuildingUnitsSummary = async ({
|
|
17
|
+
buildingSlug,
|
|
18
|
+
}: FetchBuildingUnitsSummaryParams): Promise<UnitSummary[]> => {
|
|
19
|
+
try {
|
|
20
|
+
const response = await axios.get(
|
|
21
|
+
`${BASE_DOMAIN}/platformApi/webchat/${buildingSlug}/units/summary`
|
|
22
|
+
);
|
|
23
|
+
if (response.data) {
|
|
24
|
+
return camelize<UnitSummary[]>(response.data);
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
sendLoggingEvent({
|
|
28
|
+
logType: LogType.error,
|
|
29
|
+
buildingSlug,
|
|
30
|
+
logTitle: "[ERROR_GETTING_UNITS_SUMMARY]",
|
|
31
|
+
logData: { error },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default fetchBuildingUnitsSummary;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
export interface CentralConvoBuilding {
|
|
4
|
+
building_id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch the buildings that belong to the same central conversation group as the
|
|
11
|
+
* current building. Returns an empty array when the building is not part of a
|
|
12
|
+
* central conversation group (in which case no building selector is shown).
|
|
13
|
+
*
|
|
14
|
+
* @param orgSlug - The org slug, e.g. "big-prop-co"
|
|
15
|
+
* @param buildingSlug - The current building's slug, e.g. "gravity-falls"
|
|
16
|
+
*/
|
|
17
|
+
export default async function fetchCentralConvoBuildings(
|
|
18
|
+
orgSlug: string,
|
|
19
|
+
buildingSlug: string
|
|
20
|
+
): Promise<CentralConvoBuilding[]> {
|
|
21
|
+
const host = "https://app.meetelise.com";
|
|
22
|
+
const url = `${host}/platformApi/webchat/central-convo-buildings`;
|
|
23
|
+
|
|
24
|
+
const response = await axios.get<CentralConvoBuilding[]>(url, {
|
|
25
|
+
headers: {
|
|
26
|
+
["building-slug"]: buildingSlug,
|
|
27
|
+
["org-slug"]: orgSlug,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return response.data ?? [];
|
|
32
|
+
}
|
|
@@ -10,4 +10,8 @@ export enum TourAvailabilityResponseRankOrderedSupportedTourTypesEnum {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
// Types are safe to re-export as they are not included in runtime code.
|
|
13
|
-
|
|
13
|
+
import type { TourAvailabilityResponse as SdkTourAvailabilityResponse } from "@meetelise/rest-sdk";
|
|
14
|
+
|
|
15
|
+
export type TourAvailabilityResponse = SdkTourAvailabilityResponse & {
|
|
16
|
+
allowOccupiedUnitTours?: boolean;
|
|
17
|
+
};
|