@7365admin1/layer-common 3.2.8-staging.204 → 3.2.8-staging.206

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.
@@ -62,13 +62,13 @@
62
62
  v-model="selectedReaderId"
63
63
  class="reader-select"
64
64
  :items="readerOptions"
65
- @update:model-value="loadUsers"
65
+ @update:model-value="reloadUsersFromFirstPage"
66
66
  />
67
67
  <AppField
68
68
  v-model="search"
69
69
  search
70
70
  placeholder="Search"
71
- @keyup.enter="loadUsers"
71
+ @keyup.enter="reloadUsersFromFirstPage"
72
72
  />
73
73
  <AppSelect
74
74
  v-if="!props.cardManagement"
@@ -76,7 +76,7 @@
76
76
  :items="statusOptions"
77
77
  placeholder="Status"
78
78
  clearable
79
- @update:model-value="loadUsers"
79
+ @update:model-value="reloadUsersFromFirstPage"
80
80
  />
81
81
  </div>
82
82
  </template>
@@ -239,6 +239,24 @@
239
239
  klass="mb-3"
240
240
  />
241
241
 
242
+ <div class="field-label">Link To <span>*</span></div>
243
+ <AppSelect
244
+ v-model="form.subjectCategory"
245
+ :items="subjectCategoryOptions"
246
+ placeholder="Select person type"
247
+ klass="mb-3"
248
+ @update:model-value="onSubjectCategoryChanged"
249
+ />
250
+
251
+ <div class="field-label">Resident / Staff / Provider <span>*</span></div>
252
+ <AppSelect
253
+ v-model="form.subjectId"
254
+ :items="subjectOptions"
255
+ :loading="loadingSubjects"
256
+ placeholder="Select an existing site record"
257
+ klass="mb-3"
258
+ />
259
+
242
260
  <div class="field-label">ID</div>
243
261
  <AppField v-model="form.hidUserId" aria-label="ID" readonly klass="mb-3" />
244
262
 
@@ -400,15 +418,30 @@
400
418
  No physical HID card is assigned to this user.
401
419
  </v-alert>
402
420
 
403
- <div class="field-label">Card data type</div>
404
- <AppSelect
405
- v-model="cardForm.cardType"
406
- :items="physicalCardTypeOptions"
421
+ <div class="field-label">Reader card data mode</div>
422
+ <v-alert
423
+ :type="cardProfileConfigured ? 'info' : 'warning'"
424
+ variant="tonal"
425
+ density="compact"
407
426
  class="mb-3"
408
- />
427
+ >
428
+ {{ cardProfileLabel }}
429
+ </v-alert>
409
430
  <p class="field-hint hid-card-type-hint">
410
431
  PACS uses Facility Code + Card Number. CSN uses the raw serial number read from the physical card.
411
432
  </p>
433
+ <template v-if="hybridCardProfile">
434
+ <div class="field-label mt-3">Presented Card Type</div>
435
+ <AppSelect
436
+ v-model="cardForm.cardType"
437
+ :items="hybridCardTypeOptions"
438
+ aria-label="Presented Card Type"
439
+ class="mb-3"
440
+ />
441
+ <p class="field-hint hid-card-type-hint">
442
+ Choose MIFARE CSN for a MIFARE card. Choose PACS for any other card in this reader mode.
443
+ </p>
444
+ </template>
412
445
  <p class="field-hint hid-card-access-hint">
413
446
  The card uses this HID user's existing portal and access rules; assigning a card does not grant new access.
414
447
  </p>
@@ -453,14 +486,14 @@
453
486
  <div class="hid-card-actions mt-4">
454
487
  <AppButton
455
488
  variant="secondary"
456
- :disabled="savingCard || enrollingCard"
489
+ :disabled="savingCard || enrollingCard || !cardProfileConfigured"
457
490
  @click="assignPhysicalCard"
458
491
  >
459
492
  Assign Manually
460
493
  </AppButton>
461
494
  <AppButton
462
495
  icon="mdi-contactless-payment"
463
- :disabled="savingCard || enrollingCard"
496
+ :disabled="savingCard || enrollingCard || !cardProfileConfigured"
464
497
  @click="enrollPhysicalCard"
465
498
  >
466
499
  Read at Reader
