@7365admin1/layer-common 3.2.1 → 3.2.2-staging.76

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 (55) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/.changeset/camera-wall-plain-english.md +60 -0
  3. package/.changeset/camera-wall-selection-and-guidance.md +65 -0
  4. package/.changeset/dark-primary-contrast.md +39 -0
  5. package/.changeset/dashboard-dark-theme-contrast.md +15 -0
  6. package/.changeset/service-provider-invitation-actions.md +28 -0
  7. package/components/CameraWall.vue +1034 -0
  8. package/components/CameraWallTile.vue +818 -0
  9. package/components/DashboardMain.vue +84 -88
  10. package/components/DashboardPanel.vue +1 -1
  11. package/components/EntryPassInformation.vue +4 -4
  12. package/components/FeedbackMain.vue +7 -7
  13. package/components/HidAccessLogDashboard.vue +119 -44
  14. package/components/HidCameraCaptureDialog.vue +199 -0
  15. package/components/HidIntercomManagement.vue +808 -201
  16. package/components/HidQrCodeConfiguration.vue +907 -95
  17. package/components/HidServiceSettingsPanel.vue +20 -1
  18. package/components/HidUserEnrollment.vue +392 -81
  19. package/components/Layout/NavigationDrawer.vue +43 -2
  20. package/components/NavigationItem.vue +9 -0
  21. package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
  22. package/components/OnlineFormConfigurationForm.vue +620 -224
  23. package/components/OnlineFormFill.vue +36 -8
  24. package/components/OnlineFormsConfiguration.vue +6 -2
  25. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  26. package/components/QrTemplate/PrintDialog.vue +209 -196
  27. package/components/ServiceProviderInvitationPrompt.vue +150 -0
  28. package/components/ServiceProviderMain.vue +258 -14
  29. package/components/SiteSettings.vue +4 -2
  30. package/components/VisitorForm.vue +201 -78
  31. package/components/VisitorManagement.vue +2 -3
  32. package/components/VisitorSocketPopUp.vue +56 -2
  33. package/composables/useComment.ts +3 -3
  34. package/composables/useHidAmico.ts +127 -0
  35. package/composables/useHidNavigation.ts +0 -7
  36. package/composables/useLocalAuth.ts +9 -36
  37. package/composables/useOnlineFormPages.ts +2 -0
  38. package/composables/useServiceProviderInvitation.ts +145 -0
  39. package/composables/useSipWebPhone.ts +311 -0
  40. package/composables/useSiteSettings.ts +50 -0
  41. package/composables/useVisitorSocket.ts +26 -0
  42. package/composables/useWebUsb.ts +127 -37
  43. package/package.json +3 -1
  44. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  45. package/plugins/secure-member.client.ts +21 -56
  46. package/plugins/vuetify.ts +37 -9
  47. package/test/visitor-socket.test.mjs +36 -0
  48. package/types/site.d.ts +71 -0
  49. package/utils/camera-wall.test.ts +571 -0
  50. package/utils/camera-wall.ts +926 -0
  51. package/utils/theme.test.ts +216 -0
  52. package/utils/theme.ts +163 -0
  53. package/components/HidIdentityMapping.vue +0 -975
  54. package/middleware/member.ts +0 -4
  55. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -50,7 +50,13 @@
50
50
 
51
51
  <v-card flat border class="table-card">
52
52
  <div class="table-refresh">
53
- <v-btn icon="mdi-refresh" variant="text" density="comfortable" :loading="loading" @click="loadUsers" />
53
+ <v-btn
54
+ icon="mdi-refresh"
55
+ variant="text"
56
+ density="comfortable"
57
+ :loading="loading"
58
+ @click="loadUsers"
59
+ />
54
60
  <div class="page-count">{{ pageRange }}</div>
55
61
  </div>
56
62
 
@@ -66,10 +72,18 @@
66
72
  </tr>
67
73
  </thead>
68
74
  <tbody>
69
- <tr v-for="user in users" :key="user._rowKey || user._id || user.hidUserId">
75
+ <tr
76
+ v-for="user in users"
77
+ :key="user._rowKey || user._id || user.hidUserId"
78
+ >
70
79
  <td>{{ getName(user) }}</td>
71
80
  <td>
72
- <v-avatar v-if="getUserImageSrc(user)" size="34" rounded="lg" class="hid-face-avatar">
81
+ <v-avatar
82
+ v-if="getUserImageSrc(user)"
83
+ size="34"
84
+ rounded="lg"
85
+ class="hid-face-avatar"
86
+ >
73
87
  <v-img :src="getUserImageSrc(user)" cover />
74
88
  </v-avatar>
75
89
  <span v-else>{{ getFacialData(user) }}</span>
@@ -88,13 +102,22 @@
88
102
  <td class="text-right">
89
103
  <v-menu>
90
104
  <template #activator="{ props: menuProps }">
