@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.
@@ -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" @focus="isNricFieldFocused = true" @blur="isNricFieldFocused = false"
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
- <div class="d-flex align-center justify-space-between ga-4 w-100">
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
- <InputLabel class="text-capitalize" title="Phone Number" required />
88
- <v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
89
- <template #activator="{ props }">
90
- <InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]" @focus="isPhoneFieldFocused = true" @blur="isPhoneFieldFocused = false"
91
- density="comfortable" :key="currentAutofillSource + 'phone-key'"
92
- @update:model-value="handleUpdatePhoneNumber" />
93
- </template>
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
- <v-list v-if="phoneSearchResults.length" class="pa-1">
96
- <v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
97
- @click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
98
- <div class="d-flex align-center justify-space-between ga-4 w-100">
99
- <span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
100
- <span class="text-subtitle-1 text-no-wrap">NRIC: {{ item.nric || 'N/A' }}</span>
101
- </div>
102
- </v-list-item>
103
- </v-list>
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" @blur="isPlateNumberFieldFocused = false" />
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
- class="cursor-pointer nric-search-item">
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
- :loading="fetchCompanyListPending" @update:model-value="handleSelectCompanyName" variant="outlined"
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="fetchCompanyListPending">
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="fetchCompanyListPending">
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 > 1">
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 > 1">
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
- visitorPlateNumbers?: string | string[];
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
- return findPersonByNRICMultipleResult(NRIC, prop.site);
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
- const items = obj?.items ?? [];
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
- // visitorPlateNumbers: ["34566", "SMA1234A"],
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
- // visitorPlateNumbers: ["SGB5678B"],
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.visitorPlateNumbers);
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
- companyNames.value = item.companyName ?? [];
954
- if (!visitor.company) {
955
- visitor.company = companyNames.value?.[0] || visitor.company;
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 (source === "nric" && (contactOptions.length > 1 || plateOptions.length > 1)) {
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 = contactOptions[0] || "";
989
- selectedNricPlate.value = plateOptions[0] || "";
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: contactOptions[0],
996
- plateNumber: plateOptions[0],
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
- watch(fetchCompanyListReq, (arr) => {
1029
- if (Array.isArray(arr)) {
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
- if (!visitor.company) {
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
- return findPersonByPhoneNumberMultipleResult(contact, prop.site);
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 < 1) {
1395
+ if (search.length < 4) {
1385
1396
  phoneSearchResults.value = [];
1386
1397
  phoneSearchMenu.value = false;
1387
1398
  return;
1388
1399
  }
1389
1400
 
1390
- phoneSearchResults.value = mockPhoneNumberSearchResults
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
- if(!isPlateNumberFieldFocused.value) return;
1423
+ if (!isPlateNumberFieldFocused.value) return;
1396
1424
 
1397
1425
  const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
1398
- if (search.length < 1) {
1426
+ if (search.length < 4) {
1399
1427
  vehicleSearchResults.value = [];
1400
1428
  vehicleSearchMenu.value = false;
1401
1429
  return;
1402
1430
  }
1403
1431
 
1404
- vehicleSearchResults.value = mockVehicleNumberSearchResults
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>
@@ -37,43 +37,50 @@ export default function () {
37
37
  });
38
38
  }
39
39
 
40
- async function findPersonByNRICMultipleResult(nric: string, site: string) {
41
- return await $fetch<Record<any, any>>(`/api/people/all-nric`, {
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
- async function findPersonByPhoneNumberMultipleResult(contact: string, site: string) {
48
- return await $fetch<Record<any, any>>(`/api/people/contact`, {
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
- async function findPersonByPlateNumberMultipleResult(plateNumber: string, site: string) {
54
- return await $fetch<Record<any, any>>(`/api/people/plate-number`, {
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/contact/${contact}`, {
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
- `/api/people/plateNumber/${plateNumber}`,
72
- {
73
- method: "GET",
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
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.31-staging.17",
5
+ "version": "3.0.31-staging.19",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
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
- visitorPlateNumbers?: string [] //used in autofill only in VMS
14
+ plateNumbers?: string [] //used in autofill only in VMS
15
15
  remarks?: string;
16
16
  start?: string;
17
17
  end?: string;