@7365admin1/layer-common 4.0.4-staging.240 → 4.0.4-staging.242

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,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>
@@ -0,0 +1,273 @@
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">Visitor Arrival</p>
6
+ <InputDatePicker v-model="visitor.expectedCheckIn" placeholder="DD/MM/YYYY" :rules="[requiredRule]" />
7
+ </v-col>
8
+
9
+ <v-col cols="6" class="mb-4 pr-1">
10
+ <v-text-field
11
+ v-model="visitor.arrivalTime"
12
+ type="time"
13
+ label="Time"
14
+ density="comfortable"
15
+ variant="outlined"
16
+ hide-details
17
+ />
18
+ </v-col>
19
+ <v-col cols="6" class="mb-4 pl-1">
20
+ <v-text-field
21
+ v-model.number="visitor.duration"
22
+ type="number"
23
+ min="0"
24
+ label="Duration Hour(s)"
25
+ density="comfortable"
26
+ variant="outlined"
27
+ hide-details
28
+ />
29
+ </v-col>
30
+
31
+ <v-col cols="12" class="mb-2">
32
+ <p class="text-caption text-medium-emphasis">Visitor Information</p>
33
+ </v-col>
34
+
35
+ <v-col cols="12" class="mb-4">
36
+ <v-text-field
37
+ v-model="visitor.name"
38
+ label="Name"
39
+ density="comfortable"
40
+ variant="outlined"
41
+ :rules="[requiredRule]"
42
+ />
43
+ </v-col>
44
+
45
+ <v-col cols="12" class="mb-1">
46
+ <InputPhoneNumberV2 v-model="visitor.contact" density="comfortable" :rules="[requiredRule]" />
47
+ </v-col>
48
+ <v-col v-if="isContactPickerSupported" cols="12" class="mb-4 text-right">
49
+ <a href="#" class="text-caption" @click.prevent="fillFromContactPicker">
50
+ or select contact in your phone book
51
+ </a>
52
+ </v-col>
53
+
54
+ <v-col cols="12" class="mb-4">
55
+ <PublicOnboardingLocationFields
56
+ :site="site"
57
+ v-model:block="visitor.block"
58
+ v-model:level="visitor.level"
59
+ v-model:unit="visitor.unit"
60
+ />
61
+ </v-col>
62
+
63
+ <v-col cols="12" class="mb-4">
64
+ <v-text-field
65
+ v-model="visitor.email"
66
+ type="email"
67
+ label="Email"
68
+ density="comfortable"
69
+ variant="outlined"
70
+ hide-details
71
+ />
72
+ </v-col>
73
+
74
+ <v-col cols="12" class="mb-4">
75
+ <div class="d-flex align-center justify-space-between pa-3 bordered-field">
76
+ <span>Overnight Parking</span>
77
+ <v-switch
78
+ :model-value="visitor.isOvernightParking"
79
+ color="primary"
80
+ base-color="grey-lighten-1"
81
+ hide-details
82
+ @update:model-value="onToggleOvernightParking"
83
+ />
84
+ </div>
85
+ </v-col>
86
+
87
+ <v-col cols="12" class="mb-4">
88
+ <div class="d-flex align-center justify-space-between pa-3 bordered-field">
89
+ <span>No. of passenger (Optional)</span>
90
+ <div class="d-flex align-center ga-3">
91
+ <v-btn icon size="small" variant="outlined" :disabled="!visitor.numberOfPassengers" @click="visitor.numberOfPassengers = Math.max(0, (visitor.numberOfPassengers || 0) - 1)">
92
+ <v-icon>mdi-minus</v-icon>
93
+ </v-btn>
94
+ <span>{{ visitor.numberOfPassengers || 0 }}</span>
95
+ <v-btn icon size="small" variant="outlined" @click="visitor.numberOfPassengers = (visitor.numberOfPassengers || 0) + 1">
96
+ <v-icon>mdi-plus</v-icon>
97
+ </v-btn>
98
+ </div>
99
+ </div>
100
+ </v-col>
101
+
102
+ <v-col cols="12" class="mb-4">
103
+ <v-textarea
104
+ v-model="visitor.purpose"
105
+ label="Enter purpose of visit"
106
+ density="comfortable"
107
+ variant="outlined"
108
+ rows="4"
109
+ auto-grow
110
+ />
111
+ </v-col>
112
+
113
+ <v-col v-if="submitError" cols="12" class="mb-4">
114
+ <v-alert type="error" variant="tonal" density="compact">{{ submitError }}</v-alert>
115
+ </v-col>
116
+
117
+ <v-col cols="12">
118
+ <v-btn color="primary" variant="flat" block size="large" :loading="submitting" :disabled="!formValid" type="submit">
119
+ Submit
120
+ </v-btn>
121
+ </v-col>
122
+ </v-row>
123
+
124
+ <v-dialog v-model="showOvernightDialog" max-width="420" persistent>
125
+ <v-card class="pa-6">
126
+ <p class="text-body-1 text-center mb-4">
127
+ Overnight Parking requests outside our below business hours will not
128
+ be guaranteed a response
129
+ </p>
130
+ <div class="mb-4">
131
+ <div v-for="day in overnightScheduleDisplay" :key="day.label" class="d-flex justify-space-between text-body-2 py-1">
132
+ <span class="text-capitalize">{{ day.label }}</span>
133
+ <span>{{ day.text }}</span>
134
+ </div>
135
+ </div>
136
+ <v-row no-gutters class="ga-2">
137
+ <v-col>
138
+ <v-btn color="success" variant="tonal" block @click="confirmOvernightParking(true)">Yes</v-btn>
139
+ </v-col>
140
+ <v-col>
141
+ <v-btn color="error" variant="tonal" block @click="confirmOvernightParking(false)">No</v-btn>
142
+ </v-col>
143
+ </v-row>
144
+ </v-card>
145
+ </v-dialog>
146
+ </v-form>
147
+ </template>
148
+
149
+ <script lang="ts" setup>
150
+ const prop = defineProps({
151
+ site: { type: String, required: true },
152
+ });
153
+
154
+ const emit = defineEmits(["done"]);
155
+
156
+ const { createVisitor, getOvernightParkingHours } = usePublicVisitorOnboarding();
157
+ const { isSupported: isContactPickerSupported, pickContact } = useContactPicker();
158
+
159
+ const requiredRule = (v: any) => (v !== null && v !== undefined && v !== "") || "This field is required";
160
+
161
+ const visitor = reactive({
162
+ expectedCheckIn: null as string | null,
163
+ arrivalTime: "",
164
+ duration: null as number | null,
165
+ name: "",
166
+ contact: "",
167
+ block: "",
168
+ level: "",
169
+ unit: "",
170
+ email: "",
171
+ isOvernightParking: false,
172
+ numberOfPassengers: 0,
173
+ purpose: "",
174
+ });
175
+
176
+ const formValid = ref(false);
177
+ const submitting = ref(false);
178
+ const submitError = ref("");
179
+
180
+ async function fillFromContactPicker() {
181
+ const contact = await pickContact();
182
+ if (!contact) return;
183
+ if (contact.name) visitor.name = contact.name;
184
+ if (contact.tel) visitor.contact = contact.tel;
185
+ }
186
+
187
+ const DAY_LABELS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
188
+ const overnightSchedule = ref<Record<string, any>>({});
189
+ const showOvernightDialog = ref(false);
190
+
191
+ const overnightScheduleDisplay = computed(() =>
192
+ DAY_LABELS.map((day) => {
193
+ const d = overnightSchedule.value[day];
194
+ return {
195
+ label: day,
196
+ text: d?.isEnabled && d?.startTime && d?.endTime ? `${d.startTime} - ${d.endTime}` : "Closed",
197
+ };
198
+ }),
199
+ );
200
+
201
+ async function onToggleOvernightParking(value: boolean | null) {
202
+ if (!value) {
203
+ visitor.isOvernightParking = false;
204
+ return;
205
+ }
206
+ try {
207
+ const res: any = await getOvernightParkingHours(prop.site);
208
+ overnightSchedule.value = res?.data ?? res ?? {};
209
+ } catch {
210
+ overnightSchedule.value = {};
211
+ }
212
+ showOvernightDialog.value = true;
213
+ }
214
+
215
+ function confirmOvernightParking(confirmed: boolean) {
216
+ visitor.isOvernightParking = confirmed;
217
+ showOvernightDialog.value = false;
218
+ }
219
+
220
+ async function submit() {
221
+ submitError.value = "";
222
+ submitting.value = true;
223
+ try {
224
+ const payload: Record<string, any> = {
225
+ type: "guest",
226
+ name: visitor.name,
227
+ contact: visitor.contact,
228
+ email: visitor.email || undefined,
229
+ block: visitor.block || undefined,
230
+ level: visitor.level || undefined,
231
+ unit: visitor.unit || undefined,
232
+ expectedCheckIn: visitor.expectedCheckIn || undefined,
233
+ arrivalTime: visitor.arrivalTime || undefined,
234
+ duration: visitor.duration ?? undefined,
235
+ isOvernightParking: visitor.isOvernightParking,
236
+ numberOfPassengers: visitor.numberOfPassengers || undefined,
237
+ purpose: visitor.purpose || undefined,
238
+ };
239
+
240
+ const res: any = await createVisitor(prop.site, payload);
241
+ emit("done", res?.status || "pending", res?._id);
242
+ } catch (error: any) {
243
+ submitError.value =
244
+ error?.data?.message || error?.message || "Unable to submit your registration. Please try again.";
245
+ } finally {
246
+ submitting.value = false;
247
+ }
248
+ }
249
+ </script>
250
+
251
+ <style scoped>
252
+ /* Matches the surrounding v-text-field/v-select "outlined" variant's own
253
+ border exactly (Vuetify's field border color/opacity/radius tokens),
254
+ rather than an approximated color - these two rows aren't real Vuetify
255
+ fields, so without this they were the only inconsistent borders on the
256
+ form (one had a mismatched color/radius, the other had none at all). */
257
+ .bordered-field {
258
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
259
+ border-radius: 4px;
260
+ }
261
+
262
+ /* InputDatePicker/InputPhoneNumberV2 hardcode `.filter-field` on their own
263
+ internal v-text-field/v-select, independent of anything on this form's
264
+ own markup - that class sets border-radius: var(--r-inner) globally
265
+ (assets/css/screens.css), which is why they never matched the other
266
+ plain fields' 4px default no matter what class this component's own
267
+ elements had. Scoped :deep() here targets just their rendering within
268
+ this form (not the shared components' other usages elsewhere in the
269
+ app) with higher specificity than that global rule. */
270
+ :deep(.filter-field .v-field) {
271
+ border-radius: 4px;
272
+ }
273
+ </style>
@@ -175,22 +175,6 @@
175
175
  autocomplete="off" :hide-no-data="true" :items="companyNames" item-value="value"
