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