@7365admin1/layer-common 3.0.7 → 3.0.9

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/components/CameraMain.vue +17 -1
  3. package/components/DashboardMain.vue +1854 -848
  4. package/components/EntryPassInformation.vue +60 -0
  5. package/components/HidAccessLogDashboard.vue +1691 -0
  6. package/components/HidEnabledGate.vue +51 -0
  7. package/components/HidIdentityMapping.vue +975 -0
  8. package/components/HidQrCodeConfiguration.vue +618 -0
  9. package/components/HidReaderForm.vue +246 -0
  10. package/components/HidReaderManagement.vue +1233 -0
  11. package/components/HidServiceSettingsPanel.vue +150 -0
  12. package/components/HidUserEnrollment.vue +1274 -0
  13. package/components/Layout/NavigationDrawer.vue +31 -1
  14. package/components/MemberInformation.vue +23 -15
  15. package/components/NavigationItem.vue +11 -4
  16. package/components/OnlineFormFill.vue +34 -0
  17. package/components/PassInformation.vue +73 -34
  18. package/components/SiteSettings.vue +60 -116
  19. package/components/VisitorForm.vue +815 -290
  20. package/components/VisitorFormSelection.vue +12 -2
  21. package/components/VisitorManagement.vue +652 -205
  22. package/composables/useCommonPermission.ts +59 -0
  23. package/composables/useDashboard.ts +46 -1
  24. package/composables/useHidAmico.ts +152 -0
  25. package/composables/useHidNavigation.ts +106 -0
  26. package/composables/useMember.ts +7 -1
  27. package/composables/useOptionalServices.ts +118 -0
  28. package/composables/useSettingsPermission.ts +138 -0
  29. package/composables/useSiteSettings.ts +1 -1
  30. package/composables/useUser.ts +1 -2
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +3 -2
  33. package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
  34. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
  35. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
  36. package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
  37. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
  38. package/types/local.d.ts +2 -1
  39. package/types/visitor.d.ts +4 -2
@@ -1,7 +1,11 @@
1
1
  <template>
2
2
  <v-card width="100%" :loading="processing">
3
3
  <v-toolbar>
4
- <v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
4
+ <v-row
5
+ no-gutters
6
+ class="fill-height px-6 d-flex justify-space-between align-center"
7
+ align="center"
8
+ >
5
9
  <span class="font-weight-bold text-h5 text-capitalize">
6
10
  {{ prop.mode }} {{ formatVisitorType(type) }}
7
11
  </span>
@@ -12,25 +16,54 @@
12
16
  <div class="w-100 d-flex justify-space-between ga-2">
13
17
  <span class="text-subtitle-1 w-100 font-weight-bold mb-3">{{
14
18
  formTitle
15
- }}</span>
16
- <span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
17
- <span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
19
+ }}</span>
20
+ <span
21
+ v-if="withStep2 || withStep3"
22
+ class="text-subtitle-2 font-weight-bold"
23
+ style="text-wrap: nowrap"
24
+ >Step <span class="text-primary-button">{{ contractorStep }}</span
25
+ >/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span
26
+ >
18
27
  </div>
19
- <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
28
+ <v-form
29
+ ref="formRef"
30
+ v-model="validForm"
31
+ :disabled="processing"
32
+ @click="errorMessage = ''"
33
+ >
20
34
  <v-row no-gutters class="pt-4">
21
35
  <v-col v-if="shouldShowField('contractorType')" cols="12">
22
- <InputLabel class="text-capitalize" title="Contractor Type" required />
23
- <v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" ref="contractorTypeCombo"
24
- autocomplete="off" :hide-no-data="false" @update:focused="handleFocusedContractorType"
25
- :items="contractorTypes" :rules="[requiredRule]" item-value="value"
26
- @update:model-value="handleSelectContractorType" variant="outlined" density="comfortable" persistent-hint
27
- small-chips>
36
+ <InputLabel
37
+ class="text-capitalize"
38
+ title="Contractor Type"
39
+ required
40
+ />
41
+ <v-combobox
42
+ v-model="contractorTypeObj"
43
+ v-model:search="contractorTypeInput"
44
+ ref="contractorTypeCombo"
45
+ autocomplete="off"
46
+ :hide-no-data="false"
47
+ @update:focused="handleFocusedContractorType"
48
+ :items="contractorTypes"
49
+ :rules="[requiredRule]"
50
+ item-value="value"
51
+ @update:model-value="handleSelectContractorType"
52
+ variant="outlined"
53
+ density="comfortable"
54
+ persistent-hint
55
+ small-chips
56
+ >
28
57
  <template v-slot:no-data>
29
58
  <v-list-item>
30
- <v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
59
+ <v-list-item-title
60
+ @click.stop="handleAddNewContractorType"
61
+ class="d-flex align-center ga-1"
62
+ >
31
63
  <span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
32
64
  contractorTypeInput
33
- }}</strong>" as custom contractor type.
65
+ }}</strong
66
+ >" as custom contractor type.
34
67
  </v-list-item-title>
35
68
  </v-list-item>
36
69
  </template>
@@ -39,14 +72,26 @@
39
72
 
40
73
  <v-col v-if="shouldShowField('attachments')" class="w-100">
41
74
  <InputLabel class="text-capitalize" title="Attach Image" />
42
- <InputFileV2 v-model="visitor.attachments" multiple :max-length="3" />
75
+ <InputFileV2
76
+ v-model="visitor.attachments"
77
+ multiple
78
+ :max-length="3"
79
+ />
43
80
  </v-col>
44
81
 
45
82
  <v-col v-if="shouldShowField('name')" cols="12">
46
83
  <v-row>
47
84
  <v-col cols="12">
48
- <InputLabel class="text-capitalize" title="Full Name" required />
49
- <v-text-field v-model.trim="visitor.name" density="comfortable" :rules="[requiredRule]" />
85
+ <InputLabel
86
+ class="text-capitalize"
87
+ title="Full Name"
88
+ required
89
+ />
90
+ <v-text-field
91
+ v-model.trim="visitor.name"
92
+ density="comfortable"
93
+ :rules="[requiredRule]"
94
+ />
50
95
  </v-col>
51
96
  </v-row>
52
97
  </v-col>
@@ -54,25 +99,44 @@
54
99
  <v-col v-if="shouldShowField('nric')" cols="12">
55
100
  <v-row>
56
101
  <v-col cols="12">
57
- <InputLabel class="text-capitalize" title="NRIC" :required="requireNRIC" />
58
- <InputNRICNumber v-model="visitor.nric" density="comfortable"
59
- :rules="[requireNRIC ? requiredRule : () => true]" :key="currentAutofillSource + 'nric-key'"
60
- @update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
102
+ <InputLabel
103
+ class="text-capitalize"
104
+ title="NRIC"
105
+ :required="requireNRIC"
106
+ />
107
+ <InputNRICNumber
108
+ v-model="visitor.nric"
109
+ density="comfortable"
110
+ :rules="[requireNRIC ? requiredRule : () => true]"
111
+ :key="currentAutofillSource + 'nric-key'"
112
+ @update:model-value="handleUpdateNRIC"
113
+ :loading="fetchPersonByNRICPending"
114
+ />
61
115
  </v-col>
62
116
  </v-row>
63
117
  </v-col>
64
118
 
65
119
  <v-col v-if="shouldShowField('contact')" cols="12">
66
120
  <InputLabel class="text-capitalize" title="Phone Number" required />
67
- <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable"
68
- :key="currentAutofillSource + 'phone-key'" :loading="fetchPersonByContactPending"
69
- @update:model-value="handleUpdateContact" />
121
+ <InputPhoneNumberV2
122
+ v-model="visitor.contact"
123
+ :rules="[requiredRule]"
124
+ density="comfortable"
125
+ :key="currentAutofillSource + 'phone-key'"
126
+ :loading="fetchPersonByContactPending"
127
+ @update:model-value="handleUpdateContact"
128
+ />
70
129
  </v-col>
71
130
 
72
131
  <v-col v-if="shouldShowField('deliveryType')" cols="12">
73
132
  <v-row>
74
133
  <v-col cols="12">
75
- <v-radio-group v-model="visitor.deliveryType" inline color="primary" :rules="[requiredRule]">
134
+ <v-radio-group
135
+ v-model="visitor.deliveryType"
136
+ inline
137
+ color="primary"
138
+ :rules="[requiredRule]"
139
+ >
76
140
  <v-radio label="Food" value="Food"></v-radio>
77
141
  <v-radio label="Parcel" value="Parcel"></v-radio>
78
142
  </v-radio-group>
@@ -83,22 +147,44 @@
83
147
  <template v-if="shouldShowField('company')">
84
148
  <v-col cols="12">
