@7365admin1/layer-common 3.0.30 → 3.0.31-staging.11

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.
Files changed (27) hide show
  1. package/components/AddPassKeyToVisitor.vue +115 -53
  2. package/components/DashboardMain.vue +29 -33
  3. package/components/InvitationMain.vue +4 -3
  4. package/components/ManageChecklistMain.vue +32 -25
  5. package/components/PhotoUpload.vue +19 -4
  6. package/components/VisitorForm.vue +385 -59
  7. package/components/VisitorManagement.vue +109 -30
  8. package/composables/useCleaningSchedulePermission.ts +16 -51
  9. package/package.json +1 -1
  10. package/types/customer.d.ts +2 -1
  11. package/types/people.d.ts +2 -0
  12. /package/components/{NFC → Nfc}/NFCPatrolReportMain.vue +0 -0
  13. /package/components/{NFC → Nfc}/NFCPatrolRouteForm.vue +0 -0
  14. /package/components/{NFC → Nfc}/NFCPatrolRouteMain.vue +0 -0
  15. /package/components/{NFC → Nfc}/NFCPatrolSettings.vue +0 -0
  16. /package/components/{NFC → Nfc}/NFCTagForm.vue +0 -0
  17. /package/components/{NFC → Nfc}/NFCTagMain.vue +0 -0
  18. /package/components/{NFC → Nfc}/PatrolReport/DailyReportTab.vue +0 -0
  19. /package/components/{NFC → Nfc}/PatrolReport/MonthlyReportTab.vue +0 -0
  20. /package/components/{NFC → Nfc}/PatrolReport/PatrolActivityTable.vue +0 -0
  21. /package/components/{NFC → Nfc}/PatrolReport/PatrolReportTab.vue +0 -0
  22. /package/components/{NFC → Nfc}/PatrolReport/PatrolRouteSummary.vue +0 -0
  23. /package/components/{NFC → Nfc}/PatrolReport/RemarksDialog.vue +0 -0
  24. /package/components/{NFC → Nfc}/PatrolReport/ReportEmptyState.vue +0 -0
  25. /package/components/{NFC → Nfc}/PatrolReport/ReportFilters.vue +0 -0
  26. /package/components/{NFC → Nfc}/PatrolReport/ReportLocationHeader.vue +0 -0
  27. /package/components/{NFC → Nfc}/PatrolReport/SummaryReportTable.vue +0 -0
@@ -12,7 +12,7 @@
12
12
  <div class="w-100 d-flex justify-space-between ga-2">
13
13
  <span class="text-subtitle-1 w-100 font-weight-bold mb-3">{{
14
14
  formTitle
15
- }}</span>
15
+ }}</span>
16
16
  <span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
17
17
  <span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
18
18
  </div>
@@ -30,7 +30,7 @@
30
30
  <v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
31
31
  <span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
32
32
  contractorTypeInput
33
- }}</strong>" as custom contractor type.
33
+ }}</strong>" as custom contractor type.
34
34
  </v-list-item-title>
35
35
  </v-list-item>
36
36
  </template>
@@ -62,11 +62,20 @@
62
62
  @update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
63
63
  </template>
64
64
 
65
- <v-list v-if="nricSearchResults.length">
65
+ <v-list v-if="nricSearchResults.length" class="pa-1">
66
66
  <v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
67
- @click="selectNricSearchResult(item)" class="cursor-pointer">
68
- <v-list-item-title>{{ item.name || 'Unknown' }}</v-list-item-title>
69
- <v-list-item-subtitle>{{ item.contact }}</v-list-item-subtitle>
67
+ @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>
70
79
  </v-list-item>
71
80
  </v-list>
72
81
  </v-menu>
@@ -76,8 +85,28 @@
76
85
 
77
86
  <v-col v-if="shouldShowField('contact')" cols="12">
78
87
  <InputLabel class="text-capitalize" title="Phone Number" required />
