@7365admin1/layer-common 3.0.18 → 3.0.19
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/BuildingManagement/buildings.vue +7 -4
- package/components/BuildingManagement/units.vue +187 -9
- package/components/HidAccessLogDashboard.vue +184 -21
- package/components/HidReaderForm.vue +20 -12
- package/components/HidReaderManagement.vue +113 -80
- package/components/MemberInformation.vue +240 -163
- package/components/PassInformation.vue +6 -4
- package/components/VisitorForm.vue +41 -18
- package/composables/useBuilding.ts +14 -0
- package/composables/useVisitor.ts +19 -15
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +3 -1
- package/types/visitor.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
<v-btn fab icon density="comfortable" @click="getBuildings()">
|
|
16
16
|
<v-icon>mdi-refresh</v-icon>
|
|
17
17
|
</v-btn>
|
|
18
|
-
<v-text-field v-model="searchInput" autocomplete="off" placeholder="Search" density="compact" hide-details
|
|
19
|
-
class="mx-2" style="width: 200px;" />
|
|
18
|
+
<v-text-field v-model="searchInput" autocomplete="off" placeholder="Search" density="compact" hide-details
|
|
19
|
+
dense clearable class="mx-2" style="width: 200px;" />
|
|
20
20
|
</template>
|
|
21
21
|
|
|
22
22
|
<template #append>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<v-dialog v-model="createDialog" width="450" persistent>
|
|
46
46
|
<BuildingForm :site="site" mode="add" :blocks="items" @cancel="createDialog = false" @success="successCreate()"
|
|
47
47
|
@success:create-more="getBuildings()" />
|
|
48
|
-
</v-dialog>
|
|
48
|
+
</v-dialog>
|
|
49
49
|
|
|
50
50
|
<!-- Edit Dialog -->
|
|
51
51
|
<v-dialog v-model="editDialog" width="450" persistent>
|
|
@@ -222,7 +222,10 @@ const site = (useRoute().params.site as string) ?? "";
|
|
|
222
222
|
const status = (useRoute().params.status as string) ?? "active";
|
|
223
223
|
|
|
224
224
|
const { debounce } = useUtils();
|
|
225
|
-
const {
|
|
225
|
+
const {
|
|
226
|
+
getAll: _getBuildings,
|
|
227
|
+
deleteById,
|
|
228
|
+
} = useBuilding();
|
|
226
229
|
|
|
227
230
|
const searchInput = ref("");
|
|
228
231
|
const headerSearch = ref("");
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
v-if="canCreate && canCreateUnit">
|
|
7
7
|
Add Unit
|
|
8
8
|
</v-btn>
|
|
9
|
+
<v-btn class="text-none ml-2" rounded="pill" variant="tonal" @click="openBulkUploadFilePicker()" size="large"
|
|
10
|
+
:loading="bulkUploadLoading" v-if="canCreate && canCreateUnit">
|
|
11
|
+
<v-icon start>mdi-upload</v-icon>
|
|
12
|
+
Bulk Upload
|
|
13
|
+
</v-btn>
|
|
14
|
+
<input ref="bulkUploadFileInput" type="file" accept=".xlsx,.xls,.csv" class="d-none"
|
|
15
|
+
@change="handleBulkUploadFileChange" />
|
|
9
16
|
</v-row>
|
|
10
17
|
</v-col>
|
|
11
18
|
<v-col cols="12">
|
|
@@ -30,8 +37,7 @@
|
|
|
30
37
|
</v-toolbar>
|
|
31
38
|
|
|
32
39
|
<v-data-table :headers="props.headers" :items="items" item-value="_id" items-per-page="20" fixed-header
|
|
33
|
-
hide-default-footer
|
|
34
|
-
style="max-height: calc(100vh - (200px))">
|
|
40
|
+
hide-default-footer @click:row="tableRowClickHandler" style="max-height: calc(100vh - (200px))">
|
|
35
41
|
<template #item.block="{ value, item }">
|
|
36
42
|
{{ value ? `Block ${value}` : "" }}
|
|
37
43
|
</template>
|
|
@@ -44,10 +50,10 @@
|
|
|
44
50
|
<BuildingUnitFormAdd :site="site" @cancel="createDialog = false" @success="successCreate()"
|
|
45
51
|
@success:create-more="getBuildingUnit()" />
|
|
46
52
|
</v-dialog>
|
|
47
|
-
|
|
48
|
-
<v-dialog v-if="canViewUnitDetails"
|
|
49
|
-
|
|
50
|
-
:roomFacility="selectedBuildingUnit" />
|
|
53
|
+
|
|
54
|
+
<v-dialog v-if="canViewUnitDetails" v-model="dialogPreview" :width="smAndUp ? '70svw' : '100%'" persistent>
|
|
55
|
+
<BuildingUnitFormEdit v-if="selectedBuildingUnit" @cancel="dialogPreview = false" @success="successUpdate()"
|
|
56
|
+
@delete-unit="openDeleteDialog()" :roomFacility="selectedBuildingUnit" />
|
|
51
57
|
</v-dialog>
|
|
52
58
|
|
|
53
59
|
<v-dialog v-model="dialogDelete" :loading="deleteLoading" width="450" persistent>
|
|
@@ -88,6 +94,67 @@
|
|
|
88
94
|
</v-card>
|
|
89
95
|
</v-dialog>
|
|
90
96
|
|
|
97
|
+
<v-dialog v-model="bulkUploadSkippedDialog" :width="smAndUp ? '720' : '100%'" persistent>
|
|
98
|
+
<v-card width="100%">
|
|
99
|
+
<v-toolbar density="compact" class="pl-4">
|
|
100
|
+
<span class="font-weight-medium text-h5">Skipped Rows</span>
|
|
101
|
+
</v-toolbar>
|
|
102
|
+
|
|
103
|
+
<v-card-text style="max-height: 70vh; overflow-y: auto">
|
|
104
|
+
<p class="text-subtitle-2 mb-4">
|
|
105
|
+
Some rows were not imported. Review the details below and update the sheet if needed.
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<v-alert v-if="bulkUploadSkippedSummary.length" type="warning" variant="tonal" density="compact"
|
|
109
|
+
class="mb-4">
|
|
110
|
+
{{ bulkUploadSkippedSummary.length }} row(s) skipped during bulk upload.
|
|
111
|
+
</v-alert>
|
|
112
|
+
|
|
113
|
+
<!-- <v-list v-if="bulkUploadSkippedSummary.length" density="compact" class="pa-0 mb-4">
|
|
114
|
+
<v-list-item v-for="(item, index) in bulkUploadSkippedSummary" :key="`summary-${index}`"
|
|
115
|
+
class="px-0">
|
|
116
|
+
<v-list-item-title class="text-subtitle-2">
|
|
117
|
+
Row {{ item.rowNumber ?? item.row ?? "N/A" }}
|
|
118
|
+
</v-list-item-title>
|
|
119
|
+
<v-list-item-subtitle class="text-wrap">
|
|
120
|
+
{{ formatSkippedSummaryReason(item) }}
|
|
121
|
+
</v-list-item-subtitle>
|
|
122
|
+
</v-list-item>
|
|
123
|
+
</v-list> -->
|
|
124
|
+
|
|
125
|
+
<template v-if="bulkUploadSkippedErrors.length">
|
|
126
|
+
<!-- <v-divider class="mb-4" /> -->
|
|
127
|
+
<p class="text-subtitle-2 mb-2">Details</p>
|
|
128
|
+
<v-table density="compact">
|
|
129
|
+
<thead>
|
|
130
|
+
<tr>
|
|
131
|
+
<th class="text-left">Row</th>
|
|
132
|
+
<th class="text-left">Block</th>
|
|
133
|
+
<th class="text-left">Building</th>
|
|
134
|
+
<th class="text-left">Reason</th>
|
|
135
|
+
</tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody>
|
|
138
|
+
<tr v-for="(item, index) in bulkUploadSkippedErrors" :key="`error-${index}`">
|
|
139
|
+
<td>{{ item.row ?? "N/A" }}</td>
|
|
140
|
+
<td>{{ item.block ?? "N/A" }}</td>
|
|
141
|
+
<td>{{ item.name ?? "N/A" }}</td>
|
|
142
|
+
<td>{{ item.reason ?? "Skipped" }}</td>
|
|
143
|
+
</tr>
|
|
144
|
+
</tbody>
|
|
145
|
+
</v-table>
|
|
146
|
+
</template>
|
|
147
|
+
</v-card-text>
|
|
148
|
+
|
|
149
|
+
<v-toolbar density="compact">
|
|
150
|
+
<v-btn tile block size="48" color="black" variant="flat" class="text-none"
|
|
151
|
+
@click="bulkUploadSkippedDialog = false">
|
|
152
|
+
Close
|
|
153
|
+
</v-btn>
|
|
154
|
+
</v-toolbar>
|
|
155
|
+
</v-card>
|
|
156
|
+
</v-dialog>
|
|
157
|
+
|
|
91
158
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
92
159
|
</v-row>
|
|
93
160
|
</template>
|
|
@@ -103,7 +170,7 @@ const props = defineProps({
|
|
|
103
170
|
title: "Unit Name",
|
|
104
171
|
value: "name",
|
|
105
172
|
},
|
|
106
|
-
|
|
173
|
+
{
|
|
107
174
|
title: "Level",
|
|
108
175
|
value: "level.name",
|
|
109
176
|
},
|
|
@@ -115,7 +182,7 @@ const props = defineProps({
|
|
|
115
182
|
title: "Block Name",
|
|
116
183
|
value: "buildingName",
|
|
117
184
|
},
|
|
118
|
-
|
|
185
|
+
|
|
119
186
|
// {
|
|
120
187
|
// title: "Type",
|
|
121
188
|
// value: "type",
|
|
@@ -162,6 +229,8 @@ const { debounce, formatDateDDMMYYYYLocal, toOrdinal } = useUtils();
|
|
|
162
229
|
|
|
163
230
|
const { getAll, deleteById } = useBuildingUnit();
|
|
164
231
|
|
|
232
|
+
const { uploadSpreadsheetBuilding } = useBuilding();
|
|
233
|
+
|
|
165
234
|
const searchInput = ref("");
|
|
166
235
|
const headerSearch = ref("");
|
|
167
236
|
const page = ref(1);
|
|
@@ -243,6 +312,12 @@ function openEditDialog() {
|
|
|
243
312
|
|
|
244
313
|
const dialogDelete = ref(false);
|
|
245
314
|
const deleteLoading = ref(false);
|
|
315
|
+
const bulkUploadFile = ref<File | null>(null);
|
|
316
|
+
const bulkUploadFileInput = ref<HTMLInputElement | null>(null);
|
|
317
|
+
const bulkUploadLoading = ref(false);
|
|
318
|
+
const bulkUploadSkippedDialog = ref(false);
|
|
319
|
+
const bulkUploadSkippedErrors = ref<Array<Record<string, any>>>([]);
|
|
320
|
+
const bulkUploadSkippedSummary = ref<Array<Record<string, any>>>([]);
|
|
246
321
|
|
|
247
322
|
function openDeleteDialog() {
|
|
248
323
|
dialogDelete.value = true;
|
|
@@ -272,6 +347,109 @@ async function handleDeleteBuildingUnit() {
|
|
|
272
347
|
}
|
|
273
348
|
}
|
|
274
349
|
|
|
350
|
+
function openBulkUploadFilePicker() {
|
|
351
|
+
if (bulkUploadLoading.value) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
bulkUploadFile.value = null;
|
|
356
|
+
bulkUploadSkippedErrors.value = [];
|
|
357
|
+
bulkUploadSkippedSummary.value = [];
|
|
358
|
+
bulkUploadSkippedDialog.value = false;
|
|
359
|
+
if (bulkUploadFileInput.value) {
|
|
360
|
+
bulkUploadFileInput.value.value = "";
|
|
361
|
+
bulkUploadFileInput.value.click();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function formatSkippedSummaryReason(item: Record<string, any>) {
|
|
366
|
+
if (Array.isArray(item.errors) && item.errors.length) {
|
|
367
|
+
return item.errors.join(", ");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return item.reason ?? "Skipped";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getSkippedRowsCount(result: Record<string, any> | null | undefined) {
|
|
374
|
+
if (typeof result?.skippedRows === "number") {
|
|
375
|
+
return result.skippedRows;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (Array.isArray(result?.skippedRows)) {
|
|
379
|
+
return result.skippedRows.length;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getBulkUploadSkippedErrors(result: Record<string, any> | null | undefined) {
|
|
386
|
+
if (Array.isArray(result?.skippedErrors)) {
|
|
387
|
+
return result.skippedErrors;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (Array.isArray(result?.skippedRows)) {
|
|
391
|
+
return result.skippedRows;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getBulkUploadSkippedSummary(result: Record<string, any> | null | undefined) {
|
|
398
|
+
if (Array.isArray(result?.skippedItemsSummary)) {
|
|
399
|
+
return result.skippedItemsSummary;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (Array.isArray(result?.skippedRows)) {
|
|
403
|
+
return result.skippedRows.map((item: Record<string, any>) => ({
|
|
404
|
+
rowNumber: item.row,
|
|
405
|
+
reason: item.reason,
|
|
406
|
+
block: item.block,
|
|
407
|
+
name: item.name,
|
|
408
|
+
data: item,
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function handleBulkUploadFileChange(event: Event) {
|
|
416
|
+
const input = event.target as HTMLInputElement;
|
|
417
|
+
const file = input.files?.[0] ?? null;
|
|
418
|
+
|
|
419
|
+
if (!file) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
bulkUploadFile.value = file;
|
|
424
|
+
bulkUploadLoading.value = true;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const result = await uploadSpreadsheetBuilding({
|
|
428
|
+
site,
|
|
429
|
+
file,
|
|
430
|
+
});
|
|
431
|
+
await getBuildingUnit();
|
|
432
|
+
bulkUploadSkippedErrors.value = getBulkUploadSkippedErrors(result);
|
|
433
|
+
bulkUploadSkippedSummary.value = getBulkUploadSkippedSummary(result);
|
|
434
|
+
bulkUploadSkippedDialog.value =
|
|
435
|
+
bulkUploadSkippedErrors.value.length > 0 ||
|
|
436
|
+
bulkUploadSkippedSummary.value.length > 0;
|
|
437
|
+
showMessage(
|
|
438
|
+
`Bulk upload completed. Inserted ${result?.insertedUnits ?? 0} unit(s). Skipped ${getSkippedRowsCount(result)} row(s).`,
|
|
439
|
+
"success"
|
|
440
|
+
);
|
|
441
|
+
} catch (error: any) {
|
|
442
|
+
showMessage(
|
|
443
|
+
error?.response?._data?.message || "Failed to upload spreadsheet.",
|
|
444
|
+
"error"
|
|
445
|
+
);
|
|
446
|
+
} finally {
|
|
447
|
+
bulkUploadLoading.value = false;
|
|
448
|
+
bulkUploadFile.value = null;
|
|
449
|
+
input.value = "";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
275
453
|
function setBuilding({
|
|
276
454
|
mode = "create",
|
|
277
455
|
dialog = true,
|
|
@@ -290,7 +468,7 @@ function setBuilding({
|
|
|
290
468
|
</script>
|
|
291
469
|
|
|
292
470
|
<style scoped>
|
|
293
|
-
|
|
471
|
+
.v-data-table :deep(thead th) {
|
|
294
472
|
font-weight: bold !important;
|
|
295
473
|
}
|
|
296
474
|
</style>
|
|
@@ -313,6 +313,10 @@
|
|
|
313
313
|
|
|
314
314
|
<script setup lang="ts">
|
|
315
315
|
const props = defineProps({
|
|
316
|
+
org: {
|
|
317
|
+
type: String,
|
|
318
|
+
default: "",
|
|
319
|
+
},
|
|
316
320
|
site: {
|
|
317
321
|
type: String,
|
|
318
322
|
required: true,
|
|
@@ -360,6 +364,7 @@ const status = ref("Status");
|
|
|
360
364
|
const page = ref(1);
|
|
361
365
|
const limit = 20;
|
|
362
366
|
const loading = ref(false);
|
|
367
|
+
const loadSequence = ref(0);
|
|
363
368
|
const exportDialog = ref(false);
|
|
364
369
|
const historyDialog = ref(false);
|
|
365
370
|
const selectedHistoryRow = ref<AccessRow>();
|
|
@@ -503,7 +508,7 @@ const exportRowsData = computed(() =>
|
|
|
503
508
|
);
|
|
504
509
|
|
|
505
510
|
watch(
|
|
506
|
-
() => props.site,
|
|
511
|
+
() => [props.org, props.site],
|
|
507
512
|
() => init(),
|
|
508
513
|
{ immediate: true },
|
|
509
514
|
);
|
|
@@ -532,18 +537,18 @@ async function loadReaders() {
|
|
|
532
537
|
|
|
533
538
|
async function loadDashboard() {
|
|
534
539
|
if (!selectedReaderId.value) {
|
|
535
|
-
|
|
536
|
-
visitorRecords.value = {};
|
|
537
|
-
logs.value = [];
|
|
538
|
-
liveAccessLogs.value = [];
|
|
540
|
+
resetDashboardData();
|
|
539
541
|
return;
|
|
540
542
|
}
|
|
541
543
|
|
|
544
|
+
const sequence = loadSequence.value + 1;
|
|
545
|
+
loadSequence.value = sequence;
|
|
542
546
|
loading.value = true;
|
|
547
|
+
resetDashboardData();
|
|
543
548
|
try {
|
|
544
549
|
const isAdministratorTab = activeTab.value === "administrator";
|
|
545
550
|
const isVisitorTab = activeTab.value === "visitor";
|
|
546
|
-
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse] = await Promise.all([
|
|
551
|
+
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
547
552
|
getIdentities(selectedReaderId.value, {
|
|
548
553
|
page: 1,
|
|
549
554
|
limit: 500,
|
|
@@ -556,29 +561,43 @@ async function loadDashboard() {
|
|
|
556
561
|
loadLiveAccessLogs(selectedReaderId.value),
|
|
557
562
|
loadReaderConfiguration(selectedReaderId.value),
|
|
558
563
|
isAdministratorTab ? loadAdministratorRoles(selectedReaderId.value) : Promise.resolve(null),
|
|
559
|
-
isVisitorTab
|
|
564
|
+
isVisitorTab && props.org
|
|
565
|
+
? getVisitors({
|
|
566
|
+
org: props.org,
|
|
567
|
+
site: props.site,
|
|
568
|
+
page: 1,
|
|
569
|
+
limit: 500,
|
|
570
|
+
order: "desc",
|
|
571
|
+
})
|
|
572
|
+
: Promise.resolve(null),
|
|
573
|
+
loadReaderUsers(selectedReaderId.value),
|
|
560
574
|
]);
|
|
561
575
|
|
|
576
|
+
if (sequence !== loadSequence.value) return;
|
|
577
|
+
|
|
562
578
|
const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
|
|
579
|
+
const mergedIdentities = mergeReaderUsersWithIdentities(readerUserResponse, loadedIdentities);
|
|
563
580
|
const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
|
|
564
581
|
visitorRecords.value = isVisitorTab
|
|
565
582
|
? Object.fromEntries(loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]))
|
|
566
583
|
: {};
|
|
567
584
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
568
585
|
identities.value = isAdministratorTab
|
|
569
|
-
?
|
|
586
|
+
? mergedIdentities.filter((identity: Record<string, any>) => {
|
|
570
587
|
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
571
588
|
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
572
589
|
})
|
|
573
590
|
: isVisitorTab
|
|
574
|
-
?
|
|
575
|
-
:
|
|
591
|
+
? mergedIdentities.filter((identity: Record<string, any>) => Boolean(getVisitorRecord(identity)))
|
|
592
|
+
: mergedIdentities;
|
|
576
593
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
577
594
|
liveAccessLogs.value = hidAccessLogs;
|
|
578
595
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
579
596
|
await loadUserImages(identities.value);
|
|
580
597
|
} finally {
|
|
581
|
-
|
|
598
|
+
if (sequence === loadSequence.value) {
|
|
599
|
+
loading.value = false;
|
|
600
|
+
}
|
|
582
601
|
}
|
|
583
602
|
}
|
|
584
603
|
|
|
@@ -586,9 +605,9 @@ async function loadUserImages(items: Record<string, any>[]) {
|
|
|
586
605
|
if (!selectedReaderId.value) return;
|
|
587
606
|
|
|
588
607
|
await Promise.all(items.map(async (identity) => {
|
|
589
|
-
const hidUserId = identity.hidUserId;
|
|
608
|
+
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
590
609
|
const key = String(hidUserId || "");
|
|
591
|
-
if (!hidUserId || userImages.value[key] || identity?.metadata?.photo) return;
|
|
610
|
+
if (!hidUserId || userImages.value[key] || identity?.metadata?.photo || !hasHidImageHint(identity)) return;
|
|
592
611
|
|
|
593
612
|
try {
|
|
594
613
|
const response = await getUserImage(selectedReaderId.value, hidUserId);
|
|
@@ -602,6 +621,18 @@ async function loadUserImages(items: Record<string, any>[]) {
|
|
|
602
621
|
}));
|
|
603
622
|
}
|
|
604
623
|
|
|
624
|
+
function hasHidImageHint(identity: Record<string, any>) {
|
|
625
|
+
return Boolean(
|
|
626
|
+
identity?.metadata?.imageTimestamp ||
|
|
627
|
+
identity?.metadata?.facialData ||
|
|
628
|
+
identity?.metadata?.photo ||
|
|
629
|
+
identity?.imageTimestamp ||
|
|
630
|
+
identity?.image_timestamp ||
|
|
631
|
+
identity?.facialData ||
|
|
632
|
+
identity?.photo,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
605
636
|
async function loadLiveAccessLogs(readerId: string) {
|
|
606
637
|
const allLogs: Record<string, any>[] = [];
|
|
607
638
|
const limit = 500;
|
|
@@ -652,20 +683,113 @@ async function loadReaderConfiguration(readerId: string) {
|
|
|
652
683
|
}
|
|
653
684
|
}
|
|
654
685
|
|
|
655
|
-
function onTabChange() {
|
|
686
|
+
async function onTabChange(tab: AccessTab) {
|
|
687
|
+
if (tab) {
|
|
688
|
+
activeTab.value = tab;
|
|
689
|
+
}
|
|
656
690
|
page.value = 1;
|
|
691
|
+
resetDashboardData();
|
|
692
|
+
await nextTick();
|
|
657
693
|
loadDashboard();
|
|
658
694
|
}
|
|
659
695
|
|
|
696
|
+
async function loadReaderUsers(readerId: string) {
|
|
697
|
+
try {
|
|
698
|
+
const response = await runObjectOperation(readerId, {
|
|
699
|
+
operation: "load",
|
|
700
|
+
object: "users",
|
|
701
|
+
limit: 500,
|
|
702
|
+
offset: 0,
|
|
703
|
+
});
|
|
704
|
+
return normalizeHidUsers(response);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.warn("Unable to load HID users for access log images", error);
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function normalizeHidUsers(response: Record<string, any>) {
|
|
712
|
+
const items =
|
|
713
|
+
response?.data?.users ||
|
|
714
|
+
response?.data?.data?.users ||
|
|
715
|
+
response?.users ||
|
|
716
|
+
[];
|
|
717
|
+
|
|
718
|
+
return Array.isArray(items)
|
|
719
|
+
? items.map((user) => ({
|
|
720
|
+
hidUserId: String(user.id || ""),
|
|
721
|
+
rawHidUserId: user.id,
|
|
722
|
+
registration: user.registration,
|
|
723
|
+
cardNo: user.card_value || user.cardNo || "",
|
|
724
|
+
name: user.name,
|
|
725
|
+
metadata: {
|
|
726
|
+
name: user.name,
|
|
727
|
+
imageTimestamp: user.image_timestamp ? String(user.image_timestamp) : "",
|
|
728
|
+
hidReaderSource: true,
|
|
729
|
+
},
|
|
730
|
+
}))
|
|
731
|
+
: [];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], identities: Record<string, any>[]) {
|
|
735
|
+
const identityById = new Map<string, Record<string, any>>();
|
|
736
|
+
identities.forEach((identity) => {
|
|
737
|
+
const id = toHidNumericId(identity.hidUserId);
|
|
738
|
+
if (id) identityById.set(String(id), identity);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const mergedReaderUsers = readerUsers.map((readerUser) => {
|
|
742
|
+
const identity = identityById.get(String(toHidNumericId(readerUser.hidUserId)));
|
|
743
|
+
if (!identity) return readerUser;
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
...readerUser,
|
|
747
|
+
...identity,
|
|
748
|
+
hidUserId: identity.hidUserId || readerUser.hidUserId,
|
|
749
|
+
rawHidUserId: readerUser.rawHidUserId,
|
|
750
|
+
name: identity.metadata?.name || identity.name || readerUser.name,
|
|
751
|
+
registration: identity.registration || readerUser.registration,
|
|
752
|
+
cardNo: identity.cardNo || readerUser.cardNo,
|
|
753
|
+
metadata: {
|
|
754
|
+
...readerUser.metadata,
|
|
755
|
+
...identity.metadata,
|
|
756
|
+
imageTimestamp: identity.metadata?.imageTimestamp || readerUser.metadata?.imageTimestamp,
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const readerUserIds = new Set(readerUsers.map((user) => String(toHidNumericId(user.hidUserId))));
|
|
762
|
+
const identitiesOnly = identities.filter((identity) => !readerUserIds.has(String(toHidNumericId(identity.hidUserId))));
|
|
763
|
+
return [...mergedReaderUsers, ...identitiesOnly];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function resetDashboardData() {
|
|
767
|
+
selectedHistoryRow.value = undefined;
|
|
768
|
+
identities.value = [];
|
|
769
|
+
visitorRecords.value = {};
|
|
770
|
+
logs.value = [];
|
|
771
|
+
liveAccessLogs.value = [];
|
|
772
|
+
readerConfiguration.value = {};
|
|
773
|
+
administratorUserIds.value = new Set();
|
|
774
|
+
}
|
|
775
|
+
|
|
660
776
|
function getIdentityType(tab: AccessTab) {
|
|
661
777
|
if (tab === "administrator") return "admin";
|
|
662
778
|
return tab;
|
|
663
779
|
}
|
|
664
780
|
|
|
665
781
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
782
|
+
const identity = findIdentityByKey(row.identityKey);
|
|
783
|
+
if (activeTab.value === "visitor") {
|
|
784
|
+
return Boolean(identity && getVisitorRecord(identity));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (activeTab.value === "administrator") {
|
|
788
|
+
const hidUserId = toHidNumericId(identity?.hidUserId ?? row.identityKey.split("|")[0]);
|
|
789
|
+
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return !identity || !getVisitorRecord(identity);
|
|
669
793
|
}
|
|
670
794
|
|
|
671
795
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -715,6 +839,11 @@ function getIdentityKey(identity: Record<string, any>) {
|
|
|
715
839
|
].filter(Boolean).join("|");
|
|
716
840
|
}
|
|
717
841
|
|
|
842
|
+
function findIdentityByKey(identityKey: string) {
|
|
843
|
+
if (!identityKey) return undefined;
|
|
844
|
+
return identities.value.find((identity) => getIdentityKey(identity) === identityKey);
|
|
845
|
+
}
|
|
846
|
+
|
|
718
847
|
function getLogIdentityKey(log: Record<string, any>) {
|
|
719
848
|
const identity = log.payload?.identity || {};
|
|
720
849
|
const lookup = log.payload?.identityLookup || {};
|
|
@@ -731,11 +860,12 @@ function getName(identity: Record<string, any>) {
|
|
|
731
860
|
}
|
|
732
861
|
|
|
733
862
|
function getFacialData(identity: Record<string, any>) {
|
|
734
|
-
return identity.metadata?.facialData || (identity.metadata?.photo ? identity.hidUserId : "N/A");
|
|
863
|
+
return identity.metadata?.facialData || identity.metadata?.imageTimestamp || (identity.metadata?.photo ? identity.hidUserId : "N/A");
|
|
735
864
|
}
|
|
736
865
|
|
|
737
866
|
function getUserImageSrc(identity: Record<string, any>) {
|
|
738
|
-
|
|
867
|
+
const hidUserId = toHidNumericId(identity?.hidUserId);
|
|
868
|
+
return identity?.metadata?.photo || (hidUserId ? userImages.value[String(hidUserId)] : "") || "";
|
|
739
869
|
}
|
|
740
870
|
|
|
741
871
|
function getUnit(identity: Record<string, any>) {
|
|
@@ -758,27 +888,60 @@ function getAccessMethod(identity: Record<string, any>, log?: Record<string, any
|
|
|
758
888
|
if (configuredLogType) return configuredLogType;
|
|
759
889
|
const configuredRule = getConfigurationLabel("identification_rules", payload.identification_rule_id || payload.identificationRuleId);
|
|
760
890
|
if (configuredRule) return configuredRule;
|
|
891
|
+
const rawIdentification = String(payload.identification || payload.identification_type || payload.identificationType || "").toLowerCase();
|
|
892
|
+
if (rawIdentification.includes("facial") || rawIdentification.includes("face")) return "Facial";
|
|
761
893
|
if (payload.qrcode_value || payload.qrCode || payload.qrcode) return "QR Code";
|
|
762
894
|
if (payload.card_value || payload.cardNo || identity.cardNo) return "Card";
|
|
763
895
|
if (payload.pin_value) return "PIN";
|
|
764
|
-
if (payload.password_value || payload.password || payload.identifier_id) return "ID and Password";
|
|
765
896
|
if (payload.confidence || payload.user_id || payload.userId || payload.hidUserId) return "Facial";
|
|
897
|
+
if (payload.password_value || payload.password || isValidIdentifierId(payload.identifier_id)) return "ID and Password";
|
|
766
898
|
if (identity.metadata?.pinPassword) return "ID and Password";
|
|
767
899
|
return identity.metadata?.photo || identity.metadata?.facialData ? "Facial" : "N/A";
|
|
768
900
|
}
|
|
769
901
|
|
|
770
902
|
function formatDate(value?: string | number) {
|
|
771
903
|
if (!value) return "N/A";
|
|
904
|
+
if (typeof value === "number") return formatHidEpochDate(value);
|
|
772
905
|
const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
|
|
773
906
|
return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
|
|
774
907
|
}
|
|
775
908
|
|
|
776
909
|
function parseDate(value?: string | number) {
|
|
777
910
|
if (!value) return undefined;
|
|
778
|
-
const date = typeof value === "number" ?
|
|
911
|
+
const date = typeof value === "number" ? parseHidEpochDate(value) : new Date(value);
|
|
779
912
|
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
780
913
|
}
|
|
781
914
|
|
|
915
|
+
function isValidIdentifierId(value: unknown) {
|
|
916
|
+
if (value === undefined || value === null || value === "") return false;
|
|
917
|
+
return Number(value) >= 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function parseHidEpochDate(value: number) {
|
|
921
|
+
const date = new Date(value * 1000);
|
|
922
|
+
return new Date(
|
|
923
|
+
date.getUTCFullYear(),
|
|
924
|
+
date.getUTCMonth(),
|
|
925
|
+
date.getUTCDate(),
|
|
926
|
+
date.getUTCHours(),
|
|
927
|
+
date.getUTCMinutes(),
|
|
928
|
+
date.getUTCSeconds(),
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function formatHidEpochDate(value: number) {
|
|
933
|
+
const date = new Date(value * 1000);
|
|
934
|
+
if (Number.isNaN(date.getTime())) return "N/A";
|
|
935
|
+
|
|
936
|
+
const time = [
|
|
937
|
+
date.getUTCHours(),
|
|
938
|
+
date.getUTCMinutes(),
|
|
939
|
+
date.getUTCSeconds(),
|
|
940
|
+
].map((part) => String(part).padStart(2, "0")).join(":");
|
|
941
|
+
|
|
942
|
+
return `${time} ${date.getUTCDate()}/${date.getUTCMonth() + 1}/${date.getUTCFullYear()}`;
|
|
943
|
+
}
|
|
944
|
+
|
|
782
945
|
function openHistoryDialog(row: AccessRow) {
|
|
783
946
|
selectedHistoryRow.value = row;
|
|
784
947
|
historyDialog.value = true;
|