@7365admin1/layer-common 3.0.17 → 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.
@@ -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;
@@ -17,16 +17,18 @@
17
17
  class="mb-3"
18
18
  />
19
19
 
20
- <div class="field-label">Base URL / IP address <span>*</span></div>
21
- <v-text-field
22
- v-model="draft.baseUrl"
23
- aria-label="Base URL / IP address"
24
- placeholder="http://192.168.0.129"
25
- variant="outlined"
26
- density="compact"
27
- hide-details="auto"
28
- class="mb-3"
29
- />
20
+ <template v-if="mode === 'add'">
21
+ <div class="field-label">Base URL / IP address <span>*</span></div>
22
+ <v-text-field
23
+ v-model="draft.baseUrl"
24
+ aria-label="Base URL / IP address"
25
+ placeholder="http://192.168.x.x."
26
+ variant="outlined"
27
+ density="compact"
28
+ hide-details="auto"
29
+ class="mb-3"
30
+ />
31
+ </template>
30
32
 
31
33
  <div class="field-label">Device ID <span>*</span></div>
32
34
  <v-text-field
@@ -166,12 +168,18 @@ function close() {
166
168
  }
167
169
 
168
170
  function submit() {
169
- emit("submit", {
171
+ const payload: Record<string, any> = {
170
172
  ...draft,
171
173
  monitorPath: monitorEnabled.value ? "api/notifications" : "",
172
174
  enabled: monitorEnabled.value,
173
175
  status: monitorEnabled.value ? "active" : "inactive",
174
- });
176
+ };
177
+
178
+ if (props.mode === "edit") {
179
+ delete payload.baseUrl;
180
+ }
181
+
182
+ emit("submit", payload);
175
183
  }
176
184
  </script>
177
185
 
@@ -38,31 +38,26 @@
38
38
  <v-btn icon="mdi-refresh" variant="text" density="comfortable" :loading="loading" @click="loadReaders" />
39
39
  </div>
40
40
 
41
- <v-table>
41
+ <v-table class="reader-table">
42
42
  <thead>
43
43
  <tr>
44
- <th>Name</th>
45
- <th>Location</th>
46
- <th>Base URL</th>
47
- <th>Device ID</th>
48
- <th>Status</th>
49
- <th>Last sync at</th>
50
- <th>Last sync message</th>
51
- <th class="text-right"></th>
44
+ <th class="name-column">Name</th>
45
+ <th class="location-column">Location</th>
46
+ <th class="device-column">Device ID</th>
47
+ <th class="status-column">Status</th>
48
+ <th class="sync-at-column">Last sync at</th>
49
+ <th class="message-column">Last sync message</th>
50
+ <th class="action-column"></th>
52
51
  </tr>
53
52
  </thead>
54
53
  <tbody>
55
54
  <tr v-for="reader in filteredReaders" :key="reader._id">
56
- <td>{{ reader.name || "N/A" }}</td>
57
- <td>{{ reader.location || "N/A" }}</td>
58
- <td>
59
- <a v-if="reader.baseUrl" :href="reader.baseUrl" target="_blank" rel="noopener">
60
- {{ reader.baseUrl }}
61
- </a>
62
- <span v-else>N/A</span>
55
+ <td class="name-cell">
56
+ <span class="cell-strong">{{ reader.name || "N/A" }}</span>
63
57
  </td>
64
- <td>{{ reader.deviceId || "N/A" }}</td>
65
- <td>
58
+ <td class="location-cell">{{ reader.location || "N/A" }}</td>
59
+ <td class="device-cell">{{ reader.deviceId || "N/A" }}</td>
60
+ <td class="status-cell">
66
61
  <v-chip
67
62
  size="small"
68
63
  variant="flat"
@@ -72,12 +67,22 @@
72
67
  {{ formatReaderStatus(resolveReaderStatus(reader)) }}
73
68
  </v-chip>
74
69
  </td>
75
- <td>{{ formatDate(reader.lastSyncAt) }}</td>
76
- <td>{{ reader.lastSyncMessage || "N/A" }}</td>
77
- <td class="text-right">
70
+ <td class="sync-at-cell">{{ formatDate(reader.lastSyncAt) }}</td>
71
+ <td class="message-cell">
72
+ <span class="message-text" :title="reader.lastSyncMessage || 'N/A'">
73
+ {{ reader.lastSyncMessage || "N/A" }}
74
+ </span>
75
+ </td>
76
+ <td class="action-cell">
78
77
  <v-menu>
79
78
  <template #activator="{ props: menuProps }">