176
176
  @update:model-value="handleSelectCompanyName" variant="outlined" density="comfortable" persistent-hint
177
177
  small-chips>
178
- <!-- <template v-slot:no-data>
179
- <v-list-item>
180
- <v-list-item-title v-if="companyNameInput" @click.stop="handleAddNewCompany"
181
- class="d-flex align-center ga-1">
182
- <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
183
- companyNameInput
184
- }}</strong>" as new company.
185
- </v-list-item-title>
186
- <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
187
- Start typing to search for companies.
188
- </v-list-item-title>
189
- <v-list-item-title v-else>
190
- No companies available. Start typing to add a new one.
191
- </v-list-item-title>
192
- </v-list-item>
193
- </template> -->
194
178
  </v-combobox>
195
179
  </v-col>
196
180
  </template>
@@ -204,21 +188,14 @@
204
188
  :loading="siteDataPending" variant="outlined" density="comfortable" persistent-hint small-chips>
205
189
  <template v-slot:no-data>
206
190
  <v-list-item>
207
- <v-list-item-title v-if="fetchCompanyListPending">
208
- <v-progress-circular indeterminate size="20" class="mr-3" />
209
- Searching companies…
210
- </v-list-item-title>
211
- <v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
191
+ <v-list-item-title v-if="deliveryCompanyInput" @click.stop="deliveryCompany = deliveryCompanyInput"
212
192
  class="d-flex align-center ga-1">
