@7365admin1/layer-common 3.2.2-staging.107 → 3.2.2-staging.109
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/components/EntryPassInformation.vue +29 -1
- package/components/HidAccessLogDashboard.vue +42 -29
- package/components/HidIntercomManagement.vue +43 -9
- package/components/HidQrCodeConfiguration.vue +1 -2
- package/components/HidReaderForm.vue +119 -11
- package/components/HidReaderManagement.vue +1 -0
- package/components/HidUserEnrollment.vue +21 -2
- package/components/MemberInformation.vue +9 -0
- package/components/VisitorForm.vue +42 -1
- package/composables/useHidAmico.ts +13 -0
- package/package.json +1 -1
|
@@ -166,8 +166,28 @@
|
|
|
166
166
|
</template>
|
|
167
167
|
|
|
168
168
|
<template v-if="passType === 'HID_QR'">
|
|
169
|
+
<v-card variant="tonal" color="primary" class="mt-3 pa-3" rounded="lg">
|
|
170
|
+
<div class="d-flex align-center ga-3">
|
|
171
|
+
<v-icon icon="mdi-door-open" size="28" />
|
|
172
|
+
<div>
|
|
173
|
+
<div class="text-caption text-medium-emphasis">Access Reader</div>
|
|
174
|
+
<div class="text-subtitle-2 font-weight-bold">{{ hidReaderName || "Reader not configured" }}</div>
|
|
175
|
+
<div class="text-caption mt-1">Access Portal: {{ hidPortalName || "Portal not configured" }}</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</v-card>
|
|
179
|
+
<v-alert
|
|
180
|
+
v-if="!hidPortalName"
|
|
181
|
+
type="warning"
|
|
182
|
+
variant="tonal"
|
|
183
|
+
density="compact"
|
|
184
|
+
class="mt-3"
|
|
185
|
+
icon="mdi-alert"
|
|
186
|
+
>
|
|
187
|
+
Reconnect this HID reader and select a portal before generating a visitor QR code.
|
|
188
|
+
</v-alert>
|
|
169
189
|
<v-alert
|
|
170
|
-
v-if="!hidQrPrinterConfigured"
|
|
190
|
+
v-else-if="!hidQrPrinterConfigured"
|
|
171
191
|
type="info"
|
|
172
192
|
variant="tonal"
|
|
173
193
|
density="compact"
|
|
@@ -283,6 +303,14 @@ const props = defineProps({
|
|
|
283
303
|
type: Boolean,
|
|
284
304
|
default: false,
|
|
285
305
|
},
|
|
306
|
+
hidReaderName: {
|
|
307
|
+
type: String,
|
|
308
|
+
default: "",
|
|
309
|
+
},
|
|
310
|
+
hidPortalName: {
|
|
311
|
+
type: String,
|
|
312
|
+
default: "",
|
|
313
|
+
},
|
|
286
314
|
excludedCardIds: {
|
|
287
315
|
type: Array as PropType<string[]>,
|
|
288
316
|
default: () => [],
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</v-btn>
|
|
13
13
|
|
|
14
14
|
<v-select
|
|
15
|
-
v-if="readers.length
|
|
15
|
+
v-if="readers.length"
|
|
16
16
|
v-model="selectedReaderId"
|
|
17
17
|
:items="readerOptions"
|
|
18
18
|
item-title="title"
|
|
@@ -412,7 +412,7 @@ const accessMethodOptions = ["All", "Facial", "QR Code", "ID and Password", "PIN
|
|
|
412
412
|
|
|
413
413
|
const readerOptions = computed(() =>
|
|
414
414
|
readers.value.map((reader) => ({
|
|
415
|
-
title: reader.name || reader.deviceId || reader._id
|
|
415
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
416
416
|
value: reader._id,
|
|
417
417
|
})),
|
|
418
418
|
);
|
|
@@ -582,7 +582,6 @@ async function loadDashboard() {
|
|
|
582
582
|
loading.value = true;
|
|
583
583
|
resetDashboardData();
|
|
584
584
|
try {
|
|
585
|
-
const isAdministratorTab = activeTab.value === "administrator";
|
|
586
585
|
const isVisitorTab = activeTab.value === "visitor";
|
|
587
586
|
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
588
587
|
loadDashboardSource(
|
|
@@ -603,9 +602,7 @@ async function loadDashboard() {
|
|
|
603
602
|
),
|
|
604
603
|
loadDashboardSource(loadLiveAccessLogs(selectedReaderId.value), [], "live access logs"),
|
|
605
604
|
loadDashboardSource(loadReaderConfiguration(selectedReaderId.value), {}, "reader configuration"),
|
|
606
|
-
|
|
607
|
-
? loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles")
|
|
608
|
-
: Promise.resolve(null),
|
|
605
|
+
loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles"),
|
|
609
606
|
isVisitorTab && props.org
|
|
610
607
|
? loadDashboardSource(
|
|
611
608
|
getVisitors({
|
|
@@ -632,14 +629,9 @@ async function loadDashboard() {
|
|
|
632
629
|
);
|
|
633
630
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
634
631
|
allIdentities.value = mergedIdentities;
|
|
635
|
-
identities.value =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
639
|
-
})
|
|
640
|
-
: activeTab.value === "visitor"
|
|
641
|
-
? mergedIdentities.filter(isVisitorIdentity)
|
|
642
|
-
: mergedIdentities.filter((identity: Record<string, any>) => !isVisitorIdentity(identity));
|
|
632
|
+
identities.value = mergedIdentities.filter(
|
|
633
|
+
(identity: Record<string, unknown>) => classifyIdentity(identity) === activeTab.value,
|
|
634
|
+
);
|
|
643
635
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
644
636
|
liveAccessLogs.value = hidAccessLogs;
|
|
645
637
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
@@ -781,8 +773,10 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
781
773
|
registration: user.registration,
|
|
782
774
|
cardNo: user.card_value || user.cardNo || "",
|
|
783
775
|
name: user.name,
|
|
776
|
+
hidUserTypeId: user.user_type_id,
|
|
784
777
|
metadata: {
|
|
785
778
|
name: user.name,
|
|
779
|
+
hidUserTypeId: user.user_type_id,
|
|
786
780
|
imageTimestamp: user.image_timestamp ? String(user.image_timestamp) : "",
|
|
787
781
|
hidReaderSource: true,
|
|
788
782
|
},
|
|
@@ -835,21 +829,22 @@ function resetDashboardData() {
|
|
|
835
829
|
|
|
836
830
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
837
831
|
const identity = findIdentityByKey(row.identityKey);
|
|
838
|
-
|
|
839
|
-
if (activeTab.value === "visitor") {
|
|
840
|
-
return isVisitor;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (activeTab.value === "administrator") {
|
|
844
|
-
const hidUserId = toHidNumericId(identity?.hidUserId ?? row.identityKey.split("|")[0]);
|
|
845
|
-
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return !isVisitor;
|
|
832
|
+
return classifyIdentity(identity, row) === activeTab.value;
|
|
849
833
|
}
|
|
850
834
|
|
|
851
835
|
function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
852
|
-
return Boolean(identity && (
|
|
836
|
+
return Boolean(identity && (
|
|
837
|
+
identity.type === "visitor"
|
|
838
|
+
|| identity.visitor
|
|
839
|
+
|| Number(identity.hidUserTypeId ?? (identity.metadata as Record<string, unknown> | undefined)?.hidUserTypeId) === 1
|
|
840
|
+
));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function classifyIdentity(identity?: Record<string, unknown>, row?: AccessRow): AccessTab {
|
|
844
|
+
const hidUserId = toHidNumericId(identity?.hidUserId ?? row?.identityKey.split("|")[0]);
|
|
845
|
+
if (hidUserId && administratorUserIds.value.has(hidUserId)) return "administrator";
|
|
846
|
+
if (row?.identityType === "visitor" || row?.visitorId || isVisitorIdentity(identity)) return "visitor";
|
|
847
|
+
return "resident";
|
|
853
848
|
}
|
|
854
849
|
|
|
855
850
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -1200,7 +1195,18 @@ function getRawAccessLogs(event: Record<string, any>) {
|
|
|
1200
1195
|
payload.access_logs ||
|
|
1201
1196
|
payload.accessLogs;
|
|
1202
1197
|
|
|
1203
|
-
|
|
1198
|
+
if (Array.isArray(accessLogs)) return accessLogs;
|
|
1199
|
+
|
|
1200
|
+
const objectChanges: unknown = payload.object_changes || result.object_changes || [];
|
|
1201
|
+
return Array.isArray(objectChanges)
|
|
1202
|
+
? objectChanges.flatMap((change: unknown) => {
|
|
1203
|
+
const record = toUnknownRecord(change);
|
|
1204
|
+
const values = toUnknownRecord(record.values);
|
|
1205
|
+
return record.object === "access_logs" && record.type !== "deleted" && Object.keys(values).length
|
|
1206
|
+
? [values]
|
|
1207
|
+
: [];
|
|
1208
|
+
})
|
|
1209
|
+
: [];
|
|
1204
1210
|
}
|
|
1205
1211
|
|
|
1206
1212
|
function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
|
|
@@ -1212,7 +1218,9 @@ function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, an
|
|
|
1212
1218
|
return {
|
|
1213
1219
|
key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
|
|
1214
1220
|
identityKey,
|
|
1215
|
-
identityType:
|
|
1221
|
+
identityType: identity
|
|
1222
|
+
? classifyIdentity(identity)
|
|
1223
|
+
: String(eventIdentity.type || "").toLowerCase() || undefined,
|
|
1216
1224
|
visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
|
|
1217
1225
|
name: identity ? getName(identity) : getRawLogName(rawLog, event),
|
|
1218
1226
|
facialData: identity ? getFacialData(identity) : "N/A",
|
|
@@ -1276,7 +1284,12 @@ function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<str
|
|
|
1276
1284
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1277
1285
|
if (eventIdentityId) {
|
|
1278
1286
|
const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
1279
|
-
|
|
1287
|
+
const rawUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId;
|
|
1288
|
+
if (byId && (
|
|
1289
|
+
rawUserId === undefined
|
|
1290
|
+
|| rawUserId === null
|
|
1291
|
+
|| String(byId.hidUserId) === String(rawUserId)
|
|
1292
|
+
)) return byId;
|
|
1280
1293
|
}
|
|
1281
1294
|
|
|
1282
1295
|
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
@@ -24,6 +24,19 @@
|
|
|
24
24
|
{{ webPhone.callState.value === 'incoming' ? 'Incoming Call' : 'Web Call' }}
|
|
25
25
|
</v-btn>
|
|
26
26
|
|
|
27
|
+
<v-select
|
|
28
|
+
v-if="readers.length"
|
|
29
|
+
v-model="selectedReaderId"
|
|
30
|
+
:items="readerOptions"
|
|
31
|
+
item-title="title"
|
|
32
|
+
item-value="value"
|
|
33
|
+
variant="outlined"
|
|
34
|
+
density="compact"
|
|
35
|
+
hide-details
|
|
36
|
+
class="reader-input"
|
|
37
|
+
@update:model-value="page = 1"
|
|
38
|
+
/>
|
|
39
|
+
|
|
27
40
|
<div class="toolbar-spacer" />
|
|
28
41
|
|
|
29
42
|
<v-text-field
|
|
@@ -702,6 +715,7 @@ const {
|
|
|
702
715
|
|
|
703
716
|
const activeTab = ref<IntercomTab>("intercom");
|
|
704
717
|
const readers = ref<Record<string, any>[]>([]);
|
|
718
|
+
const selectedReaderId = ref("");
|
|
705
719
|
const intercomRows = ref<IntercomRow[]>([]);
|
|
706
720
|
const contacts = ref<ContactRow[]>([]);
|
|
707
721
|
const loading = ref(false);
|
|
@@ -821,6 +835,11 @@ const contactForm = reactive({
|
|
|
821
835
|
serverAddress: "",
|
|
822
836
|
});
|
|
823
837
|
|
|
838
|
+
const readerOptions = computed(() => readers.value.map((reader) => ({
|
|
839
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
840
|
+
value: reader._id,
|
|
841
|
+
})));
|
|
842
|
+
|
|
824
843
|
const visibleRows = computed(() => activeTab.value === "intercom" ? filteredIntercomRows.value : filteredContacts.value);
|
|
825
844
|
const pages = computed(() => Math.max(1, Math.ceil(visibleRows.value.length / limit)));
|
|
826
845
|
const pageRange = computed(() => {
|
|
@@ -834,6 +853,7 @@ const pageRange = computed(() => {
|
|
|
834
853
|
const filteredIntercomRows = computed(() => {
|
|
835
854
|
const query = search.value.trim().toLowerCase();
|
|
836
855
|
return intercomRows.value.filter((row) => {
|
|
856
|
+
const matchesReader = !selectedReaderId.value || row.reader._id === selectedReaderId.value;
|
|
837
857
|
const matchesSearch = !query || [
|
|
838
858
|
row.name,
|
|
839
859
|
row.location,
|
|
@@ -843,18 +863,22 @@ const filteredIntercomRows = computed(() => {
|
|
|
843
863
|
row.status,
|
|
844
864
|
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
845
865
|
const matchesStatus = statusFilter.value === "Status" || row.status === statusFilter.value;
|
|
846
|
-
return matchesSearch && matchesStatus;
|
|
866
|
+
return matchesReader && matchesSearch && matchesStatus;
|
|
847
867
|
});
|
|
848
868
|
});
|
|
849
869
|
|
|
850
870
|
const filteredContacts = computed(() => {
|
|
851
871
|
const query = search.value.trim().toLowerCase();
|
|
852
|
-
return contacts.value.filter((contact) =>
|
|
853
|
-
contact.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
872
|
+
return contacts.value.filter((contact) => {
|
|
873
|
+
const matchesReader = !selectedReaderId.value || contact.readerId === selectedReaderId.value;
|
|
874
|
+
const matchesSearch = !query || [
|
|
875
|
+
contact.name,
|
|
876
|
+
contact.location,
|
|
877
|
+
contact.serverAddress,
|
|
878
|
+
contact.dialCode,
|
|
879
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
880
|
+
return matchesReader && matchesSearch;
|
|
881
|
+
});
|
|
858
882
|
});
|
|
859
883
|
|
|
860
884
|
const paginatedIntercomRows = computed(() => paginate(filteredIntercomRows.value));
|
|
@@ -977,6 +1001,9 @@ async function reload() {
|
|
|
977
1001
|
try {
|
|
978
1002
|
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
979
1003
|
readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
1004
|
+
if (!readers.value.some((reader) => reader._id === selectedReaderId.value)) {
|
|
1005
|
+
selectedReaderId.value = readers.value[0]?._id || "";
|
|
1006
|
+
}
|
|
980
1007
|
await loadIntercomRows();
|
|
981
1008
|
await loadContacts();
|
|
982
1009
|
} catch (error: any) {
|
|
@@ -1328,9 +1355,10 @@ function toggleWebMute() {
|
|
|
1328
1355
|
function openContactDialog(contact?: ContactRow) {
|
|
1329
1356
|
selectedContact.value = contact || null;
|
|
1330
1357
|
contactForm.id = contact?.id;
|
|
1331
|
-
contactForm.readerId = contact?.readerId || readers.value[0]?._id || "";
|
|
1358
|
+
contactForm.readerId = contact?.readerId || selectedReaderId.value || readers.value[0]?._id || "";
|
|
1332
1359
|
contactForm.name = contact?.name === "N/A" ? "" : contact?.name || "";
|
|
1333
|
-
|
|
1360
|
+
const selectedReader = readers.value.find((reader) => reader._id === contactForm.readerId);
|
|
1361
|
+
contactForm.location = contact?.location === "N/A" ? "" : contact?.location || selectedReader?.location || "";
|
|
1334
1362
|
contactForm.serverAddress = contact?.serverAddress === "N/A" ? "" : contact?.serverAddress || "";
|
|
1335
1363
|
contactDialog.value = true;
|
|
1336
1364
|
}
|
|
@@ -1695,6 +1723,11 @@ export default {
|
|
|
1695
1723
|
max-width: 220px;
|
|
1696
1724
|
}
|
|
1697
1725
|
|
|
1726
|
+
.reader-input {
|
|
1727
|
+
width: 300px;
|
|
1728
|
+
max-width: 36vw;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1698
1731
|
.status-input {
|
|
1699
1732
|
max-width: 160px;
|
|
1700
1733
|
}
|
|
@@ -2673,6 +2706,7 @@ export default {
|
|
|
2673
2706
|
}
|
|
2674
2707
|
|
|
2675
2708
|
.top-placeholder,
|
|
2709
|
+
.reader-input,
|
|
2676
2710
|
.search-input,
|
|
2677
2711
|
.status-input {
|
|
2678
2712
|
width: 100%;
|
|
@@ -501,7 +501,6 @@ const identificationMethodOptions: Array<{
|
|
|
501
501
|
{ key: "qrCode", label: "QR Code" },
|
|
502
502
|
{ key: "idPassword", label: "ID Password" },
|
|
503
503
|
{ key: "pin", label: "PIN" },
|
|
504
|
-
{ key: "bluetooth", label: "Bluetooth" },
|
|
505
504
|
];
|
|
506
505
|
const accessPermissionOptions: Array<{
|
|
507
506
|
category: THidPermissionCategory;
|
|
@@ -590,7 +589,7 @@ async function loadConfig() {
|
|
|
590
589
|
qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
|
|
591
590
|
idPassword: configuredMethods?.idPassword ?? true,
|
|
592
591
|
pin: configuredMethods?.pin ?? false,
|
|
593
|
-
bluetooth:
|
|
592
|
+
bluetooth: false,
|
|
594
593
|
},
|
|
595
594
|
printer: {
|
|
596
595
|
vendorId: String(config.printer?.vendorId || ""),
|
|
@@ -28,17 +28,6 @@
|
|
|
28
28
|
class="mb-3"
|
|
29
29
|
/>
|
|
30
30
|
|
|
31
|
-
<div class="field-label">Device ID <span>*</span></div>
|
|
32
|
-
<v-text-field
|
|
33
|
-
v-model="draft.deviceId"
|
|
34
|
-
aria-label="Device ID"
|
|
35
|
-
placeholder="Enter device id"
|
|
36
|
-
variant="outlined"
|
|
37
|
-
density="compact"
|
|
38
|
-
hide-details="auto"
|
|
39
|
-
class="mb-3"
|
|
40
|
-
/>
|
|
41
|
-
|
|
42
31
|
<div class="field-label">Location <span>*</span></div>
|
|
43
32
|
<v-text-field
|
|
44
33
|
v-model="draft.location"
|
|
@@ -79,6 +68,49 @@
|
|
|
79
68
|
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
|
80
69
|
@click:append-inner="showPassword = !showPassword"
|
|
81
70
|
/>
|
|
71
|
+
|
|
72
|
+
<v-btn
|
|
73
|
+
v-if="mode === 'add'"
|
|
74
|
+
block
|
|
75
|
+
variant="tonal"
|
|
76
|
+
color="primary"
|
|
77
|
+
prepend-icon="mdi-lan-connect"
|
|
78
|
+
class="text-none mt-3"
|
|
79
|
+
:loading="discovering"
|
|
80
|
+
:disabled="!canDiscover"
|
|
81
|
+
@click="connectReader"
|
|
82
|
+
>
|
|
83
|
+
Connect & discover reader
|
|
84
|
+
</v-btn>
|
|
85
|
+
<v-alert v-if="discoveryError" type="error" density="compact" variant="tonal" class="mt-3">
|
|
86
|
+
{{ discoveryError }}
|
|
87
|
+
</v-alert>
|
|
88
|
+
|
|
89
|
+
<div class="field-label mt-3">Device ID <span>*</span></div>
|
|
90
|
+
<v-text-field
|
|
91
|
+
v-model="draft.deviceId"
|
|
92
|
+
aria-label="Device ID"
|
|
93
|
+
placeholder="Connect to detect device ID"
|
|
94
|
+
variant="outlined"
|
|
95
|
+
density="compact"
|
|
96
|
+
hide-details="auto"
|
|
97
|
+
class="mb-3"
|
|
98
|
+
readonly
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<div class="field-label">HID Portal <span>*</span></div>
|
|
102
|
+
<v-select
|
|
103
|
+
v-model="draft.portalId"
|
|
104
|
+
:items="portals"
|
|
105
|
+
item-title="name"
|
|
106
|
+
item-value="id"
|
|
107
|
+
aria-label="HID Portal"
|
|
108
|
+
placeholder="Connect to load portals"
|
|
109
|
+
variant="outlined"
|
|
110
|
+
density="compact"
|
|
111
|
+
hide-details="auto"
|
|
112
|
+
:readonly="mode === 'edit'"
|
|
113
|
+
/>
|
|
82
114
|
</v-card-text>
|
|
83
115
|
|
|
84
116
|
<v-card-actions class="form-actions pa-0">
|
|
@@ -95,6 +127,7 @@
|
|
|
95
127
|
variant="flat"
|
|
96
128
|
height="48"
|
|
97
129
|
:loading="loading"
|
|
130
|
+
:disabled="mode === 'add' && (!draft.deviceId || !draft.portalId)"
|
|
98
131
|
@click="submit"
|
|
99
132
|
>
|
|
100
133
|
Submit
|
|
@@ -135,11 +168,18 @@ const dialogModel = computed({
|
|
|
135
168
|
});
|
|
136
169
|
|
|
137
170
|
const showPassword = ref(false);
|
|
171
|
+
const discovering = ref(false);
|
|
172
|
+
const discoveryError = ref("");
|
|
173
|
+
const portals = ref<Array<{ id: number; name: string }>>([]);
|
|
174
|
+
const discoveredConnection = ref("");
|
|
175
|
+
const { discoverReader } = useHidAmico();
|
|
138
176
|
const monitorEnabled = ref(true);
|
|
139
177
|
const draft = reactive({
|
|
140
178
|
name: "",
|
|
141
179
|
baseUrl: "",
|
|
142
180
|
deviceId: "",
|
|
181
|
+
portalId: null as number | null,
|
|
182
|
+
portalName: "",
|
|
143
183
|
location: "",
|
|
144
184
|
username: "",
|
|
145
185
|
password: "",
|
|
@@ -153,6 +193,13 @@ watch(
|
|
|
153
193
|
draft.name = props.reader?.name ?? "";
|
|
154
194
|
draft.baseUrl = props.reader?.baseUrl ?? "";
|
|
155
195
|
draft.deviceId = props.reader?.deviceId ?? "";
|
|
196
|
+
draft.portalId = props.reader?.portalId ?? null;
|
|
197
|
+
draft.portalName = props.reader?.portalName ?? "";
|
|
198
|
+
portals.value = draft.portalId
|
|
199
|
+
? [{ id: draft.portalId, name: draft.portalName || `Portal ${draft.portalId}` }]
|
|
200
|
+
: [];
|
|
201
|
+
discoveryError.value = "";
|
|
202
|
+
discoveredConnection.value = "";
|
|
156
203
|
draft.location = props.reader?.location ?? "";
|
|
157
204
|
draft.username = props.reader?.username ?? "";
|
|
158
205
|
draft.password = props.reader?.password ?? "";
|
|
@@ -165,9 +212,70 @@ function close() {
|
|
|
165
212
|
emit("update:modelValue", false);
|
|
166
213
|
}
|
|
167
214
|
|
|
215
|
+
const canDiscover = computed(() => Boolean(
|
|
216
|
+
draft.baseUrl.trim() && draft.username.trim() && draft.password,
|
|
217
|
+
));
|
|
218
|
+
|
|
219
|
+
watch(
|
|
220
|
+
() => [draft.baseUrl, draft.username, draft.password],
|
|
221
|
+
() => {
|
|
222
|
+
if (props.mode !== "add" || !discoveredConnection.value) return;
|
|
223
|
+
const current = [draft.baseUrl.trim(), draft.username.trim(), draft.password].join("\n");
|
|
224
|
+
if (current !== discoveredConnection.value) {
|
|
225
|
+
draft.deviceId = "";
|
|
226
|
+
draft.portalId = null;
|
|
227
|
+
portals.value = [];
|
|
228
|
+
discoveredConnection.value = "";
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
async function connectReader() {
|
|
234
|
+
discovering.value = true;
|
|
235
|
+
discoveryError.value = "";
|
|
236
|
+
try {
|
|
237
|
+
const response = await discoverReader({
|
|
238
|
+
baseUrl: draft.baseUrl.trim(),
|
|
239
|
+
username: draft.username.trim(),
|
|
240
|
+
password: draft.password,
|
|
241
|
+
});
|
|
242
|
+
const nestedData = response?.data;
|
|
243
|
+
const data = typeof nestedData === "object" && nestedData !== null
|
|
244
|
+
? nestedData as Record<string, unknown>
|
|
245
|
+
: response;
|
|
246
|
+
draft.deviceId = String(data?.deviceId || "");
|
|
247
|
+
portals.value = Array.isArray(data?.portals)
|
|
248
|
+
? data.portals.flatMap((portal: unknown) => {
|
|
249
|
+
if (typeof portal !== "object" || portal === null) return [];
|
|
250
|
+
const item = portal as Record<string, unknown>;
|
|
251
|
+
const id = Number(item.id);
|
|
252
|
+
const name = String(item.name || "").trim();
|
|
253
|
+
return Number.isSafeInteger(id) && id > 0 && name ? [{ id, name }] : [];
|
|
254
|
+
})
|
|
255
|
+
: [];
|
|
256
|
+
draft.portalId = portals.value.length === 1 ? portals.value[0].id : null;
|
|
257
|
+
discoveredConnection.value = [draft.baseUrl.trim(), draft.username.trim(), draft.password].join("\n");
|
|
258
|
+
} catch (error: unknown) {
|
|
259
|
+
draft.deviceId = "";
|
|
260
|
+
draft.portalId = null;
|
|
261
|
+
portals.value = [];
|
|
262
|
+
const record = typeof error === "object" && error !== null ? error as Record<string, unknown> : {};
|
|
263
|
+
const errorData = typeof record.data === "object" && record.data !== null
|
|
264
|
+
? record.data as Record<string, unknown>
|
|
265
|
+
: {};
|
|
266
|
+
discoveryError.value = String(
|
|
267
|
+
errorData.message || record.message || "Unable to connect to the HID reader.",
|
|
268
|
+
);
|
|
269
|
+
} finally {
|
|
270
|
+
discovering.value = false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
168
274
|
function submit() {
|
|
275
|
+
const portal = portals.value.find((item) => item.id === Number(draft.portalId));
|
|
169
276
|
const payload: Record<string, any> = {
|
|
170
277
|
...draft,
|
|
278
|
+
portalName: portal?.name || draft.portalName,
|
|
171
279
|
monitorPath: monitorEnabled.value ? "api/notifications" : "",
|
|
172
280
|
enabled: monitorEnabled.value,
|
|
173
281
|
status: monitorEnabled.value ? "active" : "inactive",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
<span>HID reader name</span><strong>{{ selectedReader?.name || "N/A" }}</strong>
|
|
120
120
|
<span>Device ID</span><strong>{{ selectedReader?.deviceId || "N/A" }}</strong>
|
|
121
121
|
<span>Location</span><strong>{{ selectedReader?.location || "N/A" }}</strong>
|
|
122
|
+
<span>HID Portal</span><strong>{{ selectedReader?.portalName || selectedReader?.portalId || "N/A" }}</strong>
|
|
122
123
|
<span>Monitor Path</span><strong>{{ selectedReader?.monitorPath || "N/A" }}</strong>
|
|
123
124
|
<span>Status</span><strong>{{ formatReaderStatus(resolveReaderStatus(selectedReader)) }}</strong>
|
|
124
125
|
<span>Username</span><strong>{{ selectedReader?.username || "N/A" }}</strong>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</v-btn>
|
|
13
13
|
|
|
14
14
|
<v-select
|
|
15
|
-
v-if="readers.length
|
|
15
|
+
v-if="readers.length"
|
|
16
16
|
v-model="selectedReaderId"
|
|
17
17
|
:items="readerOptions"
|
|
18
18
|
item-title="title"
|
|
@@ -48,6 +48,18 @@
|
|
|
48
48
|
/>
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
|
+
<v-alert
|
|
52
|
+
v-if="selectedReader"
|
|
53
|
+
:type="selectedReader.portalId && selectedReader.portalName ? 'info' : 'warning'"
|
|
54
|
+
variant="tonal"
|
|
55
|
+
density="compact"
|
|
56
|
+
class="mb-4"
|
|
57
|
+
icon="mdi-door-open"
|
|
58
|
+
>
|
|
59
|
+
Access Reader: {{ selectedReader.name || "HID Reader" }} · Access Portal:
|
|
60
|
+
{{ selectedReader.portalName || "Not configured" }}
|
|
61
|
+
</v-alert>
|
|
62
|
+
|
|
51
63
|
<v-card flat border class="table-card">
|
|
52
64
|
<div class="table-refresh">
|
|
53
65
|
<v-btn
|
|
@@ -429,10 +441,13 @@ const statusOptions = ["Status", "Mapped", "Unmapped"];
|
|
|
429
441
|
|
|
430
442
|
const readerOptions = computed(() =>
|
|
431
443
|
readers.value.map((reader) => ({
|
|
432
|
-
title: reader.name || reader.deviceId || reader._id
|
|
444
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
433
445
|
value: reader._id,
|
|
434
446
|
}))
|
|
435
447
|
);
|
|
448
|
+
const selectedReader = computed(() =>
|
|
449
|
+
readers.value.find((reader) => reader._id === (selectedReaderId.value || form.reader)) || null,
|
|
450
|
+
);
|
|
436
451
|
|
|
437
452
|
const pageRange = computed(() => {
|
|
438
453
|
if (serverPageRange.value) return serverPageRange.value;
|
|
@@ -689,6 +704,10 @@ async function saveUser() {
|
|
|
689
704
|
showToast("Access PIN must contain numbers only.", "error");
|
|
690
705
|
return;
|
|
691
706
|
}
|
|
707
|
+
if (selectedPhotoFile.value && (!selectedReader.value?.portalId || !selectedReader.value?.portalName)) {
|
|
708
|
+
showToast("Reconnect this HID reader and select a portal before enrolling facial recognition.", "error");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
692
711
|
|
|
693
712
|
saving.value = true;
|
|
694
713
|
try {
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
v-model:cards="memberPassCards" :settings="props.settings" :loading="props.settingsLoading"
|
|
76
76
|
:site-id="props.site" :unit-id="props.unitId" :visitor-type="props.type"
|
|
77
77
|
:hid-qr-code-enabled="props.hidQrCodeEnabled" :hid-qr-printer-configured="props.hidQrPrinterConfigured"
|
|
78
|
+
:hid-reader-name="props.hidReaderName" :hid-portal-name="props.hidPortalName"
|
|
78
79
|
:excluded-card-ids="props.selectedNfcCards.map((c) => c._id)" />
|
|
79
80
|
</v-col>
|
|
80
81
|
|
|
@@ -192,6 +193,14 @@ const props = defineProps({
|
|
|
192
193
|
type: Boolean,
|
|
193
194
|
default: false,
|
|
194
195
|
},
|
|
196
|
+
hidReaderName: {
|
|
197
|
+
type: String,
|
|
198
|
+
default: "",
|
|
199
|
+
},
|
|
200
|
+
hidPortalName: {
|
|
201
|
+
type: String,
|
|
202
|
+
default: "",
|
|
203
|
+
},
|
|
195
204
|
selectedNfcCards: {
|
|
196
205
|
type: Array as PropType<{ _id: string; cardNo: string }[]>,
|
|
197
206
|
default: () => [],
|
|
@@ -260,6 +260,8 @@
|
|
|
260
260
|
:loading="entryPassSettingsPending || siteDataPending" :site-id="prop.site"
|
|
261
261
|
:unit-id="visitor.unit || null" :visitor-type="prop.type" :hid-qr-code-enabled="hidQrCodePassAllowed"
|
|
262
262
|
:hid-qr-printer-configured="hasHidQrPrinterConfig"
|
|
263
|
+
:hid-reader-name="hidQrReader?.name || ''"
|
|
264
|
+
:hid-portal-name="hidQrReader?.portalName || ''"
|
|
263
265
|
@update:available-qr-count="(val) => availableQrCount = val" />
|
|
264
266
|
</v-col>
|
|
265
267
|
|
|
@@ -269,6 +271,7 @@
|
|
|
269
271
|
:settings="entryPassSettings" :settings-loading="entryPassSettingsPending || siteDataPending"
|
|
270
272
|
:unit-id="visitor.unit || null" :hid-qr-code-enabled="hidQrCodePassAllowed"
|
|
271
273
|
:hid-qr-printer-configured="hasHidQrPrinterConfig"
|
|
274
|
+
:hid-reader-name="hidQrReader?.name || ''" :hid-portal-name="hidQrReader?.portalName || ''"
|
|
272
275
|
:selected-nfc-cards="passType === 'NFC' ? passCards : []" />
|
|
273
276
|
</v-col>
|
|
274
277
|
|
|
@@ -533,6 +536,9 @@
|
|
|
533
536
|
</v-btn>
|
|
534
537
|
|
|
535
538
|
<div class="text-h6 font-weight-bold">{{ visitor.name }}</div>
|
|
539
|
+
<div v-if="hidQrReader?.portalName" class="text-caption text-medium-emphasis mt-1">
|
|
540
|
+
{{ hidQrReader.name || "HID Reader" }} · {{ hidQrReader.portalName }}
|
|
541
|
+
</div>
|
|
536
542
|
<div v-if="hidQrPreview.expiresAt" class="text-caption text-medium-emphasis mt-1">
|
|
537
543
|
Valid until {{ formatHidQrExpiry(hidQrPreview.expiresAt) }}
|
|
538
544
|
</div>
|
|
@@ -668,7 +674,7 @@ const {
|
|
|
668
674
|
const { getBySiteId: getEntryPassSettingsBySiteId } =
|
|
669
675
|
useSiteEntryPassSettings();
|
|
670
676
|
const { createVisitorPass, signQr } = useAccessManagement();
|
|
671
|
-
const { issueVisitorQr } = useHidAmico();
|
|
677
|
+
const { getReaders, issueVisitorQr } = useHidAmico();
|
|
672
678
|
const { testConnection } = useWebUsb();
|
|
673
679
|
const {
|
|
674
680
|
findPersonByNRIC,
|
|
@@ -877,6 +883,35 @@ const hidQrValidityMinutes = computed(() => {
|
|
|
877
883
|
});
|
|
878
884
|
|
|
879
885
|
const hidQrReaderId = computed(() => String(hidQrCodePassConfig.value?.readerId || ""));
|
|
886
|
+
type HidQrReaderSummary = {
|
|
887
|
+
_id: string;
|
|
888
|
+
name?: string;
|
|
889
|
+
portalId?: number;
|
|
890
|
+
portalName?: string;
|
|
891
|
+
};
|
|
892
|
+
const hidQrReader = ref<HidQrReaderSummary | null>(null);
|
|
893
|
+
|
|
894
|
+
watch(
|
|
895
|
+
[hidQrReaderId, () => prop.site],
|
|
896
|
+
async ([readerId, site]) => {
|
|
897
|
+
hidQrReader.value = null;
|
|
898
|
+
if (!readerId || !site) return;
|
|
899
|
+
try {
|
|
900
|
+
const response = await getReaders({ site, page: 1, limit: 100 });
|
|
901
|
+
const readers: unknown = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
902
|
+
hidQrReader.value = Array.isArray(readers)
|
|
903
|
+
? readers.find((reader: unknown): reader is HidQrReaderSummary => (
|
|
904
|
+
typeof reader === "object"
|
|
905
|
+
&& reader !== null
|
|
906
|
+
&& String((reader as Record<string, unknown>)._id || "") === readerId
|
|
907
|
+
)) || null
|
|
908
|
+
: null;
|
|
909
|
+
} catch {
|
|
910
|
+
hidQrReader.value = null;
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
{ immediate: true },
|
|
914
|
+
);
|
|
880
915
|
|
|
881
916
|
const hidQrCodePassAllowed = computed(() => {
|
|
882
917
|
return (
|
|
@@ -2100,6 +2135,12 @@ function validateHidQrReaderConfig() {
|
|
|
2100
2135
|
return false;
|
|
2101
2136
|
}
|
|
2102
2137
|
|
|
2138
|
+
if (!hidQrReader.value?.portalId || !hidQrReader.value?.portalName) {
|
|
2139
|
+
errorMessage.value =
|
|
2140
|
+
"The selected HID reader has no portal configured. Reconnect the reader and select a portal before generating a QR code.";
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2103
2144
|
return true;
|
|
2104
2145
|
}
|
|
2105
2146
|
|
|
@@ -6,6 +6,8 @@ type HidReaderPayload = {
|
|
|
6
6
|
username?: string;
|
|
7
7
|
password?: string;
|
|
8
8
|
deviceId?: string;
|
|
9
|
+
portalId?: number;
|
|
10
|
+
portalName?: string;
|
|
9
11
|
monitorPath?: string;
|
|
10
12
|
enabled?: boolean;
|
|
11
13
|
status?: string;
|
|
@@ -34,6 +36,9 @@ type HidVisitorQrData = {
|
|
|
34
36
|
hidUserId: number;
|
|
35
37
|
qrValue: string;
|
|
36
38
|
qrFormat: "0";
|
|
39
|
+
portalId: number;
|
|
40
|
+
portalName: string;
|
|
41
|
+
readerName: string;
|
|
37
42
|
issuedAt: string;
|
|
38
43
|
expiresAt: string;
|
|
39
44
|
};
|
|
@@ -189,6 +194,13 @@ export default function useHidAmico() {
|
|
|
189
194
|
);
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
function discoverReader(payload: Pick<HidReaderPayload, "baseUrl" | "username" | "password">) {
|
|
198
|
+
return useNuxtApp().$api<Record<string, unknown>>(`${basePath}/readers/discover`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
body: payload,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
function getUserPinStatus(readerId: string, hidUserId: string | number) {
|
|
193
205
|
return useNuxtApp().$api<{ data: { pinEnrolled: boolean } }>(
|
|
194
206
|
`${basePath}/readers/${readerId}/users/${hidUserId}/pin`,
|
|
@@ -294,6 +306,7 @@ export default function useHidAmico() {
|
|
|
294
306
|
|
|
295
307
|
return {
|
|
296
308
|
getReaders,
|
|
309
|
+
discoverReader,
|
|
297
310
|
createReader,
|
|
298
311
|
updateReader,
|
|
299
312
|
deleteReader,
|