85
149
  <InputLabel class="text-capitalize" title="Company Name" />
86
- <v-combobox v-model="visitor.company" v-model:search="companyNameInput" ref="companyCombo"
87
- autocomplete="off" :hide-no-data="false" :items="companyNames" item-value="value"
88
- :loading="fetchCompanyListPending" @update:model-value="handleSelectCompanyName" variant="outlined"
89
- density="comfortable" persistent-hint small-chips>
150
+ <v-combobox
151
+ v-model="visitor.company"
152
+ v-model:search="companyNameInput"
153
+ ref="companyCombo"
154
+ autocomplete="off"
155
+ :hide-no-data="false"
156
+ :items="companyNames"
157
+ item-value="value"
158
+ :loading="fetchCompanyListPending"
159
+ @update:model-value="handleSelectCompanyName"
160
+ variant="outlined"
161
+ density="comfortable"
162
+ persistent-hint
163
+ small-chips
164
+ >
90
165
  <template v-slot:no-data>
91
166
  <v-list-item>
92
167
  <v-list-item-title v-if="fetchCompanyListPending">
93
- <v-progress-circular indeterminate size="20" class="mr-3" />
168
+ <v-progress-circular
169
+ indeterminate
170
+ size="20"
171
+ class="mr-3"
172
+ />
94
173
  Searching companies…
95
174
  </v-list-item-title>
96
- <v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
97
- class="d-flex align-center ga-1">
98
- <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{ companyNameInput }}</strong>" as new
99
- company.
175
+ <v-list-item-title
176
+ v-else-if="companyNameInput"
177
+ @click.stop="handleAddNewCompany"
178
+ class="d-flex align-center ga-1"
179
+ >
180
+ <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
181
+ companyNameInput
182
+ }}</strong
183
+ >" as new company.
100
184
  </v-list-item-title>
101
- <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
185
+ <v-list-item-title
186
+ v-else-if="!companyNameInput && companyNames.length === 0"
187
+ >
102
188
  Start typing to search for companies.
103
189
  </v-list-item-title>
104
190
  <v-list-item-title v-else>
@@ -113,21 +199,43 @@
113
199
  <template v-if="shouldShowField('delivery-company')">
114
200
  <v-col cols="12">
115
201
  <InputLabel class="text-capitalize" title="Delivery Company" />
116
- <v-combobox v-model="deliveryCompany" v-model:search="deliveryCompanyInput" ref="companyCombo"
117
- autocomplete="off" :hide-no-data="false" :items="deliveryCompanyList" item-value="value"
118
- :loading="siteDataPending" variant="outlined" density="comfortable" persistent-hint small-chips>
202
+ <v-combobox
203
+ v-model="deliveryCompany"
204
+ v-model:search="deliveryCompanyInput"
205
+ ref="companyCombo"
206
+ autocomplete="off"
207
+ :hide-no-data="false"
208
+ :items="deliveryCompanyList"
209
+ item-value="value"
210
+ :loading="siteDataPending"
211
+ variant="outlined"
212
+ density="comfortable"
213
+ persistent-hint
214
+ small-chips
215
+ >
119
216
  <template v-slot:no-data>
120
217
  <v-list-item>
121
218
  <v-list-item-title v-if="fetchCompanyListPending">
122
- <v-progress-circular indeterminate size="20" class="mr-3" />
219
+ <v-progress-circular
220
+ indeterminate
221
+ size="20"
222
+ class="mr-3"
223
+ />
123
224
  Searching companies…
124
225
  </v-list-item-title>
125
- <v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
126
- class="d-flex align-center ga-1">
127
- <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{ companyNameInput }}</strong>" as new
128
- company.
226
+ <v-list-item-title
227
+ v-else-if="companyNameInput"
228
+ @click.stop="handleAddNewCompany"
229
+ class="d-flex align-center ga-1"
230
+ >
231
+ <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
232
+ companyNameInput
233
+ }}</strong
234
+ >" as new company.
129
235
  </v-list-item-title>
130
- <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
236
+ <v-list-item-title
237
+ v-else-if="!companyNameInput && companyNames.length === 0"
238
+ >
131
239
  Start typing to search for companies.
132
240
  </v-list-item-title>
133
241
  <v-list-item-title v-else>
@@ -139,60 +247,138 @@
139
247
  </v-col>
140
248
  </template>
141
249
 
142
-
143
250
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
144
251
  <v-row>
145
252
  <v-col cols="12">
146
- <InputLabel class="text-capitalize" title="Vehicle Number" required />
253
+ <InputLabel
254
+ class="text-capitalize"
255
+ title="Vehicle Number"
256
+ required
257
+ />
147
258
  <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
148
- <InputVehicleNumber v-model="visitor.plateNumber" density="comfortable" />
259
+ <InputVehicleNumber
260
+ v-model="visitor.plateNumber"
261
+ density="comfortable"
262
+ />
149
263
  </v-col>
150
264
  </v-row>
151
265
  </v-col>
152
266
 
153
267
  <v-col v-if="shouldShowField('block')" cols="12">
154
268
  <InputLabel class="text-capitalize" title="Block" required />
155
- <v-autocomplete v-model="visitor.block" :items="blocksArray" item-value="value" autocomplete="off" item-title="title" :key="blockListDataPending ? 'block-list-pending' : 'block-list-ready'"
156
- :loading="blockListDataPending" @update:model-value="handleChangeBlock" density="comfortable"
157
- :rules="[requiredRule]" />
269
+ <v-autocomplete
270
+ v-model="visitor.block"
271
+ :items="blocksArray"
272
+ item-value="value"
273
+ autocomplete="off"
274
+ item-title="title"
275
+ :key="
276
+ blockListDataPending ? 'block-list-pending' : 'block-list-ready'
277
+ "
278
+ :loading="blockListDataPending"
279
+ @update:model-value="handleChangeBlock"
280
+ density="comfortable"
281
+ :rules="[requiredRule]"
282
+ />
158
283
  </v-col>
159
284
 
160
285
  <v-col v-if="shouldShowField('level')" cols="12">
161
286
  <InputLabel class="text-capitalize" title="Level" required />
162
- <v-autocomplete v-model="visitor.level" :items="levelsArray" density="comfortable" autocomplete="off"
163
- :disabled="!visitor.block" :key="levelListDataPending ? 'level-list-pending' : 'level-list-ready'" :loading="levelsDataPending" @update:model-value="handleChangeLevel"
164
- :rules="[requiredRule]" />
287
+ <v-autocomplete
288
+ v-model="visitor.level"
289
+ :items="levelsArray"
290
+ density="comfortable"
291
+ autocomplete="off"
292
+ :disabled="!visitor.block"
293
+ :key="
294
+ levelListDataPending ? 'level-list-pending' : 'level-list-ready'
295
+ "
296
+ :loading="levelsDataPending"
297
+ @update:model-value="handleChangeLevel"
298
+ :rules="[requiredRule]"
299
+ />
165
300
  </v-col>
166
301
  <v-col v-if="shouldShowField('unit')" cols="12">
167
302
  <InputLabel class="text-capitalize" title="Unit" required />
168
- <v-autocomplete v-model="visitor.unit" :items="unitsArray" density="comfortable" autocomplete="off" item-title="title"
169
- item-value="value" :disabled="!visitor.level" :key="unitListDataPending ? 'unit-list-pending' : 'unit-list-ready'" :loading="unitsDataPending" :rules="[requiredRule]"
170
- @update:model-value="handleUpdateUnit" />
303
+ <v-autocomplete
304
+ v-model="visitor.unit"
305
+ :items="unitsArray"
306
+ density="comfortable"
307
+ autocomplete="off"
308
+ item-title="title"
309
+ item-value="value"
310
+ :disabled="!visitor.level"
311
+ :key="
312
+ unitListDataPending ? 'unit-list-pending' : 'unit-list-ready'
313
+ "
314
+ :loading="unitsDataPending"
315
+ :rules="[requiredRule]"
316
+ @update:model-value="handleUpdateUnit"
317
+ />
171
318
  </v-col>
172
319
 
173
320
  <v-col v-if="shouldShowField('unit')" cols="12">
174
- <InputLabel class="text-capitalize" title="Registered Unit Company Name" required
175
- :key="'unit-key' + visitor.level" />
176
- <v-text-field v-model.trim="registeredUnitCompanyName" density="comfortable"
177
- :loading="buildingUnitDataPending" readonly class="no-pointer" />
321
+ <InputLabel
322
+ class="text-capitalize"
323
+ title="Registered Unit Company Name"
324
+ required
325
+ :key="'unit-key' + visitor.level"
326
+ />
327
+ <v-text-field
328
+ v-model.trim="registeredUnitCompanyName"
329
+ density="comfortable"
330
+ :loading="buildingUnitDataPending"
331
+ readonly
332
+ class="no-pointer"
333
+ />
178
334
  </v-col>
