@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.
- package/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/components/CameraWall.vue +625 -0
- package/components/CameraWallTile.vue +333 -0
- package/components/EntryPassInformation.vue +4 -4
- package/components/HidAccessLogDashboard.vue +53 -8
- package/components/HidIntercomManagement.vue +842 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +96 -2
- package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +6 -2
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +0 -1
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useSipWebPhone.ts +332 -0
- package/composables/useSiteSettings.ts +28 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +2 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +149 -0
- package/utils/camera-wall.ts +299 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- 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
|
-
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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();
|