@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.
Files changed (283) hide show
  1. package/assets/css/primitives.css +1113 -0
  2. package/assets/css/screens.css +1097 -0
  3. package/assets/css/shell.css +258 -0
  4. package/assets/css/tokens.css +236 -0
  5. package/components/AcceptDialog.vue +17 -18
  6. package/components/AccessCard/AvailableStats.vue +24 -16
  7. package/components/AccessCardAddForm.vue +42 -42
  8. package/components/AccessCardAssignToUnitForm.vue +13 -21
  9. package/components/AccessCardDeleteDialog.vue +41 -39
  10. package/components/AccessCardDetailsDialog.vue +125 -79
  11. package/components/AccessCardPreviewDialog.vue +58 -84
  12. package/components/AccessCardQrTagging.vue +60 -63
  13. package/components/AccessCardReplaceForm.vue +27 -23
  14. package/components/AccessManagement.vue +86 -85
  15. package/components/AddEqupmentForm.vue +18 -22
  16. package/components/AddPassKeyToVisitor.vue +95 -73
  17. package/components/AppButton.vue +62 -0
  18. package/components/AppCard.vue +20 -0
  19. package/components/AppDateField.vue +49 -0
  20. package/components/AppField.vue +76 -0
  21. package/components/AppKpiTile.vue +65 -0
  22. package/components/AppSegmented.vue +38 -0
  23. package/components/AppSelect.vue +247 -0
  24. package/components/AreaChecklistHistoryLogs.vue +51 -44
  25. package/components/AreaChecklistHistoryMain.vue +14 -7
  26. package/components/AreaFormDialog.vue +71 -59
  27. package/components/AreaMain.vue +73 -107
  28. package/components/AttachmentsViewer.vue +84 -22
  29. package/components/AttendanceCheckInOutDialog.vue +85 -88
  30. package/components/AttendanceDetailsDialog.vue +57 -63
  31. package/components/AttendanceMain.vue +4 -3
  32. package/components/AttendanceMapSearchDialog.vue +42 -44
  33. package/components/AttendanceSettingsDialog.vue +79 -63
  34. package/components/Avatar/Main.vue +54 -2
  35. package/components/BillingMain.vue +26 -30
  36. package/components/BtnUploadFile.vue +53 -11
  37. package/components/BuildingForm.vue +34 -27
  38. package/components/BuildingManagement/buildings.vue +39 -39
  39. package/components/BuildingManagement/units.vue +26 -42
  40. package/components/BuildingUnitFormAdd.vue +20 -23
  41. package/components/BuildingUnitFormEdit.vue +8 -4
  42. package/components/BulletinBoardForm.vue +19 -24
  43. package/components/BulletinBoardManagement.vue +49 -57
  44. package/components/BulletinBoardView.vue +9 -6
  45. package/components/BulletinExpirationChip.vue +13 -4
  46. package/components/Button/Close.vue +2 -2
  47. package/components/CameraForm.vue +21 -22
  48. package/components/CameraMain.vue +50 -49
  49. package/components/Card/DeleteConfirmation.vue +24 -22
  50. package/components/Card/MemberInfoSummary.vue +28 -3
  51. package/components/Card/Toggle.vue +15 -3
  52. package/components/CardToolbar.vue +37 -0
  53. package/components/Carousel.vue +56 -36
  54. package/components/Chat/Bubbles.vue +43 -14
  55. package/components/Chat/Information.vue +49 -32
  56. package/components/Chat/ListCard.vue +38 -9
  57. package/components/Chat/Message.vue +41 -15
  58. package/components/Chat/Navigation.vue +27 -15
  59. package/components/Chat/SkeletonLoader.vue +9 -2
  60. package/components/ChecklistItemRow.vue +25 -1
  61. package/components/CleaningScheduleMain.vue +44 -49
  62. package/components/ClientDetailForm.vue +81 -56
  63. package/components/ClientMain.vue +92 -82
  64. package/components/ConfirmDialog.vue +14 -8
  65. package/components/Container/Standard.vue +9 -7
  66. package/components/CreateSubsciptionPlan.vue +148 -89
  67. package/components/DashboardEmptyState.vue +48 -3
  68. package/components/DashboardMain.vue +924 -676
  69. package/components/DashboardPanel.vue +84 -21
  70. package/components/DeliveryCompany.vue +34 -20
  71. package/components/Dialog/DeleteConfirmation.vue +20 -21
  72. package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
  73. package/components/Dialog/ReusablePrompt.vue +30 -29
  74. package/components/Dialog/UpdateMoreAction.vue +62 -59
  75. package/components/DocumentForm.vue +14 -21
  76. package/components/DocumentManagement.vue +162 -108
  77. package/components/DocumentViewer.vue +29 -12
  78. package/components/DrawImage.vue +29 -21
  79. package/components/EntryPass/QrTemplatePreview.vue +27 -16
  80. package/components/EntryPassInformation.vue +40 -10
  81. package/components/EntryPassMain.vue +49 -27
  82. package/components/EquipmentItemMain.vue +84 -97
  83. package/components/EquipmentManagementMain.vue +81 -103
  84. package/components/Facility/BookingSetup.vue +43 -52
  85. package/components/Feedback/Form.vue +26 -12
  86. package/components/FeedbackMain.vue +35 -49
  87. package/components/FileInput.vue +75 -58
  88. package/components/FileInputWithList.vue +59 -12
  89. package/components/FormDialog.vue +60 -21
  90. package/components/HidAccessLogDashboard.vue +512 -490
  91. package/components/HidCameraCaptureDialog.vue +216 -0
  92. package/components/HidEnabledGate.vue +67 -9
  93. package/components/HidIntercomManagement.vue +1322 -653
  94. package/components/HidQrCodeConfiguration.vue +1030 -258
  95. package/components/HidReaderForm.vue +142 -59
  96. package/components/HidReaderManagement.vue +211 -323
  97. package/components/HidServiceSettingsPanel.vue +44 -13
  98. package/components/HidUserEnrollment.vue +1185 -412
  99. package/components/HygieneUpdateMoreAction.vue +44 -44
  100. package/components/IncidentReport/Authorities.vue +30 -18
  101. package/components/IncidentReport/IncidentInformation.vue +10 -10
  102. package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
  103. package/components/IncidentReport/affectedEntities.vue +41 -20
  104. package/components/Input/Date.vue +3 -0
  105. package/components/Input/DatePicker.vue +1 -2
  106. package/components/Input/DateTimePicker.vue +1 -1
  107. package/components/Input/File.vue +55 -10
  108. package/components/Input/FileV2.vue +39 -6
  109. package/components/Input/InputPhoneNumberV2.vue +2 -1
  110. package/components/Input/ListGroupSelection.vue +56 -8
  111. package/components/Input/NRICNumber.vue +1 -0
  112. package/components/Input/NewDate.vue +1 -0
  113. package/components/Input/Number.vue +1 -0
  114. package/components/Input/Password.vue +1 -0
  115. package/components/Input/PhoneNumber.vue +21 -1
  116. package/components/Input/VehicleNumber.vue +1 -0
  117. package/components/InputLabel.vue +2 -2
  118. package/components/InvitationClientForm.vue +21 -19
  119. package/components/InvitationForm.vue +15 -21
  120. package/components/InvitationMain.vue +46 -61
  121. package/components/Layout/Header.vue +263 -36
  122. package/components/Layout/NavigationDrawer.vue +300 -30
  123. package/components/ListItem.vue +33 -3
  124. package/components/LocalPagination.vue +27 -3
  125. package/components/ManageChecklistMain.vue +136 -86
  126. package/components/MemberInformation.vue +32 -16
  127. package/components/MemberMain.vue +113 -93
  128. package/components/MyAttendanceMain.vue +3 -5
  129. package/components/NavigationItem.vue +83 -3
  130. package/components/Nfc/NFCPatrolReportMain.vue +47 -33
  131. package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
  132. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  133. package/components/Nfc/NFCPatrolSettings.vue +14 -4
  134. package/components/Nfc/NFCTagForm.vue +22 -8
  135. package/components/Nfc/NFCTagMain.vue +41 -43
  136. package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
  137. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
  138. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
  139. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
  140. package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
  141. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
  142. package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
  143. package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
  144. package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
  145. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
  146. package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
  147. package/components/NotificationSettings.vue +373 -0
  148. package/components/NumberSettingField.vue +9 -2
  149. package/components/OnlineFormConfigurationForm.vue +725 -237
  150. package/components/OnlineFormFill.vue +213 -106
  151. package/components/OnlineFormRenderPreview.vue +25 -30
  152. package/components/OnlineFormsConfiguration.vue +113 -109
  153. package/components/OrgViewDialog.vue +34 -16
  154. package/components/OvernightParkingAvailability.vue +49 -26
  155. package/components/OvernightParkingManagement.vue +78 -18
  156. package/components/PageHeader.vue +43 -0
  157. package/components/PassInformation.vue +109 -53
  158. package/components/PasswordConfirmation.vue +44 -26
  159. package/components/PeopleForm.vue +41 -20
  160. package/components/PhotoUpload.vue +110 -48
  161. package/components/PlaceholderComponent.vue +35 -19
  162. package/components/PlanActionsMenu.vue +12 -2
  163. package/components/PlanStatusBadge.vue +7 -8
  164. package/components/PlateNumberDisplay.vue +9 -6
  165. package/components/QrTemplate/PrintDialog.vue +66 -43
  166. package/components/RolePermissionFormCreate.vue +104 -79
  167. package/components/RolePermissionFormPreviewUpdate.vue +111 -109
  168. package/components/RolePermissionMain.vue +183 -81
  169. package/components/ScanVisitorQRCode.vue +99 -35
  170. package/components/ScheduleAreaMain.vue +34 -59
  171. package/components/ScheduleTaskForm.vue +61 -42
  172. package/components/ScheduleTaskMain.vue +56 -69
  173. package/components/SearchVehicleNumberUser.vue +39 -37
  174. package/components/ServiceProviderFormCreate.vue +29 -20
  175. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  176. package/components/ServiceProviderMain.vue +601 -432
  177. package/components/Signature.vue +40 -34
  178. package/components/SignaturePad.vue +5 -29
  179. package/components/SiteSettings.vue +41 -11
  180. package/components/SiteSettingsWorkOrder.vue +61 -30
  181. package/components/SlideCardGroup.vue +35 -11
  182. package/components/Snackbar.vue +36 -3
  183. package/components/SpecificAttr.vue +18 -8
  184. package/components/StatusChip.vue +156 -0
  185. package/components/StockCard.vue +17 -12
  186. package/components/SubscriptionPlanTable.vue +207 -140
  187. package/components/SwitchContext.vue +201 -9
  188. package/components/TableHygiene.vue +36 -46
  189. package/components/TableList.vue +10 -10
  190. package/components/TableListSecondary.vue +105 -54
  191. package/components/TableMain.vue +251 -78
  192. package/components/TableV3.vue +61 -70
  193. package/components/TiptapEditor.vue +33 -40
  194. package/components/TiptapEditorQr.vue +5 -2
  195. package/components/Tooltip/Info.vue +69 -24
  196. package/components/UnitMain.vue +40 -60
  197. package/components/UnitPersonCard.vue +42 -13
  198. package/components/VehicleAddSelection.vue +48 -32
  199. package/components/VehicleForm.vue +112 -129
  200. package/components/VehicleManagement.vue +26 -12
  201. package/components/VehicleUpdateMoreAction.vue +59 -46
  202. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  203. package/components/VisitorForm.vue +774 -210
  204. package/components/VisitorFormSelection.vue +40 -37
  205. package/components/VisitorManagement.vue +239 -82
  206. package/components/VisitorPassKeyQRScanner.vue +65 -18
  207. package/components/VisitorSearchResultSummary.vue +60 -16
  208. package/components/VisitorSocketPopUp.vue +22 -14
  209. package/components/VisitorsReportPreview.vue +44 -29
  210. package/components/WorkOrder/Create.vue +26 -17
  211. package/components/WorkOrder/ListView.vue +38 -7
  212. package/components/WorkOrder/Main.vue +62 -59
  213. package/composables/useFeedback.ts +3 -3
  214. package/composables/useHidAmico.ts +269 -0
  215. package/composables/useHidNavigation.ts +2 -9
  216. package/composables/useMember.ts +3 -3
  217. package/composables/useNFCPatrolDailyReport.ts +13 -0
  218. package/composables/useNFCPatrolMonthlyReport.ts +22 -56
  219. package/composables/useNFCPatrolReport.ts +13 -0
  220. package/composables/useNotificationPreference.ts +87 -0
  221. package/composables/useOnlineFormPages.ts +2 -0
  222. package/composables/useRole.ts +22 -0
  223. package/composables/useServiceProvider.ts +3 -3
  224. package/composables/useServiceProviderInvitation.ts +145 -0
  225. package/composables/useSipWebPhone.ts +336 -0
  226. package/composables/useSubscriptionPlan.ts +62 -0
  227. package/composables/useThemePreference.ts +63 -0
  228. package/composables/useVisitor.ts +6 -1
  229. package/composables/useWebUsb.ts +127 -37
  230. package/composables/useWorkOrder.ts +17 -3
  231. package/error.vue +30 -6
  232. package/nuxt.config.ts +36 -0
  233. package/package.json +25 -4
  234. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
  235. package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
  236. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  237. package/pages/notification-settings.vue +19 -0
  238. package/pages/payment-method-linked.vue +34 -10
  239. package/pages/require-customer.vue +48 -10
  240. package/pages/require-organization-membership.vue +32 -8
  241. package/pages/unauthorized.vue +29 -4
  242. package/plugins/secure-member.client.ts +21 -56
  243. package/plugins/vuetify.ts +69 -2
  244. package/types/nfc-patrol-report.ts +3 -2
  245. package/types/site.d.ts +71 -0
  246. package/types/subscription.ts +18 -11
  247. package/utils/breadcrumb.test.ts +108 -0
  248. package/utils/breadcrumb.ts +82 -0
  249. package/utils/dashboard.test.ts +118 -0
  250. package/utils/dashboard.ts +53 -0
  251. package/utils/data.test.ts +95 -0
  252. package/utils/data.ts +40 -2
  253. package/utils/hid-discover.test.ts +102 -0
  254. package/utils/list-page.test.ts +60 -0
  255. package/utils/list-page.ts +38 -0
  256. package/utils/materialSymbols.ts +363 -0
  257. package/utils/nav-sections.test.ts +190 -0
  258. package/utils/nav-sections.ts +151 -0
  259. package/utils/nfc-patrol-report.test.ts +130 -0
  260. package/utils/nfc-patrol-report.ts +101 -0
  261. package/utils/role.test.ts +69 -0
  262. package/utils/role.ts +10 -0
  263. package/utils/route-name.test.ts +85 -0
  264. package/utils/route-name.ts +24 -0
  265. package/utils/status.test.ts +110 -0
  266. package/utils/status.ts +135 -0
  267. package/utils/table-card.test.ts +84 -0
  268. package/utils/theme-aa-ledger.ts +148 -0
  269. package/utils/theme.test.ts +485 -7
  270. package/utils/theme.ts +295 -110
  271. package/.changeset/README.md +0 -8
  272. package/.changeset/config.json +0 -11
  273. package/.editorconfig +0 -12
  274. package/.github/workflows/main.yml +0 -17
  275. package/.github/workflows/publish-staging.yml +0 -47
  276. package/.github/workflows/publish.yml +0 -39
  277. package/PUBLISHING.md +0 -269
  278. package/components/HidIdentityMapping.vue +0 -975
  279. package/components/ScheduleTaskAreaFormDialog.vue +0 -144
  280. package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
  281. package/components/ScheduleTastTicketMain.vue +0 -182
  282. package/components/TableWithButton.vue +0 -94
  283. 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