179
335
 
180
336
  <v-col v-if="shouldShowField('remarks')" cols="12">
181
337
  <InputLabel class="text-capitalize" title="Remarks" />
182
- <v-textarea v-model="visitor.remarks" density="comfortable" :rows="3" no-resize />
338
+ <v-textarea
339
+ v-model="visitor.remarks"
340
+ density="comfortable"
341
+ :rows="3"
342
+ no-resize
343
+ />
183
344
  </v-col>
184
345
 
185
- <v-col v-if="(withStep2 || withStep3) && contractorStep === 2" cols="12">
186
- <PassInformation v-model:pass="visitor.visitorPass" v-model:keys="visitor.passKeys" :site="prop.site"
187
- :type="prop.type" :contractor-type="visitor.contractorType" :hide-keys="!showShowKeysField" />
188
- <EntryPassInformation v-if="entryPassSettings?.data?.settings?.nfcPass" v-model="passType"
189
- v-model:quantity="passQuantity" v-model:cards="passCards" :settings="entryPassSettings"
190
- :loading="entryPassSettingsPending" :site-id="prop.site" :unit-id="visitor.unit || null" />
346
+ <v-col
347
+ v-if="(withStep2 || withStep3) && contractorStep === 2"
348
+ cols="12"
349
+ >
350
+ <PassInformation
351
+ v-model:pass="visitor.visitorPass"
352
+ v-model:keys="visitor.passKeys"
353
+ :site="prop.site"
354
+ :type="prop.type"
355
+ :contractor-type="visitor.contractorType"
356
+ :hide-keys="!showShowKeysField"
357
+ :passKeys="visitor.passKeys"
358
+ />
359
+ <EntryPassInformation
360
+ v-if="showEntryPassInformation"
361
+ v-model="passType"
362
+ v-model:quantity="passQuantity"
363
+ v-model:cards="passCards"
364
+ :settings="entryPassSettings"
365
+ :loading="entryPassSettingsPending || siteDataPending"
366
+ :site-id="prop.site"
367
+ :unit-id="visitor.unit || null"
368
+ :visitor-type="prop.type"
369
+ :hid-qr-code-enabled="hidQrCodePassAllowed"
370
+ :hid-qr-printer-configured="hasHidQrPrinterConfig"
371
+ />
191
372
  </v-col>
192
373
 
193
374
  <v-col v-if="withStep3 && contractorStep === 3" cols="12">
194
- <MemberInformation v-model="visitor.members" :type="prop.type" :contractor-type="visitor.contractorType"
195
- :site="prop.site" :selected-visitor-pass="visitor.visitorPass" />
375
+ <MemberInformation
376
+ v-model="visitor.members"
377
+ :type="prop.type"
378
+ :contractor-type="visitor.contractorType"
379
+ :site="prop.site"
380
+ :selected-visitor-pass="visitor.visitorPass"
381
+ />
196
382
  </v-col>
197
383
 
198
384
  <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
@@ -216,33 +402,89 @@
216
402
  <v-toolbar density="compact">
217
403
  <v-row no-gutters>
218
404
  <v-col cols="6">
219
- <v-btn v-if="
220
- prop.mode === 'add' &&
221
- (withStep2 || withStep3) &&
222
- contractorStep > 1
223
- " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
224
- <v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
225
- size="48" @click="backToSelection" text="Back to Selection" />
226
- <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
405
+ <v-btn
406
+ v-if="
407
+ prop.mode === 'add' &&
408
+ (withStep2 || withStep3) &&
409
+ contractorStep > 1
410
+ "
411
+ tile
412
+ block
413
+ variant="text"
414
+ class="text-none"
415
+ size="48"
416
+ @click="handleGoToPreviousPage"
417
+ text="Back"
418
+ />
419
+ <v-btn
420
+ v-else-if="prop.mode === 'add' || prop.mode === 'register'"
421
+ tile
422
+ block
423
+ variant="text"
424
+ class="text-none"
425
+ size="48"
426
+ @click="backToSelection"
427
+ text="Back to Selection"
428
+ />
429
+ <v-btn
430
+ v-else
431
+ tile
432
+ block
433
+ variant="text"
434
+ class="text-none"
435
+ size="48"
436
+ @click="emit('close:all')"
437
+ text="Close"
438
+ />
227
439
  </v-col>
228
440
  <v-col cols="6">
229
- <v-btn v-if="showNextButton" tile block variant="flat" color="primary-button" class="text-none" size="48"
230
- :disabled="processing" @click="handleNextPage" :text="'Next'" />
231
- <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
232
- :disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
233
- :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
441
+ <v-btn
442
+ v-if="showNextButton"
443
+ tile
444
+ block
445
+ variant="flat"
446
+ color="primary-button"
447
+ class="text-none"
448
+ size="48"
449
+ :disabled="processing"
450
+ @click="handleNextPage"
451
+ :text="'Next'"
452
+ />
453
+ <v-btn
454
+ v-else
455
+ tile
456
+ block
457
+ variant="flat"
458
+ color="black"
459
+ class="text-none"
460
+ size="48"
461
+ :disabled="!validForm || processing || membersFieldIncomplete"
462
+ @click="submit"
463
+ :text="prop.mode == 'add' ? 'Submit' : 'Update'"
464
+ />
234
465
  </v-col>
235
466
  </v-row>
236
467
  </v-toolbar>
237
468
 
238
- <v-dialog v-model="dialog.vehicleNumberUsersList" v-if="vehicleNumberUserItems.length > 0" persistent
239
- max-width="600">
240
- <SearchVehicleNumberUser :vehicle-number="visitor.plateNumber ?? ''"
241
- :vehicle-number-users-list="vehicleNumberUserItems" @close="dialog.vehicleNumberUsersList = false"
242
- @update:people="handleAutofillDataViaVehicleNumber" />
469
+ <v-dialog
470
+ v-model="dialog.vehicleNumberUsersList"
471
+ v-if="vehicleNumberUserItems.length > 0"
472
+ persistent
473
+ max-width="600"
474
+ >
475
+ <SearchVehicleNumberUser
476
+ :vehicle-number="visitor.plateNumber ?? ''"
477
+ :vehicle-number-users-list="vehicleNumberUserItems"
478
+ @close="dialog.vehicleNumberUsersList = false"
479
+ @update:people="handleAutofillDataViaVehicleNumber"
480
+ />
243
481
  </v-dialog>
244
482
 
245
- <v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="700" persistent>
483
+ <v-dialog
484
+ v-model="dialog.showNonCheckedOutDialog"
485
+ max-width="700"
486
+ persistent
487
+ >
246
488
  <v-card :loading="loading.checkingOut">
247
489
  <v-toolbar>
248
490
  <v-toolbar-title>
@@ -255,10 +497,13 @@
255
497
  </v-toolbar>
256
498
 
257
499
  <v-card-text>
258
-
259
500
  <v-list lines="three">
260
- <v-list-item v-if="matchingPlateNumberNonCheckedOutArr.length > 0"
261
- v-for="v in matchingPlateNumberNonCheckedOutArr" :key="v._id" class="cursor-pointer">
501
+ <v-list-item
502
+ v-if="matchingPlateNumberNonCheckedOutArr.length > 0"
503
+ v-for="v in matchingPlateNumberNonCheckedOutArr"
504
+ :key="v._id"
505
+ class="cursor-pointer"
506
+ >
262
507
  <v-list-item-title>
263
508
  {{ v.plateNumber }} - {{ v.name }}
264
509
  </v-list-item-title>
@@ -268,14 +513,19 @@
268
513
  </v-list-item-subtitle>
269
514
 
270
515
  <template #append>
271
- <v-btn size="x-small" class="text-capitalize" color="red" text="Checkout"
272
- :loading="loading.checkingOut && v?._id === prop.visitorData?._id"
273
- @click.stop="handleCheckout(v._id)" />
516
+ <v-btn
517
+ size="x-small"
518
+ class="text-capitalize"
519
+ color="red"
520
+ text="Checkout"
521
+ :loading="
522
+ loading.checkingOut && v?._id === prop.visitorData?._id
523
+ "
524
+ @click.stop="handleCheckout(v._id)"
525
+ />
274
526
  </template>
275
-
276
527
  </v-list-item>
277
528
  </v-list>
278
-
279
529
  </v-card-text>
280
530
  </v-card>
281
531
  </v-dialog>
@@ -283,9 +533,9 @@
283
533
  </template>
284
534
 
285
535
  <script setup lang="ts">