91
- <v-btn v-bind="menuProps" icon="mdi-dots-vertical" variant="text" density="comfortable" />
105
+ <v-btn
106
+ v-bind="menuProps"
107
+ icon="mdi-dots-vertical"
108
+ variant="text"
109
+ density="comfortable"
110
+ />
92
111
  </template>
93
112
 
94
113
  <v-list density="compact" min-width="150">
95
114
  <v-list-item title="View" @click="openView(user)" />
96
115
  <v-list-item title="Edit" @click="openEdit(user)" />
97
- <v-list-item title="Delete" class="text-error" @click="openDelete(user)" />
116
+ <v-list-item
117
+ title="Delete"
118
+ class="text-error"
119
+ @click="openDelete(user)"
120
+ />
98
121
  </v-list>
99
122
  </v-menu>
100
123
  </td>
@@ -109,8 +132,26 @@
109
132
 
110
133
  <div class="table-footer">
111
134
  <span>{{ pageRange }}</span>
112
- <v-btn icon="mdi-chevron-left" variant="text" density="comfortable" :disabled="page <= 1" @click="page--; loadUsers()" />
113
- <v-btn icon="mdi-chevron-right" variant="text" density="comfortable" :disabled="page >= pages" @click="page++; loadUsers()" />
135
+ <v-btn
136
+ icon="mdi-chevron-left"
137
+ variant="text"
138
+ density="comfortable"
139
+ :disabled="page <= 1"
140
+ @click="
141
+ page--;
142
+ loadUsers();
143
+ "
144
+ />
145
+ <v-btn
146
+ icon="mdi-chevron-right"
147
+ variant="text"
148
+ density="comfortable"
149
+ :disabled="page >= pages"
150
+ @click="
151
+ page++;
152
+ loadUsers();
153
+ "
154
+ />
114
155
  </div>
115
156
  </v-card>
116
157
 
@@ -125,7 +166,12 @@
125
166
  <v-menu>
126
167
  <template #activator="{ props: menuProps }">
127
168
  <button v-bind="menuProps" class="photo-button" type="button">
128
- <v-img v-if="form.photoPreview" :src="form.photoPreview" cover class="photo-preview" />
169
+ <v-img
170
+ v-if="form.photoPreview"
171
+ :src="form.photoPreview"
172
+ cover
173
+ class="photo-preview"
174
+ />
129
175
  <span v-if="form.photoPreview" class="photo-edit-icon">
130
176
  <v-icon icon="mdi-pencil" size="16" />
131
177
  </span>
@@ -137,10 +183,25 @@
137
183
  </template>
138
184
  <v-list density="compact" min-width="190">
139
185
  <v-list-item title="Browse" @click="triggerPhotoInput" />
140
- <v-list-item title="Take Photo using Camera" @click="triggerPhotoInput" />
186
+ <v-list-item
187
+ title="Take Photo using Camera"
188
+ @click="triggerCameraInput"
189
+ />
190
+ <v-list-item
191
+ v-if="form.photoPreview"
192
+ title="Remove Photo"
193
+ class="text-error"
194
+ @click="removePhoto"
195
+ />
141
196
  </v-list>
142
197
  </v-menu>
143
- <input ref="photoInput" type="file" accept="image/*" class="hidden-input" @change="onPhotoChange" />
198
+ <input
199
+ ref="photoInput"
200
+ type="file"
201
+ accept="image/*"
202
+ class="hidden-input"
203
+ @change="onPhotoChange"
204
+ />
144
205
  </div>
145
206
 
146
207
  <div class="field-label">Name <span>*</span></div>
@@ -203,31 +264,61 @@
203
264
  </v-card-text>
204
265
 
205
266
  <v-card-actions class="pa-0">
206
- <v-btn class="text-none action-cancel" variant="flat" height="44" @click="formDialog = false">Cancel</v-btn>
207
- <v-btn class="text-none action-submit" variant="flat" height="44" :loading="saving" @click="saveUser">
267
+ <v-btn
268
+ class="text-none action-cancel"
269
+ variant="flat"
270
+ height="44"
271
+ @click="formDialog = false"
272
+ >Cancel</v-btn
273
+ >
274
+ <v-btn
275
+ class="text-none action-submit"
276
+ variant="flat"
277
+ height="44"
278
+ :loading="saving"
279
+ @click="saveUser"
280
+ >
208
281
  Save
209
282
  </v-btn>
210
283
  </v-card-actions>
211
284
  </v-card>
212
285
  </v-dialog>
213
286
 
287
+ <HidCameraCaptureDialog v-model="cameraDialog" @capture="onCameraCapture" />
288
+
214
289
  <v-dialog v-model="viewDialog" max-width="430">
215
290
  <v-card rounded="lg">
216
291
  <v-card-title class="small-title">User Information</v-card-title>
217
292
  <v-card-text>
218
293
  <div class="detail-grid">
219
294
  <span>Name</span><strong>{{ getName(selectedUser) }}</strong>
