@7365admin1/layer-common 3.2.0 → 3.2.1-staging.59

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 (33) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/components/CameraWall.vue +625 -0
  3. package/components/CameraWallTile.vue +333 -0
  4. package/components/EntryPassInformation.vue +4 -4
  5. package/components/HidAccessLogDashboard.vue +53 -8
  6. package/components/HidIntercomManagement.vue +842 -201
  7. package/components/HidQrCodeConfiguration.vue +907 -95
  8. package/components/HidServiceSettingsPanel.vue +20 -1
  9. package/components/HidUserEnrollment.vue +96 -2
  10. package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
  11. package/components/OnlineFormConfigurationForm.vue +620 -224
  12. package/components/OnlineFormFill.vue +36 -8
  13. package/components/OnlineFormsConfiguration.vue +6 -2
  14. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  15. package/components/QrTemplate/PrintDialog.vue +209 -196
  16. package/components/SiteSettings.vue +4 -2
  17. package/components/VisitorForm.vue +201 -78
  18. package/components/VisitorManagement.vue +0 -1
  19. package/composables/useHidAmico.ts +127 -0
  20. package/composables/useHidNavigation.ts +0 -7
  21. package/composables/useOnlineFormPages.ts +2 -0
  22. package/composables/useSipWebPhone.ts +332 -0
  23. package/composables/useSiteSettings.ts +28 -0
  24. package/composables/useWebUsb.ts +127 -37
  25. package/package.json +2 -1
  26. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  27. package/plugins/secure-member.client.ts +21 -56
  28. package/types/site.d.ts +71 -0
  29. package/utils/camera-wall.test.ts +149 -0
  30. package/utils/camera-wall.ts +299 -0
  31. package/components/HidIdentityMapping.vue +0 -975
  32. package/middleware/member.ts +0 -4
  33. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -12,7 +12,8 @@
12
12
  <v-col cols="12" md="4" class="d-flex justify-md-end">
13
13
  <v-switch
14
14
  v-model="hidEnabled"
15
- color="primary"
15
+ class="hid-service-switch"
16
+ color="success"
16
17
  hide-details
17
18
  inset
18
19
  :loading="isSaving"
@@ -148,3 +149,21 @@ function goToHidReaders() {
148
149
  });
149
150
  }
150
151
  </script>
152
+
153
+ <style scoped>
154
+ .hid-service-switch :deep(.v-switch__track) {
155
+ background-color: #8b9297;
156
+ opacity: 1;
157
+ }
158
+
159
+ .hid-service-switch :deep(.v-selection-control--dirty .v-switch__track) {
160
+ background-color: #2eaf55 !important;
161
+ color: #2eaf55 !important;
162
+ opacity: 1;
163
+ }
164
+
165
+ .hid-service-switch :deep(.v-switch__thumb) {
166
+ background-color: #fff !important;
167
+ color: #fff !important;
168
+ }
169
+ </style>
@@ -137,10 +137,24 @@
137
137
  </template>
138
138
  <v-list density="compact" min-width="190">
139
139
  <v-list-item title="Browse" @click="triggerPhotoInput" />
140
- <v-list-item title="Take Photo using Camera" @click="triggerPhotoInput" />
140
+ <v-list-item title="Take Photo using Camera" @click="triggerCameraInput" />
141
+ <v-list-item
142
+ v-if="form.photoPreview"
143
+ title="Remove Photo"
144
+ class="text-error"
145
+ @click="removePhoto"
146
+ />
141
147
  </v-list>
142
148
  </v-menu>
143
149
  <input ref="photoInput" type="file" accept="image/*" class="hidden-input" @change="onPhotoChange" />
150
+ <input
151
+ ref="cameraInput"
152
+ type="file"
153
+ accept="image/*"
154
+ capture="user"
155
+ class="hidden-input"
156
+ @change="onPhotoChange"
157
+ />
144
158
  </div>
145
159
 
146
160
  <div class="field-label">Name <span>*</span></div>
