@7365admin1/layer-common 1.8.4 → 1.8.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.8.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 8db0ed5: Update on components and config
8
+
3
9
  ## 1.8.4
4
10
 
5
11
  ### Patch Changes
@@ -1,71 +1,83 @@
1
1
  <template>
2
- <v-row no-gutters class="mb-5">
3
- <v-col cols="12" class="d-flex ga-2">
4
- <v-select v-model="selectedCode" :variant="variant" :items="countries" item-title="code" item-value="code"
5
- hide-details class="px-0" :density="density" style="max-width: 95px" :rules="[...props.rules]"
6
- :readonly="props.readOnly" @update:model-value="handleUpdateCountry">
7
- <template v-slot:item="{ props: itemProps, item }">
8
- <v-list-item v-bind="itemProps" :title="item.raw.name" :subtitle="item.raw.dial_code" width="300" />
9
- </template>
10
- </v-select>
11
- <v-mask-input v-model="input" :mask="currentMask" :rules="[...props.rules, validatePhone]" ref="maskRef" :key="`mask-key-${maskKey}`"
12
- :loading="loading" :readonly="props.readOnly" :variant="variant" hint="Enter a valid phone number"
13
- hide-details persistent-hint return-masked-value :prefix="phonePrefix || '###'" persistent-placeholder
14
- :density="density" :placeholder="placeholder || currentMask"></v-mask-input>
15
- </v-col>
16
- <span class="text-error text-caption w-100" v-if="errorMessage && !hideDetails">{{ errorMessage }}</span>
17
- </v-row>
18
-
2
+ <v-row no-gutters class="mb-5">
3
+ <v-col cols="12" class="d-flex ga-2">
4
+ <v-select
5
+ v-model="selectedCode"
6
+ :variant="variant"
7
+ :items="countries"
8
+ item-title="code"
9
+ item-value="code"
10
+ hide-details
11
+ class="px-0"
12
+ :density="density"
13
+ style="max-width: 95px"
14
+ :rules="[...props.rules]"
15
+ :readonly="props.readOnly"
16
+ @update:model-value="handleUpdateCountry"
17
+ >
18
+ <template v-slot:item="{ props: itemProps, item }">
19
+ <v-list-item
20
+ v-bind="itemProps"
21
+ :title="item.raw.name"
22
+ :subtitle="item.raw.dial_code"
23
+ />
24
+ </template>
25
+ </v-select>
26
+
27
+ <v-mask-input
28
+ v-model="input"
29
+ :mask="currentMask"
30
+ :rules="[...props.rules, validatePhone]"
31
+ ref="maskRef"
32
+ :key="`mask-key-${maskKey}`"
33
+ :loading="loading"
34
+ :readonly="props.readOnly"
35
+ :variant="variant"
36
+ hint="Enter a valid phone number"
37
+ hide-details
38
+ persistent-hint
39
+ return-masked-value
40
+ :prefix="phonePrefix || ''"
41
+ persistent-placeholder
42
+ :density="density"
43
+ :placeholder="placeholder || currentMask"
44
+ />
45
+ </v-col>
46
+ <span class="text-error text-caption w-100" v-if="errorMessage && !hideDetails">
47
+ {{ errorMessage }}
48
+ </span>
49
+ </v-row>
19
50
  </template>
20
51
 
21
52
  <script setup lang="ts">
22
- import { ref, computed, type PropType } from 'vue'
23
- import type { ValidationRule } from 'vuetify/lib/types.mjs'
53
+ import { ref, computed, watch, onMounted, type PropType } from 'vue'
24
54
  //@ts-ignore
25
55
  import phoneMasks from '~/utils/phoneMasks'
56
+ import type { ValidationRule } from 'vuetify/lib/types.mjs'
26
57
 