79
- <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable"
80
- :key="currentAutofillSource + 'phone-key'" />
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]"
91
+ density="comfortable" :key="currentAutofillSource + 'phone-key'"
92
+ @update:model-value="handleUpdatePhoneNumber" />
93
+ </template>
94
+
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="selectAutofillSearchResult(item, 'contact')" 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
+ <div v-if="getPersonContactOptions(item).length < 2 && getPersonPlateOptions(item).length < 2"
103
+ class="d-flex align-center justify-space-between ga-4 w-100">
104
+ <span class="text-subtitle-1">{{ getPersonContactOptions(item)[0] || 'N/A' }}</span>
105
+ <span class="text-subtitle-1 text-no-wrap">{{ getPersonPlateOptions(item)[0] || 'N/A' }}</span>
106
+ </div>
107
+ </v-list-item>
108
+ </v-list>
109
+ </v-menu>
81
110
  </v-col>
82
111
 
83
112
  <v-col v-if="shouldShowField('deliveryType')" cols="12">
@@ -108,7 +137,7 @@
108
137
  class="d-flex align-center ga-1">
109
138
  <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
110
139
  companyNameInput
111
- }}</strong>" as new company.
140
+ }}</strong>" as new company.
112
141
  </v-list-item-title>
113
142
  <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
114
143
  Start typing to search for companies.
@@ -138,7 +167,7 @@
138
167
  class="d-flex align-center ga-1">
139
168
  <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
140
169
  companyNameInput
141
- }}</strong>" as new company.
170
+ }}</strong>" as new company.
142
171
  </v-list-item-title>
143
172
  <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
144
173
  Start typing to search for companies.
@@ -155,9 +184,30 @@
155
184
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
156
185
  <v-row>
157
186
  <v-col cols="12">
158
- <InputLabel class="text-capitalize" title="Vehicle Number" required />
187
+ <InputLabel class="text-capitalize" title="Vehicle Number" :required="isVehicleRequired" />
159
188
  <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
160
- <InputVehicleNumber v-model="visitor.plateNumber" density="comfortable" />
189
+ <v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
190
+ <template #activator="{ props }">
191
+ <InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
192
+ @update:model-value="handleUpdateVehicleNumber" />
193
+ </template>
194
+
195
+ <v-list v-if="vehicleSearchResults.length" class="pa-1">
196
+ <v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
197
+ @click="selectAutofillSearchResult(item, 'vehicleNumber')"
198
+ class="cursor-pointer nric-search-item">
199
+ <div class="d-flex align-center justify-space-between ga-4 w-100">
200
+ <span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
201
+ <span class="text-subtitle-1 text-no-wrap">NRIC: {{ item.nric || 'N/A' }}</span>
202
+ </div>
203
+ <div v-if="getPersonContactOptions(item).length < 2 && getPersonPlateOptions(item).length < 2"
204
+ class="d-flex align-center justify-space-between ga-4 w-100">
205
+ <span class="text-subtitle-1">{{ getPersonContactOptions(item)[0] || 'N/A' }}</span>
206
+ <span class="text-subtitle-1 text-no-wrap">{{ getPersonPlateOptions(item)[0] || 'N/A' }}</span>
207
+ </div>
208
+ </v-list-item>
209
+ </v-list>
210
+ </v-menu>
161
211
  </v-col>
162
212
  </v-row>
163
213
  </v-col>
@@ -212,8 +262,8 @@
212
262
  <MemberInformation v-model="visitor.members" :type="prop.type" :contractor-type="visitor.contractorType"
213
263
  :site="prop.site" :selected-visitor-pass="visitor.visitorPass" :clearable="true"
214
264
  :settings="entryPassSettings" :settings-loading="entryPassSettingsPending || siteDataPending"
215
- :unit-id="visitor.unit || null"
216
- :hid-qr-code-enabled="hidQrCodePassAllowed" :hid-qr-printer-configured="hasHidQrPrinterConfig"
265
+ :unit-id="visitor.unit || null" :hid-qr-code-enabled="hidQrCodePassAllowed"
266
+ :hid-qr-printer-configured="hasHidQrPrinterConfig"
217
267
  :selected-nfc-cards="passType === 'NFC' ? passCards : []" />
218
268
  </v-col>
219
269
 
@@ -264,6 +314,47 @@
264
314
  @update:people="handleAutofillDataViaVehicleNumber" />
265
315
  </v-dialog>
266
316
 
