@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/.changeset/camera-wall-plain-english.md +60 -0
  3. package/.changeset/camera-wall-selection-and-guidance.md +65 -0
  4. package/.changeset/dark-primary-contrast.md +39 -0
  5. package/.changeset/dashboard-dark-theme-contrast.md +15 -0
  6. package/.changeset/notification-preferences.md +17 -0
  7. package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
  8. package/.changeset/service-provider-invitation-actions.md +28 -0
  9. package/assets/css/primitives.css +680 -0
  10. package/assets/css/screens.css +541 -0
  11. package/assets/css/shell.css +258 -0
  12. package/assets/css/tokens.css +220 -0
  13. package/components/AccessCardQrTagging.vue +1 -1
  14. package/components/AccessManagement.vue +53 -72
  15. package/components/AppButton.vue +52 -0
  16. package/components/AppCard.vue +20 -0
  17. package/components/AppDateField.vue +35 -0
  18. package/components/AppField.vue +51 -0
  19. package/components/AppKpiTile.vue +65 -0
  20. package/components/AppSegmented.vue +38 -0
  21. package/components/AppSelect.vue +203 -0
  22. package/components/AreaChecklistHistoryLogs.vue +51 -44
  23. package/components/AreaChecklistHistoryMain.vue +6 -2
  24. package/components/AreaMain.vue +9 -17
  25. package/components/AttendanceMain.vue +4 -3
  26. package/components/Avatar/Main.vue +54 -2
  27. package/components/BillingMain.vue +26 -30
  28. package/components/BuildingManagement/buildings.vue +27 -38
  29. package/components/BuildingManagement/units.vue +26 -42
  30. package/components/BulletinBoardManagement.vue +23 -10
  31. package/components/CameraWall.vue +1034 -0
  32. package/components/CameraWallTile.vue +818 -0
  33. package/components/CardToolbar.vue +37 -0
  34. package/components/CleaningScheduleMain.vue +29 -46
  35. package/components/ClientMain.vue +90 -78
  36. package/components/DashboardEmptyState.vue +11 -1
  37. package/components/DashboardMain.vue +349 -275
  38. package/components/DashboardPanel.vue +1 -1
  39. package/components/DocumentManagement.vue +129 -93
  40. package/components/EntryPassInformation.vue +4 -4
  41. package/components/EntryPassMain.vue +49 -27
  42. package/components/EquipmentItemMain.vue +4 -6
  43. package/components/EquipmentManagementMain.vue +9 -7
  44. package/components/FeedbackMain.vue +42 -56
  45. package/components/FormDialog.vue +60 -21
  46. package/components/HidAccessLogDashboard.vue +119 -44
  47. package/components/HidCameraCaptureDialog.vue +199 -0
  48. package/components/HidIntercomManagement.vue +825 -202
  49. package/components/HidQrCodeConfiguration.vue +927 -93
  50. package/components/HidServiceSettingsPanel.vue +20 -1
  51. package/components/HidUserEnrollment.vue +454 -106
  52. package/components/InvitationMain.vue +46 -61
  53. package/components/Layout/Header.vue +289 -37
  54. package/components/Layout/NavigationDrawer.vue +340 -30
  55. package/components/ManageChecklistMain.vue +17 -17
  56. package/components/MemberMain.vue +79 -60
  57. package/components/MyAttendanceMain.vue +3 -5
  58. package/components/NavigationItem.vue +9 -0
  59. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  60. package/components/NotificationSettings.vue +361 -0
  61. package/components/OnlineFormConfigurationForm.vue +620 -224
  62. package/components/OnlineFormFill.vue +36 -8
  63. package/components/OnlineFormsConfiguration.vue +7 -3
  64. package/components/PageHeader.vue +43 -0
  65. package/components/PassInformation.vue +104 -45
  66. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  67. package/components/QrTemplate/PrintDialog.vue +209 -196
  68. package/components/RolePermissionMain.vue +184 -91
  69. package/components/ScanVisitorQRCode.vue +35 -7
  70. package/components/ScheduleAreaMain.vue +15 -49
  71. package/components/ScheduleTaskMain.vue +23 -26
  72. package/components/ScheduleTastTicketMain.vue +18 -43
  73. package/components/ServiceProviderInvitationPrompt.vue +150 -0
  74. package/components/ServiceProviderMain.vue +346 -157
  75. package/components/SiteSettings.vue +25 -10
  76. package/components/SiteSettingsWorkOrder.vue +61 -30
  77. package/components/SpecificAttr.vue +10 -2
  78. package/components/StatusChip.vue +93 -0
  79. package/components/StockCard.vue +17 -12
  80. package/components/SwitchContext.vue +33 -1
  81. package/components/TableHygiene.vue +29 -46
  82. package/components/TableList.vue +1 -1
  83. package/components/TableListSecondary.vue +1 -1
  84. package/components/TableMain.vue +161 -77
  85. package/components/TableV3.vue +61 -66
  86. package/components/UnitMain.vue +9 -17
  87. package/components/VehicleManagement.vue +20 -9
  88. package/components/VisitorForm.vue +294 -78
  89. package/components/VisitorManagement.vue +221 -71
  90. package/components/VisitorSocketPopUp.vue +56 -2
  91. package/components/WorkOrder/Main.vue +42 -51
  92. package/composables/useComment.ts +3 -3
  93. package/composables/useHidAmico.ts +151 -0
  94. package/composables/useHidNavigation.ts +0 -7
  95. package/composables/useLocalAuth.ts +9 -36
  96. package/composables/useNotificationPreference.ts +87 -0
  97. package/composables/useOnlineFormPages.ts +2 -0
  98. package/composables/useOrg.ts +1 -1
  99. package/composables/useRole.ts +22 -0
  100. package/composables/useServiceProviderInvitation.ts +145 -0
  101. package/composables/useSipWebPhone.ts +336 -0
  102. package/composables/useSiteSettings.ts +50 -0
  103. package/composables/useThemePreference.ts +63 -0
  104. package/composables/useVisitor.ts +6 -1
  105. package/composables/useVisitorSocket.ts +26 -0
  106. package/composables/useWebUsb.ts +127 -37
  107. package/nuxt.config.ts +31 -0
  108. package/package.json +4 -1
  109. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  110. package/pages/notification-settings.vue +19 -0
  111. package/plugins/secure-member.client.ts +21 -56
  112. package/plugins/vuetify.ts +68 -9
  113. package/test/visitor-socket.test.mjs +36 -0
  114. package/types/org.d.ts +2 -0
  115. package/types/site.d.ts +71 -0
  116. package/utils/camera-wall.test.ts +571 -0
  117. package/utils/camera-wall.ts +926 -0
  118. package/utils/nav-sections.test.ts +190 -0
  119. package/utils/nav-sections.ts +151 -0
  120. package/utils/status.test.ts +77 -0
  121. package/utils/status.ts +90 -0
  122. package/utils/theme.test.ts +534 -0
  123. package/utils/theme.ts +294 -0
  124. package/components/HidIdentityMapping.vue +0 -975
  125. package/middleware/member.ts +0 -4
  126. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -1,7 +1,17 @@
