@iservice365/layer-common 1.6.0 → 1.7.0

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.
@@ -19,16 +19,17 @@
19
19
  <v-row no-gutters class="pt-4">
20
20
  <v-col v-if="shouldShowField('contractorType')" cols="12">
21
21
  <InputLabel class="text-capitalize" title="Contractor Type" required />
22
- <v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" :hide-no-data="false"
23
- @update:focused="handleFocusedContractorType" :items="contractorTypes" :rules="[requiredRule]"
24
- item-value="value" @update:model-value="handleSelectContractorType" variant="outlined"
25
- density="comfortable" persistent-hint small-chips>
22
+ <v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" ref="contractorTypeCombo"
23
+ autocomplete="off" :hide-no-data="false" @update:focused="handleFocusedContractorType"
24
+ :items="contractorTypes" :rules="[requiredRule]" item-value="value"
25
+ @update:model-value="handleSelectContractorType" variant="outlined" density="comfortable" persistent-hint
26
+ small-chips>
26
27
  <template v-slot:no-data>
27
28
  <v-list-item>
28
- <v-list-item-title>
29
- No results matching "<strong>{{
29
+ <v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
30
+ <span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
30
31
  contractorTypeInput
31
- }}</strong>". This value will be added as new option.
32
+ }}</strong>" as custom contractor type.
32
33
  </v-list-item-title>
33
34
  </v-list-item>
34
35
  </template>
@@ -53,14 +54,16 @@
53
54
  <v-row>
54
55
  <v-col cols="12">
55
56
  <InputLabel class="text-capitalize" title="NRIC/Passport/ID No." required />
56
- <InputNRICNumber v-model.trim="visitor.nric" density="comfortable" :rules="[requiredRule]" @update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
57
+ <InputNRICNumber v-model="visitor.nric" density="comfortable" :rules="[requiredRule]"
58
+ @update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
57
59
  </v-col>
58
60
  </v-row>
59
61
  </v-col>
60
62
 
61
63
  <v-col v-if="shouldShowField('contact')" cols="12">
62
64
  <InputLabel class="text-capitalize" title="Phone Number" required />
63
- <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable" :loading="fetchPersonByContactPending" @update:model-value="handleUpdateContact" />
65
+ <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable"
66
+ :loading="fetchPersonByContactPending" @update:model-value="handleUpdateContact" />
64
67
  </v-col>
65
68
 
66
69
  <v-col v-if="shouldShowField('deliveryType')" cols="12">
@@ -75,16 +78,25 @@
75
78
  </v-col>
76
79
 
77
80
  <template v-if="shouldShowField('company')">
78
- <v-col cols="12">
79
- <InputLabel class="text-capitalize" title="Company Name" required />
80
- <v-combobox v-model="visitor.company" v-model:search="companyNameInput" :hide-no-data="false"
81
- :items="companyNames" :rules="[requiredRule]"
82
- item-value="value" @update:model-value="handleSelectCompanyName" variant="outlined"
81
+ <v-col cols="12">
82
+ <InputLabel class="text-capitalize" title="Company Name" />
83
+ <v-combobox v-model="visitor.company" v-model:search="companyNameInput" ref="companyCombo"
84
+ autocomplete="off" :hide-no-data="false" :items="companyNames" item-value="value"
85
+ :loading="fetchCompanyListPending" @update:model-value="handleSelectCompanyName" variant="outlined"
83
86
  density="comfortable" persistent-hint small-chips>
84
87
  <template v-slot:no-data>
85
88
  <v-list-item>
86
- <v-list-item-title v-if="companyNameInput">
87
- No results matching "<strong>{{companyNameInput}}</strong>". This value will be added as new option.
89
+ <v-list-item-title v-if="fetchCompanyListPending">
90
+ <v-progress-circular indeterminate size="20" class="mr-3" />
91
+ Searching companies…
92
+ </v-list-item-title>
93
+ <v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
94
+ class="d-flex align-center ga-1">
95
+ <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{ companyNameInput }}</strong>" as new
96
+ company.
97
+ </v-list-item-title>
98
+ <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
99
+ Start typing to search for companies.
88
100
  </v-list-item-title>
89
101
  <v-list-item-title v-else>
90
102
  No companies available. Start typing to add a new one.
@@ -101,7 +113,7 @@
101
113
  <v-col cols="12">
102
114
  <InputLabel class="text-capitalize" title="Vehicle Number" required />
103
115
  <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
