@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 3.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 96c813f: Update Layer Common version
8
+
3
9
  ## 3.0.18
4
10
 
5
11
  ### Patch Changes
@@ -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 dense clearable
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 { getAll: _getBuildings, deleteById } = useBuilding();
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 @click:row="tableRowClickHandler"
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" v-model="dialogPreview":width="smAndUp ? '70svw' : '100%'" persistent>
49
- <BuildingUnitFormEdit v-if="selectedBuildingUnit" @cancel="dialogPreview = false" @success="successUpdate()" @delete-unit="openDeleteDialog()"
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
- .v-data-table :deep(thead th) {
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
- identities.value = [];
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 ? getVisitors({ site: props.site, page: 1, limit: 500, order: "desc" }) : Promise.resolve(null),
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
- ? loadedIdentities.filter((identity: Record<string, any>) => {
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
- ? loadedIdentities.filter((identity: Record<string, any>) => Boolean(getVisitorRecord(identity)))
575
- : loadedIdentities;
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
- loading.value = false;
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
- if (activeTab.value !== "administrator") return true;
667
- const hidUserId = toHidNumericId(row.identityKey.split("|")[0]);
668
- return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
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
- return identity?.metadata?.photo || userImages.value[String(identity?.hidUserId || "")] || "";
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" ? new Date(value * 1000) : new Date(value);
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;