286
- import useBuildingUnit from '../composables/useBuildingUnit';
287
- import useWebUsb from '../composables/useWebUsb';
288
-
536
+ import { propsFactory } from "vuetify/lib/util/propsFactory.mjs";
537
+ import useBuildingUnit from "../composables/useBuildingUnit";
538
+ import useWebUsb from "../composables/useWebUsb";
289
539
 
290
540
  const prop = defineProps({
291
541
  type: {
@@ -308,21 +558,37 @@ const prop = defineProps({
308
558
  type: Object as PropType<Partial<TVisitor> | null>,
309
559
  default: null,
310
560
  },
561
+ currentUser: {
562
+ type: String,
563
+ default: "",
564
+ },
311
565
  });
312
566
 
313
567
  type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
314
568
  const currentAutofillSource = ref<AutofillSource>(null);
315
569
 
316
-
317
570
  const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
318
571
  const { getSiteById } = useSiteSettings();
319
- const { getBlocksSelection, getLevelsSelection, getUnitSelection } = useBuilding();
320
- const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
321
- const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
572
+ const { getBlocksSelection, getLevelsSelection, getUnitSelection } =
573
+ useBuilding();
574
+ const {
575
+ createVisitor,
576
+ typeFieldMap,
577
+ contractorTypes,
578
+ getVisitors,
579
+ updateVisitor,
580
+ } = useVisitor();
581
+ const { getBySiteId: getEntryPassSettingsBySiteId } =
582
+ useSiteEntryPassSettings();
322
583
  const { createVisitorPass, signQr } = useAccessManagement();
323
584
  const { testConnection } = useWebUsb();
324
- const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
325
- const { getById: getUnitDataById } = useBuildingUnit()
585
+ const {
586
+ findPersonByNRIC,
587
+ findPersonByContact,
588
+ searchCompanyList,
589
+ findUsersByPlateNumber,
590
+ } = usePeople();
591
+ const { getById: getUnitDataById } = useBuildingUnit();
326
592
 
327
593
  const emit = defineEmits([
328
594
  "back",
@@ -331,7 +597,7 @@ const emit = defineEmits([
331
597
  "done:more",
332
598
  "error",
333
599
  "close",
334
- "close:all"
600
+ "close:all",
335
601
  ]);
336
602
 
337
603
  const visitor = reactive<Partial<TVisitorPayload>>({
@@ -350,14 +616,16 @@ const visitor = reactive<Partial<TVisitorPayload>>({
350
616
  attachments: [],
351
617
  members: [],
352
618
  visitorPass: [],
353
- passKeys: []
619
+ passKeys: [],
354
620
  });
355
621
 
356
622
  const passType = ref<string | null>(null);
357
623
  const passQuantity = ref<number | null>(1);
358
624
  const passCards = ref<{ _id: string; cardNo: string }[]>([]);
625
+ const hidQrPrintPayload = ref<string | null>(null);
626
+ const hidQrPrinted = ref(false);
359
627
 
360
- const registeredUnitCompanyName = ref('N/A')
628
+ const registeredUnitCompanyName = ref("N/A");
361
629
 
362
630
  const dialog = reactive({
363
631
  vehicleNumberUsersList: false,
@@ -366,7 +634,7 @@ const dialog = reactive({
366
634
 
367
635
  const loading = reactive({
368
636
  checkingOut: false,
369
- })
637
+ });
370
638
 
371
639
  const validForm = ref(false);
372
640
  const formRef = ref<HTMLFormElement | null>(null);
@@ -382,7 +650,6 @@ const contractorStep = ref(1);
382
650
  const companyNameInput = ref("");
383
651
  const companyNameObj = ref<string | null>(null);
384
652
 
385
-
386
653
  const blocksArray = ref<TDefaultOptionObj[]>([]);
387
654
  const levelsArray = ref<TDefaultOptionObj[]>([]);
388
655
  const unitsArray = ref<TDefaultOptionObj[]>([]);
@@ -391,13 +658,13 @@ const deliveryCompany = ref("");
391
658
  const deliveryCompanyInput = ref("");
392
659
  const deliveryCompanyList = ref<string[]>([]);
393
660
 
394
- const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([])
395
-
396
-
397
- const vehicleNumberUserItems = ref<TPeople[]>([])
661
+ const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([]);
398
662
 
663
+ const vehicleNumberUserItems = ref<TPeople[]>([]);
399
664
 
400
- const shouldShowField = (fieldKey: keyof TVisitorPayload | 'delivery-company') => {
665
+ const shouldShowField = (
666
+ fieldKey: keyof TVisitorPayload | "delivery-company"
667
+ ) => {
401
668
  if ((!withStep2.value && !withStep3.value) || contractorStep.value === 1) {
402
669
  const visibleFields = typeFieldMap[prop.type];
403
670
  return visibleFields?.includes(fieldKey);
@@ -407,31 +674,72 @@ const shouldShowField = (fieldKey: keyof TVisitorPayload | 'delivery-company') =
407
674
  const withStep2 = computed(() => {
408
675
  const step2Types = ["guest", "walk-in"];
409
676
  return step2Types.includes(prop.type);
410
- })
677
+ });
411
678
 
412
679
  const withStep3 = computed(() => {
413
680
  const step3Types = ["contractor"];
414
681
  return step3Types.includes(prop.type);
415
- })
682
+ });
416
683
 
417
684
  const showNextButton = computed(() => {
418
685
  if (withStep3.value) {
419
- return contractorStep.value === 1 || contractorStep.value === 2
686
+ return contractorStep.value === 1 || contractorStep.value === 2;
420
687
  } else if (withStep2.value) {
421
- return contractorStep.value === 1
422
- } else return false
423
- })
688
+ return contractorStep.value === 1;
689
+ } else return false;
690
+ });
424
691
 
425
692
  const showShowKeysField = computed(() => {
426
- return prop.type === "contractor"
427
- })
693
+ return ["guest", "contractor", "walk-in"].includes(prop.type);
694
+ });
428
695
 
429
696
  const requireNRIC = computed(() => {
430
- return prop.type !== 'walk-in' && prop.type !== 'guest';
431
- })
697
+ return prop.type !== "walk-in" && prop.type !== "guest";
698
+ });
699
+
700
+ const hidQrCodePassConfig = computed(() => {
701
+ return (siteData.value as any)?.metadata?.hidQrCodePass || {};
702
+ });
703
+
704
+ const hidQrCodePassEnabled = computed(() => {
705
+ return Boolean(hidQrCodePassConfig.value?.enabled);
706
+ });
707
+
708
+ const hidQrPrinterConfig = computed(() => {
709
+ return hidQrCodePassConfig.value?.printer || {};
710
+ });
711
+
712
+ const hasHidQrPrinterConfig = computed(() => {
713
+ return Boolean(hidQrPrinterConfig.value?.vendorId && hidQrPrinterConfig.value?.productId);
714
+ });
715
+
716
+ const hidQrTemplate = computed(() => {
717
+ return hidQrCodePassConfig.value?.template || {};
718
+ });
719
+
720
+ const hidQrValidityMinutes = computed(() => {
721
+ const value = Number(hidQrCodePassConfig.value?.validityMinutes);
722
+ return Number.isFinite(value) && value > 0 ? value : 60;
723
+ });
724
+
725
+ const hidQrCodePassAllowed = computed(() => {
726
+ return hidQrCodePassEnabled.value && ["guest", "contractor", "walk-in"].includes(prop.type);
727
+ });
432
728
 
433
- const companyNames = ref<string[]>([])
434
- const companyAutofillDataArray = ref<{ companyName: string[], unit: string, block: number, level: string, unitName: string }[]>([])
729
+ const showEntryPassInformation = computed(() => {
730
+ return Boolean(entryPassSettings.value?.data?.settings?.nfcPass || hidQrCodePassAllowed.value);
731
+ });
732
+
733
+ const companyNames = ref<string[]>([]);
734
+ const companyAutofillDataArray = ref<
735
+ {
736
+ companyName: string[];
737
+ unit: string;
738
+ block: number;
739
+ level: string;
740
+ unitName: string;
741
+ }[]
742
+ >([]);
435
743
 