317
+ <v-dialog v-model="dialog.nricAutofillOptions" max-width="430">
318
+ <v-card v-if="selectedNricSearchResult" class="nric-options-card">
319
+ <v-card-actions class="justify-end pa-1">
320
+ <v-btn icon="mdi-close" variant="text" size="small" @click="closeNricAutofillOptions" />
321
+ </v-card-actions>
322
+
323
+ <v-card-text class="pa-4 pt-1">
324
+ <h2 class="nric-options-title font-weight-bold mb-4">Select</h2>
325
+
326
+ <div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
327
+ <span>{{ selectedNricSearchResult.name || "Unknown" }}</span>
328
+ <span class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
329
+ </div>
330
+
331
+ <template v-if="selectedNricContactOptions.length > 1">
332
+ <h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
333
+ <v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
334
+ <v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
335
+ class="nric-option-radio" />
336
+ </v-radio-group>
337
+ </template>
338
+
339
+ <template v-if="selectedNricPlateOptions.length > 1">
340
+ <h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
341
+ <v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
342
+ <v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
343
+ class="nric-option-radio" />
344
+ </v-radio-group>
345
+ </template>
346
+ </v-card-text>
347
+
348
+ <v-card-actions class="justify-end px-4 pb-4">
349
+ <v-btn variant="text" class="text-none" size="small" @click="closeNricAutofillOptions">Cancel</v-btn>
350
+ <v-btn color="primary-button" variant="flat" class="text-none px-5" size="small"
351
+ @click="confirmNricAutofillOptions">
352
+ Select
353
+ </v-btn>
354
+ </v-card-actions>
355
+ </v-card>
356
+ </v-dialog>
357
+
267
358
  <v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="1200">
268
359
  <v-card :loading="loading.checkingOut || loading.searchingNonCheckedOut">
269
360
  <v-card-actions class="justify-end bg-grey-lighten-3 pa-4">
@@ -280,14 +371,8 @@
280
371
  <div>Phone Number: {{ visitor.contact || "N/A" }}</div>
281
372
  </div>
282
373
 
283
- <v-data-table
284
- class="border rounded"
285
- :headers="nonCheckedOutVisitorHeaders"
286
- :items="matchingNonCheckedOutVisitors"
287
- item-value="_id"
288
- :items-per-page="5"
289
- hide-default-footer
290
- >
374
+ <v-data-table class="border rounded" :headers="nonCheckedOutVisitorHeaders"
375
+ :items="matchingNonCheckedOutVisitors" item-value="_id" :items-per-page="5" hide-default-footer>
291
376
  <template #item.name="{ item }">
292
377
  <div class="d-flex align-center ga-3">
293
378
  <v-avatar color="red" size="32">
@@ -336,7 +421,9 @@
336
421
  <v-icon icon="mdi-clock-check-outline" color="green" size="20" class="mr-1" />
337
422
  {{ formatNonCheckedOutDate(item.checkIn) }}
338
423
  </div>
339
- <v-btn size="small" class="text-none" color="red" variant="flat" :loading="loading.checkingOut && selectedCheckoutVisitorId === item._id" :disabled="!item._id" @click.stop="item._id && handleCheckout(item._id)">
424
+ <v-btn size="small" class="text-none" color="red" variant="flat"
425
+ :loading="loading.checkingOut && selectedCheckoutVisitorId === item._id" :disabled="!item._id"
426
+ @click.stop="item._id && handleCheckout(item._id)">
340
427
  Check Out
341
428
  </v-btn>
342
429
  </div>
@@ -344,7 +431,8 @@
344
431
  </v-data-table>
345
432
 
346
433
  <div class="d-flex justify-end mt-8">
347
- <v-btn color="red" variant="flat" size="large" class="text-none px-10" :loading="loading.searchingNonCheckedOut" @click="handleSkipNricNonCheckedOut">
434
+ <v-btn color="red" variant="flat" size="large" class="text-none px-10"
435
+ :loading="loading.searchingNonCheckedOut" @click="handleSkipNricNonCheckedOut">
348
436
  Skip NRIC: {{ visitor.nric || "N/A" }}
349
437
  </v-btn>
350
438
  </div>
@@ -352,20 +440,10 @@
352
440
  </v-card>
353
441
  </v-dialog>
354
442
 
355
- <v-dialog
356
- v-if="enableUnregistered"
357
- v-model="dialog.showUnregisteredPlateDialog"
358
- max-width="900"
359
- persistent
360
- >
443
+ <v-dialog v-if="enableUnregistered" v-model="dialog.showUnregisteredPlateDialog" max-width="900" persistent>
361
444
  <v-card color="background" class="pa-4">
