@7365admin1/layer-common 3.2.2-staging.120 → 3.2.2-staging.121
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 +168 -0
- package/assets/css/screens.css +76 -1
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AppDateField.vue +15 -1
- package/components/AppField.vue +25 -0
- package/components/BuildingUnitFormEdit.vue +5 -1
- package/components/Card/DeleteConfirmation.vue +5 -2
- package/components/HidAccessLogDashboard.vue +288 -447
- package/components/HidIntercomManagement.vue +387 -521
- package/components/HidQrCodeConfiguration.vue +293 -369
- package/components/HidReaderManagement.vue +197 -320
- package/components/HidServiceSettingsPanel.vue +38 -26
- package/components/HidUserEnrollment.vue +239 -344
- package/components/Nfc/NFCTagForm.vue +5 -1
- package/components/TableMain.vue +50 -1
- package/components/VisitorsReportPreview.vue +4 -1
- package/package.json +1 -1
- package/tools/render-harness/baselines.json +95 -0
- package/tools/render-harness/harness-init.ps1 +4 -0
- package/utils/status.test.ts +33 -0
- package/utils/status.ts +36 -0
- package/utils/theme-aa-ledger.ts +142 -0
- package/utils/theme.test.ts +95 -34
- package/utils/theme.ts +55 -28
|
@@ -1,140 +1,140 @@
|
|
|
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
|
-
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>Last Access Method</th>
|
|
67
|
-
<th>Last access time</th>
|
|
68
|
-
<th>Status</th>
|
|
69
|
-
<th class="text-right"></th>
|
|
70
|
-
</tr>
|
|
71
|
-
</thead>
|
|
72
|
-
<tbody>
|
|
73
|
-
<tr v-for="row in paginatedRows" :key="row.key">
|
|
74
|
-
<td>
|
|
75
|
-
<div class="name-cell">
|
|
76
|
-
{{ row.name }}
|
|
77
|
-
<v-tooltip v-if="row.name && row.name !== 'N/A'" activator="parent" location="top">
|
|
78
|
-
{{ row.name }}
|
|
79
|
-
</v-tooltip>
|
|
80
|
-
</div>
|
|
81
|
-
</td>
|
|
82
|
-
<td>
|
|
83
|
-
<v-avatar v-if="row.faceImage" size="34" rounded="lg" class="hid-face-avatar">
|
|
84
|
-
<v-img :src="row.faceImage" cover />
|
|
85
|
-
</v-avatar>
|
|
86
|
-
<span v-else>{{ row.facialData }}</span>
|
|
87
|
-
</td>
|
|
88
|
-
<td>{{ row.method }}</td>
|
|
89
|
-
<td>{{ row.lastAccessTime }}</td>
|
|
90
|
-
<td>
|
|
91
|
-
<v-chip
|
|
92
|
-
size="small"
|
|
93
|
-
variant="flat"
|
|
94
|
-
class="status-chip"
|
|
95
|
-
:class="getAuthorizationChipClass(row.status)"
|
|
96
|
-
>
|
|
97
|
-
{{ row.status }}
|
|
98
|
-
<v-tooltip v-if="row.authorizationReason" activator="parent" location="top">
|
|
99
|
-
{{ row.authorizationReason }}
|
|
100
|
-
</v-tooltip>
|
|
101
|
-
</v-chip>
|
|
102
|
-
</td>
|
|
103
|
-
<td class="text-right">
|
|
104
|
-
<v-btn
|
|
105
|
-
icon="mdi-history"
|
|
106
|
-
variant="text"
|
|
107
|
-
density="comfortable"
|
|
108
|
-
@click="openHistoryDialog(row)"
|
|
109
|
-
/>
|
|
110
|
-
</td>
|
|
111
|
-
</tr>
|
|
112
|
-
</tbody>
|
|
113
|
-
</v-table>
|
|
114
|
-
|
|
115
|
-
<div v-if="!filteredRows.length" class="empty-state">
|
|
116
|
-
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 v-model="status" :items="statusOptions" />
|
|
117
69
|
</div>
|
|
118
|
-
</
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<!-- Staging's hover-the-full-name tooltip, kept. -->
|
|
73
|
+
<template #[`item.name`]="{ item }">
|
|
74
|
+
<span class="app-cell--strong">
|
|
75
|
+
{{ item.name }}
|
|
76
|
+
<v-tooltip
|
|
77
|
+
v-if="item.name && item.name !== 'N/A'"
|
|
78
|
+
activator="parent"
|
|
79
|
+
location="top"
|
|
80
|
+
>
|
|
81
|
+
{{ item.name }}
|
|
82
|
+
</v-tooltip>
|
|
83
|
+
</span>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<!-- The face thumbnail, unchanged. It prints whatever the row already
|
|
87
|
+
carried; this screen does not read or export a template. -->
|
|
88
|
+
<template #[`item.facialData`]="{ item }">
|
|
89
|
+
<v-avatar v-if="item.faceImage" size="34" rounded="lg" class="hid-face-avatar">
|
|
90
|
+
<v-img :src="item.faceImage" cover />
|
|
91
|
+
</v-avatar>
|
|
92
|
+
<span v-else class="app-cell--muted">{{ item.facialData }}</span>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<template #[`item.method`]="{ item }">
|
|
96
|
+
<span class="app-cell--muted">{{ item.method }}</span>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<template #[`item.lastAccessTime`]="{ item }">
|
|
100
|
+
<span class="app-cell--num">{{ item.lastAccessTime }}</span>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<!-- The shared chip. This screen's `mapped-chip` / `unmapped-chip` /
|
|
104
|
+
`unknown-chip` were a private copy of the same three tones
|
|
105
|
+
`utils/status.ts` now carries for Authorized / Not Authorized /
|
|
106
|
+
Unknown. Staging's reason tooltip rides on the chip. -->
|
|
107
|
+
<template #[`item.status`]="{ item }">
|
|
108
|
+
<StatusChip :status="item.status">
|
|
109
|
+
{{ item.status }}
|
|
110
|
+
<v-tooltip
|
|
111
|
+
v-if="item.authorizationReason"
|
|
112
|
+
activator="parent"
|
|
113
|
+
location="top"
|
|
114
|
+
>
|
|
115
|
+
{{ item.authorizationReason }}
|
|
116
|
+
</v-tooltip>
|
|
117
|
+
</StatusChip>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template #[`item.action-table`]="{ item }">
|
|
121
|
+
<AppButton
|
|
122
|
+
variant="row"
|
|
123
|
+
icon="mdi-history"
|
|
124
|
+
aria-label="View access history"
|
|
125
|
+
@click="openHistoryDialog(item)"
|
|
135
126
|
/>
|
|
136
|
-
</
|
|
137
|
-
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<!-- The screen's own sentence, kept: the shared empty state says "No data
|
|
130
|
+
available", which says less than naming what was searched for. -->
|
|
131
|
+
<template #no-data>
|
|
132
|
+
<div class="table-card__empty">
|
|
133
|
+
<v-icon icon="mdi-history" size="32" />
|
|
134
|
+
<span>No access logs found.</span>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
</TableMain>
|
|
138
138
|
|
|
139
139
|
<v-dialog v-model="historyDialog" max-width="980">
|
|
140
140
|
<v-card class="history-dialog" rounded="lg">
|
|
@@ -142,17 +142,16 @@
|
|
|
142
142
|
|
|
143
143
|
<v-card-text class="history-body">
|
|
144
144
|
<div class="history-toolbar">
|
|
145
|
-
<
|
|
146
|
-
rounded="xl"
|
|
147
|
-
class="text-none export-button"
|
|
148
|
-
variant="flat"
|
|
149
|
-
prepend-icon="mdi-download"
|
|
150
|
-
@click="openExportDialog('history')"
|
|
151
|
-
>
|
|
145
|
+
<AppButton icon="mdi-download" @click="openExportDialog('history')">
|
|
152
146
|
Export Log
|
|
153
|
-
</
|
|
147
|
+
</AppButton>
|
|
154
148
|
<div class="toolbar-spacer" />
|
|
155
|
-
<
|
|
149
|
+
<AppButton
|
|
150
|
+
variant="icon"
|
|
151
|
+
icon="mdi-close"
|
|
152
|
+
aria-label="Close access log"
|
|
153
|
+
@click="historyDialog = false"
|
|
154
|
+
/>
|
|
156
155
|
</div>
|
|
157
156
|
|
|
158
157
|
<v-card flat border class="history-table-card">
|
|
@@ -177,18 +176,17 @@
|
|
|
177
176
|
<td>{{ row.readerLocation }}</td>
|
|
178
177
|
<td>{{ row.method }}</td>
|
|
179
178
|
<td>{{ row.lastAccessTime }}</td>
|
|
179
|
+
<!-- Staging's authorization column, on the shared chip: the
|
|
180
|
+
private `mapped-chip`/`unmapped-chip`/`unknown-chip`
|
|
181
|
+
palette is gone, and `utils/status.ts` carries the same
|
|
182
|
+
three tones for these three words. -->
|
|
180
183
|
<td>
|
|
181
|
-
<
|
|
182
|
-
size="small"
|
|
183
|
-
variant="flat"
|
|
184
|
-
class="status-chip"
|
|
185
|
-
:class="getAuthorizationChipClass(row.status)"
|
|
186
|
-
>
|
|
184
|
+
<StatusChip :status="row.status">
|
|
187
185
|
{{ row.status }}
|
|
188
186
|
<v-tooltip v-if="row.authorizationReason" activator="parent" location="top">
|
|
189
187
|
{{ row.authorizationReason }}
|
|
190
188
|
</v-tooltip>
|
|
191
|
-
</
|
|
189
|
+
</StatusChip>
|
|
192
190
|
</td>
|
|
193
191
|
</tr>
|
|
194
192
|
</tbody>
|
|
@@ -207,116 +205,54 @@
|
|
|
207
205
|
<v-card-title class="dialog-title">Export</v-card-title>
|
|
208
206
|
|
|
209
207
|
<v-card-text class="dialog-body">
|
|
208
|
+
<!--
|
|
209
|
+
Was a `v-menu` + `v-text-field` + `v-date-picker` per date. The
|
|
210
|
+
design's date end is `AppDateField` - a native `<input type="date">`,
|
|
211
|
+
which brings the platform's own picker, localisation and keyboard
|
|
212
|
+
support instead of a 320px popover. `exportForm` still holds
|
|
213
|
+
DD/MM/YYYY, so everything downstream is untouched; the two computeds
|
|
214
|
+
below translate at the edge.
|
|
215
|
+
-->
|
|
210
216
|
<div class="field-label">Start Date <span>*</span></div>
|
|
211
|
-
<v-
|
|
212
|
-
v-model="startDateMenu"
|
|
213
|
-
:close-on-content-click="false"
|
|
214
|
-
location="bottom"
|
|
215
|
-
content-class="compact-date-menu"
|
|
216
|
-
>
|
|
217
|
-
<template #activator="{ props: menuProps }">
|
|
218
|
-
<v-text-field
|
|
219
|
-
v-bind="menuProps"
|
|
220
|
-
v-model="exportForm.startDate"
|
|
221
|
-
aria-label="Start Date"
|
|
222
|
-
prepend-inner-icon="mdi-calendar"
|
|
223
|
-
placeholder="DD/MM/YYYY"
|
|
224
|
-
density="compact"
|
|
225
|
-
variant="outlined"
|
|
226
|
-
hide-details="auto"
|
|
227
|
-
readonly
|
|
228
|
-
class="mb-3"
|
|
229
|
-
/>
|
|
230
|
-
</template>
|
|
231
|
-
<v-date-picker
|
|
232
|
-
:model-value="parseExportDate(exportForm.startDate, false)"
|
|
233
|
-
hide-header
|
|
234
|
-
width="320"
|
|
235
|
-
class="compact-date-picker"
|
|
236
|
-
@update:model-value="setExportDate('startDate', $event)"
|
|
237
|
-
/>
|
|
238
|
-
</v-menu>
|
|
217
|
+
<AppDateField v-model="exportStartInput" aria-label="Start Date" class="mb-3" />
|
|
239
218
|
|
|
240
219
|
<div class="field-label">End Date <span>*</span></div>
|
|
241
|
-
<v-
|
|
242
|
-
v-model="endDateMenu"
|
|
243
|
-
:close-on-content-click="false"
|
|
244
|
-
location="bottom"
|
|
245
|
-
content-class="compact-date-menu"
|
|
246
|
-
>
|
|
247
|
-
<template #activator="{ props: menuProps }">
|
|
248
|
-
<v-text-field
|
|
249
|
-
v-bind="menuProps"
|
|
250
|
-
v-model="exportForm.endDate"
|
|
251
|
-
aria-label="End Date"
|
|
252
|
-
prepend-inner-icon="mdi-calendar"
|
|
253
|
-
placeholder="DD/MM/YYYY"
|
|
254
|
-
density="compact"
|
|
255
|
-
variant="outlined"
|
|
256
|
-
hide-details="auto"
|
|
257
|
-
readonly
|
|
258
|
-
class="mb-3"
|
|
259
|
-
/>
|
|
260
|
-
</template>
|
|
261
|
-
<v-date-picker
|
|
262
|
-
:model-value="parseExportDate(exportForm.endDate, false)"
|
|
263
|
-
hide-header
|
|
264
|
-
width="320"
|
|
265
|
-
class="compact-date-picker"
|
|
266
|
-
@update:model-value="setExportDate('endDate', $event)"
|
|
267
|
-
/>
|
|
268
|
-
</v-menu>
|
|
220
|
+
<AppDateField v-model="exportEndInput" aria-label="End Date" class="mb-3" />
|
|
269
221
|
|
|
270
222
|
<div class="field-label">Users</div>
|
|
271
|
-
<
|
|
223
|
+
<AppSelect
|
|
272
224
|
v-model="exportForm.users"
|
|
273
225
|
:items="userOptions"
|
|
274
|
-
item-title="title"
|
|
275
|
-
item-value="value"
|
|
276
226
|
aria-label="Users"
|
|
277
227
|
placeholder="Select"
|
|
278
|
-
density="compact"
|
|
279
|
-
variant="outlined"
|
|
280
|
-
hide-details="auto"
|
|
281
228
|
multiple
|
|
282
|
-
chips
|
|
283
|
-
closable-chips
|
|
284
229
|
class="mb-3"
|
|
285
230
|
/>
|
|
286
231
|
|
|
287
232
|
<div class="field-label">Access Method</div>
|
|
288
|
-
<
|
|
233
|
+
<AppSelect
|
|
289
234
|
v-model="exportForm.accessMethod"
|
|
290
235
|
:items="accessMethodOptions"
|
|
291
236
|
aria-label="Access Method"
|
|
292
237
|
placeholder="Select"
|
|
293
|
-
density="compact"
|
|
294
|
-
variant="outlined"
|
|
295
|
-
hide-details="auto"
|
|
296
238
|
class="mb-3"
|
|
297
239
|
/>
|
|
298
240
|
|
|
299
241
|
<div class="field-label">Reader Location</div>
|
|
300
|
-
<
|
|
242
|
+
<AppSelect
|
|
301
243
|
v-model="exportForm.readerLocation"
|
|
302
244
|
:items="readerLocationOptions"
|
|
303
245
|
aria-label="Reader Location"
|
|
304
246
|
placeholder="Select"
|
|
305
|
-
density="compact"
|
|
306
|
-
variant="outlined"
|
|
307
|
-
hide-details="auto"
|
|
308
247
|
class="mb-3"
|
|
309
248
|
/>
|
|
310
249
|
|
|
311
250
|
<div class="field-label">Status (Optional)</div>
|
|
312
|
-
<
|
|
251
|
+
<AppSelect
|
|
313
252
|
v-model="exportForm.status"
|
|
314
253
|
:items="statusOptions"
|
|
315
254
|
aria-label="Status"
|
|
316
255
|
placeholder="Select"
|
|
317
|
-
density="compact"
|
|
318
|
-
variant="outlined"
|
|
319
|
-
hide-details="auto"
|
|
320
256
|
/>
|
|
321
257
|
</v-card-text>
|
|
322
258
|
|
|
@@ -396,8 +332,6 @@ const exportDialog = ref(false);
|
|
|
396
332
|
const historyDialog = ref(false);
|
|
397
333
|
const selectedHistoryRow = ref<AccessRow>();
|
|
398
334
|
const exportScope = ref<"dashboard" | "history">("dashboard");
|
|
399
|
-
const startDateMenu = ref(false);
|
|
400
|
-
const endDateMenu = ref(false);
|
|
401
335
|
const exportForm = reactive({
|
|
402
336
|
startDate: "",
|
|
403
337
|
endDate: "",
|
|
@@ -514,6 +448,33 @@ const filteredRows = computed(() => {
|
|
|
514
448
|
});
|
|
515
449
|
});
|
|
516
450
|
|
|
451
|
+
/**
|
|
452
|
+
* The three tabs, as data rather than three `v-tab` elements, so the shared
|
|
453
|
+
* `app-tab` strip can render them. Same values, same order, same handler.
|
|
454
|
+
*/
|
|
455
|
+
const tabOptions: Array<{ label: string; value: AccessTab }> = [
|
|
456
|
+
{ label: "Resident", value: "resident" },
|
|
457
|
+
{ label: "Visitor", value: "visitor" },
|
|
458
|
+
{ label: "Administrator", value: "administrator" },
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* `sortable: false` keeps the previous behaviour - the raw `v-table` never
|
|
463
|
+
* sorted - and no `weight` is set, so the table uses `TableMain`'s automatic
|
|
464
|
+
* layout. The handoff draws no HID table, so the screen's own fixed column
|
|
465
|
+
* percentages had no design number behind them.
|
|
466
|
+
*/
|
|
467
|
+
const logHeaders = [
|
|
468
|
+
{ title: "Name", value: "name", sortable: false },
|
|
469
|
+
{ title: "Facial Data", value: "facialData", sortable: false },
|
|
470
|
+
// Staging (b0667a7) dropped Unit from this table - the unit still shows in
|
|
471
|
+
// the per-identity history dialog, so the column is not carried back.
|
|
472
|
+
{ title: "Last Access Method", value: "method", sortable: false },
|
|
473
|
+
{ title: "Last access time", value: "lastAccessTime", sortable: false },
|
|
474
|
+
{ title: "Status", value: "status", sortable: false },
|
|
475
|
+
{ title: "", value: "action-table", sortable: false },
|
|
476
|
+
];
|
|
477
|
+
|
|
517
478
|
const pages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / limit)));
|
|
518
479
|
|
|
519
480
|
const paginatedRows = computed(() => {
|
|
@@ -1062,18 +1023,32 @@ function parseExportDate(value: string, endOfDay: boolean) {
|
|
|
1062
1023
|
return date;
|
|
1063
1024
|
}
|
|
1064
1025
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1026
|
+
/**
|
|
1027
|
+
* `exportForm` stores DD/MM/YYYY, because that is what `parseExportDate`,
|
|
1028
|
+
* `isWithinExportRange` and the PDF header all read. A native `<input
|
|
1029
|
+
* type="date">` speaks YYYY-MM-DD. Rather than change the stored format - which
|
|
1030
|
+
* would reach into the export itself - the translation happens here, at the
|
|
1031
|
+
* edge, exactly where the old `v-date-picker` did it.
|
|
1032
|
+
*/
|
|
1033
|
+
function exportDateModel(field: "startDate" | "endDate") {
|
|
1034
|
+
return computed({
|
|
1035
|
+
get: () => {
|
|
1036
|
+
const date = parseExportDate(exportForm[field], false);
|
|
1037
|
+
if (!date) return "";
|
|
1038
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1039
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1040
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
1041
|
+
},
|
|
1042
|
+
set: (value: string) => {
|
|
1043
|
+
const date = normalizePickerDate(value);
|
|
1044
|
+
exportForm[field] = date ? toDisplayDate(date) : "";
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1075
1047
|
}
|
|
1076
1048
|
|
|
1049
|
+
const exportStartInput = exportDateModel("startDate");
|
|
1050
|
+
const exportEndInput = exportDateModel("endDate");
|
|
1051
|
+
|
|
1077
1052
|
function normalizePickerDate(value: unknown) {
|
|
1078
1053
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
1079
1054
|
return value;
|
|
@@ -1274,12 +1249,6 @@ function getAuthorizationStatus(
|
|
|
1274
1249
|
};
|
|
1275
1250
|
}
|
|
1276
1251
|
|
|
1277
|
-
function getAuthorizationChipClass(status: AuthorizationStatus) {
|
|
1278
|
-
if (status === "Authorized") return "mapped-chip";
|
|
1279
|
-
if (status === "Not Authorized") return "unmapped-chip";
|
|
1280
|
-
return "unknown-chip";
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
1252
|
function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1284
1253
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1285
1254
|
if (eventIdentityId) {
|
|
@@ -1501,6 +1470,12 @@ function drawAccessLogPdf(pdf: any, rows: PdfAccessRow[]) {
|
|
|
1501
1470
|
});
|
|
1502
1471
|
}
|
|
1503
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* The EXPORTED PDF, not a screen. Its colours stay literal on purpose: a PDF is
|
|
1475
|
+
* white paper with dark ink whichever theme the operator happens to be in, and
|
|
1476
|
+
* theming it would produce a document that does not match what prints. Same
|
|
1477
|
+
* reasoning as the printed pass preview in `HidQrCodeConfiguration`.
|
|
1478
|
+
*/
|
|
1504
1479
|
function buildPdfHtml() {
|
|
1505
1480
|
const rows = exportRowsData.value.map((row) => ({
|
|
1506
1481
|
name: row.name,
|
|
@@ -1678,106 +1653,53 @@ function escapeHtml(value: unknown) {
|
|
|
1678
1653
|
</script>
|
|
1679
1654
|
|
|
1680
1655
|
<style scoped>
|
|
1656
|
+
/*
|
|
1657
|
+
WHAT IS LEFT AFTER THE CARD, TABS AND TABLE MOVED TO `TableMain`.
|
|
1658
|
+
|
|
1659
|
+
The toolbar, the card, the `v-tabs` strip and its seven column widths, the
|
|
1660
|
+
footer pager, the empty state and the two private chip palettes are gone -
|
|
1661
|
+
they were a private copy of the shared table drawn in literal hexes. The
|
|
1662
|
+
`compact-date-menu` / `compact-date-picker` global overrides go with the
|
|
1663
|
+
`v-date-picker` they were correcting: a native date input needs none of them.
|
|
1664
|
+
|
|
1665
|
+
What remains is the two dialogs, and every colour in them is now a token, so
|
|
1666
|
+
the dark theme reaches them for the first time.
|
|
1667
|
+
*/
|
|
1681
1668
|
.hid-access-log-dashboard {
|
|
1682
1669
|
padding: 24px 0;
|
|
1683
1670
|
width: 100%;
|
|
1684
1671
|
min-width: 0;
|
|
1685
1672
|
}
|
|
1686
1673
|
|
|
1687
|
-
.toolbar {
|
|
1688
|
-
display: flex;
|
|
1689
|
-
flex-wrap: wrap;
|
|
1690
|
-
gap: 12px;
|
|
1691
|
-
align-items: center;
|
|
1692
|
-
margin-bottom: 16px;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
1674
|
.toolbar-spacer {
|
|
1696
1675
|
flex: 1;
|
|
1697
1676
|
}
|
|
1698
1677
|
|
|
1699
|
-
.
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1678
|
+
.hid-log-filters {
|
|
1679
|
+
/* The filter row sits INSIDE the card's extension, so it needs the card's
|
|
1680
|
+
own side padding rather than the row's default bottom margin. */
|
|
1681
|
+
margin: 0;
|
|
1682
|
+
padding: 10px 14px;
|
|
1703
1683
|
}
|
|
1704
1684
|
|
|
1705
|
-
.
|
|
1706
|
-
.search-input {
|
|
1685
|
+
.hid-log-filters :deep(.app-field) {
|
|
1707
1686
|
max-width: 240px;
|
|
1708
1687
|
}
|
|
1709
1688
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
display: flex;
|
|
1716
|
-
flex-direction: column;
|
|
1717
|
-
min-height: 430px;
|
|
1718
|
-
overflow: hidden;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
.table-body {
|
|
1722
|
-
flex: 1;
|
|
1723
|
-
overflow-x: auto;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
.table-card :deep(table) {
|
|
1727
|
-
table-layout: fixed;
|
|
1728
|
-
width: 100%;
|
|
1729
|
-
min-width: 880px;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
.table-card :deep(th),
|
|
1733
|
-
.table-card :deep(td) {
|
|
1734
|
-
white-space: nowrap;
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
.table-card :deep(th:nth-child(1)),
|
|
1738
|
-
.table-card :deep(td:nth-child(1)) {
|
|
1739
|
-
width: 25%;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
.table-card :deep(th:nth-child(2)),
|
|
1743
|
-
.table-card :deep(td:nth-child(2)) {
|
|
1744
|
-
width: 13%;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
.table-card :deep(th:nth-child(3)),
|
|
1748
|
-
.table-card :deep(td:nth-child(3)) {
|
|
1749
|
-
width: 21%;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
.table-card :deep(th:nth-child(4)),
|
|
1753
|
-
.table-card :deep(td:nth-child(4)) {
|
|
1754
|
-
width: 21%;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
.table-card :deep(th:nth-child(5)),
|
|
1758
|
-
.table-card :deep(td:nth-child(5)) {
|
|
1759
|
-
width: 14%;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
.table-card :deep(th:nth-child(6)),
|
|
1763
|
-
.table-card :deep(td:nth-child(6)) {
|
|
1764
|
-
width: 6%;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
.name-cell {
|
|
1768
|
-
display: block;
|
|
1769
|
-
min-width: 0;
|
|
1770
|
-
overflow: hidden;
|
|
1771
|
-
text-overflow: ellipsis;
|
|
1772
|
-
white-space: nowrap;
|
|
1689
|
+
/* The reader option now reads "Reader - Portal", so it needs more room than
|
|
1690
|
+
the other filters. `AppSelect`'s root class IS `app-field`, so this has to
|
|
1691
|
+
be more specific than the rule above rather than a `.app-select` one. */
|
|
1692
|
+
.hid-log-filters :deep(.app-field.reader-select) {
|
|
1693
|
+
max-width: 320px;
|
|
1773
1694
|
}
|
|
1774
1695
|
|
|
1775
1696
|
.history-dialog {
|
|
1776
1697
|
overflow: hidden;
|
|
1777
1698
|
}
|
|
1778
1699
|
|
|
1700
|
+
/* Was `#4d9bc2`, a fixed blue that sat on the dark card unchanged. */
|
|
1779
1701
|
.history-title {
|
|
1780
|
-
color:
|
|
1702
|
+
color: var(--accent-text);
|
|
1781
1703
|
font-size: 18px;
|
|
1782
1704
|
font-weight: 600;
|
|
1783
1705
|
padding: 16px 20px 8px;
|
|
@@ -1797,10 +1719,34 @@ function escapeHtml(value: unknown) {
|
|
|
1797
1719
|
.history-table-card {
|
|
1798
1720
|
overflow-x: auto;
|
|
1799
1721
|
overflow-y: hidden;
|
|
1722
|
+
border: 1px solid var(--border);
|
|
1723
|
+
border-radius: var(--r-inner);
|
|
1724
|
+
background: var(--card);
|
|
1800
1725
|
}
|
|
1801
1726
|
|
|
1802
1727
|
.history-table-card :deep(table) {
|
|
1803
1728
|
min-width: 760px;
|
|
1729
|
+
background: transparent;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/*
|
|
1733
|
+
The history table stays a `v-table` rather than a nested `TableMain` - it is
|
|
1734
|
+
a read-only strip inside a dialog with no toolbar, pager or filters, and
|
|
1735
|
+
nesting the full card inside a dialog would draw a second heading. Its
|
|
1736
|
+
colours become tokens so it follows the theme; its SHAPE is unchanged.
|
|
1737
|
+
*/
|
|
1738
|
+
.history-table-card :deep(th) {
|
|
1739
|
+
background: var(--thead);
|
|
1740
|
+
color: var(--muted);
|
|
1741
|
+
font-size: 11px;
|
|
1742
|
+
font-weight: 800;
|
|
1743
|
+
text-transform: uppercase;
|
|
1744
|
+
letter-spacing: 0.04em;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.history-table-card :deep(td) {
|
|
1748
|
+
color: var(--text);
|
|
1749
|
+
font-size: 13px;
|
|
1804
1750
|
}
|
|
1805
1751
|
|
|
1806
1752
|
.table-refresh {
|
|
@@ -1808,79 +1754,16 @@ function escapeHtml(value: unknown) {
|
|
|
1808
1754
|
display: flex;
|
|
1809
1755
|
align-items: center;
|
|
1810
1756
|
padding: 0 14px;
|
|
1811
|
-
background:
|
|
1812
|
-
border-bottom: 1px solid
|
|
1813
|
-
color:
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
.table-footer {
|
|
1817
|
-
display: flex;
|
|
1818
|
-
justify-content: flex-end;
|
|
1819
|
-
align-items: center;
|
|
1820
|
-
gap: 8px;
|
|
1821
|
-
min-height: 44px;
|
|
1822
|
-
padding: 4px 10px;
|
|
1823
|
-
border-top: 1px solid #eeeeee;
|
|
1824
|
-
background: #f6f6f6;
|
|
1825
|
-
color: #0a2638;
|
|
1826
|
-
font-size: 13px;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
.access-tabs {
|
|
1830
|
-
border-bottom: 1px solid #eeeeee;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
.access-tabs :deep(.v-slide-group__content) {
|
|
1834
|
-
display: grid;
|
|
1835
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1836
|
-
width: 100%;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
.access-tabs :deep(.v-tab) {
|
|
1840
|
-
flex: none;
|
|
1841
|
-
min-width: 0;
|
|
1842
|
-
width: 100%;
|
|
1843
|
-
max-width: none;
|
|
1844
|
-
color: #0a2638;
|
|
1845
|
-
font-size: 12px;
|
|
1846
|
-
font-weight: 600;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
.access-tabs :deep(.v-tab--selected) {
|
|
1850
|
-
font-weight: 600;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
.status-chip {
|
|
1854
|
-
min-width: 110px;
|
|
1855
|
-
justify-content: center;
|
|
1856
|
-
font-weight: 600;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
.mapped-chip {
|
|
1860
|
-
background: #dff7e7 !important;
|
|
1861
|
-
color: #1b7f3a !important;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
.unmapped-chip {
|
|
1865
|
-
background: #fde8e8 !important;
|
|
1866
|
-
color: #b42318 !important;
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
.unknown-chip {
|
|
1870
|
-
background: #fff0cc !important;
|
|
1871
|
-
color: #9a6700 !important;
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
.hid-face-avatar {
|
|
1875
|
-
border: 1px solid #d8dde4;
|
|
1876
|
-
background: #f2f4f7;
|
|
1757
|
+
background: var(--thead);
|
|
1758
|
+
border-bottom: 1px solid var(--border);
|
|
1759
|
+
color: var(--muted);
|
|
1877
1760
|
}
|
|
1878
1761
|
|
|
1879
1762
|
.empty-state {
|
|
1880
1763
|
min-height: 280px;
|
|
1881
1764
|
display: grid;
|
|
1882
1765
|
place-items: center;
|
|
1883
|
-
color:
|
|
1766
|
+
color: var(--muted);
|
|
1884
1767
|
font-size: 13px;
|
|
1885
1768
|
}
|
|
1886
1769
|
|
|
@@ -1892,73 +1775,52 @@ function escapeHtml(value: unknown) {
|
|
|
1892
1775
|
font-size: 14px;
|
|
1893
1776
|
font-weight: 700;
|
|
1894
1777
|
padding: 12px 16px;
|
|
1895
|
-
border-bottom: 1px solid
|
|
1778
|
+
border-bottom: 1px solid var(--border);
|
|
1779
|
+
color: var(--text);
|
|
1896
1780
|
}
|
|
1897
1781
|
|
|
1898
1782
|
.dialog-body {
|
|
1899
1783
|
padding: 18px 16px 20px;
|
|
1900
1784
|
}
|
|
1901
1785
|
|
|
1786
|
+
/* Was `#4f5a66` / `#e53935` - the same two colours on both themes. */
|
|
1902
1787
|
.field-label {
|
|
1903
|
-
color:
|
|
1788
|
+
color: var(--text2);
|
|
1904
1789
|
font-size: 12px;
|
|
1905
1790
|
font-weight: 600;
|
|
1906
1791
|
margin-bottom: 6px;
|
|
1907
1792
|
}
|
|
1908
1793
|
|
|
1909
1794
|
.field-label span {
|
|
1910
|
-
color:
|
|
1795
|
+
color: var(--err);
|
|
1911
1796
|
}
|
|
1912
1797
|
|
|
1798
|
+
/* The split footer bar. The handoff does not draw one, so the SHAPE is the
|
|
1799
|
+
screen's own; only the colours change - `#062d42` was a fixed navy that sat
|
|
1800
|
+
on the dark card unchanged. Same treatment as panels 2 and 4. */
|
|
1913
1801
|
.dialog-actions {
|
|
1914
1802
|
display: grid;
|
|
1915
1803
|
grid-template-columns: 1fr 1fr;
|
|
1916
1804
|
}
|
|
1917
1805
|
|
|
1918
1806
|
.action-cancel {
|
|
1919
|
-
background:
|
|
1920
|
-
color:
|
|
1807
|
+
background: var(--card);
|
|
1808
|
+
color: var(--text2);
|
|
1921
1809
|
border-radius: 0;
|
|
1922
1810
|
}
|
|
1923
1811
|
|
|
1924
1812
|
.action-submit {
|
|
1925
|
-
background:
|
|
1926
|
-
color:
|
|
1813
|
+
background: var(--accent-strong);
|
|
1814
|
+
color: var(--on-accent-strong);
|
|
1927
1815
|
border-radius: 0;
|
|
1928
1816
|
}
|
|
1929
1817
|
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
:global(.compact-date-menu .v-overlay__content) {
|
|
1935
|
-
max-width: 320px;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
:global(.compact-date-picker) {
|
|
1939
|
-
border-radius: 8px;
|
|
1940
|
-
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
:global(.compact-date-picker .v-picker-title),
|
|
1944
|
-
:global(.compact-date-picker .v-date-picker-header) {
|
|
1945
|
-
display: none;
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
:global(.compact-date-picker .v-picker__body) {
|
|
1949
|
-
padding-top: 4px;
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
:global(.compact-date-picker .v-date-picker-month) {
|
|
1953
|
-
padding: 0 12px 12px;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
:global(.compact-date-picker .v-date-picker-month__day) {
|
|
1957
|
-
height: 34px;
|
|
1818
|
+
.hid-face-avatar {
|
|
1819
|
+
border: 1px solid var(--border);
|
|
1820
|
+
background: var(--hover);
|
|
1958
1821
|
}
|
|
1959
1822
|
|
|
1960
1823
|
@media (max-width: 960px) {
|
|
1961
|
-
.toolbar,
|
|
1962
1824
|
.history-toolbar {
|
|
1963
1825
|
align-items: stretch;
|
|
1964
1826
|
}
|
|
@@ -1967,17 +1829,11 @@ function escapeHtml(value: unknown) {
|
|
|
1967
1829
|
display: none;
|
|
1968
1830
|
}
|
|
1969
1831
|
|
|
1970
|
-
.
|
|
1971
|
-
.
|
|
1972
|
-
.search-input,
|
|
1973
|
-
.status-input {
|
|
1832
|
+
.hid-log-filters :deep(.app-field),
|
|
1833
|
+
.hid-log-filters :deep(.app-select) {
|
|
1974
1834
|
max-width: none;
|
|
1975
1835
|
width: 100%;
|
|
1976
1836
|
}
|
|
1977
|
-
|
|
1978
|
-
.table-card {
|
|
1979
|
-
min-height: 360px;
|
|
1980
|
-
}
|
|
1981
1837
|
}
|
|
1982
1838
|
|
|
1983
1839
|
@media (max-width: 600px) {
|
|
@@ -1985,14 +1841,6 @@ function escapeHtml(value: unknown) {
|
|
|
1985
1841
|
padding: 12px 0;
|
|
1986
1842
|
}
|
|
1987
1843
|
|
|
1988
|
-
.access-tabs :deep(.v-slide-group__content) {
|
|
1989
|
-
display: flex;
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
.access-tabs :deep(.v-tab) {
|
|
1993
|
-
min-width: 132px;
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
1844
|
.history-body {
|
|
1997
1845
|
padding: 0 12px 12px;
|
|
1998
1846
|
}
|
|
@@ -2009,12 +1857,5 @@ function escapeHtml(value: unknown) {
|
|
|
2009
1857
|
max-width: calc(100vw - 24px) !important;
|
|
2010
1858
|
margin: 12px !important;
|
|
2011
1859
|
}
|
|
2012
|
-
|
|
2013
|
-
:global(.compact-date-menu),
|
|
2014
|
-
:global(.compact-date-menu .v-overlay__content),
|
|
2015
|
-
:global(.compact-date-picker) {
|
|
2016
|
-
max-width: calc(100vw - 32px) !important;
|
|
2017
|
-
width: calc(100vw - 32px) !important;
|
|
2018
|
-
}
|
|
2019
1860
|
}
|
|
2020
1861
|
</style>
|