213
193
  <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
214
- companyNameInput
194
+ deliveryCompanyInput
215
195
  }}</strong>" as new company.
216
196
  </v-list-item-title>
217
- <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
218
- Start typing to search for companies.
219
- </v-list-item-title>
220
197
  <v-list-item-title v-else>
221
- No companies available. Start typing to add a new one.
198
+ No companies available.
222
199
  </v-list-item-title>
223
200
  </v-list-item>
224
201
  </template>
@@ -696,7 +673,6 @@ const {
696
673
  findPersonByNRICMultipleResult,
697
674
  findPersonByPhoneNumberMultipleResult,
698
675
  findPersonByPlateNumberMultipleResult,
699
- searchCompanyList,
700
676
  } = usePeople();
701
677
  const { getById: getUnitDataById } = useBuildingUnit();
702
678
 
@@ -949,15 +925,6 @@ const showEntryPassInformation = computed(() => {
949
925
  });
950
926
 
951
927
  const companyNames = ref<string[]>([]);
952
- const companyAutofillDataArray = ref<
953
- {
954
- companyName: string[];
955
- unit: string;
956
- block: number;
957
- level: string;
958
- unitName: string;
959
- }[]
960
- >([]);
961
928
  const nricSearchMenu = ref(false);
962
929
  const nricSearchResults = ref<Partial<TPeople>[]>([]);