362
445
  <v-card-actions class="justify-end pa-0">
363
- <v-btn
364
- icon="mdi-close"
365
- variant="text"
366
- size="large"
367
- @click="closeUnregisteredPlateDialog"
368
- />
446
+ <v-btn icon="mdi-close" variant="text" size="large" @click="closeUnregisteredPlateDialog" />
369
447
  </v-card-actions>
370
448
 
371
449
  <v-card-text class="text-center pt-2">
@@ -388,10 +466,7 @@
388
466
  </tr>
389
467
  </thead>
390
468
  <tbody>
391
- <tr
392
- v-for="match in unregisteredPlateMatches"
393
- :key="match._id || match.plateNumber"
394
- >
469
+ <tr v-for="match in unregisteredPlateMatches" :key="match._id || match.plateNumber">
395
470
  <td class="text-left font-weight-bold text-uppercase">
396
471
  {{ match.plateNumber || "N/A" }}
397
472
  </td>
@@ -399,13 +474,8 @@
399
474
  {{ formatUnregisteredCheckIn(match.checkIn) }}
400
475
  </td>
401
476
  <td class="text-center">
402
- <v-btn
403
- color="red"
404
- class="text-capitalize"
405
- density="compact"
406
- size="large"
407
- @click="selectUnregisteredPlateMatch(match)"
408
- >
477
+ <v-btn color="red" class="text-capitalize" density="compact" size="large"
478
+ @click="selectUnregisteredPlateMatch(match)">
409
479
  Update
410
480
  </v-btn>
411
481
  </td>
@@ -415,13 +485,7 @@
415
485
  </v-card-text>
416
486
 
417
487
  <v-card-actions>
418
- <v-btn
419
- block
420
- color="red"
421
- variant="flat"
422
- size="x-large"
423
- @click="closeUnregisteredPlateDialog"
424
- >
488
+ <v-btn block color="red" variant="flat" size="x-large" @click="closeUnregisteredPlateDialog">
425
489
  Cancel & Add New
426
490
  </v-btn>
427
491
  </v-card-actions>
@@ -541,6 +605,7 @@ const hidQrPrinted = ref(false);
541
605
  const registeredUnitCompanyName = ref("N/A");
542
606
 
