@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="!
|
|
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 {
|
|
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
|
-
|
|
772
|
+
if (!user?._id) return;
|
|
774
773
|
|
|
775
|
-
|
|
776
|
-
user: user._id,
|
|
777
|
-
});
|
|
774
|
+
const members = await getAllByUserId(user._id);
|
|
778
775
|
|
|
779
|
-
|
|
780
|
-
|
|
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
|
|
1033
|
-
|
|
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:
|
|
1040
|
-
name: member.name ||
|
|
1041
|
-
role: member.
|
|
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"
|
|
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">
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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 (
|
|
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 =
|
|
989
|
-
selectedNricPlate.value =
|
|
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:
|
|
996
|
-
plateNumber:
|
|
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
|
-
|
|
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 <
|
|
1417
|
+
if (search.length < 4) {
|
|
1385
1418
|
phoneSearchResults.value = [];
|
|
1386
1419
|
phoneSearchMenu.value = false;
|
|
1387
1420
|
return;
|
|
1388
1421
|
}
|
|
1389
1422
|
|
|
1390
|
-
|
|
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
|
-
|
|
1445
|
+
if (!isPlateNumberFieldFocused.value) return;
|
|
1396
1446
|
|
|
1397
1447
|
const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
|
|
1398
|
-
if (search.length <
|
|
1448
|
+
if (search.length < 4) {
|
|
1399
1449
|
vehicleSearchResults.value = [];
|
|
1400
1450
|
vehicleSearchMenu.value = false;
|
|
1401
1451
|
return;
|
|
1402
1452
|
}
|
|
1403
1453
|
|
|
1404
|
-
|
|
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>
|
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;
|