104
- <InputVehicleNumber v-model.trim="visitor.plateNumber" density="comfortable" :rules="[requiredRule]" />
116
+ <InputVehicleNumber v-model="visitor.plateNumber" density="comfortable" :rules="[requiredRule]" />
105
117
  </v-col>
106
118
  </v-row>
107
119
  </v-col>
@@ -121,7 +133,8 @@
121
133
 
122
134
  <v-col v-if="shouldShowField('unit')" cols="12">
123
135
  <InputLabel class="text-capitalize" title="Unit" required />
124
- <v-select v-model="visitor.unit" :items="unitsArray" density="comfortable" :disabled="!visitor.level"
136
+ <v-select v-model="visitor.unit" :items="unitsArray" density="comfortable" item-title="title"
137
+ item-value="value" :disabled="!visitor.level"
125
138
  :loading="unitsStatus === 'pending'" :rules="[requiredRule]" @update:model-value="handleUpdateUnit" />
126
139
  </v-col>
127
140
 
@@ -207,7 +220,7 @@ const prop = defineProps({
207
220
  const { requiredRule, debounce } = useUtils();
208
221
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
209
222
  const { createVisitor, typeFieldMap, contractorTypes } = useVisitor();
210
- const { findPersonByNRIC, findPersonByContact } = usePeople()
223
+ const { findPersonByNRIC, findPersonByContact, searchCompanyList } = usePeople()
211
224
 
212
225
  const emit = defineEmits([
213
226
  "back",
@@ -262,6 +275,7 @@ const shouldShowField = (fieldKey: keyof TVisitorPayload) => {
262
275
  };
263
276
 
264
277
  const companyNames = ref<string[]>([])
278
+ const companyAutofillDataArray = ref<{ companyName: string[], unit: string, block: number, level: string, unitName: string }[]>([])
265
279
 
266
280
  const formTitle = computed(() => {
267
281
  const isContractorForm = prop.type === "contractor";
@@ -278,48 +292,73 @@ function handleSelectContractorType(obj: { title: string; value: string }) {
278
292
  }
279
293
 
280
294
  function handleFocusedContractorType() {
281
- const obj = contractorTypeObj.value;
282
- const matched = contractorTypes.some(
283
- (x) => x.value === obj?.value && x.title === obj?.title
284
- );
285
- if (!matched) {
286
- contractorTypeObj.value = null;
287
- contractorTypeInput.value = ""
288
- visitor.contractorType = "";
289
- }
295
+ // const obj = contractorTypeObj.value;
296
+ // const matched = contractorTypes.some(
297
+ // (x) => x.value === obj?.value && x.title === obj?.title
298
+ // );
299
+ // if (!matched) {
300
+ // contractorTypeObj.value = null;
301
+ // contractorTypeInput.value = ""
302
+ // visitor.contractorType = "";
303
+ // }
304
+ }
305
+
306
+ const contractorTypeCombo = ref(null)
307
+
308
+ async function handleAddNewContractorType() {
309
+ visitor.contractorType = contractorTypeInput.value
310
+
311
+ const combo = contractorTypeCombo.value as any
312
+ await nextTick();
313
+
314
+ combo.isMenuActive = false;
315
+ combo.$el.querySelector("input")?.blur(); ``
316
+
290
317
  }
291
318
 
292
- function handleSelectCompanyName(company: string) {
319
+ async function handleSelectCompanyName(company: string) {
293
320
  visitor.company = company
321
+
322
+ const selected = companyAutofillDataArray.value.find(x => x.companyName?.includes(company))
323
+ if (!selected) return
324
+
325
+ visitor.block = selected.block || ""
326
+ visitor.level = selected.level || ""
327
+ visitor.unit = selected.unit || ""
328
+ visitor.unitName = selected.unitName || ""
329
+
330
+ }
331
+
332
+ const companyCombo = ref(null)
333
+
334
+ async function handleAddNewCompany() {
335
+ visitor.company = companyNameInput.value
336
+
337
+ const combo = companyCombo.value as any
338
+ await nextTick();
339
+
340
+ combo.isMenuActive = false;
341
+ combo.$el.querySelector("input")?.blur();
294
342
  }
295
343
 
296
- // function handleFocusedCompanyName() {
297
- // const companyNameSelected = companyNameObj.value;
298
- // const matched = companyNames.value.some((x) => x === companyNameSelected);
299
- // if (!matched) {
300
- // companyNameObj.value = null;
301
- // companyNameInput.value = "";
302
- // visitor.company = "";
303
- // }
304
- // }
305
344
 
306
345
  const {
307
346
  data: fetchPersonByNRICReq,
308
347
  refresh: fetchPersonByNRICRefresh,
309
348
  pending: fetchPersonByNRICPending,
310
- } = useLazyAsyncData(`fetch-person`, () =>{
349
+ } = useLazyAsyncData(`fetch-person`, () => {
311
350
  const NRIC = visitor.nric;
312
- if(!NRIC) return Promise.resolve(null)
351
+ if (!NRIC) return Promise.resolve(null)
313
352
  return findPersonByNRIC(NRIC)
314
353
  }
315
354
  );
316
355
 
317
356
  watch(fetchPersonByNRICReq, (obj) => {
318
- if(obj){
357
+ if (obj) {
319
358
  companyNames.value = obj.companyName ?? []
320
359
  visitor.name = obj.name
321
360
  visitor.contact = obj.contact
322
- if(!visitor.company){
361
+ if (!visitor.company) {
323
362
  visitor.company = companyNames.value?.[0]
324
363
  }
325
364
  visitor.plateNumber = obj.plateNumber ?? ""
@@ -333,28 +372,57 @@ const {
333
372
  data: fetchPersonByContactReq,
334
373
  refresh: fetchPersonByContactRefresh,
335
374
  pending: fetchPersonByContactPending,
336
- } = useLazyAsyncData(`fetch-contact`, () =>{
375
+ } = useLazyAsyncData(`fetch-contact`, () => {
337
376
  const contact = visitor.contact;
338
- if(!contact) return Promise.resolve(null)
377
+ if (!contact) return Promise.resolve(null)
339
378
  return findPersonByContact(contact)
340
379
  }
341
380
  );
342
381
 
343
382
  watch(fetchPersonByContactReq, (obj) => {
344
- if(obj){
383
+ if (obj) {
345
384
  companyNames.value = obj.companyName ?? []
346
385
  visitor.name = obj.name
347
- if(!visitor.company){
386
+ if (!visitor.company) {
348
387
  visitor.company = companyNames.value?.[0]
349
388
  }
350
389
  visitor.plateNumber = obj.plateNumber ?? ""
351
390
  visitor.block = obj.block ?? ""
352
391
  visitor.level = obj.level ?? ""
353
392
  visitor.unit = obj.unit ?? ""
354
- visitor.nric = obj.nric
393
+ visitor.nric = obj.nric ?? ""
394
+ }
395
+ })
396
+
397
+ const {
398
+ data: fetchCompanyListReq,
399
+ refresh: fetchCompanyListRefresh,
400
+ pending: fetchCompanyListPending,
401
+
402
+ } = useLazyAsyncData(`fetch-company-list`, () => {
403
+ if (!companyNameInput.value) return Promise.resolve(null)
404
+ return searchCompanyList(companyNameInput.value)
405
+ })
406
+
407
+ watch(fetchCompanyListReq, (arr) => {
408
+ if (Array.isArray(arr)) {
409
+ companyAutofillDataArray.value = arr;
410
+ companyNames.value = arr?.flatMap(x => x?.companyName)
355
411
  }
356
412
  })
357
413
 
414
+ const debounceFetchCompany = debounce(async () => fetchCompanyListRefresh(), 200)
415
+
416
+ watch(companyNameInput, async (val) => {
417
+ if (!val) {
418
+ companyNames.value = []
419
+ return
420
+ }
421
+ await debounceFetchCompany()
422
+ })
423
+
424
+
425
+
358
426
  const {
359
427
  data: siteData,
360
428
  refresh: refreshSiteData,
@@ -372,7 +440,7 @@ const {
372
440
  async () => {
373
441
  if (!visitor.block) return Promise.resolve(null);
374
442
  return await getSiteLevels(prop.site, { block: Number(visitor.block) });
375
- }, {watch: [() => visitor.block]}
443
+ }, { watch: [() => visitor.block] }
376
444
  );
377
445
 
378
446
  const {
@@ -384,7 +452,7 @@ const {
384
452
  async () => {
385
453
  if (!visitor.level) return Promise.resolve(null);
386
454
  return await getSiteUnits(prop.site, Number(visitor.block), visitor.level);
387
- }, {watch: [() => visitor.level]}
455
+ }, { watch: [() => visitor.level] }
388
456
  );
389
457
 
390
458
  watch(
@@ -425,18 +493,31 @@ watch(
425
493
  watch(
426
494
  unitsData,
427
495
  (newVal: any) => {
428
- if (newVal && Array.isArray(newVal)) {
429
- const arr = newVal || [];
430
- unitsArray.value = arr?.map((unit: any) => ({
431
- title: unit?.name,
432
- value: unit?._id,
433
- }));
434
- } else {
435
- unitsArray.value = [];
496
+ if (!newVal) {
497
+ unitsArray.value = []
498
+ return
436
499
  }
500
+
501
+ const mapped = newVal.map((unit: any) => ({
502
+ title: unit.name,
503
+ value: unit._id,
504
+ }))
505
+
506
+ // keep custom unit pushed from autofill
507
+ const selected = visitor.unit
508
+ const exists = mapped.some((u: TDefaultOptionObj) => u.value === selected)
509
+
510
+ if (!exists && visitor.unitName) {
511
+ mapped.push({
512
+ title: visitor.unitName,
513
+ value: selected,
514
+ })
515
+ }
516
+
517
+ unitsArray.value = mapped
437
518
  },
438
519
  { immediate: true }
439
- );
520
+ )
440
521
 
441
522
 
442
523
  function handleChangeBlock(value: any) {
@@ -456,12 +537,12 @@ function handleUpdateUnit(value: any) {
456
537
  const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500)
457
538
  const debounceFetchContact = debounce(fetchPersonByContactRefresh, 500)
458
539
 
459
- function handleUpdateNRIC(){
460
- debounceFetchNRIC()
540
+ function handleUpdateNRIC() {
541
+ debounceFetchNRIC()
461
542
  }
462
543
 
463
- function handleUpdateContact(){
464
- debounceFetchContact()
544
+ function handleUpdateContact() {
545
+ debounceFetchContact()
465
546
  }
466
547
 
467
548
  function backToSelection() {
@@ -256,6 +256,7 @@ const formattedFields = {
256
256
  nric: "NRIC",
257
257
  contact: "Phone Number",
258
258
  plateNumber: "Vehicle Number",
259
+ company: "Company",
259
260
  block: "Block",
260
261
  level: "Level",
261
262
  unitName: "Unit",
@@ -434,20 +435,6 @@ async function handleCheckout(userId: string) {
434
435
  }
435
436
  }
436
437
 
437
- // get dates in ISO String (UTC Time)
438
- function getUTCDates() {
439
- const today = new Date();
440
- const yesterday = new Date();
441
- yesterday.setUTCDate(today.getUTCDate() - 1);
442
-
443
- const dateFrom = ref(yesterday.toISOString()); // yesterday in UTC
444
- const dateTo = ref(today.toISOString()); // today in UTC
445
-
446
- const dateYesterday = yesterday.toISOString();
447
- const dateToday = today.toISOString();
448
-
449
- return { dateYesterday, dateToday };
450
- }
451
438
 
452
439
 
453
440
  const updateRouteQuery = debounce(
@@ -0,0 +1,46 @@
1
+ export default function useCard() {
2
+ function getAll({
3
+ page = 1,
4
+ search = "",
5
+ limit = 10,
6
+ status = "active",
7
+ site = "",
8
+ } = {}) {
9
+ return useNuxtApp().$api<Record<string, any>>(`/api/cards`, {
10
+ method: "GET",
11
+ query: {
12
+ page,
13
+ search,
14
+ limit,
15
+ status,
16
+ site,
17
+ },
18
+ });
19
+ }
20
+ function add(value: any) {
21
+ return useNuxtApp().$api<Record<string, any>>("/api/cards", {
22
+ method: "POST",
23
+ body: value,
24
+ });
25
+ }
26
+
27
+ function deleteById(id: string) {
28
+ return useNuxtApp().$api(`/api/cards/${id}`, {
29
+ method: "DELETE",
30
+ });
31
+ }
32
+
33
+ function updateById(id: string, value: any) {
34
+ return useNuxtApp().$api(`/api/cards/${id}`, {
35
+ method: "PUT",
36
+ body: value,
37
+ });
38
+ }
39
+
40
+ return {
41
+ getAll,
42
+ add,
43
+ deleteById,
44
+ updateById,
45
+ };
46
+ }
@@ -0,0 +1,51 @@
1
+ export default function useNFCPatrolTag() {
2
+ function getAll({
3
+ page = 1,
4
+ limit = 10,
5
+ site = "",
6
+ } = {}) {
7
+ return useNuxtApp().$api<Record<string, any>>(`/api/nfc-patrol-tag`, {
8
+ method: "GET",
9
+ query: {
10
+ page,
11
+ limit,
12
+ site,
13
+ },
14
+ });
15
+ }
16
+
17
+ function add(value: { site: string; tagID: string; name: string }) {
18
+ return useNuxtApp().$api<Record<string, any>>("/api/nfc-patrol-tag", {
19
+ method: "POST",
20
+ body: {
21
+ site: value.site,
22
+ tagID: value.tagID,
23
+ name: value.name,
24
+ },
25
+ });
26
+ }
27
+
28
+ function deleteById(id: string) {
29
+ return useNuxtApp().$api(`/api/nfc-patrol-tag/${id}`, {
30
+ method: "DELETE",
31
+ });
32
+ }
33
+
34
+ function updateById(id: string, value: { site: string; tagID: string; name: string }) {
35
+ return useNuxtApp().$api(`/api/nfc-patrol-tag/${id}`, {
36
+ method: "PUT",
37
+ body: {
38
+ site: value.site,
39
+ tagID: value.tagID,
40
+ name: value.name,
41
+ },
42
+ });
43
+ }
44
+
45
+ return {
46
+ getAll,
47
+ add,
48
+ deleteById,
49
+ updateById,
50
+ };
51
+ }
@@ -31,6 +31,13 @@ export default function(){
31
31
  });
32
32
  }
33
33
 
34
+ async function searchCompanyList(company: string): Promise<null | Partial<TPeople>> {
35
+ return await $fetch<Record<string, any>>('/api/people/company', {
36
+ method: "GET",
37
+ query: { search: company}
38
+ });
39
+ }
40
+
34
41
  async function create(payload: Partial<TPeoplePayload>){
35
42
  return await useNuxtApp().$api<Record<string, any>>("/api/people", {
36
43
  method: "POST",
@@ -51,6 +58,15 @@ export default function(){
51
58
  });
52
59
  }
53
60
 
61
+ async function getPeopleByUnit( _id: string,{
62
+ status = "active",
63
+ type = "resident",
64
+ } = {}){
65
+ return await useNuxtApp().$api<Record<string, any>>(`/api/people/unit/${_id}`, {
66
+ method: "GET",
67
+ query: { status, type }
68
+ });
69
+ }
54
70
 
55
71
  return {
56
72
  create,
@@ -58,6 +74,8 @@ export default function(){
58
74
  updateById,
59
75
  deleteById,
60
76
  findPersonByNRIC,
61
- findPersonByContact
77
+ findPersonByContact,
78
+ getPeopleByUnit,
79
+ searchCompanyList
62
80
  }
63
81
  }
@@ -14,7 +14,7 @@ export default function () {
14
14
  ];
15
15
 
16
16
  const typeFieldMap: Record<TVisitorType, (keyof TVisitorPayload)[]> = {
17
- guest: ['name', 'nric', 'contact', 'block', 'level', 'unit' , 'unitName', 'remarks'],
17
+ guest: ['name', 'nric', 'contact', 'company', 'block', 'level', 'unit' , 'unitName', 'remarks'],
18
18
  contractor: ['contractorType', 'name', 'nric', 'company', 'contact', 'plateNumber', 'block', 'level', 'unit', 'unitName', 'remarks'],
19
19
  delivery: ['attachments', 'name', 'deliveryType', 'company', 'nric', 'contact', 'plateNumber', 'block', 'level', 'unit' , 'unitName', 'remarks'],
20
20
  'walk-in': ['name', 'company', 'nric', 'contact', 'block', 'level', 'unit' , 'unitName', 'remarks'],
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@iservice365/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.6.0",
5
+ "version": "1.7.0",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",
@@ -11,6 +11,8 @@ declare type TBuildingUnit = {
11
11
  _id?: string;
12
12
  site: string;
13
13
  name?: string;
14
+ owner?: string;
15
+ ownerName?: string;
14
16
  building: string;
15
17
  buildingName?: string;
16
18
  block: number | null;
@@ -0,0 +1,22 @@
1
+ declare type TCard = {
2
+ _id?: string;
3
+ name: string;
4
+ accessCardType?: string;
5
+ type?: string;
6
+ cardNumber?: string;
7
+ startDate?: string | Date;
8
+ endDate?: string | Date;
9
+ door?: string;
10
+ accessGroup?: string[];
11
+ cardType?: string;
12
+ pinNo?: string;
13
+ useAsLiftCard?: boolean;
14
+ liftAccessLevel?: string;
15
+ isActivate?: boolean;
16
+ isAntiPassBack?: boolean;
17
+ status?: string;
18
+ org?: string | ObjectId;
19
+ site?: string | ObjectId;
20
+ unit: string;
21
+ assign: string;
22
+ };