963
930
  const skipNricFetch = ref(false);
@@ -1028,27 +995,10 @@ async function handleAddNewContractorType() {
1028
995
 
1029
996
  async function handleSelectCompanyName(company: string) {
1030
997
  visitor.company = company;
1031
- // const selected = companyAutofillDataArray.value.find(x => x.companyName?.includes(company))
1032
- // if (!selected) return
1033
-
1034
- // visitor.block = selected.block || ""
1035
- // visitor.level = selected.level || ""
1036
- // visitor.unit = selected.unit || ""
1037
- // visitor.unitName = selected.unitName || ""
1038
998
  }
1039
999
 
1040
1000
  const companyCombo = ref(null);
1041
1001
 
1042
- async function handleAddNewCompany() {
1043
- visitor.company = companyNameInput.value;
1044
-
1045
- const combo = companyCombo.value as any;
1046
- await nextTick();
1047
-
1048
- combo.isMenuActive = false;
1049
- combo.$el.querySelector("input")?.blur();
1050
- }
1051
-
1052
1002
  const {
1053
1003
  data: fetchPersonByNRICReq,
1054
1004
  refresh: fetchPersonByNRICRefresh,
@@ -1321,21 +1271,6 @@ function confirmNricAutofillOptions() {
1321
1271
  closeNricAutofillOptions();
1322
1272
  }
1323
1273
 
1324
- const {
1325
- data: fetchCompanyListReq,
1326
- refresh: fetchCompanyListRefresh,
1327
- pending: fetchCompanyListPending,
1328
- } = useLazyAsyncData(`fetch-company-list`, () => {
1329
- if (!companyNameInput.value) return Promise.resolve(null);
1330
- return searchCompanyList(companyNameInput.value);
1331
- });
1332
-
1333
- watch(fetchCompanyListReq, (arr) => {
1334
- if (Array.isArray(arr)) {
1335
- companyAutofillDataArray.value = arr;
1336
- companyNames.value = arr?.flatMap((x) => x?.companyName);
1337
- }
1338
- });
1339
1274
  function getVisitorSearchItems(res: any): TVisitor[] {
1340
1275
  const items =
1341
1276
  res?.items ||
@@ -1498,19 +1433,6 @@ async function handleNfcReturnCheckout() {
1498
1433
  }
1499
1434
  }
1500
1435
 
1501
- const debounceFetchCompany = debounce(
1502
- async () => fetchCompanyListRefresh(),
1503
- 200
1504
- );
1505
-
1506
- watch(companyNameInput, async (val) => {
1507
- if (!val) {
1508
- companyNames.value = [];
1509
- return;
1510
- }
1511
- await debounceFetchCompany();
1512
- });
1513
-
1514
1436
  function handleAutofillDataViaVehicleNumber(item: TPeople) {
1515
1437
  currentAutofillSource.value = "vehicleNumber";
1516
1438