@7365admin1/layer-common 3.2.1 → 3.2.2-staging.77
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/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/DashboardMain.vue +84 -88
- package/components/DashboardPanel.vue +1 -1
- package/components/EntryPassInformation.vue +4 -4
- package/components/FeedbackMain.vue +7 -7
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +808 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +392 -81
- package/components/Layout/NavigationDrawer.vue +43 -2
- package/components/NavigationItem.vue +9 -0
- 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/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +258 -14
- package/components/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +2 -3
- package/components/VisitorSocketPopUp.vue +56 -2
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +311 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +3 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +37 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/theme.test.ts +216 -0
- package/utils/theme.ts +163 -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
|
@@ -50,7 +50,13 @@
|
|
|
50
50
|
|
|
51
51
|
<v-card flat border class="table-card">
|
|
52
52
|
<div class="table-refresh">
|
|
53
|
-
<v-btn
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
113
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
207
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<span>
|
|
225
|
-
|
|
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"
|
|
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
|
|
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
|
|
246
|
-
|
|
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 =
|
|
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()
|
|
489
|
+
isAdministratorIdentity()
|
|
490
|
+
? loadAdministratorRoles(selectedReaderId.value)
|
|
491
|
+
: Promise.resolve(null),
|
|
379
492
|
]);
|
|
380
493
|
|
|
381
|
-
const 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(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
585
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
921
|
-
|
|
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 =
|
|
933
|
-
|
|
934
|
-
item.hidUserId,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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>
|