220
- <span>User ID</span><strong>{{ selectedUser ? formatHidUid(selectedUser.hidUserId) : "N/A" }}</strong>
221
- <span>Registration No.</span><strong>{{ selectedUser?.registration || "N/A" }}</strong>
222
- <span>Last access time</span><strong>{{ formatDate(selectedUser?.metadata?.lastAccessAt) }}</strong>
223
- <span>Status</span><strong>{{ selectedUser ? getMappingStatus(selectedUser) : "N/A" }}</strong>
224
- <span>Panic Pin</span><strong>{{ selectedUser?.cardNo || "N/A" }}</strong>
225
- <span>Private Password</span><strong>{{ selectedUser?.metadata?.pinPassword ? "********" : "N/A" }}</strong>
295
+ <span>User ID</span
296
+ ><strong>{{
297
+ selectedUser ? formatHidUid(selectedUser.hidUserId) : "N/A"
298
+ }}</strong>
299
+ <span>Registration No.</span
300
+ ><strong>{{ selectedUser?.registration || "N/A" }}</strong>
301
+ <span>Last access time</span
302
+ ><strong>{{
303
+ formatDate(selectedUser?.metadata?.lastAccessAt)
304
+ }}</strong>
305
+ <span>Status</span
306
+ ><strong>{{
307
+ selectedUser ? getMappingStatus(selectedUser) : "N/A"
308
+ }}</strong>
309
+ <span>Panic Pin</span
310
+ ><strong>{{ selectedUser?.cardNo || "N/A" }}</strong>
311
+ <span>Private Password</span
312
+ ><strong>{{
313
+ selectedUser?.metadata?.pinPassword ? "********" : "N/A"
314
+ }}</strong>
226
315
  </div>
227
316
  </v-card-text>
228
317
  <v-card-actions>
229
318
  <v-spacer />
230
- <v-btn class="text-none" variant="text" @click="viewDialog = false">Close</v-btn>
319
+ <v-btn class="text-none" variant="text" @click="viewDialog = false"
320
+ >Close</v-btn
321
+ >
231
322
  </v-card-actions>
232
323
  </v-card>
233
324
  </v-dialog>
@@ -238,12 +329,26 @@
238
329
  <v-card-text class="text-body-2">
239
330
  Are you sure you want to permanently delete this HID user?
240
331
  <div class="text-caption text-medium-emphasis mt-2">
241
- This action will remove the user from the HID reader and mark the identity as deleted.
332
+ This action will remove the user from the HID reader and mark the
333
+ identity as deleted.
242
334
  </div>
243
335
  </v-card-text>
244
336
  <v-card-actions class="pa-0">
245
- <v-btn class="text-none action-cancel" variant="flat" height="44" @click="deleteDialog = false">Cancel</v-btn>
246
- <v-btn class="text-none action-submit" variant="flat" height="44" :loading="deleting" @click="deleteUser">Delete</v-btn>
337
+ <v-btn
338
+ class="text-none action-cancel"
339
+ variant="flat"
340
+ height="44"
341
+ @click="deleteDialog = false"
342
+ >Cancel</v-btn
343
+ >
344
+ <v-btn
345
+ class="text-none action-submit"
346
+ variant="flat"
347
+ height="44"
348
+ :loading="deleting"
349
+ @click="deleteUser"
350
+ >Delete</v-btn
351
+ >
247
352
  </v-card-actions>
248
353
  </v-card>
249
354
  </v-dialog>
@@ -270,6 +375,8 @@ const {
270
375
  getReaders,
271
376
  getIdentities,
272
377
  getUserImage,
378
+ setUserImage,
379
+ deleteUserImage,
273
380
  runObjectOperation,
274
381
  createIdentity,
275
382
  updateIdentity,
@@ -296,6 +403,9 @@ const deleteDialog = ref(false);
296
403
  const selectedUser = ref<Record<string, any> | null>(null);
297
404
  const showPassword = ref(false);
298
405
  const photoInput = ref<HTMLInputElement | null>(null);
406
+ const cameraDialog = ref(false);
407
+ const selectedPhotoFile = ref<File | null>(null);
408
+ const removePhotoRequested = ref(false);
299
409
  const snackbar = reactive({
300
410
  show: false,
301
411
  color: "success",
@@ -321,7 +431,7 @@ const readerOptions = computed(() =>
321
431
  readers.value.map((reader) => ({
322
432
  title: reader.name || reader.deviceId || reader._id,
323
433
  value: reader._id,
324
- })),
434
+ }))
325
435
  );
326
436
 
