@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201
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/assets/css/primitives.css +1113 -0
- package/assets/css/screens.css +1097 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +236 -0
- package/components/AcceptDialog.vue +17 -18
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AccessCardAddForm.vue +42 -42
- package/components/AccessCardAssignToUnitForm.vue +13 -21
- package/components/AccessCardDeleteDialog.vue +41 -39
- package/components/AccessCardDetailsDialog.vue +125 -79
- package/components/AccessCardPreviewDialog.vue +58 -84
- package/components/AccessCardQrTagging.vue +60 -63
- package/components/AccessCardReplaceForm.vue +27 -23
- package/components/AccessManagement.vue +86 -85
- package/components/AddEqupmentForm.vue +18 -22
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppButton.vue +62 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +49 -0
- package/components/AppField.vue +76 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +247 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +14 -7
- package/components/AreaFormDialog.vue +71 -59
- package/components/AreaMain.vue +73 -107
- package/components/AttachmentsViewer.vue +84 -22
- package/components/AttendanceCheckInOutDialog.vue +85 -88
- package/components/AttendanceDetailsDialog.vue +57 -63
- package/components/AttendanceMain.vue +4 -3
- package/components/AttendanceMapSearchDialog.vue +42 -44
- package/components/AttendanceSettingsDialog.vue +79 -63
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BtnUploadFile.vue +53 -11
- package/components/BuildingForm.vue +34 -27
- package/components/BuildingManagement/buildings.vue +39 -39
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BuildingUnitFormAdd.vue +20 -23
- package/components/BuildingUnitFormEdit.vue +8 -4
- package/components/BulletinBoardForm.vue +19 -24
- package/components/BulletinBoardManagement.vue +49 -57
- package/components/BulletinBoardView.vue +9 -6
- package/components/BulletinExpirationChip.vue +13 -4
- package/components/Button/Close.vue +2 -2
- package/components/CameraForm.vue +21 -22
- package/components/CameraMain.vue +50 -49
- package/components/Card/DeleteConfirmation.vue +24 -22
- package/components/Card/MemberInfoSummary.vue +28 -3
- package/components/Card/Toggle.vue +15 -3
- package/components/CardToolbar.vue +37 -0
- package/components/Carousel.vue +56 -36
- package/components/Chat/Bubbles.vue +43 -14
- package/components/Chat/Information.vue +49 -32
- package/components/Chat/ListCard.vue +38 -9
- package/components/Chat/Message.vue +41 -15
- package/components/Chat/Navigation.vue +27 -15
- package/components/Chat/SkeletonLoader.vue +9 -2
- package/components/ChecklistItemRow.vue +25 -1
- package/components/CleaningScheduleMain.vue +44 -49
- package/components/ClientDetailForm.vue +81 -56
- package/components/ClientMain.vue +92 -82
- package/components/ConfirmDialog.vue +14 -8
- package/components/Container/Standard.vue +9 -7
- package/components/CreateSubsciptionPlan.vue +148 -89
- package/components/DashboardEmptyState.vue +48 -3
- package/components/DashboardMain.vue +924 -676
- package/components/DashboardPanel.vue +84 -21
- package/components/DeliveryCompany.vue +34 -20
- package/components/Dialog/DeleteConfirmation.vue +20 -21
- package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
- package/components/Dialog/ReusablePrompt.vue +30 -29
- package/components/Dialog/UpdateMoreAction.vue +62 -59
- package/components/DocumentForm.vue +14 -21
- package/components/DocumentManagement.vue +162 -108
- package/components/DocumentViewer.vue +29 -12
- package/components/DrawImage.vue +29 -21
- package/components/EntryPass/QrTemplatePreview.vue +27 -16
- package/components/EntryPassInformation.vue +40 -10
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +84 -97
- package/components/EquipmentManagementMain.vue +81 -103
- package/components/Facility/BookingSetup.vue +43 -52
- package/components/Feedback/Form.vue +26 -12
- package/components/FeedbackMain.vue +35 -49
- package/components/FileInput.vue +75 -58
- package/components/FileInputWithList.vue +59 -12
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +512 -490
- package/components/HidCameraCaptureDialog.vue +216 -0
- package/components/HidEnabledGate.vue +67 -9
- package/components/HidIntercomManagement.vue +1322 -653
- package/components/HidQrCodeConfiguration.vue +1030 -258
- package/components/HidReaderForm.vue +142 -59
- package/components/HidReaderManagement.vue +211 -323
- package/components/HidServiceSettingsPanel.vue +44 -13
- package/components/HidUserEnrollment.vue +1185 -412
- package/components/HygieneUpdateMoreAction.vue +44 -44
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/IncidentReport/IncidentInformation.vue +10 -10
- package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
- package/components/IncidentReport/affectedEntities.vue +41 -20
- package/components/Input/Date.vue +3 -0
- package/components/Input/DatePicker.vue +1 -2
- package/components/Input/DateTimePicker.vue +1 -1
- package/components/Input/File.vue +55 -10
- package/components/Input/FileV2.vue +39 -6
- package/components/Input/InputPhoneNumberV2.vue +2 -1
- package/components/Input/ListGroupSelection.vue +56 -8
- package/components/Input/NRICNumber.vue +1 -0
- package/components/Input/NewDate.vue +1 -0
- package/components/Input/Number.vue +1 -0
- package/components/Input/Password.vue +1 -0
- package/components/Input/PhoneNumber.vue +21 -1
- package/components/Input/VehicleNumber.vue +1 -0
- package/components/InputLabel.vue +2 -2
- package/components/InvitationClientForm.vue +21 -19
- package/components/InvitationForm.vue +15 -21
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +263 -36
- package/components/Layout/NavigationDrawer.vue +300 -30
- package/components/ListItem.vue +33 -3
- package/components/LocalPagination.vue +27 -3
- package/components/ManageChecklistMain.vue +136 -86
- package/components/MemberInformation.vue +32 -16
- package/components/MemberMain.vue +113 -93
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +83 -3
- package/components/Nfc/NFCPatrolReportMain.vue +47 -33
- package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/Nfc/NFCPatrolSettings.vue +14 -4
- package/components/Nfc/NFCTagForm.vue +22 -8
- package/components/Nfc/NFCTagMain.vue +41 -43
- package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
- package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
- package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
- package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
- package/components/NotificationSettings.vue +373 -0
- package/components/NumberSettingField.vue +9 -2
- package/components/OnlineFormConfigurationForm.vue +725 -237
- package/components/OnlineFormFill.vue +213 -106
- package/components/OnlineFormRenderPreview.vue +25 -30
- package/components/OnlineFormsConfiguration.vue +113 -109
- package/components/OrgViewDialog.vue +34 -16
- package/components/OvernightParkingAvailability.vue +49 -26
- package/components/OvernightParkingManagement.vue +78 -18
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +109 -53
- package/components/PasswordConfirmation.vue +44 -26
- package/components/PeopleForm.vue +41 -20
- package/components/PhotoUpload.vue +110 -48
- package/components/PlaceholderComponent.vue +35 -19
- package/components/PlanActionsMenu.vue +12 -2
- package/components/PlanStatusBadge.vue +7 -8
- package/components/PlateNumberDisplay.vue +9 -6
- package/components/QrTemplate/PrintDialog.vue +66 -43
- package/components/RolePermissionFormCreate.vue +104 -79
- package/components/RolePermissionFormPreviewUpdate.vue +111 -109
- package/components/RolePermissionMain.vue +183 -81
- package/components/ScanVisitorQRCode.vue +99 -35
- package/components/ScheduleAreaMain.vue +34 -59
- package/components/ScheduleTaskForm.vue +61 -42
- package/components/ScheduleTaskMain.vue +56 -69
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/ServiceProviderFormCreate.vue +29 -20
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +601 -432
- package/components/Signature.vue +40 -34
- package/components/SignaturePad.vue +5 -29
- package/components/SiteSettings.vue +41 -11
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SlideCardGroup.vue +35 -11
- package/components/Snackbar.vue +36 -3
- package/components/SpecificAttr.vue +18 -8
- package/components/StatusChip.vue +156 -0
- package/components/StockCard.vue +17 -12
- package/components/SubscriptionPlanTable.vue +207 -140
- package/components/SwitchContext.vue +201 -9
- package/components/TableHygiene.vue +36 -46
- package/components/TableList.vue +10 -10
- package/components/TableListSecondary.vue +105 -54
- package/components/TableMain.vue +251 -78
- package/components/TableV3.vue +61 -70
- package/components/TiptapEditor.vue +33 -40
- package/components/TiptapEditorQr.vue +5 -2
- package/components/Tooltip/Info.vue +69 -24
- package/components/UnitMain.vue +40 -60
- package/components/UnitPersonCard.vue +42 -13
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleManagement.vue +26 -12
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorForm.vue +774 -210
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorManagement.vue +239 -82
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSearchResultSummary.vue +60 -16
- package/components/VisitorSocketPopUp.vue +22 -14
- package/components/VisitorsReportPreview.vue +44 -29
- package/components/WorkOrder/Create.vue +26 -17
- package/components/WorkOrder/ListView.vue +38 -7
- package/components/WorkOrder/Main.vue +62 -59
- package/composables/useFeedback.ts +3 -3
- package/composables/useHidAmico.ts +269 -0
- package/composables/useHidNavigation.ts +2 -9
- package/composables/useMember.ts +3 -3
- package/composables/useNFCPatrolDailyReport.ts +13 -0
- package/composables/useNFCPatrolMonthlyReport.ts +22 -56
- package/composables/useNFCPatrolReport.ts +13 -0
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProvider.ts +3 -3
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSubscriptionPlan.ts +62 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useWebUsb.ts +127 -37
- package/composables/useWorkOrder.ts +17 -3
- package/error.vue +30 -6
- package/nuxt.config.ts +36 -0
- package/package.json +25 -4
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/pages/payment-method-linked.vue +34 -10
- package/pages/require-customer.vue +48 -10
- package/pages/require-organization-membership.vue +32 -8
- package/pages/unauthorized.vue +29 -4
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +69 -2
- package/types/nfc-patrol-report.ts +3 -2
- package/types/site.d.ts +71 -0
- package/types/subscription.ts +18 -11
- package/utils/breadcrumb.test.ts +108 -0
- package/utils/breadcrumb.ts +82 -0
- package/utils/dashboard.test.ts +118 -0
- package/utils/dashboard.ts +53 -0
- package/utils/data.test.ts +95 -0
- package/utils/data.ts +40 -2
- package/utils/hid-discover.test.ts +102 -0
- package/utils/list-page.test.ts +60 -0
- package/utils/list-page.ts +38 -0
- package/utils/materialSymbols.ts +363 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/nfc-patrol-report.test.ts +130 -0
- package/utils/nfc-patrol-report.ts +101 -0
- package/utils/role.test.ts +69 -0
- package/utils/role.ts +10 -0
- package/utils/route-name.test.ts +85 -0
- package/utils/route-name.ts +24 -0
- package/utils/status.test.ts +110 -0
- package/utils/status.ts +135 -0
- package/utils/table-card.test.ts +84 -0
- package/utils/theme-aa-ledger.ts +148 -0
- package/utils/theme.test.ts +485 -7
- package/utils/theme.ts +295 -110
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.editorconfig +0 -12
- package/.github/workflows/main.yml +0 -17
- package/.github/workflows/publish-staging.yml +0 -47
- package/.github/workflows/publish.yml +0 -39
- package/PUBLISHING.md +0 -269
- package/components/HidIdentityMapping.vue +0 -975
- package/components/ScheduleTaskAreaFormDialog.vue +0 -144
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
- package/components/ScheduleTastTicketMain.vue +0 -182
- package/components/TableWithButton.vue +0 -94
- package/test/visitor-socket.test.mjs +0 -36
|
@@ -1,132 +1,151 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
HID ACCESS LOG DASHBOARD - who went through a reader, and when.
|
|
3
|
+
|
|
4
|
+
Was a bespoke screen: an above-card toolbar, a `v-tabs` strip, a raw
|
|
5
|
+
`v-table`, a footer pager, an empty state and its own two-colour chip
|
|
6
|
+
palette, in about forty literal hexes. Those literals are why it had no dark
|
|
7
|
+
theme - `#0a2638` on the dark card measures **1.07:1**.
|
|
8
|
+
|
|
9
|
+
It is the design's standard table screen WITH TABS, so it is drawn with the
|
|
10
|
+
shared one: `TableMain` supplies the heading, the card, the in-card toolbar,
|
|
11
|
+
the `app-tab` strip, the uppercase header row, the row spec and the pager;
|
|
12
|
+
search, the reader picker and the status filter move onto the card's
|
|
13
|
+
`extension` row. Every row, tab, action, dialog and handler is unchanged -
|
|
14
|
+
the same `paginatedRows` still feed the table and the history icon still
|
|
15
|
+
opens the same dialog.
|
|
16
|
+
|
|
17
|
+
The handoff draws no HID table, so it states no column proportions - the
|
|
18
|
+
table takes `TableMain`'s automatic layout, as panels 2 and 4 do.
|
|
19
|
+
-->
|
|
1
20
|
<template>
|
|
2
21
|
<section class="hid-access-log-dashboard">
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<v-table>
|
|
62
|
-
<thead>
|
|
63
|
-
<tr>
|
|
64
|
-
<th>Name</th>
|
|
65
|
-
<th>Facial Data</th>
|
|
66
|
-
<th>Unit</th>
|
|
67
|
-
<th>Last Access Method</th>
|
|
68
|
-
<th>Last access time</th>
|
|
69
|
-
<th>Status</th>
|
|
70
|
-
<th class="text-right"></th>
|
|
71
|
-
</tr>
|
|
72
|
-
</thead>
|
|
73
|
-
<tbody>
|
|
74
|
-
<tr v-for="row in paginatedRows" :key="row.key">
|
|
75
|
-
<td>{{ row.name }}</td>
|
|
76
|
-
<td>
|
|
77
|
-
<v-avatar v-if="row.faceImage" size="34" rounded="lg" class="hid-face-avatar">
|
|
78
|
-
<v-img :src="row.faceImage" cover />
|
|
79
|
-
</v-avatar>
|
|
80
|
-
<span v-else>{{ row.facialData }}</span>
|
|
81
|
-
</td>
|
|
82
|
-
<td>{{ row.unit }}</td>
|
|
83
|
-
<td>{{ row.method }}</td>
|
|
84
|
-
<td>{{ row.lastAccessTime }}</td>
|
|
85
|
-
<td>
|
|
86
|
-
<v-chip
|
|
87
|
-
size="small"
|
|
88
|
-
variant="flat"
|
|
89
|
-
class="status-chip"
|
|
90
|
-
:class="row.status === 'Mapped' ? 'mapped-chip' : 'unmapped-chip'"
|
|
91
|
-
>
|
|
92
|
-
{{ row.status }}
|
|
93
|
-
</v-chip>
|
|
94
|
-
</td>
|
|
95
|
-
<td class="text-right">
|
|
96
|
-
<v-btn
|
|
97
|
-
icon="mdi-history"
|
|
98
|
-
variant="text"
|
|
99
|
-
density="comfortable"
|
|
100
|
-
@click="openHistoryDialog(row)"
|
|
101
|
-
/>
|
|
102
|
-
</td>
|
|
103
|
-
</tr>
|
|
104
|
-
</tbody>
|
|
105
|
-
</v-table>
|
|
106
|
-
|
|
107
|
-
<div v-if="!filteredRows.length" class="empty-state">
|
|
108
|
-
No access logs found.
|
|
22
|
+
<TableMain
|
|
23
|
+
title="Access Logs"
|
|
24
|
+
:headers="logHeaders"
|
|
25
|
+
:items="paginatedRows"
|
|
26
|
+
:loading="loading"
|
|
27
|
+
:items-per-page="-1"
|
|
28
|
+
:page="page"
|
|
29
|
+
:pages="pages"
|
|
30
|
+
:page-range="pageRange"
|
|
31
|
+
item-value="key"
|
|
32
|
+
show-header
|
|
33
|
+
@refresh="loadDashboard"
|
|
34
|
+
@update:page="page = $event"
|
|
35
|
+
>
|
|
36
|
+
<template #actions>
|
|
37
|
+
<AppButton icon="mdi-download" @click="openExportDialog">Export</AppButton>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<!-- Was `v-tabs`/`v-tab`, whose selected state is drawn from Vuetify's
|
|
41
|
+
`on-surface` rather than the design's accent underline. `.app-tab` is
|
|
42
|
+
the shared strip every other tabbed table already uses. -->
|
|
43
|
+
<template #tabs>
|
|
44
|
+
<button
|
|
45
|
+
v-for="tab in tabOptions"
|
|
46
|
+
:key="tab.value"
|
|
47
|
+
type="button"
|
|
48
|
+
role="tab"
|
|
49
|
+
class="app-tab"
|
|
50
|
+
:class="{ 'app-tab--active': activeTab === tab.value }"
|
|
51
|
+
:aria-selected="activeTab === tab.value"
|
|
52
|
+
@click="activeTab = tab.value; onTabChange(tab.value)"
|
|
53
|
+
>
|
|
54
|
+
{{ tab.label }}
|
|
55
|
+
</button>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #extension>
|
|
59
|
+
<div class="app-filter-row hid-log-filters">
|
|
60
|
+
<AppSelect
|
|
61
|
+
v-if="readers.length"
|
|
62
|
+
v-model="selectedReaderId"
|
|
63
|
+
class="reader-select"
|
|
64
|
+
:items="readerOptions"
|
|
65
|
+
@update:model-value="loadDashboard"
|
|
66
|
+
/>
|
|
67
|
+
<AppField v-model="search" search placeholder="Search" />
|
|
68
|
+
<AppSelect
|
|
69
|
+
v-model="status"
|
|
70
|
+
:items="statusOptions"
|
|
71
|
+
placeholder="Status"
|
|
72
|
+
clearable
|
|
73
|
+
/>
|
|
74
|
+
<AppSelect
|
|
75
|
+
v-model="accessMethod"
|
|
76
|
+
:items="accessMethodFilterOptions"
|
|
77
|
+
placeholder="Access Method"
|
|
78
|
+
clearable
|
|
79
|
+
/>
|
|
109
80
|
</div>
|
|
110
|
-
</
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<!-- Staging's hover-the-full-name tooltip, kept. -->
|
|
84
|
+
<template #[`item.name`]="{ item }">
|
|
85
|
+
<span class="app-cell--strong">
|
|
86
|
+
{{ item.name }}
|
|
87
|
+
<v-tooltip
|
|
88
|
+
v-if="item.name && item.name !== 'N/A'"
|
|
89
|
+
activator="parent"
|
|
90
|
+
location="top"
|
|
91
|
+
>
|
|
92
|
+
{{ item.name }}
|
|
93
|
+
</v-tooltip>
|
|
94
|
+
</span>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<!-- The face thumbnail, unchanged. It prints whatever the row already
|
|
98
|
+
carried; this screen does not read or export a template. -->
|
|
99
|
+
<template #[`item.facialData`]="{ item }">
|
|
100
|
+
<v-avatar v-if="item.faceImage" size="34" rounded="lg" class="hid-face-avatar">
|
|
101
|
+
<v-img :src="item.faceImage" cover />
|
|
102
|
+
</v-avatar>
|
|
103
|
+
<span v-else class="app-cell--muted">{{ item.facialData }}</span>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<template #[`item.method`]="{ item }">
|
|
107
|
+
<span class="app-cell--muted">{{ item.method }}</span>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<template #[`item.lastAccessTime`]="{ item }">
|
|
111
|
+
<span class="app-cell--num">{{ item.lastAccessTime }}</span>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
<!-- The shared chip. This screen's `mapped-chip` / `unmapped-chip` /
|
|
115
|
+
`unknown-chip` were a private copy of the same three tones
|
|
116
|
+
`utils/status.ts` now carries for Authorized / Not Authorized /
|
|
117
|
+
Unknown. Staging's reason tooltip rides on the chip. -->
|
|
118
|
+
<template #[`item.status`]="{ item }">
|
|
119
|
+
<StatusChip :status="item.status">
|
|
120
|
+
{{ item.status }}
|
|
121
|
+
<v-tooltip
|
|
122
|
+
v-if="item.authorizationReason"
|
|
123
|
+
activator="parent"
|
|
124
|
+
location="top"
|
|
125
|
+
>
|
|
126
|
+
{{ item.authorizationReason }}
|
|
127
|
+
</v-tooltip>
|
|
128
|
+
</StatusChip>
|
|
129
|
+
</template>
|
|
130
|
+
|
|
131
|
+
<template #[`item.action-table`]="{ item }">
|
|
132
|
+
<AppButton
|
|
133
|
+
variant="row"
|
|
134
|
+
icon="mdi-history"
|
|
135
|
+
aria-label="View access history"
|
|
136
|
+
@click="openHistoryDialog(item)"
|
|
127
137
|
/>
|
|
128
|
-
</
|
|
129
|
-
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<!-- The screen's own sentence, kept: the shared empty state says "No data
|
|
141
|
+
available", which says less than naming what was searched for. -->
|
|
142
|
+
<template #no-data>
|
|
143
|
+
<div class="table-card__empty">
|
|
144
|
+
<v-icon icon="mdi-history" size="32" />
|
|
145
|
+
<span>No access logs found.</span>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
</TableMain>
|
|
130
149
|
|
|
131
150
|
<v-dialog v-model="historyDialog" max-width="980">
|
|
132
151
|
<v-card class="history-dialog" rounded="lg">
|
|
@@ -134,17 +153,16 @@
|
|
|
134
153
|
|
|
135
154
|
<v-card-text class="history-body">
|
|
136
155
|
<div class="history-toolbar">
|
|
137
|
-
<
|
|
138
|
-
rounded="xl"
|
|
139
|
-
class="text-none export-button"
|
|
140
|
-
variant="flat"
|
|
141
|
-
prepend-icon="mdi-download"
|
|
142
|
-
@click="openExportDialog('history')"
|
|
143
|
-
>
|
|
156
|
+
<AppButton icon="mdi-download" @click="openExportDialog('history')">
|
|
144
157
|
Export Log
|
|
145
|
-
</
|
|
158
|
+
</AppButton>
|
|
146
159
|
<div class="toolbar-spacer" />
|
|
147
|
-
<
|
|
160
|
+
<AppButton
|
|
161
|
+
variant="icon"
|
|
162
|
+
icon="mdi-close"
|
|
163
|
+
aria-label="Close access log"
|
|
164
|
+
@click="historyDialog = false"
|
|
165
|
+
/>
|
|
148
166
|
</div>
|
|
149
167
|
|
|
150
168
|
<v-card flat border class="history-table-card">
|
|
@@ -159,6 +177,7 @@
|
|
|
159
177
|
<th>Reader Location</th>
|
|
160
178
|
<th>Access Method</th>
|
|
161
179
|
<th>Access Date and Time</th>
|
|
180
|
+
<th>Status</th>
|
|
162
181
|
</tr>
|
|
163
182
|
</thead>
|
|
164
183
|
<tbody>
|
|
@@ -168,6 +187,18 @@
|
|
|
168
187
|
<td>{{ row.readerLocation }}</td>
|
|
169
188
|
<td>{{ row.method }}</td>
|
|
170
189
|
<td>{{ row.lastAccessTime }}</td>
|
|
190
|
+
<!-- Staging's authorization column, on the shared chip: the
|
|
191
|
+
private `mapped-chip`/`unmapped-chip`/`unknown-chip`
|
|
192
|
+
palette is gone, and `utils/status.ts` carries the same
|
|
193
|
+
three tones for these three words. -->
|
|
194
|
+
<td>
|
|
195
|
+
<StatusChip :status="row.status">
|
|
196
|
+
{{ row.status }}
|
|
197
|
+
<v-tooltip v-if="row.authorizationReason" activator="parent" location="top">
|
|
198
|
+
{{ row.authorizationReason }}
|
|
199
|
+
</v-tooltip>
|
|
200
|
+
</StatusChip>
|
|
201
|
+
</td>
|
|
171
202
|
</tr>
|
|
172
203
|
</tbody>
|
|
173
204
|
</v-table>
|
|
@@ -185,116 +216,55 @@
|
|
|
185
216
|
<v-card-title class="dialog-title">Export</v-card-title>
|
|
186
217
|
|
|
187
218
|
<v-card-text class="dialog-body">
|
|
219
|
+
<!--
|
|
220
|
+
Was a `v-menu` + `v-text-field` + `v-date-picker` per date. The
|
|
221
|
+
design's date end is `AppDateField` - a native `<input type="date">`,
|
|
222
|
+
which brings the platform's own picker, localisation and keyboard
|
|
223
|
+
support instead of a 320px popover. `exportForm` still holds
|
|
224
|
+
DD/MM/YYYY, so everything downstream is untouched; the two computeds
|
|
225
|
+
below translate at the edge.
|
|
226
|
+
-->
|
|
188
227
|
<div class="field-label">Start Date <span>*</span></div>
|
|
189
|
-
<v-
|
|
190
|
-
v-model="startDateMenu"
|
|
191
|
-
:close-on-content-click="false"
|
|
192
|
-
location="bottom"
|
|
193
|
-
content-class="compact-date-menu"
|
|
194
|
-
>
|
|
195
|
-
<template #activator="{ props: menuProps }">
|
|
196
|
-
<v-text-field
|
|
197
|
-
v-bind="menuProps"
|
|
198
|
-
v-model="exportForm.startDate"
|
|
199
|
-
aria-label="Start Date"
|
|
200
|
-
prepend-inner-icon="mdi-calendar"
|
|
201
|
-
placeholder="DD/MM/YYYY"
|
|
202
|
-
density="compact"
|
|
203
|
-
variant="outlined"
|
|
204
|
-
hide-details="auto"
|
|
205
|
-
readonly
|
|
206
|
-
class="mb-3"
|
|
207
|
-
/>
|
|
208
|
-
</template>
|
|
209
|
-
<v-date-picker
|
|
210
|
-
:model-value="parseExportDate(exportForm.startDate, false)"
|
|
211
|
-
hide-header
|
|
212
|
-
width="320"
|
|
213
|
-
class="compact-date-picker"
|
|
214
|
-
@update:model-value="setExportDate('startDate', $event)"
|
|
215
|
-
/>
|
|
216
|
-
</v-menu>
|
|
228
|
+
<AppDateField v-model="exportStartInput" aria-label="Start Date" class="mb-3" />
|
|
217
229
|
|
|
218
230
|
<div class="field-label">End Date <span>*</span></div>
|
|
219
|
-
<v-
|
|
220
|
-
v-model="endDateMenu"
|
|
221
|
-
:close-on-content-click="false"
|
|
222
|
-
location="bottom"
|
|
223
|
-
content-class="compact-date-menu"
|
|
224
|
-
>
|
|
225
|
-
<template #activator="{ props: menuProps }">
|
|
226
|
-
<v-text-field
|
|
227
|
-
v-bind="menuProps"
|
|
228
|
-
v-model="exportForm.endDate"
|
|
229
|
-
aria-label="End Date"
|
|
230
|
-
prepend-inner-icon="mdi-calendar"
|
|
231
|
-
placeholder="DD/MM/YYYY"
|
|
232
|
-
density="compact"
|
|
233
|
-
variant="outlined"
|
|
234
|
-
hide-details="auto"
|
|
235
|
-
readonly
|
|
236
|
-
class="mb-3"
|
|
237
|
-
/>
|
|
238
|
-
</template>
|
|
239
|
-
<v-date-picker
|
|
240
|
-
:model-value="parseExportDate(exportForm.endDate, false)"
|
|
241
|
-
hide-header
|
|
242
|
-
width="320"
|
|
243
|
-
class="compact-date-picker"
|
|
244
|
-
@update:model-value="setExportDate('endDate', $event)"
|
|
245
|
-
/>
|
|
246
|
-
</v-menu>
|
|
231
|
+
<AppDateField v-model="exportEndInput" aria-label="End Date" class="mb-3" />
|
|
247
232
|
|
|
248
233
|
<div class="field-label">Users</div>
|
|
249
|
-
<
|
|
234
|
+
<AppSelect
|
|
250
235
|
v-model="exportForm.users"
|
|
251
236
|
:items="userOptions"
|
|
252
|
-
item-title="title"
|
|
253
|
-
item-value="value"
|
|
254
237
|
aria-label="Users"
|
|
255
238
|
placeholder="Select"
|
|
256
|
-
density="compact"
|
|
257
|
-
variant="outlined"
|
|
258
|
-
hide-details="auto"
|
|
259
239
|
multiple
|
|
260
|
-
chips
|
|
261
|
-
closable-chips
|
|
262
240
|
class="mb-3"
|
|
263
241
|
/>
|
|
264
242
|
|
|
265
243
|
<div class="field-label">Access Method</div>
|
|
266
|
-
<
|
|
244
|
+
<AppSelect
|
|
267
245
|
v-model="exportForm.accessMethod"
|
|
268
246
|
:items="accessMethodOptions"
|
|
269
247
|
aria-label="Access Method"
|
|
270
248
|
placeholder="Select"
|
|
271
|
-
density="compact"
|
|
272
|
-
variant="outlined"
|
|
273
|
-
hide-details="auto"
|
|
274
249
|
class="mb-3"
|
|
275
250
|
/>
|
|
276
251
|
|
|
277
252
|
<div class="field-label">Reader Location</div>
|
|
278
|
-
<
|
|
253
|
+
<AppSelect
|
|
279
254
|
v-model="exportForm.readerLocation"
|
|
280
255
|
:items="readerLocationOptions"
|
|
281
256
|
aria-label="Reader Location"
|
|
282
257
|
placeholder="Select"
|
|
283
|
-
density="compact"
|
|
284
|
-
variant="outlined"
|
|
285
|
-
hide-details="auto"
|
|
286
258
|
class="mb-3"
|
|
287
259
|
/>
|
|
288
260
|
|
|
289
261
|
<div class="field-label">Status (Optional)</div>
|
|
290
|
-
<
|
|
262
|
+
<AppSelect
|
|
291
263
|
v-model="exportForm.status"
|
|
292
264
|
:items="statusOptions"
|
|
293
265
|
aria-label="Status"
|
|
294
266
|
placeholder="Select"
|
|
295
|
-
|
|
296
|
-
variant="outlined"
|
|
297
|
-
hide-details="auto"
|
|
267
|
+
clearable
|
|
298
268
|
/>
|
|
299
269
|
</v-card-text>
|
|
300
270
|
|
|
@@ -324,9 +294,12 @@ const props = defineProps({
|
|
|
324
294
|
});
|
|
325
295
|
|
|
326
296
|
type AccessTab = "resident" | "visitor" | "administrator";
|
|
297
|
+
type AuthorizationStatus = "Authorized" | "Not Authorized" | "Unknown";
|
|
327
298
|
type AccessRow = {
|
|
328
299
|
key: string;
|
|
329
300
|
identityKey: string;
|
|
301
|
+
identityType?: string;
|
|
302
|
+
visitorId?: string;
|
|
330
303
|
name: string;
|
|
331
304
|
facialData: string;
|
|
332
305
|
faceImage?: string;
|
|
@@ -335,7 +308,8 @@ type AccessRow = {
|
|
|
335
308
|
lastAccessTime: string;
|
|
336
309
|
lastAccessDate?: Date;
|
|
337
310
|
readerLocation: string;
|
|
338
|
-
status:
|
|
311
|
+
status: AuthorizationStatus;
|
|
312
|
+
authorizationReason?: string;
|
|
339
313
|
};
|
|
340
314
|
|
|
341
315
|
type PdfAccessRow = {
|
|
@@ -351,6 +325,7 @@ const { getVisitors } = useVisitor();
|
|
|
351
325
|
|
|
352
326
|
const readers = ref<Record<string, any>[]>([]);
|
|
353
327
|
const identities = ref<Record<string, any>[]>([]);
|
|
328
|
+
const allIdentities = ref<Record<string, any>[]>([]);
|
|
354
329
|
const visitorRecords = ref<Record<string, any>>({});
|
|
355
330
|
const logs = ref<Record<string, any>[]>([]);
|
|
356
331
|
const liveAccessLogs = ref<Record<string, any>[]>([]);
|
|
@@ -360,32 +335,32 @@ const userImages = ref<Record<string, string>>({});
|
|
|
360
335
|
const selectedReaderId = ref("");
|
|
361
336
|
const activeTab = ref<AccessTab>("resident");
|
|
362
337
|
const search = ref("");
|
|
363
|
-
const status = ref("
|
|
338
|
+
const status = ref("");
|
|
339
|
+
const accessMethod = ref("");
|
|
364
340
|
const page = ref(1);
|
|
365
|
-
const limit =
|
|
341
|
+
const limit = 10;
|
|
366
342
|
const loading = ref(false);
|
|
367
343
|
const loadSequence = ref(0);
|
|
368
344
|
const exportDialog = ref(false);
|
|
369
345
|
const historyDialog = ref(false);
|
|
370
346
|
const selectedHistoryRow = ref<AccessRow>();
|
|
371
347
|
const exportScope = ref<"dashboard" | "history">("dashboard");
|
|
372
|
-
const startDateMenu = ref(false);
|
|
373
|
-
const endDateMenu = ref(false);
|
|
374
348
|
const exportForm = reactive({
|
|
375
349
|
startDate: "",
|
|
376
350
|
endDate: "",
|
|
377
351
|
users: [] as string[],
|
|
378
352
|
accessMethod: "All",
|
|
379
353
|
readerLocation: "All",
|
|
380
|
-
status: "
|
|
354
|
+
status: "",
|
|
381
355
|
});
|
|
382
356
|
|
|
383
|
-
const statusOptions = ["
|
|
384
|
-
const
|
|
357
|
+
const statusOptions = ["Authorized", "Not Authorized", "Unknown"];
|
|
358
|
+
const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card"];
|
|
359
|
+
const accessMethodOptions = ["All", ...accessMethodFilterOptions];
|
|
385
360
|
|
|
386
361
|
const readerOptions = computed(() =>
|
|
387
362
|
readers.value.map((reader) => ({
|
|
388
|
-
title: reader.name || reader.deviceId || reader._id
|
|
363
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
389
364
|
value: reader._id,
|
|
390
365
|
})),
|
|
391
366
|
);
|
|
@@ -418,8 +393,17 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
418
393
|
if (!selectedHistoryRow.value) return [];
|
|
419
394
|
|
|
420
395
|
const selectedKey = selectedHistoryRow.value.identityKey;
|
|
421
|
-
const
|
|
422
|
-
const
|
|
396
|
+
const selectedIdentity = findIdentityByKey(selectedKey);
|
|
397
|
+
const matchedLogs = logs.value.filter((log) =>
|
|
398
|
+
selectedIdentity
|
|
399
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
400
|
+
: getLogIdentityKey(log) === selectedKey,
|
|
401
|
+
);
|
|
402
|
+
const matchedLiveLogs = liveAccessLogs.value.filter((log) =>
|
|
403
|
+
selectedIdentity
|
|
404
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
405
|
+
: getAccessLogIdentityKey(log) === selectedKey,
|
|
406
|
+
);
|
|
423
407
|
|
|
424
408
|
if (!matchedLogs.length && !matchedLiveLogs.length) {
|
|
425
409
|
return rows.value.filter((row) => row.identityKey === selectedKey);
|
|
@@ -436,7 +420,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
436
420
|
lastAccessTime: formatDate(getLogAccessDateValue(log)),
|
|
437
421
|
lastAccessDate: parseDate(getLogAccessDateValue(log)),
|
|
438
422
|
readerLocation: getLogReaderLocation(log),
|
|
439
|
-
|
|
423
|
+
...getAuthorizationStatus(log.payload || log, log),
|
|
440
424
|
}));
|
|
441
425
|
|
|
442
426
|
const liveRows = matchedLiveLogs.map((log, index) => ({
|
|
@@ -450,7 +434,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
450
434
|
lastAccessTime: formatDate(getAccessLogTime(log, {})),
|
|
451
435
|
lastAccessDate: parseDate(getAccessLogTime(log, {})),
|
|
452
436
|
readerLocation: selectedReader.value?.location || "N/A",
|
|
453
|
-
|
|
437
|
+
...getAuthorizationStatus(log, {}),
|
|
454
438
|
}));
|
|
455
439
|
|
|
456
440
|
return [...historyRows, ...liveRows].sort((a, b) => {
|
|
@@ -472,12 +456,40 @@ const filteredRows = computed(() => {
|
|
|
472
456
|
row.status,
|
|
473
457
|
].some((value) => String(value ?? "").toLowerCase().includes(query));
|
|
474
458
|
|
|
475
|
-
const matchesStatus = status.value
|
|
459
|
+
const matchesStatus = !status.value || row.status === status.value;
|
|
460
|
+
const matchesMethod = !accessMethod.value || row.method === accessMethod.value;
|
|
476
461
|
|
|
477
|
-
return matchesSearch && matchesStatus;
|
|
462
|
+
return matchesSearch && matchesStatus && matchesMethod;
|
|
478
463
|
});
|
|
479
464
|
});
|
|
480
465
|
|
|
466
|
+
/**
|
|
467
|
+
* The three tabs, as data rather than three `v-tab` elements, so the shared
|
|
468
|
+
* `app-tab` strip can render them. Same values, same order, same handler.
|
|
469
|
+
*/
|
|
470
|
+
const tabOptions: Array<{ label: string; value: AccessTab }> = [
|
|
471
|
+
{ label: "Resident", value: "resident" },
|
|
472
|
+
{ label: "Visitor", value: "visitor" },
|
|
473
|
+
{ label: "Administrator", value: "administrator" },
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* `sortable: false` keeps the previous behaviour - the raw `v-table` never
|
|
478
|
+
* sorted - and no `weight` is set, so the table uses `TableMain`'s automatic
|
|
479
|
+
* layout. The handoff draws no HID table, so the screen's own fixed column
|
|
480
|
+
* percentages had no design number behind them.
|
|
481
|
+
*/
|
|
482
|
+
const logHeaders = [
|
|
483
|
+
{ title: "Name", value: "name", sortable: false },
|
|
484
|
+
{ title: "Facial Data", value: "facialData", sortable: false },
|
|
485
|
+
// Staging (b0667a7) dropped Unit from this table - the unit still shows in
|
|
486
|
+
// the per-identity history dialog, so the column is not carried back.
|
|
487
|
+
{ title: "Last Access Method", value: "method", sortable: false },
|
|
488
|
+
{ title: "Last access time", value: "lastAccessTime", sortable: false },
|
|
489
|
+
{ title: "Status", value: "status", sortable: false },
|
|
490
|
+
{ title: "", value: "action-table", sortable: false },
|
|
491
|
+
];
|
|
492
|
+
|
|
481
493
|
const pages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / limit)));
|
|
482
494
|
|
|
483
495
|
const paginatedRows = computed(() => {
|
|
@@ -500,7 +512,7 @@ const exportRowsData = computed(() =>
|
|
|
500
512
|
const matchesUsers = !selectedUsers.length || selectedUsers.includes(row.key) || selectedUsers.includes(row.identityKey);
|
|
501
513
|
const matchesMethod = exportForm.accessMethod === "All" || row.method === exportForm.accessMethod;
|
|
502
514
|
const matchesLocation = exportForm.readerLocation === "All" || row.readerLocation === exportForm.readerLocation;
|
|
503
|
-
const matchesStatus = exportForm.status
|
|
515
|
+
const matchesStatus = !exportForm.status || row.status === exportForm.status;
|
|
504
516
|
const matchesDate = isWithinExportRange(row.lastAccessDate);
|
|
505
517
|
|
|
506
518
|
return matchesUsers && matchesMethod && matchesLocation && matchesStatus && matchesDate;
|
|
@@ -513,7 +525,7 @@ watch(
|
|
|
513
525
|
{ immediate: true },
|
|
514
526
|
);
|
|
515
527
|
|
|
516
|
-
watch([search, status, activeTab], () => {
|
|
528
|
+
watch([search, status, accessMethod, activeTab], () => {
|
|
517
529
|
page.value = 1;
|
|
518
530
|
});
|
|
519
531
|
|
|
@@ -546,31 +558,41 @@ async function loadDashboard() {
|
|
|
546
558
|
loading.value = true;
|
|
547
559
|
resetDashboardData();
|
|
548
560
|
try {
|
|
549
|
-
const isAdministratorTab = activeTab.value === "administrator";
|
|
550
561
|
const isVisitorTab = activeTab.value === "visitor";
|
|
551
562
|
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
563
|
+
loadDashboardSource(
|
|
564
|
+
getIdentities(selectedReaderId.value, {
|
|
565
|
+
page: 1,
|
|
566
|
+
limit: 500,
|
|
567
|
+
}),
|
|
568
|
+
null,
|
|
569
|
+
"identities",
|
|
570
|
+
),
|
|
571
|
+
loadDashboardSource(
|
|
572
|
+
getLogs(selectedReaderId.value, {
|
|
573
|
+
page: 1,
|
|
574
|
+
limit: 500,
|
|
575
|
+
}),
|
|
576
|
+
null,
|
|
577
|
+
"persisted access logs",
|
|
578
|
+
),
|
|
579
|
+
loadDashboardSource(loadLiveAccessLogs(selectedReaderId.value), [], "live access logs"),
|
|
580
|
+
loadDashboardSource(loadReaderConfiguration(selectedReaderId.value), {}, "reader configuration"),
|
|
581
|
+
loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles"),
|
|
564
582
|
isVisitorTab && props.org
|
|
565
|
-
?
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
583
|
+
? loadDashboardSource(
|
|
584
|
+
getVisitors({
|
|
585
|
+
org: props.org,
|
|
586
|
+
site: props.site,
|
|
587
|
+
page: 1,
|
|
588
|
+
limit: 500,
|
|
589
|
+
order: "desc",
|
|
590
|
+
}),
|
|
591
|
+
null,
|
|
592
|
+
"visitor records",
|
|
593
|
+
)
|
|
572
594
|
: Promise.resolve(null),
|
|
573
|
-
loadReaderUsers(selectedReaderId.value),
|
|
595
|
+
loadDashboardSource(loadReaderUsers(selectedReaderId.value), [], "reader users"),
|
|
574
596
|
]);
|
|
575
597
|
|
|
576
598
|
if (sequence !== loadSequence.value) return;
|
|
@@ -578,18 +600,14 @@ async function loadDashboard() {
|
|
|
578
600
|
const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
|
|
579
601
|
const mergedIdentities = mergeReaderUsersWithIdentities(readerUserResponse, loadedIdentities);
|
|
580
602
|
const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
|
|
581
|
-
visitorRecords.value =
|
|
582
|
-
|
|
583
|
-
|
|
603
|
+
visitorRecords.value = Object.fromEntries(
|
|
604
|
+
loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]),
|
|
605
|
+
);
|
|
584
606
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
})
|
|
590
|
-
: isVisitorTab
|
|
591
|
-
? mergedIdentities.filter((identity: Record<string, any>) => Boolean(getVisitorRecord(identity)))
|
|
592
|
-
: mergedIdentities;
|
|
607
|
+
allIdentities.value = mergedIdentities;
|
|
608
|
+
identities.value = mergedIdentities.filter(
|
|
609
|
+
(identity: Record<string, unknown>) => classifyIdentity(identity) === activeTab.value,
|
|
610
|
+
);
|
|
593
611
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
594
612
|
liveAccessLogs.value = hidAccessLogs;
|
|
595
613
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
@@ -601,6 +619,15 @@ async function loadDashboard() {
|
|
|
601
619
|
}
|
|
602
620
|
}
|
|
603
621
|
|
|
622
|
+
async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: string): Promise<T> {
|
|
623
|
+
try {
|
|
624
|
+
return await request;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
console.warn(`Unable to load HID ${label}`, error);
|
|
627
|
+
return fallback;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
604
631
|
async function loadUserImages(items: Record<string, any>[]) {
|
|
605
632
|
if (!selectedReaderId.value) return;
|
|
606
633
|
|
|
@@ -722,8 +749,10 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
722
749
|
registration: user.registration,
|
|
723
750
|
cardNo: user.card_value || user.cardNo || "",
|
|
724
751
|
name: user.name,
|
|
752
|
+
hidUserTypeId: user.user_type_id,
|
|
725
753
|
metadata: {
|
|
726
754
|
name: user.name,
|
|
755
|
+
hidUserTypeId: user.user_type_id,
|
|
727
756
|
imageTimestamp: user.image_timestamp ? String(user.image_timestamp) : "",
|
|
728
757
|
hidReaderSource: true,
|
|
729
758
|
},
|
|
@@ -766,6 +795,7 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
766
795
|
function resetDashboardData() {
|
|
767
796
|
selectedHistoryRow.value = undefined;
|
|
768
797
|
identities.value = [];
|
|
798
|
+
allIdentities.value = [];
|
|
769
799
|
visitorRecords.value = {};
|
|
770
800
|
logs.value = [];
|
|
771
801
|
liveAccessLogs.value = [];
|
|
@@ -773,23 +803,24 @@ function resetDashboardData() {
|
|
|
773
803
|
administratorUserIds.value = new Set();
|
|
774
804
|
}
|
|
775
805
|
|
|
776
|
-
function getIdentityType(tab: AccessTab) {
|
|
777
|
-
if (tab === "administrator") return "admin";
|
|
778
|
-
return tab;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
806
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
782
807
|
const identity = findIdentityByKey(row.identityKey);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
808
|
+
return classifyIdentity(identity, row) === activeTab.value;
|
|
809
|
+
}
|
|
786
810
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
811
|
+
function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
812
|
+
return Boolean(identity && (
|
|
813
|
+
identity.type === "visitor"
|
|
814
|
+
|| identity.visitor
|
|
815
|
+
|| Number(identity.hidUserTypeId ?? (identity.metadata as Record<string, unknown> | undefined)?.hidUserTypeId) === 1
|
|
816
|
+
));
|
|
817
|
+
}
|
|
791
818
|
|
|
792
|
-
|
|
819
|
+
function classifyIdentity(identity?: Record<string, unknown>, row?: AccessRow): AccessTab {
|
|
820
|
+
const hidUserId = toHidNumericId(identity?.hidUserId ?? row?.identityKey.split("|")[0]);
|
|
821
|
+
if (hidUserId && administratorUserIds.value.has(hidUserId)) return "administrator";
|
|
822
|
+
if (row?.identityType === "visitor" || row?.visitorId || isVisitorIdentity(identity)) return "visitor";
|
|
823
|
+
return "resident";
|
|
793
824
|
}
|
|
794
825
|
|
|
795
826
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -835,13 +866,13 @@ function getIdentityKey(identity: Record<string, any>) {
|
|
|
835
866
|
return [
|
|
836
867
|
identity.hidUserId,
|
|
837
868
|
identity.registration,
|
|
838
|
-
identity.cardNo,
|
|
869
|
+
identity.cardNo || identity.metadata?.qrCodeValue,
|
|
839
870
|
].filter(Boolean).join("|");
|
|
840
871
|
}
|
|
841
872
|
|
|
842
873
|
function findIdentityByKey(identityKey: string) {
|
|
843
874
|
if (!identityKey) return undefined;
|
|
844
|
-
return
|
|
875
|
+
return allIdentities.value.find((identity) => getIdentityKey(identity) === identityKey);
|
|
845
876
|
}
|
|
846
877
|
|
|
847
878
|
function getLogIdentityKey(log: Record<string, any>) {
|
|
@@ -851,7 +882,7 @@ function getLogIdentityKey(log: Record<string, any>) {
|
|
|
851
882
|
return [
|
|
852
883
|
identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
|
|
853
884
|
identity.registration || lookup.registration,
|
|
854
|
-
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
|
|
885
|
+
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
855
886
|
].filter(Boolean).join("|");
|
|
856
887
|
}
|
|
857
888
|
|
|
@@ -1007,18 +1038,32 @@ function parseExportDate(value: string, endOfDay: boolean) {
|
|
|
1007
1038
|
return date;
|
|
1008
1039
|
}
|
|
1009
1040
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1041
|
+
/**
|
|
1042
|
+
* `exportForm` stores DD/MM/YYYY, because that is what `parseExportDate`,
|
|
1043
|
+
* `isWithinExportRange` and the PDF header all read. A native `<input
|
|
1044
|
+
* type="date">` speaks YYYY-MM-DD. Rather than change the stored format - which
|
|
1045
|
+
* would reach into the export itself - the translation happens here, at the
|
|
1046
|
+
* edge, exactly where the old `v-date-picker` did it.
|
|
1047
|
+
*/
|
|
1048
|
+
function exportDateModel(field: "startDate" | "endDate") {
|
|
1049
|
+
return computed({
|
|
1050
|
+
get: () => {
|
|
1051
|
+
const date = parseExportDate(exportForm[field], false);
|
|
1052
|
+
if (!date) return "";
|
|
1053
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1054
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1055
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
1056
|
+
},
|
|
1057
|
+
set: (value: string) => {
|
|
1058
|
+
const date = normalizePickerDate(value);
|
|
1059
|
+
exportForm[field] = date ? toDisplayDate(date) : "";
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1020
1062
|
}
|
|
1021
1063
|
|
|
1064
|
+
const exportStartInput = exportDateModel("startDate");
|
|
1065
|
+
const exportEndInput = exportDateModel("endDate");
|
|
1066
|
+
|
|
1022
1067
|
function normalizePickerDate(value: unknown) {
|
|
1023
1068
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
1024
1069
|
return value;
|
|
@@ -1140,17 +1185,33 @@ function getRawAccessLogs(event: Record<string, any>) {
|
|
|
1140
1185
|
payload.access_logs ||
|
|
1141
1186
|
payload.accessLogs;
|
|
1142
1187
|
|
|
1143
|
-
|
|
1188
|
+
if (Array.isArray(accessLogs)) return accessLogs;
|
|
1189
|
+
|
|
1190
|
+
const objectChanges: unknown = payload.object_changes || result.object_changes || [];
|
|
1191
|
+
return Array.isArray(objectChanges)
|
|
1192
|
+
? objectChanges.flatMap((change: unknown) => {
|
|
1193
|
+
const record = toUnknownRecord(change);
|
|
1194
|
+
const values = toUnknownRecord(record.values);
|
|
1195
|
+
return record.object === "access_logs" && record.type !== "deleted" && Object.keys(values).length
|
|
1196
|
+
? [values]
|
|
1197
|
+
: [];
|
|
1198
|
+
})
|
|
1199
|
+
: [];
|
|
1144
1200
|
}
|
|
1145
1201
|
|
|
1146
1202
|
function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
|
|
1147
1203
|
const identity = findIdentityForAccessLog(rawLog, event);
|
|
1204
|
+
const eventIdentity = event.payload?.identity || event.payload?.identityLookup || {};
|
|
1148
1205
|
const rawAccessTime = getAccessLogTime(rawLog, event);
|
|
1149
1206
|
const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
|
|
1150
1207
|
|
|
1151
1208
|
return {
|
|
1152
1209
|
key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
|
|
1153
1210
|
identityKey,
|
|
1211
|
+
identityType: identity
|
|
1212
|
+
? classifyIdentity(identity)
|
|
1213
|
+
: String(eventIdentity.type || "").toLowerCase() || undefined,
|
|
1214
|
+
visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
|
|
1154
1215
|
name: identity ? getName(identity) : getRawLogName(rawLog, event),
|
|
1155
1216
|
facialData: identity ? getFacialData(identity) : "N/A",
|
|
1156
1217
|
faceImage: identity ? getUserImageSrc(identity) : "",
|
|
@@ -1159,39 +1220,85 @@ function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, an
|
|
|
1159
1220
|
lastAccessTime: formatDate(rawAccessTime),
|
|
1160
1221
|
lastAccessDate: parseDate(rawAccessTime),
|
|
1161
1222
|
readerLocation: getLogReaderLocation(event),
|
|
1162
|
-
|
|
1223
|
+
...getAuthorizationStatus(rawLog, event),
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function getAuthorizationStatus(
|
|
1228
|
+
rawLog: Record<string, any>,
|
|
1229
|
+
event: Record<string, any>,
|
|
1230
|
+
): Pick<AccessRow, "status" | "authorizationReason"> {
|
|
1231
|
+
const authorization = event.payload?.authorization || rawLog.authorization;
|
|
1232
|
+
if (typeof authorization?.granted === "boolean") {
|
|
1233
|
+
return {
|
|
1234
|
+
status: authorization.granted ? "Authorized" : "Not Authorized",
|
|
1235
|
+
authorizationReason: String(
|
|
1236
|
+
authorization.reason || (authorization.granted ? "Access granted" : "Access denied"),
|
|
1237
|
+
),
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const eventCode = Number(
|
|
1242
|
+
rawLog.event ?? rawLog.event_id ?? rawLog.eventId ?? event.payload?.event,
|
|
1243
|
+
);
|
|
1244
|
+
if (eventCode === 7) {
|
|
1245
|
+
return { status: "Authorized", authorizationReason: "Access granted by HID authorization." };
|
|
1246
|
+
}
|
|
1247
|
+
if (eventCode === 6) {
|
|
1248
|
+
return { status: "Not Authorized", authorizationReason: "Access denied by HID authorization." };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const eventLabel = String(
|
|
1252
|
+
getConfigurationLabel("event_types", eventCode) || rawLog.event_name || rawLog.eventName || "",
|
|
1253
|
+
).toLowerCase();
|
|
1254
|
+
if (/not authorized|unauthori[sz]ed|access denied|denied|rejected/.test(eventLabel)) {
|
|
1255
|
+
return { status: "Not Authorized", authorizationReason: eventLabel };
|
|
1256
|
+
}
|
|
1257
|
+
if (/authorized|access granted|granted|released|opened/.test(eventLabel)) {
|
|
1258
|
+
return { status: "Authorized", authorizationReason: eventLabel };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
return {
|
|
1262
|
+
status: "Unknown",
|
|
1263
|
+
authorizationReason: "This access log does not include an authorization result.",
|
|
1163
1264
|
};
|
|
1164
1265
|
}
|
|
1165
1266
|
|
|
1166
1267
|
function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1167
1268
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1168
1269
|
if (eventIdentityId) {
|
|
1169
|
-
const byId =
|
|
1170
|
-
|
|
1270
|
+
const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
1271
|
+
const rawUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId;
|
|
1272
|
+
if (byId && (
|
|
1273
|
+
rawUserId === undefined
|
|
1274
|
+
|| rawUserId === null
|
|
1275
|
+
|| String(byId.hidUserId) === String(rawUserId)
|
|
1276
|
+
)) return byId;
|
|
1171
1277
|
}
|
|
1172
1278
|
|
|
1173
1279
|
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
1174
1280
|
const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
|
|
1175
1281
|
const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
|
|
1282
|
+
const qrCodeValue = rawLog.qrcode_value || rawLog.qrCode || rawLog.qrcode;
|
|
1176
1283
|
|
|
1177
|
-
return
|
|
1284
|
+
return allIdentities.value.find((identity) => {
|
|
1178
1285
|
return (
|
|
1179
1286
|
(hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
|
|
1180
1287
|
(registration && identity.registration === registration) ||
|
|
1181
|
-
(cardNo && String(identity.cardNo) === String(cardNo))
|
|
1288
|
+
(cardNo && String(identity.cardNo) === String(cardNo)) ||
|
|
1289
|
+
(qrCodeValue && String(identity.metadata?.qrCodeValue || "") === String(qrCodeValue))
|
|
1182
1290
|
);
|
|
1183
1291
|
});
|
|
1184
1292
|
}
|
|
1185
1293
|
|
|
1186
1294
|
function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
1187
|
-
const identityKey = getIdentityKey(identity);
|
|
1188
1295
|
const accessLogs = [
|
|
1189
1296
|
...liveAccessLogs.value,
|
|
1190
1297
|
...logs.value.flatMap((event) => getRawAccessLogs(event)),
|
|
1191
1298
|
];
|
|
1192
1299
|
|
|
1193
1300
|
return accessLogs
|
|
1194
|
-
.filter((log) =>
|
|
1301
|
+
.filter((log) => accessLogMatchesIdentity(log, identity))
|
|
1195
1302
|
.sort((a, b) => {
|
|
1196
1303
|
const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
|
|
1197
1304
|
const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
|
|
@@ -1199,11 +1306,46 @@ function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
|
1199
1306
|
})[0];
|
|
1200
1307
|
}
|
|
1201
1308
|
|
|
1309
|
+
function toUnknownRecord(value: unknown): Record<string, unknown> {
|
|
1310
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1311
|
+
? value as Record<string, unknown>
|
|
1312
|
+
: {};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function accessLogMatchesIdentity(log: unknown, identity: unknown) {
|
|
1316
|
+
const logRecord = toUnknownRecord(log);
|
|
1317
|
+
const identityRecord = toUnknownRecord(identity);
|
|
1318
|
+
const payload = toUnknownRecord(logRecord.payload);
|
|
1319
|
+
const rawLog = Object.keys(toUnknownRecord(payload.rawAccessLog)).length
|
|
1320
|
+
? toUnknownRecord(payload.rawAccessLog)
|
|
1321
|
+
: logRecord;
|
|
1322
|
+
const eventIdentity = Object.keys(toUnknownRecord(payload.identity)).length
|
|
1323
|
+
? toUnknownRecord(payload.identity)
|
|
1324
|
+
: toUnknownRecord(payload.identityLookup);
|
|
1325
|
+
const metadata = toUnknownRecord(identityRecord.metadata);
|
|
1326
|
+
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? eventIdentity.hidUserId;
|
|
1327
|
+
const registration = rawLog.registration || eventIdentity.registration;
|
|
1328
|
+
const credential =
|
|
1329
|
+
rawLog.card_value ||
|
|
1330
|
+
rawLog.cardNo ||
|
|
1331
|
+
rawLog.qrcode_value ||
|
|
1332
|
+
rawLog.qrCode ||
|
|
1333
|
+
rawLog.qrcode ||
|
|
1334
|
+
eventIdentity.cardNo;
|
|
1335
|
+
const identityCredential = identityRecord.cardNo || metadata.qrCodeValue;
|
|
1336
|
+
|
|
1337
|
+
return Boolean(
|
|
1338
|
+
(hidUserId !== undefined && hidUserId !== null && String(identityRecord.hidUserId) === String(hidUserId)) ||
|
|
1339
|
+
(registration && identityRecord.registration === registration) ||
|
|
1340
|
+
(credential && identityCredential && String(identityCredential) === String(credential)),
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1202
1344
|
function getAccessLogIdentityKey(rawLog: Record<string, any>) {
|
|
1203
1345
|
return [
|
|
1204
1346
|
rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
|
|
1205
1347
|
rawLog.registration,
|
|
1206
|
-
rawLog.card_value || rawLog.cardNo,
|
|
1348
|
+
rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
1207
1349
|
].filter(Boolean).join("|");
|
|
1208
1350
|
}
|
|
1209
1351
|
|
|
@@ -1343,6 +1485,12 @@ function drawAccessLogPdf(pdf: any, rows: PdfAccessRow[]) {
|
|
|
1343
1485
|
});
|
|
1344
1486
|
}
|
|
1345
1487
|
|
|
1488
|
+
/**
|
|
1489
|
+
* The EXPORTED PDF, not a screen. Its colours stay literal on purpose: a PDF is
|
|
1490
|
+
* white paper with dark ink whichever theme the operator happens to be in, and
|
|
1491
|
+
* theming it would produce a document that does not match what prints. Same
|
|
1492
|
+
* reasoning as the printed pass preview in `HidQrCodeConfiguration`.
|
|
1493
|
+
*/
|
|
1346
1494
|
function buildPdfHtml() {
|
|
1347
1495
|
const rows = exportRowsData.value.map((row) => ({
|
|
1348
1496
|
name: row.name,
|
|
@@ -1520,103 +1668,53 @@ function escapeHtml(value: unknown) {
|
|
|
1520
1668
|
</script>
|
|
1521
1669
|
|
|
1522
1670
|
<style scoped>
|
|
1671
|
+
/*
|
|
1672
|
+
WHAT IS LEFT AFTER THE CARD, TABS AND TABLE MOVED TO `TableMain`.
|
|
1673
|
+
|
|
1674
|
+
The toolbar, the card, the `v-tabs` strip and its seven column widths, the
|
|
1675
|
+
footer pager, the empty state and the two private chip palettes are gone -
|
|
1676
|
+
they were a private copy of the shared table drawn in literal hexes. The
|
|
1677
|
+
`compact-date-menu` / `compact-date-picker` global overrides go with the
|
|
1678
|
+
`v-date-picker` they were correcting: a native date input needs none of them.
|
|
1679
|
+
|
|
1680
|
+
What remains is the two dialogs, and every colour in them is now a token, so
|
|
1681
|
+
the dark theme reaches them for the first time.
|
|
1682
|
+
*/
|
|
1523
1683
|
.hid-access-log-dashboard {
|
|
1524
1684
|
padding: 24px 0;
|
|
1525
1685
|
width: 100%;
|
|
1526
1686
|
min-width: 0;
|
|
1527
1687
|
}
|
|
1528
1688
|
|
|
1529
|
-
.toolbar {
|
|
1530
|
-
display: flex;
|
|
1531
|
-
flex-wrap: wrap;
|
|
1532
|
-
gap: 12px;
|
|
1533
|
-
align-items: center;
|
|
1534
|
-
margin-bottom: 16px;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
1689
|
.toolbar-spacer {
|
|
1538
1690
|
flex: 1;
|
|
1539
1691
|
}
|
|
1540
1692
|
|
|
1541
|
-
.
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1693
|
+
.hid-log-filters {
|
|
1694
|
+
/* The filter row sits INSIDE the card's extension, so it needs the card's
|
|
1695
|
+
own side padding rather than the row's default bottom margin. */
|
|
1696
|
+
margin: 0;
|
|
1697
|
+
padding: 10px 14px;
|
|
1545
1698
|
}
|
|
1546
1699
|
|
|
1547
|
-
.
|
|
1548
|
-
.search-input {
|
|
1700
|
+
.hid-log-filters :deep(.app-field) {
|
|
1549
1701
|
max-width: 240px;
|
|
1550
1702
|
}
|
|
1551
1703
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
display: flex;
|
|
1558
|
-
flex-direction: column;
|
|
1559
|
-
min-height: 430px;
|
|
1560
|
-
overflow: hidden;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
.table-body {
|
|
1564
|
-
flex: 1;
|
|
1565
|
-
overflow-x: auto;
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
.table-card :deep(table) {
|
|
1569
|
-
table-layout: fixed;
|
|
1570
|
-
width: 100%;
|
|
1571
|
-
min-width: 980px;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
.table-card :deep(th),
|
|
1575
|
-
.table-card :deep(td) {
|
|
1576
|
-
white-space: nowrap;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
.table-card :deep(th:nth-child(1)),
|
|
1580
|
-
.table-card :deep(td:nth-child(1)) {
|
|
1581
|
-
width: 16%;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
.table-card :deep(th:nth-child(2)),
|
|
1585
|
-
.table-card :deep(td:nth-child(2)) {
|
|
1586
|
-
width: 14%;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
.table-card :deep(th:nth-child(3)),
|
|
1590
|
-
.table-card :deep(td:nth-child(3)) {
|
|
1591
|
-
width: 12%;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
.table-card :deep(th:nth-child(4)),
|
|
1595
|
-
.table-card :deep(td:nth-child(4)) {
|
|
1596
|
-
width: 20%;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
.table-card :deep(th:nth-child(5)),
|
|
1600
|
-
.table-card :deep(td:nth-child(5)) {
|
|
1601
|
-
width: 18%;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
.table-card :deep(th:nth-child(6)),
|
|
1605
|
-
.table-card :deep(td:nth-child(6)) {
|
|
1606
|
-
width: 14%;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
.table-card :deep(th:nth-child(7)),
|
|
1610
|
-
.table-card :deep(td:nth-child(7)) {
|
|
1611
|
-
width: 6%;
|
|
1704
|
+
/* The reader option now reads "Reader - Portal", so it needs more room than
|
|
1705
|
+
the other filters. `AppSelect`'s root class IS `app-field`, so this has to
|
|
1706
|
+
be more specific than the rule above rather than a `.app-select` one. */
|
|
1707
|
+
.hid-log-filters :deep(.app-field.reader-select) {
|
|
1708
|
+
max-width: 320px;
|
|
1612
1709
|
}
|
|
1613
1710
|
|
|
1614
1711
|
.history-dialog {
|
|
1615
1712
|
overflow: hidden;
|
|
1616
1713
|
}
|
|
1617
1714
|
|
|
1715
|
+
/* Was `#4d9bc2`, a fixed blue that sat on the dark card unchanged. */
|
|
1618
1716
|
.history-title {
|
|
1619
|
-
color:
|
|
1717
|
+
color: var(--accent-text);
|
|
1620
1718
|
font-size: 18px;
|
|
1621
1719
|
font-weight: 600;
|
|
1622
1720
|
padding: 16px 20px 8px;
|
|
@@ -1636,10 +1734,34 @@ function escapeHtml(value: unknown) {
|
|
|
1636
1734
|
.history-table-card {
|
|
1637
1735
|
overflow-x: auto;
|
|
1638
1736
|
overflow-y: hidden;
|
|
1737
|
+
border: 1px solid var(--border);
|
|
1738
|
+
border-radius: var(--r-inner);
|
|
1739
|
+
background: var(--card);
|
|
1639
1740
|
}
|
|
1640
1741
|
|
|
1641
1742
|
.history-table-card :deep(table) {
|
|
1642
1743
|
min-width: 760px;
|
|
1744
|
+
background: transparent;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/*
|
|
1748
|
+
The history table stays a `v-table` rather than a nested `TableMain` - it is
|
|
1749
|
+
a read-only strip inside a dialog with no toolbar, pager or filters, and
|
|
1750
|
+
nesting the full card inside a dialog would draw a second heading. Its
|
|
1751
|
+
colours become tokens so it follows the theme; its SHAPE is unchanged.
|
|
1752
|
+
*/
|
|
1753
|
+
.history-table-card :deep(th) {
|
|
1754
|
+
background: var(--thead);
|
|
1755
|
+
color: var(--muted);
|
|
1756
|
+
font-size: 11px;
|
|
1757
|
+
font-weight: 800;
|
|
1758
|
+
text-transform: uppercase;
|
|
1759
|
+
letter-spacing: 0.04em;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.history-table-card :deep(td) {
|
|
1763
|
+
color: var(--text);
|
|
1764
|
+
font-size: 13px;
|
|
1643
1765
|
}
|
|
1644
1766
|
|
|
1645
1767
|
.table-refresh {
|
|
@@ -1647,74 +1769,16 @@ function escapeHtml(value: unknown) {
|
|
|
1647
1769
|
display: flex;
|
|
1648
1770
|
align-items: center;
|
|
1649
1771
|
padding: 0 14px;
|
|
1650
|
-
background:
|
|
1651
|
-
border-bottom: 1px solid
|
|
1652
|
-
color:
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
.table-footer {
|
|
1656
|
-
display: flex;
|
|
1657
|
-
justify-content: flex-end;
|
|
1658
|
-
align-items: center;
|
|
1659
|
-
gap: 8px;
|
|
1660
|
-
min-height: 44px;
|
|
1661
|
-
padding: 4px 10px;
|
|
1662
|
-
border-top: 1px solid #eeeeee;
|
|
1663
|
-
background: #f6f6f6;
|
|
1664
|
-
color: #0a2638;
|
|
1665
|
-
font-size: 13px;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
.access-tabs {
|
|
1669
|
-
border-bottom: 1px solid #eeeeee;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
.access-tabs :deep(.v-slide-group__content) {
|
|
1673
|
-
display: grid;
|
|
1674
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1675
|
-
width: 100%;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
.access-tabs :deep(.v-tab) {
|
|
1679
|
-
flex: none;
|
|
1680
|
-
min-width: 0;
|
|
1681
|
-
width: 100%;
|
|
1682
|
-
max-width: none;
|
|
1683
|
-
color: #0a2638;
|
|
1684
|
-
font-size: 12px;
|
|
1685
|
-
font-weight: 600;
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
.access-tabs :deep(.v-tab--selected) {
|
|
1689
|
-
font-weight: 600;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
.status-chip {
|
|
1693
|
-
min-width: 110px;
|
|
1694
|
-
justify-content: center;
|
|
1695
|
-
font-weight: 600;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
.mapped-chip {
|
|
1699
|
-
background: #dff7e7 !important;
|
|
1700
|
-
color: #1b7f3a !important;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
.unmapped-chip {
|
|
1704
|
-
background: #fff0cc !important;
|
|
1705
|
-
color: #9a6700 !important;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
.hid-face-avatar {
|
|
1709
|
-
border: 1px solid #d8dde4;
|
|
1710
|
-
background: #f2f4f7;
|
|
1772
|
+
background: var(--thead);
|
|
1773
|
+
border-bottom: 1px solid var(--border);
|
|
1774
|
+
color: var(--muted);
|
|
1711
1775
|
}
|
|
1712
1776
|
|
|
1713
1777
|
.empty-state {
|
|
1714
1778
|
min-height: 280px;
|
|
1715
1779
|
display: grid;
|
|
1716
1780
|
place-items: center;
|
|
1717
|
-
color:
|
|
1781
|
+
color: var(--muted);
|
|
1718
1782
|
font-size: 13px;
|
|
1719
1783
|
}
|
|
1720
1784
|
|
|
@@ -1726,73 +1790,52 @@ function escapeHtml(value: unknown) {
|
|
|
1726
1790
|
font-size: 14px;
|
|
1727
1791
|
font-weight: 700;
|
|
1728
1792
|
padding: 12px 16px;
|
|
1729
|
-
border-bottom: 1px solid
|
|
1793
|
+
border-bottom: 1px solid var(--border);
|
|
1794
|
+
color: var(--text);
|
|
1730
1795
|
}
|
|
1731
1796
|
|
|
1732
1797
|
.dialog-body {
|
|
1733
1798
|
padding: 18px 16px 20px;
|
|
1734
1799
|
}
|
|
1735
1800
|
|
|
1801
|
+
/* Was `#4f5a66` / `#e53935` - the same two colours on both themes. */
|
|
1736
1802
|
.field-label {
|
|
1737
|
-
color:
|
|
1803
|
+
color: var(--text2);
|
|
1738
1804
|
font-size: 12px;
|
|
1739
1805
|
font-weight: 600;
|
|
1740
1806
|
margin-bottom: 6px;
|
|
1741
1807
|
}
|
|
1742
1808
|
|
|
1743
1809
|
.field-label span {
|
|
1744
|
-
color:
|
|
1810
|
+
color: var(--err);
|
|
1745
1811
|
}
|
|
1746
1812
|
|
|
1813
|
+
/* The split footer bar. The handoff does not draw one, so the SHAPE is the
|
|
1814
|
+
screen's own; only the colours change - `#062d42` was a fixed navy that sat
|
|
1815
|
+
on the dark card unchanged. Same treatment as panels 2 and 4. */
|
|
1747
1816
|
.dialog-actions {
|
|
1748
1817
|
display: grid;
|
|
1749
1818
|
grid-template-columns: 1fr 1fr;
|
|
1750
1819
|
}
|
|
1751
1820
|
|
|
1752
1821
|
.action-cancel {
|
|
1753
|
-
background:
|
|
1754
|
-
color:
|
|
1822
|
+
background: var(--card);
|
|
1823
|
+
color: var(--text2);
|
|
1755
1824
|
border-radius: 0;
|
|
1756
1825
|
}
|
|
1757
1826
|
|
|
1758
1827
|
.action-submit {
|
|
1759
|
-
background:
|
|
1760
|
-
color:
|
|
1828
|
+
background: var(--accent-strong);
|
|
1829
|
+
color: var(--on-accent-strong);
|
|
1761
1830
|
border-radius: 0;
|
|
1762
1831
|
}
|
|
1763
1832
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
:global(.compact-date-menu .v-overlay__content) {
|
|
1769
|
-
max-width: 320px;
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
:global(.compact-date-picker) {
|
|
1773
|
-
border-radius: 8px;
|
|
1774
|
-
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
:global(.compact-date-picker .v-picker-title),
|
|
1778
|
-
:global(.compact-date-picker .v-date-picker-header) {
|
|
1779
|
-
display: none;
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
:global(.compact-date-picker .v-picker__body) {
|
|
1783
|
-
padding-top: 4px;
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
:global(.compact-date-picker .v-date-picker-month) {
|
|
1787
|
-
padding: 0 12px 12px;
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
:global(.compact-date-picker .v-date-picker-month__day) {
|
|
1791
|
-
height: 34px;
|
|
1833
|
+
.hid-face-avatar {
|
|
1834
|
+
border: 1px solid var(--border);
|
|
1835
|
+
background: var(--hover);
|
|
1792
1836
|
}
|
|
1793
1837
|
|
|
1794
1838
|
@media (max-width: 960px) {
|
|
1795
|
-
.toolbar,
|
|
1796
1839
|
.history-toolbar {
|
|
1797
1840
|
align-items: stretch;
|
|
1798
1841
|
}
|
|
@@ -1801,17 +1844,11 @@ function escapeHtml(value: unknown) {
|
|
|
1801
1844
|
display: none;
|
|
1802
1845
|
}
|
|
1803
1846
|
|
|
1804
|
-
.
|
|
1805
|
-
.
|
|
1806
|
-
.search-input,
|
|
1807
|
-
.status-input {
|
|
1847
|
+
.hid-log-filters :deep(.app-field),
|
|
1848
|
+
.hid-log-filters :deep(.app-select) {
|
|
1808
1849
|
max-width: none;
|
|
1809
1850
|
width: 100%;
|
|
1810
1851
|
}
|
|
1811
|
-
|
|
1812
|
-
.table-card {
|
|
1813
|
-
min-height: 360px;
|
|
1814
|
-
}
|
|
1815
1852
|
}
|
|
1816
1853
|
|
|
1817
1854
|
@media (max-width: 600px) {
|
|
@@ -1819,14 +1856,6 @@ function escapeHtml(value: unknown) {
|
|
|
1819
1856
|
padding: 12px 0;
|
|
1820
1857
|
}
|
|
1821
1858
|
|
|
1822
|
-
.access-tabs :deep(.v-slide-group__content) {
|
|
1823
|
-
display: flex;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
.access-tabs :deep(.v-tab) {
|
|
1827
|
-
min-width: 132px;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
1859
|
.history-body {
|
|
1831
1860
|
padding: 0 12px 12px;
|
|
1832
1861
|
}
|
|
@@ -1843,12 +1872,5 @@ function escapeHtml(value: unknown) {
|
|
|
1843
1872
|
max-width: calc(100vw - 24px) !important;
|
|
1844
1873
|
margin: 12px !important;
|
|
1845
1874
|
}
|
|
1846
|
-
|
|
1847
|
-
:global(.compact-date-menu),
|
|
1848
|
-
:global(.compact-date-menu .v-overlay__content),
|
|
1849
|
-
:global(.compact-date-picker) {
|
|
1850
|
-
max-width: calc(100vw - 32px) !important;
|
|
1851
|
-
width: calc(100vw - 32px) !important;
|
|
1852
|
-
}
|
|
1853
1875
|
}
|
|
1854
1876
|
</style>
|