80
- <v-btn v-bind="menuProps" icon="mdi-dots-vertical" variant="text" density="comfortable" />
79
+ <v-btn
80
+ v-bind="menuProps"
81
+ icon="mdi-dots-vertical"
82
+ variant="text"
83
+ density="comfortable"
84
+ class="action-menu-button"
85
+ />
81
86
  </template>
82
87
 
83
88
  <v-list density="compact" min-width="180">
@@ -112,7 +117,6 @@
112
117
  <v-card-text>
113
118
  <div class="detail-grid">
114
119
  <span>HID reader name</span><strong>{{ selectedReader?.name || "N/A" }}</strong>
115
- <span>Base URL / IP address</span><strong>{{ selectedReader?.baseUrl || "N/A" }}</strong>
116
120
  <span>Device ID</span><strong>{{ selectedReader?.deviceId || "N/A" }}</strong>
117
121
  <span>Location</span><strong>{{ selectedReader?.location || "N/A" }}</strong>
118
122
  <span>Monitor Path</span><strong>{{ selectedReader?.monitorPath || "N/A" }}</strong>
@@ -140,10 +144,6 @@
140
144
  <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
141
145
  <h3>Test Device Connection</h3>
142
146
  <p>You are about to test the connection for {{ selectedReader?.name || "this HID Reader" }}.</p>
143
- <div class="reader-action-meta">
144
- <span>IP Address:</span>
145
- <strong>{{ readerIpAddress }}</strong>
146
- </div>
147
147
  </v-card-text>
148
148
 
149
149
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'confirm'" class="reader-action-content">
@@ -312,7 +312,7 @@ const confirmMessage = computed(() => {
312
312
  return `You're about to sync data with ${selectedReader.value?.name || "this HID Reader"}.`;
313
313
  }
314
314
  if (pendingAction.value === "test") {
315
- return `You are about to test the connection to ${selectedReader.value?.baseUrl || "this HID Reader"}.`;
315
+ return `You are about to test the connection for ${selectedReader.value?.name || "this HID Reader"}.`;
316
316
  }
317
317
  return "Are you sure you want to permanently delete reader?";
318
318
  });
@@ -325,8 +325,6 @@ const confirmActionLabel = computed(() => {
325
325
  return "Confirm";
326
326
  });
327
327
 
