@meetelise/chat 1.13.9 → 1.14.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.
|
@@ -11,7 +11,8 @@ import "./time-picker.ts";
|
|
|
11
11
|
import "./me-select.ts";
|
|
12
12
|
import {
|
|
13
13
|
DateWithTimeZoneOffset,
|
|
14
|
-
|
|
14
|
+
getAvailabilitiesGroupedByDay,
|
|
15
|
+
getExistenceOfAvailabilitiesByTourType,
|
|
15
16
|
} from "../../getAvailabilities";
|
|
16
17
|
import { TourAvailabilityResponseRankOrderedSupportedTourTypesEnum } from "@meetelise/rest-sdk";
|
|
17
18
|
import { format } from "date-fns";
|
|
@@ -37,6 +38,12 @@ export class TourScheduler extends LitElement {
|
|
|
37
38
|
@state()
|
|
38
39
|
private tourType = TourType.Guided;
|
|
39
40
|
@state()
|
|
41
|
+
private shouldShowTourType = {
|
|
42
|
+
[TourType.Guided]: true,
|
|
43
|
+
[TourType.Self]: true,
|
|
44
|
+
[TourType.Virtual]: true,
|
|
45
|
+
};
|
|
46
|
+
@state()
|
|
40
47
|
private email = "";
|
|
41
48
|
@state()
|
|
42
49
|
private phoneNumber = "";
|
|
@@ -53,8 +60,10 @@ export class TourScheduler extends LitElement {
|
|
|
53
60
|
@state()
|
|
54
61
|
private tourIsBooked = false;
|
|
55
62
|
|
|
56
|
-
@query("input#
|
|
57
|
-
|
|
63
|
+
@query("input#firstName")
|
|
64
|
+
firstNameInput!: HTMLInputElement;
|
|
65
|
+
@query("input#lastName")
|
|
66
|
+
lastNameInput!: HTMLInputElement;
|
|
58
67
|
@query("input#email")
|
|
59
68
|
emailInput!: HTMLInputElement;
|
|
60
69
|
@query("input#phone")
|
|
@@ -63,9 +72,25 @@ export class TourScheduler extends LitElement {
|
|
|
63
72
|
unitTypeSelect!: MESelect;
|
|
64
73
|
|
|
65
74
|
firstUpdated = async (): Promise<void> => {
|
|
66
|
-
this.availabilitiesGroupedByDay = await
|
|
75
|
+
this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDay(
|
|
67
76
|
tourTypeMap[this.tourType]
|
|
68
77
|
);
|
|
78
|
+
|
|
79
|
+
// Show a tour type only if it is supported by the building and has
|
|
80
|
+
// time slots available.
|
|
81
|
+
const availabilitiesExistForTourType =
|
|
82
|
+
await getExistenceOfAvailabilitiesByTourType();
|
|
83
|
+
this.shouldShowTourType = {
|
|
84
|
+
[TourType.Guided]:
|
|
85
|
+
this.tourTypeOptions.map((o) => o.value).includes("WITH_AGENT") &&
|
|
86
|
+
availabilitiesExistForTourType[TourType.Guided],
|
|
87
|
+
[TourType.Self]:
|
|
88
|
+
this.tourTypeOptions.map((o) => o.value).includes("SELF_GUIDED") &&
|
|
89
|
+
availabilitiesExistForTourType[TourType.Self],
|
|
90
|
+
[TourType.Virtual]:
|
|
91
|
+
this.tourTypeOptions.map((o) => o.value).includes("VIRTUAL_SHOWING") &&
|
|
92
|
+
availabilitiesExistForTourType[TourType.Virtual],
|
|
93
|
+
};
|
|
69
94
|
};
|
|
70
95
|
|
|
71
96
|
protected willUpdate = async (
|
|
@@ -74,8 +99,9 @@ export class TourScheduler extends LitElement {
|
|
|
74
99
|
| Map<PropertyKey, unknown>
|
|
75
100
|
): Promise<void> => {
|
|
76
101
|
if (_changedProperties.has("tourType")) {
|
|
77
|
-
this.availabilitiesGroupedByDay =
|
|
78
|
-
|
|
102
|
+
this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDay(
|
|
103
|
+
tourTypeMap[this.tourType]
|
|
104
|
+
);
|
|
79
105
|
}
|
|
80
106
|
};
|
|
81
107
|
|
|
@@ -235,7 +261,7 @@ export class TourScheduler extends LitElement {
|
|
|
235
261
|
dateAndTime: (): boolean => !!this.selectedDate && !!this.selectedTime,
|
|
236
262
|
leadInfo: (): boolean => {
|
|
237
263
|
return (
|
|
238
|
-
!!this.
|
|
264
|
+
(!!this.firstNameInput?.value || !!this.lastNameInput?.value) &&
|
|
239
265
|
this.emailInput?.value.includes("@") &&
|
|
240
266
|
// TODO: deleting phone number doesn't cause validation to fail, at least on mobile
|
|
241
267
|
!!this.phoneNumber &&
|
|
@@ -272,9 +298,8 @@ export class TourScheduler extends LitElement {
|
|
|
272
298
|
email_address: this.email,
|
|
273
299
|
phone_number: `+1${this.phoneNumber.match(/\d/g)?.join("")}`, // e.g. +12125555555
|
|
274
300
|
building_id: this.buildingId,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
last_name: this.nameInput.value.split(" ").slice(1).join(" "),
|
|
301
|
+
first_name: this.firstNameInput.value,
|
|
302
|
+
last_name: this.lastNameInput.value,
|
|
278
303
|
tour_type: tourTypeForSubmission[this.tourType],
|
|
279
304
|
tour_time: `${this.selectedTime.datetime}${this.selectedTime.offset}`, // e.g., "2022-06-27T09:00:00-07:00"
|
|
280
305
|
};
|
|
@@ -474,6 +499,15 @@ export class TourScheduler extends LitElement {
|
|
|
474
499
|
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
|
|
475
500
|
}
|
|
476
501
|
|
|
502
|
+
button#schedule:not(:disabled):hover {
|
|
503
|
+
opacity: 0.7;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
button#schedule:not(:disabled):active {
|
|
507
|
+
box-shadow: none;
|
|
508
|
+
opacity: 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
477
511
|
button#schedule:disabled {
|
|
478
512
|
background: #e7e7e7;
|
|
479
513
|
box-shadow: none;
|
|
@@ -604,9 +638,8 @@ export class TourScheduler extends LitElement {
|
|
|
604
638
|
tourTypeMenu(): TemplateResult {
|
|
605
639
|
return html`<h2 id="tourType">Tour Type</h2>
|
|
606
640
|
<div id="tourTypeMenu">
|
|
607
|
-
${this.
|
|
641
|
+
${this.shouldShowTourType[TourType.Guided]
|
|
608
642
|
? html` <tour-type-option
|
|
609
|
-
tourtype="guided"
|
|
610
643
|
heading="Guided tour"
|
|
611
644
|
subtitle="with an agent"
|
|
612
645
|
@click="${() => {
|
|
@@ -637,9 +670,8 @@ export class TourScheduler extends LitElement {
|
|
|
637
670
|
</svg>
|
|
638
671
|
</tour-type-option>`
|
|
639
672
|
: ""}
|
|
640
|
-
${this.
|
|
673
|
+
${this.shouldShowTourType[TourType.Self]
|
|
641
674
|
? html`<tour-type-option
|
|
642
|
-
tourtype="self"
|
|
643
675
|
heading="Take a tour"
|
|
644
676
|
subtitle="on your own"
|
|
645
677
|
@click="${() => {
|
|
@@ -670,9 +702,8 @@ export class TourScheduler extends LitElement {
|
|
|
670
702
|
</svg>
|
|
671
703
|
</tour-type-option>`
|
|
672
704
|
: ""}
|
|
673
|
-
${this.
|
|
705
|
+
${this.shouldShowTourType[TourType.Virtual]
|
|
674
706
|
? html`<tour-type-option
|
|
675
|
-
tourtype="guided"
|
|
676
707
|
heading="Virtual tour"
|
|
677
708
|
subtitle="over video"
|
|
678
709
|
@click="${() => {
|
|
@@ -741,7 +772,8 @@ export class TourScheduler extends LitElement {
|
|
|
741
772
|
.map((date) => format(new Date(date.datetime), "h:mmaaa"))
|
|
742
773
|
.indexOf(e.target.selectedTime)
|
|
743
774
|
: null;
|
|
744
|
-
this.selectedTime =
|
|
775
|
+
this.selectedTime =
|
|
776
|
+
index !== null ? daysAvailabilities[index] : null;
|
|
745
777
|
}
|
|
746
778
|
}}
|
|
747
779
|
></time-picker>
|
|
@@ -811,8 +843,14 @@ export class TourScheduler extends LitElement {
|
|
|
811
843
|
<div id="yourInformationMenu">
|
|
812
844
|
<input
|
|
813
845
|
type="text"
|
|
814
|
-
placeholder="
|
|
815
|
-
id="
|
|
846
|
+
placeholder="First name"
|
|
847
|
+
id="firstName"
|
|
848
|
+
@input=${() => this.requestUpdate()}
|
|
849
|
+
/>
|
|
850
|
+
<input
|
|
851
|
+
type="text"
|
|
852
|
+
placeholder="Last name"
|
|
853
|
+
id="lastName"
|
|
816
854
|
@input=${() => this.requestUpdate()}
|
|
817
855
|
/>
|
|
818
856
|
<input
|
|
@@ -842,25 +880,11 @@ export class TourScheduler extends LitElement {
|
|
|
842
880
|
}}
|
|
843
881
|
/>
|
|
844
882
|
</div>
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
<me-select
|
|
851
|
-
id="unitType"
|
|
852
|
-
placeholder="Select type"
|
|
853
|
-
.options="${this.layoutOptions.map((o) => o.label)}"
|
|
854
|
-
defaultOption="Studio"
|
|
855
|
-
@change=${() => {
|
|
856
|
-
// to revalidate the form
|
|
857
|
-
this.requestUpdate();
|
|
858
|
-
}}
|
|
859
|
-
>Studio
|
|
860
|
-
</me-select>
|
|
861
|
-
</div>
|
|
862
|
-
</div>`
|
|
863
|
-
: ""} `;
|
|
883
|
+
<!--
|
|
884
|
+
Layout dropdown would go here, but has been removed pending backend support.
|
|
885
|
+
Here is the code to add it back:
|
|
886
|
+
https://github.com/MeetElise/chat-ui/blob/e17aca8b39a0eed9430f22c182f2ebcdfb796417/src/WebComponent/Scheduler/tour-scheduler.ts#L846-L863
|
|
887
|
+
--> `;
|
|
864
888
|
}
|
|
865
889
|
|
|
866
890
|
confirmationMessage(): TemplateResult {
|
|
@@ -887,7 +911,13 @@ export class TourScheduler extends LitElement {
|
|
|
887
911
|
<p>
|
|
888
912
|
Thank you!
|
|
889
913
|
<br />
|
|
890
|
-
Your
|
|
914
|
+
Your
|
|
915
|
+
${{
|
|
916
|
+
[TourType.Guided]: "guided",
|
|
917
|
+
[TourType.Self]: "self-guided",
|
|
918
|
+
[TourType.Virtual]: "virtual",
|
|
919
|
+
}[this.tourType]}
|
|
920
|
+
tour is scheduled for ${readableDateAndTime}.
|
|
891
921
|
</p>
|
|
892
922
|
<p>
|
|
893
923
|
Look for an email confirmation along with instructions and directions.
|
|
@@ -955,8 +985,6 @@ export enum TourType {
|
|
|
955
985
|
Virtual,
|
|
956
986
|
}
|
|
957
987
|
|
|
958
|
-
// TODO: we have three UI options and five TourAvailabilityResponseRankOrderedSupportedTourTypesEnum values
|
|
959
|
-
// how should they map?
|
|
960
988
|
const tourTypeMap = {
|
|
961
989
|
[TourType.Guided]:
|
|
962
990
|
TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.WithAgent,
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { css, html, LitElement, TemplateResult } from "lit";
|
|
2
2
|
import { customElement, property } from "lit/decorators.js";
|
|
3
|
-
import { TourType } from "./tour-scheduler";
|
|
4
3
|
|
|
5
4
|
@customElement("tour-type-option")
|
|
6
5
|
export class TourTypeOption extends LitElement {
|
|
7
|
-
@property({ type: String })
|
|
8
|
-
tourtype = "";
|
|
9
6
|
@property({ type: String })
|
|
10
7
|
heading = "";
|
|
11
8
|
@property({ type: String })
|
|
12
9
|
subtitle = "";
|
|
13
10
|
@property({ type: Boolean })
|
|
14
11
|
selected = false;
|
|
15
|
-
|
|
16
12
|
@property({ attribute: false })
|
|
17
|
-
onClick?: (
|
|
13
|
+
onClick?: () => void;
|
|
18
14
|
|
|
19
15
|
static styles = [
|
|
20
16
|
css`
|
|
@@ -46,7 +46,7 @@ export default function createConversation(
|
|
|
46
46
|
building.avatarType === "image" && building.avatarSrc
|
|
47
47
|
? avatarSrc
|
|
48
48
|
: defaultAvatarUrl,
|
|
49
|
-
// uncomment
|
|
49
|
+
// uncomment the following line to test changes to the default avatar if your test building has its own avatar
|
|
50
50
|
// avatarUrl: defaultAvatarUrl,
|
|
51
51
|
};
|
|
52
52
|
return conversation;
|
package/src/getAvailabilities.ts
CHANGED
|
@@ -10,31 +10,49 @@ import {
|
|
|
10
10
|
TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
|
|
11
11
|
} from "@meetelise/rest-sdk";
|
|
12
12
|
import groupBy from "lodash/groupBy";
|
|
13
|
+
import { TourType } from "./WebComponent/Scheduler/tour-scheduler";
|
|
13
14
|
|
|
14
15
|
const availabilitiesCache: {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
buildingId?: number | null;
|
|
17
|
+
availabilities: { [buildingId: number]: TourAvailabilityResponse };
|
|
18
|
+
} = { buildingId: null, availabilities: {} };
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Returns the raw availabilities.
|
|
22
|
+
*
|
|
23
|
+
* If no `buildingId` is provided, it will use a cached buildingId from the previous call.
|
|
24
|
+
* If there is none cached, it will throw an error.
|
|
25
|
+
*/
|
|
18
26
|
export const getRawAvailabilities = async (
|
|
19
|
-
buildingId
|
|
27
|
+
buildingId?: number
|
|
20
28
|
): Promise<TourAvailabilityResponse> => {
|
|
21
|
-
if (
|
|
22
|
-
|
|
29
|
+
if (buildingId) {
|
|
30
|
+
availabilitiesCache.buildingId = buildingId;
|
|
31
|
+
}
|
|
32
|
+
const buildingIdToUse = availabilitiesCache.buildingId;
|
|
33
|
+
if (!buildingIdToUse) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"No buildingId was provided to getRawAvailabilities and there is no buildingId cached."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const availabilities = availabilitiesCache.availabilities;
|
|
39
|
+
if (availabilities[buildingIdToUse]) {
|
|
40
|
+
return availabilities[buildingIdToUse];
|
|
23
41
|
}
|
|
24
42
|
const startTime = startOfToday();
|
|
25
43
|
const endTime = formatISO(endOfDay(addDays(startTime, 30)));
|
|
26
|
-
const url = `https://app.meetelise.com/api/pub/v1/buildings/${
|
|
44
|
+
const url = `https://app.meetelise.com/api/pub/v1/buildings/${buildingIdToUse}/tour/availabilities?startTime=${formatISO(
|
|
27
45
|
startTime
|
|
28
46
|
)}&endTime=${endTime}`;
|
|
29
47
|
const result = await axios.get<TourAvailabilityResponse>(url);
|
|
30
|
-
availabilitiesCache[
|
|
48
|
+
availabilitiesCache.availabilities[buildingIdToUse] = result.data;
|
|
31
49
|
// The endpoint INCORRECTLY states that these are returned as dates. They are, in fact, strings.
|
|
32
50
|
return result.data;
|
|
33
51
|
};
|
|
34
52
|
|
|
35
53
|
export const getAvailabilitiesForTourType = async (
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
|
|
55
|
+
buildingId?: number
|
|
38
56
|
): Promise<{
|
|
39
57
|
/**
|
|
40
58
|
*
|
|
@@ -68,13 +86,46 @@ export interface DateWithTimeZoneOffset {
|
|
|
68
86
|
offset: string;
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Returns an object that reveals whether each tour type supported by
|
|
91
|
+
* `tour-scheduler` has availabilities (time slots available for scheduling) in
|
|
92
|
+
* the time window of interest.
|
|
93
|
+
*
|
|
94
|
+
* Note that the existence of current availabilities is distinct from the
|
|
95
|
+
* question of whether the community supports the tour type at all. The first
|
|
96
|
+
* implies the second but not vice versa.
|
|
97
|
+
*/
|
|
98
|
+
export const getExistenceOfAvailabilitiesByTourType = async (): Promise<{
|
|
99
|
+
[TourType.Guided]: boolean;
|
|
100
|
+
[TourType.Self]: boolean;
|
|
101
|
+
[TourType.Virtual]: boolean;
|
|
102
|
+
}> => {
|
|
103
|
+
return {
|
|
104
|
+
[TourType.Guided]: !!(
|
|
105
|
+
await getAvailabilitiesForTourType(
|
|
106
|
+
TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.WithAgent
|
|
107
|
+
)
|
|
108
|
+
)?.availableTourStartTimes?.length,
|
|
109
|
+
[TourType.Self]: !!(
|
|
110
|
+
await getAvailabilitiesForTourType(
|
|
111
|
+
TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.SelfGuided
|
|
112
|
+
)
|
|
113
|
+
)?.availableTourStartTimes?.length,
|
|
114
|
+
[TourType.Virtual]: !!(
|
|
115
|
+
await getAvailabilitiesForTourType(
|
|
116
|
+
TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.VirtualShowing
|
|
117
|
+
)
|
|
118
|
+
)?.availableTourStartTimes?.length,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
71
122
|
export const getAvailabilitiesGroupedByDay = async (
|
|
72
|
-
|
|
73
|
-
|
|
123
|
+
tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
|
|
124
|
+
buildingId?: number
|
|
74
125
|
): Promise<{ [day: string]: DateWithTimeZoneOffset[] }> => {
|
|
75
126
|
const availabilitiesForTourTypeRaw = await getAvailabilitiesForTourType(
|
|
76
|
-
|
|
77
|
-
|
|
127
|
+
tourType,
|
|
128
|
+
buildingId
|
|
78
129
|
);
|
|
79
130
|
if (!availabilitiesForTourTypeRaw) {
|
|
80
131
|
return {};
|
|
@@ -97,16 +148,6 @@ export const getAvailabilitiesGroupedByDay = async (
|
|
|
97
148
|
);
|
|
98
149
|
};
|
|
99
150
|
|
|
100
|
-
// TODO: if cache is empty, observe it and wait for it to be filled in.
|
|
101
|
-
// TODO: alternative to this: cache the building id when getRawAvailabilities is called. Then I can call the normal methods.
|
|
102
|
-
export const getAvailabilitiesGroupedByDayCached = async (
|
|
103
|
-
tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum
|
|
104
|
-
): Promise<{ [day: string]: DateWithTimeZoneOffset[] }> =>
|
|
105
|
-
getAvailabilitiesGroupedByDay(
|
|
106
|
-
Object.keys(availabilitiesCache).map(Number)[0],
|
|
107
|
-
tourType
|
|
108
|
-
);
|
|
109
|
-
|
|
110
151
|
/**
|
|
111
152
|
* Takes an ISO 8601 date string with time zone offset and returns
|
|
112
153
|
* an object of our custom type DateWithTimeZoneOffset.
|