@7365admin1/layer-common 3.0.31-staging.16 → 3.0.31-staging.18

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.
@@ -548,7 +548,7 @@
548
548
  variant="flat"
549
549
  class="text-none sp-invite-submit-btn"
550
550
  height="76"
551
- :disabled="!validServiceProvider"
551
+ :disabled="!canSubmitInvite"
552
552
  :loading="disableServiceProvider"
553
553
  @click="submitServiceProviderInvite()"
554
554
  >
@@ -574,6 +574,7 @@ type ListTab = "active" | "pending" | "inactive";
574
574
 
575
575
  type TableRow = {
576
576
  _id: string;
577
+ userId: string;
577
578
  serviceProviderOrgId: string;
578
579
  companyName: string;
579
580
  email: string;
@@ -733,11 +734,10 @@ const {
733
734
  updateStatus: updateServiceProviderStatus,
734
735
  invite,
735
736
  } = useServiceProvider();
736
- const { getAll: getAllMembers } = useMember();
737
+ const { getAllByUserId } = useMember();
737
738
 
738
739
  const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
739
740
  useVerification();
740
- const { getOrgsByMembership } = useMember();
741
741
 
742
742
  const existingAccount = ref<any>(null);
743
743
  const checkingExistingAccount = ref(false);
@@ -757,11 +757,10 @@ const createInviteDialog = ref(false);
757
757
  // const loadingInviteRoles = ref(false);
758
758
  // const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
759
759
 
760
- const { getByUserType } = useMember();
761
- const { getOrgs } = useOrg();
762
760
 
763
761
  async function checkExistingAccount(email: string) {
764
762
  existingAccount.value = null;
763
+ messageServiceProvider.value = "";
765
764
 
766
765
  if (!email.trim() || emailRule(email) !== true) return;
767
766
 
@@ -770,30 +769,50 @@ async function checkExistingAccount(email: string) {
770
769
  try {
771
770
  const user = await getUserByEmail(email);
772
771
 
773
- if (!user?._id) return;
772
+ if (!user?._id) return;
774
773
 
775
- const orgs = await getOrgs({
776
- user: user._id,
777
- });
774
+ const members = await getAllByUserId(user._id);
778
775
 
779
- const matchedOrg = orgs.items?.find(
780
- (org: any) => org.type === serviceProviderInvite.value.app
781
- );
776
+ const appMember = (members.items ?? []).find(
777
+ (member: any) => member.type === serviceProviderInvite.value.app
778
+ );
779
+
780
+ if (!appMember) {
781
+ existingAccount.value = null;
782
+ return;
783
+ }
784
+
785
+ if (
786
+ appMember.org === props.orgId &&
787
+ (appMember.siteId || "") === (props.siteId || "")
788
+ ) {
789
+ existingAccount.value = null;
790
+ messageServiceProvider.value =
791
+ "This user is already a member of this service provider.";
792
+ return;
793
+ }
794
+
795
+ existingAccount.value = {
796
+ _id: appMember.org,
797
+ text: appMember.orgName,
798
+ org: appMember.org,
799
+ orgName: appMember.orgName,
800
+ siteId: appMember.siteId,
801
+ siteName: appMember.siteName,
802
+ };
782
803
 
783
- if (matchedOrg) {
784
- existingAccount.value = {
785
- _id: matchedOrg.value,
786
- text: matchedOrg.text,
787
- ...matchedOrg,
788
- };
789
- }
790
804
  } catch (error) {
791
805
  existingAccount.value = null;
792
806
  } finally {
793
807
  checkingExistingAccount.value = false;
794
808
  }
795
809
  }
796
-
810
+ const canSubmitInvite = computed(() => {
811
+ return (
812
+ validServiceProvider.value &&
813
+ !messageServiceProvider.value
814
+ );
815
+ });
797
816
  watch(
798
817
  () => serviceProviderInvite.value.email,
799
818
  async (value) => {
@@ -939,6 +958,7 @@ function formatEmailCell(value: unknown): string {
939
958
  function mapProviderRows(raw: Record<string, any>[]): TableRow[] {
940
959
  return raw.map((row) => ({
941
960
  _id: String(row._id ?? ""),
961
+ userId: String(row.user ?? ""),
942
962
  serviceProviderOrgId: String(row.serviceProviderOrgId ?? ""),
943
963
  companyName: row.name ?? "-",
944
964
  email: formatEmailCell(row.email),
@@ -961,6 +981,7 @@ function inviteTypeLabel(t: string) {
961
981
  function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
962
982
  return raw.map((row) => ({
963
983
  _id: String(row._id ?? ""),
984
+ userId: String(row.user ?? ""),
964
985
  serviceProviderOrgId: String(row.metadata?.serviceProviderOrgId ?? ""),
965
986
  companyName: row.metadata?.name || "-",
966
987
  email: formatEmailCell(row.email),
@@ -1029,16 +1050,12 @@ async function openView(row: TableRow) {
1029
1050
 
1030
1051
  loadingMembers.value = true;
1031
1052
  try {
1032
- const data = await getAllMembers({
1033
- page: 1,
1034
- limit: 1000,
1035
- org: row.serviceProviderOrgId,
1036
- status: "active",
1037
- });
1053
+ const data = await getAllByUserId(row.userId);
1054
+
1038
1055
  serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
1039
- _id: String(member._id ?? ""),
1040
- name: member.name || member.fullName || member.email || "-",
1041
- role: member.roleName || member.role?.name || "-",
1056
+ _id: member._id,
1057
+ name: member.name || "-",
1058
+ role: member.role || "-",
1042
1059
  }));
1043
1060
  } catch (error: any) {
1044
1061
  showMessage(errorConverter(error), "error");
@@ -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">
@@ -321,7 +311,7 @@
321
311
  <span class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
322
312
  </div>
323
313
 
324
- <template v-if="selectedNricContactOptions.length > 1">
314
+ <template v-if="selectedNricContactOptions.length >= 1">
325
315
  <h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
326
316
  <v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
327
317
  <v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
@@ -329,7 +319,7 @@
329
319
  </v-radio-group>
330
320
  </template>
331
321
 
332
- <template v-if="selectedNricPlateOptions.length > 1">
322
+ <template v-if="selectedNricPlateOptions.length >= 1">
333
323
  <h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
334
324
  <v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
335
325
  <v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
@@ -499,7 +489,6 @@ import useVisitor from "../composables/useVisitor";
499
489
  import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
500
490
  import useAccessManagement from "../composables/useAccessManagement";
501
491
  import usePeople from "../composables/usePeople";
502
- import { mockPhoneNumberSearchResults, mockVehicleNumberSearchResults } from "../utils/mock-people-response";
503
492
 
504
493
  const prop = defineProps({
505
494
  type: {
@@ -556,8 +545,8 @@ const {
556
545
  findPersonByNRIC,
557
546
  findPersonByNRICMultipleResult,
558
547
  findPersonByPhoneNumberMultipleResult,
548
+ findPersonByPlateNumberMultipleResult,
559
549
  searchCompanyList,
560
- findUsersByPlateNumber,
561
550
  } = usePeople();
562
551
  const { getById: getUnitDataById } = useBuildingUnit();
563
552
 
@@ -767,10 +756,18 @@ const selectedNricPlate = ref("");
767
756
  type SearchPerson = Omit<Partial<TPeople>, "contact"> & {
768
757
  contact?: string | string[];
769
758
  contacts?: string | string[];
770
- visitorPlateNumbers?: string | string[];
759
+ plateNumbers?: string | string[];
771
760
  plateNumber?: string | string[];
772
761
  };
773
762
 
763
+ function getPeopleSearchItems(res: any): Partial<TPeople>[] {
764
+ if (Array.isArray(res?.items)) return res.items;
765
+ if (Array.isArray(res?.data)) return res.data;
766
+ if (Array.isArray(res?.data?.items)) return res.data.items;
767
+ if (res?.data) return [res.data];
768
+ if (res && !res?.message) return [res];
769
+ return [];
770
+ }
774
771
 
775
772
  const formTitle = computed(() => {
776
773
  const isContractorForm = prop.type === "contractor";
@@ -843,12 +840,13 @@ const {
843
840
  } = useLazyAsyncData(`fetch-person`, () => {
844
841
  const NRIC = visitor.nric;
845
842
  if (!NRIC || NRIC.length < 1) return Promise.resolve(null);
846
- return findPersonByNRICMultipleResult(NRIC, prop.site);
843
+
844
+ const type = prop.type === 'walk-in' ? 'guest' : prop.type
845
+ return findPersonByNRICMultipleResult(NRIC, prop.site, type);
847
846
  });
848
847
 
849
848
  watch(fetchPersonByNRICReq, (obj) => {
850
- const items = obj?.items ?? [];
851
- nricSearchResults.value = Array.isArray(items) ? items : [];
849
+ nricSearchResults.value = getPeopleSearchItems(obj);
852
850
  nricSearchMenu.value = nricSearchResults.value.length > 0;
853
851
 
854
852
  // Mock data for testing NRIC autofill options.
@@ -860,7 +858,7 @@ watch(fetchPersonByNRICReq, (obj) => {
860
858
  // name: "James Smith",
861
859
  // nric: "1323232499",
862
860
  // contacts: ["653939394", "81234567"],
863
- // visitorPlateNumbers: ["34566", "SMA1234A"],
861
+ // plateNumbers: ["34566", "SMA1234A"],
864
862
  // companyName: ["Mock Company Pte Ltd"],
865
863
  // } as Partial<SearchPerson>,
866
864
  // {
@@ -868,7 +866,7 @@ watch(fetchPersonByNRICReq, (obj) => {
868
866
  // name: "Jane Tan",
869
867
  // nric: "S1234567A",
870
868
  // contacts: ["91234567"],
871
- // visitorPlateNumbers: ["SGB5678B"],
869
+ // plateNumbers: ["SGB5678B"],
872
870
  // companyName: ["Single Option Company"],
873
871
  // } as Partial<SearchPerson>,
874
872
  // ];
@@ -921,7 +919,7 @@ function getPersonContactOptions(item: Partial<TPeople>) {
921
919
 
922
920
  function getPersonPlateOptions(item: Partial<TPeople>) {
923
921
  const person = item as SearchPerson;
924
- const visitorPlateNumbers = toUniqueStringArray(person.visitorPlateNumbers);
922
+ const visitorPlateNumbers = toUniqueStringArray(person.plateNumbers);
925
923
  if (visitorPlateNumbers.length) return visitorPlateNumbers;
926
924
 
927
925
  const plateNumbers = toUniqueStringArray(person.plateNumber);
@@ -932,6 +930,28 @@ function getPersonPlateOptions(item: Partial<TPeople>) {
932
930
  return toUniqueStringArray([...plateNumbers, ...plates]);
933
931
  }
934
932
 
933
+ function findMatchingContactOption(options: string[], value?: string | null) {
934
+ const normalizedValue = value?.trim();
935
+ if (!normalizedValue) return "";
936
+
937
+ return (
938
+ options.find((option) => option.trim() === normalizedValue) ||
939
+ options.find((option) => option.replace(/\s/g, "") === normalizedValue.replace(/\s/g, "")) ||
940
+ ""
941
+ );
942
+ }
943
+
944
+ function normalizePlateOption(value?: string | null) {
945
+ return (value || "").replace(/[^A-Z0-9]/gi, "").toUpperCase();
946
+ }
947
+
948
+ function findMatchingPlateOption(options: string[], value?: string | null) {
949
+ const normalizedValue = normalizePlateOption(value);
950
+ if (!normalizedValue) return "";
951
+
952
+ return options.find((option) => normalizePlateOption(option) === normalizedValue) || "";
953
+ }
954
+
935
955
  function applyNricSearchResult(
936
956
  item: Partial<TPeople>,
937
957
  options: { contacts?: string; plateNumber?: string; source?: Exclude<AutofillSource, null> } = {}
@@ -978,22 +998,26 @@ function selectPlateNumberSearchResult(item: Partial<TPeople>) {
978
998
  function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<AutofillSource, null>) {
979
999
  const contactOptions = getPersonContactOptions(item);
980
1000
  const plateOptions = getPersonPlateOptions(item);
1001
+ const selectedContact =
1002
+ findMatchingContactOption(contactOptions, visitor.contact) || contactOptions[0] || "";
1003
+ const selectedPlate =
1004
+ findMatchingPlateOption(plateOptions, visitor.plateNumber) || plateOptions[0] || "";
981
1005
 
982
- if (source === "nric" && (contactOptions.length > 1 || plateOptions.length > 1)) {
1006
+ if (contactOptions.length > 1 || plateOptions.length > 1) {
983
1007
  nricSearchMenu.value = false;
984
1008
  phoneSearchMenu.value = false;
985
1009
  vehicleSearchMenu.value = false;
986
1010
  currentAutofillSource.value = source;
987
1011
  selectedNricSearchResult.value = item;
988
- selectedNricContact.value = contactOptions[0] || "";
989
- selectedNricPlate.value = plateOptions[0] || "";
1012
+ selectedNricContact.value = selectedContact;
1013
+ selectedNricPlate.value = selectedPlate;
990
1014
  dialog.nricAutofillOptions = true;
991
1015
  return;
992
1016
  }
993
1017
 
994
1018
  applyNricSearchResult(item, {
995
- contacts: contactOptions[0],
996
- plateNumber: plateOptions[0],
1019
+ contacts: selectedContact,
1020
+ plateNumber: selectedPlate,
997
1021
  source,
998
1022
  });
999
1023
  }
@@ -1362,7 +1386,8 @@ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500);
1362
1386
 
1363
1387
  function handleUpdateNRIC() {
1364
1388
  if (skipNricFetch.value) return;
1365
- if(!isNricFieldFocused.value) return;
1389
+ if (!isNricFieldFocused.value) return;
1390
+ if (visitor.nric && visitor.nric?.length < 4) return;
1366
1391
  debounceFetchNRIC();
1367
1392
  }
1368
1393
 
@@ -1374,35 +1399,59 @@ const {
1374
1399
  } = useLazyAsyncData(`fetch-person-phone-number`, () => {
1375
1400
  const contact = visitor.contact;
1376
1401
  if (!contact || contact.length < 4) return Promise.resolve(null);
1377
- return findPersonByPhoneNumberMultipleResult(contact, prop.site);
1402
+ const type = prop.type === 'walk-in' ? 'guest' : prop.type
1403
+ return findPersonByPhoneNumberMultipleResult(contact, prop.site, type);
1404
+ });
1405
+
1406
+ watch(fetchPersonByPhoneNumber, (obj) => {
1407
+ phoneSearchResults.value = getPeopleSearchItems(obj);
1408
+ phoneSearchMenu.value = phoneSearchResults.value.length > 0;
1378
1409
  });
1379
1410
 
1411
+ const debounceFetchPhoneNumber = debounce(fetchPersonByPhoneNumberRefresh, 500);
1412
+
1380
1413
  function handleUpdatePhoneNumber() {
1381
- if(!isPhoneFieldFocused.value) return;
1414
+ if (!isPhoneFieldFocused.value) return;
1382
1415
 
1383
1416
  const search = visitor.contact?.trim() || "";
1384
- if (search.length < 1) {
1417
+ if (search.length < 4) {
1385
1418
  phoneSearchResults.value = [];
1386
1419
  phoneSearchMenu.value = false;
1387
1420
  return;
1388
1421
  }
1389
1422
 
1390
- phoneSearchResults.value = mockPhoneNumberSearchResults
1391
- phoneSearchMenu.value = phoneSearchResults.value.length > 0;
1423
+ debounceFetchPhoneNumber();
1392
1424
  }
1393
1425
 
1426
+ const {
1427
+ data: fetchPersonByPlateNumber,
1428
+ refresh: fetchPersonByPlateNumberRefresh,
1429
+ pending: fetchPersonByPlateNumberPending,
1430
+ } = useLazyAsyncData(`fetch-person-plate-number`, () => {
1431
+ const plateNumber = visitor.plateNumber?.trim()?.toUpperCase() || "";
1432
+ if (plateNumber.length < 4) return Promise.resolve(null);
1433
+ const type = prop.type === 'walk-in' ? 'guest' : prop.type
1434
+ return findPersonByPlateNumberMultipleResult(plateNumber, prop.site, type);
1435
+ });
1436
+
1437
+ watch(fetchPersonByPlateNumber, (obj) => {
1438
+ vehicleSearchResults.value = getPeopleSearchItems(obj);
1439
+ vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
1440
+ });
1441
+
1442
+ const debounceFetchPlateNumber = debounce(fetchPersonByPlateNumberRefresh, 500);
1443
+
1394
1444
  function handleUpdateVehicleNumber() {
1395
- if(!isPlateNumberFieldFocused.value) return;
1445
+ if (!isPlateNumberFieldFocused.value) return;
1396
1446
 
1397
1447
  const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
1398
- if (search.length < 1) {
1448
+ if (search.length < 4) {
1399
1449
  vehicleSearchResults.value = [];
1400
1450
  vehicleSearchMenu.value = false;
1401
1451
  return;
1402
1452
  }
1403
1453
 
1404
- vehicleSearchResults.value = mockVehicleNumberSearchResults
1405
- vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
1454
+ debounceFetchPlateNumber();
1406
1455
  }
1407
1456
 
1408
1457
  function backToSelection() {
@@ -2009,6 +2058,11 @@ onMounted(() => {
2009
2058
  min-height: 72px;
2010
2059
  }
2011
2060
 
2061
+ .phone-search-results {
2062
+ max-height: 400px;
2063
+ overflow-y: auto;
2064
+ }
2065
+
2012
2066
  .nric-options-card {
2013
2067
  border-radius: 16px !important;
2014
2068
  }
@@ -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.16",
5
+ "version": "3.0.31-staging.18",
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;