@@ -534,6 +567,7 @@ const props = defineProps({
534
567
  const {
535
568
  getReaders,
536
569
  getIdentities,
570
+ getReaderUsers,
537
571
  getUserImage,
538
572
  setUserImage,
539
573
  deleteUserImage,
@@ -549,10 +583,70 @@ const {
549
583
  createIdentity,
550
584
  updateIdentity,
551
585
  deleteIdentity,
586
+ getPermissionCandidates,
552
587
  } = useHidAmico();
553
588
 
554
- const readers = ref<Record<string, any>[]>([]);
555
- const users = ref<Record<string, any>[]>([]);
589
+ type HidMetadata = Record<string, unknown> & {
590
+ name?: string;
591
+ block?: string;
592
+ level?: string;
593
+ unit?: string;
594
+ unitLabel?: string;
595
+ photo?: string;
596
+ facialData?: string;
597
+ imageTimestamp?: string;
598
+ lastAccessAt?: string;
599
+ facialScores?: Record<string, unknown>;
600
+ pinEnrolled?: boolean;
601
+ hidPhysicalCards?: HidCard[];
602
+ };
603
+
604
+ type HidReader = Record<string, unknown> & {
605
+ _id: string;
606
+ name?: string;
607
+ deviceId?: string;
608
+ portalName?: string;
609
+ portalId?: string;
610
+ cardReadingMode?: string;
611
+ pacsFormat?: string;
612
+ csnByteOrder?: string;
613
+ };
614
+
615
+ type HidUser = Record<string, unknown> & {
616
+ _id?: string;
617
+ _rowKey?: string;
618
+ reader?: string;
619
+ hidUserId?: string | number;
620
+ rawHidUserId?: string | number;
621
+ registration?: string;
622
+ cardNo?: string;
623
+ name?: string;
624
+ metadata?: HidMetadata;
625
+ isAdministrator?: boolean;
626
+ person?: string;
627
+ user?: string;
628
+ member?: string;
629
+ serviceProvider?: string;
630
+ visitor?: string;
631
+ };
632
+
633
+ type HidCard = Record<string, unknown> & {
634
+ id?: string | number;
635
+ cardType?: "pacs" | "csn";
636
+ facilityCode?: string | number;
637
+ cardNumber?: string | number;
638
+ value?: string | number;
639
+ source?: "reader" | "manual" | "device";
640
+ };
641
+
642
+ type HidIdentityPayload = Record<string, unknown> & {
643
+ hidUserId: string;
644
+ registration: string;
645
+ metadata: HidMetadata;
646
+ };
647
+
648
+ const readers = ref<HidReader[]>([]);
649
+ const users = ref<HidUser[]>([]);
556
650
  const userImages = ref<Record<string, string>>({});
557
651
  const administratorUserIds = ref<Set<number>>(new Set());
558
652
  const selectedReaderId = ref("");
@@ -570,10 +664,10 @@ const formDialog = ref(false);
570
664
  const viewDialog = ref(false);
571
665
  const deleteDialog = ref(false);
572
666
  const cardDialog = ref(false);
573
- const selectedUser = ref<Record<string, any> | null>(null);
574
- const cardUser = ref<Record<string, any> | null>(null);
575
- const physicalCards = ref<Record<string, any>[]>([]);
576
- const physicalCardsByUserId = ref<Record<string, Record<string, any>[]>>({});
667
+ const selectedUser = ref<HidUser | null>(null);
668
+ const cardUser = ref<HidUser | null>(null);
669
+ const physicalCards = ref<HidCard[]>([]);
670
+ const physicalCardsByUserId = ref<Record<string, HidCard[]>>({});
577
671
  const loadingCards = ref(false);
578
672
  const savingCard = ref(false);
579
673
  const enrollingCard = ref(false);
@@ -603,8 +697,24 @@ const form = reactive({
603
697
  isAdministrator: false,
604
698
  accessPin: "",
605
699
  photoPreview: "",
700
+ subjectCategory: "resident" as THidPermissionCategory,
701
+ subjectId: "",
606
702
  });
607
703
 
704
+ const route = useRoute();
705
+ const orgId = computed(() => String(route.params.org || ""));
706
+ const permissionCandidates = ref<THidPermissionCandidate[]>([]);
707
+ const loadingSubjects = ref(false);
708
+ const subjectCategoryOptions = [
709
+ { title: "Resident", value: "resident" },
710
+ { title: "Property management", value: "property_management" },
711
+ { title: "Service provider", value: "service_provider" },
712
+ ];
713
+ const subjectOptions = computed(() => permissionCandidates.value.map((candidate) => ({
714
+ title: candidate.subtitle ? `${candidate.name} · ${candidate.subtitle}` : candidate.name,
715
+ value: candidate.subjectId,
716
+ })));
717
+
608
718
  const cardForm = reactive({
609
719
  cardType: "pacs" as "pacs" | "csn",
610
720
  facilityCode: "",
@@ -612,10 +722,27 @@ const cardForm = reactive({
612
722
  cardValue: "",
613
723
  });
614
724
 
615
- const physicalCardTypeOptions = [
616
- { title: "PACS (Facility Code + Card Number)", value: "pacs" },
617
- { title: "CSN (Raw Card Serial Number)", value: "csn" },
725
+ const cardReader = computed(() => {
726
+ const readerId = String(cardUser.value?.reader || selectedReaderId.value || "");
727
+ return readers.value.find((reader) => String(reader._id) === readerId) || null;
728
+ });
729
+ const cardProfileConfigured = computed(() => ["pacs", "csn", "csn_for_mifare"].includes(
730
+ String(cardReader.value?.cardReadingMode || ""),
731
+ ));
732
+ const hybridCardProfile = computed(() => cardReader.value?.cardReadingMode === "csn_for_mifare");
733
+ const hybridCardTypeOptions = [
734
+ { title: "MIFARE card (CSN)", value: "csn" },
735
+ { title: "Other card (PACS)", value: "pacs" },
618
736
  ];
737
+ const cardProfileLabel = computed(() => {
738
+ const mode = String(cardReader.value?.cardReadingMode || "unconfigured");
739
+ if (mode === "pacs") return `PACS · ${(cardReader.value?.pacsFormat || "raw").toUpperCase()}`;
740
+ if (mode === "csn") return `CSN · ${(cardReader.value?.csnByteOrder || "msb").toUpperCase()} first`;
741
+ if (mode === "csn_for_mifare") {
742
+ return `PACS for non-MIFARE cards · CSN for MIFARE (${(cardReader.value?.csnByteOrder || "msb").toUpperCase()} first)`;
743
+ }
744
+ return "Configure the card data mode on the HID Reader screen before assigning a card.";
745
+ });
619
746
 
620
747
  const statusOptions = ["Mapped", "Unmapped"];
621
748
 
@@ -655,6 +782,11 @@ function goToPage(next: number) {
655
782
  loadUsers();
656
783
  }
657
784
 
785
+ function reloadUsersFromFirstPage() {
786
+ page.value = 1;
787
+ loadUsers();
788
+ }
789
+
658
790
  const readerOptions = computed(() =>
659
791
  readers.value.map((reader) => ({
660
792
  title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
@@ -687,8 +819,12 @@ async function init() {
687
819
  async function loadReaders() {
688
820
  if (!props.site) return;
689
821
  const response = await getReaders({ site: props.site, page: 1, limit: 100 });
690
- readers.value =
691
- response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
822
+ const responseRecord = toRecord(response);
823
+ const responseData = toRecord(responseRecord.data);
824
+ const items = responseRecord.items ?? responseData.items ?? responseData.readers ?? [];
825
+ readers.value = (Array.isArray(items) ? items : [])
826
+ .map(toHidReader)
827
+ .filter((reader): reader is HidReader => reader !== null);
692
828
  if (!selectedReaderId.value && readers.value.length) {
693
829
  selectedReaderId.value = readers.value[0]._id;
694
830
  }
@@ -704,44 +840,26 @@ async function loadUsers() {
704
840
 
705
841
  loading.value = true;
706
842
  try {
707
- const identityQuery: Record<string, any> = {
708
- page: 1,
709
- limit: 500,
710
- };
711
-
712
- const [identityResponse, readerResponse, roleResponse] = await Promise.all([
713
- getIdentities(selectedReaderId.value, identityQuery),
714
- runObjectOperation(selectedReaderId.value, {
715
- operation: "load",
716
- object: "users",
717
- limit: 500,
718
- offset: 0,
719
- }),
720
- loadAdministratorRoles(selectedReaderId.value),
721
- ]);
722
-
723
- const identities =
724
- identityResponse?.items ??
725
- identityResponse?.data?.items ??
726
- identityResponse?.data?.identities ??
727
- [];
728
- administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
729
- const readerUsers = normalizeHidUsers(readerResponse).filter(
730
- (user) => Number(user.userTypeId) !== 1,
731
- );
732
- const mergedUsers = mergeReaderUsersWithIdentities(readerUsers, identities).map(
733
- (user) => ({
734
- ...user,
735
- isAdministrator: hasAdministratorRule(user),
736
- }),
843
+ const response = await getReaderUsers(selectedReaderId.value, {
844
+ page: page.value,
845
+ limit: limit.value,
846
+ search: search.value.trim(),
847
+ status: status.value.toLowerCase() as "mapped" | "unmapped" | "",
848
+ });
849
+ const responseRecord = toRecord(response);
850
+ const responseData = toRecord(responseRecord.data);
851
+ const responseItems = responseRecord.items ?? responseData.items ?? [];
852
+ const pageItems = (Array.isArray(responseItems) ? responseItems : []).map(toHidUser);
853
+ administratorUserIds.value = new Set(
854
+ pageItems
855
+ .filter((user) => user.isAdministrator === true)
856
+ .map((user) => toHidNumericId(user.hidUserId))
857
+ .filter((userId: number | undefined): userId is number => Boolean(userId)),
737
858
  );
738
- const filteredUsers = filterUsers(mergedUsers);
739
- total.value = filteredUsers.length;
740
- pages.value = Math.max(1, Math.ceil(total.value / limit.value));
741
- if (page.value > pages.value) page.value = pages.value;
742
- const start = (page.value - 1) * limit.value;
743
- users.value = filteredUsers.slice(start, start + limit.value);
744
- serverPageRange.value = "";
859
+ users.value = pageItems;
860
+ total.value = Number(responseRecord.total ?? responseData.total ?? users.value.length);
861
+ pages.value = Number(responseRecord.pages ?? responseData.pages ?? 1);
862
+ serverPageRange.value = String(responseRecord.pageRange ?? responseData.pageRange ?? "");
745
863
  if (props.cardManagement) {
746
864
  await loadVisibleUserCards(users.value);
747
865
  } else {
@@ -761,7 +879,7 @@ async function loadUsers() {
761
879
  }
762
880
  }
763
881
 
764
- async function loadUserImages(items: Record<string, any>[]) {
882
+ async function loadUserImages(items: HidUser[]) {
765
883
  if (!selectedReaderId.value) return;
766
884
 
767
885
  await Promise.all(
@@ -799,6 +917,34 @@ function resetForm() {
799
917
  form.photoPreview = "";
800
918
  selectedPhotoFile.value = null;
801
919
  removePhotoRequested.value = false;
920
+ form.subjectCategory = "resident";
921
+ form.subjectId = "";
922
+ }
923
+
924
+ async function loadSubjectCandidates() {
925
+ const readerId = form.reader || selectedReaderId.value;
926
+ if (!readerId || !orgId.value) {
927
+ permissionCandidates.value = [];
928
+ return;
929
+ }
930
+ loadingSubjects.value = true;
931
+ try {
932
+ const response = await getPermissionCandidates(props.site, {
933
+ orgId: orgId.value,
934
+ readerId,
935
+ category: form.subjectCategory,
936
+ page: 1,
937
+ limit: 500,
938
+ });
939
+ permissionCandidates.value = response.items || [];
940
+ } finally {
941
+ loadingSubjects.value = false;
942
+ }
943
+ }
944
+
945
+ async function onSubjectCategoryChanged() {
946
+ form.subjectId = "";
947
+ await loadSubjectCandidates();
802
948
  }
803
949
 
804
950
  async function openEnroll() {
@@ -810,10 +956,11 @@ async function openEnroll() {
810
956
  ]);
811
957
  form.hidUserId = hidUserId;
812
958
  form.registration = registration;
959
+ await loadSubjectCandidates();
813
960
  formDialog.value = true;
814
961
  }
815
962
 
816
- async function openEdit(user: Record<string, any>) {
963
+ async function openEdit(user: HidUser) {
817
964
  selectedUser.value = user;
818
965
  form.reader = String(user.reader ?? selectedReaderId.value);
819
966
  form.block = user.metadata?.block ?? "";
@@ -827,6 +974,13 @@ async function openEdit(user: Record<string, any>) {
827
974
  pinEnrolled.value = Boolean(user.metadata?.pinEnrolled);
828
975
  removePinRequested.value = false;
829
976
  form.photoPreview = getUserImageSrc(user);
977
+ form.subjectCategory = user.person
978
+ ? "resident"
979
+ : user.serviceProvider
980
+ ? "service_provider"
981
+ : "property_management";
982
+ form.subjectId = String(user.person || user.member || user.serviceProvider || "");
983
+ await loadSubjectCandidates();
830
984
  selectedPhotoFile.value = null;
831
985
  removePhotoRequested.value = false;
832
986
  formDialog.value = true;
@@ -849,12 +1003,12 @@ async function openEdit(user: Record<string, any>) {
849
1003
  }
850
1004
  }
851
1005
 
852
- function openView(user: Record<string, any>) {
1006
+ function openView(user: HidUser) {
853
1007
  selectedUser.value = user;
854
1008
  viewDialog.value = true;
855
1009
  }
856
1010
 
857
- function openDelete(user: Record<string, any>) {
1011
+ function openDelete(user: HidUser) {
858
1012
  selectedUser.value = user;
859
1013
  deleteDialog.value = true;
860
1014
  }
@@ -903,8 +1057,8 @@ function onPhotoChange(event: Event) {
903
1057
  showToast("Facial enrollment only accepts JPG or PNG images.", "error");
904
1058
  return;
905
1059
  }
906
- if (file.size > 2 * 1024 * 1024) {
907
- showToast("Facial enrollment images must be smaller than 2 MB.", "error");
1060
+ if (file.size >= 1024 * 1024) {
1061
+ showToast("Facial enrollment images must be smaller than 1 MB.", "error");
908
1062
  return;
909
1063
  }
910
1064
 
@@ -920,7 +1074,7 @@ function onPhotoChange(event: Event) {
920
1074
  async function saveUser() {
921
1075
  const readerId =
922
1076
  selectedReaderId.value || form.reader || readers.value[0]?._id;
923
- if (!readerId || !form.name || !form.hidUserId || !form.registration) {
1077
+ if (!readerId || !form.name || !form.hidUserId || !form.registration || !form.subjectId) {
924
1078
  showToast("Please complete the required HID user fields.", "error");
925
1079
  return;
926
1080
  }
@@ -934,6 +1088,7 @@ async function saveUser() {
934
1088
  }
935
1089
 
936
1090
  saving.value = true;
1091
+ let newlyCreatedHidUserId: number | undefined;
937
1092
  try {
938
1093
  const hidUser = toHidUserObject({
939
1094
  hidUserId: form.hidUserId,
@@ -948,7 +1103,14 @@ async function saveUser() {
948
1103
  const payload = {
949
1104
  hidUserId: String(hidUser.id),
950
1105
  registration: form.registration,
951
- type: (selectedUser.value?.type || "resident") as
1106
+ person: form.subjectCategory === "resident" ? form.subjectId : "",
1107
+ member: form.subjectCategory === "property_management" ? form.subjectId : "",
1108
+ serviceProvider: form.subjectCategory === "service_provider" ? form.subjectId : "",
1109
+ type: (form.subjectCategory === "resident"
1110
+ ? "resident"
1111
+ : form.subjectCategory === "service_provider"
1112
+ ? "contractor"
1113
+ : form.isAdministrator ? "admin" : "staff") as
952
1114
  | "resident"
953
1115
  | "staff"
954
1116
  | "contractor"
@@ -963,7 +1125,6 @@ async function saveUser() {
963
1125
  unit: form.unit,
964
1126
  unitLabel: buildUnitLabel(),
965
1127
  facialData: form.photoPreview ? form.hidUserId : "",
966
- photo: form.photoPreview,
967
1128
  imageTimestamp: selectedUser.value?.metadata?.imageTimestamp || "",
968
1129
  facialScores: selectedUser.value?.metadata?.facialScores || {},
969
1130
  pinEnrolled: pinEnrolled.value,
@@ -983,7 +1144,8 @@ async function saveUser() {
983
1144
  await saveIdentityOnReader(readerId, payload);
984
1145
  }
985
1146
  } else {
986
- await createHidUserOnReader(readerId, hidUser);
1147
+ const created = await createHidUserOnReader(readerId, hidUser);
1148
+ if (created) newlyCreatedHidUserId = hidUser.id;
987
1149
  await syncHidUserRole(readerId, hidUser.id, form.isAdministrator);
988
1150
  const facialResult = await syncFacialImage(readerId, hidUser.id);
989
1151
  applyFacialResult(payload.metadata, facialResult);
@@ -999,7 +1161,18 @@ async function saveUser() {
999
1161
  ? "HID user updated on reader."
1000
1162
  : "HID user enrolled on reader."
1001
1163
  );
1002
- } catch (error: any) {
1164
+ } catch (error: unknown) {
1165
+ if (newlyCreatedHidUserId) {
1166
+ try {
1167
+ await runObjectOperation(readerId, {
1168
+ operation: "destroy",
1169
+ object: "users",
1170
+ where: { users: { id: newlyCreatedHidUserId } },
1171
+ });
1172
+ } catch (rollbackError) {
1173
+ console.error("Unable to roll back the partially enrolled HID user:", rollbackError);
1174
+ }
1175
+ }
1003
1176
  console.error("Unable to save HID user:", error);
1004
1177
  showToast(getHidErrorMessage(error), "error");
1005
1178
  } finally {
@@ -1094,7 +1267,7 @@ async function deleteUser() {
1094
1267
  deleteDialog.value = false;
1095
1268
  await loadUsers();
1096
1269
  showToast("HID user deleted from reader.");
1097
- } catch (error: any) {
1270
+ } catch (error: unknown) {
1098
1271
  console.error("Unable to delete HID user:", error);
1099
1272
  showToast(getHidErrorMessage(error), "error");
1100
1273
  } finally {
@@ -1104,7 +1277,7 @@ async function deleteUser() {
1104
1277
 
1105
1278
  async function saveIdentityOnReader(
1106
1279
  readerId: string,
1107
- payload: Record<string, any>
1280
+ payload: HidIdentityPayload,
1108
1281
  ) {
1109
1282
  const existingIdentity = await findExistingIdentityOnReader(
1110
1283
  readerId,
@@ -1117,7 +1290,7 @@ async function saveIdentityOnReader(
1117
1290
 
1118
1291
  try {
1119
1292
  await createIdentity(readerId, { ...payload, site: props.site });
1120
- } catch (error: any) {
1293
+ } catch (error: unknown) {
1121
1294
  const message = getHidErrorMessage(error);
1122
1295
  if (!message.toLowerCase().includes("already exists")) throw error;
1123
1296
 
@@ -1132,24 +1305,23 @@ async function saveIdentityOnReader(
1132
1305
 
1133
1306
  async function findExistingIdentityOnReader(
1134
1307
  readerId: string,
1135
- payload: Record<string, any>
1308
+ payload: HidIdentityPayload,
1136
1309
  ) {
1137
1310
  const response = await getIdentities(readerId, {
1138
1311
  page: 1,
1139
1312
  limit: 500,
1140
1313
  search: payload.hidUserId || payload.registration || payload.cardNo || "",
1141
1314
  });
1142
- const identities =
1143
- response?.items ??
1144
- response?.data?.items ??
1145
- response?.data?.identities ??
1146
- [];
1315
+ const responseRecord = toRecord(response);
1316
+ const responseData = toRecord(responseRecord.data);
1317
+ const responseItems = responseRecord.items ?? responseData.items ?? responseData.identities ?? [];
1318
+ const identities = (Array.isArray(responseItems) ? responseItems : []).map(toHidUser);
1147
1319
  const hidUserId = String(payload.hidUserId || "");
1148
1320
  const registration = String(payload.registration || "");
1149
1321
  const cardNo = String(payload.cardNo || "");
1150
1322
 
1151
1323
  return identities.find(
1152
- (identity: Record<string, any>) =>
1324
+ (identity) =>
1153
1325
  String(identity.hidUserId || "") === hidUserId ||
1154
1326
  Boolean(
1155
1327
  registration && String(identity.registration || "") === registration
@@ -1160,12 +1332,12 @@ async function findExistingIdentityOnReader(
1160
1332
 
1161
1333
  async function createHidUserOnReader(
1162
1334
  readerId: string,
1163
- user: Record<string, any>
1335
+ user: { id?: number; name: string; registration: string },
1164
1336
  ) {
1165
1337
  const existing = await getHidUserFromReader(readerId, user.id);
1166
1338
  if (existing) {
1167
1339
  await updateHidUserOnReader(readerId, user);
1168
- return;
1340
+ return false;
1169
1341
  }
1170
1342
 
1171
1343
  await runObjectOperation(readerId, {
@@ -1173,6 +1345,7 @@ async function createHidUserOnReader(
1173
1345
  object: "users",
1174
1346
  values: [user],
1175
1347
  });
1348
+ return true;
1176
1349
  }
1177
1350
 
1178
1351
  async function getHidUserFromReader(readerId: string, id: unknown) {
@@ -1193,7 +1366,7 @@ async function getHidUserFromReader(readerId: string, id: unknown) {
1193
1366
 
1194
1367
  async function updateHidUserOnReader(
1195
1368
  readerId: string,
1196
- user: Record<string, any>
1369
+ user: { id?: number; name: string; registration: string },
1197
1370
  ) {
1198
1371
  const { id, ...values } = user;
1199
1372
  await runObjectOperation(readerId, {
@@ -1210,7 +1383,7 @@ async function updateHidUserOnReader(
1210
1383
 
1211
1384
  async function deleteHidUserFromReader(
1212
1385
  readerId: string,
1213
- user: Record<string, any>
1386
+ user: HidUser,
1214
1387
  ) {
1215
1388
  const id = toHidNumericId(user.hidUserId);
1216
1389
  if (!readerId || !id) return;
@@ -1299,36 +1472,7 @@ async function getHidUserRole(readerId: string, userId: number) {
1299
1472
  return Array.isArray(roles) ? roles[0] : undefined;
1300
1473
  }
1301
1474
 
1302
- function loadAdministratorRoles(readerId: string) {
1303
- return runObjectOperation(readerId, {
1304
- operation: "load",
1305
- object: "user_roles",
1306
- where: {
1307
- user_roles: {
1308
- role: 1,
1309
- },
1310
- },
1311
- limit: 500,
1312
- offset: 0,
1313
- });
1314
- }
1315
-
1316
- function getAdministratorRoleUserIds(response: Record<string, any> | null) {
1317
- const data = response?.data || response || {};
1318
- const roles =
1319
- data?.user_roles ||
1320
- data?.data?.user_roles ||
1321
- data?.result?.user_roles ||
1322
- [];
1323
-
1324
- return new Set(
1325
- (Array.isArray(roles) ? roles : [])
1326
- .map((role) => toHidNumericId(role.user_id))
1327
- .filter((userId): userId is number => Boolean(userId))
1328
- );
1329
- }
1330
-
1331
- function toHidUserObject(identity: Record<string, any>) {
1475
+ function toHidUserObject(identity: Pick<HidUser, "hidUserId" | "name" | "registration">) {
1332
1476
  return {
1333
1477
  id: toHidNumericId(identity.hidUserId),
1334
1478
  name:
@@ -1349,12 +1493,12 @@ function toHidNumericId(value: unknown) {
1349
1493
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
1350
1494
  }
1351
1495
 
1352
- function hasAdministratorRule(user?: Record<string, any> | null) {
1496
+ function hasAdministratorRule(user?: HidUser | null) {
1353
1497
  const hidUserId = toHidNumericId(user?.hidUserId);
1354
1498
  return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
1355
1499
  }
1356
1500
 
1357
- async function loadVisibleUserCards(items: Record<string, any>[]) {
1501
+ async function loadVisibleUserCards(items: HidUser[]) {
1358
1502
  if (!selectedReaderId.value) return;
1359
1503
 
1360
1504
  const entries = await Promise.all(
@@ -1364,7 +1508,9 @@ async function loadVisibleUserCards(items: Record<string, any>[]) {
1364
1508
 
1365
1509
  try {
1366
1510
  const response = await getUserCards(selectedReaderId.value, hidUserId);
1367
- return [String(hidUserId), response?.data?.cards || []] as const;
1511
+ const responseData = toRecord(toRecord(response).data);
1512
+ const cards = responseData.cards;
1513
+ return [String(hidUserId), (Array.isArray(cards) ? cards : []).map(toHidCard)] as const;
1368
1514
  } catch {
1369
1515
  return [String(hidUserId), []] as const;
1370
1516
  }
@@ -1376,14 +1522,15 @@ async function loadVisibleUserCards(items: Record<string, any>[]) {
1376
1522
  );
1377
1523
  }
1378
1524
 
1379
- function getUserPhysicalCards(user: Record<string, any>) {
1525
+ function getUserPhysicalCards(user: HidUser) {
1380
1526
  const hidUserId = toHidNumericId(user.hidUserId);
1381
1527
  return hidUserId ? physicalCardsByUserId.value[String(hidUserId)] || [] : [];
1382
1528
  }
1383
1529
 
1384
- async function openCardManager(user: Record<string, any>) {
1530
+ async function openCardManager(user: HidUser) {
1385
1531
  cardUser.value = user;
1386
- cardForm.cardType = "pacs";
1532
+ const reader = readers.value.find((item) => String(item._id) === String(user.reader || selectedReaderId.value));
1533
+ cardForm.cardType = reader?.cardReadingMode === "pacs" ? "pacs" : "csn";
1387
1534
  cardForm.facilityCode = "";
1388
1535
  cardForm.cardNumber = "";
1389
1536
  cardForm.cardValue = "";
@@ -1405,7 +1552,9 @@ async function loadPhysicalCards() {
1405
1552
  loadingCards.value = true;
1406
1553
  try {
1407
1554
  const response = await getUserCards(readerId, hidUserId);
1408
- physicalCards.value = response?.data?.cards || [];
1555
+ const responseData = toRecord(toRecord(response).data);
1556
+ const cards = responseData.cards;
1557
+ physicalCards.value = (Array.isArray(cards) ? cards : []).map(toHidCard);
1409
1558
  } catch (error: unknown) {
1410
1559
  physicalCards.value = [];
1411
1560
  showToast(getHidErrorMessage(error), "error");
@@ -1414,17 +1563,17 @@ async function loadPhysicalCards() {
1414
1563
  }
1415
1564
  }
1416
1565
 
1417
- function parsePhysicalCardInteger(value: string, label: string) {
1566
+ function parsePhysicalCardInteger(value: string, label: string, maximum: bigint) {
1418
1567
  if (!/^\d+$/.test(value.trim())) {
1419
1568
  showToast(`${label} must contain numbers only.`, "error");
1420
1569
  return null;
1421
1570
  }
1422
- const number = Number(value);
1423
- if (!Number.isSafeInteger(number) || number < 0) {
1571
+ const normalized = value.trim();
1572
+ if (BigInt(normalized) > maximum) {
1424
1573
  showToast(`${label} is outside the supported range.`, "error");
1425
1574
  return null;
1426
1575
  }
1427
- return number;
1576
+ return normalized;
1428
1577
  }
1429
1578
 
1430
1579
  async function assignPhysicalCard() {
@@ -1432,20 +1581,20 @@ async function assignPhysicalCard() {
1432
1581
  if (!readerId || !hidUserId) return;
1433
1582
 
1434
1583
  let payload:
1435
- | { cardType: "pacs"; facilityCode: number; cardNumber: number }
1584
+ | { cardType: "pacs"; facilityCode: string; cardNumber: string }
1436
1585
  | { cardType: "csn"; cardValue: string };
1437
1586
  if (cardForm.cardType === "pacs") {
1438
- const facilityCode = parsePhysicalCardInteger(cardForm.facilityCode, "Facility Code");
1439
- const cardNumber = parsePhysicalCardInteger(cardForm.cardNumber, "Card Number");
1587
+ const facilityCode = parsePhysicalCardInteger(cardForm.facilityCode, "Facility Code", 4294967295n);
1588
+ const cardNumber = parsePhysicalCardInteger(cardForm.cardNumber, "Card Number", 4294967295n);
1440
1589
  if (facilityCode === null || cardNumber === null) return;
1441
1590
  payload = { cardType: "pacs", facilityCode, cardNumber };
1442
1591
  } else {
1443
- const cardValue = parsePhysicalCardInteger(cardForm.cardValue, "CSN");
1444
- if (cardValue === null || cardValue <= 0) {
1445
- if (cardValue === 0) showToast("CSN must be greater than zero.", "error");
1592
+ const cardValue = parsePhysicalCardInteger(cardForm.cardValue, "CSN", 18446744073709551615n);
1593
+ if (cardValue === null || BigInt(cardValue) <= 0n) {
1594
+ if (cardValue === "0") showToast("CSN must be greater than zero.", "error");
1446
1595
  return;
1447
1596
  }
1448
- payload = { cardType: "csn", cardValue: String(cardValue) };
1597
+ payload = { cardType: "csn", cardValue };
1449
1598
  }
1450
1599
 
1451
1600
  savingCard.value = true;
@@ -1496,7 +1645,7 @@ async function cancelPhysicalCardEnrollment() {
1496
1645
  }
1497
1646
  }
1498
1647
 
1499
- async function removePhysicalCard(card: Record<string, any>) {
1648
+ async function removePhysicalCard(card: HidCard) {
1500
1649
  if (!window.confirm(`Remove physical HID card ${formatPhysicalCard(card)}?`)) return;
1501
1650
  const { readerId, hidUserId } = getCardReaderAndUser();
1502
1651
  if (!readerId || !hidUserId || !card?.id) return;
@@ -1514,7 +1663,7 @@ async function removePhysicalCard(card: Record<string, any>) {
1514
1663
  }
1515
1664
  }
1516
1665
 
1517
- function formatPhysicalCard(card: Record<string, any>) {
1666
+ function formatPhysicalCard(card: HidCard) {
1518
1667
  if (card?.cardType === "pacs") {
1519
1668
  return `PACS ${card.facilityCode}.${card.cardNumber}`;
1520
1669
  }
@@ -1522,7 +1671,7 @@ function formatPhysicalCard(card: Record<string, any>) {
1522
1671
  return `Card ${card?.value || "N/A"}`;
1523
1672
  }
1524
1673
 
1525
- function getPhysicalCardSubtitle(card: Record<string, any>) {
1674
+ function getPhysicalCardSubtitle(card: HidCard) {
1526
1675
  const source = card?.source === "reader" ? "Read at reader" : card?.source === "manual" ? "Manual" : "On device";
1527
1676
  return `${source} - Raw value ${card?.value || "N/A"}`;
1528
1677
  }
@@ -1532,11 +1681,11 @@ function buildUnitLabel() {
1532
1681
  return parts.length ? `BLK ${parts.join("/")}` : "";
1533
1682
  }
1534
1683
 
1535
- function getName(user?: Record<string, any> | null) {
1684
+ function getName(user?: HidUser | null) {
1536
1685
  return user?.metadata?.name || user?.name || "N/A";
1537
1686
  }
1538
1687
 
1539
- function getUnit(user?: Record<string, any> | null) {
1688
+ function getUnit(user?: HidUser | null) {
1540
1689
  return (
1541
1690
  user?.metadata?.unitLabel ||
1542
1691
  [user?.metadata?.block, user?.metadata?.level, user?.metadata?.unit]
@@ -1546,7 +1695,7 @@ function getUnit(user?: Record<string, any> | null) {
1546
1695
  );
1547
1696
  }
1548
1697
 
1549
- function getFacialData(user: Record<string, any>) {
1698
+ function getFacialData(user: HidUser) {
1550
1699
  return (
1551
1700
  user?.metadata?.facialData ||
1552
1701
  user?.metadata?.imageTimestamp ||
@@ -1554,7 +1703,7 @@ function getFacialData(user: Record<string, any>) {
1554
1703
  );
1555
1704
  }
1556
1705
 
1557
- function getUserImageSrc(user: Record<string, any>) {
1706
+ function getUserImageSrc(user: HidUser) {
1558
1707
  const hidUserId = toHidNumericId(user?.hidUserId);
1559
1708
  return (
1560
1709
  user?.metadata?.photo ||
@@ -1621,39 +1770,41 @@ async function getExistingHidReaderUsers(fields?: string[]) {
1621
1770
  }
1622
1771
  }
1623
1772
 
1624
- function normalizeHidUsers(response: Record<string, any>) {
1625
- const items =
1626
- response?.data?.users ||
1627
- response?.data?.data?.users ||
1628
- response?.users ||
1629
- [];
1773
+ function normalizeHidUsers(response: unknown): HidUser[] {
1774
+ const responseRecord = toRecord(response);
1775
+ const responseData = toRecord(responseRecord.data);
1776
+ const nestedData = toRecord(responseData.data);
1777
+ const items = responseData.users ?? nestedData.users ?? responseRecord.users ?? [];
1630
1778
 
1631
1779
  return Array.isArray(items)
1632
- ? items.map((user) => ({
1780
+ ? items.map((value) => {
1781
+ const user = toRecord(value);
1782
+ return {
1633
1783
  _rowKey: `reader-${user.id}`,
1634
1784
  hidUserId: String(user.id || ""),
1635
- rawHidUserId: user.id,
1636
- registration: user.registration,
1637
- userTypeId: user.user_type_id,
1638
- cardNo: user.card_value || user.cardNo || "",
1639
- name: user.name,
1785
+ rawHidUserId: toText(user.id),
1786
+ registration: toText(user.registration),
1787
+ userTypeId: toText(user.user_type_id),
1788
+ cardNo: toText(user.card_value || user.cardNo),
1789
+ name: toText(user.name),
1640
1790
  metadata: {
1641
- name: user.name,
1791
+ name: toText(user.name),
1642
1792
  imageTimestamp: user.image_timestamp
1643
1793
  ? String(user.image_timestamp)
1644
1794
  : "",
1645
1795
  lastAccessAt: fromUnixSeconds(user.last_access),
1646
1796
  hidReaderSource: true,
1647
1797
  },
1648
- }))
1798
+ };
1799
+ })
1649
1800
  : [];
1650
1801
  }
1651
1802
 
1652
1803
  function mergeReaderUsersWithIdentities(
1653
- readerUsers: Record<string, any>[],
1654
- identities: Record<string, any>[]
1804
+ readerUsers: HidUser[],
1805
+ identities: HidUser[],
1655
1806
  ) {
1656
- const identityById = new Map<string, Record<string, any>>();
1807
+ const identityById = new Map<string, HidUser>();
1657
1808
  identities.forEach((identity) => {
1658
1809
  const id = toHidNumericId(identity.hidUserId);
1659
1810
  if (id) identityById.set(String(id), identity);
@@ -1671,9 +1822,9 @@ function mergeReaderUsersWithIdentities(
1671
1822
  _rowKey: `mapped-${identity._id || readerUser.rawHidUserId}`,
1672
1823
  hidUserId: identity.hidUserId || readerUser.hidUserId,
1673
1824
  rawHidUserId: readerUser.rawHidUserId,
1674
- name: identity.metadata?.name || identity.name || readerUser.name,
1675
- registration: identity.registration || readerUser.registration,
1676
- cardNo: identity.cardNo || readerUser.cardNo,
1825
+ name: identity.metadata?.name || identity.name || readerUser.name || "",
1826
+ registration: identity.registration || readerUser.registration || "",
1827
+ cardNo: identity.cardNo || readerUser.cardNo || "",
1677
1828
  metadata: {
1678
1829
  ...readerUser.metadata,
1679
1830
  ...identity.metadata,
@@ -1689,33 +1840,12 @@ function mergeReaderUsersWithIdentities(
1689
1840
  return merged;
1690
1841
  }
1691
1842
 
1692
- function filterUsers(items: Record<string, any>[]) {
1693
- const searchText = search.value.trim().toLowerCase();
1694
- return items.filter((item) => {
1695
- const matchesSearch =
1696
- !searchText ||
1697
- [getName(item), item.hidUserId, item.registration, item.cardNo].some(
1698
- (value) =>
1699
- String(value || "")
1700
- .toLowerCase()
1701
- .includes(searchText)
1702
- );
1703
- const matchesStatus = !status.value || getMappingStatus(item) === status.value;
1704
- return matchesSearch && matchesStatus;
1705
- });
1706
- }
1707
-
1708
- function getTotalFromPageRange(value: string) {
1709
- const match = value.match(/\bof\s+(\d+)$/i);
1710
- return match ? Number(match[1]) : undefined;
1711
- }
1712
-
1713
- function getMappingStatus(user: Record<string, any>) {
1843
+ function getMappingStatus(user: HidUser) {
1714
1844
  return hasMappedRecord(user) ? "Mapped" : "Unmapped";
1715
1845
  }
1716
1846
 
1717
- function hasMappedRecord(user: Record<string, any>) {
1718
- return Boolean(user?.person || user?.user || user?.member || user?.visitor);
1847
+ function hasMappedRecord(user: HidUser) {
1848
+ return Boolean(user?.person || user?.user || user?.member || user?.serviceProvider || user?.visitor);
1719
1849
  }
1720
1850
 
1721
1851
  function formatDate(value?: string) {
@@ -1741,23 +1871,66 @@ function showToast(message: string, color = "success") {
1741
1871
  snackbar.show = true;
1742
1872
  }
1743
1873
 
1744
- function getHidErrorMessage(error: any) {
1745
- const data =
1746
- error?.data ||
1747
- error?.response?._data ||
1748
- error?.response?.data ||
1749
- error?.cause?.data ||
1750
- error?.cause?.response?._data ||
1751
- {};
1752
-
1753
- return String(
1754
- data?.message ||
1755
- data?.error ||
1756
- data?.statusMessage ||
1757
- error?.statusMessage ||
1758
- error?.message ||
1759
- "Unable to save HID user."
1874
+ function getHidErrorMessage(error: unknown) {
1875
+ const errorRecord = toRecord(error);
1876
+ const response = toRecord(errorRecord.response);
1877
+ const cause = toRecord(errorRecord.cause);
1878
+ const causeResponse = toRecord(cause.response);
1879
+ const data = toRecord(
1880
+ errorRecord.data || response._data || response.data || cause.data || causeResponse._data,
1760
1881
  );
1882
+
1883
+ return toText(
1884
+ data.message || data.error || data.statusMessage || errorRecord.statusMessage || errorRecord.message,
1885
+ "Unable to save HID user.",
1886
+ );
1887
+ }
1888
+
1889
+ function toRecord(value: unknown): Record<string, unknown> {
1890
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1891
+ ? value as Record<string, unknown>
1892
+ : {};
1893
+ }
1894
+
1895
+ function toHidReader(value: unknown): HidReader | null {
1896
+ const reader = toRecord(value);
1897
+ const id = toText(reader._id);
1898
+ return id ? { ...reader, _id: id } : null;
1899
+ }
1900
+
1901
+ function toHidUser(value: unknown): HidUser {
1902
+ const user = toRecord(value);
1903
+ return {
1904
+ ...user,
1905
+ _id: toText(user._id) || undefined,
1906
+ _rowKey: toText(user._rowKey) || undefined,
1907
+ reader: toText(user.reader) || undefined,
1908
+ hidUserId: toText(user.hidUserId) || undefined,
1909
+ registration: toText(user.registration) || undefined,
1910
+ cardNo: toText(user.cardNo) || undefined,
1911
+ name: toText(user.name) || undefined,
1912
+ metadata: toHidMetadata(user.metadata),
1913
+ isAdministrator: user.isAdministrator === true,
1914
+ person: toText(user.person) || undefined,
1915
+ user: toText(user.user) || undefined,
1916
+ member: toText(user.member) || undefined,
1917
+ serviceProvider: toText(user.serviceProvider) || undefined,
1918
+ visitor: toText(user.visitor) || undefined,
1919
+ };
1920
+ }
1921
+
1922
+ function toHidMetadata(value: unknown): HidMetadata {
1923
+ return toRecord(value) as HidMetadata;
1924
+ }
1925
+
1926
+ function toHidCard(value: unknown): HidCard {
1927
+ const card = toRecord(value);
1928
+ return {
1929
+ ...card,
1930
+ id: toText(card.id) || undefined,
1931
+ cardType: card.cardType === "pacs" || card.cardType === "csn" ? card.cardType : undefined,
1932
+ source: card.source === "reader" || card.source === "manual" || card.source === "device" ? card.source : undefined,
1933
+ };
1761
1934
  }
1762
1935
  </script>
1763
1936