@@ -270,6 +284,8 @@ const {
270
284
  getReaders,
271
285
  getIdentities,
272
286
  getUserImage,
287
+ setUserImage,
288
+ deleteUserImage,
273
289
  runObjectOperation,
274
290
  createIdentity,
275
291
  updateIdentity,
@@ -296,6 +312,9 @@ const deleteDialog = ref(false);
296
312
  const selectedUser = ref<Record<string, any> | null>(null);
297
313
  const showPassword = ref(false);
298
314
  const photoInput = ref<HTMLInputElement | null>(null);
315
+ const cameraInput = ref<HTMLInputElement | null>(null);
316
+ const selectedPhotoFile = ref<File | null>(null);
317
+ const removePhotoRequested = ref(false);
299
318
  const snackbar = reactive({
300
319
  show: false,
301
320
  color: "success",
@@ -381,6 +400,7 @@ async function loadUsers() {
381
400
  const identities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
382
401
  const adminRoleIds = getAdministratorRoleUserIds(roleResponse);
383
402
  const readerUsers = normalizeHidUsers(readerResponse).filter((user) => {
403
+ if (Number(user.userTypeId) === 1) return false;
384
404
  if (!isAdministratorIdentity()) return true;
385
405
  const hidUserId = toHidNumericId(user.hidUserId);
386
406
  return Boolean(hidUserId && adminRoleIds.has(hidUserId));
@@ -437,6 +457,8 @@ function resetForm() {
437
457
  form.cardNo = "";
438
458
  form.pinPassword = "";
439
459
  form.photoPreview = "";
460
+ selectedPhotoFile.value = null;
461
+ removePhotoRequested.value = false;
440
462
  }
441
463
 
442
464
  async function openEnroll() {
@@ -463,6 +485,8 @@ async function openEdit(user: Record<string, any>) {
463
485
  form.cardNo = user.cardNo ?? "";
464
486
  form.pinPassword = user.metadata?.pinPassword ?? "";
465
487
  form.photoPreview = getUserImageSrc(user);
488
+ selectedPhotoFile.value = null;
489
+ removePhotoRequested.value = false;
466
490
  formDialog.value = true;
467
491
 
468
492
  if (!form.photoPreview) {
@@ -487,9 +511,32 @@ function triggerPhotoInput() {
487
511
  photoInput.value?.click();
488
512
  }
489
513
 
514
+ function triggerCameraInput() {
515
+ cameraInput.value?.click();
516
+ }
517
+
518
+ function removePhoto() {
519
+ form.photoPreview = "";
520
+ selectedPhotoFile.value = null;
521
+ removePhotoRequested.value = true;
522
+ }
523
+
490
524
  function onPhotoChange(event: Event) {
491
- const file = (event.target as HTMLInputElement).files?.[0];
525
+ const input = event.target as HTMLInputElement;
526
+ const file = input.files?.[0];
527
+ input.value = "";
492
528
  if (!file) return;
529
+ if (!["image/jpeg", "image/png"].includes(file.type)) {
530
+ showToast("Facial enrollment only accepts JPG or PNG images.", "error");
531
+ return;
532
+ }
533
+ if (file.size > 2 * 1024 * 1024) {
534
+ showToast("Facial enrollment images must be smaller than 2 MB.", "error");
535
+ return;
536
+ }
537
+
538
+ selectedPhotoFile.value = file;
539
+ removePhotoRequested.value = false;
493
540
  const reader = new FileReader();
494
541
  reader.onload = () => {
495
542
  form.photoPreview = String(reader.result || "");
@@ -530,6 +577,8 @@ async function saveUser() {
530
577
  unitLabel: buildUnitLabel(),
531
578
  facialData: form.photoPreview ? form.hidUserId : "",
532
579
  photo: form.photoPreview,
580
+ imageTimestamp: selectedUser.value?.metadata?.imageTimestamp || "",
581
+ facialScores: selectedUser.value?.metadata?.facialScores || {},
533
582
  pinPassword: form.pinPassword,
534
583
  },
535
584
  };
@@ -537,6 +586,8 @@ async function saveUser() {
537
586
  if (selectedUser.value) {
538
587
  await updateHidUserOnReader(readerId, hidUser);
539
588
  await syncHidUserRole(readerId, hidUser.id, isAdministratorIdentity());
589
+ const facialResult = await syncFacialImage(readerId, hidUser.id);
590
+ applyFacialResult(payload.metadata, facialResult);
540
591
  if (selectedUser.value._id) {
541
592
  await updateIdentity(selectedUser.value._id, payload);
542
593
  } else {
@@ -545,6 +596,8 @@ async function saveUser() {
545
596
  } else {
546
597
  await createHidUserOnReader(readerId, hidUser);
547
598
  await syncHidUserRole(readerId, hidUser.id, isAdministratorIdentity());
599
+ const facialResult = await syncFacialImage(readerId, hidUser.id);
600
+ applyFacialResult(payload.metadata, facialResult);
548
601
  await saveIdentityOnReader(readerId, payload);
549
602
  selectedReaderId.value = readerId;
550
603
  }
@@ -560,6 +613,46 @@ async function saveUser() {
560
613
  }
561
614
  }
562
615
 
616
+ type FacialSyncResult = {
617
+ action: "none" | "enrolled" | "removed";
618
+ timestamp?: number;
619
+ scores?: Record<string, unknown>;
620
+ };
621
+
622
+ async function syncFacialImage(readerId: string, hidUserId: number): Promise<FacialSyncResult> {
623
+ if (selectedPhotoFile.value) {
624
+ const timestamp = Math.floor(Date.now() / 1000);
625
+ const response = await setUserImage(readerId, hidUserId, selectedPhotoFile.value, {
626
+ timestamp,
627
+ match: true,
628
+ });
629
+ const result = response?.data;
630
+ userImages.value[String(hidUserId)] = form.photoPreview;
631
+ return { action: "enrolled", timestamp, scores: result?.scores };
632
+ }
633
+
634
+ if (removePhotoRequested.value) {
635
+ await deleteUserImage(readerId, hidUserId);
636
+ delete userImages.value[String(hidUserId)];
637
+ return { action: "removed" };
638
+ }
639
+
640
+ return { action: "none" };
641
+ }
642
+
643
+ function applyFacialResult(metadata: Record<string, unknown>, result: FacialSyncResult) {
644
+ if (result.action === "enrolled") {
645
+ metadata.facialData = form.hidUserId;
646
+ metadata.imageTimestamp = String(result.timestamp || "");
647
+ metadata.facialScores = result.scores || {};
648
+ } else if (result.action === "removed") {
649
+ metadata.facialData = "";
650
+ metadata.photo = "";
651
+ metadata.imageTimestamp = "";
652
+ metadata.facialScores = {};
653
+ }
654
+ }
655
+
563
656
  async function deleteUser() {
564
657
  if (!selectedUser.value) return;
565
658
 
@@ -882,6 +975,7 @@ function normalizeHidUsers(response: Record<string, any>) {
882
975
  hidUserId: String(user.id || ""),
883
976
  rawHidUserId: user.id,
884
977
  registration: user.registration,
978
+ userTypeId: user.user_type_id,
885
979
  cardNo: user.card_value || user.cardNo || "",
886
980
  name: user.name,
887
981
  metadata: {
@@ -4,6 +4,7 @@
4
4
  <v-row no-gutters class="mb-4" align="center">
5
5
  <v-col cols="12" md="6">
6
6
  <v-btn
7
+ v-if="props.canCreateNfcPatrol"
7
8
  rounded="pill"
8
9
  variant="outlined"
9
10
 
@@ -82,7 +83,12 @@
82
83
  </v-col>
83
84
 
84
85
  <!-- Add Dialog -->
85
- <v-dialog v-model="dialogAdd" persistent max-width="900">
86
+ <v-dialog
87
+ v-if="props.canCreateNfcPatrol"
88
+ v-model="dialogAdd"
89
+ persistent
90
+ max-width="900"
91
+ >
86
92
  <NFCPatrolRouteForm
87
93
  title="Add Route"
88
94
  :site="props.site"
@@ -94,7 +100,12 @@
94
100
  </v-dialog>
95
101
 
96
102
  <!-- Edit Dialog -->
97
- <v-dialog v-model="dialogEdit" persistent max-width="900">
103
+ <v-dialog
104
+ v-if="props.canUpdateNfcPatrol"
105
+ v-model="dialogEdit"
106
+ persistent
107
+ max-width="900"
108
+ >
98
109
  <NFCPatrolRouteForm
99
110
  title="Edit Route"
100
111
  :site="props.site"
@@ -190,7 +201,10 @@
190
201
 
191
202
  <v-toolbar class="pa-0" density="compact">
192
203
  <v-row no-gutters>
193
- <v-col cols="6" class="pa-0">
204
+ <v-col
205
+ :cols="props.canUpdateNfcPatrol || props.canDeleteNfcPatrol ? 6 : 12"
206
+ class="pa-0"
207
+ >
194
208
  <v-btn
195
209
  block
196
210
  variant="text"
@@ -203,7 +217,11 @@
203
217
  </v-btn>
204
218
  </v-col>
205
219
 
206
- <v-col cols="6" class="pa-0">
220
+ <v-col
221
+ v-if="props.canUpdateNfcPatrol || props.canDeleteNfcPatrol"
222
+ cols="6"
223
+ class="pa-0"
224
+ >
207
225
  <v-menu>
208
226
  <template #activator="{ props }">
209
227
  <v-btn
@@ -220,13 +238,17 @@
220
238
  </template>
221
239
 
222
240
  <v-list class="pa-0">
223
- <v-list-item @click="openDialogEdit()">
241
+ <v-list-item v-if="props.canUpdateNfcPatrol" @click="openDialogEdit()">
224
242
  <v-list-item-title class="text-subtitle-2">
225
243
  Edit Route
226
244
  </v-list-item-title>
227
245
  </v-list-item>
228
246
 
229
- <v-list-item @click="openDialogDelete()" class="text-red">
247
+ <v-list-item
248
+ v-if="props.canDeleteNfcPatrol"
249
+ @click="openDialogDelete()"
250
+ class="text-red"
251
+ >
230
252
  <v-list-item-title class="text-subtitle-2">
231
253
  Delete Route
232
254
  </v-list-item-title>
@@ -240,7 +262,12 @@
240
262
  </v-dialog>
241
263
 
242
264
  <!-- Delete Dialog -->
243
- <v-dialog v-model="dialogDelete" persistent width="540">
265
+ <v-dialog
266
+ v-if="props.canDeleteNfcPatrol"
267
+ v-model="dialogDelete"
268
+ persistent
269
+ width="540"
270
+ >
244
271
  <v-card width="100%">
245
272
  <v-card-text
246
273
  style="max-height: 100vh; overflow-y: auto"
@@ -304,6 +331,18 @@ const props = defineProps({
304
331
  type: String,
305
332
  required: true,
306
333
  },
334
+ canCreateNfcPatrol: {
335
+ type: Boolean,
336
+ default: false,
337
+ },
338
+ canUpdateNfcPatrol: {
339
+ type: Boolean,
340
+ default: false,
341
+ },
342
+ canDeleteNfcPatrol: {
343
+ type: Boolean,
344
+ default: false,
345
+ },
307
346
  headers: {
308
347
  type: Array as PropType<Array<any>>,
309
348
  default: () => [
@@ -406,6 +445,8 @@ const messageSnackbar = ref(false);
406
445
  const messageColor = ref("");
407
446
 
408
447
  async function openDialogEdit() {
448
+ if (!props.canUpdateNfcPatrol) return;
449
+
409
450
  try {
410
451
  // Fetch the full route details from the API
411
452
  const response = await getRouteById(selectedRoute.value._id);
@@ -431,6 +472,8 @@ async function openDialogEdit() {
431
472
  }
432
473
 
433
474
  function openDialogDelete() {
475
+ if (!props.canDeleteNfcPatrol) return;
476
+
434
477
  dialogDelete.value = true;
435
478
 
436
479
  if (dialogPreview.value) dialogPreview.value = false;
@@ -502,6 +545,8 @@ function handleRowClick(_: any, data: any) {
502
545
  }
503
546
 
504
547
  async function submitDelete() {
548
+ if (!props.canDeleteNfcPatrol) return;
549
+
505
550
  try {
506
551
  await deleteRouteById(selectedRoute.value._id ?? "");
507
552
  await getRoutesRefresh();