@7365admin1/layer-common 4.0.1 → 4.0.2

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.
@@ -89,8 +89,16 @@
89
89
 
90
90
  <!-- Staging's hover-the-full-name tooltip, kept. -->
91
91
  <template #[`item.name`]="{ item }">
92
- <span class="app-cell--strong">
93
- {{ item.name }}
92
+ <span class="hid-log-name app-cell--strong">
93
+ <v-avatar
94
+ v-if="item.faceImage"
95
+ size="34"
96
+ rounded="lg"
97
+ class="hid-face-avatar"
98
+ >
99
+ <v-img :src="item.faceImage" cover />
100
+ </v-avatar>
101
+ <span>{{ item.name }}</span>
94
102
  <v-tooltip
95
103
  v-if="item.name && item.name !== 'N/A'"
96
104
  activator="parent"
@@ -101,13 +109,8 @@
101
109
  </span>
102
110
  </template>
103
111
 
104
- <!-- The face thumbnail, unchanged. It prints whatever the row already
105
- carried; this screen does not read or export a template. -->
106
- <template #[`item.facialData`]="{ item }">
107
- <v-avatar v-if="item.faceImage" size="34" rounded="lg" class="hid-face-avatar">
108
- <v-img :src="item.faceImage" cover />
109
- </v-avatar>
110
- <span v-else class="app-cell--muted">{{ item.facialData }}</span>
112
+ <template #[`item.hidUserId`]="{ item }">
113
+ <span class="app-cell--num">{{ item.hidUserId }}</span>
111
114
  </template>
112
115
 
113
116
  <template #[`item.method`]="{ item }">
@@ -135,6 +138,10 @@
135
138
  </StatusChip>
136
139
  </template>
137
140
 
141
+ <template #[`item.mappingStatus`]="{ item }">
142
+ <StatusChip :status="item.mappingStatus" />
143
+ </template>
144
+
138
145
  <template #[`item.action-table`]="{ item }">
139
146
  <AppButton
140
147
  variant="row"
@@ -300,7 +307,8 @@ const props = defineProps({
300
307
  },
301
308
  });
302
309
 
303
- type AccessTab = "resident" | "visitor" | "administrator";
310
+ type AccessTab = "all" | "unmapped" | "mapped" | "visitor" | "administrator";
311
+ type IdentityCategory = "resident" | "visitor" | "administrator";
304
312
  type AuthorizationStatus = "Authorized" | "Not Authorized" | "Unknown";
305
313
  type AccessRow = {
306
314
  key: string;
@@ -308,6 +316,7 @@ type AccessRow = {
308
316
  identityType?: string;
309
317
  visitorId?: string;
310
318
  name: string;
319
+ hidUserId: string;
311
320
  facialData: string;
312
321
  faceImage?: string;
313
322
  unit: string;
@@ -316,6 +325,7 @@ type AccessRow = {
316
325
  lastAccessDate?: Date;
317
326
  readerLocation: string;
318
327
  status: AuthorizationStatus;
328
+ mappingStatus: "Mapped" | "Unmapped";
319
329
  authorizationReason?: string;
320
330
  };
321
331
 
@@ -381,7 +391,7 @@ const liveAccessLogs = ref<HidAccessRecord[]>([]);
381
391
  const administratorUserIds = ref<Set<number>>(new Set());
382
392
  const userImages = ref<Record<string, string>>({});
383
393
  const selectedReaderId = ref("");
384
- const activeTab = ref<AccessTab>("resident");
394
+ const activeTab = ref<AccessTab>("all");
385
395
  const search = ref("");
386
396
  const status = ref("");
387
397
  const accessMethod = ref("");
@@ -463,6 +473,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
463
473
  key: `${selectedHistoryRow.value?.key || "history"}-${log._id || log.id || index}`,
464
474
  identityKey: selectedKey,
465
475
  name: selectedHistoryRow.value?.name || "N/A",
476
+ hidUserId: selectedHistoryRow.value?.hidUserId || "N/A",
466
477
  facialData: selectedHistoryRow.value?.facialData || "N/A",
467
478
  faceImage: selectedHistoryRow.value?.faceImage,
468
479
  unit: selectedHistoryRow.value?.unit || "N/A",
@@ -470,6 +481,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
470
481
  lastAccessTime: formatDate(getLogAccessDateValue(log)),
471
482
  lastAccessDate: parseDate(getLogAccessDateValue(log)),
472
483
  readerLocation: getLogReaderLocation(log),
484
+ mappingStatus: selectedHistoryRow.value?.mappingStatus || "Unmapped",
473
485
  ...getAuthorizationStatus(log.payload || log, log),
474
486
  }));
