@7365admin1/layer-common 3.2.1-staging.62 → 3.2.1-staging.64
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.
|
@@ -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,7 +183,10 @@
|
|
|
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
|
+
/>
|
|
141
190
|
<v-list-item
|
|
142
191
|
v-if="form.photoPreview"
|
|
143
192
|
title="Remove Photo"
|
|
@@ -146,12 +195,10 @@
|
|
|
146
195
|
/>
|
|
147
196
|
</v-list>
|
|
148
197
|
</v-menu>
|
|
149
|
-
<input ref="photoInput" type="file" accept="image/*" class="hidden-input" @change="onPhotoChange" />
|
|
150
198
|
<input
|
|
151
|
-
ref="
|
|
199
|
+
ref="photoInput"
|
|
152
200
|
type="file"
|
|
153
201
|
accept="image/*"
|
|
154
|
-
capture="user"
|
|
155
202
|
class="hidden-input"
|
|
156
203
|
@change="onPhotoChange"
|
|
157
204
|
/>
|
|
@@ -217,31 +264,61 @@
|
|
|
217
264
|
</v-card-text>
|
|
218
265
|
|
|
219
266
|
<v-card-actions class="pa-0">
|
|
220
|
-
<v-btn
|
|
221
|
-
|
|
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
|
+
>
|
|
222
281
|
Save
|
|
223
282
|
</v-btn>
|
|
224
283
|
</v-card-actions>
|
|
225
284
|
</v-card>
|
|
226
285
|
</v-dialog>
|
|
227
286
|
|
|
287
|
+
<HidCameraCaptureDialog v-model="cameraDialog" @capture="onCameraCapture" />
|
|
288
|
+
|
|
228
289
|
<v-dialog v-model="viewDialog" max-width="430">
|
|
229
290
|
<v-card rounded="lg">
|
|
230
291
|
<v-card-title class="small-title">User Information</v-card-title>
|
|
231
292
|
<v-card-text>
|
|
232
293
|
<div class="detail-grid">
|
|
233
294
|
<span>Name</span><strong>{{ getName(selectedUser) }}</strong>
|
|
234
|
-
<span>User ID</span
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<span>
|
|
239
|
-
|
|
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>
|
|
240
315
|
</div>
|
|
241
316
|
</v-card-text>
|
|
242
317
|
<v-card-actions>
|
|
243
318
|
<v-spacer />
|
|
244
|
-
<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
|
+
>
|
|
245
322
|
</v-card-actions>
|
|
246
323
|
</v-card>
|
|
247
324
|
</v-dialog>
|
|
@@ -252,12 +329,26 @@
|
|
|
252
329
|
<v-card-text class="text-body-2">
|
|
253
330
|
Are you sure you want to permanently delete this HID user?
|
|
254
331
|
<div class="text-caption text-medium-emphasis mt-2">
|
|
255
|
-
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.
|
|
256
334
|
</div>
|
|
257
335
|
</v-card-text>
|
|
258
336
|
<v-card-actions class="pa-0">
|
|
259
|
-
<v-btn
|
|
260
|
-
|
|
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
|
+
>
|
|
261
352
|
</v-card-actions>
|
|
262
353
|
</v-card>
|
|
263
354
|
</v-dialog>
|
|
@@ -312,7 +403,7 @@ const deleteDialog = ref(false);
|
|
|
312
403
|
const selectedUser = ref<Record<string, any> | null>(null);
|
|
313
404
|
const showPassword = ref(false);
|
|
314
405
|
const photoInput = ref<HTMLInputElement | null>(null);
|
|
315
|
-
const
|
|
406
|
+
const cameraDialog = ref(false);
|
|
316
407
|
const selectedPhotoFile = ref<File | null>(null);
|
|
317
408
|
const removePhotoRequested = ref(false);
|
|
318
409
|
const snackbar = reactive({
|
|
@@ -340,7 +431,7 @@ const readerOptions = computed(() =>
|
|
|
340
431
|
readers.value.map((reader) => ({
|
|
341
432
|
title: reader.name || reader.deviceId || reader._id,
|
|
342
433
|
value: reader._id,
|
|
343
|
-
}))
|
|
434
|
+
}))
|
|
344
435
|
);
|
|
345
436
|
|
|
346
437
|
const pageRange = computed(() => {
|
|
@@ -354,7 +445,7 @@ const pageRange = computed(() => {
|
|
|
354
445
|
watch(
|
|
355
446
|
() => props.site,
|
|
356
447
|
() => init(),
|
|
357
|
-
{ immediate: true }
|
|
448
|
+
{ immediate: true }
|
|
358
449
|
);
|
|
359
450
|
|
|
360
451
|
async function init() {
|
|
@@ -365,7 +456,8 @@ async function init() {
|
|
|
365
456
|
async function loadReaders() {
|
|
366
457
|
if (!props.site) return;
|
|
367
458
|
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
368
|
-
readers.value =
|
|
459
|
+
readers.value =
|
|
460
|
+
response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
369
461
|
if (!selectedReaderId.value && readers.value.length) {
|
|
370
462
|
selectedReaderId.value = readers.value[0]._id;
|
|
371
463
|
}
|
|
@@ -394,10 +486,16 @@ async function loadUsers() {
|
|
|
394
486
|
limit: 500,
|
|
395
487
|
offset: 0,
|
|
396
488
|
}),
|
|
397
|
-
isAdministratorIdentity()
|
|
489
|
+
isAdministratorIdentity()
|
|
490
|
+
? loadAdministratorRoles(selectedReaderId.value)
|
|
491
|
+
: Promise.resolve(null),
|
|
398
492
|
]);
|
|
399
493
|
|
|
400
|
-
const identities =
|
|
494
|
+
const identities =
|
|
495
|
+
identityResponse?.items ??
|
|
496
|
+
identityResponse?.data?.items ??
|
|
497
|
+
identityResponse?.data?.identities ??
|
|
498
|
+
[];
|
|
401
499
|
const adminRoleIds = getAdministratorRoleUserIds(roleResponse);
|
|
402
500
|
const readerUsers = normalizeHidUsers(readerResponse).filter((user) => {
|
|
403
501
|
if (Number(user.userTypeId) === 1) return false;
|
|
@@ -429,21 +527,23 @@ async function loadUsers() {
|
|
|
429
527
|
async function loadUserImages(items: Record<string, any>[]) {
|
|
430
528
|
if (!selectedReaderId.value) return;
|
|
431
529
|
|
|
432
|
-
await Promise.all(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
);
|
|
447
547
|
}
|
|
448
548
|
|
|
449
549
|
function resetForm() {
|
|
@@ -512,7 +612,28 @@ function triggerPhotoInput() {
|
|
|
512
612
|
}
|
|
513
613
|
|
|
514
614
|
function triggerCameraInput() {
|
|
515
|
-
|
|
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;
|
|
516
637
|
}
|
|
517
638
|
|
|
518
639
|
function removePhoto() {
|
|
@@ -545,7 +666,8 @@ function onPhotoChange(event: Event) {
|
|
|
545
666
|
}
|
|
546
667
|
|
|
547
668
|
async function saveUser() {
|
|
548
|
-
const readerId =
|
|
669
|
+
const readerId =
|
|
670
|
+
selectedReaderId.value || form.reader || readers.value[0]?._id;
|
|
549
671
|
if (!readerId || !form.name || !form.hidUserId || !form.registration) {
|
|
550
672
|
showToast("Please complete the required HID user fields.", "error");
|
|
551
673
|
return;
|
|
@@ -567,7 +689,13 @@ async function saveUser() {
|
|
|
567
689
|
hidUserId: String(hidUser.id),
|
|
568
690
|
registration: form.registration,
|
|
569
691
|
cardNo: form.cardNo,
|
|
570
|
-
type: props.identityType as
|
|
692
|
+
type: props.identityType as
|
|
693
|
+
| "resident"
|
|
694
|
+
| "staff"
|
|
695
|
+
| "contractor"
|
|
696
|
+
| "visitor"
|
|
697
|
+
| "admin"
|
|
698
|
+
| "unknown",
|
|
571
699
|
status: "active" as const,
|
|
572
700
|
metadata: {
|
|
573
701
|
name: form.name,
|
|
@@ -604,7 +732,11 @@ async function saveUser() {
|
|
|
604
732
|
|
|
605
733
|
formDialog.value = false;
|
|
606
734
|
await loadUsers();
|
|
607
|
-
showToast(
|
|
735
|
+
showToast(
|
|
736
|
+
selectedUser.value?._id
|
|
737
|
+
? "HID user updated on reader."
|
|
738
|
+
: "HID user enrolled on reader."
|
|
739
|
+
);
|
|
608
740
|
} catch (error: any) {
|
|
609
741
|
console.error("Unable to save HID user:", error);
|
|
610
742
|
showToast(getHidErrorMessage(error), "error");
|
|
@@ -619,13 +751,21 @@ type FacialSyncResult = {
|
|
|
619
751
|
scores?: Record<string, unknown>;
|
|
620
752
|
};
|
|
621
753
|
|
|
622
|
-
async function syncFacialImage(
|
|
754
|
+
async function syncFacialImage(
|
|
755
|
+
readerId: string,
|
|
756
|
+
hidUserId: number
|
|
757
|
+
): Promise<FacialSyncResult> {
|
|
623
758
|
if (selectedPhotoFile.value) {
|
|
624
759
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
625
|
-
const response = await setUserImage(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
760
|
+
const response = await setUserImage(
|
|
761
|
+
readerId,
|
|
762
|
+
hidUserId,
|
|
763
|
+
selectedPhotoFile.value,
|
|
764
|
+
{
|
|
765
|
+
timestamp,
|
|
766
|
+
match: true,
|
|
767
|
+
}
|
|
768
|
+
);
|
|
629
769
|
const result = response?.data;
|
|
630
770
|
userImages.value[String(hidUserId)] = form.photoPreview;
|
|
631
771
|
return { action: "enrolled", timestamp, scores: result?.scores };
|
|
@@ -640,7 +780,10 @@ async function syncFacialImage(readerId: string, hidUserId: number): Promise<Fac
|
|
|
640
780
|
return { action: "none" };
|
|
641
781
|
}
|
|
642
782
|
|
|
643
|
-
function applyFacialResult(
|
|
783
|
+
function applyFacialResult(
|
|
784
|
+
metadata: Record<string, unknown>,
|
|
785
|
+
result: FacialSyncResult
|
|
786
|
+
) {
|
|
644
787
|
if (result.action === "enrolled") {
|
|
645
788
|
metadata.facialData = form.hidUserId;
|
|
646
789
|
metadata.imageTimestamp = String(result.timestamp || "");
|
|
@@ -658,7 +801,11 @@ async function deleteUser() {
|
|
|
658
801
|
|
|
659
802
|
deleting.value = true;
|
|
660
803
|
try {
|
|
661
|
-
await syncHidUserRole(
|
|
804
|
+
await syncHidUserRole(
|
|
805
|
+
selectedReaderId.value,
|
|
806
|
+
toHidNumericId(selectedUser.value.hidUserId),
|
|
807
|
+
false
|
|
808
|
+
);
|
|
662
809
|
await deleteHidUserFromReader(selectedReaderId.value, selectedUser.value);
|
|
663
810
|
if (selectedUser.value._id) {
|
|
664
811
|
await deleteIdentity(selectedUser.value._id);
|
|
@@ -674,8 +821,14 @@ async function deleteUser() {
|
|
|
674
821
|
}
|
|
675
822
|
}
|
|
676
823
|
|
|
677
|
-
async function saveIdentityOnReader(
|
|
678
|
-
|
|
824
|
+
async function saveIdentityOnReader(
|
|
825
|
+
readerId: string,
|
|
826
|
+
payload: Record<string, any>
|
|
827
|
+
) {
|
|
828
|
+
const existingIdentity = await findExistingIdentityOnReader(
|
|
829
|
+
readerId,
|
|
830
|
+
payload
|
|
831
|
+
);
|
|
679
832
|
if (existingIdentity?._id) {
|
|
680
833
|
await updateIdentity(existingIdentity._id, payload);
|
|
681
834
|
return;
|
|
@@ -687,31 +840,47 @@ async function saveIdentityOnReader(readerId: string, payload: Record<string, an
|
|
|
687
840
|
const message = getHidErrorMessage(error);
|
|
688
841
|
if (!message.toLowerCase().includes("already exists")) throw error;
|
|
689
842
|
|
|
690
|
-
const duplicatedIdentity = await findExistingIdentityOnReader(
|
|
843
|
+
const duplicatedIdentity = await findExistingIdentityOnReader(
|
|
844
|
+
readerId,
|
|
845
|
+
payload
|
|
846
|
+
);
|
|
691
847
|
if (!duplicatedIdentity?._id) throw error;
|
|
692
848
|
await updateIdentity(duplicatedIdentity._id, payload);
|
|
693
849
|
}
|
|
694
850
|
}
|
|
695
851
|
|
|
696
|
-
async function findExistingIdentityOnReader(
|
|
852
|
+
async function findExistingIdentityOnReader(
|
|
853
|
+
readerId: string,
|
|
854
|
+
payload: Record<string, any>
|
|
855
|
+
) {
|
|
697
856
|
const response = await getIdentities(readerId, {
|
|
698
857
|
page: 1,
|
|
699
858
|
limit: 500,
|
|
700
859
|
search: payload.hidUserId || payload.registration || payload.cardNo || "",
|
|
701
860
|
});
|
|
702
|
-
const identities =
|
|
861
|
+
const identities =
|
|
862
|
+
response?.items ??
|
|
863
|
+
response?.data?.items ??
|
|
864
|
+
response?.data?.identities ??
|
|
865
|
+
[];
|
|
703
866
|
const hidUserId = String(payload.hidUserId || "");
|
|
704
867
|
const registration = String(payload.registration || "");
|
|
705
868
|
const cardNo = String(payload.cardNo || "");
|
|
706
869
|
|
|
707
|
-
return identities.find(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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)
|
|
711
877
|
);
|
|
712
878
|
}
|
|
713
879
|
|
|
714
|
-
async function createHidUserOnReader(
|
|
880
|
+
async function createHidUserOnReader(
|
|
881
|
+
readerId: string,
|
|
882
|
+
user: Record<string, any>
|
|
883
|
+
) {
|
|
715
884
|
const existing = await getHidUserFromReader(readerId, user.id);
|
|
716
885
|
if (existing) {
|
|
717
886
|
await updateHidUserOnReader(readerId, user);
|
|
@@ -741,7 +910,10 @@ async function getHidUserFromReader(readerId: string, id: unknown) {
|
|
|
741
910
|
return normalizeHidUsers(response)[0];
|
|
742
911
|
}
|
|
743
912
|
|
|
744
|
-
async function updateHidUserOnReader(
|
|
913
|
+
async function updateHidUserOnReader(
|
|
914
|
+
readerId: string,
|
|
915
|
+
user: Record<string, any>
|
|
916
|
+
) {
|
|
745
917
|
const { id, ...values } = user;
|
|
746
918
|
await runObjectOperation(readerId, {
|
|
747
919
|
operation: "modify",
|
|
@@ -755,7 +927,10 @@ async function updateHidUserOnReader(readerId: string, user: Record<string, any>
|
|
|
755
927
|
});
|
|
756
928
|
}
|
|
757
929
|
|
|
758
|
-
async function deleteHidUserFromReader(
|
|
930
|
+
async function deleteHidUserFromReader(
|
|
931
|
+
readerId: string,
|
|
932
|
+
user: Record<string, any>
|
|
933
|
+
) {
|
|
759
934
|
const id = toHidNumericId(user.hidUserId);
|
|
760
935
|
if (!readerId || !id) return;
|
|
761
936
|
|
|
@@ -770,7 +945,11 @@ async function deleteHidUserFromReader(readerId: string, user: Record<string, an
|
|
|
770
945
|
});
|
|
771
946
|
}
|
|
772
947
|
|
|
773
|
-
async function syncHidUserRole(
|
|
948
|
+
async function syncHidUserRole(
|
|
949
|
+
readerId: string,
|
|
950
|
+
hidUserId: unknown,
|
|
951
|
+
isAdministrator: boolean
|
|
952
|
+
) {
|
|
774
953
|
const userId = toHidNumericId(hidUserId);
|
|
775
954
|
if (!readerId || !userId) return;
|
|
776
955
|
|
|
@@ -864,14 +1043,17 @@ function getAdministratorRoleUserIds(response: Record<string, any> | null) {
|
|
|
864
1043
|
return new Set(
|
|
865
1044
|
(Array.isArray(roles) ? roles : [])
|
|
866
1045
|
.map((role) => toHidNumericId(role.user_id))
|
|
867
|
-
.filter((userId): userId is number => Boolean(userId))
|
|
1046
|
+
.filter((userId): userId is number => Boolean(userId))
|
|
868
1047
|
);
|
|
869
1048
|
}
|
|
870
1049
|
|
|
871
1050
|
function toHidUserObject(identity: Record<string, any>) {
|
|
872
1051
|
return {
|
|
873
1052
|
id: toHidNumericId(identity.hidUserId),
|
|
874
|
-
name:
|
|
1053
|
+
name:
|
|
1054
|
+
identity.metadata?.name ||
|
|
1055
|
+
identity.name ||
|
|
1056
|
+
`User ${identity.hidUserId || ""}`.trim(),
|
|
875
1057
|
registration: identity.registration || "",
|
|
876
1058
|
};
|
|
877
1059
|
}
|
|
@@ -900,23 +1082,40 @@ function getName(user?: Record<string, any> | null) {
|
|
|
900
1082
|
}
|
|
901
1083
|
|
|
902
1084
|
function getUnit(user?: Record<string, any> | null) {
|
|
903
|
-
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
|
+
);
|
|
904
1092
|
}
|
|
905
1093
|
|
|
906
1094
|
function getFacialData(user: Record<string, any>) {
|
|
907
|
-
return
|
|
1095
|
+
return (
|
|
1096
|
+
user?.metadata?.facialData ||
|
|
1097
|
+
user?.metadata?.imageTimestamp ||
|
|
1098
|
+
(user?.metadata?.photo ? user.hidUserId : "N/A")
|
|
1099
|
+
);
|
|
908
1100
|
}
|
|
909
1101
|
|
|
910
1102
|
function getUserImageSrc(user: Record<string, any>) {
|
|
911
1103
|
const hidUserId = toHidNumericId(user?.hidUserId);
|
|
912
|
-
return
|
|
1104
|
+
return (
|
|
1105
|
+
user?.metadata?.photo ||
|
|
1106
|
+
(hidUserId ? userImages.value[String(hidUserId)] : "") ||
|
|
1107
|
+
""
|
|
1108
|
+
);
|
|
913
1109
|
}
|
|
914
1110
|
|
|
915
1111
|
async function getNextUid() {
|
|
916
1112
|
const readerIds = await getExistingHidReaderIds();
|
|
917
1113
|
const maxNumber = users.value.reduce((max, user) => {
|
|
918
1114
|
const match = String(user.hidUserId ?? "").match(/^UID(\d+)$/i);
|
|
919
|
-
return Math.max(
|
|
1115
|
+
return Math.max(
|
|
1116
|
+
max,
|
|
1117
|
+
match ? Number(match[1]) : toHidNumericId(user.hidUserId) || 0
|
|
1118
|
+
);
|
|
920
1119
|
}, 0);
|
|
921
1120
|
const nextNumber = Math.max(maxNumber, ...readerIds, 0) + 1;
|
|
922
1121
|
return `UID${String(nextNumber).padStart(6, "0")}`;
|
|
@@ -930,10 +1129,15 @@ async function getNextRegistration() {
|
|
|
930
1129
|
const numericRegistrations = registrations
|
|
931
1130
|
.map((registration) => {
|
|
932
1131
|
const match = registration.match(/^(\d+)$/);
|
|
933
|
-
return match
|
|
1132
|
+
return match
|
|
1133
|
+
? { value: Number(match[1]), width: match[1].length }
|
|
1134
|
+
: undefined;
|
|
934
1135
|
})
|
|
935
1136
|
.filter((item): item is { value: number; width: number } => Boolean(item));
|
|
936
|
-
const maxRegistration = numericRegistrations.reduce(
|
|
1137
|
+
const maxRegistration = numericRegistrations.reduce(
|
|
1138
|
+
(max, item) => Math.max(max, item.value),
|
|
1139
|
+
0
|
|
1140
|
+
);
|
|
937
1141
|
const width = Math.max(6, ...numericRegistrations.map((item) => item.width));
|
|
938
1142
|
|
|
939
1143
|
return String(maxRegistration + 1).padStart(width, "0");
|
|
@@ -980,7 +1184,9 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
980
1184
|
name: user.name,
|
|
981
1185
|
metadata: {
|
|
982
1186
|
name: user.name,
|
|
983
|
-
imageTimestamp: user.image_timestamp
|
|
1187
|
+
imageTimestamp: user.image_timestamp
|
|
1188
|
+
? String(user.image_timestamp)
|
|
1189
|
+
: "",
|
|
984
1190
|
lastAccessAt: fromUnixSeconds(user.last_access),
|
|
985
1191
|
hidReaderSource: true,
|
|
986
1192
|
},
|
|
@@ -988,7 +1194,10 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
988
1194
|
: [];
|
|
989
1195
|
}
|
|
990
1196
|
|
|
991
|
-
function mergeReaderUsersWithIdentities(
|
|
1197
|
+
function mergeReaderUsersWithIdentities(
|
|
1198
|
+
readerUsers: Record<string, any>[],
|
|
1199
|
+
identities: Record<string, any>[]
|
|
1200
|
+
) {
|
|
992
1201
|
const identityById = new Map<string, Record<string, any>>();
|
|
993
1202
|
identities.forEach((identity) => {
|
|
994
1203
|
const id = toHidNumericId(identity.hidUserId);
|
|
@@ -996,7 +1205,9 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
996
1205
|
});
|
|
997
1206
|
|
|
998
1207
|
const merged = readerUsers.map((readerUser) => {
|
|
999
|
-
const identity = identityById.get(
|
|
1208
|
+
const identity = identityById.get(
|
|
1209
|
+
String(toHidNumericId(readerUser.hidUserId))
|
|
1210
|
+
);
|
|
1000
1211
|
if (!identity) return readerUser;
|
|
1001
1212
|
|
|
1002
1213
|
return {
|
|
@@ -1011,8 +1222,11 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
1011
1222
|
metadata: {
|
|
1012
1223
|
...readerUser.metadata,
|
|
1013
1224
|
...identity.metadata,
|
|
1014
|
-
imageTimestamp:
|
|
1015
|
-
|
|
1225
|
+
imageTimestamp:
|
|
1226
|
+
identity.metadata?.imageTimestamp ||
|
|
1227
|
+
readerUser.metadata?.imageTimestamp,
|
|
1228
|
+
lastAccessAt:
|
|
1229
|
+
identity.metadata?.lastAccessAt || readerUser.metadata?.lastAccessAt,
|
|
1016
1230
|
},
|
|
1017
1231
|
};
|
|
1018
1232
|
});
|
|
@@ -1023,13 +1237,16 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
1023
1237
|
function filterUsers(items: Record<string, any>[]) {
|
|
1024
1238
|
const searchText = search.value.trim().toLowerCase();
|
|
1025
1239
|
return items.filter((item) => {
|
|
1026
|
-
const matchesSearch =
|
|
1027
|
-
|
|
1028
|
-
item.hidUserId,
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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;
|
|
1033
1250
|
return matchesSearch && matchesStatus;
|
|
1034
1251
|
});
|
|
1035
1252
|
}
|
|
@@ -1089,7 +1306,7 @@ function getHidErrorMessage(error: any) {
|
|
|
1089
1306
|
data?.statusMessage ||
|
|
1090
1307
|
error?.statusMessage ||
|
|
1091
1308
|
error?.message ||
|
|
1092
|
-
"Unable to save HID user."
|
|
1309
|
+
"Unable to save HID user."
|
|
1093
1310
|
);
|
|
1094
1311
|
}
|
|
1095
1312
|
</script>
|