@7365admin1/layer-common 4.1.3-staging.252 → 4.1.3-staging.253

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,15 +89,9 @@
89
89
 
90
90
  <!-- Staging's hover-the-full-name tooltip, kept. -->
91
91
  <template #[`item.name`]="{ item }">
92
+ <!-- Client requirement (2026-08-26): no face photo on an access log row.
93
+ The reader keeps the image; we neither fetch nor hold one. -->
92
94
  <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
95
  <span>{{ item.name }}</span>
102
96
  <v-tooltip
103
97
  v-if="item.name && item.name !== 'N/A'"
@@ -314,7 +308,6 @@ type AccessRow = {
314
308
  name: string;
315
309
  hidUserId: string;
316
310
  facialData: string;
317
- faceImage?: string;
318
311
  unit: string;
319
312
  method: string;
320
313
  lastAccessTime: string;
@@ -371,7 +364,6 @@ type JsPdf = {
371
364
  const {
372
365
  getReaders,
373
366
  getLogs,
374
- getUserImage,
375
367
  getReaderUsers,
376
368
  getReaderAccessLogs,
377
369
  } = useHidAmico();
@@ -384,7 +376,6 @@ const visitorRecords = ref<Record<string, HidAccessRecord>>({});
384
376
  const logs = ref<HidAccessRecord[]>([]);
385
377
  const liveAccessLogs = ref<HidAccessRecord[]>([]);
386
378
  const administratorUserIds = ref<Set<number>>(new Set());
387
- const userImages = ref<Record<string, string>>({});
388
379
  const selectedReaderId = ref("");
389
380
  const activeTab = ref<AccessTab>("all");
390
381
  const search = ref("");
@@ -470,7 +461,6 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
470
461
  name: selectedHistoryRow.value?.name || "N/A",
471
462
  hidUserId: selectedHistoryRow.value?.hidUserId || "N/A",
472
463
  facialData: selectedHistoryRow.value?.facialData || "N/A",
473
- faceImage: selectedHistoryRow.value?.faceImage,
474
464
  unit: selectedHistoryRow.value?.unit || "N/A",
475
465
  method: getAccessMethod({}, log),
476
466
  lastAccessTime: formatDate(getLogAccessDateValue(log)),
@@ -486,7 +476,6 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
486
476
  name: selectedHistoryRow.value?.name || "N/A",
487
477
  hidUserId: selectedHistoryRow.value?.hidUserId || "N/A",
488
478
  facialData: selectedHistoryRow.value?.facialData || "N/A",
489
- faceImage: selectedHistoryRow.value?.faceImage,
490
479
  unit: selectedHistoryRow.value?.unit || "N/A",
491
480
  method: getAccessMethod({}, { payload: { rawAccessLog: log } }),
492
481
  lastAccessTime: formatDate(getAccessLogTime(log, {})),
@@ -700,14 +689,6 @@ async function loadDashboard() {
700
689
  serverPageRange.value = String(
701
690
  hidAccessLogResponse?.pageRange ?? hidAccessLogResponse?.data?.pageRange ?? "",
702
691
  );
703
- const accessLogUserIds = getAccessLogUserIds(liveItems);
704
- const accessLogImageCandidates = [...accessLogUserIds].map((hidUserId) => ({
705
- hidUserId: String(hidUserId),
706
- }));
707
- await loadUserImages(
708
- [...readerUsers, ...accessLogImageCandidates],
709
- accessLogUserIds,
710
- );
711
692
  } finally {
712
693
  if (sequence === loadSequence.value) {
713
694
  loading.value = false;
@@ -724,47 +705,8 @@ async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: s
724
705
  }
725
706
  }
726
707
 
727
- async function loadUserImages(items: HidAccessRecord[], forceUserIds = new Set<number>()) {
728
- if (!selectedReaderId.value) return;
729
-
730
- await Promise.all(items.map(async (identity) => {
731
- const hidUserId = toHidNumericId(identity.hidUserId);
732
- const key = String(hidUserId || "");
733
- if (
734
- !hidUserId ||
735
- key in userImages.value ||
736
- (!forceUserIds.has(hidUserId) && !hasHidImageHint(identity))
737
- ) return;
738
-
739
- try {
740
- const response = await getUserImage(selectedReaderId.value, hidUserId);
741
- const image = response?.data || response;
742
- userImages.value[key] = image?.base64
743
- ? `data:${image.contentType || "image/jpeg"};base64,${image.base64}`
744
- : "";
745
- } catch {
746
- userImages.value[key] = "";
747
- }
748
- }));
749
- }
750
708
 
751
- function getAccessLogUserIds(items: HidAccessRecord[]) {
752
- return new Set(
753
- items
754
- .map((item) => toHidNumericId(item.user_id ?? item.userId ?? item.hidUserId))
755
- .filter((userId): userId is number => userId !== undefined),
756
- );
757
- }
758
709
 
759
- function hasHidImageHint(identity: HidAccessRecord) {
760
- return Boolean(
761
- identity?.metadata?.imageTimestamp ||
762
- identity?.metadata?.facialData ||
763
- identity?.imageTimestamp ||
764
- identity?.image_timestamp ||
765
- identity?.facialData,
766
- );
767
- }
768
710
 
769
711
  async function onTabChange(tab: AccessTab) {
770
712
  if (tab) {
@@ -783,7 +725,6 @@ function resetDashboardData() {
783
725
  visitorRecords.value = {};
784
726
  logs.value = [];
785
727
  liveAccessLogs.value = [];
786
- userImages.value = {};
787
728
  administratorUserIds.value = new Set();
788
729
  serverPages.value = 1;
789
730
  serverPageRange.value = "";
@@ -1128,7 +1069,6 @@ function mapAccessLogToRow(rawLog: HidAccessRecord, event: HidAccessRecord, inde
1128
1069
  name: getRawLogName(rawLog, event, identity),
1129
1070
  hidUserId: formatHidUid(rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? identity?.hidUserId),
1130
1071
  facialData: identity ? getFacialData(identity) : "N/A",
1131
- faceImage: logHidUserId ? userImages.value[String(logHidUserId)] || "" : "",
1132
1072
  unit: identity ? getUnit(identity) : "N/A",
1133
1073
  method: getAccessMethod(identity || {}, { ...event, payload: { ...event.payload, rawAccessLog: rawLog } }),
1134
1074
  lastAccessTime: formatDate(rawAccessTime),
@@ -1773,10 +1713,6 @@ function escapeHtml(value: unknown) {
1773
1713
  border-radius: 0;
1774
1714
  }
1775
1715
 
1776
- .hid-face-avatar {
1777
- border: 1px solid var(--border);
1778
- background: var(--hover);
1779
- }
1780
1716
 
1781
1717
  .hid-log-name {
1782
1718
  align-items: center;
@@ -56,16 +56,10 @@
56
56
  <span class="app-cell--num">{{ item.registration || "N/A" }}</span>
57
57
  </template>
58
58
 
59
+ <!-- Client requirement (2026-08-26): the face photo is not shown once
60
+ registration is complete. Enrollment STATE only. -->
59
61
  <template #[`item.facialData`]="{ item }">
60
- <v-avatar
61
- v-if="getUserImageSrc(item)"
62
- size="34"
63
- rounded="lg"
64
- class="hid-face-avatar"
65
- >
66
- <v-img :src="getUserImageSrc(item)" cover />
67
- </v-avatar>
68
- <span v-else :class="hasFacialData(item) ? 'facial-data--available' : 'app-cell--muted'">
62
+ <span :class="hasFacialData(item) ? 'facial-data--available' : 'app-cell--muted'">
69
63
  <v-icon
70
64
  :icon="hasFacialData(item) ? 'mdi-face-recognition' : 'mdi-face-man-shimmer-outline'"
71
65
  size="19"
@@ -208,14 +202,12 @@ type HidReaderUser = Record<string, unknown> & {
208
202
  const {
209
203
  getReaders,
210
204
  getReaderUsers,
211
- getUserImage,
212
205
  getUserPinStatus,
213
206
  setUserPin,
214
207
  deleteUserPin,
215
208
  } = useHidAmico();
216
209
  const readers = ref<HidReader[]>([]);
217
210
  const users = ref<HidReaderUser[]>([]);
218
- const userImages = ref<Record<string, string>>({});
219
211
  const selectedReaderId = ref("");
220
212
  const search = ref("");
221
213
  const userType = ref<"all" | "user" | "visitor">("all");
@@ -328,7 +320,6 @@ async function loadUsers() {
328
320
  ? matchingItems.slice(pageStart, pageStart + 10)
329
321
  : matchingItems;
330
322
  users.value = pageItems.map(toReaderUser);
331
- await loadUserImages(users.value);
332
323
  total.value = clientFiltersUserType
333
324
  ? matchingItems.length
334
325
  : toNumber(response.total ?? data.total, users.value.length);
@@ -356,7 +347,6 @@ function goToPage(nextPage: number) {
356
347
 
357
348
  function reloadFromFirstPage() {
358
349
  page.value = 1;
359
- userImages.value = {};
360
350
  loadUsers();
361
351
  }
362
352
 
@@ -364,31 +354,6 @@ function hasFacialData(user: HidReaderUser) {
364
354
  return user.metadata?.facialEnrolled === true || Boolean(toText(user.metadata?.facialData));
365
355
  }
366
356
 
367
- async function loadUserImages(items: HidReaderUser[]) {
368
- if (!selectedReaderId.value) return;
369
-
370
- await Promise.all(items.map(async (user) => {
371
- const hidUserId = toHidNumericId(user.hidUserId);
372
- const key = String(hidUserId || "");
373
- if (!hidUserId || key in userImages.value) return;
374
-
375
- try {
376
- const response = toRecord(await getUserImage(selectedReaderId.value, hidUserId));
377
- const image = toRecord(response.data ?? response);
378
- const base64 = toText(image.base64);
379
- userImages.value[key] = base64
380
- ? `data:${toText(image.contentType) || "image/jpeg"};base64,${base64}`
381
- : "";
382
- } catch {
383
- userImages.value[key] = "";
384
- }
385
- }));
386
- }
387
-
388
- function getUserImageSrc(user: HidReaderUser) {
389
- const hidUserId = toHidNumericId(user.hidUserId);
390
- return (hidUserId ? userImages.value[String(hidUserId)] : "") || "";
391
- }
392
357
 
393
358
  async function openPinDialog(user: HidReaderUser) {
394
359
  const hidUserId = toHidNumericId(user.hidUserId);
@@ -583,9 +548,6 @@ function toNumber(value: unknown, fallback: number) {
583
548
  color: var(--warning, #9a6b00);
584
549
  }
585
550
 
586
- .hid-face-avatar {
587
- border: 1px solid var(--border-color, #e0e3e8);
588
- }
589
551
 
590
552
  .facial-data--available {
591
553
  align-items: center;
@@ -85,18 +85,11 @@
85
85
  <span class="app-cell--strong">{{ getName(item) }}</span>
86
86
  </template>
87
87
 
88
- <!-- The face thumbnail, unchanged. It prints whatever `getUserImageSrc`
89
- already returned; this screen does not read or export a template. -->
88
+ <!-- Client requirement (2026-08-26): the face photo is not shown once
89
+ registration is complete. The column reports enrollment STATE only --
90
+ the reader holds the image and the biometric template, we hold neither. -->
90
91
  <template #[`item.facialData`]="{ item }">
91
- <v-avatar
92
- v-if="getUserImageSrc(item)"
93
- size="34"
94
- rounded="lg"
95
- class="hid-face-avatar"
96
- >
97
- <v-img :src="getUserImageSrc(item)" cover />
98
- </v-avatar>
99
- <span v-else class="app-cell--muted">{{ getFacialData(item) }}</span>
92
+ <span class="app-cell--muted">{{ getFacialData(item) }}</span>
100
93
  </template>
101
94
 
102
95
  <template #[`item.hidUserId`]="{ item }">
@@ -192,6 +185,12 @@
192
185
  <span v-if="form.photoPreview" class="photo-edit-icon">
193
186
  <v-icon icon="mdi-pencil" size="16" />
194
187
  </span>
188
+ <template v-else-if="facialEnrolled">
189
+ <!-- Enrolled, but the photo is deliberately not shown or held.
190
+ Re-enrolling needs a fresh capture. -->
191
+ <v-icon icon="mdi-face-recognition" size="40" />
192
+ <span>Face Enrolled</span>
193
+ </template>
195
194
  <template v-else>
196
195
  <!-- Was `color="#a4acb5"`, the same grey on both themes.
197
196
  `.photo-button` already sets `--muted`, so it inherits. -->
@@ -207,7 +206,7 @@
207
206
  @click="triggerCameraInput"
208
207
  />
209
208
  <v-list-item
210
- v-if="form.photoPreview"
209
+ v-if="form.photoPreview || facialEnrolled"
211
210
  title="Remove Photo"
212
211
  class="text-error"
213
212
  @click="removePhoto"
@@ -538,7 +537,6 @@ const {
538
537
  getReaders,
539
538
  getIdentities,
540
539
  getReaderUsers,
541
- getUserImage,
542
540
  setUserImage,
543
541
  deleteUserImage,
544
542
  getUserCards,
@@ -617,7 +615,6 @@ type HidIdentityPayload = Record<string, unknown> & {
617
615
 
618
616
  const readers = ref<HidReader[]>([]);
619
617
  const users = ref<HidUser[]>([]);
620
- const userImages = ref<Record<string, string>>({});
621
618
  const administratorUserIds = ref<Set<number>>(new Set());
622
619
  const selectedReaderId = ref("");
623
620
  const search = ref("");
@@ -650,6 +647,20 @@ const photoInput = ref<HTMLInputElement | null>(null);
650
647
  const cameraDialog = ref(false);
651
648
  const selectedPhotoFile = ref<File | null>(null);
652
649
  const removePhotoRequested = ref(false);
650
+ /**
651
+ * Client requirement (2026-08-26): the photo is neither shown nor kept once registration
652
+ * completes, so the form tracks only WHETHER this person is enrolled. Re-enrolling needs a
653
+ * fresh capture -- there is no stored image to re-send.
654
+ */
655
+ const facialEnrolled = ref(false);
656
+
657
+ function hasFacialEnrollment(user?: HidUser | null) {
658
+ return Boolean(
659
+ user?.metadata?.facialEnrolled ||
660
+ user?.metadata?.facialData ||
661
+ user?.metadata?.imageTimestamp
662
+ );
663
+ }
653
664
  const removePinRequested = ref(false);
654
665
  const snackbar = reactive({
655
666
  show: false,
@@ -832,8 +843,6 @@ async function loadUsers() {
832
843
  serverPageRange.value = String(responseRecord.pageRange ?? responseData.pageRange ?? "");
833
844
  if (props.cardManagement) {
834
845
  await loadVisibleUserCards(users.value);
835
- } else {
836
- await loadUserImages(users.value);
837
846
  }
838
847
  } catch (error) {
839
848
  console.error("Unable to load HID reader users:", error);
@@ -850,27 +859,6 @@ async function loadUsers() {
850
859
  }
851
860
  }
852
861
 
853
- async function loadUserImages(items: HidUser[]) {
854
- if (!selectedReaderId.value) return;
855
-
856
- await Promise.all(
857
- items.map(async (user) => {
858
- const hidUserId = toHidNumericId(user.hidUserId);
859
- const key = String(hidUserId || "");
860
- if (!hidUserId || userImages.value[key] || user?.metadata?.photo) return;
861
-
862
- try {
863
- const response = await getUserImage(selectedReaderId.value, hidUserId);
864
- const image = response?.data || response;
865
- userImages.value[key] = image?.base64
866
- ? `data:${image.contentType || "image/jpeg"};base64,${image.base64}`
867
- : "";
868
- } catch {
869
- userImages.value[key] = "";
870
- }
871
- })
872
- );
873
- }
874
862
 
875
863
  function resetForm() {
876
864
  form.reader = selectedReaderId.value || readers.value[0]?._id || "";
@@ -888,6 +876,7 @@ function resetForm() {
888
876
  form.photoPreview = "";
889
877
  selectedPhotoFile.value = null;
890
878
  removePhotoRequested.value = false;
879
+ facialEnrolled.value = false;
891
880
  form.subjectCategory = "resident";
892
881
  form.subjectId = "";
893
882
  }
@@ -944,7 +933,8 @@ async function openEdit(user: HidUser) {
944
933
  form.accessPin = "";
945
934
  pinEnrolled.value = Boolean(user.metadata?.pinEnrolled);
946
935
  removePinRequested.value = false;
947
- form.photoPreview = getUserImageSrc(user);
936
+ form.photoPreview = "";
937
+ facialEnrolled.value = hasFacialEnrollment(user);
948
938
  form.subjectCategory = user.person
949
939
  ? "resident"
950
940
  : user.serviceProvider
@@ -966,12 +956,6 @@ async function openEdit(user: HidUser) {
966
956
  // Preserve the last known status if the reader is temporarily offline.
967
957
  }
968
958
 
969
- if (!form.photoPreview) {
970
- await loadUserImages([user]);
971
- if (selectedUser.value?._rowKey === user._rowKey) {
972
- form.photoPreview = getUserImageSrc(user);
973
- }
974
- }
975
959
  }
976
960
 
977
961
  function openView(user: HidUser) {
@@ -1017,6 +1001,7 @@ function removePhoto() {
1017
1001
  form.photoPreview = "";
1018
1002
  selectedPhotoFile.value = null;
1019
1003
  removePhotoRequested.value = true;
1004
+ facialEnrolled.value = false;
1020
1005
  }
1021
1006
 
1022
1007
  function onPhotoChange(event: Event) {
@@ -1094,7 +1079,12 @@ async function saveUser() {
1094
1079
  level: form.level,
1095
1080
  unit: form.unit,
1096
1081
  unitLabel: buildUnitLabel(),
1097
- facialData: form.photoPreview ? form.hidUserId : "",
1082
+ // Enrollment STATE carries forward on its own. It used to be derived from the
1083
+ // photo loaded into the form, which now stays empty -- editing a name would have
1084
+ // silently un-enrolled the person.
1085
+ facialEnrolled: hasFacialEnrollment(selectedUser.value),
1086
+ facialEnrolledAt: selectedUser.value?.metadata?.facialEnrolledAt || "",
1087
+ facialData: selectedUser.value?.metadata?.facialData || "",
1098
1088
  imageTimestamp: selectedUser.value?.metadata?.imageTimestamp || "",
1099
1089
  facialScores: selectedUser.value?.metadata?.facialScores || {},
1100
1090
  pinEnrolled: pinEnrolled.value,
@@ -1145,6 +1135,11 @@ async function saveUser() {
1145
1135
  showToast(getHidErrorMessage(error), "error");
1146
1136
  } finally {
1147
1137
  saving.value = false;
1138
+ // Discard the captured image on every path. On a part-way failure this is what stops
1139
+ // the photo lingering in the open form after a retry or a cancel.
1140
+ form.photoPreview = "";
1141
+ selectedPhotoFile.value = null;
1142
+ removePhotoRequested.value = false;
1148
1143
  }
1149
1144
  }
1150
1145
 
@@ -1185,13 +1180,11 @@ async function syncFacialImage(
1185
1180
  }
1186
1181
  );
1187
1182
  const result = response?.data;
1188
- userImages.value[String(hidUserId)] = form.photoPreview;
1189
1183
  return { action: "enrolled", timestamp, scores: result?.scores };
1190
1184
  }
1191
1185
 
1192
1186
  if (removePhotoRequested.value) {
1193
1187
  await deleteUserImage(readerId, hidUserId);
1194
- delete userImages.value[String(hidUserId)];
1195
1188
  return { action: "removed" };
1196
1189
  }
1197
1190
 
@@ -1203,12 +1196,15 @@ function applyFacialResult(
1203
1196
  result: FacialSyncResult
1204
1197
  ) {
1205
1198
  if (result.action === "enrolled") {
1199
+ metadata.facialEnrolled = true;
1200
+ metadata.facialEnrolledAt = new Date().toISOString();
1206
1201
  metadata.facialData = form.hidUserId;
1207
1202
  metadata.imageTimestamp = String(result.timestamp || "");
1208
1203
  metadata.facialScores = result.scores || {};
1209
1204
  } else if (result.action === "removed") {
1205
+ metadata.facialEnrolled = false;
1206
+ metadata.facialEnrolledAt = "";
1210
1207
  metadata.facialData = "";
1211
- metadata.photo = "";
1212
1208
  metadata.imageTimestamp = "";
1213
1209
  metadata.facialScores = {};
1214
1210
  }
@@ -1675,16 +1671,7 @@ function getFacialData(user: HidUser) {
1675
1671
  return (
1676
1672
  user?.metadata?.facialData ||
1677
1673
  user?.metadata?.imageTimestamp ||
1678
- (user?.metadata?.photo ? user.hidUserId : "N/A")
1679
- );
1680
- }
1681
-
1682
- function getUserImageSrc(user: HidUser) {
1683
- const hidUserId = toHidNumericId(user?.hidUserId);
1684
- return (
1685
- user?.metadata?.photo ||
1686
- (hidUserId ? userImages.value[String(hidUserId)] : "") ||
1687
- ""
1674
+ (user?.metadata?.facialEnrolled ? user.hidUserId : "N/A")
1688
1675
  );
1689
1676
  }
1690
1677
 
@@ -2016,10 +2003,6 @@ function toHidCard(value: unknown): HidCard {
2016
2003
  box-shadow: var(--shadow-menu);
2017
2004
  }
2018
2005
 
2019
- .hid-face-avatar {
2020
- border: 1px solid var(--border);
2021
- background: var(--hover);
2022
- }
2023
2006
 
2024
2007
  .admin-rule-indicator {
2025
2008
  display: inline-flex;
@@ -299,11 +299,6 @@ export default function useHidAmico() {
299
299
  });
300
300
  }
301
301
 
302
- function getUserImage(readerId: string, hidUserId: string | number) {
303
- return useNuxtApp().$api<HidApiRecord>(`${basePath}/readers/${readerId}/users/${hidUserId}/image`, {
304
- method: "GET",
305
- });
306
- }
307
302
 
308
303
  function setUserImage(
309
304
  readerId: string,
@@ -525,7 +520,6 @@ export default function useHidAmico() {
525
520
  getReaderConfiguration,
526
521
  setReaderConfiguration,
527
522
  getLogs,
528
- getUserImage,
529
523
  setUserImage,
530
524
  deleteUserImage,
531
525
  getUserCards,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.1.3-staging.252",
5
+ "version": "4.1.3-staging.253",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",