1
1
  <template>
2
- <v-row no-gutters>
2
+ <div>
3
+ <!-- The design's title row: the page's name at 22px and its primary
4
+ action on the right of that same line. Placement only - the same
5
+ button opens the same dialog under the same permission. -->
6
+ <PageHeader title="Feedbacks">
7
+ <template v-if="canCreateFeedback" #actions>
8
+ <AppButton @click="showCreateDialog = true">Create Feedback</AppButton>
9
+ </template>
10
+ </PageHeader>
11
+
12
+ <v-row no-gutters>
3
13
  <v-col cols="12" class="mb-2">
4
- <v-row no-gutters align="center">
14
+ <v-row no-gutters align="center" class="filter-bar">
5
15
  <v-text-field
6
16
  v-model="searchText"
7
17
  placeholder="Search feedback..."
@@ -50,51 +60,32 @@
50
60
  <v-date-picker v-model="endDate" width="320" :locale="locale" />
51
61
  </v-card>
52
62
  </v-dialog>
53
- <v-btn
54
- v-if="canCreateFeedback"
55
- text="create feedback"
56
- class="text-none text-capitalize ml-2"
57
- rounded="pill"
58
- variant="tonal"
59
- size="large"
60
- @click="showCreateDialog = true"
61
- />
62
63
  </v-row>