27
58
  const props = defineProps({
28
- rules: {
29
- type: Array as PropType<ValidationRule[]>,
30
- default: []
31
- },
32
- variant: {
33
- type: String as PropType<any>,
34
- default: "outlined"
35
- },
36
- density: {
37
- type: String as PropType<Density>,
38
- default: "default"
39
- },
40
- placeholder: {
41
- type: String,
42
- },
43
- hideDetails: {
44
- type: Boolean,
45
- default: false
46
- },
47
- loading: {
48
- type: Boolean,
49
- default: false
50
- },
51
- readOnly: {
52
- type: Boolean,
53
- default: false
54
- }
59
+ modelValue: { type: String as PropType<string>, default: '' },
60
+ rules: { type: Array as PropType<ValidationRule[]>, default: () => [] },
61
+ variant: { type: String as PropType<any>, default: 'outlined' },
62
+ density: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
63
+ placeholder: { type: String },
64
+ hideDetails: { type: Boolean, default: false },
65
+ loading: { type: Boolean, default: false },
66
+ readOnly: { type: Boolean, default: false },
55
67
  })
56
68
 
57
- type Density = 'default' | 'comfortable' | 'compact';
69
+ const emit = defineEmits(['update:modelValue'])
58
70
 
59
- type TPhoneMask =
60
- {
61
- name: string
62
- flag: string
63
- code: string;
64
- dial_code: string;
65
- regex: string;
66
- }
71
+ type TPhoneMask = {
72
+ name: string
73
+ flag: string
74
+ code: string
75
+ dial_code: string
76
+ regex: string
77
+ }
67
78
 
68
- const phone = defineModel({ default: '' })
79
+ // Main reactive values
80
+ const phone = ref(props.modelValue)
69
81
  const input = ref('')
70
82
  const selectedCode = ref('SG')
71
83
  const countries = phoneMasks
@@ -73,92 +85,82 @@ const errorMessage = ref('')
73
85
  const maskRef = ref()
74
86
  const maskKey = ref(0)
75
87
 
88
+
76
89
  const currentMask = computed(() => {
77
- const country = phoneMasks.find((c: TPhoneMask) => c.code === selectedCode.value)
78
- if (!country) return '############'
79
- return generateMaskFromRegex(country.regex)
90
+ const country = countries.find((c: TPhoneMask) => c.code === selectedCode.value)
91
+ if (!country) return '############'
92
+ return generateMaskFromRegex(country.regex)
80
93
  })
81
94
 
82
95
 
83
-
84
- const validatePhone = (): boolean | string => {
85
- if(props.readOnly) return true;
86
- const value = phone.value
87
- if (!value) {
88
- errorMessage.value = ''
89
- return true;
90
- }
91
- const country = phoneMasks.find((c: any) => c.code === selectedCode.value)
92
- if (!country) {
93
- errorMessage.value = ''
94
- return true
95
- }
96
-
97
- const regex = new RegExp(country.regex)
98
- const isValid = regex.test(value)
99
-
100
- errorMessage.value = isValid ? '' : `Invalid ${country.name} phone number`
101
- return isValid
102
- }
103
-
104
-
96
+ const phonePrefix = computed(() => {
97
+ const country = countries.find((c: TPhoneMask) => c.code === selectedCode.value)
98
+ return country?.dial_code || ''
99
+ })
105
100
 
106
101
  function generateMaskFromRegex(regex: string): string {
107
- let pattern = regex.replace(/^\^|\$$/g, '');
108
-
109
- pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, '');
110
- pattern = pattern.replace(/\+?\d{1,4}/, '');
111
-
112
- pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) => '#'.repeat(Number(count)));
102
+ let pattern = regex.replace(/^\^|\$$/g, '')
103
+ pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, '')
104
+ pattern = pattern.replace(/\+?\d{1,4}/, '')
105
+ pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) => '#'.repeat(Number(count)))
106
+ pattern = pattern.replace(/\\d/g, '#')
107
+ pattern = pattern.replace(/\\/g, '')
108
+ pattern = pattern.replace(/\(\?:/g, '')
109
+ return pattern.trim()
110
+ }
113
111
 
