@7365admin1/layer-common 4.0.0 → 4.0.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 +124 -0
- package/components/CameraForm.vue +2 -5
- package/components/CameraMain.vue +3 -6
- package/components/ClientDetailForm.vue +65 -5
- package/components/ClientMain.vue +201 -59
- package/components/HidAccessLogDashboard.vue +114 -34
- package/components/HidQrCodeConfiguration.vue +1 -1
- package/components/HidReaderManagement.vue +34 -11
- package/components/HidReaderUserRoster.vue +376 -0
- package/components/HidUserEnrollment.vue +7 -1
- package/components/HidUserMapping.vue +583 -0
- package/components/MemberMain.vue +12 -2
- package/components/RolePermissionMain.vue +2 -1
- package/composables/useConsoleTier.ts +73 -0
- package/composables/useHidAmico.ts +30 -6
- package/composables/useHidNavigation.ts +8 -1
- package/composables/useMember.ts +0 -5
- package/composables/usePromoCode.ts +57 -0
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-user-mapping/index.vue +23 -0
- package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +1 -1
- package/utils/client-subscription.test.ts +95 -0
- package/utils/console-tier.test.ts +87 -0
- package/utils/console-tier.ts +67 -0
- package/utils/data.test.ts +84 -0
- package/utils/data.ts +92 -0
- package/utils/promo-code-form.test.ts +437 -0
- package/utils/promo-code-form.ts +246 -0
|
@@ -89,8 +89,16 @@
|
|
|
89
89
|
|
|
90
90
|
<!-- Staging's hover-the-full-name tooltip, kept. -->
|
|
91
91
|
<template #[`item.name`]="{ item }">
|
|
92
|
-
<span class="app-cell--strong">
|
|
93
|
-
|
|
92
|
+
<span class="hid-log-name app-cell--strong">
|
|
93
|
+
<v-avatar
|
|
94
|
+
v-if="item.faceImage"
|
|
95
|
+
size="34"
|
|
96
|
+
rounded="lg"
|
|
97
|
+
class="hid-face-avatar"
|
|
98
|
+
>
|
|
99
|
+
<v-img :src="item.faceImage" cover />
|
|
100
|
+
</v-avatar>
|
|
101
|
+
<span>{{ item.name }}</span>
|
|
94
102
|
<v-tooltip
|
|
95
103
|
v-if="item.name && item.name !== 'N/A'"
|
|
96
104
|
activator="parent"
|
|
@@ -101,13 +109,8 @@
|
|
|
101
109
|
</span>
|
|
102
110
|
</template>
|
|
103
111
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<template #[`item.facialData`]="{ item }">
|
|
107
|
-
<v-avatar v-if="item.faceImage" size="34" rounded="lg" class="hid-face-avatar">
|
|
108
|
-
<v-img :src="item.faceImage" cover />
|
|
109
|
-
</v-avatar>
|
|
110
|
-
<span v-else class="app-cell--muted">{{ item.facialData }}</span>
|
|
112
|
+
<template #[`item.hidUserId`]="{ item }">
|
|
113
|
+
<span class="app-cell--num">{{ item.hidUserId }}</span>
|
|
111
114
|
</template>
|
|
112
115
|
|
|
113
116
|
<template #[`item.method`]="{ item }">
|
|
@@ -135,6 +138,10 @@
|
|
|
135
138
|
</StatusChip>
|
|
136
139
|
</template>
|
|
137
140
|
|
|
141
|
+
<template #[`item.mappingStatus`]="{ item }">
|
|
142
|
+
<StatusChip :status="item.mappingStatus" />
|
|
143
|
+
</template>
|
|
144
|
+
|
|
138
145
|
<template #[`item.action-table`]="{ item }">
|
|
139
146
|
<AppButton
|
|
140
147
|
variant="row"
|
|
@@ -300,7 +307,8 @@ const props = defineProps({
|
|
|
300
307
|
},
|
|
301
308
|
});
|
|
302
309
|
|
|
303
|
-
type AccessTab = "
|
|
310
|
+
type AccessTab = "all" | "unmapped" | "mapped" | "visitor" | "administrator";
|
|
311
|
+
type IdentityCategory = "resident" | "visitor" | "administrator";
|
|
304
312
|
type AuthorizationStatus = "Authorized" | "Not Authorized" | "Unknown";
|
|
305
313
|
type AccessRow = {
|
|
306
314
|
key: string;
|
|
@@ -308,6 +316,7 @@ type AccessRow = {
|
|
|
308
316
|
identityType?: string;
|
|
309
317
|
visitorId?: string;
|
|
310
318
|
name: string;
|
|
319
|
+
hidUserId: string;
|
|
311
320
|
facialData: string;
|
|
312
321
|
faceImage?: string;
|
|
313
322
|
unit: string;
|
|
@@ -316,6 +325,7 @@ type AccessRow = {
|
|
|
316
325
|
lastAccessDate?: Date;
|
|
317
326
|
readerLocation: string;
|
|
318
327
|
status: AuthorizationStatus;
|
|
328
|
+
mappingStatus: "Mapped" | "Unmapped";
|
|
319
329
|
authorizationReason?: string;
|
|
320
330
|
};
|
|
321
331
|
|
|
@@ -381,7 +391,7 @@ const liveAccessLogs = ref<HidAccessRecord[]>([]);
|
|
|
381
391
|
const administratorUserIds = ref<Set<number>>(new Set());
|
|
382
392
|
const userImages = ref<Record<string, string>>({});
|
|
383
393
|
const selectedReaderId = ref("");
|
|
384
|
-
const activeTab = ref<AccessTab>("
|
|
394
|
+
const activeTab = ref<AccessTab>("all");
|
|
385
395
|
const search = ref("");
|
|
386
396
|
const status = ref("");
|
|
387
397
|
const accessMethod = ref("");
|
|
@@ -463,6 +473,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
463
473
|
key: `${selectedHistoryRow.value?.key || "history"}-${log._id || log.id || index}`,
|
|
464
474
|
identityKey: selectedKey,
|
|
465
475
|
name: selectedHistoryRow.value?.name || "N/A",
|
|
476
|
+
hidUserId: selectedHistoryRow.value?.hidUserId || "N/A",
|
|
466
477
|
facialData: selectedHistoryRow.value?.facialData || "N/A",
|
|
467
478
|
faceImage: selectedHistoryRow.value?.faceImage,
|
|
468
479
|
unit: selectedHistoryRow.value?.unit || "N/A",
|
|
@@ -470,6 +481,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
470
481
|
lastAccessTime: formatDate(getLogAccessDateValue(log)),
|
|
471
482
|
lastAccessDate: parseDate(getLogAccessDateValue(log)),
|
|
472
483
|
readerLocation: getLogReaderLocation(log),
|
|
484
|
+
mappingStatus: selectedHistoryRow.value?.mappingStatus || "Unmapped",
|
|
473
485
|
...getAuthorizationStatus(log.payload || log, log),
|
|
474
486
|
}));
|
|
475
487
|
|
|
@@ -477,6 +489,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
477
489
|
key: `${selectedHistoryRow.value?.key || "history"}-live-${log.id || log.time || index}`,
|
|
478
490
|
identityKey: selectedKey,
|
|
479
491
|
name: selectedHistoryRow.value?.name || "N/A",
|
|
492
|
+
hidUserId: selectedHistoryRow.value?.hidUserId || "N/A",
|
|
480
493
|
facialData: selectedHistoryRow.value?.facialData || "N/A",
|
|
481
494
|
faceImage: selectedHistoryRow.value?.faceImage,
|
|
482
495
|
unit: selectedHistoryRow.value?.unit || "N/A",
|
|
@@ -484,6 +497,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
484
497
|
lastAccessTime: formatDate(getAccessLogTime(log, {})),
|
|
485
498
|
lastAccessDate: parseDate(getAccessLogTime(log, {})),
|
|
486
499
|
readerLocation: selectedReader.value?.location || "N/A",
|
|
500
|
+
mappingStatus: selectedHistoryRow.value?.mappingStatus || "Unmapped",
|
|
487
501
|
...getAuthorizationStatus(log, {}),
|
|
488
502
|
}));
|
|
489
503
|
|
|
@@ -499,11 +513,13 @@ const filteredRows = computed(() => {
|
|
|
499
513
|
return rows.value.filter((row) => {
|
|
500
514
|
const matchesSearch = !query || [
|
|
501
515
|
row.name,
|
|
516
|
+
row.hidUserId,
|
|
502
517
|
row.facialData,
|
|
503
518
|
row.unit,
|
|
504
519
|
row.method,
|
|
505
520
|
row.lastAccessTime,
|
|
506
521
|
row.status,
|
|
522
|
+
row.mappingStatus,
|
|
507
523
|
].some((value) => String(value ?? "").toLowerCase().includes(query));
|
|
508
524
|
|
|
509
525
|
const matchesStatus = !status.value || row.status === status.value;
|
|
@@ -514,11 +530,13 @@ const filteredRows = computed(() => {
|
|
|
514
530
|
});
|
|
515
531
|
|
|
516
532
|
/**
|
|
517
|
-
* The
|
|
518
|
-
*
|
|
533
|
+
* The source of truth is the Amico access log, so All is the initial view.
|
|
534
|
+
* The other tabs are audit filters; unmapped events never fall into Resident.
|
|
519
535
|
*/
|
|
520
536
|
const tabOptions: Array<{ label: string; value: AccessTab }> = [
|
|
521
|
-
{ label: "
|
|
537
|
+
{ label: "All", value: "all" },
|
|
538
|
+
{ label: "Unmapped", value: "unmapped" },
|
|
539
|
+
{ label: "Mapped", value: "mapped" },
|
|
522
540
|
{ label: "Visitor", value: "visitor" },
|
|
523
541
|
{ label: "Administrator", value: "administrator" },
|
|
524
542
|
];
|
|
@@ -530,13 +548,12 @@ const tabOptions: Array<{ label: string; value: AccessTab }> = [
|
|
|
530
548
|
* percentages had no design number behind them.
|
|
531
549
|
*/
|
|
532
550
|
const logHeaders = [
|
|
533
|
-
{ title: "Name", value: "name", sortable: false },
|
|
534
|
-
{ title: "
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
{ title: "Last Access Method", value: "method", sortable: false },
|
|
538
|
-
{ title: "Last access time", value: "lastAccessTime", sortable: false },
|
|
551
|
+
{ title: "Amico Name", value: "name", sortable: false },
|
|
552
|
+
{ title: "UID", value: "hidUserId", sortable: false },
|
|
553
|
+
{ title: "Access Method", value: "method", sortable: false },
|
|
554
|
+
{ title: "Access Date and Time", value: "lastAccessTime", sortable: false },
|
|
539
555
|
{ title: "Status", value: "status", sortable: false },
|
|
556
|
+
{ title: "iService Mapping", value: "mappingStatus", sortable: false },
|
|
540
557
|
{ title: "", value: "action-table", sortable: false },
|
|
541
558
|
];
|
|
542
559
|
|
|
@@ -664,7 +681,11 @@ async function loadDashboard() {
|
|
|
664
681
|
)
|
|
665
682
|
: Promise.resolve(null),
|
|
666
683
|
loadDashboardSource(
|
|
667
|
-
getReaderUsers(selectedReaderId.value, {
|
|
684
|
+
getReaderUsers(selectedReaderId.value, {
|
|
685
|
+
page: 1,
|
|
686
|
+
limit: 100,
|
|
687
|
+
includeVisitors: true,
|
|
688
|
+
}),
|
|
668
689
|
{ items: [] },
|
|
669
690
|
"reader users",
|
|
670
691
|
),
|
|
@@ -693,9 +714,7 @@ async function loadDashboard() {
|
|
|
693
714
|
.filter((userId: number | undefined): userId is number => Boolean(userId)),
|
|
694
715
|
);
|
|
695
716
|
allIdentities.value = mergedIdentities;
|
|
696
|
-
identities.value = mergedIdentities.filter(
|
|
697
|
-
(identity: Record<string, unknown>) => classifyIdentity(identity) === activeTab.value,
|
|
698
|
-
);
|
|
717
|
+
identities.value = mergedIdentities.filter(isMappedIdentity);
|
|
699
718
|
logs.value = liveItems.length
|
|
700
719
|
? []
|
|
701
720
|
: logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
@@ -704,7 +723,14 @@ async function loadDashboard() {
|
|
|
704
723
|
serverPageRange.value = String(
|
|
705
724
|
hidAccessLogResponse?.pageRange ?? hidAccessLogResponse?.data?.pageRange ?? "",
|
|
706
725
|
);
|
|
707
|
-
|
|
726
|
+
const accessLogUserIds = getAccessLogUserIds(liveItems);
|
|
727
|
+
const accessLogImageCandidates = [...accessLogUserIds].map((hidUserId) => ({
|
|
728
|
+
hidUserId: String(hidUserId),
|
|
729
|
+
}));
|
|
730
|
+
await loadUserImages(
|
|
731
|
+
[...mergedIdentities, ...accessLogImageCandidates],
|
|
732
|
+
accessLogUserIds,
|
|
733
|
+
);
|
|
708
734
|
} finally {
|
|
709
735
|
if (sequence === loadSequence.value) {
|
|
710
736
|
loading.value = false;
|
|
@@ -721,13 +747,18 @@ async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: s
|
|
|
721
747
|
}
|
|
722
748
|
}
|
|
723
749
|
|
|
724
|
-
async function loadUserImages(items: HidAccessRecord[]) {
|
|
750
|
+
async function loadUserImages(items: HidAccessRecord[], forceUserIds = new Set<number>()) {
|
|
725
751
|
if (!selectedReaderId.value) return;
|
|
726
752
|
|
|
727
753
|
await Promise.all(items.map(async (identity) => {
|
|
728
754
|
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
729
755
|
const key = String(hidUserId || "");
|
|
730
|
-
if (
|
|
756
|
+
if (
|
|
757
|
+
!hidUserId ||
|
|
758
|
+
key in userImages.value ||
|
|
759
|
+
identity?.metadata?.photo ||
|
|
760
|
+
(!forceUserIds.has(hidUserId) && !hasHidImageHint(identity))
|
|
761
|
+
) return;
|
|
731
762
|
|
|
732
763
|
try {
|
|
733
764
|
const response = await getUserImage(selectedReaderId.value, hidUserId);
|
|
@@ -741,6 +772,14 @@ async function loadUserImages(items: HidAccessRecord[]) {
|
|
|
741
772
|
}));
|
|
742
773
|
}
|
|
743
774
|
|
|
775
|
+
function getAccessLogUserIds(items: HidAccessRecord[]) {
|
|
776
|
+
return new Set(
|
|
777
|
+
items
|
|
778
|
+
.map((item) => toHidNumericId(item.user_id ?? item.userId ?? item.hidUserId))
|
|
779
|
+
.filter((userId): userId is number => userId !== undefined),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
744
783
|
function hasHidImageHint(identity: HidAccessRecord) {
|
|
745
784
|
return Boolean(
|
|
746
785
|
identity?.metadata?.imageTimestamp ||
|
|
@@ -809,9 +848,16 @@ function resetDashboardData() {
|
|
|
809
848
|
|
|
810
849
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
811
850
|
const identity = findIdentityByKey(row.identityKey);
|
|
851
|
+
if (activeTab.value === "all") return true;
|
|
852
|
+
if (activeTab.value === "unmapped") return !isMappedIdentity(identity);
|
|
853
|
+
if (activeTab.value === "mapped") return isMappedIdentity(identity);
|
|
812
854
|
return classifyIdentity(identity, row) === activeTab.value;
|
|
813
855
|
}
|
|
814
856
|
|
|
857
|
+
function isMappedIdentity(identity: HidAccessRecord | undefined) {
|
|
858
|
+
return Boolean(identity?.person || identity?.member || identity?.serviceProvider || identity?.visitor);
|
|
859
|
+
}
|
|
860
|
+
|
|
815
861
|
function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
816
862
|
return Boolean(identity && (
|
|
817
863
|
identity.type === "visitor"
|
|
@@ -820,7 +866,7 @@ function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
|
820
866
|
));
|
|
821
867
|
}
|
|
822
868
|
|
|
823
|
-
function classifyIdentity(identity?: Record<string, unknown>, row?: AccessRow):
|
|
869
|
+
function classifyIdentity(identity?: Record<string, unknown>, row?: AccessRow): IdentityCategory {
|
|
824
870
|
const hidUserId = toHidNumericId(identity?.hidUserId ?? row?.identityKey.split("|")[0]);
|
|
825
871
|
if (hidUserId && administratorUserIds.value.has(hidUserId)) return "administrator";
|
|
826
872
|
if (row?.identityType === "visitor" || row?.visitorId || isVisitorIdentity(identity)) return "visitor";
|
|
@@ -1128,6 +1174,7 @@ function getRawAccessLogs(event: HidAccessRecord): HidAccessRecord[] {
|
|
|
1128
1174
|
|
|
1129
1175
|
function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, index: string): AccessRow {
|
|
1130
1176
|
const identity = findIdentityForAccessLog(rawLog, event);
|
|
1177
|
+
const mapped = isMappedIdentity(identity);
|
|
1131
1178
|
const eventIdentity = event.payload?.identity || event.payload?.identityLookup || {};
|
|
1132
1179
|
const rawAccessTime = getAccessLogTime(rawLog, event);
|
|
1133
1180
|
const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
|
|
@@ -1139,7 +1186,8 @@ function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, inde
|
|
|
1139
1186
|
? classifyIdentity(identity)
|
|
1140
1187
|
: String(eventIdentity.type || "").toLowerCase() || undefined,
|
|
1141
1188
|
visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
|
|
1142
|
-
name:
|
|
1189
|
+
name: getRawLogName(rawLog, event, identity),
|
|
1190
|
+
hidUserId: formatHidUid(rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? identity?.hidUserId),
|
|
1143
1191
|
facialData: identity ? getFacialData(identity) : "N/A",
|
|
1144
1192
|
faceImage: identity ? getUserImageSrc(identity) : "",
|
|
1145
1193
|
unit: identity ? getUnit(identity) : "N/A",
|
|
@@ -1147,6 +1195,7 @@ function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, inde
|
|
|
1147
1195
|
lastAccessTime: formatDate(rawAccessTime),
|
|
1148
1196
|
lastAccessDate: parseDate(rawAccessTime),
|
|
1149
1197
|
readerLocation: getLogReaderLocation(event),
|
|
1198
|
+
mappingStatus: mapped ? "Mapped" : "Unmapped",
|
|
1150
1199
|
...getAuthorizationStatus(rawLog, event),
|
|
1151
1200
|
};
|
|
1152
1201
|
}
|
|
@@ -1190,9 +1239,7 @@ function getAuthorizationStatus(
|
|
|
1190
1239
|
}
|
|
1191
1240
|
|
|
1192
1241
|
function findIdentityForAccessLog(rawLog: HidAccessRecord, event: HidAccessRecord) {
|
|
1193
|
-
if (rawLog.identity
|
|
1194
|
-
return rawLog.identity;
|
|
1195
|
-
}
|
|
1242
|
+
if (isUnknownRecord(rawLog.identity)) return rawLog.identity;
|
|
1196
1243
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1197
1244
|
if (eventIdentityId) {
|
|
1198
1245
|
const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
@@ -1283,8 +1330,35 @@ function getAccessLogTime(rawLog: HidAccessRecord, event: HidAccessRecord) {
|
|
|
1283
1330
|
return rawLog.time || rawLog.createdAt || rawLog.accessedAt || event.createdAt;
|
|
1284
1331
|
}
|
|
1285
1332
|
|
|
1286
|
-
function getRawLogName(
|
|
1287
|
-
|
|
1333
|
+
function getRawLogName(
|
|
1334
|
+
rawLog: HidAccessRecord,
|
|
1335
|
+
event: HidAccessRecord,
|
|
1336
|
+
identity?: HidAccessRecord,
|
|
1337
|
+
) {
|
|
1338
|
+
return getText(rawLog.user_name)
|
|
1339
|
+
|| getText(rawLog.userName)
|
|
1340
|
+
|| getText(rawLog.name)
|
|
1341
|
+
|| getText(event.payload?.rawAccessLog?.user_name)
|
|
1342
|
+
|| getText(event.payload?.rawAccessLog?.userName)
|
|
1343
|
+
|| getText(event.payload?.rawAccessLog?.name)
|
|
1344
|
+
|| getText(identity?.metadata?.name)
|
|
1345
|
+
|| getText(identity?.name)
|
|
1346
|
+
|| "Unknown";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function formatHidUid(value: unknown) {
|
|
1350
|
+
const text = getText(value);
|
|
1351
|
+
if (!text) return "N/A";
|
|
1352
|
+
const numeric = Number(text);
|
|
1353
|
+
return Number.isSafeInteger(numeric) && numeric > 0
|
|
1354
|
+
? `UID${String(numeric).padStart(6, "0")}`
|
|
1355
|
+
: text;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function getText(value: unknown) {
|
|
1359
|
+
if (typeof value === "string") return value.trim();
|
|
1360
|
+
if (typeof value === "number") return String(value);
|
|
1361
|
+
return "";
|
|
1288
1362
|
}
|
|
1289
1363
|
|
|
1290
1364
|
function exportRows() {
|
|
@@ -1765,6 +1839,12 @@ function escapeHtml(value: unknown) {
|
|
|
1765
1839
|
background: var(--hover);
|
|
1766
1840
|
}
|
|
1767
1841
|
|
|
1842
|
+
.hid-log-name {
|
|
1843
|
+
align-items: center;
|
|
1844
|
+
display: inline-flex;
|
|
1845
|
+
gap: 8px;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1768
1848
|
@media (max-width: 960px) {
|
|
1769
1849
|
.history-toolbar {
|
|
1770
1850
|
align-items: stretch;
|
|
@@ -616,7 +616,7 @@ async function openPermissionDialog(category: THidPermissionCategory | "intercom
|
|
|
616
616
|
page: 1,
|
|
617
617
|
limit: 500,
|
|
618
618
|
});
|
|
619
|
-
permissionCandidates.value = response.items
|
|
619
|
+
permissionCandidates.value = response.items ?? response.data?.items ?? [];
|
|
620
620
|
selectedPermissionIds.value = new Set(
|
|
621
621
|
permissionCandidates.value
|
|
622
622
|
.filter((candidate) => category === "intercom" ? candidate.intercom : candidate.selected)
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
<strong>{{ syncPreviewStats.updatedInformation }}</strong>
|
|
173
173
|
</div>
|
|
174
174
|
<div>
|
|
175
|
-
<span>Facial Data</span>
|
|
175
|
+
<span>Existing Facial Data</span>
|
|
176
176
|
<strong>{{ syncPreviewStats.facialData }}</strong>
|
|
177
177
|
</div>
|
|
178
178
|
</div>
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
<strong>{{ resultStats.updatedInformation }}</strong>
|
|
230
230
|
</div>
|
|
231
231
|
<div>
|
|
232
|
-
<span>
|
|
232
|
+
<span>Synced Facial Data</span>
|
|
233
233
|
<strong>{{ resultStats.facialData }}</strong>
|
|
234
234
|
</div>
|
|
235
235
|
</div>
|
|
@@ -331,6 +331,7 @@ const {
|
|
|
331
331
|
updateReader,
|
|
332
332
|
deleteReader,
|
|
333
333
|
testReader,
|
|
334
|
+
syncReaderFacialData,
|
|
334
335
|
configureReaderIntegration,
|
|
335
336
|
setReaderMonitor,
|
|
336
337
|
setReaderOperatingMode,
|
|
@@ -596,13 +597,17 @@ async function runConfirmedAction() {
|
|
|
596
597
|
const unmappedCount = Number(payload.unmappedCount || 0);
|
|
597
598
|
|
|
598
599
|
if (!payload.objects.length) {
|
|
600
|
+
const facialSync = await syncReaderFacialData(selectedReader.value._id);
|
|
601
|
+
const facialStats = toFacialSyncStats(facialSync);
|
|
599
602
|
if (unmappedCount) {
|
|
600
|
-
const message =
|
|
601
|
-
|| `Found ${unmappedCount} HID user(s) that must be linked from the HID
|
|
603
|
+
const message = [
|
|
604
|
+
syncMessage.value || `Found ${unmappedCount} HID user(s) that must be linked from the HID User Mapping screen.`,
|
|
605
|
+
facialStats.message,
|
|
606
|
+
].filter(Boolean).join(" ");
|
|
602
607
|
await applyReaderSyncState(selectedReader.value._id, "completed", message);
|
|
603
608
|
showResult(true, "Sync Review Required", message, {
|
|
604
609
|
readerName: selectedReader.value.name || "HID Reader",
|
|
605
|
-
stats: syncPreviewStats.value,
|
|
610
|
+
stats: { ...syncPreviewStats.value, facialData: facialStats.syncedCount },
|
|
606
611
|
});
|
|
607
612
|
confirmDialog.value = false;
|
|
608
613
|
resetActionFlow();
|
|
@@ -619,14 +624,18 @@ async function runConfirmedAction() {
|
|
|
619
624
|
return;
|
|
620
625
|
}
|
|
621
626
|
const result = await syncHidUsers(selectedReader.value._id, payload.users);
|
|
627
|
+
const facialSync = await syncReaderFacialData(selectedReader.value._id);
|
|
628
|
+
const facialStats = toFacialSyncStats(facialSync);
|
|
622
629
|
const mappingMessage = unmappedCount
|
|
623
|
-
? `${unmappedCount} HID user(s) still need to be linked from the HID
|
|
630
|
+
? `${unmappedCount} HID user(s) still need to be linked from the HID User Mapping screen.`
|
|
624
631
|
: "";
|
|
625
|
-
const message = syncMessage.value
|
|
632
|
+
const message = [syncMessage.value, result.message, facialStats.message, mappingMessage]
|
|
633
|
+
.filter(Boolean)
|
|
634
|
+
.join(" ");
|
|
626
635
|
await applyReaderSyncState(selectedReader.value._id, "completed", message);
|
|
627
636
|
showResult(true, "Sync Successful", message, {
|
|
628
637
|
readerName: selectedReader.value.name || "HID Reader",
|
|
629
|
-
stats: syncPreviewStats.value,
|
|
638
|
+
stats: { ...syncPreviewStats.value, facialData: facialStats.syncedCount },
|
|
630
639
|
});
|
|
631
640
|
}
|
|
632
641
|
if (pendingAction.value === "delete") {
|
|
@@ -714,7 +723,7 @@ async function buildSyncPayload(readerId: string) {
|
|
|
714
723
|
facialData: countFacialData(hidUsers),
|
|
715
724
|
},
|
|
716
725
|
defaultMessage: hidUsers.length
|
|
717
|
-
? `Found ${hidUsers.length} HID user(s) that must be linked from the HID
|
|
726
|
+
? `Found ${hidUsers.length} HID user(s) that must be linked from the HID User Mapping screen.`
|
|
718
727
|
: "",
|
|
719
728
|
};
|
|
720
729
|
}
|
|
@@ -760,7 +769,7 @@ async function buildSyncPayload(readerId: string) {
|
|
|
760
769
|
defaultMessage: [
|
|
761
770
|
totalUsersToSync ? `Ready to sync ${totalUsersToSync} linked HID user(s).` : "",
|
|
762
771
|
hidUsersToImport.length
|
|
763
|
-
? `${hidUsersToImport.length} HID user(s) must be linked from the HID
|
|
772
|
+
? `${hidUsersToImport.length} HID user(s) must be linked from the HID User Mapping screen.`
|
|
764
773
|
: "",
|
|
765
774
|
].filter(Boolean).join(" "),
|
|
766
775
|
};
|
|
@@ -832,7 +841,11 @@ function normalizeHidUsers(response: HidRecord) {
|
|
|
832
841
|
[];
|
|
833
842
|
|
|
834
843
|
return Array.isArray(users)
|
|
835
|
-
? users
|
|
844
|
+
? users
|
|
845
|
+
// Visitor QR passes are linked by their visitor transaction and must not
|
|
846
|
+
// be offered for manual HID user mapping or counted as unmapped users.
|
|
847
|
+
.filter((user) => Number(user?.user_type_id) !== 1)
|
|
848
|
+
.map((user) => ({
|
|
836
849
|
hidUserId: user.id,
|
|
837
850
|
registration: user.registration,
|
|
838
851
|
cardNo: user.card_value || user.cardNo,
|
|
@@ -970,6 +983,16 @@ function countFacialData(users: HidRecord[]) {
|
|
|
970
983
|
return users.filter((user) => user.face || user.photo || user.image || user.imageUrl || user.faceImage).length;
|
|
971
984
|
}
|
|
972
985
|
|
|
986
|
+
function toFacialSyncStats(response: unknown) {
|
|
987
|
+
const root = asHidRecord(response);
|
|
988
|
+
const data = asHidRecord(root.data ?? root);
|
|
989
|
+
const syncedCount = Number(data.syncedCount);
|
|
990
|
+
return {
|
|
991
|
+
syncedCount: Number.isSafeInteger(syncedCount) && syncedCount >= 0 ? syncedCount : 0,
|
|
992
|
+
message: String(data.message || "").trim(),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
973
996
|
function getFriendlyHidError(error: unknown) {
|
|
974
997
|
const message = getHidErrorMessage(error);
|
|
975
998
|
const lowerMessage = message.toLowerCase();
|