@7365admin1/layer-common 3.0.8 → 3.0.9
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/CHANGELOG.md +6 -0
- package/components/DashboardMain.vue +1894 -844
- package/components/EntryPassInformation.vue +60 -0
- package/components/HidAccessLogDashboard.vue +1691 -0
- package/components/HidEnabledGate.vue +51 -0
- package/components/HidIdentityMapping.vue +975 -0
- package/components/HidQrCodeConfiguration.vue +618 -0
- package/components/HidReaderForm.vue +246 -0
- package/components/HidReaderManagement.vue +1233 -0
- package/components/HidServiceSettingsPanel.vue +150 -0
- package/components/HidUserEnrollment.vue +1274 -0
- package/components/Layout/NavigationDrawer.vue +31 -1
- package/components/NavigationItem.vue +11 -4
- package/components/SiteSettings.vue +24 -1
- package/components/VisitorForm.vue +195 -4
- package/components/VisitorFormSelection.vue +12 -2
- package/composables/useDashboard.ts +29 -2
- package/composables/useHidAmico.ts +152 -0
- package/composables/useHidNavigation.ts +106 -0
- package/composables/useMember.ts +7 -1
- package/composables/useOptionalServices.ts +118 -0
- package/composables/useSiteSettings.ts +1 -1
- package/composables/useUser.ts +1 -2
- package/nuxt.config.ts +1 -1
- package/package.json +2 -1
- package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
- package/types/local.d.ts +2 -1
|
@@ -0,0 +1,1691 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<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.
|
|
109
|
+
</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++"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</v-card>
|
|
130
|
+
|
|
131
|
+
<v-dialog v-model="historyDialog" max-width="980">
|
|
132
|
+
<v-card class="history-dialog" rounded="lg">
|
|
133
|
+
<v-card-title class="history-title">Access Log</v-card-title>
|
|
134
|
+
|
|
135
|
+
<v-card-text class="history-body">
|
|
136
|
+
<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
|
+
>
|
|
144
|
+
Export Log
|
|
145
|
+
</v-btn>
|
|
146
|
+
<div class="toolbar-spacer" />
|
|
147
|
+
<v-btn icon="mdi-close" variant="text" density="comfortable" @click="historyDialog = false" />
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<v-card flat border class="history-table-card">
|
|
151
|
+
<div class="table-refresh">
|
|
152
|
+
<v-icon size="16">mdi-refresh</v-icon>
|
|
153
|
+
</div>
|
|
154
|
+
<v-table>
|
|
155
|
+
<thead>
|
|
156
|
+
<tr>
|
|
157
|
+
<th>Name</th>
|
|
158
|
+
<th>Unit No.</th>
|
|
159
|
+
<th>Reader Location</th>
|
|
160
|
+
<th>Access Method</th>
|
|
161
|
+
<th>Access Date and Time</th>
|
|
162
|
+
</tr>
|
|
163
|
+
</thead>
|
|
164
|
+
<tbody>
|
|
165
|
+
<tr v-for="row in selectedHistoryRows" :key="row.key">
|
|
166
|
+
<td>{{ row.name }}</td>
|
|
167
|
+
<td>{{ row.unit }}</td>
|
|
168
|
+
<td>{{ row.readerLocation }}</td>
|
|
169
|
+
<td>{{ row.method }}</td>
|
|
170
|
+
<td>{{ row.lastAccessTime }}</td>
|
|
171
|
+
</tr>
|
|
172
|
+
</tbody>
|
|
173
|
+
</v-table>
|
|
174
|
+
|
|
175
|
+
<div v-if="!selectedHistoryRows.length" class="empty-state">
|
|
176
|
+
No access logs found.
|
|
177
|
+
</div>
|
|
178
|
+
</v-card>
|
|
179
|
+
</v-card-text>
|
|
180
|
+
</v-card>
|
|
181
|
+
</v-dialog>
|
|
182
|
+
|
|
183
|
+
<v-dialog v-model="exportDialog" max-width="430" persistent>
|
|
184
|
+
<v-card class="export-dialog" rounded="lg">
|
|
185
|
+
<v-card-title class="dialog-title">Export</v-card-title>
|
|
186
|
+
|
|
187
|
+
<v-card-text class="dialog-body">
|
|
188
|
+
<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>
|
|
217
|
+
|
|
218
|
+
<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>
|
|
247
|
+
|
|
248
|
+
<div class="field-label">Users</div>
|
|
249
|
+
<v-select
|
|
250
|
+
v-model="exportForm.users"
|
|
251
|
+
:items="userOptions"
|
|
252
|
+
item-title="title"
|
|
253
|
+
item-value="value"
|
|
254
|
+
aria-label="Users"
|
|
255
|
+
placeholder="Select"
|
|
256
|
+
density="compact"
|
|
257
|
+
variant="outlined"
|
|
258
|
+
hide-details="auto"
|
|
259
|
+
multiple
|
|
260
|
+
chips
|
|
261
|
+
closable-chips
|
|
262
|
+
class="mb-3"
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
<div class="field-label">Access Method</div>
|
|
266
|
+
<v-select
|
|
267
|
+
v-model="exportForm.accessMethod"
|
|
268
|
+
:items="accessMethodOptions"
|
|
269
|
+
aria-label="Access Method"
|
|
270
|
+
placeholder="Select"
|
|
271
|
+
density="compact"
|
|
272
|
+
variant="outlined"
|
|
273
|
+
hide-details="auto"
|
|
274
|
+
class="mb-3"
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
<div class="field-label">Reader Location</div>
|
|
278
|
+
<v-select
|
|
279
|
+
v-model="exportForm.readerLocation"
|
|
280
|
+
:items="readerLocationOptions"
|
|
281
|
+
aria-label="Reader Location"
|
|
282
|
+
placeholder="Select"
|
|
283
|
+
density="compact"
|
|
284
|
+
variant="outlined"
|
|
285
|
+
hide-details="auto"
|
|
286
|
+
class="mb-3"
|
|
287
|
+
/>
|
|
288
|
+
|
|
289
|
+
<div class="field-label">Status (Optional)</div>
|
|
290
|
+
<v-select
|
|
291
|
+
v-model="exportForm.status"
|
|
292
|
+
:items="statusOptions"
|
|
293
|
+
aria-label="Status"
|
|
294
|
+
placeholder="Select"
|
|
295
|
+
density="compact"
|
|
296
|
+
variant="outlined"
|
|
297
|
+
hide-details="auto"
|
|
298
|
+
/>
|
|
299
|
+
</v-card-text>
|
|
300
|
+
|
|
301
|
+
<v-card-actions class="dialog-actions pa-0">
|
|
302
|
+
<v-btn class="text-none action-cancel" variant="flat" height="48" @click="exportDialog = false">
|
|
303
|
+
Cancel
|
|
304
|
+
</v-btn>
|
|
305
|
+
<v-btn class="text-none action-submit" variant="flat" height="48" @click="exportRows">
|
|
306
|
+
Export
|
|
307
|
+
</v-btn>
|
|
308
|
+
</v-card-actions>
|
|
309
|
+
</v-card>
|
|
310
|
+
</v-dialog>
|
|
311
|
+
</section>
|
|
312
|
+
</template>
|
|
313
|
+
|
|
314
|
+
<script setup lang="ts">
|
|
315
|
+
const props = defineProps({
|
|
316
|
+
site: {
|
|
317
|
+
type: String,
|
|
318
|
+
required: true,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
type AccessTab = "resident" | "visitor" | "administrator";
|
|
323
|
+
type AccessRow = {
|
|
324
|
+
key: string;
|
|
325
|
+
identityKey: string;
|
|
326
|
+
name: string;
|
|
327
|
+
facialData: string;
|
|
328
|
+
faceImage?: string;
|
|
329
|
+
unit: string;
|
|
330
|
+
method: string;
|
|
331
|
+
lastAccessTime: string;
|
|
332
|
+
lastAccessDate?: Date;
|
|
333
|
+
readerLocation: string;
|
|
334
|
+
status: "Mapped" | "Unmapped";
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
type PdfAccessRow = {
|
|
338
|
+
name: string;
|
|
339
|
+
unit: string;
|
|
340
|
+
readerLocation: string;
|
|
341
|
+
method: string;
|
|
342
|
+
accessDateTime: string;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const { getReaders, getIdentities, getLogs, getUserImage, runObjectOperation, getReaderConfiguration } = useHidAmico();
|
|
346
|
+
const { getVisitors } = useVisitor();
|
|
347
|
+
|
|
348
|
+
const readers = ref<Record<string, any>[]>([]);
|
|
349
|
+
const identities = ref<Record<string, any>[]>([]);
|
|
350
|
+
const visitorRecords = ref<Record<string, any>>({});
|
|
351
|
+
const logs = ref<Record<string, any>[]>([]);
|
|
352
|
+
const liveAccessLogs = ref<Record<string, any>[]>([]);
|
|
353
|
+
const readerConfiguration = ref<Record<string, any>>({});
|
|
354
|
+
const administratorUserIds = ref<Set<number>>(new Set());
|
|
355
|
+
const userImages = ref<Record<string, string>>({});
|
|
356
|
+
const selectedReaderId = ref("");
|
|
357
|
+
const activeTab = ref<AccessTab>("resident");
|
|
358
|
+
const search = ref("");
|
|
359
|
+
const status = ref("Status");
|
|
360
|
+
const page = ref(1);
|
|
361
|
+
const limit = 20;
|
|
362
|
+
const loading = ref(false);
|
|
363
|
+
const exportDialog = ref(false);
|
|
364
|
+
const historyDialog = ref(false);
|
|
365
|
+
const selectedHistoryRow = ref<AccessRow>();
|
|
366
|
+
const exportScope = ref<"dashboard" | "history">("dashboard");
|
|
367
|
+
const startDateMenu = ref(false);
|
|
368
|
+
const endDateMenu = ref(false);
|
|
369
|
+
const exportForm = reactive({
|
|
370
|
+
startDate: "",
|
|
371
|
+
endDate: "",
|
|
372
|
+
users: [] as string[],
|
|
373
|
+
accessMethod: "All",
|
|
374
|
+
readerLocation: "All",
|
|
375
|
+
status: "Status",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const statusOptions = ["Status", "Mapped", "Unmapped"];
|
|
379
|
+
const accessMethodOptions = ["All", "Facial", "QR Code", "ID and Password", "PIN", "Card"];
|
|
380
|
+
|
|
381
|
+
const readerOptions = computed(() =>
|
|
382
|
+
readers.value.map((reader) => ({
|
|
383
|
+
title: reader.name || reader.deviceId || reader._id,
|
|
384
|
+
value: reader._id,
|
|
385
|
+
})),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const selectedReader = computed(() =>
|
|
389
|
+
readers.value.find((reader) => reader._id === selectedReaderId.value),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const readerLocationOptions = computed(() => {
|
|
393
|
+
const locations = readers.value
|
|
394
|
+
.map((reader) => reader.location)
|
|
395
|
+
.filter((location) => !!location);
|
|
396
|
+
return ["All", ...Array.from(new Set(locations))];
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const userOptions = computed(() => [
|
|
400
|
+
{ title: "All", value: "all" },
|
|
401
|
+
...identities.value.map((identity) => ({
|
|
402
|
+
title: getName(identity),
|
|
403
|
+
value: getIdentityKey(identity),
|
|
404
|
+
})),
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
const rows = computed<AccessRow[]>(() => {
|
|
408
|
+
const logRows = normalizeAccessRows(normalizeAccessLogs()).filter(isVisibleForActiveTab);
|
|
409
|
+
return logRows.sort(sortAccessRowsByDateDesc);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
413
|
+
if (!selectedHistoryRow.value) return [];
|
|
414
|
+
|
|
415
|
+
const selectedKey = selectedHistoryRow.value.identityKey;
|
|
416
|
+
const matchedLogs = logs.value.filter((log) => getLogIdentityKey(log) === selectedKey);
|
|
417
|
+
const matchedLiveLogs = liveAccessLogs.value.filter((log) => getAccessLogIdentityKey(log) === selectedKey);
|
|
418
|
+
|
|
419
|
+
if (!matchedLogs.length && !matchedLiveLogs.length) {
|
|
420
|
+
return rows.value.filter((row) => row.identityKey === selectedKey);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const historyRows = matchedLogs.map((log, index) => ({
|
|
424
|
+
key: `${selectedHistoryRow.value?.key || "history"}-${log._id || log.id || index}`,
|
|
425
|
+
identityKey: selectedKey,
|
|
426
|
+
name: selectedHistoryRow.value?.name || "N/A",
|
|
427
|
+
facialData: selectedHistoryRow.value?.facialData || "N/A",
|
|
428
|
+
faceImage: selectedHistoryRow.value?.faceImage,
|
|
429
|
+
unit: selectedHistoryRow.value?.unit || "N/A",
|
|
430
|
+
method: getAccessMethod({}, log),
|
|
431
|
+
lastAccessTime: formatDate(getLogAccessDateValue(log)),
|
|
432
|
+
lastAccessDate: parseDate(getLogAccessDateValue(log)),
|
|
433
|
+
readerLocation: getLogReaderLocation(log),
|
|
434
|
+
status: selectedHistoryRow.value?.status || "Mapped",
|
|
435
|
+
}));
|
|
436
|
+
|
|
437
|
+
const liveRows = matchedLiveLogs.map((log, index) => ({
|
|
438
|
+
key: `${selectedHistoryRow.value?.key || "history"}-live-${log.id || log.time || index}`,
|
|
439
|
+
identityKey: selectedKey,
|
|
440
|
+
name: selectedHistoryRow.value?.name || "N/A",
|
|
441
|
+
facialData: selectedHistoryRow.value?.facialData || "N/A",
|
|
442
|
+
faceImage: selectedHistoryRow.value?.faceImage,
|
|
443
|
+
unit: selectedHistoryRow.value?.unit || "N/A",
|
|
444
|
+
method: getAccessMethod({}, { payload: { rawAccessLog: log } }),
|
|
445
|
+
lastAccessTime: formatDate(getAccessLogTime(log, {})),
|
|
446
|
+
lastAccessDate: parseDate(getAccessLogTime(log, {})),
|
|
447
|
+
readerLocation: selectedReader.value?.location || "N/A",
|
|
448
|
+
status: selectedHistoryRow.value?.status || "Mapped",
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
return [...historyRows, ...liveRows].sort((a, b) => {
|
|
452
|
+
const aTime = a.lastAccessDate?.getTime() || 0;
|
|
453
|
+
const bTime = b.lastAccessDate?.getTime() || 0;
|
|
454
|
+
return bTime - aTime;
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const filteredRows = computed(() => {
|
|
459
|
+
const query = search.value.trim().toLowerCase();
|
|
460
|
+
return rows.value.filter((row) => {
|
|
461
|
+
const matchesSearch = !query || [
|
|
462
|
+
row.name,
|
|
463
|
+
row.facialData,
|
|
464
|
+
row.unit,
|
|
465
|
+
row.method,
|
|
466
|
+
row.lastAccessTime,
|
|
467
|
+
row.status,
|
|
468
|
+
].some((value) => String(value ?? "").toLowerCase().includes(query));
|
|
469
|
+
|
|
470
|
+
const matchesStatus = status.value === "Status" || row.status === status.value;
|
|
471
|
+
|
|
472
|
+
return matchesSearch && matchesStatus;
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const pages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / limit)));
|
|
477
|
+
|
|
478
|
+
const paginatedRows = computed(() => {
|
|
479
|
+
const currentPage = Math.min(page.value, pages.value);
|
|
480
|
+
const start = (currentPage - 1) * limit;
|
|
481
|
+
return filteredRows.value.slice(start, start + limit);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const pageRange = computed(() => {
|
|
485
|
+
if (!filteredRows.value.length) return "0-0 of 0";
|
|
486
|
+
const currentPage = Math.min(page.value, pages.value);
|
|
487
|
+
const start = (currentPage - 1) * limit + 1;
|
|
488
|
+
const end = Math.min(currentPage * limit, filteredRows.value.length);
|
|
489
|
+
return `${start}-${end} of ${filteredRows.value.length}`;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const exportRowsData = computed(() =>
|
|
493
|
+
getExportSourceRows().filter((row) => {
|
|
494
|
+
const selectedUsers = exportForm.users.filter((user) => user !== "all");
|
|
495
|
+
const matchesUsers = !selectedUsers.length || selectedUsers.includes(row.key) || selectedUsers.includes(row.identityKey);
|
|
496
|
+
const matchesMethod = exportForm.accessMethod === "All" || row.method === exportForm.accessMethod;
|
|
497
|
+
const matchesLocation = exportForm.readerLocation === "All" || row.readerLocation === exportForm.readerLocation;
|
|
498
|
+
const matchesStatus = exportForm.status === "Status" || row.status === exportForm.status;
|
|
499
|
+
const matchesDate = isWithinExportRange(row.lastAccessDate);
|
|
500
|
+
|
|
501
|
+
return matchesUsers && matchesMethod && matchesLocation && matchesStatus && matchesDate;
|
|
502
|
+
}),
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
watch(
|
|
506
|
+
() => props.site,
|
|
507
|
+
() => init(),
|
|
508
|
+
{ immediate: true },
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
watch([search, status, activeTab], () => {
|
|
512
|
+
page.value = 1;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
watch(pages, () => {
|
|
516
|
+
if (page.value > pages.value) page.value = pages.value;
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
async function init() {
|
|
520
|
+
await loadReaders();
|
|
521
|
+
await loadDashboard();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function loadReaders() {
|
|
525
|
+
if (!props.site) return;
|
|
526
|
+
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
527
|
+
readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
528
|
+
if (!selectedReaderId.value && readers.value.length) {
|
|
529
|
+
selectedReaderId.value = readers.value[0]._id;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function loadDashboard() {
|
|
534
|
+
if (!selectedReaderId.value) {
|
|
535
|
+
identities.value = [];
|
|
536
|
+
visitorRecords.value = {};
|
|
537
|
+
logs.value = [];
|
|
538
|
+
liveAccessLogs.value = [];
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
loading.value = true;
|
|
543
|
+
try {
|
|
544
|
+
const isAdministratorTab = activeTab.value === "administrator";
|
|
545
|
+
const isVisitorTab = activeTab.value === "visitor";
|
|
546
|
+
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse] = await Promise.all([
|
|
547
|
+
getIdentities(selectedReaderId.value, {
|
|
548
|
+
page: 1,
|
|
549
|
+
limit: 500,
|
|
550
|
+
...(!isAdministratorTab ? { type: getIdentityType(activeTab.value) } : {}),
|
|
551
|
+
}),
|
|
552
|
+
getLogs(selectedReaderId.value, {
|
|
553
|
+
page: 1,
|
|
554
|
+
limit: 500,
|
|
555
|
+
}),
|
|
556
|
+
loadLiveAccessLogs(selectedReaderId.value),
|
|
557
|
+
loadReaderConfiguration(selectedReaderId.value),
|
|
558
|
+
isAdministratorTab ? loadAdministratorRoles(selectedReaderId.value) : Promise.resolve(null),
|
|
559
|
+
isVisitorTab ? getVisitors({ site: props.site, page: 1, limit: 500, order: "desc" }) : Promise.resolve(null),
|
|
560
|
+
]);
|
|
561
|
+
|
|
562
|
+
const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
|
|
563
|
+
const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
|
|
564
|
+
visitorRecords.value = isVisitorTab
|
|
565
|
+
? Object.fromEntries(loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]))
|
|
566
|
+
: {};
|
|
567
|
+
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
568
|
+
identities.value = isAdministratorTab
|
|
569
|
+
? loadedIdentities.filter((identity: Record<string, any>) => {
|
|
570
|
+
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
571
|
+
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
572
|
+
})
|
|
573
|
+
: isVisitorTab
|
|
574
|
+
? loadedIdentities.filter((identity: Record<string, any>) => Boolean(getVisitorRecord(identity)))
|
|
575
|
+
: loadedIdentities;
|
|
576
|
+
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
577
|
+
liveAccessLogs.value = hidAccessLogs;
|
|
578
|
+
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
579
|
+
await loadUserImages(identities.value);
|
|
580
|
+
} finally {
|
|
581
|
+
loading.value = false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function loadUserImages(items: Record<string, any>[]) {
|
|
586
|
+
if (!selectedReaderId.value) return;
|
|
587
|
+
|
|
588
|
+
await Promise.all(items.map(async (identity) => {
|
|
589
|
+
const hidUserId = identity.hidUserId;
|
|
590
|
+
const key = String(hidUserId || "");
|
|
591
|
+
if (!hidUserId || userImages.value[key] || identity?.metadata?.photo) return;
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const response = await getUserImage(selectedReaderId.value, hidUserId);
|
|
595
|
+
const image = response?.data || response;
|
|
596
|
+
userImages.value[key] = image?.base64
|
|
597
|
+
? `data:${image.contentType || "image/jpeg"};base64,${image.base64}`
|
|
598
|
+
: "";
|
|
599
|
+
} catch {
|
|
600
|
+
userImages.value[key] = "";
|
|
601
|
+
}
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function loadLiveAccessLogs(readerId: string) {
|
|
606
|
+
const allLogs: Record<string, any>[] = [];
|
|
607
|
+
const limit = 500;
|
|
608
|
+
const maxPages = 20;
|
|
609
|
+
|
|
610
|
+
for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
|
|
611
|
+
try {
|
|
612
|
+
const response = await runObjectOperation(readerId, {
|
|
613
|
+
operation: "load",
|
|
614
|
+
object: "access_logs",
|
|
615
|
+
where: {
|
|
616
|
+
access_logs: {},
|
|
617
|
+
},
|
|
618
|
+
limit,
|
|
619
|
+
offset: pageIndex * limit,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const pageLogs = normalizeHidAccessLogs(response);
|
|
623
|
+
allLogs.push(...pageLogs);
|
|
624
|
+
|
|
625
|
+
if (pageLogs.length < limit) break;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.warn("Unable to load HID live access logs", error);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return dedupeAccessLogs(allLogs).sort((a, b) => {
|
|
633
|
+
const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
|
|
634
|
+
const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
|
|
635
|
+
return bTime - aTime;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function loadReaderConfiguration(readerId: string) {
|
|
640
|
+
try {
|
|
641
|
+
return await getReaderConfiguration(readerId, {
|
|
642
|
+
access_logs: [],
|
|
643
|
+
event_types: [],
|
|
644
|
+
log_types: [],
|
|
645
|
+
identification_rules: [],
|
|
646
|
+
portals: [],
|
|
647
|
+
users: [],
|
|
648
|
+
});
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.warn("Unable to load HID configuration", error);
|
|
651
|
+
return {};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function onTabChange() {
|
|
656
|
+
page.value = 1;
|
|
657
|
+
loadDashboard();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function getIdentityType(tab: AccessTab) {
|
|
661
|
+
if (tab === "administrator") return "admin";
|
|
662
|
+
return tab;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isVisibleForActiveTab(row: AccessRow) {
|
|
666
|
+
if (activeTab.value !== "administrator") return true;
|
|
667
|
+
const hidUserId = toHidNumericId(row.identityKey.split("|")[0]);
|
|
668
|
+
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function loadAdministratorRoles(readerId: string) {
|
|
672
|
+
return runObjectOperation(readerId, {
|
|
673
|
+
operation: "load",
|
|
674
|
+
object: "user_roles",
|
|
675
|
+
where: {
|
|
676
|
+
user_roles: {
|
|
677
|
+
role: 1,
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
limit: 500,
|
|
681
|
+
offset: 0,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function getAdministratorRoleUserIds(response: Record<string, any> | null) {
|
|
686
|
+
const data = response?.data || response || {};
|
|
687
|
+
const roles =
|
|
688
|
+
data?.user_roles ||
|
|
689
|
+
data?.data?.user_roles ||
|
|
690
|
+
data?.result?.user_roles ||
|
|
691
|
+
[];
|
|
692
|
+
|
|
693
|
+
return new Set(
|
|
694
|
+
(Array.isArray(roles) ? roles : [])
|
|
695
|
+
.map((role) => toHidNumericId(role.user_id))
|
|
696
|
+
.filter((userId): userId is number => Boolean(userId)),
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function toHidNumericId(value: unknown) {
|
|
701
|
+
const raw = String(value ?? "").trim();
|
|
702
|
+
if (!raw) return undefined;
|
|
703
|
+
const numeric = Number(raw);
|
|
704
|
+
if (Number.isInteger(numeric) && numeric > 0) return numeric;
|
|
705
|
+
const match = raw.match(/(\d+)$/);
|
|
706
|
+
const parsed = match ? Number(match[1]) : NaN;
|
|
707
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function getIdentityKey(identity: Record<string, any>) {
|
|
711
|
+
return [
|
|
712
|
+
identity.hidUserId,
|
|
713
|
+
identity.registration,
|
|
714
|
+
identity.cardNo,
|
|
715
|
+
].filter(Boolean).join("|");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function getLogIdentityKey(log: Record<string, any>) {
|
|
719
|
+
const identity = log.payload?.identity || {};
|
|
720
|
+
const lookup = log.payload?.identityLookup || {};
|
|
721
|
+
const rawLog = log.payload?.rawAccessLog || log;
|
|
722
|
+
return [
|
|
723
|
+
identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
|
|
724
|
+
identity.registration || lookup.registration,
|
|
725
|
+
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
|
|
726
|
+
].filter(Boolean).join("|");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getName(identity: Record<string, any>) {
|
|
730
|
+
return getVisitorRecord(identity)?.name || identity.metadata?.name || identity.name || "N/A";
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function getFacialData(identity: Record<string, any>) {
|
|
734
|
+
return identity.metadata?.facialData || (identity.metadata?.photo ? identity.hidUserId : "N/A");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function getUserImageSrc(identity: Record<string, any>) {
|
|
738
|
+
return identity?.metadata?.photo || userImages.value[String(identity?.hidUserId || "")] || "";
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function getUnit(identity: Record<string, any>) {
|
|
742
|
+
const visitor = getVisitorRecord(identity);
|
|
743
|
+
if (visitor) {
|
|
744
|
+
return visitor.unitName || [visitor.block, visitor.level, visitor.unit].filter(Boolean).join("/") || "N/A";
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return identity.metadata?.unitLabel || [identity.metadata?.block, identity.metadata?.level, identity.metadata?.unit].filter(Boolean).join("/") || "N/A";
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function getVisitorRecord(identity: Record<string, any>) {
|
|
751
|
+
const visitorId = identity?.visitor;
|
|
752
|
+
return visitorId ? visitorRecords.value[String(visitorId)] : undefined;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function getAccessMethod(identity: Record<string, any>, log?: Record<string, any>) {
|
|
756
|
+
const payload = log?.payload?.rawAccessLog || log?.payload || log || {};
|
|
757
|
+
const configuredLogType = getConfigurationLabel("log_types", payload.log_type_id || payload.logTypeId);
|
|
758
|
+
if (configuredLogType) return configuredLogType;
|
|
759
|
+
const configuredRule = getConfigurationLabel("identification_rules", payload.identification_rule_id || payload.identificationRuleId);
|
|
760
|
+
if (configuredRule) return configuredRule;
|
|
761
|
+
if (payload.qrcode_value || payload.qrCode || payload.qrcode) return "QR Code";
|
|
762
|
+
if (payload.card_value || payload.cardNo || identity.cardNo) return "Card";
|
|
763
|
+
if (payload.pin_value) return "PIN";
|
|
764
|
+
if (payload.password_value || payload.password || payload.identifier_id) return "ID and Password";
|
|
765
|
+
if (payload.confidence || payload.user_id || payload.userId || payload.hidUserId) return "Facial";
|
|
766
|
+
if (identity.metadata?.pinPassword) return "ID and Password";
|
|
767
|
+
return identity.metadata?.photo || identity.metadata?.facialData ? "Facial" : "N/A";
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function formatDate(value?: string | number) {
|
|
771
|
+
if (!value) return "N/A";
|
|
772
|
+
const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
|
|
773
|
+
return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function parseDate(value?: string | number) {
|
|
777
|
+
if (!value) return undefined;
|
|
778
|
+
const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
|
|
779
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function openHistoryDialog(row: AccessRow) {
|
|
783
|
+
selectedHistoryRow.value = row;
|
|
784
|
+
historyDialog.value = true;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function openExportDialog(scope: "dashboard" | "history" = "dashboard") {
|
|
788
|
+
exportScope.value = scope;
|
|
789
|
+
|
|
790
|
+
if (scope === "history" && selectedHistoryRow.value) {
|
|
791
|
+
exportForm.users = [selectedHistoryRow.value.identityKey];
|
|
792
|
+
} else {
|
|
793
|
+
exportForm.users = ["all"];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const dateRange = getDefaultExportDateRange();
|
|
797
|
+
const today = toDisplayDate(new Date());
|
|
798
|
+
exportForm.startDate = dateRange.startDate || today;
|
|
799
|
+
exportForm.endDate = dateRange.endDate || today;
|
|
800
|
+
exportDialog.value = true;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function getDefaultExportDateRange() {
|
|
804
|
+
const dates = getExportSourceRows()
|
|
805
|
+
.map((row) => row.lastAccessDate)
|
|
806
|
+
.filter((date): date is Date => Boolean(date && !Number.isNaN(date.getTime())))
|
|
807
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
startDate: dates[0] ? toDisplayDate(dates[0]) : "",
|
|
811
|
+
endDate: dates[dates.length - 1] ? toDisplayDate(dates[dates.length - 1]) : "",
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function toDisplayDate(date: Date) {
|
|
816
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
817
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
818
|
+
return `${day}/${month}/${date.getFullYear()}`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function isWithinExportRange(value?: Date) {
|
|
822
|
+
if (!value || (!exportForm.startDate && !exportForm.endDate)) return true;
|
|
823
|
+
|
|
824
|
+
const start = parseExportDate(exportForm.startDate, false);
|
|
825
|
+
const end = parseExportDate(exportForm.endDate, true);
|
|
826
|
+
|
|
827
|
+
if (start && value < start) return false;
|
|
828
|
+
if (end && value > end) return false;
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function parseExportDate(value: string, endOfDay: boolean) {
|
|
833
|
+
if (!value) return undefined;
|
|
834
|
+
const match = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
835
|
+
if (!match) return undefined;
|
|
836
|
+
const [, day, month, year] = match;
|
|
837
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
838
|
+
if (Number.isNaN(date.getTime())) return undefined;
|
|
839
|
+
if (endOfDay) {
|
|
840
|
+
date.setHours(23, 59, 59, 999);
|
|
841
|
+
} else {
|
|
842
|
+
date.setHours(0, 0, 0, 0);
|
|
843
|
+
}
|
|
844
|
+
return date;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function setExportDate(field: "startDate" | "endDate", value: unknown) {
|
|
848
|
+
const date = normalizePickerDate(value);
|
|
849
|
+
if (!date) return;
|
|
850
|
+
|
|
851
|
+
exportForm[field] = toDisplayDate(date);
|
|
852
|
+
if (field === "startDate") {
|
|
853
|
+
startDateMenu.value = false;
|
|
854
|
+
} else {
|
|
855
|
+
endDateMenu.value = false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function normalizePickerDate(value: unknown) {
|
|
860
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
861
|
+
return value;
|
|
862
|
+
}
|
|
863
|
+
if (typeof value === "string") {
|
|
864
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
865
|
+
if (year && month && day) {
|
|
866
|
+
return new Date(year, month - 1, day);
|
|
867
|
+
}
|
|
868
|
+
const date = new Date(value);
|
|
869
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
870
|
+
}
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function normalizeAccessLogs() {
|
|
875
|
+
const persistedRows = logs.value.flatMap((event, eventIndex) => {
|
|
876
|
+
const rawLogs = getRawAccessLogs(event);
|
|
877
|
+
if (rawLogs.length) {
|
|
878
|
+
return rawLogs.map((rawLog, rawIndex) => mapAccessLogToRow(rawLog, event, `${eventIndex}-${rawIndex}`));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (event.payload?.identity || event.payload?.identityLookup) {
|
|
882
|
+
return [mapAccessLogToRow(event.payload, event, `${eventIndex}`)];
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return [];
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
const liveRows = liveAccessLogs.value.map((rawLog, rawIndex) =>
|
|
889
|
+
mapAccessLogToRow(rawLog, { id: "hid-live", payload: { reader: selectedReader.value } }, `live-${rawIndex}`),
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
return [...persistedRows, ...liveRows];
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function normalizeAccessRows(rows: AccessRow[]) {
|
|
896
|
+
const seen = new Set<string>();
|
|
897
|
+
return rows.filter((row) => {
|
|
898
|
+
const key = [
|
|
899
|
+
row.identityKey,
|
|
900
|
+
row.lastAccessDate?.getTime() || row.lastAccessTime,
|
|
901
|
+
row.method,
|
|
902
|
+
row.readerLocation,
|
|
903
|
+
].join("|");
|
|
904
|
+
|
|
905
|
+
if (seen.has(key)) return false;
|
|
906
|
+
seen.add(key);
|
|
907
|
+
return true;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function sortAccessRowsByDateDesc(a: AccessRow, b: AccessRow) {
|
|
912
|
+
const aTime = a.lastAccessDate?.getTime() || 0;
|
|
913
|
+
const bTime = b.lastAccessDate?.getTime() || 0;
|
|
914
|
+
return bTime - aTime;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function dedupeAccessLogs(items: Record<string, any>[]) {
|
|
918
|
+
const seen = new Set<string>();
|
|
919
|
+
return items.filter((item) => {
|
|
920
|
+
const key = [
|
|
921
|
+
item.id,
|
|
922
|
+
item.time,
|
|
923
|
+
item.user_id ?? item.userId ?? item.hidUserId,
|
|
924
|
+
item.card_value ?? item.cardNo,
|
|
925
|
+
].join("|");
|
|
926
|
+
|
|
927
|
+
if (seen.has(key)) return false;
|
|
928
|
+
seen.add(key);
|
|
929
|
+
return true;
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function normalizeHidAccessLogs(response: Record<string, any>) {
|
|
934
|
+
const data = response?.data || response || {};
|
|
935
|
+
const accessLogs =
|
|
936
|
+
data.access_logs ||
|
|
937
|
+
data.data?.access_logs ||
|
|
938
|
+
data.accessLogs ||
|
|
939
|
+
data.data?.accessLogs ||
|
|
940
|
+
data.result?.access_logs ||
|
|
941
|
+
data.result?.data?.access_logs;
|
|
942
|
+
|
|
943
|
+
return Array.isArray(accessLogs) ? accessLogs : [];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function normalizeReaderConfiguration(response: Record<string, any>) {
|
|
947
|
+
const data = response?.data || response || {};
|
|
948
|
+
return data?.data || data?.result || data;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function getConfigurationLabel(section: string, id: unknown) {
|
|
952
|
+
if (id === undefined || id === null || id === "" || Number(id) === -1) return "";
|
|
953
|
+
const entries = readerConfiguration.value?.[section];
|
|
954
|
+
if (!entries) return "";
|
|
955
|
+
|
|
956
|
+
const items = Array.isArray(entries)
|
|
957
|
+
? entries
|
|
958
|
+
: typeof entries === "object"
|
|
959
|
+
? Object.values(entries)
|
|
960
|
+
: [];
|
|
961
|
+
|
|
962
|
+
const match = items.find((item: any) => {
|
|
963
|
+
if (!item || typeof item !== "object") return false;
|
|
964
|
+
return String(item.id ?? item.value ?? item.key ?? item.code) === String(id);
|
|
965
|
+
}) as Record<string, any> | undefined;
|
|
966
|
+
|
|
967
|
+
return match?.name || match?.label || match?.description || match?.title || "";
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function getRawAccessLogs(event: Record<string, any>) {
|
|
971
|
+
const payload = event.payload || {};
|
|
972
|
+
const result = payload.result || payload.data || {};
|
|
973
|
+
const accessLogs =
|
|
974
|
+
result.access_logs ||
|
|
975
|
+
result.data?.access_logs ||
|
|
976
|
+
result.accessLogs ||
|
|
977
|
+
payload.access_logs ||
|
|
978
|
+
payload.accessLogs;
|
|
979
|
+
|
|
980
|
+
return Array.isArray(accessLogs) ? accessLogs : [];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
|
|
984
|
+
const identity = findIdentityForAccessLog(rawLog, event);
|
|
985
|
+
const rawAccessTime = getAccessLogTime(rawLog, event);
|
|
986
|
+
const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
|
|
987
|
+
|
|
988
|
+
return {
|
|
989
|
+
key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
|
|
990
|
+
identityKey,
|
|
991
|
+
name: identity ? getName(identity) : getRawLogName(rawLog, event),
|
|
992
|
+
facialData: identity ? getFacialData(identity) : "N/A",
|
|
993
|
+
faceImage: identity ? getUserImageSrc(identity) : "",
|
|
994
|
+
unit: identity ? getUnit(identity) : "N/A",
|
|
995
|
+
method: getAccessMethod(identity || {}, { ...event, payload: { ...event.payload, rawAccessLog: rawLog } }),
|
|
996
|
+
lastAccessTime: formatDate(rawAccessTime),
|
|
997
|
+
lastAccessDate: parseDate(rawAccessTime),
|
|
998
|
+
readerLocation: getLogReaderLocation(event),
|
|
999
|
+
status: identity ? "Mapped" : "Unmapped",
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1004
|
+
const eventIdentityId = event.payload?.identity?._id;
|
|
1005
|
+
if (eventIdentityId) {
|
|
1006
|
+
const byId = identities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
1007
|
+
if (byId) return byId;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
1011
|
+
const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
|
|
1012
|
+
const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
|
|
1013
|
+
|
|
1014
|
+
return identities.value.find((identity) => {
|
|
1015
|
+
return (
|
|
1016
|
+
(hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
|
|
1017
|
+
(registration && identity.registration === registration) ||
|
|
1018
|
+
(cardNo && String(identity.cardNo) === String(cardNo))
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
1024
|
+
const identityKey = getIdentityKey(identity);
|
|
1025
|
+
const accessLogs = [
|
|
1026
|
+
...liveAccessLogs.value,
|
|
1027
|
+
...logs.value.flatMap((event) => getRawAccessLogs(event)),
|
|
1028
|
+
];
|
|
1029
|
+
|
|
1030
|
+
return accessLogs
|
|
1031
|
+
.filter((log) => getAccessLogIdentityKey(log) === identityKey)
|
|
1032
|
+
.sort((a, b) => {
|
|
1033
|
+
const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
|
|
1034
|
+
const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
|
|
1035
|
+
return bTime - aTime;
|
|
1036
|
+
})[0];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function getAccessLogIdentityKey(rawLog: Record<string, any>) {
|
|
1040
|
+
return [
|
|
1041
|
+
rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
|
|
1042
|
+
rawLog.registration,
|
|
1043
|
+
rawLog.card_value || rawLog.cardNo,
|
|
1044
|
+
].filter(Boolean).join("|");
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function getAccessLogTime(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1048
|
+
return rawLog.time || rawLog.createdAt || rawLog.accessedAt || event.createdAt;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function getRawLogName(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1052
|
+
return rawLog.name || event.payload?.identity?.name || event.payload?.name || "Unmapped";
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function exportRows() {
|
|
1056
|
+
exportRowsAsPdf();
|
|
1057
|
+
exportDialog.value = false;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function getExportSourceRows() {
|
|
1061
|
+
return exportScope.value === "history" ? selectedHistoryRows.value : filteredRows.value;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function getLogAccessDateValue(log: Record<string, any>) {
|
|
1065
|
+
const payload = log.payload || {};
|
|
1066
|
+
return log.createdAt || payload.createdAt || payload.accessedAt || payload.time || payload.eventTime;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function getLogReaderLocation(log: Record<string, any>) {
|
|
1070
|
+
return log.payload?.reader?.location || log.payload?.readerLocation || selectedReader.value?.location || "N/A";
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function exportRowsAsPdf() {
|
|
1074
|
+
if (typeof window === "undefined") return;
|
|
1075
|
+
|
|
1076
|
+
const { jsPDF } = await import("jspdf");
|
|
1077
|
+
const pdf = new jsPDF({ unit: "mm", format: "a4", orientation: "portrait" });
|
|
1078
|
+
const rows = exportRowsData.value.map((row) => ({
|
|
1079
|
+
name: row.name,
|
|
1080
|
+
unit: row.unit,
|
|
1081
|
+
readerLocation: row.readerLocation,
|
|
1082
|
+
method: row.method,
|
|
1083
|
+
accessDateTime: row.lastAccessTime,
|
|
1084
|
+
}));
|
|
1085
|
+
|
|
1086
|
+
drawAccessLogPdf(pdf, rows);
|
|
1087
|
+
pdf.save(`hid-access-log-${activeTab.value}.pdf`);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function drawAccessLogPdf(pdf: any, rows: PdfAccessRow[]) {
|
|
1091
|
+
const margin = 12;
|
|
1092
|
+
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
1093
|
+
const pageHeight = pdf.internal.pageSize.getHeight();
|
|
1094
|
+
const tableWidth = pageWidth - margin * 2;
|
|
1095
|
+
const columns = [
|
|
1096
|
+
{ title: "Name", key: "name", width: 38 },
|
|
1097
|
+
{ title: "Unit No.", key: "unit", width: 26 },
|
|
1098
|
+
{ title: "Reader Location", key: "readerLocation", width: 42 },
|
|
1099
|
+
{ title: "Access Method", key: "method", width: 36 },
|
|
1100
|
+
{ title: "Access Date and Time", key: "accessDateTime", width: tableWidth - 142 },
|
|
1101
|
+
];
|
|
1102
|
+
let y = margin;
|
|
1103
|
+
|
|
1104
|
+
const addHeader = () => {
|
|
1105
|
+
pdf.setFillColor(6, 45, 66);
|
|
1106
|
+
pdf.rect(margin, y, tableWidth, 14, "F");
|
|
1107
|
+
pdf.setTextColor(255, 255, 255);
|
|
1108
|
+
pdf.setFont("helvetica", "bold");
|
|
1109
|
+
pdf.setFontSize(13);
|
|
1110
|
+
pdf.text("Access Log", margin + 4, y + 9);
|
|
1111
|
+
y += 20;
|
|
1112
|
+
|
|
1113
|
+
pdf.setTextColor(10, 38, 56);
|
|
1114
|
+
pdf.setFontSize(9);
|
|
1115
|
+
pdf.setFont("helvetica", "bold");
|
|
1116
|
+
pdf.text("Date", margin, y);
|
|
1117
|
+
pdf.text("Site", margin + 72, y);
|
|
1118
|
+
pdf.setFont("helvetica", "normal");
|
|
1119
|
+
pdf.text(`${exportForm.startDate || "-"} - ${exportForm.endDate || "-"}`, margin, y + 6);
|
|
1120
|
+
pdf.text(getPdfSiteName(), margin + 72, y + 6);
|
|
1121
|
+
y += 16;
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
const addTableHeader = () => {
|
|
1125
|
+
pdf.setFillColor(241, 243, 245);
|
|
1126
|
+
pdf.rect(margin, y, tableWidth, 9, "F");
|
|
1127
|
+
pdf.setDrawColor(218, 224, 231);
|
|
1128
|
+
pdf.setTextColor(10, 38, 56);
|
|
1129
|
+
pdf.setFont("helvetica", "bold");
|
|
1130
|
+
pdf.setFontSize(8);
|
|
1131
|
+
|
|
1132
|
+
let x = margin;
|
|
1133
|
+
columns.forEach((column) => {
|
|
1134
|
+
pdf.rect(x, y, column.width, 9);
|
|
1135
|
+
pdf.text(column.title, x + 2, y + 6);
|
|
1136
|
+
x += column.width;
|
|
1137
|
+
});
|
|
1138
|
+
y += 9;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const ensureSpace = (height: number) => {
|
|
1142
|
+
if (y + height <= pageHeight - margin) return;
|
|
1143
|
+
pdf.addPage();
|
|
1144
|
+
y = margin;
|
|
1145
|
+
addHeader();
|
|
1146
|
+
addTableHeader();
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
addHeader();
|
|
1150
|
+
addTableHeader();
|
|
1151
|
+
|
|
1152
|
+
if (!rows.length) {
|
|
1153
|
+
pdf.setFont("helvetica", "normal");
|
|
1154
|
+
pdf.setFontSize(10);
|
|
1155
|
+
pdf.setTextColor(104, 115, 130);
|
|
1156
|
+
pdf.text("No access logs found.", margin, y + 8);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
rows.forEach((row) => {
|
|
1161
|
+
const cellLines = columns.map((column) => {
|
|
1162
|
+
const value = String(row[column.key as keyof PdfAccessRow] ?? "N/A");
|
|
1163
|
+
return pdf.splitTextToSize(value, column.width - 4);
|
|
1164
|
+
});
|
|
1165
|
+
const rowHeight = Math.max(9, Math.max(...cellLines.map((lines: string[]) => lines.length)) * 4.2 + 5);
|
|
1166
|
+
ensureSpace(rowHeight);
|
|
1167
|
+
|
|
1168
|
+
pdf.setFont("helvetica", "normal");
|
|
1169
|
+
pdf.setFontSize(8);
|
|
1170
|
+
pdf.setTextColor(10, 38, 56);
|
|
1171
|
+
|
|
1172
|
+
let x = margin;
|
|
1173
|
+
columns.forEach((column, index) => {
|
|
1174
|
+
pdf.setDrawColor(230, 233, 237);
|
|
1175
|
+
pdf.rect(x, y, column.width, rowHeight);
|
|
1176
|
+
pdf.text(cellLines[index], x + 2, y + 5);
|
|
1177
|
+
x += column.width;
|
|
1178
|
+
});
|
|
1179
|
+
y += rowHeight;
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function buildPdfHtml() {
|
|
1184
|
+
const rows = exportRowsData.value.map((row) => ({
|
|
1185
|
+
name: row.name,
|
|
1186
|
+
unit: row.unit,
|
|
1187
|
+
readerLocation: row.readerLocation,
|
|
1188
|
+
method: row.method,
|
|
1189
|
+
accessDateTime: row.lastAccessTime,
|
|
1190
|
+
}));
|
|
1191
|
+
const groupedRows = groupRowsByName(rows);
|
|
1192
|
+
const dateRange = `${exportForm.startDate || "-"} - ${exportForm.endDate || "-"}`;
|
|
1193
|
+
const siteName = getPdfSiteName();
|
|
1194
|
+
|
|
1195
|
+
return `
|
|
1196
|
+
<div class="hid-access-log-pdf">
|
|
1197
|
+
<style>
|
|
1198
|
+
.hid-access-log-pdf {
|
|
1199
|
+
width: 190mm;
|
|
1200
|
+
min-height: 277mm;
|
|
1201
|
+
padding: 10mm;
|
|
1202
|
+
background: #ffffff;
|
|
1203
|
+
color: #0a2638;
|
|
1204
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
1205
|
+
font-size: 9px;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.pdf-summary {
|
|
1209
|
+
display: grid;
|
|
1210
|
+
grid-template-columns: 64mm 64mm 1fr;
|
|
1211
|
+
border: 1px solid #d9dee5;
|
|
1212
|
+
margin-bottom: 8mm;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.pdf-title {
|
|
1216
|
+
display: flex;
|
|
1217
|
+
align-items: center;
|
|
1218
|
+
justify-content: center;
|
|
1219
|
+
background: #062d42;
|
|
1220
|
+
color: #ffffff;
|
|
1221
|
+
font-size: 12px;
|
|
1222
|
+
font-weight: 600;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.pdf-summary-cell {
|
|
1226
|
+
min-height: 18mm;
|
|
1227
|
+
padding: 4mm 3mm;
|
|
1228
|
+
border-left: 1px solid #d9dee5;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.pdf-label {
|
|
1232
|
+
display: block;
|
|
1233
|
+
margin-bottom: 2mm;
|
|
1234
|
+
color: #314155;
|
|
1235
|
+
font-size: 7px;
|
|
1236
|
+
font-weight: 700;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.pdf-value {
|
|
1240
|
+
font-size: 10px;
|
|
1241
|
+
line-height: 1.35;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.pdf-table {
|
|
1245
|
+
width: 100%;
|
|
1246
|
+
border-collapse: collapse;
|
|
1247
|
+
margin-bottom: 8mm;
|
|
1248
|
+
page-break-inside: avoid;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.pdf-table th {
|
|
1252
|
+
background: #f1f1f1;
|
|
1253
|
+
color: #0a2638;
|
|
1254
|
+
font-size: 8px;
|
|
1255
|
+
font-weight: 700;
|
|
1256
|
+
text-align: left;
|
|
1257
|
+
padding: 4mm 3mm;
|
|
1258
|
+
border: 1px solid #e2e5e9;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.pdf-table td {
|
|
1262
|
+
padding: 3.2mm 3mm;
|
|
1263
|
+
border: 1px solid #e6e9ed;
|
|
1264
|
+
color: #0a2638;
|
|
1265
|
+
vertical-align: middle;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.pdf-empty {
|
|
1269
|
+
padding: 12mm;
|
|
1270
|
+
border: 1px solid #e6e9ed;
|
|
1271
|
+
color: #687382;
|
|
1272
|
+
text-align: center;
|
|
1273
|
+
}
|
|
1274
|
+
</style>
|
|
1275
|
+
|
|
1276
|
+
<div class="pdf-summary">
|
|
1277
|
+
<div class="pdf-title">Access Log</div>
|
|
1278
|
+
<div class="pdf-summary-cell">
|
|
1279
|
+
<span class="pdf-label">Date</span>
|
|
1280
|
+
<div class="pdf-value">${escapeHtml(dateRange)}</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
<div class="pdf-summary-cell">
|
|
1283
|
+
<span class="pdf-label">Site</span>
|
|
1284
|
+
<div class="pdf-value">${escapeHtml(siteName)}</div>
|
|
1285
|
+
</div>
|
|
1286
|
+
</div>
|
|
1287
|
+
|
|
1288
|
+
${
|
|
1289
|
+
groupedRows.length
|
|
1290
|
+
? groupedRows.map((group) => buildPdfTable(group.rows)).join("")
|
|
1291
|
+
: `<div class="pdf-empty">No access logs found.</div>`
|
|
1292
|
+
}
|
|
1293
|
+
</div>
|
|
1294
|
+
`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function groupRowsByName(rows: PdfAccessRow[]) {
|
|
1298
|
+
const groups = new Map<string, PdfAccessRow[]>();
|
|
1299
|
+
rows.forEach((row) => {
|
|
1300
|
+
const key = row.name || "N/A";
|
|
1301
|
+
const group = groups.get(key) || [];
|
|
1302
|
+
group.push(row);
|
|
1303
|
+
groups.set(key, group);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
return Array.from(groups.entries()).map(([name, groupRows]) => ({
|
|
1307
|
+
name,
|
|
1308
|
+
rows: groupRows,
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function buildPdfTable(rows: PdfAccessRow[]) {
|
|
1313
|
+
const body = rows
|
|
1314
|
+
.map(
|
|
1315
|
+
(row) => `
|
|
1316
|
+
<tr>
|
|
1317
|
+
<td>${escapeHtml(row.name)}</td>
|
|
1318
|
+
<td>${escapeHtml(row.unit)}</td>
|
|
1319
|
+
<td>${escapeHtml(row.readerLocation)}</td>
|
|
1320
|
+
<td>${escapeHtml(row.method)}</td>
|
|
1321
|
+
<td>${escapeHtml(row.accessDateTime)}</td>
|
|
1322
|
+
</tr>
|
|
1323
|
+
`,
|
|
1324
|
+
)
|
|
1325
|
+
.join("");
|
|
1326
|
+
|
|
1327
|
+
return `
|
|
1328
|
+
<table class="pdf-table">
|
|
1329
|
+
<thead>
|
|
1330
|
+
<tr>
|
|
1331
|
+
<th>Name</th>
|
|
1332
|
+
<th>Unit No.</th>
|
|
1333
|
+
<th>Reader Location</th>
|
|
1334
|
+
<th>Access Method</th>
|
|
1335
|
+
<th>Access Date and Time</th>
|
|
1336
|
+
</tr>
|
|
1337
|
+
</thead>
|
|
1338
|
+
<tbody>${body}</tbody>
|
|
1339
|
+
</table>
|
|
1340
|
+
`;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function getPdfSiteName() {
|
|
1344
|
+
const readerSite = selectedReader.value?.site;
|
|
1345
|
+
if (typeof readerSite === "object" && readerSite?.name) return readerSite.name;
|
|
1346
|
+
return selectedReader.value?.siteName || selectedReader.value?.metadata?.siteName || props.site;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function escapeHtml(value: unknown) {
|
|
1350
|
+
return String(value ?? "")
|
|
1351
|
+
.replace(/&/g, "&")
|
|
1352
|
+
.replace(/</g, "<")
|
|
1353
|
+
.replace(/>/g, ">")
|
|
1354
|
+
.replace(/"/g, """)
|
|
1355
|
+
.replace(/'/g, "'");
|
|
1356
|
+
}
|
|
1357
|
+
</script>
|
|
1358
|
+
|
|
1359
|
+
<style scoped>
|
|
1360
|
+
.hid-access-log-dashboard {
|
|
1361
|
+
padding: 24px 0;
|
|
1362
|
+
width: 100%;
|
|
1363
|
+
min-width: 0;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.toolbar {
|
|
1367
|
+
display: flex;
|
|
1368
|
+
flex-wrap: wrap;
|
|
1369
|
+
gap: 12px;
|
|
1370
|
+
align-items: center;
|
|
1371
|
+
margin-bottom: 16px;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.toolbar-spacer {
|
|
1375
|
+
flex: 1;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.export-button {
|
|
1379
|
+
min-width: 170px;
|
|
1380
|
+
background: #e4e4e4;
|
|
1381
|
+
color: #0a2638;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.reader-input,
|
|
1385
|
+
.search-input {
|
|
1386
|
+
max-width: 240px;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
.status-input {
|
|
1390
|
+
max-width: 160px;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.table-card {
|
|
1394
|
+
display: flex;
|
|
1395
|
+
flex-direction: column;
|
|
1396
|
+
min-height: 430px;
|
|
1397
|
+
overflow: hidden;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.table-body {
|
|
1401
|
+
flex: 1;
|
|
1402
|
+
overflow-x: auto;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.table-card :deep(table) {
|
|
1406
|
+
table-layout: fixed;
|
|
1407
|
+
width: 100%;
|
|
1408
|
+
min-width: 980px;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.table-card :deep(th),
|
|
1412
|
+
.table-card :deep(td) {
|
|
1413
|
+
white-space: nowrap;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.table-card :deep(th:nth-child(1)),
|
|
1417
|
+
.table-card :deep(td:nth-child(1)) {
|
|
1418
|
+
width: 16%;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.table-card :deep(th:nth-child(2)),
|
|
1422
|
+
.table-card :deep(td:nth-child(2)) {
|
|
1423
|
+
width: 14%;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.table-card :deep(th:nth-child(3)),
|
|
1427
|
+
.table-card :deep(td:nth-child(3)) {
|
|
1428
|
+
width: 12%;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.table-card :deep(th:nth-child(4)),
|
|
1432
|
+
.table-card :deep(td:nth-child(4)) {
|
|
1433
|
+
width: 20%;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.table-card :deep(th:nth-child(5)),
|
|
1437
|
+
.table-card :deep(td:nth-child(5)) {
|
|
1438
|
+
width: 18%;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
.table-card :deep(th:nth-child(6)),
|
|
1442
|
+
.table-card :deep(td:nth-child(6)) {
|
|
1443
|
+
width: 14%;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
.table-card :deep(th:nth-child(7)),
|
|
1447
|
+
.table-card :deep(td:nth-child(7)) {
|
|
1448
|
+
width: 6%;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.history-dialog {
|
|
1452
|
+
overflow: hidden;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.history-title {
|
|
1456
|
+
color: #4d9bc2;
|
|
1457
|
+
font-size: 18px;
|
|
1458
|
+
font-weight: 600;
|
|
1459
|
+
padding: 16px 20px 8px;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.history-body {
|
|
1463
|
+
padding: 0 20px 20px;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
.history-toolbar {
|
|
1467
|
+
display: flex;
|
|
1468
|
+
align-items: center;
|
|
1469
|
+
gap: 12px;
|
|
1470
|
+
margin-bottom: 14px;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.history-table-card {
|
|
1474
|
+
overflow-x: auto;
|
|
1475
|
+
overflow-y: hidden;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.history-table-card :deep(table) {
|
|
1479
|
+
min-width: 760px;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.table-refresh {
|
|
1483
|
+
height: 34px;
|
|
1484
|
+
display: flex;
|
|
1485
|
+
align-items: center;
|
|
1486
|
+
padding: 0 14px;
|
|
1487
|
+
background: #f3f3f3;
|
|
1488
|
+
border-bottom: 1px solid #e7e7e7;
|
|
1489
|
+
color: #0a2638;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.table-footer {
|
|
1493
|
+
display: flex;
|
|
1494
|
+
justify-content: flex-end;
|
|
1495
|
+
align-items: center;
|
|
1496
|
+
gap: 8px;
|
|
1497
|
+
min-height: 44px;
|
|
1498
|
+
padding: 4px 10px;
|
|
1499
|
+
border-top: 1px solid #eeeeee;
|
|
1500
|
+
background: #f6f6f6;
|
|
1501
|
+
color: #0a2638;
|
|
1502
|
+
font-size: 13px;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.access-tabs {
|
|
1506
|
+
border-bottom: 1px solid #eeeeee;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.access-tabs :deep(.v-slide-group__content) {
|
|
1510
|
+
display: grid;
|
|
1511
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1512
|
+
width: 100%;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.access-tabs :deep(.v-tab) {
|
|
1516
|
+
flex: none;
|
|
1517
|
+
min-width: 0;
|
|
1518
|
+
width: 100%;
|
|
1519
|
+
max-width: none;
|
|
1520
|
+
color: #0a2638;
|
|
1521
|
+
font-size: 12px;
|
|
1522
|
+
font-weight: 600;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
.access-tabs :deep(.v-tab--selected) {
|
|
1526
|
+
font-weight: 600;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.status-chip {
|
|
1530
|
+
min-width: 110px;
|
|
1531
|
+
justify-content: center;
|
|
1532
|
+
font-weight: 600;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.mapped-chip {
|
|
1536
|
+
background: #dff7e7 !important;
|
|
1537
|
+
color: #1b7f3a !important;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
.unmapped-chip {
|
|
1541
|
+
background: #fff0cc !important;
|
|
1542
|
+
color: #9a6700 !important;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.hid-face-avatar {
|
|
1546
|
+
border: 1px solid #d8dde4;
|
|
1547
|
+
background: #f2f4f7;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
.empty-state {
|
|
1551
|
+
min-height: 280px;
|
|
1552
|
+
display: grid;
|
|
1553
|
+
place-items: center;
|
|
1554
|
+
color: #687382;
|
|
1555
|
+
font-size: 13px;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.export-dialog {
|
|
1559
|
+
overflow: hidden;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.dialog-title {
|
|
1563
|
+
font-size: 14px;
|
|
1564
|
+
font-weight: 700;
|
|
1565
|
+
padding: 12px 16px;
|
|
1566
|
+
border-bottom: 1px solid #ececec;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.dialog-body {
|
|
1570
|
+
padding: 18px 16px 20px;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.field-label {
|
|
1574
|
+
color: #4f5a66;
|
|
1575
|
+
font-size: 12px;
|
|
1576
|
+
font-weight: 600;
|
|
1577
|
+
margin-bottom: 6px;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.field-label span {
|
|
1581
|
+
color: #e53935;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.dialog-actions {
|
|
1585
|
+
display: grid;
|
|
1586
|
+
grid-template-columns: 1fr 1fr;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.action-cancel {
|
|
1590
|
+
background: #ffffff;
|
|
1591
|
+
color: #111827;
|
|
1592
|
+
border-radius: 0;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.action-submit {
|
|
1596
|
+
background: #062d42;
|
|
1597
|
+
color: #ffffff;
|
|
1598
|
+
border-radius: 0;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
:global(.compact-date-menu) {
|
|
1602
|
+
max-width: 320px;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
:global(.compact-date-menu .v-overlay__content) {
|
|
1606
|
+
max-width: 320px;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
:global(.compact-date-picker) {
|
|
1610
|
+
border-radius: 8px;
|
|
1611
|
+
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
:global(.compact-date-picker .v-picker-title),
|
|
1615
|
+
:global(.compact-date-picker .v-date-picker-header) {
|
|
1616
|
+
display: none;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
:global(.compact-date-picker .v-picker__body) {
|
|
1620
|
+
padding-top: 4px;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
:global(.compact-date-picker .v-date-picker-month) {
|
|
1624
|
+
padding: 0 12px 12px;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
:global(.compact-date-picker .v-date-picker-month__day) {
|
|
1628
|
+
height: 34px;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
@media (max-width: 960px) {
|
|
1632
|
+
.toolbar,
|
|
1633
|
+
.history-toolbar {
|
|
1634
|
+
align-items: stretch;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.toolbar-spacer {
|
|
1638
|
+
display: none;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.export-button,
|
|
1642
|
+
.reader-input,
|
|
1643
|
+
.search-input,
|
|
1644
|
+
.status-input {
|
|
1645
|
+
max-width: none;
|
|
1646
|
+
width: 100%;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.table-card {
|
|
1650
|
+
min-height: 360px;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
@media (max-width: 600px) {
|
|
1655
|
+
.hid-access-log-dashboard {
|
|
1656
|
+
padding: 12px 0;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.access-tabs :deep(.v-slide-group__content) {
|
|
1660
|
+
display: flex;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
.access-tabs :deep(.v-tab) {
|
|
1664
|
+
min-width: 132px;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.history-body {
|
|
1668
|
+
padding: 0 12px 12px;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
.history-toolbar {
|
|
1672
|
+
flex-wrap: wrap;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.dialog-actions {
|
|
1676
|
+
grid-template-columns: 1fr;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
:global(.v-dialog > .v-overlay__content) {
|
|
1680
|
+
max-width: calc(100vw - 24px) !important;
|
|
1681
|
+
margin: 12px !important;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
:global(.compact-date-menu),
|
|
1685
|
+
:global(.compact-date-menu .v-overlay__content),
|
|
1686
|
+
:global(.compact-date-picker) {
|
|
1687
|
+
max-width: calc(100vw - 32px) !important;
|
|
1688
|
+
width: calc(100vw - 32px) !important;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
</style>
|