@7365admin1/layer-common 3.1.3-staging.39 → 3.1.3-staging.41
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/components/CameraMain.vue +15 -6
- package/components/MemberInformation.vue +375 -110
- package/package.json +1 -1
|
@@ -280,8 +280,8 @@ const prop = defineProps({
|
|
|
280
280
|
headers: {
|
|
281
281
|
type: Array as PropType<Array<any>>,
|
|
282
282
|
default: () => [
|
|
283
|
-
{ title: "Host", key: "host" },
|
|
284
|
-
{ title: "Category", key: "category" },
|
|
283
|
+
// { title: "Host", key: "host" },
|
|
284
|
+
// { title: "Category", key: "category" },
|
|
285
285
|
// { title: "Guard House", key: "guardPost" },
|
|
286
286
|
// { title: "Actions", key: "action", sortable: false },
|
|
287
287
|
],
|
|
@@ -294,11 +294,20 @@ const prop = defineProps({
|
|
|
294
294
|
|
|
295
295
|
const computedHeaders = computed(() => {
|
|
296
296
|
if (prop.type === "ip") {
|
|
297
|
-
const base = [...prop.headers];
|
|
298
|
-
base.splice(0, 0, { title: "Name", key: "name" });
|
|
299
|
-
return base;
|
|
297
|
+
// const base = [...prop.headers];
|
|
298
|
+
// base.splice(0, 0, { title: "Name", key: "name" });
|
|
299
|
+
// return base;
|
|
300
|
+
return [
|
|
301
|
+
{ title: "Camera Name", key: "name" },
|
|
302
|
+
{ title: "URL", key: "host" },
|
|
303
|
+
]
|
|
304
|
+
}else if(prop.type === "anpr"){
|
|
305
|
+
return [
|
|
306
|
+
{ title: "URL", key: "host" },
|
|
307
|
+
{ title: "Type", key: "direction" },
|
|
308
|
+
]
|
|
300
309
|
}
|
|
301
|
-
return prop.headers;
|
|
310
|
+
// return prop.headers;
|
|
302
311
|
});
|
|
303
312
|
|
|
304
313
|
const items = ref<Array<TCamera>>([]);
|
|
@@ -2,152 +2,144 @@
|
|
|
2
2
|
<v-row no-gutters class="w-100">
|
|
3
3
|
<v-card class="w-100">
|
|
4
4
|
<v-card-text>
|
|
5
|
-
<v-btn
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
:height="40"
|
|
9
|
-
text="Scan QR Code"
|
|
10
|
-
class="text-capitalize"
|
|
11
|
-
prepend-icon="mdi-qrcode"
|
|
12
|
-
@click="handleScanQRPass"
|
|
13
|
-
/>
|
|
14
|
-
<v-form
|
|
15
|
-
ref="formRef"
|
|
16
|
-
v-model="validForm"
|
|
17
|
-
:disabled="processing"
|
|
18
|
-
@click="errorScanPassMessage = ''"
|
|
19
|
-
>
|
|
5
|
+
<v-btn block color="primary-button" :height="40" text="Scan QR Code" class="text-capitalize"
|
|
6
|
+
prepend-icon="mdi-qrcode" @click="handleScanQRPass" />
|
|
7
|
+
<v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorScanPassMessage = ''">
|
|
20
8
|
<v-row no-gutters class="pt-5 ga-2">
|
|
21
9
|
<v-col cols="12">
|
|
22
10
|
<InputLabel class="text-capitalize" title="Full Name" required />
|
|
23
|
-
<v-text-field
|
|
24
|
-
v-model.trim="memberForm.name"
|
|
25
|
-
density="compact"
|
|
26
|
-
hide-details
|
|
27
|
-
:rules="[requiredRule]"
|
|
28
|
-
/>
|
|
11
|
+
<v-text-field v-model.trim="memberForm.name" density="compact" hide-details :rules="[requiredRule]" />
|
|
29
12
|
</v-col>
|
|
30
13
|
|
|
31
14
|
<v-col cols="12">
|
|
32
15
|
<InputLabel class="text-capitalize" title="NRIC" required />
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
16
|
+
<v-menu v-model="memberNRICSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
17
|
+
<template #activator="{ props: menuProps }">
|
|
18
|
+
<InputNRICNumber v-bind="menuProps" v-model.trim="memberForm.nric" density="compact" hide-details
|
|
19
|
+
:rules="[requiredRule]" :key="memberCurrentAutofillSource + 'member-nric-key'"
|
|
20
|
+
@focus="isMemberNRICFieldFocused = true" @blur="isMemberNRICFieldFocused = false"
|
|
21
|
+
@update:model-value="handleUpdateMemberNRIC" />
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<v-list v-if="memberNRICSearchResults.length" class="pa-1 phone-search-results">
|
|
25
|
+
<v-list-item v-for="(item, index) in memberNRICSearchResults" :key="item._id || index"
|
|
26
|
+
class="cursor-pointer nric-search-item" @click="selectMemberNRICSearchResult(item)">
|
|
27
|
+
<VisitorSearchResultSummary :item="item" />
|
|
28
|
+
</v-list-item>
|
|
29
|
+
</v-list>
|
|
30
|
+
</v-menu>
|
|
31
|
+
</v-col>
|
|
32
|
+
|
|
33
|
+
<v-col cols="12">
|
|
34
|
+
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
35
|
+
<v-menu v-model="memberPhoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
36
|
+
<template #activator="{ props: menuProps }">
|
|
37
|
+
<InputPhoneNumberV2 v-bind="menuProps" v-model="memberForm.contact" density="compact"
|
|
38
|
+
default-country="SG" hide-details :key="memberCurrentAutofillSource + 'member-phone-key'"
|
|
39
|
+
@focus="isMemberPhoneFieldFocused = true" @blur="isMemberPhoneFieldFocused = false"
|
|
40
|
+
@update:model-value="handleUpdateMemberPhoneNumber" />
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<v-list v-if="memberPhoneSearchResults.length" class="pa-1 phone-search-results">
|
|
44
|
+
<v-list-item v-for="(item, index) in memberPhoneSearchResults" :key="item._id || index"
|
|
45
|
+
class="cursor-pointer nric-search-item" @click="selectMemberPhoneNumberSearchResult(item)">
|
|
46
|
+
<VisitorSearchResultSummary :item="item" />
|
|
47
|
+
</v-list-item>
|
|
48
|
+
</v-list>
|
|
49
|
+
</v-menu>
|
|
39
50
|
</v-col>
|
|
40
51
|
|
|
41
52
|
<v-col cols="12">
|
|
42
|
-
<v-autocomplete
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
:
|
|
46
|
-
class="mt-3"
|
|
47
|
-
:items="passItemsFilteredFinal"
|
|
48
|
-
item-title="prefixAndName"
|
|
49
|
-
item-value="_id"
|
|
50
|
-
label="Pass (optional)"
|
|
51
|
-
variant="outlined"
|
|
52
|
-
density="compact"
|
|
53
|
-
:error-messages="errorScanPassMessage"
|
|
54
|
-
persistent-hint
|
|
55
|
-
small-chips
|
|
56
|
-
:clearable="clearable"
|
|
57
|
-
@click="fetchPasses"
|
|
58
|
-
>
|
|
53
|
+
<v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false" class="mt-3"
|
|
54
|
+
:items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id" label="Pass (optional)"
|
|
55
|
+
variant="outlined" density="compact" :error-messages="errorScanPassMessage" persistent-hint small-chips
|
|
56
|
+
:clearable="clearable" @click="fetchPasses">
|
|
59
57
|
<template v-slot:no-data>
|
|
60
58
|
<v-list-item density="compact">
|
|
61
59
|
<v-list-item-title v-if="passInput">
|
|
62
|
-
No results matching "<strong>{{ passInput }}</strong
|
|
63
|
-
>". This value will be added as new option.
|
|
60
|
+
No results matching "<strong>{{ passInput }}</strong>". This value will be added as new option.
|
|
64
61
|
</v-list-item-title>
|
|
65
62
|
<v-list-item v-else>No data available</v-list-item>
|
|
66
63
|
</v-list-item>
|
|
67
64
|
</template>
|
|
68
65
|
|
|
69
66
|
<template v-slot:chip="{ props, item }">
|
|
70
|
-
<v-chip
|
|
71
|
-
|
|
72
|
-
v-bind="props"
|
|
73
|
-
prepend-icon="mdi-card-bulleted-outline"
|
|
74
|
-
:text="item.raw?.prefixAndName"
|
|
75
|
-
></v-chip>
|
|
67
|
+
<v-chip v-if="selectedPass" v-bind="props" prepend-icon="mdi-card-bulleted-outline"
|
|
68
|
+
:text="item.raw?.prefixAndName"></v-chip>
|
|
76
69
|
</template>
|
|
77
70
|
</v-autocomplete>
|
|
78
71
|
</v-col>
|
|
79
72
|
|
|
80
73
|
<v-col v-if="props.unitId" cols="12">
|
|
81
|
-
<EntryPassInformation
|
|
82
|
-
v-model="
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
:loading="props.settingsLoading"
|
|
87
|
-
:site-id="props.site"
|
|
88
|
-
:unit-id="props.unitId"
|
|
89
|
-
:visitor-type="props.type"
|
|
90
|
-
:hid-qr-code-enabled="props.hidQrCodeEnabled"
|
|
91
|
-
:hid-qr-printer-configured="props.hidQrPrinterConfigured"
|
|
92
|
-
:excluded-card-ids="props.selectedNfcCards.map((c) => c._id)"
|
|
93
|
-
/>
|
|
94
|
-
</v-col>
|
|
95
|
-
|
|
96
|
-
<v-col cols="12">
|
|
97
|
-
<InputLabel
|
|
98
|
-
class="text-capitalize"
|
|
99
|
-
title="Phone Number"
|
|
100
|
-
required
|
|
101
|
-
/>
|
|
102
|
-
<InputPhoneNumberV2
|
|
103
|
-
v-model="memberForm.contact"
|
|
104
|
-
density="compact"
|
|
105
|
-
default-country="SG"
|
|
106
|
-
hide-details
|
|
107
|
-
/>
|
|
74
|
+
<EntryPassInformation v-model="memberPassType" v-model:quantity="memberPassQuantity"
|
|
75
|
+
v-model:cards="memberPassCards" :settings="props.settings" :loading="props.settingsLoading"
|
|
76
|
+
:site-id="props.site" :unit-id="props.unitId" :visitor-type="props.type"
|
|
77
|
+
:hid-qr-code-enabled="props.hidQrCodeEnabled" :hid-qr-printer-configured="props.hidQrPrinterConfigured"
|
|
78
|
+
:excluded-card-ids="props.selectedNfcCards.map((c) => c._id)" />
|
|
108
79
|
</v-col>
|
|
109
80
|
|
|
110
81
|
<v-row class="pt-3" justify="space-between">
|
|
111
82
|
<v-col cols="6">
|
|
112
|
-
<v-btn
|
|
113
|
-
text="
|
|
114
|
-
:disabled="!validForm"
|
|
115
|
-
prepend-icon="mdi-plus"
|
|
116
|
-
color="primary-button"
|
|
117
|
-
class="text-capitalize"
|
|
118
|
-
@click="handleAddMember"
|
|
119
|
-
/>
|
|
83
|
+
<v-btn text="Add Member" :disabled="!validForm" prepend-icon="mdi-plus" color="primary-button"
|
|
84
|
+
class="text-capitalize" @click="handleAddMember" />
|
|
120
85
|
</v-col>
|
|
121
86
|
<v-col cols="6" align="end">
|
|
122
|
-
<v-btn
|
|
123
|
-
|
|
124
|
-
prepend-icon="mdi-refresh"
|
|
125
|
-
color="primary-button"
|
|
126
|
-
class="text-capitalize"
|
|
127
|
-
@click.stop="handleClearForm"
|
|
128
|
-
/>
|
|
87
|
+
<v-btn text="Clear Form" prepend-icon="mdi-refresh" color="primary-button" class="text-capitalize"
|
|
88
|
+
@click.stop="handleClearForm" />
|
|
129
89
|
</v-col>
|
|
130
90
|
</v-row>
|
|
131
91
|
</v-row>
|
|
132
92
|
</v-form>
|
|
133
93
|
</v-card-text>
|
|
134
94
|
</v-card>
|
|
135
|
-
<visitor-pass-key-q-r-scanner
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
95
|
+
<visitor-pass-key-q-r-scanner :dialog="dialog.scanQRCode" v-model:scannedValue="scannedPassValue"
|
|
96
|
+
@close-dialog="dialog.scanQRCode = false" />
|
|
97
|
+
<v-dialog v-model="dialog.memberAutofillOptions" max-width="430">
|
|
98
|
+
<v-card v-if="memberSelectedSearchResult" class="nric-options-card">
|
|
99
|
+
<v-card-actions class="justify-end pa-1">
|
|
100
|
+
<v-btn icon="mdi-close" variant="text" size="small" @click="memberCloseAutofillOptions" />
|
|
101
|
+
</v-card-actions>
|
|
102
|
+
|
|
103
|
+
<v-card-text class="pa-4 pt-1">
|
|
104
|
+
<h2 class="nric-options-title font-weight-bold mb-4">Select</h2>
|
|
105
|
+
|
|
106
|
+
<div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
|
|
107
|
+
<span>{{ memberSelectedSearchResult.name || "Unknown" }}</span>
|
|
108
|
+
<span class="text-no-wrap">
|
|
109
|
+
NRIC: {{ memberSelectedSearchResult.nric || "N/A" }}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<template v-if="
|
|
114
|
+
(!memberForm.contact ||
|
|
115
|
+
memberCurrentAutofillSource === 'contact') &&
|
|
116
|
+
memberSelectedContactOptions.length >= 1
|
|
117
|
+
">
|
|
118
|
+
<h3 class="nric-options-section-title font-weight-bold mb-2">
|
|
119
|
+
Contact
|
|
120
|
+
</h3>
|
|
121
|
+
<v-radio-group v-model="memberSelectedContact" hide-details color="success" class="nric-option-group">
|
|
122
|
+
<v-radio v-for="contact in memberSelectedContactOptions" :key="contact" :label="contact" :value="contact"
|
|
123
|
+
class="nric-option-radio" />
|
|
124
|
+
</v-radio-group>
|
|
125
|
+
</template>
|
|
126
|
+
</v-card-text>
|
|
127
|
+
|
|
128
|
+
<v-card-actions class="justify-end px-4 pb-4">
|
|
129
|
+
<v-btn variant="text" class="text-none" size="small" @click="memberCloseAutofillOptions">
|
|
130
|
+
Cancel
|
|
131
|
+
</v-btn>
|
|
132
|
+
<v-btn color="primary-button" variant="flat" class="text-none px-5" size="small"
|
|
133
|
+
@click="memberConfirmAutofillOptions">
|
|
134
|
+
Select
|
|
135
|
+
</v-btn>
|
|
136
|
+
</v-card-actions>
|
|
137
|
+
</v-card>
|
|
138
|
+
</v-dialog>
|
|
140
139
|
<v-divider class="my-3" />
|
|
141
|
-
<v-row
|
|
142
|
-
v-if="committedMembers.length > 0"
|
|
143
|
-
no-gutters
|
|
144
|
-
class="w-100 mt-5 ga-3"
|
|
145
|
-
>
|
|
140
|
+
<v-row v-if="committedMembers.length > 0" no-gutters class="w-100 mt-5 ga-3">
|
|
146
141
|
<template v-for="(member, index) in committedMembers" :key="member.nric">
|
|
147
|
-
<CardMemberInfoSummary
|
|
148
|
-
:member="membersDisplayed[index]"
|
|
149
|
-
@remove="handleRemoveMember(index)"
|
|
150
|
-
/>
|
|
142
|
+
<CardMemberInfoSummary :member="membersDisplayed[index]" @remove="handleRemoveMember(index)" />
|
|
151
143
|
</template>
|
|
152
144
|
</v-row>
|
|
153
145
|
<!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
|
|
@@ -206,8 +198,12 @@ const props = defineProps({
|
|
|
206
198
|
},
|
|
207
199
|
});
|
|
208
200
|
|
|
209
|
-
const { requiredRule } = useUtils();
|
|
201
|
+
const { requiredRule, debounce } = useUtils();
|
|
210
202
|
const { getPassKeysByPageSearch } = usePassKey();
|
|
203
|
+
const {
|
|
204
|
+
findPersonByNRICMultipleResult,
|
|
205
|
+
findPersonByPhoneNumberMultipleResult,
|
|
206
|
+
} = usePeople();
|
|
211
207
|
|
|
212
208
|
const memberForm = reactive<TMemberInfo>({
|
|
213
209
|
name: "",
|
|
@@ -238,6 +234,7 @@ const scannedPassValue = ref<string>("");
|
|
|
238
234
|
|
|
239
235
|
const dialog = reactive({
|
|
240
236
|
scanQRCode: false,
|
|
237
|
+
memberAutofillOptions: false,
|
|
241
238
|
});
|
|
242
239
|
|
|
243
240
|
const selectedPass = ref<string>("");
|
|
@@ -248,6 +245,23 @@ const memberPassCards = ref<{ _id: string; cardNo: string }[]>([]);
|
|
|
248
245
|
const members = defineModel<TMemberInfo[]>({ required: true, default: [] });
|
|
249
246
|
const committedMembers = ref<TMemberInfo[]>([]);
|
|
250
247
|
|
|
248
|
+
type AutofillSource = "nric" | "contact" | null;
|
|
249
|
+
type SearchPerson = Omit<Partial<TPeople>, "contact"> & {
|
|
250
|
+
contact?: string | string[];
|
|
251
|
+
contacts?: string | string[];
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const memberPhoneSearchMenu = ref(false);
|
|
255
|
+
const memberNRICSearchMenu = ref(false);
|
|
256
|
+
const isMemberPhoneFieldFocused = ref(false);
|
|
257
|
+
const isMemberNRICFieldFocused = ref(false);
|
|
258
|
+
const memberPhoneSearchResults = ref<Partial<TPeople>[]>([]);
|
|
259
|
+
const memberNRICSearchResults = ref<Partial<TPeople>[]>([]);
|
|
260
|
+
const memberCurrentAutofillSource = ref<AutofillSource>(null);
|
|
261
|
+
const memberSelectedSearchResult = ref<Partial<TPeople> | null>(null);
|
|
262
|
+
const memberSelectedContact = ref("");
|
|
263
|
+
const skipMemberNRICFetch = ref(false);
|
|
264
|
+
|
|
251
265
|
// Always keep the model in sync: committed members + current form draft
|
|
252
266
|
watch(
|
|
253
267
|
() => memberForm,
|
|
@@ -352,9 +366,219 @@ function resetAccessCard() {
|
|
|
352
366
|
memberPassCards.value = [];
|
|
353
367
|
}
|
|
354
368
|
|
|
369
|
+
function getPeopleSearchItems(res: any): Partial<TPeople>[] {
|
|
370
|
+
if (Array.isArray(res?.items)) return res.items;
|
|
371
|
+
if (Array.isArray(res?.data)) return res.data;
|
|
372
|
+
if (Array.isArray(res?.data?.items)) return res.data.items;
|
|
373
|
+
if (res?.data) return [res.data];
|
|
374
|
+
if (res && !res?.message) return [res];
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function toUniqueStringArray(value: unknown) {
|
|
379
|
+
const rawValues = Array.isArray(value) ? value : value ? [value] : [];
|
|
380
|
+
return [...new Set(rawValues.map((v) => String(v).trim()).filter(Boolean))];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function getPersonContactOptions(item: Partial<TPeople>) {
|
|
384
|
+
const person = item as SearchPerson;
|
|
385
|
+
return toUniqueStringArray(person.contacts || person.contact);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function findMatchingContactOption(options: string[], value?: string | null) {
|
|
389
|
+
const normalizedValue = value?.trim();
|
|
390
|
+
if (!normalizedValue) return "";
|
|
391
|
+
const compactValue = normalizedValue.replace(/\s/g, "");
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
options.find((option) => option.trim() === normalizedValue) ||
|
|
395
|
+
options.find((option) => option.replace(/\s/g, "") === compactValue) ||
|
|
396
|
+
options.find((option) => option.trim().includes(normalizedValue)) ||
|
|
397
|
+
options.find((option) =>
|
|
398
|
+
option.replace(/\s/g, "").includes(compactValue)
|
|
399
|
+
) ||
|
|
400
|
+
""
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const memberSelectedContactOptions = computed(() => {
|
|
405
|
+
return memberSelectedSearchResult.value
|
|
406
|
+
? getPersonContactOptions(memberSelectedSearchResult.value)
|
|
407
|
+
: [];
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const {
|
|
411
|
+
data: fetchMemberByPhoneNumber,
|
|
412
|
+
refresh: fetchMemberByPhoneNumberRefresh,
|
|
413
|
+
} = useLazyAsyncData(`fetch-member-phone-number-${props.site}`, () => {
|
|
414
|
+
const contact = memberForm.contact?.trim() || "";
|
|
415
|
+
if (contact.length < 4) return Promise.resolve(null);
|
|
416
|
+
const type = props.type === "walk-in" ? "guest" : props.type;
|
|
417
|
+
return findPersonByPhoneNumberMultipleResult(contact, props.site, type);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
watch(fetchMemberByPhoneNumber, (obj) => {
|
|
421
|
+
memberPhoneSearchResults.value = getPeopleSearchItems(obj);
|
|
422
|
+
memberPhoneSearchMenu.value = memberPhoneSearchResults.value.length > 0;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const debounceFetchMemberPhoneNumber = debounce(
|
|
426
|
+
fetchMemberByPhoneNumberRefresh,
|
|
427
|
+
500
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
function handleUpdateMemberPhoneNumber() {
|
|
431
|
+
if (!isMemberPhoneFieldFocused.value) return;
|
|
432
|
+
|
|
433
|
+
const search = memberForm.contact?.trim() || "";
|
|
434
|
+
if (search.length < 4) {
|
|
435
|
+
memberPhoneSearchResults.value = [];
|
|
436
|
+
memberPhoneSearchMenu.value = false;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (props.type === "drop-off" || props.type === "pick-up") return;
|
|
441
|
+
debounceFetchMemberPhoneNumber();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const {
|
|
445
|
+
data: fetchMemberByNRIC,
|
|
446
|
+
refresh: fetchMemberByNRICRefresh,
|
|
447
|
+
} = useLazyAsyncData(`fetch-member-nric-${props.site}`, () => {
|
|
448
|
+
const nric = memberForm.nric?.trim() || "";
|
|
449
|
+
if (nric.length < 4) return Promise.resolve(null);
|
|
450
|
+
const type = props.type === "walk-in" ? "guest" : props.type;
|
|
451
|
+
return findPersonByNRICMultipleResult(nric, props.site, type);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
watch(fetchMemberByNRIC, (obj) => {
|
|
455
|
+
memberNRICSearchResults.value = getPeopleSearchItems(obj);
|
|
456
|
+
memberNRICSearchMenu.value = memberNRICSearchResults.value.length > 0;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const debounceFetchMemberNRIC = debounce(fetchMemberByNRICRefresh, 500);
|
|
460
|
+
|
|
461
|
+
function handleUpdateMemberNRIC() {
|
|
462
|
+
if (skipMemberNRICFetch.value) return;
|
|
463
|
+
if (!isMemberNRICFieldFocused.value) return;
|
|
464
|
+
|
|
465
|
+
const search = memberForm.nric?.trim() || "";
|
|
466
|
+
if (search.length < 4) {
|
|
467
|
+
memberNRICSearchResults.value = [];
|
|
468
|
+
memberNRICSearchMenu.value = false;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (props.type === "drop-off" || props.type === "pick-up") return;
|
|
473
|
+
debounceFetchMemberNRIC();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function selectMemberPhoneNumberSearchResult(item: Partial<TPeople>) {
|
|
477
|
+
memberSelectAutofillSearchResult(item, "contact");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function selectMemberNRICSearchResult(item: Partial<TPeople>) {
|
|
481
|
+
memberSelectAutofillSearchResult(item, "nric");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function memberSelectAutofillSearchResult(
|
|
485
|
+
item: Partial<TPeople>,
|
|
486
|
+
source: Exclude<AutofillSource, null>
|
|
487
|
+
) {
|
|
488
|
+
memberCurrentAutofillSource.value = source;
|
|
489
|
+
const contactOptions = getPersonContactOptions(item);
|
|
490
|
+
const selectedContact =
|
|
491
|
+
findMatchingContactOption(contactOptions, memberForm.contact) ||
|
|
492
|
+
contactOptions[0] ||
|
|
493
|
+
"";
|
|
494
|
+
|
|
495
|
+
const needsContactChoice =
|
|
496
|
+
(!memberForm.contact?.trim() || source === "contact") &&
|
|
497
|
+
contactOptions.length > 1;
|
|
498
|
+
|
|
499
|
+
if (needsContactChoice) {
|
|
500
|
+
memberPhoneSearchMenu.value = false;
|
|
501
|
+
memberNRICSearchMenu.value = false;
|
|
502
|
+
memberSelectedSearchResult.value = item;
|
|
503
|
+
memberSelectedContact.value = selectedContact;
|
|
504
|
+
dialog.memberAutofillOptions = true;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
applyMemberSearchResult(item, {
|
|
509
|
+
contacts: selectedContact,
|
|
510
|
+
source,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function applyMemberSearchResult(
|
|
515
|
+
item: Partial<TPeople>,
|
|
516
|
+
options: {
|
|
517
|
+
contacts?: string;
|
|
518
|
+
source?: Exclude<AutofillSource, null>;
|
|
519
|
+
} = {}
|
|
520
|
+
) {
|
|
521
|
+
memberNRICSearchMenu.value = false;
|
|
522
|
+
memberPhoneSearchMenu.value = false;
|
|
523
|
+
memberNRICSearchResults.value = [];
|
|
524
|
+
memberPhoneSearchResults.value = [];
|
|
525
|
+
skipMemberNRICFetch.value = true;
|
|
526
|
+
memberCurrentAutofillSource.value = options.source || null;
|
|
527
|
+
|
|
528
|
+
const shouldAutofillField = (fieldSource?: Exclude<AutofillSource, null>) => {
|
|
529
|
+
if (!fieldSource) return false;
|
|
530
|
+
return memberCurrentAutofillSource.value === fieldSource;
|
|
531
|
+
};
|
|
532
|
+
const getAutofillValue = (
|
|
533
|
+
currentValue: string | undefined,
|
|
534
|
+
nextValue: string | undefined,
|
|
535
|
+
fieldSource?: Exclude<AutofillSource, null>
|
|
536
|
+
) => {
|
|
537
|
+
if (!nextValue) return currentValue;
|
|
538
|
+
if (currentValue && !shouldAutofillField(fieldSource)) return currentValue;
|
|
539
|
+
return nextValue;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
memberForm.nric =
|
|
543
|
+
getAutofillValue(memberForm.nric, item.nric, "nric") ?? "";
|
|
544
|
+
memberForm.name = getAutofillValue(memberForm.name, item.name) ?? "";
|
|
545
|
+
memberForm.contact =
|
|
546
|
+
getAutofillValue(
|
|
547
|
+
memberForm.contact,
|
|
548
|
+
options.contacts || getPersonContactOptions(item)[0],
|
|
549
|
+
"contact"
|
|
550
|
+
) ?? "";
|
|
551
|
+
|
|
552
|
+
setTimeout(() => {
|
|
553
|
+
memberCurrentAutofillSource.value = null;
|
|
554
|
+
skipMemberNRICFetch.value = false;
|
|
555
|
+
}, 1000);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function memberCloseAutofillOptions() {
|
|
559
|
+
dialog.memberAutofillOptions = false;
|
|
560
|
+
memberSelectedSearchResult.value = null;
|
|
561
|
+
memberSelectedContact.value = "";
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function memberConfirmAutofillOptions() {
|
|
565
|
+
if (!memberSelectedSearchResult.value) return;
|
|
566
|
+
|
|
567
|
+
applyMemberSearchResult(memberSelectedSearchResult.value, {
|
|
568
|
+
contacts: memberSelectedContact.value,
|
|
569
|
+
source: memberCurrentAutofillSource.value || "nric",
|
|
570
|
+
});
|
|
571
|
+
memberCloseAutofillOptions();
|
|
572
|
+
}
|
|
573
|
+
|
|
355
574
|
function handleClearForm() {
|
|
356
575
|
formRef.value?.reset();
|
|
357
576
|
resetAccessCard();
|
|
577
|
+
memberNRICSearchMenu.value = false;
|
|
578
|
+
memberPhoneSearchMenu.value = false;
|
|
579
|
+
memberNRICSearchResults.value = [];
|
|
580
|
+
memberPhoneSearchResults.value = [];
|
|
581
|
+
memberCloseAutofillOptions();
|
|
358
582
|
}
|
|
359
583
|
|
|
360
584
|
async function handleAddMember() {
|
|
@@ -375,4 +599,45 @@ function handleRemoveMember(index: number) {
|
|
|
375
599
|
}
|
|
376
600
|
</script>
|
|
377
601
|
|
|
378
|
-
<style scoped
|
|
602
|
+
<style scoped>
|
|
603
|
+
.nric-search-item {
|
|
604
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
605
|
+
border-radius: 4px;
|
|
606
|
+
margin: 4px;
|
|
607
|
+
min-height: 72px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.phone-search-results {
|
|
611
|
+
max-height: 400px;
|
|
612
|
+
overflow-y: auto;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.nric-options-card {
|
|
616
|
+
border-radius: 16px !important;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.nric-options-title {
|
|
620
|
+
color: #06253b;
|
|
621
|
+
font-size: 1.45rem;
|
|
622
|
+
line-height: 1.2;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.nric-options-section-title {
|
|
626
|
+
color: #06253b;
|
|
627
|
+
font-size: 1.05rem;
|
|
628
|
+
line-height: 1.25;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.nric-selected-person {
|
|
632
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
633
|
+
border-radius: 12px;
|
|
634
|
+
color: #06253b;
|
|
635
|
+
font-size: 0.95rem;
|
|
636
|
+
min-height: 52px;
|
|
637
|
+
padding: 10px 14px;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.nric-option-group {
|
|
641
|
+
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
642
|
+
}
|
|
643
|
+
</style>
|