@7365admin1/layer-common 4.0.4-staging.241 → 4.1.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.
@@ -0,0 +1,278 @@
1
+ <template>
2
+ <v-form v-model="formValid" @submit.prevent="submit">
3
+ <v-row no-gutters>
4
+ <v-col cols="12" class="mb-4">
5
+ <p class="text-caption text-medium-emphasis mb-2">Arrival Date</p>
6
+ <InputDatePicker v-model="visitor.expectedCheckIn" placeholder="DD/MM/YYYY" :rules="[requiredRule]" />
7
+ </v-col>
8
+
9
+ <v-col cols="12" class="mb-4">
10
+ <v-select
11
+ v-model="visitor.contractorType"
12
+ :items="contractorTypes"
13
+ item-title="title"
14
+ item-value="value"
15
+ label="Contractor Type"
16
+ density="comfortable"
17
+ variant="outlined"
18
+ hide-details
19
+ />
20
+ </v-col>
21
+
22
+ <v-col cols="12" class="mb-4">
23
+ <v-text-field
24
+ v-model="visitor.name"
25
+ label="Name"
26
+ density="comfortable"
27
+ variant="outlined"
28
+ :rules="[requiredRule]"
29
+ />
30
+ </v-col>
31
+
32
+ <v-col cols="12" class="mb-4">
33
+ <v-text-field
34
+ v-model="visitor.nric"
35
+ label="NRIC / Passport No. (Optional)"
36
+ density="comfortable"
37
+ variant="outlined"
38
+ hide-details
39
+ />
40
+ </v-col>
41
+
42
+ <v-col cols="12" class="mb-4">
43
+ <v-text-field
44
+ v-model="visitor.email"
45
+ type="email"
46
+ label="Email"
47
+ density="comfortable"
48
+ variant="outlined"
49
+ hide-details
50
+ />
51
+ </v-col>
52
+
53
+ <v-col cols="12" class="mb-1">
54
+ <InputPhoneNumberV2 v-model="visitor.contact" density="comfortable" :rules="[requiredRule]" />
55
+ </v-col>
56
+ <v-col v-if="isContactPickerSupported" cols="12" class="mb-4 text-right">
57
+ <a href="#" class="text-caption" @click.prevent="fillFromContactPicker">
58
+ or select contact in your phone book
59
+ </a>
60
+ </v-col>
61
+
62
+ <v-col cols="12" class="mb-4">
63
+ <PublicOnboardingLocationFields
64
+ :site="site"
65
+ v-model:block="visitor.block"
66
+ v-model:level="visitor.level"
67
+ v-model:unit="visitor.unit"
68
+ />
69
+ </v-col>
70
+
71
+ <v-col cols="12" class="mb-4">
72
+ <v-text-field
73
+ v-model="visitor.company"
74
+ label="Company Name"
75
+ density="comfortable"
76
+ variant="outlined"
77
+ :rules="[requiredRule]"
78
+ />
79
+ </v-col>
80
+
81
+ <v-col cols="12" class="mb-4">
82
+ <v-text-field
83
+ v-model="visitor.plateNumber"
84
+ label="Vehicle Number (Optional)"
85
+ density="comfortable"
86
+ variant="outlined"
87
+ hide-details
88
+ />
89
+ </v-col>
90
+
91
+ <v-col cols="12" class="mb-4">
92
+ <v-btn variant="outlined" block class="justify-space-between" append-icon="mdi-pencil-outline" @click="showMembersDialog = true">
93
+ {{ members.length > 0 ? `${members.length} Member(s) Added` : "Add Member" }}
94
+ </v-btn>
95
+ </v-col>
96
+
97
+ <v-col cols="12" class="mb-4">
98
+ <v-textarea
99
+ v-model="visitor.remarks"
100
+ label="Remarks (optional)"
101
+ density="comfortable"
102
+ variant="outlined"
103
+ rows="3"
104
+ auto-grow
105
+ />
106
+ </v-col>
107
+
108
+ <v-col v-if="submitError" cols="12" class="mb-4">
109
+ <v-alert type="error" variant="tonal" density="compact">{{ submitError }}</v-alert>
110
+ </v-col>
111
+
112
+ <v-col cols="12">
113
+ <v-btn color="primary" variant="flat" block size="large" :loading="submitting" :disabled="!formValid" type="submit">
114
+ Submit
115
+ </v-btn>
116
+ </v-col>
117
+ </v-row>
118
+
119
+ <v-dialog v-model="showMembersDialog" max-width="420" persistent>
120
+ <v-card class="pa-4">
121
+ <div class="d-flex align-center mb-4">
122
+ <v-btn icon variant="text" @click="showMembersDialog = false">
123
+ <v-icon>mdi-chevron-left</v-icon>
124
+ </v-btn>
125
+ <span class="text-subtitle-1 ml-2">Members</span>
126
+ </div>
127
+
128
+ <div
129
+ v-for="(member, index) in draftMembers"
130
+ :key="index"
131
+ class="pa-3 mb-3 bordered-field"
132
+ >
133
+ <v-text-field
134
+ v-model="member.name"
135
+ label="Name"
136
+ density="comfortable"
137
+ variant="outlined"
138
+ hide-details
139
+ class="mb-2"
140
+ />
141
+ <v-text-field
142
+ v-model="member.nric"
143
+ label="NRIC / Passport No. (Optional)"
144
+ density="comfortable"
145
+ variant="outlined"
146
+ hide-details
147
+ class="mb-2"
148
+ />
149
+ <InputPhoneNumberV2 v-model="member.contact" density="comfortable" hide-details />
150
+ </div>
151
+
152
+ <v-btn variant="outlined" block class="mb-3" prepend-icon="mdi-plus" @click="addDraftMember">
153
+ Add
154
+ </v-btn>
155
+ <v-btn color="primary" variant="flat" block @click="saveMembers">Save</v-btn>
156
+ </v-card>
157
+ </v-dialog>
158
+ </v-form>
159
+ </template>
160
+
161
+ <script lang="ts" setup>
162
+ const prop = defineProps({
163
+ site: { type: String, required: true },
164
+ });
165
+
166
+ const emit = defineEmits(["done"]);
167
+
168
+ const { createVisitor } = usePublicVisitorOnboarding();
169
+ const { isSupported: isContactPickerSupported, pickContact } = useContactPicker();
170
+
171
+ const requiredRule = (v: any) => (v !== null && v !== undefined && v !== "") || "This field is required";
172
+
173
+ const contractorTypes = [
174
+ { title: "Home Contractor", value: "home-contractor" },
175
+ { title: "House Mover", value: "house-mover" },
176
+ { title: "Estate Contractor", value: "estate-contractor" },
177
+ { title: "Property Agent", value: "property-agent" },
178
+ ];
179
+
180
+ const visitor = reactive({
181
+ expectedCheckIn: null as string | null,
182
+ contractorType: "",
183
+ name: "",
184
+ nric: "",
185
+ email: "",
186
+ contact: "",
187
+ block: "",
188
+ level: "",
189
+ unit: "",
190
+ company: "",
191
+ plateNumber: "",
192
+ remarks: "",
193
+ });
194
+
195
+ type TDraftMember = { name: string; nric: string; contact: string };
196
+ const members = ref<TDraftMember[]>([]);
197
+ const draftMembers = ref<TDraftMember[]>([]);
198
+ const showMembersDialog = ref(false);
199
+
200
+ watch(showMembersDialog, (open) => {
201
+ if (open) {
202
+ draftMembers.value = members.value.length
203
+ ? members.value.map((m) => ({ ...m }))
204
+ : [{ name: "", nric: "", contact: "" }];
205
+ }
206
+ });
207
+
208
+ function addDraftMember() {
209
+ draftMembers.value.push({ name: "", nric: "", contact: "" });
210
+ }
211
+
212
+ function saveMembers() {
213
+ members.value = draftMembers.value.filter((m) => m.name && m.contact);
214
+ showMembersDialog.value = false;
215
+ }
216
+
217
+ const formValid = ref(false);
218
+ const submitting = ref(false);
219
+ const submitError = ref("");
220
+
221
+ async function fillFromContactPicker() {
222
+ const contact = await pickContact();
223
+ if (!contact) return;
224
+ if (contact.name) visitor.name = contact.name;
225
+ if (contact.tel) visitor.contact = contact.tel;
226
+ }
227
+
228
+ async function submit() {
229
+ submitError.value = "";
230
+ submitting.value = true;
231
+ try {
232
+ const payload: Record<string, any> = {
233
+ type: "contractor",
234
+ contractorType: visitor.contractorType || undefined,
235
+ name: visitor.name,
236
+ contact: visitor.contact,
237
+ nric: visitor.nric || undefined,
238
+ email: visitor.email || undefined,
239
+ block: visitor.block || undefined,
240
+ level: visitor.level || undefined,
241
+ unit: visitor.unit || undefined,
242
+ company: visitor.company,
243
+ plateNumber: visitor.plateNumber || undefined,
244
+ remarks: visitor.remarks || undefined,
245
+ members: members.value.length ? members.value : undefined,
246
+ };
247
+
248
+ const res: any = await createVisitor(prop.site, payload);
249
+ emit("done", res?.status || "pending");
250
+ } catch (error: any) {
251
+ submitError.value =
252
+ error?.data?.message || error?.message || "Unable to submit your registration. Please try again.";
253
+ } finally {
254
+ submitting.value = false;
255
+ }
256
+ }
257
+ </script>
258
+
259
+ <style scoped>
260
+ /* Matches the surrounding v-text-field/InputPhoneNumberV2 fields' own
261
+ border color (Vuetify's border color/opacity token) - the per-member row
262
+ in the Members dialog isn't a real Vuetify field, so without this it was
263
+ the one inconsistent border on the form. */
264
+ .bordered-field {
265
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
266
+ border-radius: 4px;
267
+ }
268
+
269
+ /* InputDatePicker/InputPhoneNumberV2 hardcode `.filter-field` on their own
270
+ internal v-text-field/v-select, which sets border-radius: var(--r-inner)
271
+ globally (assets/css/screens.css) - overriding it just within this form
272
+ (including inside the teleported Members dialog; :deep()'s scoping
273
+ attribute stays on the element through teleport) rather than in the
274
+ shared components themselves, which are also used elsewhere. */
275
+ :deep(.filter-field .v-field) {
276
+ border-radius: 4px;
277
+ }
278
+ </style>
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <div>
3
+ <p class="text-caption text-medium-emphasis mb-2">Location</p>
4
+ <v-row no-gutters class="flex-nowrap">
5
+ <v-col cols="4" class="pr-1">
6
+ <v-autocomplete
7
+ v-model="block"
8
+ :items="blocksArray"
9
+ item-title="title"
10
+ item-value="value"
11
+ label="Block"
12
+ :loading="blocksLoading"
13
+ :rules="[requiredRule]"
14
+ density="comfortable"
15
+ variant="outlined"
16
+ hide-details="auto"
17
+ @update:model-value="onBlockChange"
18
+ />
19
+ </v-col>
20
+ <v-col cols="4" class="px-1">
21
+ <v-autocomplete
22
+ v-model="level"
23
+ :items="levelsArray"
24
+ item-title="title"
25
+ item-value="value"
26
+ label="Level"
27
+ :loading="levelsLoading"
28
+ :disabled="!block"
29
+ :rules="[requiredRule]"
30
+ density="comfortable"
31
+ variant="outlined"
32
+ hide-details="auto"
33
+ @update:model-value="onLevelChange"
34
+ />
35
+ </v-col>
36
+ <v-col cols="4" class="pl-1">
37
+ <v-autocomplete
38
+ v-model="unit"
39
+ :items="unitsArray"
40
+ item-title="title"
41
+ item-value="value"
42
+ label="Unit"
43
+ :loading="unitsLoading"
44
+ :disabled="!level"
45
+ :rules="[requiredRule]"
46
+ density="comfortable"
47
+ variant="outlined"
48
+ hide-details="auto"
49
+ />
50
+ </v-col>
51
+ </v-row>
52
+ </div>
53
+ </template>
54
+
55
+ <script lang="ts" setup>
56
+ const prop = defineProps({
57
+ site: { type: String, required: true },
58
+ });
59
+
60
+ const block = defineModel<string>("block", { default: "" });
61
+ const level = defineModel<string>("level", { default: "" });
62
+ const unit = defineModel<string>("unit", { default: "" });
63
+
64
+ const requiredRule = (v: any) => (v !== null && v !== undefined && v !== "") || "Required";
65
+
66
+ const { getBlocks, getLevels, getUnits } = usePublicVisitorOnboarding();
67
+
68
+ const blocksArray = ref<{ title: string; value: string }[]>([]);
69
+ const levelsArray = ref<{ title: string; value: string }[]>([]);
70
+ const unitsArray = ref<{ title: string; value: string }[]>([]);
71
+ const blocksLoading = ref(false);
72
+ const levelsLoading = ref(false);
73
+ const unitsLoading = ref(false);
74
+
75
+ function toArray(res: any) {
76
+ return Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
77
+ }
78
+
79
+ async function loadBlocks() {
80
+ blocksLoading.value = true;
81
+ try {
82
+ const res = await getBlocks(prop.site);
83
+ blocksArray.value = toArray(res).map((b: any) => ({ title: b.name, value: b._id }));
84
+ } finally {
85
+ blocksLoading.value = false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Takes the new value as an argument rather than reading `block.value` -
91
+ * this handler and `v-model="block"` are both bound to the same
92
+ * `update:model-value` event on the same element, and listener order
93
+ * between them isn't guaranteed, so `block.value` can still be the OLD
94
+ * value at the moment this runs.
95
+ */
96
+ async function onBlockChange(newBlock: string | null) {
97
+ level.value = "";
98
+ unit.value = "";
99
+ unitsArray.value = [];
100
+ levelsArray.value = [];
101
+ if (!newBlock) return;
102
+ levelsLoading.value = true;
103
+ try {
104
+ const res = await getLevels(prop.site, newBlock);
105
+ levelsArray.value = toArray(res).map((l: any) => ({ title: l.name, value: l._id }));
106
+ } finally {
107
+ levelsLoading.value = false;
108
+ }
109
+ }
110
+
111
+ async function onLevelChange(newLevel: string | null) {
112
+ unit.value = "";
113
+ unitsArray.value = [];
114
+ if (!block.value || !newLevel) return;
115
+ unitsLoading.value = true;
116
+ try {
117
+ const res = await getUnits(prop.site, block.value, newLevel);
118
+ unitsArray.value = toArray(res).map((u: any) => ({ title: u.name, value: u._id }));
119
+ } finally {
120
+ unitsLoading.value = false;
121
+ }
122
+ }
123
+
124
+ onMounted(loadBlocks);
125
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <v-card class="pa-0" elevation="2">
3
+ <v-card-title class="px-4 py-3">Self-service onboarding portal</v-card-title>
4
+ <v-divider />
5
+ <v-list class="pa-3">
6
+ <v-list-item
7
+ v-for="option in options"
8
+ :key="option.value"
9
+ class="mb-2 rounded-lg"
10
+ style="border: 1px solid rgba(128,128,128,0.3)"
11
+ @click="emit('select', option.value)"
12
+ >
13
+ <template #prepend>
14
+ <v-avatar color="grey-darken-3" class="mr-3">
15
+ <v-icon color="white">{{ option.icon }}</v-icon>
16
+ </v-avatar>
17
+ </template>
18
+ <v-list-item-title>{{ option.label }}</v-list-item-title>
19
+ <template #append>
20
+ <v-icon>mdi-chevron-right</v-icon>
21
+ </template>
22
+ </v-list-item>
23
+ </v-list>
24
+ </v-card>
25
+ </template>
26
+
27
+ <script lang="ts" setup>
28
+ const emit = defineEmits(["select"]);
29
+
30
+ const options = [
31
+ { label: "Visitor", value: "guest", icon: "mdi-walk" },
32
+ { label: "Contractor", value: "contractor", icon: "mdi-account-hard-hat-outline" },
33
+ ];
34
+ </script>