- <div class="toolbar">
4
- <v-btn
5
- rounded="xl"
6
- class="text-none export-button"
7
- variant="flat"
8
- prepend-icon="mdi-download"
9
- @click="openExportDialog"
10
- >
11
- Export
12
- </v-btn>
13
-
14
- <v-select
15
- v-if="readers.length > 1"
16
- v-model="selectedReaderId"
17
- :items="readerOptions"
18
- item-title="title"
19
- item-value="value"
20
- variant="outlined"
21
- density="compact"
22
- hide-details
23
- class="reader-input"
24
- @update:model-value="loadDashboard"
25
- />
26
-
27
- <div class="toolbar-spacer" />
28
-
29
- <v-text-field
30
- v-model="search"
31
- placeholder="Search"
32
- variant="outlined"
33
- density="compact"
34
- hide-details
35
- class="search-input"
36
- />
37
-
38
- <v-select
39
- v-model="status"
40
- :items="statusOptions"
41
- variant="outlined"
42
- density="compact"
43
- hide-details
44
- class="status-input"
45
- />
46
- </div>
47
-
48
- <v-card flat border class="table-card">
49
- <v-tabs
50
- v-model="activeTab"
51
- class="access-tabs"
52
- density="compact"
53
- @update:model-value="onTabChange"
54
- >
55
- <v-tab value="resident" class="text-none">Resident</v-tab>
56
- <v-tab value="visitor" class="text-none">Visitor</v-tab>
57
- <v-tab value="administrator" class="text-none">Administrator</v-tab>
58
- </v-tabs>
59
-
60
- <div class="table-body">
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
- </div>
111
-
112
- <div class="table-footer">
113
- <span>{{ pageRange }}</span>
114
- <v-btn
115
- icon="mdi-chevron-left"
116
- variant="text"
117
- density="comfortable"
118
- :disabled="page <= 1"
119
- @click="page--"
120
- />
121
- <v-btn
122
- icon="mdi-chevron-right"
123
- variant="text"
124
- density="comfortable"
125
- :disabled="page >= pages"
126
- @click="page++"
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
- </div>
129
- </v-card>
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
- <v-btn
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
- </v-btn>
158
+ </AppButton>
146
159
  <div class="toolbar-spacer" />