543
607
  const dialog = reactive({
608
+ nricAutofillOptions: false,
544
609
  vehicleNumberUsersList: false,
545
610
  showNonCheckedOutDialog: false,
546
611
  showUnregisteredPlateDialog: false,
@@ -635,6 +700,10 @@ const requireNRIC = computed(() => {
635
700
  return prop.type !== "walk-in" && prop.type !== "guest";
636
701
  });
637
702
 
703
+ const isVehicleRequired = computed(() => {
704
+ return ["guest", "drop-off", "pick-up"].includes(prop.type);
705
+ });
706
+
638
707
  const hidQrCodePassConfig = computed(() => {
639
708
  return (siteData.value as any)?.metadata?.hidQrCodePass || {};
640
709
  });
@@ -689,6 +758,58 @@ const companyAutofillDataArray = ref<
689
758
  const nricSearchMenu = ref(false);
690
759
  const nricSearchResults = ref<Partial<TPeople>[]>([]);
691
760
  const skipNricFetch = ref(false);
761
+ const phoneSearchMenu = ref(false);
762
+ const phoneSearchResults = ref<Partial<TPeople>[]>([]);
763
+ const vehicleSearchMenu = ref(false);
764
+ const vehicleSearchResults = ref<Partial<TPeople>[]>([]);
765
+ const selectedNricSearchResult = ref<Partial<TPeople> | null>(null);
766
+ const selectedNricContact = ref("");
767
+ const selectedNricPlate = ref("");
768
+
769
+ type NricSearchPerson = Omit<Partial<TPeople>, "contact"> & {
770
+ contact?: string | string[];
771
+ contacts?: string | string[];
772
+ visitorPlateNumbers?: string | string[];
773
+ plateNumber?: string | string[];
774
+ };
775
+
776
+ const mockPhoneNumberSearchResults: Partial<NricSearchPerson>[] = [
777
+ {
778
+ _id: "mock-phone-multiple-options",
779
+ name: "James Smith",
780
+ nric: "1323232499",
781
+ contacts: ["653939394", "81234567"],
782
+ visitorPlateNumbers: ["34566", "SMA1234A"],
783
+ companyName: ["Mock Company Pte Ltd"],
784
+ },
785
+ {
786
+ _id: "mock-phone-single-option",
787
+ name: "Jane Tan",
788
+ nric: "S1234567A",
789
+ contacts: ["91234567"],
790
+ visitorPlateNumbers: ["SGB5678B"],
791
+ companyName: ["Single Option Company"],
792
+ },
793
+ ];
794
+
795
+ const mockVehicleNumberSearchResults: Partial<NricSearchPerson>[] = [
796
+ {
797
+ _id: "mock-vehicle-multiple-options",
798
+ name: "James Smith",
799
+ nric: "1323232499",
800
+ contacts: ["653939394", "81234567"],
801
+ visitorPlateNumbers: ["34566", "SMA1234A"],
802
+ companyName: ["Mock Company Pte Ltd"],
803
+ },
804
+ {
805
+ _id: "mock-vehicle-single-option",
806
+ name: "Ali Lim",
807
+ nric: "T7654321B",
808
+ contacts: ["98765432"],
809
+ visitorPlateNumbers: ["SKX2468C"],
810
+ companyName: ["Vehicle Single Company"],
811
+ },
812
+ ];
692
813
 
693
814
  const formTitle = computed(() => {
694
815
  const isContractorForm = prop.type === "contractor";
@@ -768,6 +889,29 @@ watch(fetchPersonByNRICReq, (obj) => {
768
889
  const items = obj?.items ?? [];
769
890
  nricSearchResults.value = Array.isArray(items) ? items : [];
770
891
  nricSearchMenu.value = nricSearchResults.value.length > 0;
892
+
893
+ // Mock data for testing NRIC autofill options.
894
+ // Uncomment this block and type at least 4 characters in NRIC to test
895
+ // both the multi-option dialog and immediate single-option autofill.
896
+ // nricSearchResults.value = [
897
+ // {
898
+ // _id: "mock-person-multiple-options",
899
+ // name: "James Smith",
900
+ // nric: "1323232499",
901
+ // contacts: ["653939394", "81234567"],
902
+ // visitorPlateNumbers: ["34566", "SMA1234A"],
903
+ // companyName: ["Mock Company Pte Ltd"],
904
+ // } as Partial<NricSearchPerson>,
905
+ // {
906
+ // _id: "mock-person-single-option",
907
+ // name: "Jane Tan",
908
+ // nric: "S1234567A",
909
+ // contacts: ["91234567"],
910
+ // visitorPlateNumbers: ["SGB5678B"],
911
+ // companyName: ["Single Option Company"],
912
+ // } as Partial<NricSearchPerson>,
913
+ // ];
914
+ // nricSearchMenu.value = true;
771
915
  });
772
916
 
773
917
  watch(
@@ -783,21 +927,68 @@ watch(
783
927
 
784
928
  watch(
785
929
  () => visitor.contact,
786
- () => {
930
+ (value) => {
787
931
  skipNonCheckedOutNric.value = false;
932
+ if (!value || value.length < 4) {
933
+ phoneSearchMenu.value = false;
934
+ phoneSearchResults.value = [];
935
+ }
788
936
  }
789
937
  );
790
938
 
791
- function selectNricSearchResult(item: Partial<TPeople>) {
939
+ const selectedNricContactOptions = computed(() => {
940
+ return selectedNricSearchResult.value
941
+ ? getPersonContactOptions(selectedNricSearchResult.value)
942
+ : [];
943
+ });
944
+
945
+ const selectedNricPlateOptions = computed(() => {
946
+ return selectedNricSearchResult.value
947
+ ? getPersonPlateOptions(selectedNricSearchResult.value)
948
+ : [];
949
+ });
950
+
951
+ function toUniqueStringArray(value: unknown) {
952
+ const rawValues = Array.isArray(value) ? value : value ? [value] : [];
953
+ return [...new Set(rawValues.map((v) => String(v).trim()).filter(Boolean))];
954
+ }
955
+
956
+ function getPersonContactOptions(item: Partial<TPeople>) {
957
+ const person = item as NricSearchPerson;
958
+ return toUniqueStringArray(person.contacts || person.contact);
959
+ }
960
+
961
+ function getPersonPlateOptions(item: Partial<TPeople>) {
962
+ const person = item as NricSearchPerson;
963
+ const visitorPlateNumbers = toUniqueStringArray(person.visitorPlateNumbers);
964
+ if (visitorPlateNumbers.length) return visitorPlateNumbers;
965
+
966
+ const plateNumbers = toUniqueStringArray(person.plateNumber);
967
+ const plates = Array.isArray(person.plates)
968
+ ? person.plates.map((plate) => plate?.plateNumber)
969
+ : [];
970
+
971
+ return toUniqueStringArray([...plateNumbers, ...plates]);
972
+ }
973
+
974
+ function applyNricSearchResult(
975
+ item: Partial<TPeople>,
976
+ options: { contacts?: string; plateNumber?: string; source?: Exclude<AutofillSource, null> } = {}
977
+ ) {
792
978
  nricSearchMenu.value = false;
979
+ phoneSearchMenu.value = false;
980
+ vehicleSearchMenu.value = false;
793
981
  nricSearchResults.value = [];
982
+ phoneSearchResults.value = [];
983
+ vehicleSearchResults.value = [];
794
984
  skipNricFetch.value = true;
795
985
 
796
- currentAutofillSource.value = "nric";
986
+ currentAutofillSource.value = options.source || "nric";
797
987
 
798
988
  visitor.nric = item.nric || visitor.nric;
799
989
  visitor.name = item.name || visitor.name;
800
- visitor.contact = item.contact || visitor.contact;
990
+ visitor.contact = options.contacts || getPersonContactOptions(item)[0] || visitor.contact;
991
+ visitor.plateNumber = options.plateNumber || getPersonPlateOptions(item)[0] || visitor.plateNumber;
801
992
  companyNames.value = item.companyName ?? [];
802
993
  if (!visitor.company) {
803
994
  visitor.company = companyNames.value?.[0] || visitor.company;
@@ -813,6 +1004,51 @@ function selectNricSearchResult(item: Partial<TPeople>) {
813
1004
  }, 1000);
814
1005
  }
815
1006
 
1007
+ function selectNricSearchResult(item: Partial<TPeople>) {
1008
+ selectAutofillSearchResult(item, "nric");
1009
+ }
1010
+
1011
+ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<AutofillSource, null>) {
1012
+ const contactOptions = getPersonContactOptions(item);
1013
+ const plateOptions = getPersonPlateOptions(item);
1014
+
1015
+ if (contactOptions.length > 1 || plateOptions.length > 1) {
1016
+ nricSearchMenu.value = false;
1017
+ phoneSearchMenu.value = false;
1018
+ vehicleSearchMenu.value = false;
1019
+ currentAutofillSource.value = source;
1020
+ selectedNricSearchResult.value = item;
1021
+ selectedNricContact.value = contactOptions[0] || "";
1022
+ selectedNricPlate.value = plateOptions[0] || "";
1023
+ dialog.nricAutofillOptions = true;
1024
+ return;
1025
+ }
1026
+
1027
+ applyNricSearchResult(item, {
1028
+ contacts: contactOptions[0],
1029
+ plateNumber: plateOptions[0],
1030
+ source,
1031
+ });
1032
+ }
1033
+
1034
+ function closeNricAutofillOptions() {
1035
+ dialog.nricAutofillOptions = false;
1036
+ selectedNricSearchResult.value = null;
1037
+ selectedNricContact.value = "";
1038
+ selectedNricPlate.value = "";
1039
+ }
1040
+
1041
+ function confirmNricAutofillOptions() {
1042
+ if (!selectedNricSearchResult.value) return;
1043
+
1044
+ applyNricSearchResult(selectedNricSearchResult.value, {
1045
+ contacts: selectedNricContact.value,
1046
+ plateNumber: selectedNricPlate.value,
1047
+ source: currentAutofillSource.value || "nric",
1048
+ });
1049
+ closeNricAutofillOptions();
1050
+ }
1051
+
816
1052
  const {
817
1053
  data: fetchCompanyListReq,
818
1054
  refresh: fetchCompanyListRefresh,
@@ -1164,6 +1400,44 @@ function handleUpdateNRIC() {
1164
1400
  debounceFetchNRIC();
1165
1401
  }
1166
1402
 
1403
+ function handleUpdatePhoneNumber() {
1404
+ if (currentAutofillSource.value && currentAutofillSource.value !== "contact")
1405
+ return;
1406
+
1407
+ const search = visitor.contact?.trim() || "";
1408
+ if (search.length < 4) {
1409
+ phoneSearchResults.value = [];
1410
+ phoneSearchMenu.value = false;
1411
+ return;
1412
+ }
1413
+
1414
+ phoneSearchResults.value = mockPhoneNumberSearchResults.filter((item) =>
1415
+ getPersonContactOptions(item as Partial<TPeople>).some((contact) =>
1416
+ contact.includes(search)
1417
+ )
1418
+ ) as Partial<TPeople>[];
1419
+ phoneSearchMenu.value = phoneSearchResults.value.length > 0;
1420
+ }
1421
+
1422
+ function handleUpdateVehicleNumber() {
1423
+ if (currentAutofillSource.value && currentAutofillSource.value !== "vehicleNumber")
1424
+ return;
1425
+
1426
+ const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
1427
+ if (search.length < 4) {
1428
+ vehicleSearchResults.value = [];
1429
+ vehicleSearchMenu.value = false;
1430
+ return;
1431
+ }
1432
+
1433
+ vehicleSearchResults.value = mockVehicleNumberSearchResults.filter((item) =>
1434
+ getPersonPlateOptions(item as Partial<TPeople>).some((plate) =>
1435
+ plate.toUpperCase().includes(search)
1436
+ )
1437
+ ) as Partial<TPeople>[];
1438
+ vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
1439
+ }
1440
+
1167
1441
  function backToSelection() {
1168
1442
  emit("back");
1169
1443
  message.value = "";
@@ -1383,7 +1657,7 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1383
1657
  if (formMode.value === "register") {
1384
1658
  payload.status = "registered";
1385
1659
  const currentUserId = currentUser.value?._id;
1386
- if(currentUserId) {
1660
+ if (currentUserId) {
1387
1661
  payload.updatedBy = currentUserId;
1388
1662
  }
1389
1663
  }
@@ -1704,6 +1978,10 @@ watch(
1704
1978
  skipUnregisteredPlateCheck.value = false;
1705
1979
  unregisteredPlateMatches.value = [];
1706
1980
  dialog.showUnregisteredPlateDialog = false;
1981
+ if (!normalizedPlateNumber || normalizedPlateNumber.length < 4) {
1982
+ vehicleSearchMenu.value = false;
1983
+ vehicleSearchResults.value = [];
1984
+ }
1707
1985
  }
1708
1986
  );
1709
1987
 
@@ -1756,4 +2034,52 @@ onMounted(() => {
1756
2034
  .no-pointer {
1757
2035
  pointer-events: none;
1758
2036
  }
2037
+
2038
+ .nric-search-item {
2039
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2040
+ border-radius: 4px;
2041
+ margin: 4px;
2042
+ min-height: 72px;
2043
+ }
2044
+
2045
+ .nric-options-card {
2046
+ border-radius: 16px !important;
2047
+ }
2048
+
2049
+ .nric-options-title {
2050
+ color: #06253b;
2051
+ font-size: 1.45rem;
2052
+ line-height: 1.2;
2053
+ }
2054
+
2055
+ .nric-options-section-title {
2056
+ color: #06253b;
2057
+ font-size: 1.05rem;
2058
+ line-height: 1.25;
2059
+ }
2060
+
2061
+ .nric-selected-person {
2062
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2063
+ border-radius: 12px;
2064
+ color: #06253b;
2065
+ font-size: 0.95rem;
2066
+ min-height: 52px;
2067
+ padding: 10px 14px;
2068
+ }
2069
+
2070
+ .nric-option-group {
2071
+ border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2072
+ }
2073
+
2074
+ .nric-option-radio {
2075
+ border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
2076
+ color: #06253b;
2077
+ font-size: 0.95rem;
2078
+ min-height: 46px;
2079
+ }
2080
+
2081
+ .nric-option-radio :deep(.v-label) {
2082
+ font-size: 0.95rem;
2083
+ opacity: 1;
2084
+ }
1759
2085
  </style>