328
- const readerIpAddress = computed(() => getReaderHost(selectedReader.value?.baseUrl));
329
-
330
328
  watch(
331
329
  () => props.site,
332
330
  () => loadReaders(),
@@ -830,15 +828,6 @@ function countFacialData(users: Record<string, any>[]) {
830
828
  return users.filter((user) => user.face || user.photo || user.image || user.imageUrl || user.faceImage).length;
831
829
  }
832
830
 
833
- function getReaderHost(value?: string) {
834
- if (!value) return "N/A";
835
- try {
836
- return new URL(value).hostname || value;
837
- } catch {
838
- return value.replace(/^https?:\/\//, "").split("/")[0] || value;
839
- }
840
- }
841
-
842
831
  function getFriendlyHidError(error: any) {
843
832
  const message = getHidErrorMessage(error);
844
833
  const lowerMessage = message.toLowerCase();
@@ -863,7 +852,7 @@ function getFriendlyHidError(error: any) {
863
852
  lowerMessage.includes("network error") ||
864
853
  lowerMessage.includes("no route to host")
865
854
  ) {
866
- return "Unable to connect to the HID device. Please check that the reader is online, reachable from the server, and the Base URL/IP address is correct.";
855
+ return "Unable to connect to the HID device. Please check that the reader is online and reachable from the server.";
867
856
  }
868
857
 
869
858
  if (lowerMessage.includes("login failed") || lowerMessage.includes("session")) {
@@ -972,63 +961,119 @@ function formatReaderStatus(value?: string) {
972
961
  overflow-y: hidden;
973
962
  }
974
963
 
975
- .table-card :deep(table) {
964
+ .reader-table :deep(table) {
976
965
  table-layout: fixed;
977
966
  width: 100%;
978
- min-width: 1120px;
967
+ min-width: 920px;
968
+ border-collapse: separate;
969
+ border-spacing: 0;
970
+ }
971
+
972
+ .reader-table :deep(th) {
973
+ height: 44px;
974
+ background: #f8fafc;
975
+ color: #344054;
976
+ font-size: 12px;
977
+ font-weight: 700;
978
+ letter-spacing: 0;
979
+ text-transform: none;
980
+ border-bottom: 1px solid #e7ebef;
981
+ }
982
+
983
+ .reader-table :deep(td) {
984
+ height: 64px;
985
+ color: #1f2937;
986
+ font-size: 13px;
987
+ border-bottom: 1px solid #edf0f3;
988
+ vertical-align: middle;
989
+ }
990
+
991
+ .reader-table :deep(tbody tr) {
992
+ transition: background-color 0.15s ease;
979
993
  }
980
994
 
981
- .table-card :deep(th),
982
- .table-card :deep(td) {
995
+ .reader-table :deep(tbody tr:hover) {
996
+ background: #fbfcfe;
997
+ }
998
+
999
+ .reader-table :deep(th),
1000
+ .reader-table :deep(td) {
1001
+ padding: 0 14px !important;
983
1002
  white-space: nowrap;
984
1003
  }
985
1004
 
986
- .table-card :deep(th:nth-child(1)),
987
- .table-card :deep(td:nth-child(1)) {
1005
+ .name-column,
1006
+ .name-cell {
1007
+ width: 19%;
1008
+ }
1009
+
1010
+ .location-column,
1011
+ .location-cell {
988
1012
  width: 14%;
989
1013
  }
990
1014
 
991
- .table-card :deep(th:nth-child(2)),
992
- .table-card :deep(td:nth-child(2)) {
1015
+ .device-column,
1016
+ .device-cell {
1017
+ width: 15%;
1018
+ }
1019
+
1020
+ .status-column,
1021
+ .status-cell {
993
1022
  width: 12%;
994
1023
  }
995
1024
 
996
- .table-card :deep(th:nth-child(3)),
997
- .table-card :deep(td:nth-child(3)) {
998
- width: 18%;
1025
+ .sync-at-column,
1026
+ .sync-at-cell {
1027
+ width: 15%;
999
1028
  }
1000
1029
 
1001
- .table-card :deep(th:nth-child(4)),
1002
- .table-card :deep(td:nth-child(4)) {
1003
- width: 12%;
1030
+ .message-column,
1031
+ .message-cell {
1032
+ width: 20%;
1033
+ min-width: 0;
1004
1034
  }
1005
1035
 
1006
- .table-card :deep(th:nth-child(5)),
1007
- .table-card :deep(td:nth-child(5)) {
1008
- width: 10%;
1036
+ .action-column,
1037
+ .action-cell {
1038
+ width: 5%;
1039
+ text-align: right;
1009
1040
  }
1010
1041
 
1011
- .table-card :deep(th:nth-child(6)),
1012
- .table-card :deep(td:nth-child(6)) {
1013
- width: 14%;
1042
+ .cell-strong {
1043
+ display: block;
1044
+ overflow: hidden;
1045
+ color: #111827;
1046
+ font-weight: 600;
1047
+ text-overflow: ellipsis;
1014
1048
  }
1015
1049
 
1016
- .table-card :deep(th:nth-child(7)),
1017
- .table-card :deep(td:nth-child(7)) {
1018
- width: 14%;
1050
+ .location-cell,
1051
+ .device-cell,
1052
+ .sync-at-cell {
1053
+ overflow: hidden;
1054
+ color: #475467;
1055
+ text-overflow: ellipsis;
1056
+ }
1057
+
1058
+ .message-text {
1059
+ display: block;
1060
+ max-width: 100%;
1019
1061
  overflow: hidden;
1062
+ color: #475467;
1063
+ line-height: 1.35;
1020
1064
  text-overflow: ellipsis;
1021
1065
  }
1022
1066
 
1023
- .table-card :deep(th:nth-child(8)),
1024
- .table-card :deep(td:nth-child(8)) {
1025
- width: 6%;
1067
+ .action-menu-button {
1068
+ color: #344054;
1026
1069
  }
1027
1070
 
1028
1071
  .table-refresh {
1029
1072
  display: flex;
1030
- padding: 8px 10px;
1031
- border-bottom: 1px solid #eeeeee;
1073
+ justify-content: flex-start;
1074
+ padding: 10px 14px;
1075
+ border-bottom: 1px solid #e7ebef;
1076
+ background: #ffffff;
1032
1077
  }
1033
1078
 
1034
1079
  .empty-state {
@@ -1116,18 +1161,6 @@ function formatReaderStatus(value?: string) {
1116
1161
  line-height: 1.45;
1117
1162
  }
1118
1163
 
1119
- .reader-action-meta {
1120
- display: inline-flex;
1121
- gap: 8px;
1122
- align-items: center;
1123
- color: #344054;
1124
- font-size: 13px;
1125
- }
1126
-
1127
- .reader-action-meta strong {
1128
- font-weight: 700;
1129
- }
1130
-
1131
1164
  .reader-sync-review {
1132
1165
  justify-items: stretch;
1133
1166
  text-align: left;