@7365admin1/layer-common 3.0.31-staging.17 → 3.0.31-staging.19
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/VisitorForm.vue +165 -133
- package/components/VisitorSearchResultSummary.vue +86 -0
- package/composables/usePeople.ts +24 -17
- package/package.json +1 -1
- package/types/people.d.ts +1 -1
|
@@ -18,64 +18,27 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
<v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
|
|
20
20
|
<v-row no-gutters class="pt-4">
|
|
21
|
-
<v-col v-if="shouldShowField('contractorType')" cols="12">
|
|
22
|
-
<InputLabel class="text-capitalize" title="Contractor Type" required />
|
|
23
|
-
<v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" ref="contractorTypeCombo"
|
|
24
|
-
autocomplete="off" :hide-no-data="false" @update:focused="handleFocusedContractorType"
|
|
25
|
-
:items="contractorTypes" :rules="[requiredRule]" item-value="value"
|
|
26
|
-
@update:model-value="handleSelectContractorType" variant="outlined" density="comfortable" persistent-hint
|
|
27
|
-
small-chips>
|
|
28
|
-
<template v-slot:no-data>
|
|
29
|
-
<v-list-item>
|
|
30
|
-
<v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
|
|
31
|
-
<span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
|
|
32
|
-
contractorTypeInput
|
|
33
|
-
}}</strong>" as custom contractor type.
|
|
34
|
-
</v-list-item-title>
|
|
35
|
-
</v-list-item>
|
|
36
|
-
</template>
|
|
37
|
-
</v-combobox>
|
|
38
|
-
</v-col>
|
|
39
|
-
|
|
40
21
|
<v-col v-if="shouldShowField('attachments')" class="w-100">
|
|
41
22
|
<InputLabel class="text-capitalize" title="Attach Image" />
|
|
42
23
|
<InputFileV2 v-model="visitor.attachments" multiple :max-length="3" />
|
|
43
24
|
</v-col>
|
|
44
25
|
|
|
45
|
-
<v-col v-if="shouldShowField('name')" cols="12">
|
|
46
|
-
<v-row>
|
|
47
|
-
<v-col cols="12">
|
|
48
|
-
<InputLabel class="text-capitalize" title="Full Name" required />
|
|
49
|
-
<v-text-field v-model.trim="visitor.name" density="comfortable" :rules="[requiredRule]" />
|
|
50
|
-
</v-col>
|
|
51
|
-
</v-row>
|
|
52
|
-
</v-col>
|
|
53
|
-
|
|
54
26
|
<v-col v-if="shouldShowField('nric')" cols="12">
|
|
55
27
|
<v-row>
|
|
56
28
|
<v-col cols="12">
|
|
57
29
|
<InputLabel class="text-capitalize" title="NRIC" :required="requireNRIC" />
|
|
58
30
|
<v-menu v-model="nricSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
59
31
|
<template #activator="{ props }">
|
|
60
|
-
<InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable"
|
|
32
|
+
<InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable"
|
|
33
|
+
@focus="isNricFieldFocused = true" @blur="isNricFieldFocused = false"
|
|
61
34
|
:rules="[requireNRIC ? requiredRule : () => true]" :key="currentAutofillSource + 'nric-key'"
|
|
62
35
|
@update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
|
|
63
36
|
</template>
|
|
64
37
|
|
|
65
|
-
<v-list v-if="nricSearchResults.length" class="pa-1">
|
|
38
|
+
<v-list v-if="nricSearchResults.length" class="pa-1 phone-search-results">
|
|
66
39
|
<v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
|
|
67
40
|
@click="selectNricSearchResult(item)" class="cursor-pointer nric-search-item">
|
|
68
|
-
<
|
|
69
|
-
<span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
|
|
70
|
-
<span class="text-subtitle-1 text-no-wrap">NRIC: {{ item.nric || 'N/A' }}</span>
|
|
71
|
-
</div>
|
|
72
|
-
<template v-if="item?.contacts && item?.visitorPlateNumbers">
|
|
73
|
-
<div v-if="item.contacts?.length < 2 && item.visitorPlateNumbers?.length < 2"
|
|
74
|
-
class="d-flex align-center justify-space-between ga-4 w-100">
|
|
75
|
-
<span class="text-subtitle-1">{{ item.contacts?.[0] || 'N/A' }}</span>
|
|
76
|
-
<span class="text-subtitle-1 text-no-wrap">{{ item.visitorPlateNumbers?.[0] || 'N/A' }}</span>
|
|
77
|
-
</div>
|
|
78
|
-
</template>
|
|
41
|
+
<VisitorSearchResultSummary :item="item" />
|
|
79
42
|
</v-list-item>
|
|
80
43
|
</v-list>
|
|
81
44
|
</v-menu>
|
|
@@ -84,24 +47,25 @@
|
|
|
84
47
|
</v-col>
|
|
85
48
|
|
|
86
49
|
<v-col v-if="shouldShowField('contact')" cols="12">
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
50
|
+
<v-row>
|
|
51
|
+
<v-col cols="12">
|
|
52
|
+
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
53
|
+
<v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
54
|
+
<template #activator="{ props }">
|
|
55
|
+
<InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]"
|
|
56
|
+
@focus="isPhoneFieldFocused = true" @blur="isPhoneFieldFocused = false" density="comfortable"
|
|
57
|
+
:key="currentAutofillSource + 'phone-key'" @update:model-value="handleUpdatePhoneNumber" />
|
|
58
|
+
</template>
|
|
94
59
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
</v-menu>
|
|
60
|
+
<v-list v-if="phoneSearchResults.length" class="pa-1 phone-search-results">
|
|
61
|
+
<v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
|
|
62
|
+
@click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
|
|
63
|
+
<VisitorSearchResultSummary :item="item" />
|
|
64
|
+
</v-list-item>
|
|
65
|
+
</v-list>
|
|
66
|
+
</v-menu>
|
|
67
|
+
</v-col>
|
|
68
|
+
</v-row>
|
|
105
69
|
</v-col>
|
|
106
70
|
|
|
107
71
|
|
|
@@ -113,17 +77,14 @@
|
|
|
113
77
|
<v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
114
78
|
<template #activator="{ props }">
|
|
115
79
|
<InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
|
|
116
|
-
@update:model-value="handleUpdateVehicleNumber" @focus="isPlateNumberFieldFocused = true"
|
|
80
|
+
@update:model-value="handleUpdateVehicleNumber" @focus="isPlateNumberFieldFocused = true"
|
|
81
|
+
@blur="isPlateNumberFieldFocused = false" />
|
|
117
82
|
</template>
|
|
118
83
|
|
|
119
|
-
<v-list v-if="vehicleSearchResults.length" class="pa-1">
|
|
84
|
+
<v-list v-if="vehicleSearchResults.length" class="pa-1 phone-search-results">
|
|
120
85
|
<v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
|
|
121
|
-
@click="selectPlateNumberSearchResult(item)"
|
|
122
|
-
|
|
123
|
-
<div class="d-flex align-center justify-space-between ga-4 w-100">
|
|
124
|
-
<span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
|
|
125
|
-
<span class="text-subtitle-1 text-no-wrap">NRIC: {{ item.nric || 'N/A' }}</span>
|
|
126
|
-
</div>
|
|
86
|
+
@click="selectPlateNumberSearchResult(item)" class="cursor-pointer nric-search-item">
|
|
87
|
+
<VisitorSearchResultSummary :item="item" />
|
|
127
88
|
</v-list-item>
|
|
128
89
|
</v-list>
|
|
129
90
|
</v-menu>
|
|
@@ -131,6 +92,35 @@
|
|
|
131
92
|
</v-row>
|
|
132
93
|
</v-col>
|
|
133
94
|
|
|
95
|
+
<v-col v-if="shouldShowField('contractorType')" cols="12">
|
|
96
|
+
<InputLabel class="text-capitalize" title="Contractor Type" required />
|
|
97
|
+
<v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" ref="contractorTypeCombo"
|
|
98
|
+
autocomplete="off" :hide-no-data="false" @update:focused="handleFocusedContractorType"
|
|
99
|
+
:items="contractorTypes" :rules="[requiredRule]" item-value="value"
|
|
100
|
+
@update:model-value="handleSelectContractorType" variant="outlined" density="comfortable" persistent-hint
|
|
101
|
+
small-chips>
|
|
102
|
+
<template v-slot:no-data>
|
|
103
|
+
<v-list-item>
|
|
104
|
+
<v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
|
|
105
|
+
<span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
|
|
106
|
+
contractorTypeInput
|
|
107
|
+
}}</strong>" as custom contractor type.
|
|
108
|
+
</v-list-item-title>
|
|
109
|
+
</v-list-item>
|
|
110
|
+
</template>
|
|
111
|
+
</v-combobox>
|
|
112
|
+
</v-col>
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
<v-col v-if="shouldShowField('name')" cols="12">
|
|
116
|
+
<v-row>
|
|
117
|
+
<v-col cols="12">
|
|
118
|
+
<InputLabel class="text-capitalize" title="Full Name" required />
|
|
119
|
+
<v-text-field v-model.trim="visitor.name" density="comfortable" :rules="[requiredRule]" />
|
|
120
|
+
</v-col>
|
|
121
|
+
</v-row>
|
|
122
|
+
</v-col>
|
|
123
|
+
|
|
134
124
|
<v-col v-if="shouldShowField('deliveryType')" cols="12">
|
|
135
125
|
<v-row>
|
|
136
126
|
<v-col cols="12">
|
|
@@ -147,15 +137,11 @@
|
|
|
147
137
|
<InputLabel class="text-capitalize" title="Company Name" />
|
|
148
138
|
<v-combobox v-model="visitor.company" v-model:search="companyNameInput" ref="companyCombo"
|
|
149
139
|
autocomplete="off" :hide-no-data="false" :items="companyNames" item-value="value"
|
|
150
|
-
|
|
140
|
+
@update:model-value="handleSelectCompanyName" variant="outlined"
|
|
151
141
|
density="comfortable" persistent-hint small-chips>
|
|
152
|
-
<template v-slot:no-data>
|
|
142
|
+
<!-- <template v-slot:no-data>
|
|
153
143
|
<v-list-item>
|
|
154
|
-
<v-list-item-title v-if="
|
|
155
|
-
<v-progress-circular indeterminate size="20" class="mr-3" />
|
|
156
|
-
Searching companies…
|
|
157
|
-
</v-list-item-title>
|
|
158
|
-
<v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
|
|
144
|
+
<v-list-item-title v-if="companyNameInput" @click.stop="handleAddNewCompany"
|
|
159
145
|
class="d-flex align-center ga-1">
|
|
160
146
|
<span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
|
|
161
147
|
companyNameInput
|
|
@@ -168,7 +154,7 @@
|
|
|
168
154
|
No companies available. Start typing to add a new one.
|
|
169
155
|
</v-list-item-title>
|
|
170
156
|
</v-list-item>
|
|
171
|
-
</template>
|
|
157
|
+
</template> -->
|
|
172
158
|
</v-combobox>
|
|
173
159
|
</v-col>
|
|
174
160
|
</template>
|
|
@@ -181,7 +167,7 @@
|
|
|
181
167
|
:loading="siteDataPending" variant="outlined" density="comfortable" persistent-hint small-chips>
|
|
182
168
|
<template v-slot:no-data>
|
|
183
169
|
<v-list-item>
|
|
184
|
-
<v-list-item-title v-if="
|
|
170
|
+
<v-list-item-title v-if="siteDataPending">
|
|
185
171
|
<v-progress-circular indeterminate size="20" class="mr-3" />
|
|
186
172
|
Searching companies…
|
|
187
173
|
</v-list-item-title>
|
|
@@ -321,7 +307,7 @@
|
|
|
321
307
|
<span class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
|
|
322
308
|
</div>
|
|
323
309
|
|
|
324
|
-
<template v-if="selectedNricContactOptions.length
|
|
310
|
+
<template v-if="selectedNricContactOptions.length >= 1">
|
|
325
311
|
<h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
|
|
326
312
|
<v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
|
|
327
313
|
<v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
|
|
@@ -329,7 +315,7 @@
|
|
|
329
315
|
</v-radio-group>
|
|
330
316
|
</template>
|
|
331
317
|
|
|
332
|
-
<template v-if="selectedNricPlateOptions.length
|
|
318
|
+
<template v-if="selectedNricPlateOptions.length >= 1">
|
|
333
319
|
<h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
|
|
334
320
|
<v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
|
|
335
321
|
<v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
|
|
@@ -499,7 +485,6 @@ import useVisitor from "../composables/useVisitor";
|
|
|
499
485
|
import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
|
|
500
486
|
import useAccessManagement from "../composables/useAccessManagement";
|
|
501
487
|
import usePeople from "../composables/usePeople";
|
|
502
|
-
import { mockPhoneNumberSearchResults, mockVehicleNumberSearchResults } from "../utils/mock-people-response";
|
|
503
488
|
|
|
504
489
|
const prop = defineProps({
|
|
505
490
|
type: {
|
|
@@ -556,8 +541,8 @@ const {
|
|
|
556
541
|
findPersonByNRIC,
|
|
557
542
|
findPersonByNRICMultipleResult,
|
|
558
543
|
findPersonByPhoneNumberMultipleResult,
|
|
544
|
+
findPersonByPlateNumberMultipleResult,
|
|
559
545
|
searchCompanyList,
|
|
560
|
-
findUsersByPlateNumber,
|
|
561
546
|
} = usePeople();
|
|
562
547
|
const { getById: getUnitDataById } = useBuildingUnit();
|
|
563
548
|
|
|
@@ -767,10 +752,18 @@ const selectedNricPlate = ref("");
|
|
|
767
752
|
type SearchPerson = Omit<Partial<TPeople>, "contact"> & {
|
|
768
753
|
contact?: string | string[];
|
|
769
754
|
contacts?: string | string[];
|
|
770
|
-
|
|
755
|
+
plateNumbers?: string | string[];
|
|
771
756
|
plateNumber?: string | string[];
|
|
772
757
|
};
|
|
773
758
|
|
|
759
|
+
function getPeopleSearchItems(res: any): Partial<TPeople>[] {
|
|
760
|
+
if (Array.isArray(res?.items)) return res.items;
|
|
761
|
+
if (Array.isArray(res?.data)) return res.data;
|
|
762
|
+
if (Array.isArray(res?.data?.items)) return res.data.items;
|
|
763
|
+
if (res?.data) return [res.data];
|
|
764
|
+
if (res && !res?.message) return [res];
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
774
767
|
|
|
775
768
|
const formTitle = computed(() => {
|
|
776
769
|
const isContractorForm = prop.type === "contractor";
|
|
@@ -843,12 +836,13 @@ const {
|
|
|
843
836
|
} = useLazyAsyncData(`fetch-person`, () => {
|
|
844
837
|
const NRIC = visitor.nric;
|
|
845
838
|
if (!NRIC || NRIC.length < 1) return Promise.resolve(null);
|
|
846
|
-
|
|
839
|
+
|
|
840
|
+
const type = prop.type === 'walk-in' ? 'guest' : prop.type
|
|
841
|
+
return findPersonByNRICMultipleResult(NRIC, prop.site, type);
|
|
847
842
|
});
|
|
848
843
|
|
|
849
844
|
watch(fetchPersonByNRICReq, (obj) => {
|
|
850
|
-
|
|
851
|
-
nricSearchResults.value = Array.isArray(items) ? items : [];
|
|
845
|
+
nricSearchResults.value = getPeopleSearchItems(obj);
|
|
852
846
|
nricSearchMenu.value = nricSearchResults.value.length > 0;
|
|
853
847
|
|
|
854
848
|
// Mock data for testing NRIC autofill options.
|
|
@@ -860,7 +854,7 @@ watch(fetchPersonByNRICReq, (obj) => {
|
|
|
860
854
|
// name: "James Smith",
|
|
861
855
|
// nric: "1323232499",
|
|
862
856
|
// contacts: ["653939394", "81234567"],
|
|
863
|
-
//
|
|
857
|
+
// plateNumbers: ["34566", "SMA1234A"],
|
|
864
858
|
// companyName: ["Mock Company Pte Ltd"],
|
|
865
859
|
// } as Partial<SearchPerson>,
|
|
866
860
|
// {
|
|
@@ -868,7 +862,7 @@ watch(fetchPersonByNRICReq, (obj) => {
|
|
|
868
862
|
// name: "Jane Tan",
|
|
869
863
|
// nric: "S1234567A",
|
|
870
864
|
// contacts: ["91234567"],
|
|
871
|
-
//
|
|
865
|
+
// plateNumbers: ["SGB5678B"],
|
|
872
866
|
// companyName: ["Single Option Company"],
|
|
873
867
|
// } as Partial<SearchPerson>,
|
|
874
868
|
// ];
|
|
@@ -921,7 +915,7 @@ function getPersonContactOptions(item: Partial<TPeople>) {
|
|
|
921
915
|
|
|
922
916
|
function getPersonPlateOptions(item: Partial<TPeople>) {
|
|
923
917
|
const person = item as SearchPerson;
|
|
924
|
-
const visitorPlateNumbers = toUniqueStringArray(person.
|
|
918
|
+
const visitorPlateNumbers = toUniqueStringArray(person.plateNumbers);
|
|
925
919
|
if (visitorPlateNumbers.length) return visitorPlateNumbers;
|
|
926
920
|
|
|
927
921
|
const plateNumbers = toUniqueStringArray(person.plateNumber);
|
|
@@ -932,6 +926,28 @@ function getPersonPlateOptions(item: Partial<TPeople>) {
|
|
|
932
926
|
return toUniqueStringArray([...plateNumbers, ...plates]);
|
|
933
927
|
}
|
|
934
928
|
|
|
929
|
+
function findMatchingContactOption(options: string[], value?: string | null) {
|
|
930
|
+
const normalizedValue = value?.trim();
|
|
931
|
+
if (!normalizedValue) return "";
|
|
932
|
+
|
|
933
|
+
return (
|
|
934
|
+
options.find((option) => option.trim() === normalizedValue) ||
|
|
935
|
+
options.find((option) => option.replace(/\s/g, "") === normalizedValue.replace(/\s/g, "")) ||
|
|
936
|
+
""
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function normalizePlateOption(value?: string | null) {
|
|
941
|
+
return (value || "").replace(/[^A-Z0-9]/gi, "").toUpperCase();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function findMatchingPlateOption(options: string[], value?: string | null) {
|
|
945
|
+
const normalizedValue = normalizePlateOption(value);
|
|
946
|
+
if (!normalizedValue) return "";
|
|
947
|
+
|
|
948
|
+
return options.find((option) => normalizePlateOption(option) === normalizedValue) || "";
|
|
949
|
+
}
|
|
950
|
+
|
|
935
951
|
function applyNricSearchResult(
|
|
936
952
|
item: Partial<TPeople>,
|
|
937
953
|
options: { contacts?: string; plateNumber?: string; source?: Exclude<AutofillSource, null> } = {}
|
|
@@ -950,10 +966,18 @@ function applyNricSearchResult(
|
|
|
950
966
|
visitor.name = item.name || visitor.name;
|
|
951
967
|
visitor.contact = options.contacts || getPersonContactOptions(item)[0] || visitor.contact;
|
|
952
968
|
visitor.plateNumber = options.plateNumber || getPersonPlateOptions(item)[0] || visitor.plateNumber;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
969
|
+
|
|
970
|
+
companyNames.value = Array.isArray(item.companyName) ? item.companyName : [];
|
|
971
|
+
|
|
972
|
+
if(item.companyName && item.companyName.length === 1) {
|
|
973
|
+
visitor.company = item.companyName?.[0] || visitor.company;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if(item.companyName && item.companyName.length > 1){
|
|
977
|
+
visitor.company = ""
|
|
956
978
|
}
|
|
979
|
+
|
|
980
|
+
|
|
957
981
|
// visitor.block = (item.block as any) ?? (visitor.block as any) ?? "";
|
|
958
982
|
// visitor.level = (item.level as any) ?? (visitor.level as any) ?? "";
|
|
959
983
|
// visitor.unit = item.unit ?? visitor.unit ?? "";
|
|
@@ -978,22 +1002,26 @@ function selectPlateNumberSearchResult(item: Partial<TPeople>) {
|
|
|
978
1002
|
function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<AutofillSource, null>) {
|
|
979
1003
|
const contactOptions = getPersonContactOptions(item);
|
|
980
1004
|
const plateOptions = getPersonPlateOptions(item);
|
|
1005
|
+
const selectedContact =
|
|
1006
|
+
findMatchingContactOption(contactOptions, visitor.contact) || contactOptions[0] || "";
|
|
1007
|
+
const selectedPlate =
|
|
1008
|
+
findMatchingPlateOption(plateOptions, visitor.plateNumber) || plateOptions[0] || "";
|
|
981
1009
|
|
|
982
|
-
if (
|
|
1010
|
+
if (contactOptions.length > 1 || plateOptions.length > 1) {
|
|
983
1011
|
nricSearchMenu.value = false;
|
|
984
1012
|
phoneSearchMenu.value = false;
|
|
985
1013
|
vehicleSearchMenu.value = false;
|
|
986
1014
|
currentAutofillSource.value = source;
|
|
987
1015
|
selectedNricSearchResult.value = item;
|
|
988
|
-
selectedNricContact.value =
|
|
989
|
-
selectedNricPlate.value =
|
|
1016
|
+
selectedNricContact.value = selectedContact;
|
|
1017
|
+
selectedNricPlate.value = selectedPlate;
|
|
990
1018
|
dialog.nricAutofillOptions = true;
|
|
991
1019
|
return;
|
|
992
1020
|
}
|
|
993
1021
|
|
|
994
1022
|
applyNricSearchResult(item, {
|
|
995
|
-
contacts:
|
|
996
|
-
plateNumber:
|
|
1023
|
+
contacts: selectedContact,
|
|
1024
|
+
plateNumber: selectedPlate,
|
|
997
1025
|
source,
|
|
998
1026
|
});
|
|
999
1027
|
}
|
|
@@ -1016,21 +1044,9 @@ function confirmNricAutofillOptions() {
|
|
|
1016
1044
|
closeNricAutofillOptions();
|
|
1017
1045
|
}
|
|
1018
1046
|
|
|
1019
|
-
const {
|
|
1020
|
-
data: fetchCompanyListReq,
|
|
1021
|
-
refresh: fetchCompanyListRefresh,
|
|
1022
|
-
pending: fetchCompanyListPending,
|
|
1023
|
-
} = useLazyAsyncData(`fetch-company-list`, () => {
|
|
1024
|
-
if (!companyNameInput.value) return Promise.resolve(null);
|
|
1025
|
-
return searchCompanyList(companyNameInput.value);
|
|
1026
|
-
});
|
|
1027
1047
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
companyAutofillDataArray.value = arr;
|
|
1031
|
-
companyNames.value = arr?.flatMap((x) => x?.companyName);
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1048
|
+
|
|
1049
|
+
|
|
1034
1050
|
function getVisitorSearchItems(res: any): TVisitor[] {
|
|
1035
1051
|
const items =
|
|
1036
1052
|
res?.items ||
|
|
@@ -1141,18 +1157,6 @@ async function handleCheckout(visitorId: string) {
|
|
|
1141
1157
|
}
|
|
1142
1158
|
}
|
|
1143
1159
|
|
|
1144
|
-
const debounceFetchCompany = debounce(
|
|
1145
|
-
async () => fetchCompanyListRefresh(),
|
|
1146
|
-
200
|
|
1147
|
-
);
|
|
1148
|
-
|
|
1149
|
-
watch(companyNameInput, async (val) => {
|
|
1150
|
-
if (!val) {
|
|
1151
|
-
companyNames.value = [];
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
await debounceFetchCompany();
|
|
1155
|
-
});
|
|
1156
1160
|
|
|
1157
1161
|
function handleAutofillDataViaVehicleNumber(item: TPeople) {
|
|
1158
1162
|
currentAutofillSource.value = "vehicleNumber";
|
|
@@ -1161,9 +1165,7 @@ function handleAutofillDataViaVehicleNumber(item: TPeople) {
|
|
|
1161
1165
|
visitor.nric = item.nric;
|
|
1162
1166
|
visitor.contact = item.contact;
|
|
1163
1167
|
companyNames.value = item.companyName ?? [];
|
|
1164
|
-
|
|
1165
|
-
visitor.company = companyNames.value?.[0];
|
|
1166
|
-
}
|
|
1168
|
+
|
|
1167
1169
|
visitor.block = item.block ?? "";
|
|
1168
1170
|
visitor.level = item.level ?? "";
|
|
1169
1171
|
visitor.unit = item.unit ?? "";
|
|
@@ -1362,7 +1364,8 @@ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500);
|
|
|
1362
1364
|
|
|
1363
1365
|
function handleUpdateNRIC() {
|
|
1364
1366
|
if (skipNricFetch.value) return;
|
|
1365
|
-
if(!isNricFieldFocused.value) return;
|
|
1367
|
+
if (!isNricFieldFocused.value) return;
|
|
1368
|
+
if (visitor.nric && visitor.nric?.length < 4) return;
|
|
1366
1369
|
debounceFetchNRIC();
|
|
1367
1370
|
}
|
|
1368
1371
|
|
|
@@ -1374,35 +1377,59 @@ const {
|
|
|
1374
1377
|
} = useLazyAsyncData(`fetch-person-phone-number`, () => {
|
|
1375
1378
|
const contact = visitor.contact;
|
|
1376
1379
|
if (!contact || contact.length < 4) return Promise.resolve(null);
|
|
1377
|
-
|
|
1380
|
+
const type = prop.type === 'walk-in' ? 'guest' : prop.type
|
|
1381
|
+
return findPersonByPhoneNumberMultipleResult(contact, prop.site, type);
|
|
1378
1382
|
});
|
|
1379
1383
|
|
|
1384
|
+
watch(fetchPersonByPhoneNumber, (obj) => {
|
|
1385
|
+
phoneSearchResults.value = getPeopleSearchItems(obj);
|
|
1386
|
+
phoneSearchMenu.value = phoneSearchResults.value.length > 0;
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
const debounceFetchPhoneNumber = debounce(fetchPersonByPhoneNumberRefresh, 500);
|
|
1390
|
+
|
|
1380
1391
|
function handleUpdatePhoneNumber() {
|
|
1381
|
-
if(!isPhoneFieldFocused.value) return;
|
|
1392
|
+
if (!isPhoneFieldFocused.value) return;
|
|
1382
1393
|
|
|
1383
1394
|
const search = visitor.contact?.trim() || "";
|
|
1384
|
-
if (search.length <
|
|
1395
|
+
if (search.length < 4) {
|
|
1385
1396
|
phoneSearchResults.value = [];
|
|
1386
1397
|
phoneSearchMenu.value = false;
|
|
1387
1398
|
return;
|
|
1388
1399
|
}
|
|
1389
1400
|
|
|
1390
|
-
|
|
1391
|
-
phoneSearchMenu.value = phoneSearchResults.value.length > 0;
|
|
1401
|
+
debounceFetchPhoneNumber();
|
|
1392
1402
|
}
|
|
1393
1403
|
|
|
1404
|
+
const {
|
|
1405
|
+
data: fetchPersonByPlateNumber,
|
|
1406
|
+
refresh: fetchPersonByPlateNumberRefresh,
|
|
1407
|
+
pending: fetchPersonByPlateNumberPending,
|
|
1408
|
+
} = useLazyAsyncData(`fetch-person-plate-number`, () => {
|
|
1409
|
+
const plateNumber = visitor.plateNumber?.trim()?.toUpperCase() || "";
|
|
1410
|
+
if (plateNumber.length < 4) return Promise.resolve(null);
|
|
1411
|
+
const type = prop.type === 'walk-in' ? 'guest' : prop.type
|
|
1412
|
+
return findPersonByPlateNumberMultipleResult(plateNumber, prop.site, type);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
watch(fetchPersonByPlateNumber, (obj) => {
|
|
1416
|
+
vehicleSearchResults.value = getPeopleSearchItems(obj);
|
|
1417
|
+
vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
const debounceFetchPlateNumber = debounce(fetchPersonByPlateNumberRefresh, 500);
|
|
1421
|
+
|
|
1394
1422
|
function handleUpdateVehicleNumber() {
|
|
1395
|
-
|
|
1423
|
+
if (!isPlateNumberFieldFocused.value) return;
|
|
1396
1424
|
|
|
1397
1425
|
const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
|
|
1398
|
-
if (search.length <
|
|
1426
|
+
if (search.length < 4) {
|
|
1399
1427
|
vehicleSearchResults.value = [];
|
|
1400
1428
|
vehicleSearchMenu.value = false;
|
|
1401
1429
|
return;
|
|
1402
1430
|
}
|
|
1403
1431
|
|
|
1404
|
-
|
|
1405
|
-
vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
|
|
1432
|
+
debounceFetchPlateNumber();
|
|
1406
1433
|
}
|
|
1407
1434
|
|
|
1408
1435
|
function backToSelection() {
|
|
@@ -2009,6 +2036,11 @@ onMounted(() => {
|
|
|
2009
2036
|
min-height: 72px;
|
|
2010
2037
|
}
|
|
2011
2038
|
|
|
2039
|
+
.phone-search-results {
|
|
2040
|
+
max-height: 400px;
|
|
2041
|
+
overflow-y: auto;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2012
2044
|
.nric-options-card {
|
|
2013
2045
|
border-radius: 16px !important;
|
|
2014
2046
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-100 py-2">
|
|
3
|
+
<div class="d-flex align-center mb-3 justify-space-between ga-4 w-100">
|
|
4
|
+
<span class="text-subtitle-1" style="min-width: 50%">
|
|
5
|
+
<div>Name:</div>
|
|
6
|
+
<div>{{ displayName }}</div>
|
|
7
|
+
</span>
|
|
8
|
+
<span class="text-subtitle-1" style="min-width: 50%">
|
|
9
|
+
<div>NRIC:</div>
|
|
10
|
+
<div>{{ item?.nric || "N/A" }}</div>
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
v-if="contacts.length < 2 && plateNumbers.length < 2"
|
|
16
|
+
class="d-flex align-center justify-space-between ga-4 w-100"
|
|
17
|
+
>
|
|
18
|
+
<div class="text-subtitle-1" style="min-width: 50%">
|
|
19
|
+
<div>Contact:</div>
|
|
20
|
+
<div>{{ contacts[0] || "N/A" }}</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="text-subtitle-1" style="min-width: 50%">
|
|
23
|
+
<div>Plate:</div>
|
|
24
|
+
<div class="text-uppercase">{{ plateNumbers[0] || "N/A" }}</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
const props = defineProps<{
|
|
32
|
+
item: any;
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const displayName = computed(() => {
|
|
36
|
+
const name =
|
|
37
|
+
props.item?.name ||
|
|
38
|
+
[props.item?.firstName, props.item?.lastName].filter(Boolean).join(" ");
|
|
39
|
+
|
|
40
|
+
return name || "Unknown";
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const contacts = computed(() => {
|
|
44
|
+
const values = [
|
|
45
|
+
props.item?.contact,
|
|
46
|
+
props.item?.phoneNumber,
|
|
47
|
+
...(Array.isArray(props.item?.contacts) ? props.item.contacts : []),
|
|
48
|
+
...(Array.isArray(props.item?.contactNumbers)
|
|
49
|
+
? props.item.contactNumbers
|
|
50
|
+
: []),
|
|
51
|
+
...(Array.isArray(props.item?.phoneNumbers) ? props.item.phoneNumbers : []),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
return Array.from(
|
|
55
|
+
new Set(
|
|
56
|
+
values
|
|
57
|
+
.map((contact: any) =>
|
|
58
|
+
typeof contact === "string"
|
|
59
|
+
? contact
|
|
60
|
+
: contact?.contact || contact?.phoneNumber || contact?.number
|
|
61
|
+
)
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const plateNumbers = computed(() => {
|
|
68
|
+
const values = [
|
|
69
|
+
props.item?.plateNumber,
|
|
70
|
+
...(Array.isArray(props.item?.plates) ? props.item.plates : []),
|
|
71
|
+
...(Array.isArray(props.item?.plateNumbers) ? props.item.plateNumbers : []),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
return Array.from(
|
|
75
|
+
new Set(
|
|
76
|
+
values
|
|
77
|
+
.map((plate: any) =>
|
|
78
|
+
typeof plate === "string"
|
|
79
|
+
? plate
|
|
80
|
+
: plate?.plateNumber || plate?.number || plate?.value
|
|
81
|
+
)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
</script>
|
package/composables/usePeople.ts
CHANGED
|
@@ -37,43 +37,50 @@ export default function () {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function findPersonByNRICMultipleResult(nric: string, site
|
|
41
|
-
return await $fetch<Record<any, any>>(`/api/people
|
|
40
|
+
async function findPersonByNRICMultipleResult(nric: string, site?: string, type?:string) {
|
|
41
|
+
return await $fetch<Record<any, any>>(`/api/people`, {
|
|
42
42
|
method: "GET",
|
|
43
|
-
query: { nric, site },
|
|
43
|
+
query: site ? { nric, type, site } : { nric, type },
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
async function findPersonByPhoneNumberMultipleResult(
|
|
48
|
+
contact: string,
|
|
49
|
+
site?: string,
|
|
50
|
+
type?:string
|
|
51
|
+
) {
|
|
52
|
+
return await $fetch<Record<any, any>>(`/api/people`, {
|
|
49
53
|
method: "GET",
|
|
50
|
-
query: { contact, site },
|
|
54
|
+
query: site ? { contact, site, type } : { contact, type },
|
|
51
55
|
});
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
|
|
58
|
+
async function findPersonByPlateNumberMultipleResult(
|
|
59
|
+
plateNumber: string,
|
|
60
|
+
site?: string,
|
|
61
|
+
type?:string
|
|
62
|
+
) {
|
|
63
|
+
return await $fetch<Record<any, any>>(`/api/people`, {
|
|
55
64
|
method: "GET",
|
|
56
|
-
query: { plateNumber, site },
|
|
65
|
+
query: site ? { plateNumber, site, type } : { plateNumber, type },
|
|
57
66
|
});
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
async function findPersonByContact(
|
|
61
70
|
contact: string
|
|
62
71
|
): Promise<null | Partial<TPeople>> {
|
|
63
|
-
return await $fetch<Record<string, any>>(`/api/people
|
|
72
|
+
return await $fetch<Record<string, any>>(`/api/people`, {
|
|
64
73
|
method: "GET",
|
|
74
|
+
query: { contact },
|
|
65
75
|
});
|
|
66
76
|
}
|
|
67
77
|
async function findUsersByPlateNumber(
|
|
68
78
|
plateNumber: string
|
|
69
79
|
): Promise<null | Partial<TPeople>> {
|
|
70
|
-
return await $fetch<Record<string, any>>(
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
query: { limit: 20 },
|
|
75
|
-
}
|
|
76
|
-
);
|
|
80
|
+
return await $fetch<Record<string, any>>(`/api/people`, {
|
|
81
|
+
method: "GET",
|
|
82
|
+
query: { plateNumber },
|
|
83
|
+
});
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
async function searchCompanyList(
|
package/package.json
CHANGED
package/types/people.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ declare type TPeople = {
|
|
|
11
11
|
nric?: string;
|
|
12
12
|
contact?: string;
|
|
13
13
|
contacts?: string[] //used in autofill only in VMS
|
|
14
|
-
|
|
14
|
+
plateNumbers?: string [] //used in autofill only in VMS
|
|
15
15
|
remarks?: string;
|
|
16
16
|
start?: string;
|
|
17
17
|
end?: string;
|