475
487
 
@@ -477,6 +489,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
477
489
  key: `${selectedHistoryRow.value?.key || "history"}-live-${log.id || log.time || index}`,
478
490
  identityKey: selectedKey,
479
491
  name: selectedHistoryRow.value?.name || "N/A",
492
+ hidUserId: selectedHistoryRow.value?.hidUserId || "N/A",
480
493
  facialData: selectedHistoryRow.value?.facialData || "N/A",
481
494
  faceImage: selectedHistoryRow.value?.faceImage,
482
495
  unit: selectedHistoryRow.value?.unit || "N/A",
@@ -484,6 +497,7 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
484
497
  lastAccessTime: formatDate(getAccessLogTime(log, {})),
485
498
  lastAccessDate: parseDate(getAccessLogTime(log, {})),
486
499
  readerLocation: selectedReader.value?.location || "N/A",
500
+ mappingStatus: selectedHistoryRow.value?.mappingStatus || "Unmapped",
487
501
  ...getAuthorizationStatus(log, {}),
488
502
  }));
489
503
 
@@ -499,11 +513,13 @@ const filteredRows = computed(() => {
499
513
  return rows.value.filter((row) => {
500
514
  const matchesSearch = !query || [
501
515
  row.name,
516
+ row.hidUserId,
502
517
  row.facialData,
503
518
  row.unit,
504
519
  row.method,
505
520
  row.lastAccessTime,
506
521
  row.status,
522
+ row.mappingStatus,
507
523
  ].some((value) => String(value ?? "").toLowerCase().includes(query));
508
524
 
509
525
  const matchesStatus = !status.value || row.status === status.value;
@@ -514,11 +530,13 @@ const filteredRows = computed(() => {
514
530
  });
515
531
 
516
532
  /**
517
- * The three tabs, as data rather than three `v-tab` elements, so the shared
518
- * `app-tab` strip can render them. Same values, same order, same handler.
533
+ * The source of truth is the Amico access log, so All is the initial view.
534
+ * The other tabs are audit filters; unmapped events never fall into Resident.
519
535
  */
520
536
  const tabOptions: Array<{ label: string; value: AccessTab }> = [
521
- { label: "Resident", value: "resident" },
537
+ { label: "All", value: "all" },
538
+ { label: "Unmapped", value: "unmapped" },
539
+ { label: "Mapped", value: "mapped" },
522
540
  { label: "Visitor", value: "visitor" },
523
541
  { label: "Administrator", value: "administrator" },
524
542
  ];
@@ -530,13 +548,12 @@ const tabOptions: Array<{ label: string; value: AccessTab }> = [
530
548
  * percentages had no design number behind them.
531
549
  */
532
550
  const logHeaders = [
533
- { title: "Name", value: "name", sortable: false },
534
- { title: "Facial Data", value: "facialData", sortable: false },
535
- // Staging (b0667a7) dropped Unit from this table - the unit still shows in
536
- // the per-identity history dialog, so the column is not carried back.
537
- { title: "Last Access Method", value: "method", sortable: false },
538
- { title: "Last access time", value: "lastAccessTime", sortable: false },
551
+ { title: "Amico Name", value: "name", sortable: false },
552
+ { title: "UID", value: "hidUserId", sortable: false },
553
+ { title: "Access Method", value: "method", sortable: false },
554
+ { title: "Access Date and Time", value: "lastAccessTime", sortable: false },
539
555
  { title: "Status", value: "status", sortable: false },
556
+ { title: "iService Mapping", value: "mappingStatus", sortable: false },
540
557
  { title: "", value: "action-table", sortable: false },
541
558
  ];
542
559
 
@@ -664,7 +681,11 @@ async function loadDashboard() {
664
681
  )
665
682
  : Promise.resolve(null),
666
683
  loadDashboardSource(
667
- getReaderUsers(selectedReaderId.value, { page: page.value, limit: 100 }),
684
+ getReaderUsers(selectedReaderId.value, {
685
+ page: 1,
686
+ limit: 100,
687
+ includeVisitors: true,
688
+ }),
668
689
  { items: [] },
669
690
  "reader users",
670
691
  ),
@@ -693,9 +714,7 @@ async function loadDashboard() {
693
714
  .filter((userId: number | undefined): userId is number => Boolean(userId)),
694
715
  );
695
716
  allIdentities.value = mergedIdentities;
696
- identities.value = mergedIdentities.filter(
697
- (identity: Record<string, unknown>) => classifyIdentity(identity) === activeTab.value,
698
- );
717
+ identities.value = mergedIdentities.filter(isMappedIdentity);
699
718
  logs.value = liveItems.length
700
719
  ? []
701
720
  : logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
@@ -704,7 +723,14 @@ async function loadDashboard() {
704
723
  serverPageRange.value = String(
705
724
  hidAccessLogResponse?.pageRange ?? hidAccessLogResponse?.data?.pageRange ?? "",
706
725
  );
707
- await loadUserImages(identities.value);
726
+ const accessLogUserIds = getAccessLogUserIds(liveItems);
727
+ const accessLogImageCandidates = [...accessLogUserIds].map((hidUserId) => ({
728
+ hidUserId: String(hidUserId),
729
+ }));
730
+ await loadUserImages(
731
+ [...mergedIdentities, ...accessLogImageCandidates],
732
+ accessLogUserIds,
733
+ );
708
734
  } finally {
709
735
  if (sequence === loadSequence.value) {
710
736
  loading.value = false;
@@ -721,13 +747,18 @@ async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: s
721
747
  }
722
748
  }
723
749
 
724
- async function loadUserImages(items: HidAccessRecord[]) {
750
+ async function loadUserImages(items: HidAccessRecord[], forceUserIds = new Set<number>()) {
725
751
  if (!selectedReaderId.value) return;
726
752
 
727
753
  await Promise.all(items.map(async (identity) => {
728
754
  const hidUserId = toHidNumericId(identity.hidUserId);
729
755
  const key = String(hidUserId || "");
730
- if (!hidUserId || userImages.value[key] || identity?.metadata?.photo || !hasHidImageHint(identity)) return;
756
+ if (
757
+ !hidUserId ||
758
+ key in userImages.value ||
759
+ identity?.metadata?.photo ||
760
+ (!forceUserIds.has(hidUserId) && !hasHidImageHint(identity))
761
+ ) return;
731
762
 
732
763
  try {
733
764
  const response = await getUserImage(selectedReaderId.value, hidUserId);
@@ -741,6 +772,14 @@ async function loadUserImages(items: HidAccessRecord[]) {
741
772
  }));
742
773
  }
743
774
 
775
+ function getAccessLogUserIds(items: HidAccessRecord[]) {
776
+ return new Set(
777
+ items
778
+ .map((item) => toHidNumericId(item.user_id ?? item.userId ?? item.hidUserId))
779
+ .filter((userId): userId is number => userId !== undefined),
780
+ );
781
+ }
782
+
744
783
  function hasHidImageHint(identity: HidAccessRecord) {
745
784
  return Boolean(
746
785
  identity?.metadata?.imageTimestamp ||
@@ -809,9 +848,16 @@ function resetDashboardData() {
809
848
 
810
849
  function isVisibleForActiveTab(row: AccessRow) {
811
850
  const identity = findIdentityByKey(row.identityKey);
851
+ if (activeTab.value === "all") return true;
852
+ if (activeTab.value === "unmapped") return !isMappedIdentity(identity);
853
+ if (activeTab.value === "mapped") return isMappedIdentity(identity);
812
854
  return classifyIdentity(identity, row) === activeTab.value;
813
855
  }
814
856
 
857
+ function isMappedIdentity(identity: HidAccessRecord | undefined) {
858
+ return Boolean(identity?.person || identity?.member || identity?.serviceProvider || identity?.visitor);
859
+ }
860
+
815
861
  function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
816
862
  return Boolean(identity && (
817
863
  identity.type === "visitor"
@@ -820,7 +866,7 @@ function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
820
866
  ));
821
867
  }
822
868
 
823
- function classifyIdentity(identity?: Record<string, unknown>, row?: AccessRow): AccessTab {
869
+ function classifyIdentity(identity?: Record<string, unknown>, row?: AccessRow): IdentityCategory {
824
870
  const hidUserId = toHidNumericId(identity?.hidUserId ?? row?.identityKey.split("|")[0]);
825
871
  if (hidUserId && administratorUserIds.value.has(hidUserId)) return "administrator";
826
872
  if (row?.identityType === "visitor" || row?.visitorId || isVisitorIdentity(identity)) return "visitor";
@@ -1128,6 +1174,7 @@ function getRawAccessLogs(event: HidAccessRecord): HidAccessRecord[] {
1128
1174
 
1129
1175
  function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, index: string): AccessRow {
1130
1176
  const identity = findIdentityForAccessLog(rawLog, event);
1177
+ const mapped = isMappedIdentity(identity);
1131
1178
  const eventIdentity = event.payload?.identity || event.payload?.identityLookup || {};
1132
1179
  const rawAccessTime = getAccessLogTime(rawLog, event);
1133
1180
  const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
@@ -1139,7 +1186,8 @@ function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, inde
1139
1186
  ? classifyIdentity(identity)
1140
1187
  : String(eventIdentity.type || "").toLowerCase() || undefined,
1141
1188
  visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
1142
- name: identity ? getName(identity) : getRawLogName(rawLog, event),
1189
+ name: getRawLogName(rawLog, event, identity),
1190
+ hidUserId: formatHidUid(rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? identity?.hidUserId),
1143
1191
  facialData: identity ? getFacialData(identity) : "N/A",
1144
1192
  faceImage: identity ? getUserImageSrc(identity) : "",
1145
1193
  unit: identity ? getUnit(identity) : "N/A",
@@ -1147,6 +1195,7 @@ function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, inde
1147
1195
  lastAccessTime: formatDate(rawAccessTime),
1148
1196
  lastAccessDate: parseDate(rawAccessTime),
1149
1197
  readerLocation: getLogReaderLocation(event),
1198
+ mappingStatus: mapped ? "Mapped" : "Unmapped",
1150
1199
  ...getAuthorizationStatus(rawLog, event),
1151
1200
  };
1152
1201
  }
@@ -1190,9 +1239,7 @@ function getAuthorizationStatus(
1190
1239
  }
1191
1240
 
1192
1241
  function findIdentityForAccessLog(rawLog: HidAccessRecord, event: HidAccessRecord) {
1193
- if (rawLog.identity && typeof rawLog.identity === "object") {
1194
- return rawLog.identity;
1195
- }
1242
+ if (isUnknownRecord(rawLog.identity)) return rawLog.identity;
1196
1243
  const eventIdentityId = event.payload?.identity?._id;
1197
1244
  if (eventIdentityId) {
1198
1245
  const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
@@ -1283,8 +1330,35 @@ function getAccessLogTime(rawLog: HidAccessRecord, event: HidAccessRecord) {
1283
1330
  return rawLog.time || rawLog.createdAt || rawLog.accessedAt || event.createdAt;
1284
1331
  }
1285
1332
 
1286
- function getRawLogName(rawLog: HidAccessRecord, event: HidAccessRecord) {
1287
- return rawLog.name || event.payload?.identity?.name || event.payload?.name || "Unmapped";
1333
+ function getRawLogName(
1334
+ rawLog: HidAccessRecord,
1335
+ event: HidAccessRecord,
1336
+ identity?: HidAccessRecord,
1337
+ ) {
1338
+ return getText(rawLog.user_name)
1339
+ || getText(rawLog.userName)
1340
+ || getText(rawLog.name)
1341
+ || getText(event.payload?.rawAccessLog?.user_name)
1342
+ || getText(event.payload?.rawAccessLog?.userName)
1343
+ || getText(event.payload?.rawAccessLog?.name)
1344
+ || getText(identity?.metadata?.name)
1345
+ || getText(identity?.name)
1346
+ || "Unknown";
1347
+ }
1348
+
1349
+ function formatHidUid(value: unknown) {
1350
+ const text = getText(value);
1351
+ if (!text) return "N/A";
1352
+ const numeric = Number(text);
1353
+ return Number.isSafeInteger(numeric) && numeric > 0
1354
+ ? `UID${String(numeric).padStart(6, "0")}`
1355
+ : text;
1356
+ }
1357
+
1358
+ function getText(value: unknown) {
1359
+ if (typeof value === "string") return value.trim();
1360
+ if (typeof value === "number") return String(value);
1361
+ return "";
1288
1362
  }
1289
1363
 
1290
1364
  function exportRows() {
@@ -1765,6 +1839,12 @@ function escapeHtml(value: unknown) {
1765
1839
  background: var(--hover);
1766
1840
  }
1767
1841
 
1842
+ .hid-log-name {
1843
+ align-items: center;
1844
+ display: inline-flex;
1845
+ gap: 8px;
1846
+ }
1847
+
1768
1848
  @media (max-width: 960px) {
1769
1849
  .history-toolbar {
1770
1850
  align-items: stretch;
@@ -172,7 +172,7 @@
172
172
  <strong>{{ syncPreviewStats.updatedInformation }}</strong>
173
173
  </div>
174
174
  <div>
175
- <span>Facial Data</span>
175
+ <span>Existing Facial Data</span>
176
176
  <strong>{{ syncPreviewStats.facialData }}</strong>
177
177
  </div>
178
178
  </div>
@@ -229,7 +229,7 @@
229
229
  <strong>{{ resultStats.updatedInformation }}</strong>
230
230
  </div>
231
231
  <div>
232
- <span>New Photos</span>
232
+ <span>Synced Facial Data</span>
233
233
  <strong>{{ resultStats.facialData }}</strong>
234
234
  </div>
235
235
  </div>
@@ -331,6 +331,7 @@ const {
331
331
  updateReader,
332
332
  deleteReader,
333
333
  testReader,
334
+ syncReaderFacialData,
334
335
  configureReaderIntegration,
335
336
  setReaderMonitor,
336
337
  setReaderOperatingMode,
@@ -596,13 +597,17 @@ async function runConfirmedAction() {
596
597
  const unmappedCount = Number(payload.unmappedCount || 0);
597
598
 
598
599
  if (!payload.objects.length) {
600
+ const facialSync = await syncReaderFacialData(selectedReader.value._id);
601
+ const facialStats = toFacialSyncStats(facialSync);
599
602
  if (unmappedCount) {
600
- const message = syncMessage.value
601
- || `Found ${unmappedCount} HID user(s) that must be linked from the HID Users screen.`;
603
+ const message = [
604
+ syncMessage.value || `Found ${unmappedCount} HID user(s) that must be linked from the HID User Mapping screen.`,
605
+ facialStats.message,
606
+ ].filter(Boolean).join(" ");
602
607
  await applyReaderSyncState(selectedReader.value._id, "completed", message);
603
608
  showResult(true, "Sync Review Required", message, {
604
609
  readerName: selectedReader.value.name || "HID Reader",
605
- stats: syncPreviewStats.value,
610
+ stats: { ...syncPreviewStats.value, facialData: facialStats.syncedCount },
606
611
  });
607
612
  confirmDialog.value = false;
608
613
  resetActionFlow();
@@ -619,14 +624,18 @@ async function runConfirmedAction() {
619
624
  return;
620
625
  }
621
626
  const result = await syncHidUsers(selectedReader.value._id, payload.users);
627
+ const facialSync = await syncReaderFacialData(selectedReader.value._id);
628
+ const facialStats = toFacialSyncStats(facialSync);
622
629
  const mappingMessage = unmappedCount
623
- ? `${unmappedCount} HID user(s) still need to be linked from the HID Users screen.`
630
+ ? `${unmappedCount} HID user(s) still need to be linked from the HID User Mapping screen.`
624
631
  : "";
625
- const message = syncMessage.value || [result.message, mappingMessage].filter(Boolean).join(" ");
632
+ const message = [syncMessage.value, result.message, facialStats.message, mappingMessage]
633
+ .filter(Boolean)
634
+ .join(" ");
626
635
  await applyReaderSyncState(selectedReader.value._id, "completed", message);
627
636
  showResult(true, "Sync Successful", message, {
628
637
  readerName: selectedReader.value.name || "HID Reader",
629
- stats: syncPreviewStats.value,
638
+ stats: { ...syncPreviewStats.value, facialData: facialStats.syncedCount },
630
639
  });
631
640
  }
632
641
  if (pendingAction.value === "delete") {
@@ -714,7 +723,7 @@ async function buildSyncPayload(readerId: string) {
714
723
  facialData: countFacialData(hidUsers),
715
724
  },
716
725
  defaultMessage: hidUsers.length
717
- ? `Found ${hidUsers.length} HID user(s) that must be linked from the HID Users screen.`
726
+ ? `Found ${hidUsers.length} HID user(s) that must be linked from the HID User Mapping screen.`
718
727
  : "",
719
728
  };
720
729
  }
@@ -760,7 +769,7 @@ async function buildSyncPayload(readerId: string) {
760
769
  defaultMessage: [
761
770
  totalUsersToSync ? `Ready to sync ${totalUsersToSync} linked HID user(s).` : "",
762
771
  hidUsersToImport.length
763
- ? `${hidUsersToImport.length} HID user(s) must be linked from the HID Users screen.`
772
+ ? `${hidUsersToImport.length} HID user(s) must be linked from the HID User Mapping screen.`
764
773
  : "",
765
774
  ].filter(Boolean).join(" "),
766
775
  };
@@ -832,7 +841,11 @@ function normalizeHidUsers(response: HidRecord) {
832
841
  [];
833
842
 
834
843
  return Array.isArray(users)
835
- ? users.map((user) => ({
844
+ ? users
845
+ // Visitor QR passes are linked by their visitor transaction and must not
846
+ // be offered for manual HID user mapping or counted as unmapped users.
847
+ .filter((user) => Number(user?.user_type_id) !== 1)
848
+ .map((user) => ({
836
849
  hidUserId: user.id,
837
850
  registration: user.registration,
838
851
  cardNo: user.card_value || user.cardNo,
@@ -970,6 +983,16 @@ function countFacialData(users: HidRecord[]) {
970
983
  return users.filter((user) => user.face || user.photo || user.image || user.imageUrl || user.faceImage).length;
971
984
  }
972
985
 
986
+ function toFacialSyncStats(response: unknown) {
987
+ const root = asHidRecord(response);
988
+ const data = asHidRecord(root.data ?? root);
989
+ const syncedCount = Number(data.syncedCount);
990
+ return {
991
+ syncedCount: Number.isSafeInteger(syncedCount) && syncedCount >= 0 ? syncedCount : 0,
992
+ message: String(data.message || "").trim(),
993
+ };
994
+ }
995
+
973
996
  function getFriendlyHidError(error: unknown) {
974
997
  const message = getHidErrorMessage(error);
975
998
  const lowerMessage = message.toLowerCase();