@7365admin1/layer-common 3.0.31-staging.13 → 3.0.31-staging.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/DashboardMain.vue +202 -54
- package/components/Input/InputPhoneNumberV2.vue +89 -72
- package/components/Input/VehicleNumber.vue +14 -11
- package/components/VisitorForm.vue +44 -70
- package/composables/usePeople.ts +15 -0
- package/package.json +1 -1
- package/utils/mock-people-response.ts +38 -0
|
@@ -984,60 +984,44 @@
|
|
|
984
984
|
<div
|
|
985
985
|
v-for="item in formattedActivePatrolItems"
|
|
986
986
|
:key="item.id || item._id"
|
|
987
|
-
class="
|
|
988
|
-
style="
|
|
989
|
-
padding: 16px 20px;
|
|
990
|
-
border: 1px solid #e2e8f0;
|
|
991
|
-
border-radius: 12px;
|
|
992
|
-
margin-bottom: 12px;
|
|
993
|
-
background-color: #ffffff;
|
|
994
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
995
|
-
"
|
|
987
|
+
class="active-patrol-row"
|
|
996
988
|
>
|
|
997
|
-
<div
|
|
998
|
-
<strong
|
|
999
|
-
|
|
1000
|
-
display: block;
|
|
1001
|
-
font-size: 15px;
|
|
1002
|
-
font-weight: 700;
|
|
1003
|
-
color: #0b2f47;
|
|
1004
|
-
margin-bottom: 4px;
|
|
1005
|
-
"
|
|
1006
|
-
>{{ item.title || item.name }}</strong
|
|
1007
|
-
>
|
|
1008
|
-
<span style="font-size: 13px; color: #94a3b8">{{
|
|
1009
|
-
item.subtitle || item.location || ""
|
|
1010
|
-
}}</span>
|
|
989
|
+
<div class="active-patrol-main">
|
|
990
|
+
<strong>{{ item.title || item.name }}</strong>
|
|
991
|
+
<span>{{ item.subtitle || item.location || "" }}</span>
|
|
1011
992
|
</div>
|
|
1012
993
|
|
|
1013
|
-
<div
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
item.
|
|
1022
|
-
|
|
994
|
+
<div class="active-patrol-assignees">
|
|
995
|
+
<div class="active-patrol-assignee">
|
|
996
|
+
<AvatarMain :name="item.primaryAssignee" :size="24" />
|
|
997
|
+
<span>{{ item.primaryAssignee }}</span>
|
|
998
|
+
</div>
|
|
999
|
+
<span
|
|
1000
|
+
v-if="item.extraAssigneeCount > 0"
|
|
1001
|
+
class="active-patrol-more-pill active-patrol-tooltip"
|
|
1002
|
+
:data-tooltip="item.extraAssignees.join(', ')"
|
|
1003
|
+
>
|
|
1004
|
+
+{{ item.extraAssigneeCount }}
|
|
1005
|
+
</span>
|
|
1023
1006
|
</div>
|
|
1024
1007
|
|
|
1025
|
-
<div
|
|
1008
|
+
<div class="active-patrol-statuses">
|
|
1026
1009
|
<div
|
|
1027
|
-
|
|
1028
|
-
style="
|
|
1029
|
-
padding: 6px 16px;
|
|
1030
|
-
border-radius: 20px;
|
|
1031
|
-
font-size: 13px;
|
|
1032
|
-
font-weight: 500;
|
|
1033
|
-
text-transform: capitalize;
|
|
1034
|
-
"
|
|
1010
|
+
class="active-patrol-status-pill"
|
|
1011
|
+
:style="getPatrolStatusStyle(item.primaryStatus)"
|
|
1035
1012
|
>
|
|
1036
|
-
{{ item.
|
|
1013
|
+
{{ item.primaryStatus }}
|
|
1037
1014
|
</div>
|
|
1015
|
+
<span
|
|
1016
|
+
v-if="item.extraStatusCount > 0"
|
|
1017
|
+
class="active-patrol-more-pill active-patrol-tooltip"
|
|
1018
|
+
:data-tooltip="item.extraStatuses.join(', ')"
|
|
1019
|
+
>
|
|
1020
|
+
+{{ item.extraStatusCount }}
|
|
1021
|
+
</span>
|
|
1038
1022
|
</div>
|
|
1039
1023
|
|
|
1040
|
-
<div
|
|
1024
|
+
<div class="active-patrol-action">
|
|
1041
1025
|
<v-btn
|
|
1042
1026
|
variant="outlined"
|
|
1043
1027
|
class="text-none font-weight-medium"
|
|
@@ -2767,18 +2751,23 @@ const activePatrolItems = computed<any[]>(
|
|
|
2767
2751
|
|
|
2768
2752
|
const formattedActivePatrolItems = computed<any[]>(() => {
|
|
2769
2753
|
return activePatrolItems.value.map((item) => {
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
if (Array.isArray(person)) {
|
|
2776
|
-
person = person.join(", ");
|
|
2777
|
-
}
|
|
2754
|
+
const statuses = normalizeListValue(item.status || "Pending");
|
|
2755
|
+
const assignees = normalizeListValue(
|
|
2756
|
+
item.person || item.assignedTo || "Unassigned"
|
|
2757
|
+
);
|
|
2758
|
+
|
|
2778
2759
|
return {
|
|
2779
2760
|
...item,
|
|
2780
|
-
status,
|
|
2781
|
-
|
|
2761
|
+
status: statuses.join(", "),
|
|
2762
|
+
statuses,
|
|
2763
|
+
primaryStatus: statuses[0] || "Pending",
|
|
2764
|
+
extraStatuses: statuses.slice(1),
|
|
2765
|
+
extraStatusCount: Math.max(0, statuses.length - 1),
|
|
2766
|
+
person: assignees.join(", "),
|
|
2767
|
+
assignees,
|
|
2768
|
+
primaryAssignee: assignees[0] || "Unassigned",
|
|
2769
|
+
extraAssignees: assignees.slice(1),
|
|
2770
|
+
extraAssigneeCount: Math.max(0, assignees.length - 1),
|
|
2782
2771
|
};
|
|
2783
2772
|
});
|
|
2784
2773
|
});
|
|
@@ -3948,6 +3937,20 @@ function normalizeStatus(value: any) {
|
|
|
3948
3937
|
return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3949
3938
|
}
|
|
3950
3939
|
|
|
3940
|
+
function normalizeListValue(value: any) {
|
|
3941
|
+
if (Array.isArray(value)) {
|
|
3942
|
+
return value
|
|
3943
|
+
.flatMap((entry) => normalizeListValue(entry))
|
|
3944
|
+
.map((entry) => String(entry).trim())
|
|
3945
|
+
.filter(Boolean);
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
return String(value || "")
|
|
3949
|
+
.split(",")
|
|
3950
|
+
.map((entry) => entry.trim())
|
|
3951
|
+
.filter(Boolean);
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3951
3954
|
function getStatusTone(status: any) {
|
|
3952
3955
|
const s = String(status || "").toLowerCase();
|
|
3953
3956
|
if (
|
|
@@ -4415,6 +4418,151 @@ function formatDashboardTime(value?: string) {
|
|
|
4415
4418
|
border: 2px solid #f9c97c;
|
|
4416
4419
|
}
|
|
4417
4420
|
|
|
4421
|
+
.active-patrol-row {
|
|
4422
|
+
align-items: center;
|
|
4423
|
+
background-color: #ffffff;
|
|
4424
|
+
border: 1px solid #e2e8f0;
|
|
4425
|
+
border-radius: 8px;
|
|
4426
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
4427
|
+
display: grid;
|
|
4428
|
+
gap: 10px;
|
|
4429
|
+
grid-template-columns: 150px minmax(0, 1fr) max-content max-content;
|
|
4430
|
+
margin-bottom: 12px;
|
|
4431
|
+
min-height: 64px;
|
|
4432
|
+
overflow: visible;
|
|
4433
|
+
padding: 12px 14px;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
.active-patrol-main {
|
|
4437
|
+
min-width: 0;
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
.active-patrol-main strong {
|
|
4441
|
+
color: #0b2f47;
|
|
4442
|
+
display: block;
|
|
4443
|
+
font-size: 13px;
|
|
4444
|
+
font-weight: 700;
|
|
4445
|
+
line-height: 1.25;
|
|
4446
|
+
margin-bottom: 3px;
|
|
4447
|
+
overflow-wrap: anywhere;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
.active-patrol-main span {
|
|
4451
|
+
color: #94a3b8;
|
|
4452
|
+
display: block;
|
|
4453
|
+
font-size: 11px;
|
|
4454
|
+
line-height: 1.25;
|
|
4455
|
+
overflow-wrap: anywhere;
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
.active-patrol-assignees {
|
|
4459
|
+
align-items: center;
|
|
4460
|
+
display: flex;
|
|
4461
|
+
gap: 6px;
|
|
4462
|
+
min-width: 0;
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
.active-patrol-assignee {
|
|
4466
|
+
align-items: center;
|
|
4467
|
+
background: #f8fafc;
|
|
4468
|
+
border: 1px solid #e7edf3;
|
|
4469
|
+
border-radius: 999px;
|
|
4470
|
+
display: inline-flex;
|
|
4471
|
+
flex: 1 1 auto;
|
|
4472
|
+
gap: 6px;
|
|
4473
|
+
max-width: 100%;
|
|
4474
|
+
min-width: 0;
|
|
4475
|
+
padding: 2px 8px 2px 3px;
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
.active-patrol-assignee span {
|
|
4479
|
+
color: #475569;
|
|
4480
|
+
font-size: 11px;
|
|
4481
|
+
font-weight: 600;
|
|
4482
|
+
line-height: 1.2;
|
|
4483
|
+
min-width: 0;
|
|
4484
|
+
overflow: hidden;
|
|
4485
|
+
text-overflow: ellipsis;
|
|
4486
|
+
white-space: nowrap;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
.active-patrol-statuses {
|
|
4490
|
+
align-items: center;
|
|
4491
|
+
display: flex;
|
|
4492
|
+
gap: 6px;
|
|
4493
|
+
justify-content: flex-start;
|
|
4494
|
+
min-width: 0;
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
.active-patrol-status-pill {
|
|
4498
|
+
border-radius: 20px;
|
|
4499
|
+
font-size: 11px;
|
|
4500
|
+
font-weight: 600;
|
|
4501
|
+
line-height: 1.2;
|
|
4502
|
+
padding: 5px 10px;
|
|
4503
|
+
text-transform: capitalize;
|
|
4504
|
+
white-space: nowrap;
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
.active-patrol-more-pill {
|
|
4508
|
+
align-items: center;
|
|
4509
|
+
background: #eef3f8;
|
|
4510
|
+
border: 1px solid #dbe4ed;
|
|
4511
|
+
border-radius: 999px;
|
|
4512
|
+
color: #607386;
|
|
4513
|
+
display: inline-flex;
|
|
4514
|
+
flex: 0 0 auto;
|
|
4515
|
+
font-size: 11px;
|
|
4516
|
+
font-weight: 800;
|
|
4517
|
+
height: 24px;
|
|
4518
|
+
justify-content: center;
|
|
4519
|
+
line-height: 1;
|
|
4520
|
+
min-width: 30px;
|
|
4521
|
+
padding: 0 8px;
|
|
4522
|
+
position: relative;
|
|
4523
|
+
white-space: nowrap;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
.active-patrol-action {
|
|
4527
|
+
justify-self: end;
|
|
4528
|
+
text-align: right;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
.active-patrol-tooltip::after {
|
|
4532
|
+
background: #0f2638;
|
|
4533
|
+
border-radius: 6px;
|
|
4534
|
+
bottom: calc(100% + 8px);
|
|
4535
|
+
box-shadow: 0 8px 24px rgba(15, 38, 56, 0.18);
|
|
4536
|
+
color: #ffffff;
|
|
4537
|
+
content: attr(data-tooltip);
|
|
4538
|
+
font-size: 11px;
|
|
4539
|
+
font-weight: 600;
|
|
4540
|
+
left: 50%;
|
|
4541
|
+
line-height: 1.35;
|
|
4542
|
+
max-width: 260px;
|
|
4543
|
+
opacity: 0;
|
|
4544
|
+
padding: 7px 9px;
|
|
4545
|
+
pointer-events: none;
|
|
4546
|
+
position: absolute;
|
|
4547
|
+
text-align: left;
|
|
4548
|
+
transform: translate(-50%, 4px);
|
|
4549
|
+
transition: opacity 0.14s ease, transform 0.14s ease;
|
|
4550
|
+
white-space: normal;
|
|
4551
|
+
width: max-content;
|
|
4552
|
+
z-index: 5;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
.active-patrol-tooltip:hover::after {
|
|
4556
|
+
opacity: 1;
|
|
4557
|
+
transform: translate(-50%, 0);
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
@media (max-width: 960px) {
|
|
4561
|
+
.active-patrol-row {
|
|
4562
|
+
grid-template-columns: 120px minmax(0, 1fr) max-content max-content;
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4418
4566
|
.figma-icon-badge {
|
|
4419
4567
|
align-items: center;
|
|
4420
4568
|
border-radius: 50%;
|
|
@@ -46,136 +46,153 @@
|
|
|
46
46
|
:density="density"
|
|
47
47
|
:disabled="props.disabled"
|
|
48
48
|
:placeholder="placeholder || currentMask"
|
|
49
|
+
@focus="emit('focus')"
|
|
50
|
+
@blur="emit('blur')"
|
|
49
51
|
/>
|
|
50
52
|
</v-col>
|
|
51
|
-
<span
|
|
53
|
+
<span
|
|
54
|
+
class="text-error text-caption w-100"
|
|
55
|
+
v-if="errorMessage && !hideDetails"
|
|
56
|
+
>
|
|
52
57
|
{{ errorMessage }}
|
|
53
58
|
</span>
|
|
54
59
|
</v-row>
|
|
55
60
|
</template>
|
|
56
61
|
|
|
57
62
|
<script setup lang="ts">
|
|
58
|
-
import { ref, computed, watch, type PropType } from
|
|
63
|
+
import { ref, computed, watch, type PropType } from "vue";
|
|
59
64
|
//@ts-ignore
|
|
60
|
-
import phoneMasks from
|
|
61
|
-
import type { ValidationRule } from
|
|
65
|
+
import phoneMasks from "~/utils/phoneMasks";
|
|
66
|
+
import type { ValidationRule } from "vuetify/lib/types.mjs";
|
|
62
67
|
|
|
63
68
|
const props = defineProps({
|
|
64
|
-
modelValue: { type: String as PropType<string>, default:
|
|
69
|
+
modelValue: { type: String as PropType<string>, default: "" },
|
|
65
70
|
rules: { type: Array as PropType<ValidationRule[]>, default: () => [] },
|
|
66
|
-
variant: { type: String as PropType<any>, default:
|
|
67
|
-
density: {
|
|
71
|
+
variant: { type: String as PropType<any>, default: "outlined" },
|
|
72
|
+
density: {
|
|
73
|
+
type: String as PropType<"default" | "comfortable" | "compact">,
|
|
74
|
+
default: "default",
|
|
75
|
+
},
|
|
68
76
|
placeholder: { type: String },
|
|
69
77
|
hideDetails: { type: Boolean, default: false },
|
|
70
78
|
loading: { type: Boolean, default: false },
|
|
71
79
|
readOnly: { type: Boolean, default: false },
|
|
72
80
|
disabled: { type: Boolean, default: false },
|
|
73
|
-
})
|
|
81
|
+
});
|
|
74
82
|
|
|
75
|
-
const emit = defineEmits([
|
|
83
|
+
const emit = defineEmits(["update:modelValue", "focus", "blur"]);
|
|
76
84
|
|
|
77
85
|
type TPhoneMask = {
|
|
78
|
-
name: string
|
|
79
|
-
flag: string
|
|
80
|
-
code: string
|
|
81
|
-
dial_code: string
|
|
82
|
-
regex: string
|
|
83
|
-
}
|
|
86
|
+
name: string;
|
|
87
|
+
flag: string;
|
|
88
|
+
code: string;
|
|
89
|
+
dial_code: string;
|
|
90
|
+
regex: string;
|
|
91
|
+
};
|
|
84
92
|
|
|
85
93
|
// Main reactive values
|
|
86
|
-
const phone = ref(props.modelValue)
|
|
87
|
-
const input = ref(
|
|
88
|
-
const selectedCode = ref(
|
|
89
|
-
const countries = phoneMasks
|
|
90
|
-
const errorMessage = ref(
|
|
91
|
-
const maskRef = ref()
|
|
92
|
-
const maskKey = ref(0)
|
|
93
|
-
|
|
94
|
+
const phone = ref(props.modelValue);
|
|
95
|
+
const input = ref("");
|
|
96
|
+
const selectedCode = ref("SG");
|
|
97
|
+
const countries = phoneMasks;
|
|
98
|
+
const errorMessage = ref("");
|
|
99
|
+
const maskRef = ref();
|
|
100
|
+
const maskKey = ref(0);
|
|
94
101
|
|
|
95
102
|
const currentMask = computed(() => {
|
|
96
|
-
const country = countries.find(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
const country = countries.find(
|
|
104
|
+
(c: TPhoneMask) => c.code === selectedCode.value
|
|
105
|
+
);
|
|
106
|
+
if (!country) return "############";
|
|
107
|
+
return generateMaskFromRegex(country.regex);
|
|
108
|
+
});
|
|
101
109
|
|
|
102
110
|
const phonePrefix = computed(() => {
|
|
103
|
-
const country = countries.find(
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const country = countries.find(
|
|
112
|
+
(c: TPhoneMask) => c.code === selectedCode.value
|
|
113
|
+
);
|
|
114
|
+
return country?.dial_code || "";
|
|
115
|
+
});
|
|
106
116
|
|
|
107
117
|
const countriesByLongestDialCode = computed(() => {
|
|
108
|
-
return [...countries].sort(
|
|
109
|
-
|
|
118
|
+
return [...countries].sort(
|
|
119
|
+
(a: TPhoneMask, b: TPhoneMask) => b.dial_code.length - a.dial_code.length
|
|
120
|
+
);
|
|
121
|
+
});
|
|
110
122
|
|
|
111
123
|
function generateMaskFromRegex(regex: string): string {
|
|
112
|
-
let pattern = regex.replace(/^\^|\$$/g,
|
|
113
|
-
pattern = pattern.replace(/\(\?:\+?\d+\)\?/g,
|
|
114
|
-
pattern = pattern.replace(/\+?\d{1,4}/,
|
|
115
|
-
pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pattern = pattern.replace(
|
|
119
|
-
|
|
124
|
+
let pattern = regex.replace(/^\^|\$$/g, "");
|
|
125
|
+
pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, "");
|
|
126
|
+
pattern = pattern.replace(/\+?\d{1,4}/, "");
|
|
127
|
+
pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) =>
|
|
128
|
+
"#".repeat(Number(count))
|
|
129
|
+
);
|
|
130
|
+
pattern = pattern.replace(/\\d/g, "#");
|
|
131
|
+
pattern = pattern.replace(/\\/g, "");
|
|
132
|
+
pattern = pattern.replace(/\(\?:/g, "");
|
|
133
|
+
return pattern.trim();
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
const validatePhone = (): boolean | string => {
|
|
123
|
-
if (props.readOnly) return true
|
|
124
|
-
if (!phone.value) return true
|
|
137
|
+
if (props.readOnly) return true;
|
|
138
|
+
if (!phone.value) return true;
|
|
125
139
|
|
|
126
|
-
const country = countries.find(
|
|
127
|
-
|
|
140
|
+
const country = countries.find(
|
|
141
|
+
(c: TPhoneMask) => c.code === selectedCode.value
|
|
142
|
+
);
|
|
143
|
+
if (!country) return true;
|
|
128
144
|
|
|
129
|
-
const regex = new RegExp(country.regex)
|
|
130
|
-
const isValid = regex.test(phone.value)
|
|
131
|
-
errorMessage.value = isValid ?
|
|
132
|
-
return isValid
|
|
133
|
-
}
|
|
145
|
+
const regex = new RegExp(country.regex);
|
|
146
|
+
const isValid = regex.test(phone.value);
|
|
147
|
+
errorMessage.value = isValid ? "" : `Invalid ${country.name} phone number`;
|
|
148
|
+
return isValid;
|
|
149
|
+
};
|
|
134
150
|
|
|
135
151
|
function findCountryFromPhone(value: string): TPhoneMask | undefined {
|
|
136
|
-
return countriesByLongestDialCode.value.find((c: TPhoneMask) =>
|
|
152
|
+
return countriesByLongestDialCode.value.find((c: TPhoneMask) =>
|
|
153
|
+
value.startsWith(c.dial_code)
|
|
154
|
+
);
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
function syncInputFromPhone(value: string) {
|
|
140
|
-
const prefix = phonePrefix.value
|
|
141
|
-
if (!value) input.value =
|
|
142
|
-
else
|
|
158
|
+
const prefix = phonePrefix.value;
|
|
159
|
+
if (!value) input.value = "";
|
|
160
|
+
else
|
|
161
|
+
input.value = value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
function handleUpdateCountry() {
|
|
146
|
-
const prefix = phonePrefix.value
|
|
147
|
-
phone.value = input.value ? prefix + input.value :
|
|
165
|
+
const prefix = phonePrefix.value;
|
|
166
|
+
phone.value = input.value ? prefix + input.value : "";
|
|
148
167
|
}
|
|
149
168
|
|
|
150
|
-
|
|
151
169
|
watch(input, (newInput) => {
|
|
152
|
-
const prefix = phonePrefix.value
|
|
153
|
-
if (!newInput) phone.value =
|
|
154
|
-
else phone.value = prefix + newInput
|
|
155
|
-
})
|
|
170
|
+
const prefix = phonePrefix.value;
|
|
171
|
+
if (!newInput) phone.value = "";
|
|
172
|
+
else phone.value = prefix + newInput;
|
|
173
|
+
});
|
|
156
174
|
|
|
157
175
|
watch(
|
|
158
176
|
() => props.modelValue,
|
|
159
177
|
(val) => {
|
|
160
|
-
phone.value = val ||
|
|
178
|
+
phone.value = val || "";
|
|
161
179
|
},
|
|
162
180
|
{ immediate: true }
|
|
163
|
-
)
|
|
164
|
-
|
|
181
|
+
);
|
|
165
182
|
|
|
166
183
|
watch(
|
|
167
184
|
phone,
|
|
168
185
|
(newVal) => {
|
|
169
|
-
const country = newVal ? findCountryFromPhone(newVal) : undefined
|
|
170
|
-
if (country && country.code !== selectedCode.value)
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
const country = newVal ? findCountryFromPhone(newVal) : undefined;
|
|
187
|
+
if (country && country.code !== selectedCode.value)
|
|
188
|
+
selectedCode.value = country.code;
|
|
189
|
+
syncInputFromPhone(newVal);
|
|
190
|
+
emit("update:modelValue", newVal);
|
|
173
191
|
},
|
|
174
192
|
{ immediate: true }
|
|
175
|
-
)
|
|
176
|
-
|
|
193
|
+
);
|
|
177
194
|
|
|
178
195
|
watch(selectedCode, () => {
|
|
179
|
-
maskKey.value
|
|
180
|
-
})
|
|
196
|
+
maskKey.value++;
|
|
197
|
+
});
|
|
181
198
|
</script>
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
:placeholder="placeholder"
|
|
7
7
|
:counter="maxlength"
|
|
8
8
|
@input="onInput"
|
|
9
|
+
@focus="emit('focus')"
|
|
10
|
+
@blur="emit('blur')"
|
|
9
11
|
outlined
|
|
10
12
|
:disabled="disabled"
|
|
11
13
|
:readonly="readonly"
|
|
@@ -14,36 +16,37 @@
|
|
|
14
16
|
</template>
|
|
15
17
|
|
|
16
18
|
<script setup>
|
|
19
|
+
const emit = defineEmits(["focus", "blur"]);
|
|
17
20
|
|
|
18
21
|
const props = defineProps({
|
|
19
22
|
placeholder: {
|
|
20
23
|
type: String,
|
|
21
|
-
default:
|
|
24
|
+
default: "Vehicle Number",
|
|
22
25
|
},
|
|
23
26
|
rules: {
|
|
24
27
|
type: Array,
|
|
25
|
-
default: () => []
|
|
28
|
+
default: () => [],
|
|
26
29
|
},
|
|
27
30
|
maxlength: {
|
|
28
31
|
type: [Number, String],
|
|
29
|
-
default: false
|
|
32
|
+
default: false,
|
|
30
33
|
},
|
|
31
34
|
disabled: {
|
|
32
35
|
type: Boolean,
|
|
33
36
|
},
|
|
34
37
|
readonly: {
|
|
35
38
|
type: Boolean,
|
|
36
|
-
}
|
|
37
|
-
})
|
|
39
|
+
},
|
|
40
|
+
});
|
|
38
41
|
|
|
39
|
-
const model = defineModel({required: true })
|
|
42
|
+
const model = defineModel({ required: true });
|
|
40
43
|
|
|
41
44
|
function onInput(event) {
|
|
42
|
-
const value = typeof event ===
|
|
45
|
+
const value = typeof event === "string" ? event : event?.target?.value || "";
|
|
43
46
|
|
|
44
|
-
let formatted = value.replace(/[^A-Za-z0-9]/g,
|
|
45
|
-
formatted = formatted.toUpperCase()
|
|
47
|
+
let formatted = value.replace(/[^A-Za-z0-9]/g, "");
|
|
48
|
+
formatted = formatted.toUpperCase();
|
|
46
49
|
|
|
47
|
-
model.value = formatted
|
|
50
|
+
model.value = formatted;
|
|
48
51
|
}
|
|
49
|
-
</script>
|
|
52
|
+
</script>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
+
}}</strong>" as custom contractor type.
|
|
34
34
|
</v-list-item-title>
|
|
35
35
|
</v-list-item>
|
|
36
36
|
</template>
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<InputLabel class="text-capitalize" title="NRIC" :required="requireNRIC" />
|
|
58
58
|
<v-menu v-model="nricSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
59
59
|
<template #activator="{ props }">
|
|
60
|
-
<InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable"
|
|
60
|
+
<InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable" @focus="isNricFieldFocused = true" @blur="isNricFieldFocused = false"
|
|
61
61
|
:rules="[requireNRIC ? requiredRule : () => true]" :key="currentAutofillSource + 'nric-key'"
|
|
62
62
|
@update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
|
|
63
63
|
</template>
|
|
@@ -87,14 +87,14 @@
|
|
|
87
87
|
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
88
88
|
<v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
89
89
|
<template #activator="{ props }">
|
|
90
|
-
<InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]"
|
|
90
|
+
<InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]" @focus="isPhoneFieldFocused = true" @blur="isPhoneFieldFocused = false"
|
|
91
91
|
density="comfortable" :key="currentAutofillSource + 'phone-key'"
|
|
92
92
|
@update:model-value="handleUpdatePhoneNumber" />
|
|
93
93
|
</template>
|
|
94
94
|
|
|
95
95
|
<v-list v-if="phoneSearchResults.length" class="pa-1">
|
|
96
96
|
<v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
|
|
97
|
-
@click="
|
|
97
|
+
@click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
|
|
98
98
|
<div class="d-flex align-center justify-space-between ga-4 w-100">
|
|
99
99
|
<span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
|
|
100
100
|
<span class="text-subtitle-1 text-no-wrap">NRIC: {{ item.nric || 'N/A' }}</span>
|
|
@@ -113,12 +113,12 @@
|
|
|
113
113
|
<v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
114
114
|
<template #activator="{ props }">
|
|
115
115
|
<InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
|
|
116
|
-
@update:model-value="handleUpdateVehicleNumber" />
|
|
116
|
+
@update:model-value="handleUpdateVehicleNumber" @focus="isPlateNumberFieldFocused = true" @blur="isPlateNumberFieldFocused = false" />
|
|
117
117
|
</template>
|
|
118
118
|
|
|
119
119
|
<v-list v-if="vehicleSearchResults.length" class="pa-1">
|
|
120
120
|
<v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
|
|
121
|
-
@click="
|
|
121
|
+
@click="selectPlateNumberSearchResult(item)"
|
|
122
122
|
class="cursor-pointer nric-search-item">
|
|
123
123
|
<div class="d-flex align-center justify-space-between ga-4 w-100">
|
|
124
124
|
<span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
class="d-flex align-center ga-1">
|
|
160
160
|
<span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
|
|
161
161
|
companyNameInput
|
|
162
|
-
|
|
162
|
+
}}</strong>" as new company.
|
|
163
163
|
</v-list-item-title>
|
|
164
164
|
<v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
|
|
165
165
|
Start typing to search for companies.
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
class="d-flex align-center ga-1">
|
|
190
190
|
<span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
|
|
191
191
|
companyNameInput
|
|
192
|
-
|
|
192
|
+
}}</strong>" as new company.
|
|
193
193
|
</v-list-item-title>
|
|
194
194
|
<v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
|
|
195
195
|
Start typing to search for companies.
|
|
@@ -499,6 +499,7 @@ import useVisitor from "../composables/useVisitor";
|
|
|
499
499
|
import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
|
|
500
500
|
import useAccessManagement from "../composables/useAccessManagement";
|
|
501
501
|
import usePeople from "../composables/usePeople";
|
|
502
|
+
import { mockPhoneNumberSearchResults, mockVehicleNumberSearchResults } from "../utils/mock-people-response";
|
|
502
503
|
|
|
503
504
|
const prop = defineProps({
|
|
504
505
|
type: {
|
|
@@ -554,6 +555,7 @@ const { testConnection } = useWebUsb();
|
|
|
554
555
|
const {
|
|
555
556
|
findPersonByNRIC,
|
|
556
557
|
findPersonByNRICMultipleResult,
|
|
558
|
+
findPersonByPhoneNumberMultipleResult,
|
|
557
559
|
searchCompanyList,
|
|
558
560
|
findUsersByPlateNumber,
|
|
559
561
|
} = usePeople();
|
|
@@ -594,6 +596,9 @@ const passCards = ref<{ _id: string; cardNo: string }[]>([]);
|
|
|
594
596
|
const availableQrCount = ref<number | null>(null);
|
|
595
597
|
const hidQrPrintPayload = ref<string | null>(null);
|
|
596
598
|
const hidQrPrinted = ref(false);
|
|
599
|
+
const isPhoneFieldFocused = ref(false);
|
|
600
|
+
const isPlateNumberFieldFocused = ref(false);
|
|
601
|
+
const isNricFieldFocused = ref(false);
|
|
597
602
|
|
|
598
603
|
const registeredUnitCompanyName = ref("N/A");
|
|
599
604
|
|
|
@@ -759,50 +764,13 @@ const selectedNricSearchResult = ref<Partial<TPeople> | null>(null);
|
|
|
759
764
|
const selectedNricContact = ref("");
|
|
760
765
|
const selectedNricPlate = ref("");
|
|
761
766
|
|
|
762
|
-
type
|
|
767
|
+
type SearchPerson = Omit<Partial<TPeople>, "contact"> & {
|
|
763
768
|
contact?: string | string[];
|
|
764
769
|
contacts?: string | string[];
|
|
765
770
|
visitorPlateNumbers?: string | string[];
|
|
766
771
|
plateNumber?: string | string[];
|
|
767
772
|
};
|
|
768
773
|
|
|
769
|
-
const mockPhoneNumberSearchResults: Partial<NricSearchPerson>[] = [
|
|
770
|
-
{
|
|
771
|
-
_id: "mock-phone-multiple-options",
|
|
772
|
-
name: "James Smith",
|
|
773
|
-
nric: "1323232499",
|
|
774
|
-
contacts: ["653939394", "81234567"],
|
|
775
|
-
visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
776
|
-
companyName: ["Mock Company Pte Ltd"],
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
_id: "mock-phone-single-option",
|
|
780
|
-
name: "Jane Tan",
|
|
781
|
-
nric: "S1234567A",
|
|
782
|
-
contacts: ["91234567"],
|
|
783
|
-
visitorPlateNumbers: ["SGB5678B"],
|
|
784
|
-
companyName: ["Single Option Company"],
|
|
785
|
-
},
|
|
786
|
-
];
|
|
787
|
-
|
|
788
|
-
const mockVehicleNumberSearchResults: Partial<NricSearchPerson>[] = [
|
|
789
|
-
{
|
|
790
|
-
_id: "mock-vehicle-multiple-options",
|
|
791
|
-
name: "James Smith",
|
|
792
|
-
nric: "1323232499",
|
|
793
|
-
contacts: ["653939394", "81234567"],
|
|
794
|
-
visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
795
|
-
companyName: ["Mock Company Pte Ltd"],
|
|
796
|
-
},
|
|
797
|
-
{
|
|
798
|
-
_id: "mock-vehicle-single-option",
|
|
799
|
-
name: "Ali Lim",
|
|
800
|
-
nric: "T7654321B",
|
|
801
|
-
contacts: ["98765432"],
|
|
802
|
-
visitorPlateNumbers: ["SKX2468C"],
|
|
803
|
-
companyName: ["Vehicle Single Company"],
|
|
804
|
-
},
|
|
805
|
-
];
|
|
806
774
|
|
|
807
775
|
const formTitle = computed(() => {
|
|
808
776
|
const isContractorForm = prop.type === "contractor";
|
|
@@ -874,7 +842,7 @@ const {
|
|
|
874
842
|
pending: fetchPersonByNRICPending,
|
|
875
843
|
} = useLazyAsyncData(`fetch-person`, () => {
|
|
876
844
|
const NRIC = visitor.nric;
|
|
877
|
-
if (!NRIC || NRIC.length <
|
|
845
|
+
if (!NRIC || NRIC.length < 1) return Promise.resolve(null);
|
|
878
846
|
return findPersonByNRICMultipleResult(NRIC, prop.site);
|
|
879
847
|
});
|
|
880
848
|
|
|
@@ -894,7 +862,7 @@ watch(fetchPersonByNRICReq, (obj) => {
|
|
|
894
862
|
// contacts: ["653939394", "81234567"],
|
|
895
863
|
// visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
896
864
|
// companyName: ["Mock Company Pte Ltd"],
|
|
897
|
-
// } as Partial<
|
|
865
|
+
// } as Partial<SearchPerson>,
|
|
898
866
|
// {
|
|
899
867
|
// _id: "mock-person-single-option",
|
|
900
868
|
// name: "Jane Tan",
|
|
@@ -902,7 +870,7 @@ watch(fetchPersonByNRICReq, (obj) => {
|
|
|
902
870
|
// contacts: ["91234567"],
|
|
903
871
|
// visitorPlateNumbers: ["SGB5678B"],
|
|
904
872
|
// companyName: ["Single Option Company"],
|
|
905
|
-
// } as Partial<
|
|
873
|
+
// } as Partial<SearchPerson>,
|
|
906
874
|
// ];
|
|
907
875
|
// nricSearchMenu.value = true;
|
|
908
876
|
});
|
|
@@ -947,12 +915,12 @@ function toUniqueStringArray(value: unknown) {
|
|
|
947
915
|
}
|
|
948
916
|
|
|
949
917
|
function getPersonContactOptions(item: Partial<TPeople>) {
|
|
950
|
-
const person = item as
|
|
918
|
+
const person = item as SearchPerson;
|
|
951
919
|
return toUniqueStringArray(person.contacts || person.contact);
|
|
952
920
|
}
|
|
953
921
|
|
|
954
922
|
function getPersonPlateOptions(item: Partial<TPeople>) {
|
|
955
|
-
const person = item as
|
|
923
|
+
const person = item as SearchPerson;
|
|
956
924
|
const visitorPlateNumbers = toUniqueStringArray(person.visitorPlateNumbers);
|
|
957
925
|
if (visitorPlateNumbers.length) return visitorPlateNumbers;
|
|
958
926
|
|
|
@@ -1000,6 +968,12 @@ function applyNricSearchResult(
|
|
|
1000
968
|
function selectNricSearchResult(item: Partial<TPeople>) {
|
|
1001
969
|
selectAutofillSearchResult(item, "nric");
|
|
1002
970
|
}
|
|
971
|
+
function selectPhoneNumberSearchResult(item: Partial<TPeople>) {
|
|
972
|
+
selectAutofillSearchResult(item, "contact");
|
|
973
|
+
}
|
|
974
|
+
function selectPlateNumberSearchResult(item: Partial<TPeople>) {
|
|
975
|
+
selectAutofillSearchResult(item, "vehicleNumber");
|
|
976
|
+
}
|
|
1003
977
|
|
|
1004
978
|
function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<AutofillSource, null>) {
|
|
1005
979
|
const contactOptions = getPersonContactOptions(item);
|
|
@@ -1388,46 +1362,46 @@ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500);
|
|
|
1388
1362
|
|
|
1389
1363
|
function handleUpdateNRIC() {
|
|
1390
1364
|
if (skipNricFetch.value) return;
|
|
1391
|
-
if
|
|
1392
|
-
return;
|
|
1365
|
+
if(!isNricFieldFocused.value) return;
|
|
1393
1366
|
debounceFetchNRIC();
|
|
1394
1367
|
}
|
|
1395
1368
|
|
|
1369
|
+
|
|
1370
|
+
const {
|
|
1371
|
+
data: fetchPersonByPhoneNumber,
|
|
1372
|
+
refresh: fetchPersonByPhoneNumberRefresh,
|
|
1373
|
+
pending: fetchPersonByPhoneNumberPending,
|
|
1374
|
+
} = useLazyAsyncData(`fetch-person-phone-number`, () => {
|
|
1375
|
+
const contact = visitor.contact;
|
|
1376
|
+
if (!contact || contact.length < 4) return Promise.resolve(null);
|
|
1377
|
+
return findPersonByPhoneNumberMultipleResult(contact, prop.site);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1396
1380
|
function handleUpdatePhoneNumber() {
|
|
1397
|
-
if
|
|
1398
|
-
return;
|
|
1381
|
+
if(!isPhoneFieldFocused.value) return;
|
|
1399
1382
|
|
|
1400
1383
|
const search = visitor.contact?.trim() || "";
|
|
1401
|
-
if (search.length <
|
|
1384
|
+
if (search.length < 1) {
|
|
1402
1385
|
phoneSearchResults.value = [];
|
|
1403
1386
|
phoneSearchMenu.value = false;
|
|
1404
1387
|
return;
|
|
1405
1388
|
}
|
|
1406
1389
|
|
|
1407
|
-
phoneSearchResults.value = mockPhoneNumberSearchResults
|
|
1408
|
-
getPersonContactOptions(item as Partial<TPeople>).some((contact) =>
|
|
1409
|
-
contact.includes(search)
|
|
1410
|
-
)
|
|
1411
|
-
) as Partial<TPeople>[];
|
|
1390
|
+
phoneSearchResults.value = mockPhoneNumberSearchResults
|
|
1412
1391
|
phoneSearchMenu.value = phoneSearchResults.value.length > 0;
|
|
1413
1392
|
}
|
|
1414
1393
|
|
|
1415
1394
|
function handleUpdateVehicleNumber() {
|
|
1416
|
-
|
|
1417
|
-
return;
|
|
1395
|
+
if(!isPlateNumberFieldFocused.value) return;
|
|
1418
1396
|
|
|
1419
1397
|
const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
|
|
1420
|
-
if (search.length <
|
|
1398
|
+
if (search.length < 1) {
|
|
1421
1399
|
vehicleSearchResults.value = [];
|
|
1422
1400
|
vehicleSearchMenu.value = false;
|
|
1423
1401
|
return;
|
|
1424
1402
|
}
|
|
1425
1403
|
|
|
1426
|
-
vehicleSearchResults.value = mockVehicleNumberSearchResults
|
|
1427
|
-
getPersonPlateOptions(item as Partial<TPeople>).some((plate) =>
|
|
1428
|
-
plate.toUpperCase().includes(search)
|
|
1429
|
-
)
|
|
1430
|
-
) as Partial<TPeople>[];
|
|
1404
|
+
vehicleSearchResults.value = mockVehicleNumberSearchResults
|
|
1431
1405
|
vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
|
|
1432
1406
|
}
|
|
1433
1407
|
|
package/composables/usePeople.ts
CHANGED
|
@@ -44,6 +44,19 @@ export default function () {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
async function findPersonByPhoneNumberMultipleResult(contact: string, site: string) {
|
|
48
|
+
return await $fetch<Record<any, any>>(`/api/people/contact`, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
query: { contact, site },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function findPersonByPlateNumberMultipleResult(plateNumber: string, site: string) {
|
|
54
|
+
return await $fetch<Record<any, any>>(`/api/people/plate-number`, {
|
|
55
|
+
method: "GET",
|
|
56
|
+
query: { plateNumber, site },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
async function findPersonByContact(
|
|
48
61
|
contact: string
|
|
49
62
|
): Promise<null | Partial<TPeople>> {
|
|
@@ -124,6 +137,8 @@ export default function () {
|
|
|
124
137
|
deleteById,
|
|
125
138
|
findPersonByNRIC,
|
|
126
139
|
findPersonByNRICMultipleResult,
|
|
140
|
+
findPersonByPhoneNumberMultipleResult,
|
|
141
|
+
findPersonByPlateNumberMultipleResult,
|
|
127
142
|
findPersonByContact,
|
|
128
143
|
getPeopleByUnit,
|
|
129
144
|
searchCompanyList,
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
export const mockPhoneNumberSearchResults: Partial<TPeople>[] = [
|
|
3
|
+
{
|
|
4
|
+
_id: "mock-phone-multiple-options",
|
|
5
|
+
name: "James Smith",
|
|
6
|
+
nric: "1323232499",
|
|
7
|
+
contacts: ["653939394", "81234567"],
|
|
8
|
+
visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
9
|
+
companyName: ["Mock Company Pte Ltd"],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
_id: "mock-phone-single-option",
|
|
13
|
+
name: "Jane Tan",
|
|
14
|
+
nric: "S1234567A",
|
|
15
|
+
contacts: ["91234567"],
|
|
16
|
+
visitorPlateNumbers: ["SGB5678B"],
|
|
17
|
+
companyName: ["Single Option Company"],
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const mockVehicleNumberSearchResults: Partial<TPeople>[] = [
|
|
22
|
+
{
|
|
23
|
+
_id: "mock-vehicle-multiple-options",
|
|
24
|
+
name: "James Smith",
|
|
25
|
+
nric: "1323232499",
|
|
26
|
+
contacts: ["653939394", "81234567"],
|
|
27
|
+
visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
28
|
+
companyName: ["Mock Company Pte Ltd"],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
_id: "mock-vehicle-single-option",
|
|
32
|
+
name: "Ali Lim",
|
|
33
|
+
nric: "T7654321B",
|
|
34
|
+
contacts: ["98765432"],
|
|
35
|
+
visitorPlateNumbers: ["SKX2468C"],
|
|
36
|
+
companyName: ["Vehicle Single Company"],
|
|
37
|
+
},
|
|
38
|
+
];
|