327
437
  const pageRange = computed(() => {
@@ -335,7 +445,7 @@ const pageRange = computed(() => {
335
445
  watch(
336
446
  () => props.site,
337
447
  () => init(),
338
- { immediate: true },
448
+ { immediate: true }
339
449
  );
340
450
 
341
451
  async function init() {
@@ -346,7 +456,8 @@ async function init() {
346
456
  async function loadReaders() {
347
457
  if (!props.site) return;
348
458
  const response = await getReaders({ site: props.site, page: 1, limit: 100 });
349
- readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
459
+ readers.value =
460
+ response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
350
461
  if (!selectedReaderId.value && readers.value.length) {
351
462
  selectedReaderId.value = readers.value[0]._id;
352
463
  }
@@ -375,12 +486,19 @@ async function loadUsers() {
375
486
  limit: 500,
376
487
  offset: 0,
377
488
  }),
378
- isAdministratorIdentity() ? loadAdministratorRoles(selectedReaderId.value) : Promise.resolve(null),
489
+ isAdministratorIdentity()
490
+ ? loadAdministratorRoles(selectedReaderId.value)
491
+ : Promise.resolve(null),
379
492
  ]);
380
493
 
381
- const identities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
494
+ const identities =
495
+ identityResponse?.items ??
496
+ identityResponse?.data?.items ??
497
+ identityResponse?.data?.identities ??
498
+ [];
382
499
  const adminRoleIds = getAdministratorRoleUserIds(roleResponse);
383
500
  const readerUsers = normalizeHidUsers(readerResponse).filter((user) => {
501
+ if (Number(user.userTypeId) === 1) return false;
384
502
  if (!isAdministratorIdentity()) return true;
385
503
  const hidUserId = toHidNumericId(user.hidUserId);
386
504
  return Boolean(hidUserId && adminRoleIds.has(hidUserId));
@@ -409,21 +527,23 @@ async function loadUsers() {
409
527
  async function loadUserImages(items: Record<string, any>[]) {
410
528
  if (!selectedReaderId.value) return;
411
529
 
412
- await Promise.all(items.map(async (user) => {
413
- const hidUserId = toHidNumericId(user.hidUserId);
414
- const key = String(hidUserId || "");
415
- if (!hidUserId || userImages.value[key] || user?.metadata?.photo) return;
416
-
417
- try {
418
- const response = await getUserImage(selectedReaderId.value, hidUserId);
419
- const image = response?.data || response;
420
- userImages.value[key] = image?.base64
421
- ? `data:${image.contentType || "image/jpeg"};base64,${image.base64}`
422
- : "";
423
- } catch {
424
- userImages.value[key] = "";
425
- }
426
- }));
530
+ await Promise.all(
531
+ items.map(async (user) => {
532
+ const hidUserId = toHidNumericId(user.hidUserId);
533
+ const key = String(hidUserId || "");
534
+ if (!hidUserId || userImages.value[key] || user?.metadata?.photo) return;
535
+
536
+ try {
537
+ const response = await getUserImage(selectedReaderId.value, hidUserId);
538
+ const image = response?.data || response;
539
+ userImages.value[key] = image?.base64
540
+ ? `data:${image.contentType || "image/jpeg"};base64,${image.base64}`
541
+ : "";
542
+ } catch {
543
+ userImages.value[key] = "";
544
+ }
545
+ })
546
+ );
427
547
  }
428
548
 
429
549
  function resetForm() {
@@ -437,6 +557,8 @@ function resetForm() {
437
557
  form.cardNo = "";
438
558
  form.pinPassword = "";
439
559
  form.photoPreview = "";
560
+ selectedPhotoFile.value = null;
561
+ removePhotoRequested.value = false;
440
562
  }
441
563
 
442
564
  async function openEnroll() {
@@ -463,6 +585,8 @@ async function openEdit(user: Record<string, any>) {
463
585
  form.cardNo = user.cardNo ?? "";
464
586
  form.pinPassword = user.metadata?.pinPassword ?? "";
465
587
  form.photoPreview = getUserImageSrc(user);
588
+ selectedPhotoFile.value = null;
589
+ removePhotoRequested.value = false;
466
590
  formDialog.value = true;
467
591
 
468
592
  if (!form.photoPreview) {
@@ -487,9 +611,53 @@ function triggerPhotoInput() {
487
611
  photoInput.value?.click();
488
612
  }
489
613
 
614
+ function triggerCameraInput() {
615
+ cameraDialog.value = true;
616
+ }
617
+
618
+ function onCameraCapture(dataUrl: string) {
619
+ const [header, encodedData] = dataUrl.split(",", 2);
620
+ if (!header || !encodedData) {
621
+ showToast("Unable to capture the facial image.", "error");
622
+ return;
623
+ }
624
+
625
+ const mimeType = header.match(/^data:([^;]+);base64$/)?.[1] || "image/jpeg";
626
+ const decodedData = atob(encodedData);
627
+ const bytes = new Uint8Array(decodedData.length);
628
+ for (let index = 0; index < decodedData.length; index += 1) {
629
+ bytes[index] = decodedData.charCodeAt(index);
630
+ }
631
+
632
+ selectedPhotoFile.value = new File([bytes], `hid-facial-${Date.now()}.jpg`, {
633
+ type: mimeType,
634
+ });
635
+ form.photoPreview = dataUrl;
636
+ removePhotoRequested.value = false;
637
+ }
638
+
639
+ function removePhoto() {
640
+ form.photoPreview = "";
641
+ selectedPhotoFile.value = null;
642
+ removePhotoRequested.value = true;
643
+ }
644
+
490
645
  function onPhotoChange(event: Event) {
491
- const file = (event.target as HTMLInputElement).files?.[0];
646
+ const input = event.target as HTMLInputElement;
647
+ const file = input.files?.[0];
648
+ input.value = "";
492
649
  if (!file) return;
650
+ if (!["image/jpeg", "image/png"].includes(file.type)) {
651
+ showToast("Facial enrollment only accepts JPG or PNG images.", "error");
652
+ return;
653
+ }
654
+ if (file.size > 2 * 1024 * 1024) {
655
+ showToast("Facial enrollment images must be smaller than 2 MB.", "error");
656
+ return;
657
+ }
658
+
659
+ selectedPhotoFile.value = file;
660
+ removePhotoRequested.value = false;
493
661
  const reader = new FileReader();
494
662
  reader.onload = () => {
495
663
  form.photoPreview = String(reader.result || "");
@@ -498,7 +666,8 @@ function onPhotoChange(event: Event) {
498
666
  }
499
667
 
500
668
  async function saveUser() {
501
- const readerId = selectedReaderId.value || form.reader || readers.value[0]?._id;
669
+ const readerId =
670
+ selectedReaderId.value || form.reader || readers.value[0]?._id;
502
671
  if (!readerId || !form.name || !form.hidUserId || !form.registration) {
503
672
  showToast("Please complete the required HID user fields.", "error");
504
673
  return;
@@ -520,7 +689,13 @@ async function saveUser() {
520
689
  hidUserId: String(hidUser.id),
521
690
  registration: form.registration,
522
691
  cardNo: form.cardNo,
523
- type: props.identityType as "resident" | "staff" | "contractor" | "visitor" | "admin" | "unknown",
692
+ type: props.identityType as
693
+ | "resident"
694
+ | "staff"
695
+ | "contractor"
696
+ | "visitor"
697
+ | "admin"
698
+ | "unknown",
524
699
  status: "active" as const,
525
700
  metadata: {
526
701
  name: form.name,
@@ -530,6 +705,8 @@ async function saveUser() {
530
705
  unitLabel: buildUnitLabel(),
531
706
  facialData: form.photoPreview ? form.hidUserId : "",
532
707
  photo: form.photoPreview,
708
+ imageTimestamp: selectedUser.value?.metadata?.imageTimestamp || "",
709
+ facialScores: selectedUser.value?.metadata?.facialScores || {},
533
710
  pinPassword: form.pinPassword,
534
711
  },
535
712
  };
@@ -537,6 +714,8 @@ async function saveUser() {
537
714
  if (selectedUser.value) {
538
715
  await updateHidUserOnReader(readerId, hidUser);
539
716
  await syncHidUserRole(readerId, hidUser.id, isAdministratorIdentity());
717
+ const facialResult = await syncFacialImage(readerId, hidUser.id);
718
+ applyFacialResult(payload.metadata, facialResult);
540
719
  if (selectedUser.value._id) {
541
720
  await updateIdentity(selectedUser.value._id, payload);
542
721
  } else {
@@ -545,13 +724,19 @@ async function saveUser() {
545
724
  } else {
546
725
  await createHidUserOnReader(readerId, hidUser);
547
726
  await syncHidUserRole(readerId, hidUser.id, isAdministratorIdentity());
727
+ const facialResult = await syncFacialImage(readerId, hidUser.id);
728
+ applyFacialResult(payload.metadata, facialResult);
548
729
  await saveIdentityOnReader(readerId, payload);
549
730
  selectedReaderId.value = readerId;
550
731
  }
551
732
 
552
733
  formDialog.value = false;
553
734
  await loadUsers();
554
- showToast(selectedUser.value?._id ? "HID user updated on reader." : "HID user enrolled on reader.");
735
+ showToast(
736
+ selectedUser.value?._id
737
+ ? "HID user updated on reader."
738
+ : "HID user enrolled on reader."
739
+ );
555
740
  } catch (error: any) {
556
741
  console.error("Unable to save HID user:", error);
557
742
  showToast(getHidErrorMessage(error), "error");
@@ -560,12 +745,67 @@ async function saveUser() {
560
745
  }
561
746
  }
562
747
 
748
+ type FacialSyncResult = {
749
+ action: "none" | "enrolled" | "removed";
750
+ timestamp?: number;
751
+ scores?: Record<string, unknown>;
752
+ };
753
+
754
+ async function syncFacialImage(
755
+ readerId: string,
756
+ hidUserId: number
757
+ ): Promise<FacialSyncResult> {
758
+ if (selectedPhotoFile.value) {
759
+ const timestamp = Math.floor(Date.now() / 1000);
760
+ const response = await setUserImage(
761
+ readerId,
762
+ hidUserId,
763
+ selectedPhotoFile.value,
764
+ {
765
+ timestamp,
766
+ match: true,
767
+ }
768
+ );
769
+ const result = response?.data;
770
+ userImages.value[String(hidUserId)] = form.photoPreview;
771
+ return { action: "enrolled", timestamp, scores: result?.scores };
772
+ }
773
+
774
+ if (removePhotoRequested.value) {
775
+ await deleteUserImage(readerId, hidUserId);
776
+ delete userImages.value[String(hidUserId)];
777
+ return { action: "removed" };
778
+ }
779
+
780
+ return { action: "none" };
781
+ }
782
+
783
+ function applyFacialResult(
784
+ metadata: Record<string, unknown>,
785
+ result: FacialSyncResult
786
+ ) {
787
+ if (result.action === "enrolled") {
788
+ metadata.facialData = form.hidUserId;
789
+ metadata.imageTimestamp = String(result.timestamp || "");
790
+ metadata.facialScores = result.scores || {};
791
+ } else if (result.action === "removed") {
792
+ metadata.facialData = "";
793
+ metadata.photo = "";
794
+ metadata.imageTimestamp = "";
795
+ metadata.facialScores = {};
796
+ }
797
+ }
798
+
563
799
  async function deleteUser() {
564
800
  if (!selectedUser.value) return;
565
801
 
566
802
  deleting.value = true;
567
803
  try {
568
- await syncHidUserRole(selectedReaderId.value, toHidNumericId(selectedUser.value.hidUserId), false);
804
+ await syncHidUserRole(
805
+ selectedReaderId.value,
806
+ toHidNumericId(selectedUser.value.hidUserId),
807
+ false
808
+ );
569
809
  await deleteHidUserFromReader(selectedReaderId.value, selectedUser.value);
570
810
  if (selectedUser.value._id) {
571
811
  await deleteIdentity(selectedUser.value._id);
@@ -581,8 +821,14 @@ async function deleteUser() {
581
821
  }
582
822
  }
583
823
 
584
- async function saveIdentityOnReader(readerId: string, payload: Record<string, any>) {
585
- const existingIdentity = await findExistingIdentityOnReader(readerId, payload);
824
+ async function saveIdentityOnReader(
825
+ readerId: string,
826
+ payload: Record<string, any>
827
+ ) {
828
+ const existingIdentity = await findExistingIdentityOnReader(
829
+ readerId,
830
+ payload
831
+ );
586
832
  if (existingIdentity?._id) {
587
833
  await updateIdentity(existingIdentity._id, payload);
588
834
  return;
@@ -594,31 +840,47 @@ async function saveIdentityOnReader(readerId: string, payload: Record<string, an
594
840
  const message = getHidErrorMessage(error);
595
841
  if (!message.toLowerCase().includes("already exists")) throw error;
596
842
 
597
- const duplicatedIdentity = await findExistingIdentityOnReader(readerId, payload);
843
+ const duplicatedIdentity = await findExistingIdentityOnReader(
844
+ readerId,
845
+ payload
846
+ );
598
847
  if (!duplicatedIdentity?._id) throw error;
599
848
  await updateIdentity(duplicatedIdentity._id, payload);
600
849
  }
601
850
  }
602
851
 
603
- async function findExistingIdentityOnReader(readerId: string, payload: Record<string, any>) {
852
+ async function findExistingIdentityOnReader(
853
+ readerId: string,
854
+ payload: Record<string, any>
855
+ ) {
604
856
  const response = await getIdentities(readerId, {
605
857
  page: 1,
606
858
  limit: 500,
607
859
  search: payload.hidUserId || payload.registration || payload.cardNo || "",
608
860
  });
609
- const identities = response?.items ?? response?.data?.items ?? response?.data?.identities ?? [];
861
+ const identities =
862
+ response?.items ??
863
+ response?.data?.items ??
864
+ response?.data?.identities ??
865
+ [];
610
866
  const hidUserId = String(payload.hidUserId || "");
611
867
  const registration = String(payload.registration || "");
612
868
  const cardNo = String(payload.cardNo || "");
613
869
 
614
- return identities.find((identity: Record<string, any>) =>
615
- String(identity.hidUserId || "") === hidUserId ||
616
- Boolean(registration && String(identity.registration || "") === registration) ||
617
- Boolean(cardNo && String(identity.cardNo || "") === cardNo),
870
+ return identities.find(
871
+ (identity: Record<string, any>) =>
872
+ String(identity.hidUserId || "") === hidUserId ||
873
+ Boolean(
874
+ registration && String(identity.registration || "") === registration
875
+ ) ||
876
+ Boolean(cardNo && String(identity.cardNo || "") === cardNo)
618
877
  );
619
878
  }
620
879
 
621
- async function createHidUserOnReader(readerId: string, user: Record<string, any>) {
880
+ async function createHidUserOnReader(
881
+ readerId: string,
882
+ user: Record<string, any>
883
+ ) {
622
884
  const existing = await getHidUserFromReader(readerId, user.id);
623
885
  if (existing) {
624
886
  await updateHidUserOnReader(readerId, user);
@@ -648,7 +910,10 @@ async function getHidUserFromReader(readerId: string, id: unknown) {
648
910
  return normalizeHidUsers(response)[0];
649
911
  }
650
912
 
651
- async function updateHidUserOnReader(readerId: string, user: Record<string, any>) {
913
+ async function updateHidUserOnReader(
914
+ readerId: string,
915
+ user: Record<string, any>
916
+ ) {
652
917
  const { id, ...values } = user;
653
918
  await runObjectOperation(readerId, {
654
919
  operation: "modify",
@@ -662,7 +927,10 @@ async function updateHidUserOnReader(readerId: string, user: Record<string, any>
662
927
  });
663
928
  }
664
929
 
665
- async function deleteHidUserFromReader(readerId: string, user: Record<string, any>) {
930
+ async function deleteHidUserFromReader(
931
+ readerId: string,
932
+ user: Record<string, any>
933
+ ) {
666
934
  const id = toHidNumericId(user.hidUserId);
667
935
  if (!readerId || !id) return;
668
936
 
@@ -677,7 +945,11 @@ async function deleteHidUserFromReader(readerId: string, user: Record<string, an
677
945
  });
678
946
  }
679
947
 
680
- async function syncHidUserRole(readerId: string, hidUserId: unknown, isAdministrator: boolean) {
948
+ async function syncHidUserRole(
949
+ readerId: string,
950
+ hidUserId: unknown,
951
+ isAdministrator: boolean
952
+ ) {
681
953
  const userId = toHidNumericId(hidUserId);
682
954
  if (!readerId || !userId) return;
683
955
 
@@ -771,14 +1043,17 @@ function getAdministratorRoleUserIds(response: Record<string, any> | null) {
771
1043
  return new Set(
772
1044
  (Array.isArray(roles) ? roles : [])
773
1045
  .map((role) => toHidNumericId(role.user_id))
774
- .filter((userId): userId is number => Boolean(userId)),
1046
+ .filter((userId): userId is number => Boolean(userId))
775
1047
  );
776
1048
  }
777
1049
 
778
1050
  function toHidUserObject(identity: Record<string, any>) {
779
1051
  return {
780
1052
  id: toHidNumericId(identity.hidUserId),
781
- name: identity.metadata?.name || identity.name || `User ${identity.hidUserId || ""}`.trim(),
1053
+ name:
1054
+ identity.metadata?.name ||
1055
+ identity.name ||
1056
+ `User ${identity.hidUserId || ""}`.trim(),
782
1057
  registration: identity.registration || "",
783
1058
  };
784
1059
  }
@@ -807,23 +1082,40 @@ function getName(user?: Record<string, any> | null) {
807
1082
  }
808
1083
 
809
1084
  function getUnit(user?: Record<string, any> | null) {
810
- return user?.metadata?.unitLabel || [user?.metadata?.block, user?.metadata?.level, user?.metadata?.unit].filter(Boolean).join("/") || "N/A";
1085
+ return (
1086
+ user?.metadata?.unitLabel ||
1087
+ [user?.metadata?.block, user?.metadata?.level, user?.metadata?.unit]
1088
+ .filter(Boolean)
1089
+ .join("/") ||
1090
+ "N/A"
1091
+ );
811
1092
  }
812
1093
 
813
1094
  function getFacialData(user: Record<string, any>) {
814
- return user?.metadata?.facialData || user?.metadata?.imageTimestamp || (user?.metadata?.photo ? user.hidUserId : "N/A");
1095
+ return (
1096
+ user?.metadata?.facialData ||
1097
+ user?.metadata?.imageTimestamp ||
1098
+ (user?.metadata?.photo ? user.hidUserId : "N/A")
1099
+ );
815
1100
  }
816
1101
 
817
1102
  function getUserImageSrc(user: Record<string, any>) {
818
1103
  const hidUserId = toHidNumericId(user?.hidUserId);
819
- return user?.metadata?.photo || (hidUserId ? userImages.value[String(hidUserId)] : "") || "";
1104
+ return (
1105
+ user?.metadata?.photo ||
1106
+ (hidUserId ? userImages.value[String(hidUserId)] : "") ||
1107
+ ""
1108
+ );
820
1109
  }
821
1110
 
822
1111
  async function getNextUid() {
823
1112
  const readerIds = await getExistingHidReaderIds();
824
1113
  const maxNumber = users.value.reduce((max, user) => {
825
1114
  const match = String(user.hidUserId ?? "").match(/^UID(\d+)$/i);
826
- return Math.max(max, match ? Number(match[1]) : toHidNumericId(user.hidUserId) || 0);
1115
+ return Math.max(
1116
+ max,
1117
+ match ? Number(match[1]) : toHidNumericId(user.hidUserId) || 0
1118
+ );
827
1119
  }, 0);
828
1120
  const nextNumber = Math.max(maxNumber, ...readerIds, 0) + 1;
829
1121
  return `UID${String(nextNumber).padStart(6, "0")}`;
@@ -837,10 +1129,15 @@ async function getNextRegistration() {
837
1129
  const numericRegistrations = registrations
838
1130
  .map((registration) => {
839
1131
  const match = registration.match(/^(\d+)$/);
840
- return match ? { value: Number(match[1]), width: match[1].length } : undefined;
1132
+ return match
1133
+ ? { value: Number(match[1]), width: match[1].length }
1134
+ : undefined;
841
1135
  })
842
1136
  .filter((item): item is { value: number; width: number } => Boolean(item));
843
- const maxRegistration = numericRegistrations.reduce((max, item) => Math.max(max, item.value), 0);
1137
+ const maxRegistration = numericRegistrations.reduce(
1138
+ (max, item) => Math.max(max, item.value),
1139
+ 0
1140
+ );
844
1141
  const width = Math.max(6, ...numericRegistrations.map((item) => item.width));
845
1142
 
846
1143
  return String(maxRegistration + 1).padStart(width, "0");
@@ -882,11 +1179,14 @@ function normalizeHidUsers(response: Record<string, any>) {
882
1179
  hidUserId: String(user.id || ""),
883
1180
  rawHidUserId: user.id,
884
1181
  registration: user.registration,
1182
+ userTypeId: user.user_type_id,
885
1183
  cardNo: user.card_value || user.cardNo || "",
886
1184
  name: user.name,
887
1185
  metadata: {
888
1186
  name: user.name,
889
- imageTimestamp: user.image_timestamp ? String(user.image_timestamp) : "",
1187
+ imageTimestamp: user.image_timestamp
1188
+ ? String(user.image_timestamp)
1189
+ : "",
890
1190
  lastAccessAt: fromUnixSeconds(user.last_access),
891
1191
  hidReaderSource: true,
892
1192
  },
@@ -894,7 +1194,10 @@ function normalizeHidUsers(response: Record<string, any>) {
894
1194
  : [];
895
1195
  }
896
1196
 
897
- function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], identities: Record<string, any>[]) {
1197
+ function mergeReaderUsersWithIdentities(
1198
+ readerUsers: Record<string, any>[],
1199
+ identities: Record<string, any>[]
1200
+ ) {
898
1201
  const identityById = new Map<string, Record<string, any>>();
899
1202
  identities.forEach((identity) => {
900
1203
  const id = toHidNumericId(identity.hidUserId);
@@ -902,7 +1205,9 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
902
1205
  });
903
1206
 
904
1207
  const merged = readerUsers.map((readerUser) => {
905
- const identity = identityById.get(String(toHidNumericId(readerUser.hidUserId)));
1208
+ const identity = identityById.get(
1209
+ String(toHidNumericId(readerUser.hidUserId))
1210
+ );
906
1211
  if (!identity) return readerUser;
907
1212
 
908
1213
  return {
@@ -917,8 +1222,11 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
917
1222
  metadata: {
918
1223
  ...readerUser.metadata,
919
1224
  ...identity.metadata,
920
- imageTimestamp: identity.metadata?.imageTimestamp || readerUser.metadata?.imageTimestamp,
921
- lastAccessAt: identity.metadata?.lastAccessAt || readerUser.metadata?.lastAccessAt,
1225
+ imageTimestamp:
1226
+ identity.metadata?.imageTimestamp ||
1227
+ readerUser.metadata?.imageTimestamp,
1228
+ lastAccessAt:
1229
+ identity.metadata?.lastAccessAt || readerUser.metadata?.lastAccessAt,
922
1230
  },
923
1231
  };
924
1232
  });
@@ -929,13 +1237,16 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
929
1237
  function filterUsers(items: Record<string, any>[]) {
930
1238
  const searchText = search.value.trim().toLowerCase();
931
1239
  return items.filter((item) => {
932
- const matchesSearch = !searchText || [
933
- getName(item),
934
- item.hidUserId,
935
- item.registration,
936
- item.cardNo,
937
- ].some((value) => String(value || "").toLowerCase().includes(searchText));
938
- const matchesStatus = status.value === "Status" || getMappingStatus(item) === status.value;
1240
+ const matchesSearch =
1241
+ !searchText ||
1242
+ [getName(item), item.hidUserId, item.registration, item.cardNo].some(
1243
+ (value) =>
1244
+ String(value || "")
1245
+ .toLowerCase()
1246
+ .includes(searchText)
1247
+ );
1248
+ const matchesStatus =
1249
+ status.value === "Status" || getMappingStatus(item) === status.value;
939
1250
  return matchesSearch && matchesStatus;
940
1251
  });
941
1252
  }
@@ -995,7 +1306,7 @@ function getHidErrorMessage(error: any) {
995
1306
  data?.statusMessage ||
996
1307
  error?.statusMessage ||
997
1308
  error?.message ||
998
- "Unable to save HID user.",
1309
+ "Unable to save HID user."
999
1310
  );
1000
1311
  }
1001
1312
  </script>