114
- pattern = pattern.replace(/\\d/g, '#');
112
+ const validatePhone = (): boolean | string => {
113
+ if (props.readOnly) return true
114
+ if (!phone.value) return true
115
115
 
116
- pattern = pattern.replace(/\\/g, '');
117
- pattern = pattern.replace(/\(\?:/g, '');
118
- pattern = pattern.trim();
116
+ const country = countries.find((c: TPhoneMask) => c.code === selectedCode.value)
117
+ if (!country) return true
119
118
 
120
- return pattern;
119
+ const regex = new RegExp(country.regex)
120
+ const isValid = regex.test(phone.value)
121
+ errorMessage.value = isValid ? '' : `Invalid ${country.name} phone number`
122
+ return isValid
121
123
  }
122
124
 
123
- const phonePrefix = computed(() => {
124
- const country = phoneMasks.find((c: TPhoneMask) => c.code === selectedCode.value)
125
- return country?.dial_code || ''
126
- })
127
-
128
-
129
125
  function handleUpdateCountry() {
130
- phone.value = ''
126
+ const prefix = phonePrefix.value
127
+ if (phone.value?.startsWith(prefix)) {
128
+ input.value = phone.value.slice(prefix.length)
129
+ } else {
130
+ input.value = ''
131
+ }
131
132
  }
132
133
 
133
- const emit = defineEmits(['update:modelValue'])
134
-
135
- watch(phone, (newVal) => {
136
- emit('update:modelValue', newVal)
137
- })
138
134
 
139
135
  watch(input, (newInput) => {
140
- const prefix = phonePrefix.value
136
+ const prefix = phonePrefix.value
137
+ if (!newInput) phone.value = ''
138
+ else phone.value = prefix + newInput
139
+ })
141
140
 
142
- if (!newInput) {
143
- return
144
- }
145
141
 
146
- phone.value = prefix + newInput
142
+ watch(phone, (newVal) => {
143
+ const prefix = phonePrefix.value
144
+ if (!newVal) input.value = ''
145
+ else input.value = newVal.startsWith(prefix) ? newVal.slice(prefix.length) : newVal
146
+ emit('update:modelValue', newVal)
147
147
  })
148
148
 
149
149
 
150
- onMounted(() => {
151
- if (!phone.value) return
150
+ watch(selectedCode, () => {
151
+ const prefix = phonePrefix.value
152
+ if (phone.value?.startsWith(prefix)) input.value = phone.value.slice(prefix.length)
153
+ else input.value = phone.value || ''
154
+ })
152
155
 
153
- const found = phoneMasks.find((c: any) => phone.value?.startsWith(c?.dial_code))
154
- if (found) {
155
- selectedCode.value = found.code
156
- }
157
156
 
158
- input.value = phone.value.replace(found?.dial_code || '', '')
157
+ onMounted(() => {
158
+ if (phone.value) {
159
+ const found = countries.find((c: TPhoneMask) => phone.value.startsWith(c.dial_code))
160
+ if (found) selectedCode.value = found.code
161
+ const prefix = phonePrefix.value
162
+ input.value = phone.value.startsWith(prefix) ? phone.value.slice(prefix.length) : phone.value
159
163
  maskKey.value++
164
+ }
160
165
  })
161
-
162
-
163
-
164
166
  </script>
@@ -7,7 +7,7 @@
7
7
  </v-toolbar>
8
8
 
9
9
  <v-card-text style="max-height: 100vh; overflow-y: auto">
10
- <v-data-table :items="prop.vehicleNumberUsersList" hide-default-footer :headers="headers" density="comfortable" style="max-height: calc(100vh - (200px))"
10
+ <v-data-table :items="prop.vehicleNumberUsersList" hide-default-footer :headers="headers" density="comfortable"
11
11
  item-key="_id ">
12
12
  <template #item.name="{ item }">
13
13
  <span class="d-flex align-center ga-2">
@@ -81,6 +81,7 @@ function handleSelectUser(item: TPeople){
81
81
  function handleUpdateUserDetails(){
82
82
  confirmAction.value = false;
83
83
  emit('update:people', selectedPeople.value)
84
+ emit('close')
84
85
  }
85
86
 
86
87
  onMounted(() => {
@@ -62,7 +62,7 @@
62
62
 
63
63
  <v-col v-if="shouldShowField('contact')" cols="12">
64
64
  <InputLabel class="text-capitalize" title="Phone Number" required />
65
- <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable"
65
+ <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable" :key="currentAutofillSource + 'phone-key'"
66
66
  :loading="fetchPersonByContactPending" @update:model-value="handleUpdateContact" />
67
67
  </v-col>
68
68
 
@@ -134,8 +134,8 @@
134
134
  <v-col v-if="shouldShowField('unit')" cols="12">
135
135
  <InputLabel class="text-capitalize" title="Unit" required />
136
136
  <v-select v-model="visitor.unit" :items="unitsArray" density="comfortable" item-title="title"
137
- item-value="value" :disabled="!visitor.level"
138
- :loading="unitsStatus === 'pending'" :rules="[requiredRule]" @update:model-value="handleUpdateUnit" />
137
+ item-value="value" :disabled="!visitor.level" :loading="unitsStatus === 'pending'" :rules="[requiredRule]"
138
+ @update:model-value="handleUpdateUnit" />
139
139
  </v-col>
140
140
 
141
141
  <v-col v-if="shouldShowField('remarks')" cols="12">
@@ -189,6 +189,11 @@
189
189
  </v-col>
190
190
  </v-row>
191
191
  </v-toolbar>
192
+
193
+ <v-dialog v-model="dialog.vehicleNumberUsersList" v-if="vehicleNumberUserItems.length > 0" persistent max-width="600">
194
+ <SearchVehicleNumberUser :vehicle-number="visitor.plateNumber ?? ''" :vehicle-number-users-list="vehicleNumberUserItems"
195
+ @close="dialog.vehicleNumberUsersList = false" @update:people="handleAutofillDataViaVehicleNumber" />
196
+ </v-dialog>
192
197
  </v-card>
193
198
  </template>
194
199
 
@@ -217,10 +222,14 @@ const prop = defineProps({
217
222
  },
218
223
  });
219
224
 
225
+ type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
226
+ const currentAutofillSource = ref<AutofillSource>(null);
227
+
228
+
220
229
  const { requiredRule, debounce } = useUtils();
221
230
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
222
231
  const { createVisitor, typeFieldMap, contractorTypes } = useVisitor();
223
- const { findPersonByNRIC, findPersonByContact, searchCompanyList } = usePeople()
232
+ const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
224
233
 
225
234
  const emit = defineEmits([
226
235
  "back",
@@ -248,6 +257,10 @@ const visitor = reactive<Partial<TVisitorPayload>>({
248
257
  members: [],
249
258
  });
250
259
 
260
+ const dialog = reactive({
261
+ vehicleNumberUsersList: false,
262
+ });
263
+
251
264
  const validForm = ref(false);
252
265
  const formRef = ref<HTMLFormElement | null>(null);
253
266
  const processing = ref(false);
@@ -267,6 +280,10 @@ const blocksArray = ref<TDefaultOptionObj[]>([]);
267
280
  const levelsArray = ref<TDefaultOptionObj[]>([]);
268
281
  const unitsArray = ref<TDefaultOptionObj[]>([]);
269
282
 
283
+
284
+ const vehicleNumberUserItems = ref<TPeople[]>([])
285
+
286
+
270
287
  const shouldShowField = (fieldKey: keyof TVisitorPayload) => {
271
288
  if (prop.type !== "contractor" || contractorStep.value === 1) {
272
289
  const visibleFields = typeFieldMap[prop.type];
@@ -355,16 +372,20 @@ const {
355
372
 
356
373
  watch(fetchPersonByNRICReq, (obj) => {
357
374
  if (obj) {
375
+ currentAutofillSource.value = "nric";
358
376
  companyNames.value = obj.companyName ?? []
359
377
  visitor.name = obj.name
360
378
  visitor.contact = obj.contact
361
379
  if (!visitor.company) {
362
380
  visitor.company = companyNames.value?.[0]
363
381
  }
364
- visitor.plateNumber = obj.plateNumber ?? ""
365
382
  visitor.block = obj.block ?? ""
366
383
  visitor.level = obj.level ?? ""
367
384
  visitor.unit = obj.unit ?? ""
385
+
386
+ setTimeout(() => {
387
+ currentAutofillSource.value = null;
388
+ }, 1000);
368
389
  }
369
390
  })
370
391
 
@@ -381,16 +402,20 @@ const {
381
402
 
382
403
  watch(fetchPersonByContactReq, (obj) => {
383
404
  if (obj) {
405
+ currentAutofillSource.value = "contact";
384
406
  companyNames.value = obj.companyName ?? []
385
- visitor.name = obj.name
407
+ visitor.name = visitor.name ?? obj.name
386
408
  if (!visitor.company) {
387
409
  visitor.company = companyNames.value?.[0]
388
410
  }
389
- visitor.plateNumber = obj.plateNumber ?? ""
390
- visitor.block = obj.block ?? ""
391
- visitor.level = obj.level ?? ""
392
- visitor.unit = obj.unit ?? ""
393
- visitor.nric = obj.nric ?? ""
411
+ visitor.block = visitor.block ?? obj.block ?? ""
412
+ visitor.level = visitor.level ?? obj.level ?? ""
413
+ visitor.unit = visitor.unit ?? obj.unit ?? ""
414
+ visitor.nric = visitor.nric ?? obj.nric ?? ""
415
+
416
+ setTimeout(() => {
417
+ currentAutofillSource.value = null;
418
+ }, 1000);
394
419
  }
395
420
  })
396
421
 
@@ -410,6 +435,25 @@ watch(fetchCompanyListReq, (arr) => {
410
435
  companyNames.value = arr?.flatMap(x => x?.companyName)
411
436
  }
412
437
  })
438
+ const {
439
+ data: fetchVehicleNumberUserReq,
440
+ refresh: fetchVehicleNumberUserRefresh,
441
+ pending: fetchVehicleNumberUserPending,
442
+
443
+ } = useLazyAsyncData(`fetch-vehicle-number-user-list`, () => {
444
+ if (!visitor.plateNumber) return Promise.resolve(null)
445
+ return findUsersByPlateNumber(visitor.plateNumber)
446
+ })
447
+
448
+ watch(fetchVehicleNumberUserReq, (arr) => {
449
+ const arrayData = arr?.data
450
+ if (Array.isArray(arrayData)) {
451
+ vehicleNumberUserItems.value = arrayData;
452
+ if (arrayData.length > 0) {
453
+ dialog.vehicleNumberUsersList = true
454
+ }
455
+ }
456
+ })
413
457
 
414
458
  const debounceFetchCompany = debounce(async () => fetchCompanyListRefresh(), 200)
415
459
 
@@ -422,6 +466,41 @@ watch(companyNameInput, async (val) => {
422
466
  })
423
467
 
424
468
 
469
+ const debounceSearchVehicleUsers = debounce(() => {
470
+ if (!visitor.plateNumber) {
471
+ vehicleNumberUserItems.value = [];
472
+ dialog.vehicleNumberUsersList = false;
473
+ return;
474
+ }
475
+ fetchVehicleNumberUserRefresh();
476
+ }, 300);
477
+
478
+ watch(() => visitor.plateNumber, (newVal) => {
479
+ if(currentAutofillSource.value && currentAutofillSource.value !== "vehicleNumber") return;
480
+ debounceSearchVehicleUsers();
481
+ });
482
+
483
+
484
+ function handleAutofillDataViaVehicleNumber(item: TPeople){
485
+
486
+ currentAutofillSource.value = "vehicleNumber";
487
+
488
+ visitor.name = item.name
489
+ visitor.nric = item.nric
490
+ visitor.contact = item.contact
491
+ companyNames.value = item.companyName ?? []
492
+ if (!visitor.company) {
493
+ visitor.company = companyNames.value?.[0]
494
+ }
495
+ visitor.block = item.block ?? ""
496
+ visitor.level = item.level ?? ""
497
+ visitor.unit = item.unit ?? ""
498
+
499
+ setTimeout(() => {
500
+ currentAutofillSource.value = null;
501
+ }, 1000);
502
+ }
503
+
425
504
 
426
505
  const {
427
506
  data: siteData,
@@ -538,10 +617,12 @@ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500)
538
617
  const debounceFetchContact = debounce(fetchPersonByContactRefresh, 500)
539
618
 
540
619
  function handleUpdateNRIC() {
620
+ if (currentAutofillSource.value && currentAutofillSource.value !== "nric") return;
541
621
  debounceFetchNRIC()
542
622
  }
543
623
 
544
624
  function handleUpdateContact() {
625
+ if (currentAutofillSource.value && currentAutofillSource.value !== "contact") return;
545
626
  debounceFetchContact()
546
627
  }
547
628
 
@@ -650,6 +731,7 @@ watch(
650
731
 
651
732
  onMounted(() => {
652
733
  contractorStep.value = 1;
734
+ currentAutofillSource.value = null;
653
735
  });
654
736
  </script>
655
737
  <style scoped>
@@ -145,10 +145,6 @@
145
145
  </VehicleUpdateMoreAction>
146
146
  </v-dialog>
147
147
 
148
- <v-dialog v-model="dialog.vehicleNumberUsersList" persistent max-width="600">
149
- <SearchVehicleNumberUser :vehicle-number="'123123'" @close="dialog.vehicleNumberUsersList = false" @update:people="handleUpdateAutofillDetails"/>
150
- </v-dialog>
151
-
152
148
  <v-dialog v-model="dialog.deleteConfirmation" width="450" persistent>
153
149
  <CardDeleteConfirmation prompt-title="Are you sure want to delete this visitor?"
154
150
  :loading="loading.deletingVisitor" @close="dialog.deleteConfirmation = false"
@@ -245,7 +241,6 @@ const dialog = reactive({
245
241
  addVisitor: false,
246
242
  viewVisitor: false,
247
243
  deleteConfirmation: false,
248
- vehicleNumberUsersList: false
249
244
  });
250
245
 
251
246
  const tabOptions = [
@@ -30,6 +30,12 @@ export default function(){
30
30
  method: "GET",
31
31
  });
32
32
  }
33
+ async function findUsersByPlateNumber(plateNumber: string): Promise<null | Partial<TPeople>> {
34
+ return await $fetch<Record<string, any>>(`/api/people/plateNumber/${plateNumber}`, {
35
+ method: "GET",
36
+ query: { limit: 20 }
37
+ });
38
+ }
33
39
 
34
40
  async function searchCompanyList(company: string): Promise<null | Partial<TPeople>> {
35
41
  return await $fetch<Record<string, any>>('/api/people/company', {
@@ -76,6 +82,7 @@ export default function(){
76
82
  findPersonByNRIC,
77
83
  findPersonByContact,
78
84
  getPeopleByUnit,
79
- searchCompanyList
85
+ searchCompanyList,
86
+ findUsersByPlateNumber
80
87
  }
81
88
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.8.4",
5
+ "version": "1.8.5",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
package/tsconfig.json CHANGED
@@ -1,15 +1,3 @@
1
1
  {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "Node",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "types": ["vite/client", "nuxt"],
10
- "jsx": "preserve",
11
- "resolveJsonModule": true,
12
- "allowSyntheticDefaultImports": true
13
- },
14
- "include": ["./**/*.ts", "./**/*.vue"]
15
- }
2
+ "extends": "./.playground/.nuxt/tsconfig.json"
3
+ }