436
744
  const formTitle = computed(() => {
437
745
  const isContractorForm = prop.type === "contractor";
@@ -461,21 +769,21 @@ function handleFocusedContractorType() {
461
769
  // }
462
770
  }
463
771
 
464
- const contractorTypeCombo = ref(null)
772
+ const contractorTypeCombo = ref(null);
465
773
 
466
774
  async function handleAddNewContractorType() {
467
- visitor.contractorType = contractorTypeInput.value
775
+ visitor.contractorType = contractorTypeInput.value;
468
776
 
469
- const combo = contractorTypeCombo.value as any
777
+ const combo = contractorTypeCombo.value as any;
470
778
  await nextTick();
471
779
 
472
780
  combo.isMenuActive = false;
473
- combo.$el.querySelector("input")?.blur(); ``
474
-
781
+ combo.$el.querySelector("input")?.blur();
782
+ ``;
475
783
  }
476
784
 
477
785
  async function handleSelectCompanyName(company: string) {
478
- visitor.company = company
786
+ visitor.company = company;
479
787
  // const selected = companyAutofillDataArray.value.find(x => x.companyName?.includes(company))
480
788
  // if (!selected) return
481
789
 
@@ -485,48 +793,46 @@ async function handleSelectCompanyName(company: string) {
485
793
  // visitor.unitName = selected.unitName || ""
486
794
  }
487
795
 
488
- const companyCombo = ref(null)
796
+ const companyCombo = ref(null);
489
797
 
490
798
  async function handleAddNewCompany() {
491
- visitor.company = companyNameInput.value
799
+ visitor.company = companyNameInput.value;
492
800
 
493
- const combo = companyCombo.value as any
801
+ const combo = companyCombo.value as any;
494
802
  await nextTick();
495
803
 
496
804
  combo.isMenuActive = false;
497
805
  combo.$el.querySelector("input")?.blur();
498
806
  }
499
807
 
500
-
501
808
  const {
502
809
  data: fetchPersonByNRICReq,
503
810
  refresh: fetchPersonByNRICRefresh,
504
811
  pending: fetchPersonByNRICPending,
505
812
  } = useLazyAsyncData(`fetch-person`, () => {
506
813
  const NRIC = visitor.nric;
507
- if (!NRIC) return Promise.resolve(null)
508
- return findPersonByNRIC(NRIC)
509
- }
510
- );
814
+ if (!NRIC) return Promise.resolve(null);
815
+ return findPersonByNRIC(NRIC);
816
+ });
511
817
 
512
818
  watch(fetchPersonByNRICReq, (obj) => {
513
819
  if (obj) {
514
820
  currentAutofillSource.value = "nric";
515
- companyNames.value = obj.companyName ?? []
516
- visitor.name = obj.name
517
- visitor.contact = obj.contact
821
+ companyNames.value = obj.companyName ?? [];
822
+ visitor.name = obj.name;
823
+ visitor.contact = obj.contact;
518
824
  if (!visitor.company) {
519
- visitor.company = companyNames.value?.[0]
825
+ visitor.company = companyNames.value?.[0];
520
826
  }
521
- visitor.block = obj.block ?? ""
522
- visitor.level = obj.level ?? ""
523
- visitor.unit = obj.unit ?? ""
827
+ visitor.block = obj.block ?? "";
828
+ visitor.level = obj.level ?? "";
829
+ visitor.unit = obj.unit ?? "";
524
830
 
525
831
  setTimeout(() => {
526
832
  currentAutofillSource.value = null;
527
833
  }, 1000);
528
834
  }
529
- })
835
+ });
530
836
 
531
837
  const {
532
838
  data: fetchPersonByContactReq,
@@ -534,82 +840,86 @@ const {
534
840
  pending: fetchPersonByContactPending,
535
841
  } = useLazyAsyncData(`fetch-contact`, () => {
536
842
  const contact = visitor.contact;
537
- if (!contact) return Promise.resolve(null)
538
- return findPersonByContact(contact)
539
- }
540
- );
843
+ if (!contact) return Promise.resolve(null);
844
+ return findPersonByContact(contact);
845
+ });
541
846
 
542
847
  watch(fetchPersonByContactReq, (obj: Partial<TPerson>) => {
543
848
  if (obj) {
544
849
  currentAutofillSource.value = "contact";
545
- companyNames.value = obj.companyName ?? []
546
- visitor.name = visitor.name ?? obj.name
850
+ companyNames.value = obj.companyName ?? [];
851
+ visitor.name = visitor.name ?? obj.name;
547
852
  if (!visitor.company) {
548
- visitor.company = companyNames.value?.[0]
853
+ visitor.company = companyNames.value?.[0];
549
854
  }
550
855
  // visitor.block = visitor.block ?? obj.block ?? ""
551
856
  // visitor.level = visitor.level ?? obj.level ?? ""
552
857
  // visitor.unit = visitor.unit ?? obj.unit ?? ""
553
- visitor.nric = visitor.nric ?? obj.nric ?? ""
858
+ visitor.nric = visitor.nric ?? obj.nric ?? "";
554
859
 
555
860
  setTimeout(() => {
556
861
  currentAutofillSource.value = null;
557
862
  }, 1000);
558
863
  }
559
- })
864
+ });
560
865
 
561
866
  const {
562
867
  data: fetchCompanyListReq,
563
868
  refresh: fetchCompanyListRefresh,
564
869
  pending: fetchCompanyListPending,
565
-
566
870
  } = useLazyAsyncData(`fetch-company-list`, () => {
567
- if (!companyNameInput.value) return Promise.resolve(null)
568
- return searchCompanyList(companyNameInput.value)
569
- })
871
+ if (!companyNameInput.value) return Promise.resolve(null);
872
+ return searchCompanyList(companyNameInput.value);
873
+ });
570
874
 
571
875
  watch(fetchCompanyListReq, (arr) => {
572
876
  if (Array.isArray(arr)) {
573
877
  companyAutofillDataArray.value = arr;
574
- companyNames.value = arr?.flatMap(x => x?.companyName)
878
+ companyNames.value = arr?.flatMap((x) => x?.companyName);
575
879
  }
576
- })
880
+ });
577
881
  const {
578
882
  data: fetchVisitorListByVehicleNumberReq,
579
883
  refresh: fetchVisitorListByVehicleNumberRefresh,
580
884
  pending: fetchVisitorListByVehicleNumberPending,
581
-
582
885
  } = useLazyAsyncData(`fetch-visitor-list-by-vehicle-number`, () => {
583
- if (!visitor.plateNumber) return Promise.resolve(null)
584
- return getVisitors({ page: 1, limit: 20, site: prop.site, plateNumber: visitor.plateNumber, checkedOut: false, status: "registered" })
585
- })
886
+ if (!visitor.plateNumber) return Promise.resolve(null);
887
+ return getVisitors({
888
+ page: 1,
889
+ limit: 20,
890
+ site: prop.site,
891
+ plateNumber: visitor.plateNumber,
892
+ checkedOut: false,
893
+ status: "registered",
894
+ });
895
+ });
586
896
 
587
897
  watch(fetchVisitorListByVehicleNumberReq, (arr: any) => {
588
- const itemsArray = arr?.items || []
589
- const isValidArray = Array.isArray(itemsArray)
590
- matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : []
898
+ const itemsArray = arr?.items || [];
899
+ const isValidArray = Array.isArray(itemsArray);
900
+ matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : [];
591
901
  if (matchingPlateNumberNonCheckedOutArr.value.length > 0) {
592
902
  dialog.showNonCheckedOutDialog = true;
593
903
  } else {
594
904
  dialog.showNonCheckedOutDialog = false;
595
905
  }
596
- })
597
-
906
+ });
598
907
 
599
908
  const debounceSearchVisitorsByPlateNumbers = debounce(() => {
600
909
  if (!visitor.plateNumber) {
601
- matchingPlateNumberNonCheckedOutArr.value = []
910
+ matchingPlateNumberNonCheckedOutArr.value = [];
602
911
  dialog.showNonCheckedOutDialog = false;
603
912
  return;
604
913
  }
605
914
  fetchVisitorListByVehicleNumberRefresh();
606
915
  }, 300);
607
916
 
608
- watch(() => visitor.plateNumber, (newVal) => {
609
- debounceSearchVisitorsByPlateNumbers();
610
- });
611
-
612
-
917
+ watch(
918
+ () => visitor.plateNumber,
919
+ (newVal) => {
920
+ debounceSearchVisitorsByPlateNumbers();
921
+ }
922
+ );
613
923
 
614
924
  async function handleCheckout(visitorId: string) {
615
925
  if (!visitorId) {
@@ -624,7 +934,6 @@ async function handleCheckout(visitorId: string) {
624
934
  });
625
935
  if (res) {
626
936
  await fetchVisitorListByVehicleNumberRefresh();
627
-
628
937
  }
629
938
  } catch (error: any) {
630
939
  const errorMessage = error?.response?._data?.message;
@@ -636,50 +945,42 @@ async function handleCheckout(visitorId: string) {
636
945
  }
637
946
  }
638
947
 
639
-
640
-
641
-
642
- const debounceFetchCompany = debounce(async () => fetchCompanyListRefresh(), 200)
948
+ const debounceFetchCompany = debounce(
949
+ async () => fetchCompanyListRefresh(),
950
+ 200
951
+ );
643
952
 
