@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.
@@ -984,60 +984,44 @@
984
984
  <div
985
985
  v-for="item in formattedActivePatrolItems"
986
986
  :key="item.id || item._id"
987
- class="d-flex align-center justify-space-between"
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 style="flex: 1; min-width: 140px">
998
- <strong
999
- style="
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
- style="flex: 1; display: flex; align-items: center; gap: 12px"
1015
- >
1016
- <AvatarMain
1017
- :name="item.person || item.assignedTo"
1018
- :size="36"
1019
- />
1020
- <span style="font-size: 14px; color: #64748b">{{
1021
- item.person || item.assignedTo
1022
- }}</span>
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 style="flex: 1; display: flex; justify-content: center">
1008
+ <div class="active-patrol-statuses">
1026
1009
  <div
1027
- :style="getPatrolStatusStyle(item.status)"
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.status }}
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 style="flex: 0 0 auto; text-align: right">
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
- let status = item.status;
2771
- if (Array.isArray(status)) {
2772
- status = status.join(", ");
2773
- }
2774
- let person = item.person || item.assignedTo || "Unassigned";
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
- person,
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 class="text-error text-caption w-100" v-if="errorMessage && !hideDetails">
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 'vue'
63
+ import { ref, computed, watch, type PropType } from "vue";
59
64
  //@ts-ignore
60
- import phoneMasks from '~/utils/phoneMasks'
61
- import type { ValidationRule } from 'vuetify/lib/types.mjs'
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: 'outlined' },
67
- density: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
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(['update:modelValue'])
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('SG')
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((c: TPhoneMask) => c.code === selectedCode.value)
97
- if (!country) return '############'
98
- return generateMaskFromRegex(country.regex)
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((c: TPhoneMask) => c.code === selectedCode.value)
104
- return country?.dial_code || ''
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((a: TPhoneMask, b: TPhoneMask) => b.dial_code.length - a.dial_code.length)
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) => '#'.repeat(Number(count)))
116
- pattern = pattern.replace(/\\d/g, '#')
117
- pattern = pattern.replace(/\\/g, '')
118
- pattern = pattern.replace(/\(\?:/g, '')
119
- return pattern.trim()
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((c: TPhoneMask) => c.code === selectedCode.value)
127
- if (!country) return true
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 ? '' : `Invalid ${country.name} phone number`
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) => value.startsWith(c.dial_code))
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 input.value = value.startsWith(prefix) ? value.slice(prefix.length) : value
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) selectedCode.value = country.code
171
- syncInputFromPhone(newVal)
172
- emit('update:modelValue', newVal)
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: 'Vehicle Number'
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 === 'string' ? event : event?.target?.value || ''
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
- }}</span>
15
+ }}</span>
16
16
  <span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
17
17
  <span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
18
18
  </div>
@@ -30,7 +30,7 @@
30
30
  <v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
31
31
  <span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
32
32
  contractorTypeInput
33
- }}</strong>" as custom contractor type.
33
+ }}</strong>" as custom contractor type.
34
34
  </v-list-item-title>
35
35
  </v-list-item>
36
36
  </template>
@@ -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="selectAutofillSearchResult(item, 'contact')" class="cursor-pointer nric-search-item">
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="selectAutofillSearchResult(item, 'vehicleNumber')"
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
- }}</strong>" as new company.
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
- }}</strong>" as new company.
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 NricSearchPerson = Omit<Partial<TPeople>, "contact"> & {
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 < 4) return Promise.resolve(null);
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<NricSearchPerson>,
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<NricSearchPerson>,
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 NricSearchPerson;
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 NricSearchPerson;
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 (currentAutofillSource.value && currentAutofillSource.value !== "nric")
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 (currentAutofillSource.value && currentAutofillSource.value !== "contact")
1398
- return;
1381
+ if(!isPhoneFieldFocused.value) return;
1399
1382
 
1400
1383
  const search = visitor.contact?.trim() || "";
1401
- if (search.length < 4) {
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.filter((item) =>
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
- if (currentAutofillSource.value && currentAutofillSource.value !== "vehicleNumber")
1417
- return;
1395
+ if(!isPlateNumberFieldFocused.value) return;
1418
1396
 
1419
1397
  const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
1420
- if (search.length < 4) {
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.filter((item) =>
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
 
@@ -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
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.31-staging.13",
5
+ "version": "3.0.31-staging.15",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -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
+ ];