@7365admin1/layer-common 3.2.1 → 3.2.2-staging.100
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/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +46 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<div>
|
|
3
|
+
<!-- The design's title row: the page's name at 22px and its primary
|
|
4
|
+
action on the right of that same line. Placement only - the same
|
|
5
|
+
button opens the same dialog under the same permission. -->
|
|
6
|
+
<PageHeader title="Feedbacks">
|
|
7
|
+
<template v-if="canCreateFeedback" #actions>
|
|
8
|
+
<AppButton @click="showCreateDialog = true">Create Feedback</AppButton>
|
|
9
|
+
</template>
|
|
10
|
+
</PageHeader>
|
|
11
|
+
|
|
12
|
+
<v-row no-gutters>
|
|
3
13
|
<v-col cols="12" class="mb-2">
|
|
4
|
-
<v-row no-gutters align="center">
|
|
14
|
+
<v-row no-gutters align="center" class="filter-bar">
|
|
5
15
|
<v-text-field
|
|
6
16
|
v-model="searchText"
|
|
7
17
|
placeholder="Search feedback..."
|
|
@@ -50,51 +60,32 @@
|
|
|
50
60
|
<v-date-picker v-model="endDate" width="320" :locale="locale" />
|
|
51
61
|
</v-card>
|
|
52
62
|
</v-dialog>
|
|
53
|
-
<v-btn
|
|
54
|
-
v-if="canCreateFeedback"
|
|
55
|
-
text="create feedback"
|
|
56
|
-
class="text-none text-capitalize ml-2"
|
|
57
|
-
rounded="pill"
|
|
58
|
-
variant="tonal"
|
|
59
|
-
size="large"
|
|
60
|
-
@click="showCreateDialog = true"
|
|
61
|
-
/>
|
|
62
63
|
</v-row>
|
|
63
64
|
</v-col>
|
|
64
65
|
|
|
65
66
|
<v-col cols="12">
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
fab
|
|
77
|
-
icon
|
|
78
|
-
density="comfortable"
|
|
67
|
+
<AppCard class="table-card">
|
|
68
|
+
<!-- One row inside the card - refresh, the range and the pager - in
|
|
69
|
+
place of a `v-toolbar` that stacked them above the table. Same
|
|
70
|
+
three controls, same handlers. -->
|
|
71
|
+
<CardToolbar :count="pageRange">
|
|
72
|
+
<template #left>
|
|
73
|
+
<AppButton
|
|
74
|
+
variant="icon"
|
|
75
|
+
icon="mdi-refresh"
|
|
76
|
+
aria-label="Refresh"
|
|
79
77
|
@click="(page = 1), refreshFeedbacks()"
|
|
80
|
-
|
|
81
|
-
<v-icon icon="mdi-refresh" />
|
|
82
|
-
</v-btn>
|
|
78
|
+
/>
|
|
83
79
|
</template>
|
|
84
80
|
|
|
85
|
-
<template #
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
v-model="page"
|
|
92
|
-
:length="pages"
|
|
93
|
-
@update:value="updatePage"
|
|
94
|
-
/>
|
|
95
|
-
</v-row>
|
|
81
|
+
<template #right>
|
|
82
|
+
<local-pagination
|
|
83
|
+
v-model="page"
|
|
84
|
+
:length="pages"
|
|
85
|
+
@update:value="updatePage"
|
|
86
|
+
/>
|
|
96
87
|
</template>
|
|
97
|
-
</
|
|
88
|
+
</CardToolbar>
|
|
98
89
|
|
|
99
90
|
<v-data-table
|
|
100
91
|
:headers="headers"
|
|
@@ -104,6 +95,7 @@
|
|
|
104
95
|
fixed-header
|
|
105
96
|
hide-default-footer
|
|
106
97
|
:hide-default-header="false"
|
|
98
|
+
:loading="loading"
|
|
107
99
|
@click:row="tableRowClickHandler"
|
|
108
100
|
style="max-height: calc(100vh - (200px))"
|
|
109
101
|
>
|
|
@@ -139,13 +131,7 @@
|
|
|
139
131
|
</template>
|
|
140
132
|
<template #item.status="{ value }">
|
|
141
133
|
<span class="d-inline-block text-truncate">
|
|
142
|
-
<
|
|
143
|
-
:color="getStatusColor(value ?? '')"
|
|
144
|
-
class="pa-5 text-capitalize text-center whitespace-nowrap text-body-1"
|
|
145
|
-
variant="flat"
|
|
146
|
-
>
|
|
147
|
-
{{ value }}
|
|
148
|
-
</v-chip>
|
|
134
|
+
<StatusChip class="text-capitalize" :status="value" />
|
|
149
135
|
</span>
|
|
150
136
|
</template>
|
|
151
137
|
<template #item.assigneeInfo="{ item, index }">
|
|
@@ -183,9 +169,10 @@
|
|
|
183
169
|
<span v-else class="mt-1 text-wrap"> Not Assigned </span>
|
|
184
170
|
</template>
|
|
185
171
|
</v-data-table>
|
|
186
|
-
</
|
|
172
|
+
</AppCard>
|
|
187
173
|
</v-col>
|
|
188
|
-
|
|
174
|
+
</v-row>
|
|
175
|
+
</div>
|
|
189
176
|
|
|
190
177
|
<!-- <FeedbackForm
|
|
191
178
|
v-model="showCreateDialog"
|
|
@@ -660,7 +647,6 @@ const _feedback = ref({
|
|
|
660
647
|
});
|
|
661
648
|
|
|
662
649
|
const {
|
|
663
|
-
getStatusColor,
|
|
664
650
|
debounce,
|
|
665
651
|
standardFormatDateTime,
|
|
666
652
|
standardFormatDate,
|
|
@@ -721,9 +707,9 @@ const {
|
|
|
721
707
|
_getFeedbacks({
|
|
722
708
|
page: page.value,
|
|
723
709
|
site: route.params.site as string,
|
|
724
|
-
provider: currentUser.value
|
|
725
|
-
...(currentUser.value
|
|
726
|
-
userId: currentUser.value
|
|
710
|
+
provider: currentUser.value?.serviceProvider,
|
|
711
|
+
...(currentUser.value?.type != "site" && {
|
|
712
|
+
userId: currentUser.value?._id,
|
|
727
713
|
}),
|
|
728
714
|
service: "Security",
|
|
729
715
|
dateFrom: moment(startDate.value, "DD/MM/YYYY").startOf("day"),
|
|
@@ -998,9 +984,9 @@ async function handleAccept() {
|
|
|
998
984
|
_id: feedbackData.value._id,
|
|
999
985
|
statusUpdate: {
|
|
1000
986
|
status: "In-Progress",
|
|
1001
|
-
updatedById: currentUser.value
|
|
1002
|
-
updatedByName: `${currentUser.value
|
|
1003
|
-
assignee: currentUser.value
|
|
987
|
+
updatedById: currentUser.value?._id,
|
|
988
|
+
updatedByName: `${currentUser.value?.givenName} ${currentUser.value?.surname}`,
|
|
989
|
+
assignee: currentUser.value?._id,
|
|
1004
990
|
provider: currentUser.value?.serviceProvider,
|
|
1005
991
|
},
|
|
1006
992
|
};
|
|
@@ -1158,7 +1144,7 @@ async function submit() {
|
|
|
1158
1144
|
isSubmitting.value = true;
|
|
1159
1145
|
|
|
1160
1146
|
const result = await createFeedback({
|
|
1161
|
-
createdBy: currentUser.value
|
|
1147
|
+
createdBy: currentUser.value?._id,
|
|
1162
1148
|
description: _feedback.value.description,
|
|
1163
1149
|
attachments: _feedback.value.attachments,
|
|
1164
1150
|
site: route.params.site as string,
|
|
@@ -1,31 +1,35 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
PHASE 3 - THE MODAL.
|
|
3
|
+
|
|
4
|
+
520px, 16px radius and the design's overlay and shadow, with the BODY as the
|
|
5
|
+
only thing that scrolls so the footer never leaves the screen. Same dialog,
|
|
6
|
+
same slots, same close behaviour, same `persistent`.
|
|
7
|
+
|
|
8
|
+
The two `theme.name.value === ...` branches are gone, and that is a fix, not
|
|
9
|
+
a preference: they hard-coded `bg-white` and Vuetify's `grey-darken-4`, so
|
|
10
|
+
the card was painted by a literal colour instead of by the theme it is in.
|
|
11
|
+
`--card` is that surface in each theme.
|
|
12
|
+
-->
|
|
1
13
|
<template>
|
|
2
|
-
<v-dialog v-model="model" max-width="
|
|
3
|
-
<v-card
|
|
4
|
-
class="px-3 d-flex flex-column"
|
|
5
|
-
:class="`${theme.name.value === 'light' ? 'bg-white' : ''} ${
|
|
6
|
-
mdAndUp ? 'rounded-xl' : ''
|
|
7
|
-
}`"
|
|
8
|
-
:style="{ border: 'none', boxShadow: 'none' }"
|
|
9
|
-
>
|
|
14
|
+
<v-dialog v-model="model" :max-width="520" persistent class="form-dialog">
|
|
15
|
+
<v-card class="d-flex flex-column form-dialog__card" :style="{ border: 'none' }">
|
|
10
16
|
<v-toolbar
|
|
11
|
-
|
|
12
|
-
class="flex-shrink-0
|
|
17
|
+
color="transparent"
|
|
18
|
+
class="flex-shrink-0 form-dialog__head"
|
|
13
19
|
style="position: sticky; top: 0; z-index: 10"
|
|
14
20
|
>
|
|
15
21
|
<slot name="title">
|
|
16
|
-
<span class="
|
|
17
|
-
Dialog
|
|
18
|
-
</span>
|
|
22
|
+
<span class="form-dialog__title"> Dialog </span>
|
|
19
23
|
</slot>
|
|
20
24
|
<v-spacer />
|
|
21
|
-
<v-btn icon="mdi-close" @click="closeDialog" />
|
|
25
|
+
<v-btn icon="mdi-close" variant="text" @click="closeDialog" />
|
|
22
26
|
</v-toolbar>
|
|
23
27
|
|
|
24
|
-
<v-sheet class="flex-grow-1 overflow-y-auto px-
|
|
28
|
+
<v-sheet class="flex-grow-1 overflow-y-auto px-4 form-dialog__body">
|
|
25
29
|
<slot />
|
|
26
30
|
</v-sheet>
|
|
27
31
|
|
|
28
|
-
<v-sheet class="flex-shrink-0 px-
|
|
32
|
+
<v-sheet class="flex-shrink-0 px-4 py-3 form-dialog__footer">
|
|
29
33
|
<slot name="footer" />
|
|
30
34
|
</v-sheet>
|
|
31
35
|
</v-card>
|
|
@@ -33,11 +37,6 @@
|
|
|
33
37
|
</template>
|
|
34
38
|
|
|
35
39
|
<script setup lang="ts">
|
|
36
|
-
import { useTheme, useDisplay } from "vuetify";
|
|
37
|
-
|
|
38
|
-
const theme = useTheme().global;
|
|
39
|
-
const { mdAndUp } = useDisplay();
|
|
40
|
-
|
|
41
40
|
const props = defineProps<{
|
|
42
41
|
modelValue: boolean;
|
|
43
42
|
}>();
|
|
@@ -62,4 +61,44 @@ function closeDialog() {
|
|
|
62
61
|
:deep(.v-dialog .v-overlay__content) {
|
|
63
62
|
margin: auto;
|
|
64
63
|
}
|
|
64
|
+
|
|
65
|
+
.form-dialog__card {
|
|
66
|
+
font-family: var(--font-sans);
|
|
67
|
+
background: var(--card);
|
|
68
|
+
/* `!important` because Vuetify's own `.v-overlay__content > .v-card` rule is
|
|
69
|
+
more specific than a class on the card, and it is what keeps the dialog at
|
|
70
|
+
its factory 4px. */
|
|
71
|
+
border-radius: var(--r-modal) !important;
|
|
72
|
+
box-shadow: var(--shadow-modal);
|
|
73
|
+
/* Never taller than the screen it has to fit inside; the body is what
|
|
74
|
+
scrolls, so the Cancel/Submit footer cannot be pushed out of view. */
|
|
75
|
+
max-height: calc(100vh - 32px);
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.form-dialog__head {
|
|
80
|
+
border-bottom: 1px solid var(--border);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.form-dialog__title {
|
|
84
|
+
font-size: var(--fs-card-title);
|
|
85
|
+
font-weight: var(--fw-card-title);
|
|
86
|
+
color: var(--text);
|
|
87
|
+
text-transform: capitalize;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.form-dialog__body,
|
|
91
|
+
.form-dialog__footer {
|
|
92
|
+
background: var(--card);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.form-dialog__footer {
|
|
96
|
+
border-top: 1px solid var(--border);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (max-width: 599.98px) {
|
|
100
|
+
.form-dialog__card {
|
|
101
|
+
max-height: calc(100dvh - 32px);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
65
104
|
</style>
|
|
@@ -327,6 +327,8 @@ type AccessTab = "resident" | "visitor" | "administrator";
|
|
|
327
327
|
type AccessRow = {
|
|
328
328
|
key: string;
|
|
329
329
|
identityKey: string;
|
|
330
|
+
identityType?: string;
|
|
331
|
+
visitorId?: string;
|
|
330
332
|
name: string;
|
|
331
333
|
facialData: string;
|
|
332
334
|
faceImage?: string;
|
|
@@ -351,6 +353,7 @@ const { getVisitors } = useVisitor();
|
|
|
351
353
|
|
|
352
354
|
const readers = ref<Record<string, any>[]>([]);
|
|
353
355
|
const identities = ref<Record<string, any>[]>([]);
|
|
356
|
+
const allIdentities = ref<Record<string, any>[]>([]);
|
|
354
357
|
const visitorRecords = ref<Record<string, any>>({});
|
|
355
358
|
const logs = ref<Record<string, any>[]>([]);
|
|
356
359
|
const liveAccessLogs = ref<Record<string, any>[]>([]);
|
|
@@ -418,8 +421,17 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
418
421
|
if (!selectedHistoryRow.value) return [];
|
|
419
422
|
|
|
420
423
|
const selectedKey = selectedHistoryRow.value.identityKey;
|
|
421
|
-
const
|
|
422
|
-
const
|
|
424
|
+
const selectedIdentity = findIdentityByKey(selectedKey);
|
|
425
|
+
const matchedLogs = logs.value.filter((log) =>
|
|
426
|
+
selectedIdentity
|
|
427
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
428
|
+
: getLogIdentityKey(log) === selectedKey,
|
|
429
|
+
);
|
|
430
|
+
const matchedLiveLogs = liveAccessLogs.value.filter((log) =>
|
|
431
|
+
selectedIdentity
|
|
432
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
433
|
+
: getAccessLogIdentityKey(log) === selectedKey,
|
|
434
|
+
);
|
|
423
435
|
|
|
424
436
|
if (!matchedLogs.length && !matchedLiveLogs.length) {
|
|
425
437
|
return rows.value.filter((row) => row.identityKey === selectedKey);
|
|
@@ -549,28 +561,41 @@ async function loadDashboard() {
|
|
|
549
561
|
const isAdministratorTab = activeTab.value === "administrator";
|
|
550
562
|
const isVisitorTab = activeTab.value === "visitor";
|
|
551
563
|
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
+
loadDashboardSource(
|
|
565
|
+
getIdentities(selectedReaderId.value, {
|
|
566
|
+
page: 1,
|
|
567
|
+
limit: 500,
|
|
568
|
+
}),
|
|
569
|
+
null,
|
|
570
|
+
"identities",
|
|
571
|
+
),
|
|
572
|
+
loadDashboardSource(
|
|
573
|
+
getLogs(selectedReaderId.value, {
|
|
574
|
+
page: 1,
|
|
575
|
+
limit: 500,
|
|
576
|
+
}),
|
|
577
|
+
null,
|
|
578
|
+
"persisted access logs",
|
|
579
|
+
),
|
|
580
|
+
loadDashboardSource(loadLiveAccessLogs(selectedReaderId.value), [], "live access logs"),
|
|
581
|
+
loadDashboardSource(loadReaderConfiguration(selectedReaderId.value), {}, "reader configuration"),
|
|
582
|
+
isAdministratorTab
|
|
583
|
+
? loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles")
|
|
584
|
+
: Promise.resolve(null),
|
|
564
585
|
isVisitorTab && props.org
|
|
565
|
-
?
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
586
|
+
? loadDashboardSource(
|
|
587
|
+
getVisitors({
|
|
588
|
+
org: props.org,
|
|
589
|
+
site: props.site,
|
|
590
|
+
page: 1,
|
|
591
|
+
limit: 500,
|
|
592
|
+
order: "desc",
|
|
593
|
+
}),
|
|
594
|
+
null,
|
|
595
|
+
"visitor records",
|
|
596
|
+
)
|
|
572
597
|
: Promise.resolve(null),
|
|
573
|
-
loadReaderUsers(selectedReaderId.value),
|
|
598
|
+
loadDashboardSource(loadReaderUsers(selectedReaderId.value), [], "reader users"),
|
|
574
599
|
]);
|
|
575
600
|
|
|
576
601
|
if (sequence !== loadSequence.value) return;
|
|
@@ -578,18 +603,19 @@ async function loadDashboard() {
|
|
|
578
603
|
const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
|
|
579
604
|
const mergedIdentities = mergeReaderUsersWithIdentities(readerUserResponse, loadedIdentities);
|
|
580
605
|
const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
|
|
581
|
-
visitorRecords.value =
|
|
582
|
-
|
|
583
|
-
|
|
606
|
+
visitorRecords.value = Object.fromEntries(
|
|
607
|
+
loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]),
|
|
608
|
+
);
|
|
584
609
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
610
|
+
allIdentities.value = mergedIdentities;
|
|
585
611
|
identities.value = isAdministratorTab
|
|
586
612
|
? mergedIdentities.filter((identity: Record<string, any>) => {
|
|
587
613
|
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
588
614
|
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
589
615
|
})
|
|
590
|
-
:
|
|
591
|
-
? mergedIdentities.filter(
|
|
592
|
-
|
|
616
|
+
: activeTab.value === "visitor"
|
|
617
|
+
? mergedIdentities.filter(isVisitorIdentity)
|
|
618
|
+
: mergedIdentities.filter((identity: Record<string, any>) => !isVisitorIdentity(identity));
|
|
593
619
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
594
620
|
liveAccessLogs.value = hidAccessLogs;
|
|
595
621
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
@@ -601,6 +627,15 @@ async function loadDashboard() {
|
|
|
601
627
|
}
|
|
602
628
|
}
|
|
603
629
|
|
|
630
|
+
async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: string): Promise<T> {
|
|
631
|
+
try {
|
|
632
|
+
return await request;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.warn(`Unable to load HID ${label}`, error);
|
|
635
|
+
return fallback;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
604
639
|
async function loadUserImages(items: Record<string, any>[]) {
|
|
605
640
|
if (!selectedReaderId.value) return;
|
|
606
641
|
|
|
@@ -766,6 +801,7 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
766
801
|
function resetDashboardData() {
|
|
767
802
|
selectedHistoryRow.value = undefined;
|
|
768
803
|
identities.value = [];
|
|
804
|
+
allIdentities.value = [];
|
|
769
805
|
visitorRecords.value = {};
|
|
770
806
|
logs.value = [];
|
|
771
807
|
liveAccessLogs.value = [];
|
|
@@ -773,15 +809,11 @@ function resetDashboardData() {
|
|
|
773
809
|
administratorUserIds.value = new Set();
|
|
774
810
|
}
|
|
775
811
|
|
|
776
|
-
function getIdentityType(tab: AccessTab) {
|
|
777
|
-
if (tab === "administrator") return "admin";
|
|
778
|
-
return tab;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
812
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
782
813
|
const identity = findIdentityByKey(row.identityKey);
|
|
814
|
+
const isVisitor = row.identityType === "visitor" || Boolean(row.visitorId) || isVisitorIdentity(identity);
|
|
783
815
|
if (activeTab.value === "visitor") {
|
|
784
|
-
return
|
|
816
|
+
return isVisitor;
|
|
785
817
|
}
|
|
786
818
|
|
|
787
819
|
if (activeTab.value === "administrator") {
|
|
@@ -789,7 +821,11 @@ function isVisibleForActiveTab(row: AccessRow) {
|
|
|
789
821
|
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
790
822
|
}
|
|
791
823
|
|
|
792
|
-
return !
|
|
824
|
+
return !isVisitor;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
828
|
+
return Boolean(identity && (identity.type === "visitor" || identity.visitor));
|
|
793
829
|
}
|
|
794
830
|
|
|
795
831
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -835,13 +871,13 @@ function getIdentityKey(identity: Record<string, any>) {
|
|
|
835
871
|
return [
|
|
836
872
|
identity.hidUserId,
|
|
837
873
|
identity.registration,
|
|
838
|
-
identity.cardNo,
|
|
874
|
+
identity.cardNo || identity.metadata?.qrCodeValue,
|
|
839
875
|
].filter(Boolean).join("|");
|
|
840
876
|
}
|
|
841
877
|
|
|
842
878
|
function findIdentityByKey(identityKey: string) {
|
|
843
879
|
if (!identityKey) return undefined;
|
|
844
|
-
return
|
|
880
|
+
return allIdentities.value.find((identity) => getIdentityKey(identity) === identityKey);
|
|
845
881
|
}
|
|
846
882
|
|
|
847
883
|
function getLogIdentityKey(log: Record<string, any>) {
|
|
@@ -851,7 +887,7 @@ function getLogIdentityKey(log: Record<string, any>) {
|
|
|
851
887
|
return [
|
|
852
888
|
identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
|
|
853
889
|
identity.registration || lookup.registration,
|
|
854
|
-
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
|
|
890
|
+
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
855
891
|
].filter(Boolean).join("|");
|
|
856
892
|
}
|
|
857
893
|
|
|
@@ -1145,12 +1181,15 @@ function getRawAccessLogs(event: Record<string, any>) {
|
|
|
1145
1181
|
|
|
1146
1182
|
function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
|
|
1147
1183
|
const identity = findIdentityForAccessLog(rawLog, event);
|
|
1184
|
+
const eventIdentity = event.payload?.identity || event.payload?.identityLookup || {};
|
|
1148
1185
|
const rawAccessTime = getAccessLogTime(rawLog, event);
|
|
1149
1186
|
const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
|
|
1150
1187
|
|
|
1151
1188
|
return {
|
|
1152
1189
|
key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
|
|
1153
1190
|
identityKey,
|
|
1191
|
+
identityType: String(identity?.type || eventIdentity.type || "").toLowerCase() || undefined,
|
|
1192
|
+
visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
|
|
1154
1193
|
name: identity ? getName(identity) : getRawLogName(rawLog, event),
|
|
1155
1194
|
facialData: identity ? getFacialData(identity) : "N/A",
|
|
1156
1195
|
faceImage: identity ? getUserImageSrc(identity) : "",
|
|
@@ -1166,32 +1205,33 @@ function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, an
|
|
|
1166
1205
|
function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1167
1206
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1168
1207
|
if (eventIdentityId) {
|
|
1169
|
-
const byId =
|
|
1208
|
+
const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
1170
1209
|
if (byId) return byId;
|
|
1171
1210
|
}
|
|
1172
1211
|
|
|
1173
1212
|
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
1174
1213
|
const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
|
|
1175
1214
|
const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
|
|
1215
|
+
const qrCodeValue = rawLog.qrcode_value || rawLog.qrCode || rawLog.qrcode;
|
|
1176
1216
|
|
|
1177
|
-
return
|
|
1217
|
+
return allIdentities.value.find((identity) => {
|
|
1178
1218
|
return (
|
|
1179
1219
|
(hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
|
|
1180
1220
|
(registration && identity.registration === registration) ||
|
|
1181
|
-
(cardNo && String(identity.cardNo) === String(cardNo))
|
|
1221
|
+
(cardNo && String(identity.cardNo) === String(cardNo)) ||
|
|
1222
|
+
(qrCodeValue && String(identity.metadata?.qrCodeValue || "") === String(qrCodeValue))
|
|
1182
1223
|
);
|
|
1183
1224
|
});
|
|
1184
1225
|
}
|
|
1185
1226
|
|
|
1186
1227
|
function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
1187
|
-
const identityKey = getIdentityKey(identity);
|
|
1188
1228
|
const accessLogs = [
|
|
1189
1229
|
...liveAccessLogs.value,
|
|
1190
1230
|
...logs.value.flatMap((event) => getRawAccessLogs(event)),
|
|
1191
1231
|
];
|
|
1192
1232
|
|
|
1193
1233
|
return accessLogs
|
|
1194
|
-
.filter((log) =>
|
|
1234
|
+
.filter((log) => accessLogMatchesIdentity(log, identity))
|
|
1195
1235
|
.sort((a, b) => {
|
|
1196
1236
|
const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
|
|
1197
1237
|
const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
|
|
@@ -1199,11 +1239,46 @@ function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
|
1199
1239
|
})[0];
|
|
1200
1240
|
}
|
|
1201
1241
|
|
|
1242
|
+
function toUnknownRecord(value: unknown): Record<string, unknown> {
|
|
1243
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1244
|
+
? value as Record<string, unknown>
|
|
1245
|
+
: {};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function accessLogMatchesIdentity(log: unknown, identity: unknown) {
|
|
1249
|
+
const logRecord = toUnknownRecord(log);
|
|
1250
|
+
const identityRecord = toUnknownRecord(identity);
|
|
1251
|
+
const payload = toUnknownRecord(logRecord.payload);
|
|
1252
|
+
const rawLog = Object.keys(toUnknownRecord(payload.rawAccessLog)).length
|
|
1253
|
+
? toUnknownRecord(payload.rawAccessLog)
|
|
1254
|
+
: logRecord;
|
|
1255
|
+
const eventIdentity = Object.keys(toUnknownRecord(payload.identity)).length
|
|
1256
|
+
? toUnknownRecord(payload.identity)
|
|
1257
|
+
: toUnknownRecord(payload.identityLookup);
|
|
1258
|
+
const metadata = toUnknownRecord(identityRecord.metadata);
|
|
1259
|
+
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? eventIdentity.hidUserId;
|
|
1260
|
+
const registration = rawLog.registration || eventIdentity.registration;
|
|
1261
|
+
const credential =
|
|
1262
|
+
rawLog.card_value ||
|
|
1263
|
+
rawLog.cardNo ||
|
|
1264
|
+
rawLog.qrcode_value ||
|
|
1265
|
+
rawLog.qrCode ||
|
|
1266
|
+
rawLog.qrcode ||
|
|
1267
|
+
eventIdentity.cardNo;
|
|
1268
|
+
const identityCredential = identityRecord.cardNo || metadata.qrCodeValue;
|
|
1269
|
+
|
|
1270
|
+
return Boolean(
|
|
1271
|
+
(hidUserId !== undefined && hidUserId !== null && String(identityRecord.hidUserId) === String(hidUserId)) ||
|
|
1272
|
+
(registration && identityRecord.registration === registration) ||
|
|
1273
|
+
(credential && identityCredential && String(identityCredential) === String(credential)),
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1202
1277
|
function getAccessLogIdentityKey(rawLog: Record<string, any>) {
|
|
1203
1278
|
return [
|
|
1204
1279
|
rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
|
|
1205
1280
|
rawLog.registration,
|
|
1206
|
-
rawLog.card_value || rawLog.cardNo,
|
|
1281
|
+
rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
1207
1282
|
].filter(Boolean).join("|");
|
|
1208
1283
|
}
|
|
1209
1284
|
|