644
953
  watch(companyNameInput, async (val) => {
645
954
  if (!val) {
646
- companyNames.value = []
647
- return
955
+ companyNames.value = [];
956
+ return;
648
957
  }
649
- await debounceFetchCompany()
650
- })
651
-
652
-
653
-
654
-
958
+ await debounceFetchCompany();
959
+ });
655
960
 
656
961
  function handleAutofillDataViaVehicleNumber(item: TPeople) {
657
-
658
962
  currentAutofillSource.value = "vehicleNumber";
659
963
 
660
- visitor.name = item.name
661
- visitor.nric = item.nric
662
- visitor.contact = item.contact
663
- companyNames.value = item.companyName ?? []
964
+ visitor.name = item.name;
965
+ visitor.nric = item.nric;
966
+ visitor.contact = item.contact;
967
+ companyNames.value = item.companyName ?? [];
664
968
  if (!visitor.company) {
665
- visitor.company = companyNames.value?.[0]
969
+ visitor.company = companyNames.value?.[0];
666
970
  }
667
- visitor.block = item.block ?? ""
668
- visitor.level = item.level ?? ""
669
- visitor.unit = item.unit ?? ""
971
+ visitor.block = item.block ?? "";
972
+ visitor.level = item.level ?? "";
973
+ visitor.unit = item.unit ?? "";
670
974
 
671
975
  setTimeout(() => {
672
976
  currentAutofillSource.value = null;
673
977
  }, 1000);
674
978
  }
675
979
 
676
-
677
- const {
678
- data: entryPassSettings,
679
- pending: entryPassSettingsPending,
680
- } = useLazyAsyncData(`fetch-entrypass-settings-${prop.site}`, () =>
681
- getEntryPassSettingsBySiteId(prop.site)
682
- );
980
+ const { data: entryPassSettings, pending: entryPassSettingsPending } =
981
+ useLazyAsyncData(`fetch-entrypass-settings-${prop.site}`, () =>
982
+ getEntryPassSettingsBySiteId(prop.site)
983
+ );
683
984
 
684
985
  const {
685
986
  data: siteData,
@@ -690,18 +991,17 @@ const {
690
991
  getSiteById(prop.site)
691
992
  );
692
993
 
693
-
694
994
  const {
695
995
  data: blocksListData,
696
996
  refresh: refreshBlockListData,
697
997
  status: blocksListStatus,
698
998
  pending: blockListDataPending,
699
- } = useLazyAsyncData(`visitor-fetch-block-list-data-${prop.site}`, () =>
700
- getBlocksSelection({ siteId: prop.site }), {watch: [() => prop.site], deep: true }
999
+ } = useLazyAsyncData(
1000
+ `visitor-fetch-block-list-data-${prop.site}`,
1001
+ () => getBlocksSelection({ siteId: prop.site }),
1002
+ { watch: [() => prop.site], deep: true }
701
1003
  );
702
1004
 
703
-
704
-
705
1005
  const {
706
1006
  data: levelsData,
707
1007
  refresh: refreshLevelsData,
@@ -712,8 +1012,12 @@ const {
712
1012
  async () => {
713
1013
  if (!visitor.block) return Promise.resolve(null);
714
1014
  // return await getSiteWithLevels(prop.site, { block: Number(visitor.block) });
715
- return await getLevelsSelection({ siteId: prop.site, blockId: visitor.block });
716
- }, { watch: [() => visitor.block], deep: true }
1015
+ return await getLevelsSelection({
1016
+ siteId: prop.site,
1017
+ blockId: visitor.block,
1018
+ });
1019
+ },
1020
+ { watch: [() => visitor.block], deep: true }
717
1021
  );
718
1022
 
719
1023
  const {
@@ -725,20 +1029,26 @@ const {
725
1029
  `visitor-fetch-units-data-${prop.site}-${visitor.level}`,
726
1030
  async () => {
727
1031
  if (!visitor.level) return Promise.resolve(null);
728
- return await getUnitSelection({ siteId: prop.site, block: visitor.block, level: visitor.level });
729
- },{ watch: [() => visitor.level], deep: true }
1032
+ return await getUnitSelection({
1033
+ siteId: prop.site,
1034
+ block: visitor.block,
1035
+ level: visitor.level,
1036
+ });
1037
+ },
1038
+ { watch: [() => visitor.level], deep: true }
730
1039
  );
731
1040
 
732
-
733
1041
  watch(
734
1042
  siteData,
735
1043
  (newVal) => {
736
1044
  const siteDataValue = newVal as TSite;
737
1045
  if (siteDataValue) {
738
-
739
- deliveryCompanyList.value = Array.isArray(siteDataValue?.deliveryCompanyList) ? siteDataValue.deliveryCompanyList : [];
740
-
741
- }
1046
+ deliveryCompanyList.value = Array.isArray(
1047
+ siteDataValue?.deliveryCompanyList
1048
+ )
1049
+ ? siteDataValue.deliveryCompanyList
1050
+ : [];
1051
+ }
742
1052
  },
743
1053
  { immediate: true }
744
1054
  );
@@ -746,83 +1056,96 @@ watch(
746
1056
  watch(
747
1057
  [blocksListData],
748
1058
  () => {
749
- if (blocksListStatus.value === 'pending') {
1059
+ if (blocksListStatus.value === "pending") {
750
1060
  blocksArray.value = [];
751
1061
  return;
752
1062
  }
753
1063
 
754
- const arr = Array.isArray(blocksListData.value?.data) ? blocksListData.value?.data : [];
1064
+ const arr = Array.isArray(blocksListData.value?.data)
1065
+ ? blocksListData.value?.data
1066
+ : [];
755
1067
  blocksArray.value = arr.map((block: any) => ({
756
1068
  title: block.name,
757
1069
  value: block._id,
758
1070
  }));
759
1071
  },
760
1072
  { immediate: true, deep: true }
761
- )
762
-
1073
+ );
763
1074
 
764
- watch([levelsData], () => {
765
- if (levelsStatus.value === 'pending') {
766
- levelsArray.value = [];
767
- return;
768
- }
1075
+ watch(
1076
+ [levelsData],
1077
+ () => {
1078
+ if (levelsStatus.value === "pending") {
1079
+ levelsArray.value = [];
1080
+ return;
1081
+ }
769
1082
 
770
- const arr = Array.isArray(levelsData.value?.data) ? levelsData.value?.data : [];
771
- const mappedLevels = arr.map((level: any) => ({
772
- title: level.name,
773
- value: level._id,
774
- }));
775
- levelsArray.value = mappedLevels;
776
- }, { immediate: true, deep: true})
1083
+ const arr = Array.isArray(levelsData.value?.data)
1084
+ ? levelsData.value?.data
1085
+ : [];
1086
+ const mappedLevels = arr.map((level: any) => ({
1087
+ title: level.name,
1088
+ value: level._id,
1089
+ }));
1090
+ levelsArray.value = mappedLevels;
1091
+ },
1092
+ { immediate: true, deep: true }
1093
+ );
777
1094
 
778
1095
  watch(
779
1096
  unitsData,
780
1097
  (newVal: any) => {
781
1098
  if (!newVal || !Array.isArray(newVal)) {
782
- unitsArray.value = []
783
- return
1099
+ unitsArray.value = [];
1100
+ return;
784
1101
  }
785
1102
 
786
1103
  const mapped = newVal.map((unit: any) => ({
787
1104
  title: unit.name,
788
1105
  value: unit._id,
789
- }))
1106
+ }));
790
1107
 
791
1108
  // keep custom unit pushed from autofill
792
- const selected = visitor.unit
793
- const exists = mapped.some((u: TDefaultOptionObj) => u.value === selected)
1109
+ const selected = visitor.unit;
1110
+ const exists = mapped.some((u: TDefaultOptionObj) => u.value === selected);
794
1111
 
795
1112
  if (!exists && visitor.unitName) {
796
1113
  mapped.push({
797
1114
  title: visitor.unitName,
798
1115
  value: selected,
799
- })
1116
+ });
800
1117
  }
801
1118
 
802
- unitsArray.value = mapped
1119
+ unitsArray.value = mapped;
803
1120
  },
804
1121
  { immediate: true, deep: true }
805
- )
806
-
1122
+ );
807
1123
 
808
- const { data: buildingUnitData, refresh: refreshBuildingUnitData, pending: buildingUnitDataPending } = useLazyAsyncData(`building-unit-data-${visitor.unit}`, () => {
1124
+ const {
1125
+ data: buildingUnitData,
1126
+ refresh: refreshBuildingUnitData,
1127
+ pending: buildingUnitDataPending,
1128
+ } = useLazyAsyncData(`building-unit-data-${visitor.unit}`, () => {
809
1129
  if (!visitor.unit) return Promise.resolve(null);
810
1130
  return getUnitDataById(visitor.unit);
811
- })
1131
+ });
812
1132
 