63
64
  </v-col>
64
65
 
65
66
  <v-col cols="12">
66
- <v-card
67
- width="100%"
68
- variant="outlined"
69
- border="thin"
70
- rounded="lg"
71
- :loading="loading"
72
- >
73
- <v-toolbar density="compact" color="grey-lighten-4">
74
- <template #prepend>
75
- <v-btn
76
- fab
77
- icon
78
- density="comfortable"
67
+ <AppCard class="table-card">
68
+ <!-- One row inside the card - refresh, the range and the pager - in
69
+ place of a `v-toolbar` that stacked them above the table. Same
70
+ three controls, same handlers. -->
71
+ <CardToolbar :count="pageRange">
72
+ <template #left>
73
+ <AppButton
74
+ variant="icon"
75
+ icon="mdi-refresh"
76
+ aria-label="Refresh"
79
77
  @click="(page = 1), refreshFeedbacks()"
80
- >
81
- <v-icon icon="mdi-refresh" />
82
- </v-btn>
78
+ />
83
79
  </template>
84
80
 
85
- <template #append>
86
- <v-row no-gutters justify="end" align="center">
87
- <span class="mr-2 text-caption text-fontgray">
88
- {{ pageRange }}
89
- </span>
90
- <local-pagination
91
- v-model="page"
92
- :length="pages"
93
- @update:value="updatePage"
94
- />
95
- </v-row>
81
+ <template #right>
82
+ <local-pagination
83
+ v-model="page"
84
+ :length="pages"
85
+ @update:value="updatePage"
86
+ />
96
87
  </template>
97
- </v-toolbar>
88
+ </CardToolbar>
98
89
 
99
90
  <v-data-table
100
91
  :headers="headers"
@@ -104,6 +95,7 @@
104
95
  fixed-header
105
96
  hide-default-footer
106
97
  :hide-default-header="false"
98
+ :loading="loading"
107
99
  @click:row="tableRowClickHandler"
108
100
  style="max-height: calc(100vh - (200px))"
109
101
  >
@@ -139,13 +131,7 @@
139
131
  </template>
140
132
  <template #item.status="{ value }">
141
133
  <span class="d-inline-block text-truncate">
142
- <v-chip
143
- :color="getStatusColor(value ?? '')"
144
- class="pa-5 text-capitalize text-center whitespace-nowrap text-body-1"
145
- variant="flat"
146
- >
147
- {{ value }}
148
- </v-chip>
134
+ <StatusChip class="text-capitalize" :status="value" />
149
135
  </span>
150
136
  </template>
151
137
  <template #item.assigneeInfo="{ item, index }">
@@ -183,9 +169,10 @@
183
169
  <span v-else class="mt-1 text-wrap"> Not Assigned </span>
184
170
  </template>
185
171
  </v-data-table>
186
- </v-card>
172
+ </AppCard>
187
173
  </v-col>
188
- </v-row>
174
+ </v-row>
175
+ </div>
189
176
 
190
177
  <!-- <FeedbackForm
191
178
  v-model="showCreateDialog"
@@ -660,7 +647,6 @@ const _feedback = ref({
660
647
  });
661
648
 