147
- <v-btn icon="mdi-close" variant="text" density="comfortable" @click="historyDialog = false" />
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-menu
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-menu
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
- <v-select
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
- <v-select
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
- <v-select
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
- <v-select
262
+ <AppSelect
291
263
  v-model="exportForm.status"
292
264
  :items="statusOptions"
293
265
  aria-label="Status"
294
266
  placeholder="Select"
295
- density="compact"
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: "Mapped" | "Unmapped";
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("Status");
338
+ const status = ref("");
339
+ const accessMethod = ref("");
364
340
  const page = ref(1);
365
- const limit = 20;
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: "Status",
354
+ status: "",
381
355
  });
382
356
 
383
- const statusOptions = ["Status", "Mapped", "Unmapped"];
384
- const accessMethodOptions = ["All", "Facial", "QR Code", "ID and Password", "PIN", "Card"];
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 matchedLogs = logs.value.filter((log) => getLogIdentityKey(log) === selectedKey);
422
- const matchedLiveLogs = liveAccessLogs.value.filter((log) => getAccessLogIdentityKey(log) === selectedKey);
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
- status: selectedHistoryRow.value?.status || "Mapped",
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
- status: selectedHistoryRow.value?.status || "Mapped",
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 === "Status" || row.status === 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 === "Status" || row.status === 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
- getIdentities(selectedReaderId.value, {
553
- page: 1,
554
- limit: 500,
555
- ...(!isAdministratorTab ? { type: getIdentityType(activeTab.value) } : {}),
556
- }),
557
- getLogs(selectedReaderId.value, {
558
- page: 1,
559
- limit: 500,
560
- }),
561
- loadLiveAccessLogs(selectedReaderId.value),
562
- loadReaderConfiguration(selectedReaderId.value),
563
- isAdministratorTab ? loadAdministratorRoles(selectedReaderId.value) : Promise.resolve(null),
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
- ? getVisitors({
566
- org: props.org,
567
- site: props.site,
568
- page: 1,
569
- limit: 500,
570
- order: "desc",
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 = isVisitorTab
582
- ? Object.fromEntries(loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]))
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
- identities.value = isAdministratorTab
586
- ? mergedIdentities.filter((identity: Record<string, any>) => {
587
- const hidUserId = toHidNumericId(identity.hidUserId);
588
- return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
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
- if (activeTab.value === "visitor") {
784
- return Boolean(identity && getVisitorRecord(identity));
785
- }
808
+ return classifyIdentity(identity, row) === activeTab.value;
809
+ }
786
810
 
787
- if (activeTab.value === "administrator") {
788
- const hidUserId = toHidNumericId(identity?.hidUserId ?? row.identityKey.split("|")[0]);
789
- return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
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
- return !identity || !getVisitorRecord(identity);
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 identities.value.find((identity) => getIdentityKey(identity) === identityKey);
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
- function setExportDate(field: "startDate" | "endDate", value: unknown) {
1011
- const date = normalizePickerDate(value);
1012
- if (!date) return;
1013
-
1014
- exportForm[field] = toDisplayDate(date);
1015
- if (field === "startDate") {
1016
- startDateMenu.value = false;
1017
- } else {
1018
- endDateMenu.value = false;
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
- return Array.isArray(accessLogs) ? accessLogs : [];
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
- status: identity ? "Mapped" : "Unmapped",
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 = identities.value.find((identity) => String(identity._id) === String(eventIdentityId));
1170
- if (byId) return byId;
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 identities.value.find((identity) => {
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) => getAccessLogIdentityKey(log) === identityKey)
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
- .export-button {
1542
- min-width: 170px;
1543
- background: #e4e4e4;
1544
- color: #0a2638;
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
- .reader-input,
1548
- .search-input {
1700
+ .hid-log-filters :deep(.app-field) {
1549
1701
  max-width: 240px;
1550
1702
  }
1551
1703
 
1552
- .status-input {
1553
- max-width: 160px;
1554
- }
1555
-
1556
- .table-card {
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: #4d9bc2;
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: #f3f3f3;
1651
- border-bottom: 1px solid #e7e7e7;
1652
- color: #0a2638;
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: #687382;
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 #ececec;
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: #4f5a66;
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: #e53935;
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: #ffffff;
1754
- color: #111827;
1822
+ background: var(--card);
1823
+ color: var(--text2);
1755
1824
  border-radius: 0;
1756
1825
  }
1757
1826
 
1758
1827
  .action-submit {
1759
- background: #062d42;
1760
- color: #ffffff;
1828
+ background: var(--accent-strong);
1829
+ color: var(--on-accent-strong);
1761
1830
  border-radius: 0;
1762
1831
  }
1763
1832
 
1764
- :global(.compact-date-menu) {
1765
- max-width: 320px;
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
- .export-button,
1805
- .reader-input,
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>