813
1133
  // trigger refresh of building unit data when unit changes, to get the latest unit name in case there are changes in the backend
814
- watch(() => visitor.unit, () => {
815
- if (visitor.unit) {
816
- refreshBuildingUnitData();
817
- } else {
818
- registeredUnitCompanyName.value = 'N/A';
1134
+ watch(
1135
+ () => visitor.unit,
1136
+ () => {
1137
+ if (visitor.unit) {
1138
+ refreshBuildingUnitData();
1139
+ } else {
1140
+ registeredUnitCompanyName.value = "N/A";
1141
+ }
819
1142
  }
820
- })
1143
+ );
821
1144
 
822
1145
  watch(buildingUnitData, (newVal: any) => {
823
- registeredUnitCompanyName.value = newVal?.data?.buildingUnit?.companyName || 'N/A';
824
- })
825
-
1146
+ registeredUnitCompanyName.value =
1147
+ newVal?.data?.buildingUnit?.companyName || "N/A";
1148
+ });
826
1149
 
827
1150
  function handleChangeBlock(value: any) {
828
1151
  visitor.level = "";
@@ -839,17 +1162,19 @@ function handleUpdateUnit(value: any) {
839
1162
  visitor.unitName = selectedUnit?.title || "";
840
1163
  }
841
1164
 
842
- const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500)
843
- const debounceFetchContact = debounce(fetchPersonByContactRefresh, 500)
1165
+ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500);
1166
+ const debounceFetchContact = debounce(fetchPersonByContactRefresh, 500);
844
1167
 
845
1168
  function handleUpdateNRIC() {
846
- if (currentAutofillSource.value && currentAutofillSource.value !== "nric") return;
847
- debounceFetchNRIC()
1169
+ if (currentAutofillSource.value && currentAutofillSource.value !== "nric")
1170
+ return;
1171
+ debounceFetchNRIC();
848
1172
  }
849
1173
 
850
1174
  function handleUpdateContact() {
851
- if (currentAutofillSource.value && currentAutofillSource.value !== "contact") return;
852
- debounceFetchContact()
1175
+ if (currentAutofillSource.value && currentAutofillSource.value !== "contact")
1176
+ return;
1177
+ debounceFetchContact();
853
1178
  }
854
1179
 
