@7365admin1/layer-common 3.0.18 → 3.0.20
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 +12 -0
- package/components/BuildingManagement/buildings.vue +7 -4
- package/components/BuildingManagement/units.vue +187 -9
- package/components/CleaningScheduleMain.vue +4 -0
- package/components/ClientMain.vue +189 -39
- package/components/DashboardMain.vue +1 -7
- package/components/HidAccessLogDashboard.vue +184 -21
- package/components/HidReaderForm.vue +20 -12
- package/components/HidReaderManagement.vue +113 -80
- package/components/Input/DateTimePicker.vue +152 -137
- package/components/MemberInformation.vue +240 -163
- package/components/PassInformation.vue +6 -4
- package/components/ServiceProviderMain.vue +43 -20
- package/components/VisitorForm.vue +41 -18
- package/composables/useBuilding.ts +14 -0
- package/composables/useServiceProvider.ts +2 -1
- package/composables/useVisitor.ts +19 -15
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +3 -1
- package/types/visitor.d.ts +1 -1
|
@@ -313,6 +313,10 @@
|
|
|
313
313
|
|
|
314
314
|
<script setup lang="ts">
|
|
315
315
|
const props = defineProps({
|
|
316
|
+
org: {
|
|
317
|
+
type: String,
|
|
318
|
+
default: "",
|
|
319
|
+
},
|
|
316
320
|
site: {
|
|
317
321
|
type: String,
|
|
318
322
|
required: true,
|
|
@@ -360,6 +364,7 @@ const status = ref("Status");
|
|
|
360
364
|
const page = ref(1);
|
|
361
365
|
const limit = 20;
|
|
362
366
|
const loading = ref(false);
|
|
367
|
+
const loadSequence = ref(0);
|
|
363
368
|
const exportDialog = ref(false);
|
|
364
369
|
const historyDialog = ref(false);
|
|
365
370
|
const selectedHistoryRow = ref<AccessRow>();
|
|
@@ -503,7 +508,7 @@ const exportRowsData = computed(() =>
|
|
|
503
508
|
);
|
|
504
509
|
|
|
505
510
|
watch(
|
|
506
|
-
() => props.site,
|
|
511
|
+
() => [props.org, props.site],
|
|
507
512
|
() => init(),
|
|
508
513
|
{ immediate: true },
|
|
509
514
|
);
|
|
@@ -532,18 +537,18 @@ async function loadReaders() {
|
|
|
532
537
|
|
|
533
538
|
async function loadDashboard() {
|
|
534
539
|
if (!selectedReaderId.value) {
|
|
535
|
-
|
|
536
|
-
visitorRecords.value = {};
|
|
537
|
-
logs.value = [];
|
|
538
|
-
liveAccessLogs.value = [];
|
|
540
|
+
resetDashboardData();
|
|
539
541
|
return;
|
|
540
542
|
}
|
|
541
543
|
|
|
544
|
+
const sequence = loadSequence.value + 1;
|
|
545
|
+
loadSequence.value = sequence;
|
|
542
546
|
loading.value = true;
|
|
547
|
+
resetDashboardData();
|
|
543
548
|
try {
|
|
544
549
|
const isAdministratorTab = activeTab.value === "administrator";
|
|
545
550
|
const isVisitorTab = activeTab.value === "visitor";
|
|
546
|
-
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse] = await Promise.all([
|
|
551
|
+
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
547
552
|
getIdentities(selectedReaderId.value, {
|
|
548
553
|
page: 1,
|
|
549
554
|
limit: 500,
|
|
@@ -556,29 +561,43 @@ async function loadDashboard() {
|
|
|
556
561
|
loadLiveAccessLogs(selectedReaderId.value),
|
|
557
562
|
loadReaderConfiguration(selectedReaderId.value),
|
|
558
563
|
isAdministratorTab ? loadAdministratorRoles(selectedReaderId.value) : Promise.resolve(null),
|
|
559
|
-
isVisitorTab
|
|
564
|
+
isVisitorTab && props.org
|
|
565
|
+
? getVisitors({
|
|
566
|
+
org: props.org,
|
|
567
|
+
site: props.site,
|
|
568
|
+
page: 1,
|
|
569
|
+
limit: 500,
|
|
570
|
+
order: "desc",
|
|
571
|
+
})
|
|
572
|
+
: Promise.resolve(null),
|
|
573
|
+
loadReaderUsers(selectedReaderId.value),
|
|
560
574
|
]);
|
|
561
575
|
|
|
576
|
+
if (sequence !== loadSequence.value) return;
|
|
577
|
+
|
|
562
578
|
const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
|
|
579
|
+
const mergedIdentities = mergeReaderUsersWithIdentities(readerUserResponse, loadedIdentities);
|
|
563
580
|
const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
|
|
564
581
|
visitorRecords.value = isVisitorTab
|
|
565
582
|
? Object.fromEntries(loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]))
|
|
566
583
|
: {};
|
|
567
584
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
568
585
|
identities.value = isAdministratorTab
|
|
569
|
-
?
|
|
586
|
+
? mergedIdentities.filter((identity: Record<string, any>) => {
|
|
570
587
|
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
571
588
|
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
572
589
|
})
|
|
573
590
|
: isVisitorTab
|
|
574
|
-
?
|
|
575
|
-
:
|
|
591
|
+
? mergedIdentities.filter((identity: Record<string, any>) => Boolean(getVisitorRecord(identity)))
|
|
592
|
+
: mergedIdentities;
|
|
576
593
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
577
594
|
liveAccessLogs.value = hidAccessLogs;
|
|
578
595
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
579
596
|
await loadUserImages(identities.value);
|
|
580
597
|
} finally {
|
|
581
|
-
|
|
598
|
+
if (sequence === loadSequence.value) {
|
|
599
|
+
loading.value = false;
|
|
600
|
+
}
|
|
582
601
|
}
|
|
583
602
|
}
|
|
584
603
|
|
|
@@ -586,9 +605,9 @@ async function loadUserImages(items: Record<string, any>[]) {
|
|
|
586
605
|
if (!selectedReaderId.value) return;
|
|
587
606
|
|
|
588
607
|
await Promise.all(items.map(async (identity) => {
|
|
589
|
-
const hidUserId = identity.hidUserId;
|
|
608
|
+
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
590
609
|
const key = String(hidUserId || "");
|
|
591
|
-
if (!hidUserId || userImages.value[key] || identity?.metadata?.photo) return;
|
|
610
|
+
if (!hidUserId || userImages.value[key] || identity?.metadata?.photo || !hasHidImageHint(identity)) return;
|
|
592
611
|
|
|
593
612
|
try {
|
|
594
613
|
const response = await getUserImage(selectedReaderId.value, hidUserId);
|
|
@@ -602,6 +621,18 @@ async function loadUserImages(items: Record<string, any>[]) {
|
|
|
602
621
|
}));
|
|
603
622
|
}
|
|
604
623
|
|
|
624
|
+
function hasHidImageHint(identity: Record<string, any>) {
|
|
625
|
+
return Boolean(
|
|
626
|
+
identity?.metadata?.imageTimestamp ||
|
|
627
|
+
identity?.metadata?.facialData ||
|
|
628
|
+
identity?.metadata?.photo ||
|
|
629
|
+
identity?.imageTimestamp ||
|
|
630
|
+
identity?.image_timestamp ||
|
|
631
|
+
identity?.facialData ||
|
|
632
|
+
identity?.photo,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
605
636
|
async function loadLiveAccessLogs(readerId: string) {
|
|
606
637
|
const allLogs: Record<string, any>[] = [];
|
|
607
638
|
const limit = 500;
|
|
@@ -652,20 +683,113 @@ async function loadReaderConfiguration(readerId: string) {
|
|
|
652
683
|
}
|
|
653
684
|
}
|
|
654
685
|
|
|
655
|
-
function onTabChange() {
|
|
686
|
+
async function onTabChange(tab: AccessTab) {
|
|
687
|
+
if (tab) {
|
|
688
|
+
activeTab.value = tab;
|
|
689
|
+
}
|
|
656
690
|
page.value = 1;
|
|
691
|
+
resetDashboardData();
|
|
692
|
+
await nextTick();
|
|
657
693
|
loadDashboard();
|
|
658
694
|
}
|
|
659
695
|
|
|
696
|
+
async function loadReaderUsers(readerId: string) {
|
|
697
|
+
try {
|
|
698
|
+
const response = await runObjectOperation(readerId, {
|
|
699
|
+
operation: "load",
|
|
700
|
+
object: "users",
|
|
701
|
+
limit: 500,
|
|
702
|
+
offset: 0,
|
|
703
|
+
});
|
|
704
|
+
return normalizeHidUsers(response);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.warn("Unable to load HID users for access log images", error);
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function normalizeHidUsers(response: Record<string, any>) {
|
|
712
|
+
const items =
|
|
713
|
+
response?.data?.users ||
|
|
714
|
+
response?.data?.data?.users ||
|
|
715
|
+
response?.users ||
|
|
716
|
+
[];
|
|
717
|
+
|
|
718
|
+
return Array.isArray(items)
|
|
719
|
+
? items.map((user) => ({
|
|
720
|
+
hidUserId: String(user.id || ""),
|
|
721
|
+
rawHidUserId: user.id,
|
|
722
|
+
registration: user.registration,
|
|
723
|
+
cardNo: user.card_value || user.cardNo || "",
|
|
724
|
+
name: user.name,
|
|
725
|
+
metadata: {
|
|
726
|
+
name: user.name,
|
|
727
|
+
imageTimestamp: user.image_timestamp ? String(user.image_timestamp) : "",
|
|
728
|
+
hidReaderSource: true,
|
|
729
|
+
},
|
|
730
|
+
}))
|
|
731
|
+
: [];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], identities: Record<string, any>[]) {
|
|
735
|
+
const identityById = new Map<string, Record<string, any>>();
|
|
736
|
+
identities.forEach((identity) => {
|
|
737
|
+
const id = toHidNumericId(identity.hidUserId);
|
|
738
|
+
if (id) identityById.set(String(id), identity);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const mergedReaderUsers = readerUsers.map((readerUser) => {
|
|
742
|
+
const identity = identityById.get(String(toHidNumericId(readerUser.hidUserId)));
|
|
743
|
+
if (!identity) return readerUser;
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
...readerUser,
|
|
747
|
+
...identity,
|
|
748
|
+
hidUserId: identity.hidUserId || readerUser.hidUserId,
|
|
749
|
+
rawHidUserId: readerUser.rawHidUserId,
|
|
750
|
+
name: identity.metadata?.name || identity.name || readerUser.name,
|
|
751
|
+
registration: identity.registration || readerUser.registration,
|
|
752
|
+
cardNo: identity.cardNo || readerUser.cardNo,
|
|
753
|
+
metadata: {
|
|
754
|
+
...readerUser.metadata,
|
|
755
|
+
...identity.metadata,
|
|
756
|
+
imageTimestamp: identity.metadata?.imageTimestamp || readerUser.metadata?.imageTimestamp,
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const readerUserIds = new Set(readerUsers.map((user) => String(toHidNumericId(user.hidUserId))));
|
|
762
|
+
const identitiesOnly = identities.filter((identity) => !readerUserIds.has(String(toHidNumericId(identity.hidUserId))));
|
|
763
|
+
return [...mergedReaderUsers, ...identitiesOnly];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function resetDashboardData() {
|
|
767
|
+
selectedHistoryRow.value = undefined;
|
|
768
|
+
identities.value = [];
|
|
769
|
+
visitorRecords.value = {};
|
|
770
|
+
logs.value = [];
|
|
771
|
+
liveAccessLogs.value = [];
|
|
772
|
+
readerConfiguration.value = {};
|
|
773
|
+
administratorUserIds.value = new Set();
|
|
774
|
+
}
|
|
775
|
+
|
|
660
776
|
function getIdentityType(tab: AccessTab) {
|
|
661
777
|
if (tab === "administrator") return "admin";
|
|
662
778
|
return tab;
|
|
663
779
|
}
|
|
664
780
|
|
|
665
781
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
782
|
+
const identity = findIdentityByKey(row.identityKey);
|
|
783
|
+
if (activeTab.value === "visitor") {
|
|
784
|
+
return Boolean(identity && getVisitorRecord(identity));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (activeTab.value === "administrator") {
|
|
788
|
+
const hidUserId = toHidNumericId(identity?.hidUserId ?? row.identityKey.split("|")[0]);
|
|
789
|
+
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return !identity || !getVisitorRecord(identity);
|
|
669
793
|
}
|
|
670
794
|
|
|
671
795
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -715,6 +839,11 @@ function getIdentityKey(identity: Record<string, any>) {
|
|
|
715
839
|
].filter(Boolean).join("|");
|
|
716
840
|
}
|
|
717
841
|
|
|
842
|
+
function findIdentityByKey(identityKey: string) {
|
|
843
|
+
if (!identityKey) return undefined;
|
|
844
|
+
return identities.value.find((identity) => getIdentityKey(identity) === identityKey);
|
|
845
|
+
}
|
|
846
|
+
|
|
718
847
|
function getLogIdentityKey(log: Record<string, any>) {
|
|
719
848
|
const identity = log.payload?.identity || {};
|
|
720
849
|
const lookup = log.payload?.identityLookup || {};
|
|
@@ -731,11 +860,12 @@ function getName(identity: Record<string, any>) {
|
|
|
731
860
|
}
|
|
732
861
|
|
|
733
862
|
function getFacialData(identity: Record<string, any>) {
|
|
734
|
-
return identity.metadata?.facialData || (identity.metadata?.photo ? identity.hidUserId : "N/A");
|
|
863
|
+
return identity.metadata?.facialData || identity.metadata?.imageTimestamp || (identity.metadata?.photo ? identity.hidUserId : "N/A");
|
|
735
864
|
}
|
|
736
865
|
|
|
737
866
|
function getUserImageSrc(identity: Record<string, any>) {
|
|
738
|
-
|
|
867
|
+
const hidUserId = toHidNumericId(identity?.hidUserId);
|
|
868
|
+
return identity?.metadata?.photo || (hidUserId ? userImages.value[String(hidUserId)] : "") || "";
|
|
739
869
|
}
|
|
740
870
|
|
|
741
871
|
function getUnit(identity: Record<string, any>) {
|
|
@@ -758,27 +888,60 @@ function getAccessMethod(identity: Record<string, any>, log?: Record<string, any
|
|
|
758
888
|
if (configuredLogType) return configuredLogType;
|
|
759
889
|
const configuredRule = getConfigurationLabel("identification_rules", payload.identification_rule_id || payload.identificationRuleId);
|
|
760
890
|
if (configuredRule) return configuredRule;
|
|
891
|
+
const rawIdentification = String(payload.identification || payload.identification_type || payload.identificationType || "").toLowerCase();
|
|
892
|
+
if (rawIdentification.includes("facial") || rawIdentification.includes("face")) return "Facial";
|
|
761
893
|
if (payload.qrcode_value || payload.qrCode || payload.qrcode) return "QR Code";
|
|
762
894
|
if (payload.card_value || payload.cardNo || identity.cardNo) return "Card";
|
|
763
895
|
if (payload.pin_value) return "PIN";
|
|
764
|
-
if (payload.password_value || payload.password || payload.identifier_id) return "ID and Password";
|
|
765
896
|
if (payload.confidence || payload.user_id || payload.userId || payload.hidUserId) return "Facial";
|
|
897
|
+
if (payload.password_value || payload.password || isValidIdentifierId(payload.identifier_id)) return "ID and Password";
|
|
766
898
|
if (identity.metadata?.pinPassword) return "ID and Password";
|
|
767
899
|
return identity.metadata?.photo || identity.metadata?.facialData ? "Facial" : "N/A";
|
|
768
900
|
}
|
|
769
901
|
|
|
770
902
|
function formatDate(value?: string | number) {
|
|
771
903
|
if (!value) return "N/A";
|
|
904
|
+
if (typeof value === "number") return formatHidEpochDate(value);
|
|
772
905
|
const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
|
|
773
906
|
return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
|
|
774
907
|
}
|
|
775
908
|
|
|
776
909
|
function parseDate(value?: string | number) {
|
|
777
910
|
if (!value) return undefined;
|
|
778
|
-
const date = typeof value === "number" ?
|
|
911
|
+
const date = typeof value === "number" ? parseHidEpochDate(value) : new Date(value);
|
|
779
912
|
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
780
913
|
}
|
|
781
914
|
|
|
915
|
+
function isValidIdentifierId(value: unknown) {
|
|
916
|
+
if (value === undefined || value === null || value === "") return false;
|
|
917
|
+
return Number(value) >= 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function parseHidEpochDate(value: number) {
|
|
921
|
+
const date = new Date(value * 1000);
|
|
922
|
+
return new Date(
|
|
923
|
+
date.getUTCFullYear(),
|
|
924
|
+
date.getUTCMonth(),
|
|
925
|
+
date.getUTCDate(),
|
|
926
|
+
date.getUTCHours(),
|
|
927
|
+
date.getUTCMinutes(),
|
|
928
|
+
date.getUTCSeconds(),
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function formatHidEpochDate(value: number) {
|
|
933
|
+
const date = new Date(value * 1000);
|
|
934
|
+
if (Number.isNaN(date.getTime())) return "N/A";
|
|
935
|
+
|
|
936
|
+
const time = [
|
|
937
|
+
date.getUTCHours(),
|
|
938
|
+
date.getUTCMinutes(),
|
|
939
|
+
date.getUTCSeconds(),
|
|
940
|
+
].map((part) => String(part).padStart(2, "0")).join(":");
|
|
941
|
+
|
|
942
|
+
return `${time} ${date.getUTCDate()}/${date.getUTCMonth() + 1}/${date.getUTCFullYear()}`;
|
|
943
|
+
}
|
|
944
|
+
|
|
782
945
|
function openHistoryDialog(row: AccessRow) {
|
|
783
946
|
selectedHistoryRow.value = row;
|
|
784
947
|
historyDialog.value = true;
|
|
@@ -17,16 +17,18 @@
|
|
|
17
17
|
class="mb-3"
|
|
18
18
|
/>
|
|
19
19
|
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
v-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
<template v-if="mode === 'add'">
|
|
21
|
+
<div class="field-label">Base URL / IP address <span>*</span></div>
|
|
22
|
+
<v-text-field
|
|
23
|
+
v-model="draft.baseUrl"
|
|
24
|
+
aria-label="Base URL / IP address"
|
|
25
|
+
placeholder="http://192.168.x.x."
|
|
26
|
+
variant="outlined"
|
|
27
|
+
density="compact"
|
|
28
|
+
hide-details="auto"
|
|
29
|
+
class="mb-3"
|
|
30
|
+
/>
|
|
31
|
+
</template>
|
|
30
32
|
|
|
31
33
|
<div class="field-label">Device ID <span>*</span></div>
|
|
32
34
|
<v-text-field
|
|
@@ -166,12 +168,18 @@ function close() {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function submit() {
|
|
169
|
-
|
|
171
|
+
const payload: Record<string, any> = {
|
|
170
172
|
...draft,
|
|
171
173
|
monitorPath: monitorEnabled.value ? "api/notifications" : "",
|
|
172
174
|
enabled: monitorEnabled.value,
|
|
173
175
|
status: monitorEnabled.value ? "active" : "inactive",
|
|
174
|
-
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (props.mode === "edit") {
|
|
179
|
+
delete payload.baseUrl;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
emit("submit", payload);
|
|
175
183
|
}
|
|
176
184
|
</script>
|
|
177
185
|
|
|
@@ -38,31 +38,26 @@
|
|
|
38
38
|
<v-btn icon="mdi-refresh" variant="text" density="comfortable" :loading="loading" @click="loadReaders" />
|
|
39
39
|
</div>
|
|
40
40
|
|
|
41
|
-
<v-table>
|
|
41
|
+
<v-table class="reader-table">
|
|
42
42
|
<thead>
|
|
43
43
|
<tr>
|
|
44
|
-
<th>Name</th>
|
|
45
|
-
<th>Location</th>
|
|
46
|
-
<th>
|
|
47
|
-
<th>
|
|
48
|
-
<th>
|
|
49
|
-
<th>Last sync
|
|
50
|
-
<th
|
|
51
|
-
<th class="text-right"></th>
|
|
44
|
+
<th class="name-column">Name</th>
|
|
45
|
+
<th class="location-column">Location</th>
|
|
46
|
+
<th class="device-column">Device ID</th>
|
|
47
|
+
<th class="status-column">Status</th>
|
|
48
|
+
<th class="sync-at-column">Last sync at</th>
|
|
49
|
+
<th class="message-column">Last sync message</th>
|
|
50
|
+
<th class="action-column"></th>
|
|
52
51
|
</tr>
|
|
53
52
|
</thead>
|
|
54
53
|
<tbody>
|
|
55
54
|
<tr v-for="reader in filteredReaders" :key="reader._id">
|
|
56
|
-
<td
|
|
57
|
-
|
|
58
|
-
<td>
|
|
59
|
-
<a v-if="reader.baseUrl" :href="reader.baseUrl" target="_blank" rel="noopener">
|
|
60
|
-
{{ reader.baseUrl }}
|
|
61
|
-
</a>
|
|
62
|
-
<span v-else>N/A</span>
|
|
55
|
+
<td class="name-cell">
|
|
56
|
+
<span class="cell-strong">{{ reader.name || "N/A" }}</span>
|
|
63
57
|
</td>
|
|
64
|
-
<td>{{ reader.
|
|
65
|
-
<td>
|
|
58
|
+
<td class="location-cell">{{ reader.location || "N/A" }}</td>
|
|
59
|
+
<td class="device-cell">{{ reader.deviceId || "N/A" }}</td>
|
|
60
|
+
<td class="status-cell">
|
|
66
61
|
<v-chip
|
|
67
62
|
size="small"
|
|
68
63
|
variant="flat"
|
|
@@ -72,12 +67,22 @@
|
|
|
72
67
|
{{ formatReaderStatus(resolveReaderStatus(reader)) }}
|
|
73
68
|
</v-chip>
|
|
74
69
|
</td>
|
|
75
|
-
<td>{{ formatDate(reader.lastSyncAt) }}</td>
|
|
76
|
-
<td
|
|
77
|
-
|
|
70
|
+
<td class="sync-at-cell">{{ formatDate(reader.lastSyncAt) }}</td>
|
|
71
|
+
<td class="message-cell">
|
|
72
|
+
<span class="message-text" :title="reader.lastSyncMessage || 'N/A'">
|
|
73
|
+
{{ reader.lastSyncMessage || "N/A" }}
|
|
74
|
+
</span>
|
|
75
|
+
</td>
|
|
76
|
+
<td class="action-cell">
|
|
78
77
|
<v-menu>
|
|
79
78
|
<template #activator="{ props: menuProps }">
|
|
80
|
-
<v-btn
|
|
79
|
+
<v-btn
|
|
80
|
+
v-bind="menuProps"
|
|
81
|
+
icon="mdi-dots-vertical"
|
|
82
|
+
variant="text"
|
|
83
|
+
density="comfortable"
|
|
84
|
+
class="action-menu-button"
|
|
85
|
+
/>
|
|
81
86
|
</template>
|
|
82
87
|
|
|
83
88
|
<v-list density="compact" min-width="180">
|
|
@@ -112,7 +117,6 @@
|
|
|
112
117
|
<v-card-text>
|
|
113
118
|
<div class="detail-grid">
|
|
114
119
|
<span>HID reader name</span><strong>{{ selectedReader?.name || "N/A" }}</strong>
|
|
115
|
-
<span>Base URL / IP address</span><strong>{{ selectedReader?.baseUrl || "N/A" }}</strong>
|
|
116
120
|
<span>Device ID</span><strong>{{ selectedReader?.deviceId || "N/A" }}</strong>
|
|
117
121
|
<span>Location</span><strong>{{ selectedReader?.location || "N/A" }}</strong>
|
|
118
122
|
<span>Monitor Path</span><strong>{{ selectedReader?.monitorPath || "N/A" }}</strong>
|
|
@@ -140,10 +144,6 @@
|
|
|
140
144
|
<v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
|
|
141
145
|
<h3>Test Device Connection</h3>
|
|
142
146
|
<p>You are about to test the connection for {{ selectedReader?.name || "this HID Reader" }}.</p>
|
|
143
|
-
<div class="reader-action-meta">
|
|
144
|
-
<span>IP Address:</span>
|
|
145
|
-
<strong>{{ readerIpAddress }}</strong>
|
|
146
|
-
</div>
|
|
147
147
|
</v-card-text>
|
|
148
148
|
|
|
149
149
|
<v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'confirm'" class="reader-action-content">
|
|
@@ -312,7 +312,7 @@ const confirmMessage = computed(() => {
|
|
|
312
312
|
return `You're about to sync data with ${selectedReader.value?.name || "this HID Reader"}.`;
|
|
313
313
|
}
|
|
314
314
|
if (pendingAction.value === "test") {
|
|
315
|
-
return `You are about to test the connection
|
|
315
|
+
return `You are about to test the connection for ${selectedReader.value?.name || "this HID Reader"}.`;
|
|
316
316
|
}
|
|
317
317
|
return "Are you sure you want to permanently delete reader?";
|
|
318
318
|
});
|
|
@@ -325,8 +325,6 @@ const confirmActionLabel = computed(() => {
|
|
|
325
325
|
return "Confirm";
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
-
const readerIpAddress = computed(() => getReaderHost(selectedReader.value?.baseUrl));
|
|
329
|
-
|
|
330
328
|
watch(
|
|
331
329
|
() => props.site,
|
|
332
330
|
() => loadReaders(),
|
|
@@ -830,15 +828,6 @@ function countFacialData(users: Record<string, any>[]) {
|
|
|
830
828
|
return users.filter((user) => user.face || user.photo || user.image || user.imageUrl || user.faceImage).length;
|
|
831
829
|
}
|
|
832
830
|
|
|
833
|
-
function getReaderHost(value?: string) {
|
|
834
|
-
if (!value) return "N/A";
|
|
835
|
-
try {
|
|
836
|
-
return new URL(value).hostname || value;
|
|
837
|
-
} catch {
|
|
838
|
-
return value.replace(/^https?:\/\//, "").split("/")[0] || value;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
831
|
function getFriendlyHidError(error: any) {
|
|
843
832
|
const message = getHidErrorMessage(error);
|
|
844
833
|
const lowerMessage = message.toLowerCase();
|
|
@@ -863,7 +852,7 @@ function getFriendlyHidError(error: any) {
|
|
|
863
852
|
lowerMessage.includes("network error") ||
|
|
864
853
|
lowerMessage.includes("no route to host")
|
|
865
854
|
) {
|
|
866
|
-
return "Unable to connect to the HID device. Please check that the reader is online
|
|
855
|
+
return "Unable to connect to the HID device. Please check that the reader is online and reachable from the server.";
|
|
867
856
|
}
|
|
868
857
|
|
|
869
858
|
if (lowerMessage.includes("login failed") || lowerMessage.includes("session")) {
|
|
@@ -972,63 +961,119 @@ function formatReaderStatus(value?: string) {
|
|
|
972
961
|
overflow-y: hidden;
|
|
973
962
|
}
|
|
974
963
|
|
|
975
|
-
.table
|
|
964
|
+
.reader-table :deep(table) {
|
|
976
965
|
table-layout: fixed;
|
|
977
966
|
width: 100%;
|
|
978
|
-
min-width:
|
|
967
|
+
min-width: 920px;
|
|
968
|
+
border-collapse: separate;
|
|
969
|
+
border-spacing: 0;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.reader-table :deep(th) {
|
|
973
|
+
height: 44px;
|
|
974
|
+
background: #f8fafc;
|
|
975
|
+
color: #344054;
|
|
976
|
+
font-size: 12px;
|
|
977
|
+
font-weight: 700;
|
|
978
|
+
letter-spacing: 0;
|
|
979
|
+
text-transform: none;
|
|
980
|
+
border-bottom: 1px solid #e7ebef;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.reader-table :deep(td) {
|
|
984
|
+
height: 64px;
|
|
985
|
+
color: #1f2937;
|
|
986
|
+
font-size: 13px;
|
|
987
|
+
border-bottom: 1px solid #edf0f3;
|
|
988
|
+
vertical-align: middle;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.reader-table :deep(tbody tr) {
|
|
992
|
+
transition: background-color 0.15s ease;
|
|
979
993
|
}
|
|
980
994
|
|
|
981
|
-
.table
|
|
982
|
-
|
|
995
|
+
.reader-table :deep(tbody tr:hover) {
|
|
996
|
+
background: #fbfcfe;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.reader-table :deep(th),
|
|
1000
|
+
.reader-table :deep(td) {
|
|
1001
|
+
padding: 0 14px !important;
|
|
983
1002
|
white-space: nowrap;
|
|
984
1003
|
}
|
|
985
1004
|
|
|
986
|
-
.
|
|
987
|
-
.
|
|
1005
|
+
.name-column,
|
|
1006
|
+
.name-cell {
|
|
1007
|
+
width: 19%;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.location-column,
|
|
1011
|
+
.location-cell {
|
|
988
1012
|
width: 14%;
|
|
989
1013
|
}
|
|
990
1014
|
|
|
991
|
-
.
|
|
992
|
-
.
|
|
1015
|
+
.device-column,
|
|
1016
|
+
.device-cell {
|
|
1017
|
+
width: 15%;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.status-column,
|
|
1021
|
+
.status-cell {
|
|
993
1022
|
width: 12%;
|
|
994
1023
|
}
|
|
995
1024
|
|
|
996
|
-
.
|
|
997
|
-
.
|
|
998
|
-
width:
|
|
1025
|
+
.sync-at-column,
|
|
1026
|
+
.sync-at-cell {
|
|
1027
|
+
width: 15%;
|
|
999
1028
|
}
|
|
1000
1029
|
|
|
1001
|
-
.
|
|
1002
|
-
.
|
|
1003
|
-
width:
|
|
1030
|
+
.message-column,
|
|
1031
|
+
.message-cell {
|
|
1032
|
+
width: 20%;
|
|
1033
|
+
min-width: 0;
|
|
1004
1034
|
}
|
|
1005
1035
|
|
|
1006
|
-
.
|
|
1007
|
-
.
|
|
1008
|
-
width:
|
|
1036
|
+
.action-column,
|
|
1037
|
+
.action-cell {
|
|
1038
|
+
width: 5%;
|
|
1039
|
+
text-align: right;
|
|
1009
1040
|
}
|
|
1010
1041
|
|
|
1011
|
-
.
|
|
1012
|
-
|
|
1013
|
-
|
|
1042
|
+
.cell-strong {
|
|
1043
|
+
display: block;
|
|
1044
|
+
overflow: hidden;
|
|
1045
|
+
color: #111827;
|
|
1046
|
+
font-weight: 600;
|
|
1047
|
+
text-overflow: ellipsis;
|
|
1014
1048
|
}
|
|
1015
1049
|
|
|
1016
|
-
.
|
|
1017
|
-
.
|
|
1018
|
-
|
|
1050
|
+
.location-cell,
|
|
1051
|
+
.device-cell,
|
|
1052
|
+
.sync-at-cell {
|
|
1053
|
+
overflow: hidden;
|
|
1054
|
+
color: #475467;
|
|
1055
|
+
text-overflow: ellipsis;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.message-text {
|
|
1059
|
+
display: block;
|
|
1060
|
+
max-width: 100%;
|
|
1019
1061
|
overflow: hidden;
|
|
1062
|
+
color: #475467;
|
|
1063
|
+
line-height: 1.35;
|
|
1020
1064
|
text-overflow: ellipsis;
|
|
1021
1065
|
}
|
|
1022
1066
|
|
|
1023
|
-
.
|
|
1024
|
-
|
|
1025
|
-
width: 6%;
|
|
1067
|
+
.action-menu-button {
|
|
1068
|
+
color: #344054;
|
|
1026
1069
|
}
|
|
1027
1070
|
|
|
1028
1071
|
.table-refresh {
|
|
1029
1072
|
display: flex;
|
|
1030
|
-
|
|
1031
|
-
|
|
1073
|
+
justify-content: flex-start;
|
|
1074
|
+
padding: 10px 14px;
|
|
1075
|
+
border-bottom: 1px solid #e7ebef;
|
|
1076
|
+
background: #ffffff;
|
|
1032
1077
|
}
|
|
1033
1078
|
|
|
1034
1079
|
.empty-state {
|
|
@@ -1116,18 +1161,6 @@ function formatReaderStatus(value?: string) {
|
|
|
1116
1161
|
line-height: 1.45;
|
|
1117
1162
|
}
|
|
1118
1163
|
|
|
1119
|
-
.reader-action-meta {
|
|
1120
|
-
display: inline-flex;
|
|
1121
|
-
gap: 8px;
|
|
1122
|
-
align-items: center;
|
|
1123
|
-
color: #344054;
|
|
1124
|
-
font-size: 13px;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
.reader-action-meta strong {
|
|
1128
|
-
font-weight: 700;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
1164
|
.reader-sync-review {
|
|
1132
1165
|
justify-items: stretch;
|
|
1133
1166
|
text-align: left;
|