662
649
  const {
663
- getStatusColor,
664
650
  debounce,
665
651
  standardFormatDateTime,
666
652
  standardFormatDate,
@@ -721,9 +707,9 @@ const {
721
707
  _getFeedbacks({
722
708
  page: page.value,
723
709
  site: route.params.site as string,
724
- provider: currentUser.value.serviceProvider,
725
- ...(currentUser.value.type != "site" && {
726
- userId: currentUser.value._id,
710
+ provider: currentUser.value?.serviceProvider,
711
+ ...(currentUser.value?.type != "site" && {
712
+ userId: currentUser.value?._id,
727
713
  }),
728
714
  service: "Security",
729
715
  dateFrom: moment(startDate.value, "DD/MM/YYYY").startOf("day"),
@@ -998,9 +984,9 @@ async function handleAccept() {
998
984
  _id: feedbackData.value._id,
999
985
  statusUpdate: {
1000
986
  status: "In-Progress",
1001
- updatedById: currentUser.value._id,
1002
- updatedByName: `${currentUser.value.givenName} ${currentUser.value.surname}`,
1003
- assignee: currentUser.value._id,
987
+ updatedById: currentUser.value?._id,
988
+ updatedByName: `${currentUser.value?.givenName} ${currentUser.value?.surname}`,
989
+ assignee: currentUser.value?._id,
1004
990
  provider: currentUser.value?.serviceProvider,
1005
991
  },
1006
992
  };
@@ -1158,7 +1144,7 @@ async function submit() {
1158
1144
  isSubmitting.value = true;
1159
1145
 
1160
1146
  const result = await createFeedback({
1161
- createdBy: currentUser.value._id,
1147
+ createdBy: currentUser.value?._id,
1162
1148
  description: _feedback.value.description,
1163
1149
  attachments: _feedback.value.attachments,
1164
1150
  site: route.params.site as string,
@@ -1,31 +1,35 @@
1
+ <!--
2
+ PHASE 3 - THE MODAL.
3
+
4
+ 520px, 16px radius and the design's overlay and shadow, with the BODY as the
5
+ only thing that scrolls so the footer never leaves the screen. Same dialog,
6
+ same slots, same close behaviour, same `persistent`.
7
+
8
+ The two `theme.name.value === ...` branches are gone, and that is a fix, not
9
+ a preference: they hard-coded `bg-white` and Vuetify's `grey-darken-4`, so
10
+ the card was painted by a literal colour instead of by the theme it is in.
11
+ `--card` is that surface in each theme.
12
+ -->
1
13
  <template>
2
- <v-dialog v-model="model" max-width="600" persistent>
3
- <v-card
4
- class="px-3 d-flex flex-column"
5
- :class="`${theme.name.value === 'light' ? 'bg-white' : ''} ${
6
- mdAndUp ? 'rounded-xl' : ''
7
- }`"
8
- :style="{ border: 'none', boxShadow: 'none' }"
9
- >
14
+ <v-dialog v-model="model" :max-width="520" persistent class="form-dialog">
15
+ <v-card class="d-flex flex-column form-dialog__card" :style="{ border: 'none' }">
10
16
  <v-toolbar
11
- :color="theme.name.value === 'dark' ? 'grey-darken-4' : 'white'"
12
- class="flex-shrink-0 mb-3"
17
+ color="transparent"
18
+ class="flex-shrink-0 form-dialog__head"
13
19
  style="position: sticky; top: 0; z-index: 10"
14
20
  >
15
21
  <slot name="title">
16
- <span class="text-h6 font-weight-medium pt-1 text-capitalize">
17
- Dialog
18
- </span>
22
+ <span class="form-dialog__title"> Dialog </span>
19
23
  </slot>
20
24
  <v-spacer />
21
- <v-btn icon="mdi-close" @click="closeDialog" />
25
+ <v-btn icon="mdi-close" variant="text" @click="closeDialog" />
22
26
  </v-toolbar>
23
27
 
24
- <v-sheet class="flex-grow-1 overflow-y-auto px-3">
28
+ <v-sheet class="flex-grow-1 overflow-y-auto px-4 form-dialog__body">
25
29
  <slot />
26
30
  </v-sheet>
27
31
 
28
- <v-sheet class="flex-shrink-0 px-3 py-2">
32
+ <v-sheet class="flex-shrink-0 px-4 py-3 form-dialog__footer">
29
33
  <slot name="footer" />
30
34
  </v-sheet>
31
35
  </v-card>
@@ -33,11 +37,6 @@
33
37
  </template>
34
38
 
35
39
  <script setup lang="ts">
36
- import { useTheme, useDisplay } from "vuetify";
37
-
38
- const theme = useTheme().global;
39
- const { mdAndUp } = useDisplay();
40
-
41
40
  const props = defineProps<{
42
41
  modelValue: boolean;
43
42
  }>();
@@ -62,4 +61,44 @@ function closeDialog() {
62
61
  :deep(.v-dialog .v-overlay__content) {
63
62
  margin: auto;
64
63
  }
64
+
65
+ .form-dialog__card {
66
+ font-family: var(--font-sans);
67
+ background: var(--card);
68
+ /* `!important` because Vuetify's own `.v-overlay__content > .v-card` rule is
69
+ more specific than a class on the card, and it is what keeps the dialog at
70
+ its factory 4px. */
71
+ border-radius: var(--r-modal) !important;
72
+ box-shadow: var(--shadow-modal);
73
+ /* Never taller than the screen it has to fit inside; the body is what
74
+ scrolls, so the Cancel/Submit footer cannot be pushed out of view. */
75
+ max-height: calc(100vh - 32px);
76
+ overflow: hidden;
77
+ }
78
+
79
+ .form-dialog__head {
80
+ border-bottom: 1px solid var(--border);
81
+ }
82
+
83
+ .form-dialog__title {
84
+ font-size: var(--fs-card-title);
85
+ font-weight: var(--fw-card-title);
86
+ color: var(--text);
87
+ text-transform: capitalize;
88
+ }
89
+
90
+ .form-dialog__body,
91
+ .form-dialog__footer {
92
+ background: var(--card);
93
+ }
94
+
95
+ .form-dialog__footer {
96
+ border-top: 1px solid var(--border);
97
+ }
98
+
99
+ @media (max-width: 599.98px) {
100
+ .form-dialog__card {
101
+ max-height: calc(100dvh - 32px);
102
+ }
103
+ }
65
104
  </style>
@@ -327,6 +327,8 @@ type AccessTab = "resident" | "visitor" | "administrator";
327
327
  type AccessRow = {
328
328
  key: string;
329
329
  identityKey: string;
330
+ identityType?: string;
331
+ visitorId?: string;
330
332
  name: string;
331
333
  facialData: string;
332
334
  faceImage?: string;
@@ -351,6 +353,7 @@ const { getVisitors } = useVisitor();
351
353
 
352
354
  const readers = ref<Record<string, any>[]>([]);
353
355
  const identities = ref<Record<string, any>[]>([]);
356
+ const allIdentities = ref<Record<string, any>[]>([]);
354
357
  const visitorRecords = ref<Record<string, any>>({});
355
358
  const logs = ref<Record<string, any>[]>([]);
356
359
  const liveAccessLogs = ref<Record<string, any>[]>([]);
@@ -418,8 +421,17 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
418
421
  if (!selectedHistoryRow.value) return [];
419
422
 
420
423
  const selectedKey = selectedHistoryRow.value.identityKey;
421
- const matchedLogs = logs.value.filter((log) => getLogIdentityKey(log) === selectedKey);
422
- const matchedLiveLogs = liveAccessLogs.value.filter((log) => getAccessLogIdentityKey(log) === selectedKey);
424
+ const selectedIdentity = findIdentityByKey(selectedKey);
425
+ const matchedLogs = logs.value.filter((log) =>
426
+ selectedIdentity
427
+ ? accessLogMatchesIdentity(log, selectedIdentity)
428
+ : getLogIdentityKey(log) === selectedKey,
429
+ );
430
+ const matchedLiveLogs = liveAccessLogs.value.filter((log) =>
431
+ selectedIdentity
432
+ ? accessLogMatchesIdentity(log, selectedIdentity)
433
+ : getAccessLogIdentityKey(log) === selectedKey,
434
+ );
423
435
 
424
436
  if (!matchedLogs.length && !matchedLiveLogs.length) {
425
437
  return rows.value.filter((row) => row.identityKey === selectedKey);
@@ -549,28 +561,41 @@ async function loadDashboard() {
549
561
  const isAdministratorTab = activeTab.value === "administrator";
550
562
  const isVisitorTab = activeTab.value === "visitor";
551
563
  const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
552
- getIdentities(selectedReaderId.value, {
553
- page: 1,
554
- limit: 500,
555
- ...(!isAdministratorTab ? { type: getIdentityType(activeTab.value) } : {}),
556
- }),
557
- getLogs(selectedReaderId.value, {
558
- page: 1,
559
- limit: 500,
560
- }),
561
- loadLiveAccessLogs(selectedReaderId.value),
562
- loadReaderConfiguration(selectedReaderId.value),
563
- isAdministratorTab ? loadAdministratorRoles(selectedReaderId.value) : Promise.resolve(null),
564
+ loadDashboardSource(
565
+ getIdentities(selectedReaderId.value, {
566
+ page: 1,
567
+ limit: 500,
568
+ }),
569
+ null,
570
+ "identities",
571
+ ),
572
+ loadDashboardSource(
573
+ getLogs(selectedReaderId.value, {
574
+ page: 1,
575
+ limit: 500,
576
+ }),
577
+ null,
578
+ "persisted access logs",
579
+ ),
580
+ loadDashboardSource(loadLiveAccessLogs(selectedReaderId.value), [], "live access logs"),
581
+ loadDashboardSource(loadReaderConfiguration(selectedReaderId.value), {}, "reader configuration"),
582
+ isAdministratorTab
583
+ ? loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles")
584
+ : Promise.resolve(null),
564
585
  isVisitorTab && props.org
565
- ? getVisitors({
566
- org: props.org,
567
- site: props.site,
568
- page: 1,
569
- limit: 500,
570
- order: "desc",
571
- })
586
+ ? loadDashboardSource(
587
+ getVisitors({
588
+ org: props.org,
589
+ site: props.site,
590
+ page: 1,
591
+ limit: 500,
592
+ order: "desc",
593
+ }),
594
+ null,
595
+ "visitor records",
596
+ )
572
597
  : Promise.resolve(null),
573
- loadReaderUsers(selectedReaderId.value),
598
+ loadDashboardSource(loadReaderUsers(selectedReaderId.value), [], "reader users"),
574
599
  ]);
575
600
 
576
601
  if (sequence !== loadSequence.value) return;
@@ -578,18 +603,19 @@ async function loadDashboard() {
578
603
  const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
579
604
  const mergedIdentities = mergeReaderUsersWithIdentities(readerUserResponse, loadedIdentities);
580
605
  const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
581
- visitorRecords.value = isVisitorTab
582
- ? Object.fromEntries(loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]))
583
- : {};
606
+ visitorRecords.value = Object.fromEntries(
607
+ loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]),
608
+ );
584
609
  administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
610
+ allIdentities.value = mergedIdentities;
585
611
  identities.value = isAdministratorTab
586
612
  ? mergedIdentities.filter((identity: Record<string, any>) => {
587
613
  const hidUserId = toHidNumericId(identity.hidUserId);
588
614
  return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
589
615
  })
590
- : isVisitorTab
591
- ? mergedIdentities.filter((identity: Record<string, any>) => Boolean(getVisitorRecord(identity)))
592
- : mergedIdentities;
616
+ : activeTab.value === "visitor"
617
+ ? mergedIdentities.filter(isVisitorIdentity)
618
+ : mergedIdentities.filter((identity: Record<string, any>) => !isVisitorIdentity(identity));
593
619
  logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
594
620
  liveAccessLogs.value = hidAccessLogs;
595
621
  readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
@@ -601,6 +627,15 @@ async function loadDashboard() {
601
627
  }
602
628
  }
603
629
 
630
+ async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: string): Promise<T> {
631
+ try {
632
+ return await request;
633
+ } catch (error) {
634
+ console.warn(`Unable to load HID ${label}`, error);
635
+ return fallback;
636
+ }
637
+ }
638
+
604
639
  async function loadUserImages(items: Record<string, any>[]) {
605
640
  if (!selectedReaderId.value) return;
606
641
 
@@ -766,6 +801,7 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
766
801
  function resetDashboardData() {
767
802
  selectedHistoryRow.value = undefined;
768
803
  identities.value = [];
804
+ allIdentities.value = [];
769
805
  visitorRecords.value = {};
770
806
  logs.value = [];
771
807
  liveAccessLogs.value = [];
@@ -773,15 +809,11 @@ function resetDashboardData() {
773
809
  administratorUserIds.value = new Set();
774
810
  }
775
811
 
776
- function getIdentityType(tab: AccessTab) {
777
- if (tab === "administrator") return "admin";
778
- return tab;
779
- }
780
-
781
812
  function isVisibleForActiveTab(row: AccessRow) {
782
813
  const identity = findIdentityByKey(row.identityKey);
814
+ const isVisitor = row.identityType === "visitor" || Boolean(row.visitorId) || isVisitorIdentity(identity);
783
815
  if (activeTab.value === "visitor") {
784
- return Boolean(identity && getVisitorRecord(identity));
816
+ return isVisitor;
785
817
  }
786
818
 
787
819
  if (activeTab.value === "administrator") {
@@ -789,7 +821,11 @@ function isVisibleForActiveTab(row: AccessRow) {
789
821
  return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
790
822
  }
791
823
 
792
- return !identity || !getVisitorRecord(identity);
824
+ return !isVisitor;
825
+ }
826
+
827
+ function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
828
+ return Boolean(identity && (identity.type === "visitor" || identity.visitor));
793
829
  }
794
830
 
795
831
  function loadAdministratorRoles(readerId: string) {
@@ -835,13 +871,13 @@ function getIdentityKey(identity: Record<string, any>) {
835
871
  return [
836
872
  identity.hidUserId,
837
873
  identity.registration,
838
- identity.cardNo,
874
+ identity.cardNo || identity.metadata?.qrCodeValue,
839
875
  ].filter(Boolean).join("|");
840
876
  }
841
877
 
842
878
  function findIdentityByKey(identityKey: string) {
843
879
  if (!identityKey) return undefined;
844
- return identities.value.find((identity) => getIdentityKey(identity) === identityKey);
880
+ return allIdentities.value.find((identity) => getIdentityKey(identity) === identityKey);
845
881
  }
846
882
 
847
883
  function getLogIdentityKey(log: Record<string, any>) {
@@ -851,7 +887,7 @@ function getLogIdentityKey(log: Record<string, any>) {
851
887
  return [
852
888
  identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
853
889
  identity.registration || lookup.registration,
854
- identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
890
+ identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
855
891
  ].filter(Boolean).join("|");
856
892
  }
857
893
 
@@ -1145,12 +1181,15 @@ function getRawAccessLogs(event: Record<string, any>) {
1145
1181
 
1146
1182
  function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
1147
1183
  const identity = findIdentityForAccessLog(rawLog, event);
1184
+ const eventIdentity = event.payload?.identity || event.payload?.identityLookup || {};
1148
1185
  const rawAccessTime = getAccessLogTime(rawLog, event);
1149
1186
  const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
1150
1187
 
1151
1188
  return {
1152
1189
  key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
1153
1190
  identityKey,
1191
+ identityType: String(identity?.type || eventIdentity.type || "").toLowerCase() || undefined,
1192
+ visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
1154
1193
  name: identity ? getName(identity) : getRawLogName(rawLog, event),
1155
1194
  facialData: identity ? getFacialData(identity) : "N/A",
1156
1195
  faceImage: identity ? getUserImageSrc(identity) : "",
@@ -1166,32 +1205,33 @@ function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, an
1166
1205
  function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<string, any>) {
1167
1206
  const eventIdentityId = event.payload?.identity?._id;
1168
1207
  if (eventIdentityId) {
1169
- const byId = identities.value.find((identity) => String(identity._id) === String(eventIdentityId));
1208
+ const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
1170
1209
  if (byId) return byId;
1171
1210
  }
1172
1211
 
1173
1212
  const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
1174
1213
  const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
1175
1214
  const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
1215
+ const qrCodeValue = rawLog.qrcode_value || rawLog.qrCode || rawLog.qrcode;
1176
1216
 
1177
- return identities.value.find((identity) => {
1217
+ return allIdentities.value.find((identity) => {
1178
1218
  return (
1179
1219
  (hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
1180
1220
  (registration && identity.registration === registration) ||
1181
- (cardNo && String(identity.cardNo) === String(cardNo))
1221
+ (cardNo && String(identity.cardNo) === String(cardNo)) ||
1222
+ (qrCodeValue && String(identity.metadata?.qrCodeValue || "") === String(qrCodeValue))
1182
1223
  );
1183
1224
  });
1184
1225
  }
1185
1226
 
1186
1227
  function getLatestAccessLogForIdentity(identity: Record<string, any>) {
1187
- const identityKey = getIdentityKey(identity);
1188
1228
  const accessLogs = [
1189
1229
  ...liveAccessLogs.value,
1190
1230
  ...logs.value.flatMap((event) => getRawAccessLogs(event)),
1191
1231
  ];
1192
1232
 
1193
1233
  return accessLogs
1194
- .filter((log) => getAccessLogIdentityKey(log) === identityKey)
1234
+ .filter((log) => accessLogMatchesIdentity(log, identity))
1195
1235
  .sort((a, b) => {
1196
1236
  const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
1197
1237
  const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
@@ -1199,11 +1239,46 @@ function getLatestAccessLogForIdentity(identity: Record<string, any>) {
1199
1239
  })[0];
1200
1240
  }
1201
1241
 
1242
+ function toUnknownRecord(value: unknown): Record<string, unknown> {
1243
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1244
+ ? value as Record<string, unknown>
1245
+ : {};
1246
+ }
1247
+
1248
+ function accessLogMatchesIdentity(log: unknown, identity: unknown) {
1249
+ const logRecord = toUnknownRecord(log);
1250
+ const identityRecord = toUnknownRecord(identity);
1251
+ const payload = toUnknownRecord(logRecord.payload);
1252
+ const rawLog = Object.keys(toUnknownRecord(payload.rawAccessLog)).length
1253
+ ? toUnknownRecord(payload.rawAccessLog)
1254
+ : logRecord;
1255
+ const eventIdentity = Object.keys(toUnknownRecord(payload.identity)).length
1256
+ ? toUnknownRecord(payload.identity)
1257
+ : toUnknownRecord(payload.identityLookup);
1258
+ const metadata = toUnknownRecord(identityRecord.metadata);
1259
+ const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? eventIdentity.hidUserId;
1260
+ const registration = rawLog.registration || eventIdentity.registration;
1261
+ const credential =
1262
+ rawLog.card_value ||
1263
+ rawLog.cardNo ||
1264
+ rawLog.qrcode_value ||
1265
+ rawLog.qrCode ||
1266
+ rawLog.qrcode ||
1267
+ eventIdentity.cardNo;
1268
+ const identityCredential = identityRecord.cardNo || metadata.qrCodeValue;
1269
+
1270
+ return Boolean(
1271
+ (hidUserId !== undefined && hidUserId !== null && String(identityRecord.hidUserId) === String(hidUserId)) ||
1272
+ (registration && identityRecord.registration === registration) ||
1273
+ (credential && identityCredential && String(identityCredential) === String(credential)),
1274
+ );
1275
+ }
1276
+
1202
1277
  function getAccessLogIdentityKey(rawLog: Record<string, any>) {
1203
1278
  return [
1204
1279
  rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
1205
1280
  rawLog.registration,
1206
- rawLog.card_value || rawLog.cardNo,
1281
+ rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
1207
1282
  ].filter(Boolean).join("|");
1208
1283
  }
1209
1284