855
1180
  function backToSelection() {
@@ -902,12 +1227,20 @@ async function handleNextPage() {
902
1227
  async function submit() {
903
1228
  formRef.value!.validate();
904
1229
  errorMessage.value = "";
1230
+ if (!validForm.value) {
1231
+ errorMessage.value = "Please complete all required fields *";
1232
+ return;
1233
+ }
1234
+
1235
+ if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
1236
+
905
1237
  processing.value = true;
906
1238
 
907
1239
  let payload: Partial<TVisitorPayload> = {
908
1240
  type: prop.type,
909
1241
  org: prop.org,
910
1242
  site: prop.site,
1243
+ createdBy: prop.currentUser,
911
1244
  };
912
1245
 
913
1246
  if (prop.mode === "add" || prop.mode === "register") {
@@ -928,11 +1261,8 @@ async function submit() {
928
1261
  payload.visitorPass = visitor.visitorPass;
929
1262
  payload.passKeys = visitor.passKeys;
930
1263
  }
931
-
932
1264
  }
933
1265
 
934
-
935
-
936
1266
  if (prop.type === "contractor") {
937
1267
  // contractor type logic payload
938
1268
  payload = {
@@ -944,30 +1274,40 @@ async function submit() {
944
1274
  if (prop.type === "delivery") {
945
1275
  payload = {
946
1276
  ...payload,
947
- company: deliveryCompany.value
948
- }
1277
+ company: deliveryCompany.value,
1278
+ };
949
1279
  }
950
1280
 
951
1281
  // add register status when registering unregistered visitor
952
- if (prop.mode === 'register') {
953
- payload.status = "registered"
1282
+ if (prop.mode === "register") {
1283
+ payload.status = "registered";
954
1284
  }
955
1285
 
956
1286
  try {
957
1287
  let res;
958
1288
  if (prop.mode === "add") {
959
1289
  res = await createVisitor(payload as TVisitorPayload);
960
- } else if ((prop.mode === "edit" || prop.mode === "register") && prop.visitorData?._id) {
961
- const { unitName, ...rest } = payload
1290
+ } else if (
1291
+ (prop.mode === "edit" || prop.mode === "register") &&
1292
+ prop.visitorData?._id
1293
+ ) {
1294
+ const { unitName, ...rest } = payload;
962
1295
  res = await updateVisitor(prop.visitorData._id, rest as TVisitorPayload);
963
1296
  }
964
1297
 
965
1298
  if (res) {
966
- if (prop.type === "contractor" && passType.value) {
1299
+ if (passType.value === "HID_QR") {
1300
+ const printed = await printHidQrCodePass(getVisitorIdFromResponse(res), false);
1301
+ if (!printed) return;
1302
+ }
1303
+
1304
+ if (prop.type === "contractor" && (passType.value === "QR" || passType.value === "NFC")) {
967
1305
  const visitorId = res as unknown as string;
968
1306
  const rawUrl = entryPassSettings.value?.data?.settings?.url;
969
1307
  const { encrypted: acmUrl } = await $fetch<{ encrypted: string }>(
970
- `/api/encrypt-acm-url${rawUrl ? `?url=${encodeURIComponent(rawUrl)}` : ""}`
1308
+ `/api/encrypt-acm-url${
1309
+ rawUrl ? `?url=${encodeURIComponent(rawUrl)}` : ""
1310
+ }`
971
1311
  );
972
1312
  if (visitorId && acmUrl) {
973
1313
  const passRes = await createVisitorPass({
@@ -982,7 +1322,8 @@ async function submit() {
982
1322
 
983
1323
  if (passType.value === "QR") {
984
1324
  console.log("[QR Print] passRes:", passRes);
985
- const accessCards: any[] = (passRes as any)?.data?.accessCards ?? [];
1325
+ const accessCards: any[] =
1326
+ (passRes as any)?.data?.accessCards ?? [];
986
1327
  console.log("[QR Print] accessCards:", accessCards);
987
1328
  const printer = entryPassSettings.value?.data?.settings?.printer;
988
1329
  console.log("[QR Print] printer config:", printer);
@@ -994,23 +1335,57 @@ async function submit() {
994
1335
  const vendorId = parseInt(printer.vendorId);
995
1336
  const productId = parseInt(printer.productId);
996
1337
  const companyName = visitor.company || "";
997
- const blockLabel = blocksArray.value.find((b: any) => b.value === visitor.block)?.title || "";
998
- const levelLabel = levelsArray.value.find((l: any) => l.value === visitor.level)?.title || "";
999
- const unitLabel = unitsArray.value.find((u: any) => u.value === visitor.unit)?.title || "";
1000
- const address = [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
1001
- console.log("[QR Print] Printing", accessCards.length, "card(s). vendorId:", vendorId, "productId:", productId, "address:", address);
1338
+ const blockLabel =
1339
+ blocksArray.value.find((b: any) => b.value === visitor.block)
1340
+ ?.title || "";
1341
+ const levelLabel =
1342
+ levelsArray.value.find((l: any) => l.value === visitor.level)
1343
+ ?.title || "";
1344
+ const unitLabel =
1345
+ unitsArray.value.find((u: any) => u.value === visitor.unit)
1346
+ ?.title || "";
1347
+ const address = [blockLabel, levelLabel, unitLabel]
1348
+ .filter(Boolean)
1349
+ .join("/");
1350
+ console.log(
1351
+ "[QR Print] Printing",
1352
+ accessCards.length,
1353
+ "card(s). vendorId:",
1354
+ vendorId,
1355
+ "productId:",
1356
+ productId,
1357
+ "address:",
1358
+ address
1359
+ );
1002
1360
 
1003
1361
  await nextTick();
1004
1362
  for (const qrCode of accessCards) {
1005
1363
  console.log("[QR Print] Signing QR for card:", qrCode._id);
1006
- const signed = await signQr({ cardId: qrCode._id, purpose: "vms" });
1364
+ const signed = await signQr({
1365
+ cardId: qrCode._id,
1366
+ purpose: "vms",
1367
+ });
1007
1368
  const qrData = signed?.data;
1008
- console.log("[QR Print] signQr result:", signed, "| qrData:", qrData);
1369
+ console.log(
1370
+ "[QR Print] signQr result:",
1371
+ signed,
1372
+ "| qrData:",
1373
+ qrData
1374
+ );
1009
1375
  if (!qrData) {
1010
- console.warn("[QR Print] Skipping card", qrCode._id, "— signQr returned no data.");
1376
+ console.warn(
1377
+ "[QR Print] Skipping card",
1378
+ qrCode._id,
1379
+ "— signQr returned no data."
1380
+ );
1011
1381
  continue;
1012
1382
  }
1013
- console.log("[QR Print] Sending to printer — accessLevel:", qrCode.accessLevel, "liftLevel:", qrCode.liftAccessLevel);
1383
+ console.log(
1384
+ "[QR Print] Sending to printer — accessLevel:",
1385
+ qrCode.accessLevel,
1386
+ "liftLevel:",
1387
+ qrCode.liftAccessLevel
1388
+ );
1014
1389
  const printResult = await testConnection(
1015
1390
  vendorId,
1016
1391
  productId,
@@ -1019,9 +1394,14 @@ async function submit() {
1019
1394
  qrCode.accessLevel,
1020
1395
  qrCode.liftAccessLevel,
1021
1396
  companyName,
1022
- address,
1397
+ address
1398
+ );
1399
+ console.log(
1400
+ "[QR Print] Print result for card",
1401
+ qrCode._id,
1402
+ ":",
1403
+ printResult
1023
1404
  );
1024
- console.log("[QR Print] Print result for card", qrCode._id, ":", printResult);
1025
1405
  }
1026
1406
  }
1027
1407
  }
@@ -1035,13 +1415,139 @@ async function submit() {
1035
1415
  const err = error?.data?.message;
1036
1416
  errorMessage.value =
1037
1417
  err ||
1038
- `Failed to ${prop.mode === "add" ? "add" : "update"
1418
+ `Failed to ${
1419
+ prop.mode === "add" ? "add" : "update"
1039
1420
  } visitor. Please try again.`;
1040
1421
  } finally {
1041
1422
  processing.value = false;
1042
1423
  }
1043
1424
  }
1044
1425
 
1426
+ function parseUsbId(value: unknown) {
1427
+ if (typeof value === "number") return value;
1428
+ const text = String(value || "").trim();
1429
+ if (!text) return NaN;
1430
+ return text.toLowerCase().startsWith("0x")
1431
+ ? parseInt(text, 16)
1432
+ : parseInt(text, 10);
1433
+ }
1434
+
1435
+ function getVisitorIdFromResponse(response: any) {
1436
+ if (typeof response === "string") return response;
1437
+ return (
1438
+ response?._id ||
1439
+ response?.id ||
1440
+ response?.data?._id ||
1441
+ response?.data?.id ||
1442
+ response?.insertedId ||
1443
+ response?.data?.insertedId ||
1444
+ ""
1445
+ );
1446
+ }
1447
+
1448
+ function getHidQrPrinterUsbIds() {
1449
+ const vendorId = parseUsbId(hidQrPrinterConfig.value.vendorId);
1450
+ const productId = parseUsbId(hidQrPrinterConfig.value.productId);
1451
+ return { vendorId, productId };
1452
+ }
1453
+
1454
+ function validateHidQrPrinterConfig() {
1455
+ if (!hasHidQrPrinterConfig.value) {
1456
+ errorMessage.value = "No printer is configured on HID settings. Please configure the HID QR Code printer before continuing.";
1457
+ return false;
1458
+ }
1459
+
1460
+ const { vendorId, productId } = getHidQrPrinterUsbIds();
1461
+
1462
+ if (!Number.isFinite(vendorId) || !Number.isFinite(productId)) {
1463
+ errorMessage.value = "Invalid HID QR Code printer configuration. Please check Vendor and Product ID in Settings.";
1464
+ return false;
1465
+ }
1466
+
1467
+ return true;
1468
+ }
1469
+
1470
+ function buildHidQrCodePayload(visitorId = "") {
1471
+ if (hidQrPrintPayload.value) return hidQrPrintPayload.value;
1472
+
1473
+ const issuedAt = new Date();
1474
+ const expiresAt = new Date(issuedAt.getTime() + hidQrValidityMinutes.value * 60 * 1000);
1475
+ const nonce =
1476
+ typeof crypto !== "undefined" && "randomUUID" in crypto
1477
+ ? crypto.randomUUID()
1478
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1479
+
1480
+ hidQrPrintPayload.value = JSON.stringify({
1481
+ purpose: "hid-amico-visitor-access",
1482
+ visitorId,
1483
+ site: prop.site,
1484
+ org: prop.org,
1485
+ visitorType: prop.type,
1486
+ name: visitor.name || "",
1487
+ company: visitor.company || "",
1488
+ contact: visitor.contact || "",
1489
+ nric: visitor.nric || "",
1490
+ plateNumber: visitor.plateNumber || "",
1491
+ block: visitor.block || "",
1492
+ level: visitor.level || "",
1493
+ unit: visitor.unit || "",
1494
+ unitName: visitor.unitName || "",
1495
+ address: buildVisitorAddress(),
1496
+ issuedAt: issuedAt.toISOString(),
1497
+ expiresAt: expiresAt.toISOString(),
1498
+ nonce,
1499
+ });
1500
+
1501
+ return hidQrPrintPayload.value;
1502
+ }
1503
+
1504
+ function buildVisitorAddress() {
1505
+ const blockLabel = blocksArray.value.find((b: any) => b.value === visitor.block)?.title || "";
1506
+ const levelLabel = levelsArray.value.find((l: any) => l.value === visitor.level)?.title || "";
1507
+ const unitLabel = unitsArray.value.find((u: any) => u.value === visitor.unit)?.title || visitor.unitName || "";
1508
+ return [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
1509
+ }
1510
+
1511
+ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
1512
+ if (hidQrPrinted.value) return true;
1513
+
1514
+ if (!validateHidQrPrinterConfig()) return false;
1515
+
1516
+ const { vendorId, productId } = getHidQrPrinterUsbIds();
1517
+
1518
+ if (manageProcessing) processing.value = true;
1519
+ try {
1520
+ const printResult = await testConnection(
1521
+ vendorId,
1522
+ productId,
1523
+ true,
1524
+ buildHidQrCodePayload(visitorId),
1525
+ null,
1526
+ null,
1527
+ visitor.company || visitor.name || "",
1528
+ buildVisitorAddress(),
1529
+ hidQrTemplate.value?.header || "HID QR Code",
1530
+ hidQrTemplate.value?.subtext || "Scan QR code for access",
1531
+ );
1532
+
1533
+ if (!printResult?.success || printResult?.printTest?.success === false) {
1534
+ errorMessage.value =
1535
+ printResult?.printTest?.error ||
1536
+ printResult?.error ||
1537
+ "Unable to print HID QR Code. Please check the printer connection.";
1538
+ return false;
1539
+ }
1540
+
1541
+ hidQrPrinted.value = true;
1542
+ return true;
1543
+ } catch (error: any) {
1544
+ errorMessage.value = error?.message || "Unable to print HID QR Code.";
1545
+ return false;
1546
+ } finally {
1547
+ if (manageProcessing) processing.value = false;
1548
+ }
1549
+ }
1550
+
1045
1551
  watch(
1046
1552
  () => visitor.plateNumber,
1047
1553
  (newVal) => {
@@ -1049,6 +1555,22 @@ watch(
1049
1555
  }
1050
1556
  );
1051
1557
 
1558
+ watch(
1559
+ [
1560
+ () => passType.value,
1561
+ () => visitor.name,
1562
+ () => visitor.company,
1563
+ () => visitor.block,
1564
+ () => visitor.level,
1565
+ () => visitor.unit,
1566
+ () => visitor.unitName,
1567
+ ],
1568
+ () => {
1569
+ hidQrPrintPayload.value = null;
1570
+ hidQrPrinted.value = false;
1571
+ }
1572
+ );
1573
+
1052
1574
  const membersFieldIncomplete = computed(() => {
1053
1575
  if (!visitor.members || visitor.members.length === 0) return false;
1054
1576
 
@@ -1065,9 +1587,12 @@ onMounted(() => {
1065
1587
  contractorStep.value = 1;
1066
1588
  currentAutofillSource.value = null;
1067
1589
 
1068
- if (prop.mode === 'register' && prop.visitorData) {
1069
- console.log('Register mode, prefill visitor data', prop.visitorData?.plateNumber)
1070
- visitor.plateNumber = prop.visitorData?.plateNumber || ""
1590
+ if (prop.mode === "register" && prop.visitorData) {
1591
+ console.log(
1592
+ "Register mode, prefill visitor data",
1593
+ prop.visitorData?.plateNumber
1594
+ );
1595
+ visitor.plateNumber = prop.visitorData?.plateNumber || "";
1071
1596
  }
1072
1597
  });
1073
1598
  </script>