@7365admin1/layer-common 4.90.2-staging.490 → 4.90.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/assets/css/primitives.css +0 -57
  3. package/assets/css/screens.css +4 -8
  4. package/components/AccessCardDetailsDialog.vue +6 -59
  5. package/components/AccessCardPreviewDialog.vue +53 -371
  6. package/components/AppSelect.vue +4 -143
  7. package/components/BuildingUnitFormEdit.vue +0 -6
  8. package/components/Dialog/UpdateMoreAction.vue +1 -11
  9. package/components/DocumentForm.vue +112 -407
  10. package/components/DocumentManagement.vue +12 -213
  11. package/components/Facility/BookingSetup.vue +0 -63
  12. package/components/HidAccessLogDashboard.vue +17 -74
  13. package/components/HidIntercomManagement.vue +14 -183
  14. package/components/HidQrCodeConfiguration.vue +195 -17
  15. package/components/HidReaderManagement.vue +0 -398
  16. package/components/HidReaderUserRoster.vue +3 -44
  17. package/components/HidUserEnrollment.vue +292 -2255
  18. package/components/InvitationClientForm.vue +1 -19
  19. package/components/InvitationForm.vue +4 -31
  20. package/components/InvitationMain.vue +22 -124
  21. package/components/MemberInformation.vue +4 -4
  22. package/components/PassInformation.vue +2 -2
  23. package/components/SiteSettings.vue +0 -48
  24. package/components/TableMain.vue +7 -22
  25. package/components/VehicleManagement.vue +5 -80
  26. package/components/VisitorForm.vue +19 -65
  27. package/components/VisitorManagement.vue +5 -267
  28. package/composables/useAccessManagement.ts +1 -17
  29. package/composables/useDocument.ts +0 -7
  30. package/composables/useFacility.ts +0 -11
  31. package/composables/useHidAmico.ts +0 -165
  32. package/composables/useHidNavigation.ts +1 -25
  33. package/composables/useMember.ts +0 -12
  34. package/composables/useNFCPatrolReportFilters.ts +3 -21
  35. package/composables/useNFCPatrolRoute.ts +1 -8
  36. package/composables/usePeople.ts +0 -19
  37. package/composables/useServiceProvider.ts +2 -45
  38. package/composables/useSettingsPermission.ts +0 -14
  39. package/composables/useUser.ts +2 -6
  40. package/composables/useVehicle.ts +1 -40
  41. package/composables/useVerification.ts +0 -3
  42. package/middleware/01.auth.ts +7 -3
  43. package/package.json +1 -1
  44. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  45. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  46. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  47. package/types/document.d.ts +0 -14
  48. package/types/facility.d.ts +0 -4
  49. package/types/member.d.ts +0 -4
  50. package/types/people.d.ts +1 -28
  51. package/types/service-provider.d.ts +0 -5
  52. package/types/site.d.ts +1 -7
  53. package/components/BuildingUnitManagement.vue +0 -298
  54. package/components/HidAccessPermissions.vue +0 -424
  55. package/components/HidProfileQrCode.vue +0 -332
  56. package/components/VehicleQrStickerDialog.vue +0 -249
  57. package/composables/useHidReaderSelection.ts +0 -50
  58. package/composables/useSiteCategory.ts +0 -45
  59. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  60. package/utils/hid-enrolment-subject.ts +0 -344
  61. package/utils/hid-permission-assignments.ts +0 -198
  62. package/utils/hid-reader-selection.ts +0 -52
  63. package/utils/invite-group.ts +0 -61
  64. package/utils/invite-sites.ts +0 -25
  65. package/utils/occupancy-role.ts +0 -109
  66. package/utils/service-type.ts +0 -30
  67. package/utils/vehicle-qr-sticker.ts +0 -184
@@ -57,24 +57,7 @@
57
57
  </div>
58
58
  </v-col>
59
59
 
60
- <v-col cols="12">
61
- <v-tabs
62
- v-model="activeTab"
63
- class="doc-tabs"
64
- density="compact"
65
- :hide-slider="false"
66
- >
67
- <v-tab value="active" class="text-none">Documents</v-tab>
68
- <v-tab value="expired" class="text-none">Document (Expired)</v-tab>
69
- </v-tabs>
70
- </v-col>
71
-
72
- <!--
73
- Hidden on the Expired tab: that tab is a site-wide worklist rather than a
74
- place in the folder tree, so a breadcrumb there would name a folder whose
75
- contents are not what is on screen.
76
- -->
77
- <v-col cols="12" class="mb-2" v-if="isInsideFolder && !isExpiredTab">
60
+ <v-col cols="12" class="mb-2" v-if="isInsideFolder">
78
61
  <div class="folder-path-bar d-flex align-center ga-2 px-3 py-2">
79
62
  <v-btn
80
63
  icon="mdi-arrow-left"
@@ -166,79 +149,19 @@
166
149
  <span>{{ item.date_modified || "N/A" }}</span>
167
150
  </template>
168
151
 
169
- <template #item.expiry_date="{ item }">
170
- <span>{{ item.expiry_date || "N/A" }}</span>
171
- </template>
172
-
173
152
  <template #item.remarks="{ item }">
174
153
  <span>{{ item.remarks || "N/A" }}</span>
175
154
  </template>
176
155
 
177
156
  <template #item.action="{ item }">
178
- <!--
179
- A document carries its primary action and a kebab beside it. Which
180
- primary action depends on the tab: an expired document cannot be
181
- usefully downloaded, it needs replacing, so Download gives way to
182
- Update there.
183
- -->
184
- <template v-if="!isFolderItem(item._raw)">
185
- <v-btn
186
- v-if="isExpiredTab"
187
- variant="flat"
188
- class="table-download-btn text-none"
189
- :disabled="!canUpdateDocument"
190
- @click.stop="openUpdateDialog(item._raw)"
191
- >
192
- Update
193
- </v-btn>
194
-
195
- <v-btn
196
- v-else
197
- variant="flat"
198
- class="table-download-btn text-none"
199
- @click.stop="downloadDocument(item._raw)"
200
- >
201
- Download
202
- </v-btn>
203
-
204
- <v-menu
205
- v-if="canUpdateDocument || canDeleteDocument"
206
- v-model="documentActionMenu[item._raw._id]"
207
- location="bottom end"
208
- offset="6"
209
- >
210
- <template #activator="{ props }">
211
- <v-btn
212
- v-bind="props"
213
- icon="mdi-dots-vertical"
214
- variant="text"
215
- size="small"
216
- density="comfortable"
217
- class="folder-action-btn"
218
- @click.stop
219
- />
220
- </template>
221
-
222
- <v-list class="folder-action-list pa-0">
223
- <v-list-item
224
- v-if="canUpdateDocument"
225
- class="folder-action-item"
226
- @click.stop="openDocumentEdit(item._raw)"
227
- >
228
- <v-list-item-title class="text-body-2">Edit</v-list-item-title>
229
- </v-list-item>
230
- <v-list-item
231
- v-if="canDeleteDocument"
232
- class="folder-action-item"
233
- @click.stop="openDocumentDelete(item._raw)"
234
- >
235
- <v-list-item-title class="text-body-2">
236
- Delete
237
- </v-list-item-title>
238
- </v-list-item>
239
- </v-list>
240
- </v-menu>
241
- </template>
157
+ <v-btn
158
+ v-if="!isFolderItem(item._raw)"
159
+ variant="flat"
160
+ class="table-download-btn text-none"
161
+ @click.stop="downloadDocument(item._raw)"
162
+ >
163
+ Download
164
+ </v-btn>
242
165
 
243
166
  <v-menu
244
167
  v-else
@@ -277,11 +200,8 @@
277
200
  </TableV3>
278
201
  </v-col>
279
202
 
280
- <!--
281
- Wider than the other dialogs because Add Document holds a five-column
282
- table, one row per file, rather than a stacked pair of fields.
283
- -->
284
- <v-dialog v-model="createDialog" max-width="1100" persistent>
203
+ <!-- Create Dialog -->
204
+ <v-dialog v-model="createDialog" width="450" persistent>
285
205
  <DocumentForm
286
206
  v-if="createDialog"
287
207
  :parent-id="activeParentId"
@@ -290,17 +210,6 @@
290
210
  />
291
211
  </v-dialog>
292
212
 
293
- <!-- Update Dialog: renew an expired document by replacing its file. -->
294
- <v-dialog v-model="updateDialog" max-width="1100" persistent>
295
- <DocumentForm
296
- v-if="updateDialog"
297
- mode="update"
298
- :document="selectedDocument"
299
- @cancel="updateDialog = false"
300
- @success="successRenew()"
301
- />
302
- </v-dialog>
303
-
304
213
  <!-- Create Folder Dialog -->
305
214
  <v-dialog v-model="createFolderDialog" width="450" persistent>
306
215
  <v-card width="100%">
@@ -575,15 +484,6 @@
575
484
  </v-row>
576
485
  </v-col>
577
486
 
578
- <v-col cols="12" class="mb-2">
579
- <v-row no-gutters>
580
- <v-col cols="6" class="text-body-1">Expiry Date</v-col>
581
- <v-col cols="6" class="text-right text-body-1">{{
582
- previewDetails.expiryDate
583
- }}</v-col>
584
- </v-row>
585
- </v-col>
586
-
587
487
  <v-col cols="12">
588
488
  <v-row no-gutters>
589
489
  <v-col cols="6" class="text-body-1">Remarks</v-col>
@@ -734,10 +634,6 @@ const props = defineProps({
734
634
  title: "Date Modified",
735
635
  value: "date_modified",
736
636
  },
737
- {
738
- title: "Expiry Date",
739
- value: "expiry_date",
740
- },
741
637
  {
742
638
  title: "Remarks",
743
639
  value: "remarks",
@@ -837,13 +733,6 @@ const currentFolderPath = computed(() => {
837
733
  const fileTypes = ref(["all", "pdf", "csv", "doc"]);
838
734
  const selectedFileType = ref("all");
839
735
 
840
- /**
841
- * Which half of the list is on screen. The value is sent straight to the API as
842
- * its `expiry` filter, so the tab and the query cannot drift apart.
843
- */
844
- const activeTab = ref<"active" | "expired">("active");
845
- const isExpiredTab = computed(() => activeTab.value === "expired");
846
-
847
736
  const {
848
737
  data: getDocumentReq,
849
738
  refresh: getDocuments,
@@ -859,20 +748,12 @@ const {
859
748
  parentId: activeParentId.value,
860
749
  type:
861
750
  selectedFileType.value === "all" ? undefined : selectedFileType.value,
862
- expiry: activeTab.value,
863
751
  }),
864
752
  {
865
- watch: [page, headerSearch, activeParentId, selectedFileType, activeTab],
753
+ watch: [page, headerSearch, activeParentId, selectedFileType],
866
754
  }
867
755
  );
868
756
 
869
- // Page 3 of the Documents tab is rarely page 3 of a much shorter Expired list,
870
- // so a tab change that kept the page number would land on an empty table that
871
- // looks like "nothing has expired".
872
- watch(activeTab, () => {
873
- page.value = 1;
874
- });
875
-
876
757
  const loading = computed(() => getAllReqStatus.value === "pending");
877
758
 
878
759
  // The page totals are the server's count of the WHOLE list, read through the
@@ -907,28 +788,6 @@ function formatDate(value?: string | null) {
907
788
  });
908
789
  }
909
790
 
910
- /**
911
- * Expiry is a day, not a moment: the picker collects `YYYY-MM-DD` and the
912
- * design renders `13/12/2026`. Reusing `formatDate` would append a meaningless
913
- * 00:00 to every row.
914
- */
915
- function formatDateOnly(value?: string | null) {
916
- if (!value) {
917
- return "N/A";
918
- }
919
-
920
- const dateValue = new Date(value);
921
- if (Number.isNaN(dateValue.getTime())) {
922
- return "N/A";
923
- }
924
-
925
- return dateValue.toLocaleDateString("en-SG", {
926
- year: "numeric",
927
- month: "2-digit",
928
- day: "2-digit",
929
- });
930
- }
931
-
932
791
  function formatBytes(bytes = 0) {
933
792
  const b = Number(bytes) || 0;
934
793
  if (b === 0) return "0 B";
@@ -964,20 +823,13 @@ const tableItems = computed(() => {
964
823
  : formatBytes(Number(doc.file_size ?? doc.size ?? 0)),
965
824
  date_created: formatDate(doc.created_at || doc.createdAt),
966
825
  date_modified: formatDate(doc.updated_at || doc.updatedAt),
967
- // Folders never expire, so they read N/A rather than borrowing a date.
968
- expiry_date:
969
- doc.type === "folder"
970
- ? "N/A"
971
- : formatDateOnly(doc.expiry_date || doc.expiryDate),
972
826
  remarks: doc.remarks || "N/A",
973
827
  }));
974
828
  });
975
829
 
976
830
  const createDialog = ref(false);
977
831
  const editDialog = ref(false);
978
- const updateDialog = ref(false);
979
832
  const previewDialog = ref(false);
980
- const documentActionMenu = ref<Record<string, boolean>>({});
981
833
 
982
834
  // Create folder dialog state
983
835
  const createFolderDialog = ref(false);
@@ -1028,10 +880,6 @@ const previewDetails = computed(() => {
1028
880
  fileSize,
1029
881
  dateCreated: formatDate(document?.created_at || document?.createdAt),
1030
882
  dateModified: formatDate(document?.updated_at || document?.updatedAt),
1031
- expiryDate:
1032
- document?.type === "folder"
1033
- ? "N/A"
1034
- : formatDateOnly(document?.expiry_date || document?.expiryDate),
1035
883
  remarks: document?.remarks || "N/A",
1036
884
  };
1037
885
  });
@@ -1303,29 +1151,6 @@ function openDeleteDialog() {
1303
1151
  message.value = "";
1304
1152
  }
1305
1153
 
1306
- /*
1307
- * The three row-kebab entry points. Each names its document first: the dialogs
1308
- * below all read `selectedDocument`, which until now was only ever set by
1309
- * opening the detail modal, so firing one straight from a row without setting
1310
- * it would have acted on whichever document was inspected last.
1311
- */
1312
- function openDocumentEdit(document: TDocument) {
1313
- selectedDocument.value = document;
1314
- documentActionMenu.value = {};
1315
- editDialog.value = true;
1316
- }
1317
-
1318
- function openDocumentDelete(document: TDocument) {
1319
- selectedDocument.value = document;
1320
- documentActionMenu.value = {};
1321
- openDeleteDialog();
1322
- }
1323
-
1324
- function openUpdateDialog(document: TDocument) {
1325
- selectedDocument.value = document;
1326
- updateDialog.value = true;
1327
- }
1328
-
1329
1154
  function openEditDialog() {
1330
1155
  editDialog.value = true;
1331
1156
  }
@@ -1354,19 +1179,6 @@ function successUpdate() {
1354
1179
  showMessage("Document updated successfully!", "success");
1355
1180
  }
1356
1181
 
1357
- /**
1358
- * A renewal usually moves the document off the tab it was renewed from, so the
1359
- * refresh is what makes it disappear from the Expired list. A renewal that did
1360
- * not move the expiry date forward stays put, which is the intended behaviour
1361
- * rather than a failed save - hence the neutral wording.
1362
- */
1363
- function successRenew() {
1364
- updateDialog.value = false;
1365
- previewDialog.value = false;
1366
- getDocuments();
1367
- showMessage("Document updated successfully!", "success");
1368
- }
1369
-
1370
1182
  function selectFileType(type: string) {
1371
1183
  selectedFileType.value = type;
1372
1184
  filterMenu.value = false;
@@ -1391,19 +1203,6 @@ function selectFileType(type: string) {
1391
1203
  color: var(--text);
1392
1204
  }
1393
1205
 
1394
- /* The tab strip sits on the table card, so it takes the card's rule rather
1395
- than Vuetify's default underline, and the cell type rather than the button
1396
- scale - the same treatment the toolbar above it already gets. */
1397
- .doc-tabs {
1398
- border-bottom: 1px solid var(--border);
1399
- }
1400
-
1401
- .doc-tabs :deep(.v-tab) {
1402
- font-size: var(--fs-cell);
1403
- letter-spacing: normal;
1404
- min-width: 0;
1405
- }
1406
-
1407
1206
  .table-download-btn {
1408
1207
  min-width: 106px;
1409
1208
  height: 30px;
@@ -482,55 +482,6 @@
482
482
  </draggable>
483
483
  </section>
484
484
 
485
- <!-- RESIDENT CHECKLISTS -->
486
- <v-col cols="12" class="mt-4 border-t">
487
- <v-row align="center" no-gutters class="pt-5">
488
- <v-col cols="9" class="d-flex align-center text-body-2">
489
- <v-row no-gutters>
490
- <v-col cols="12" class="header-style">
491
- Resident Checklists
492
- </v-col>
493
- <v-col cols="12" class="text-body-2 text-medium-emphasis">
494
- Allow residents to view and complete assigned checklists. Each
495
- checklist requires a photo upload before submission.
496
- </v-col>
497
- </v-row>
498
- </v-col>
499
- <v-col cols="3" class="d-flex align-center justify-end">
500
- <v-switch
501
- v-model="facility.isResidentChecklistEnabled"
502
- class="d-flex align-center"
503
- color="accent"
504
- hide-details
505
- @update:model-value="onToggleResidentChecklist"
506
- />
507
- </v-col>
508
- </v-row>
509
-
510
- <!-- SECURITY CHECKLISTS -->
511
- <v-row align="center" no-gutters class="border-t pt-5 mt-5">
512
- <v-col cols="9" class="d-flex align-center text-body-2">
513
- <v-row no-gutters>
514
- <v-col cols="12" class="header-style">
515
- Security Checklists
516
- </v-col>
517
- <v-col cols="12" class="text-body-2 text-medium-emphasis">
518
- Allow Security to view and complete assigned checklists
519
- </v-col>
520
- </v-row>
521
- </v-col>
522
- <v-col cols="3" class="d-flex align-center justify-end">
523
- <v-switch
524
- v-model="facility.isSecurityChecklistEnabled"
525
- class="d-flex align-center"
526
- color="accent"
527
- hide-details
528
- @update:model-value="onToggleSecurityChecklist"
529
- />
530
- </v-col>
531
- </v-row>
532
- </v-col>
533
-
534
485
  <v-col cols="12" class="mt-10">
535
486
  <p class="header-style font-weight-bold mb-5 mt-md-0 ml-md-4">
536
487
  Other Settings
@@ -1069,20 +1020,6 @@ watchEffect(() => {
1069
1020
  }
1070
1021
  });
1071
1022
 
1072
- /**
1073
- * One checklist, one owner, and always one. A facility is completed either by
1074
- * its residents or by Security, so the two switches are each other's inverse:
1075
- * turning either off turns the other on. There is no state where a facility
1076
- * has no checklist owner.
1077
- */
1078
- function onToggleResidentChecklist(enabled: boolean | null) {
1079
- facility.value.isSecurityChecklistEnabled = !enabled;
1080
- }
1081
-
1082
- function onToggleSecurityChecklist(enabled: boolean | null) {
1083
- facility.value.isResidentChecklistEnabled = !enabled;
1084
- }
1085
-
1086
1023
  function onToggleBookingUnitLimit(val: any) {
1087
1024
  if (val) {
1088
1025
  isBookingUnitLimitEnabled.value = true;
@@ -376,7 +376,6 @@ const visitorRecords = ref<Record<string, HidAccessRecord>>({});
376
376
  const logs = ref<HidAccessRecord[]>([]);
377
377
  const liveAccessLogs = ref<HidAccessRecord[]>([]);
378
378
  const administratorUserIds = ref<Set<number>>(new Set());
379
- const { selectHidReader, resolveForSite } = useHidReaderSelection();
380
379
  const selectedReaderId = ref("");
381
380
  const activeTab = ref<AccessTab>("all");
382
381
  const search = ref("");
@@ -402,10 +401,7 @@ const exportForm = reactive({
402
401
  });
403
402
 
404
403
  const statusOptions = ["Authorized", "Not Authorized", "Unknown"];
405
- const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card", "Intercom"];
406
-
407
- /** The persisted event the resident app records when it opens a door on a call. */
408
- const INTERCOM_DOOR_OPEN_EVENT = "intercom_door_open";
404
+ const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card"];
409
405
  const accessMethodOptions = ["All", ...accessMethodFilterOptions];
410
406
 
411
407
  const readerOptions = computed(() =>
@@ -593,11 +589,9 @@ async function loadReaders() {
593
589
  if (!props.site) return;
594
590
  const response = await getReaders({ site: props.site, page: 1, limit: 100 });
595
591
  readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
596
- /* Shared with every other HID screen — see `useHidReaderSelection`. The
597
- resolver refuses a remembered reader that is not this site's, so a site
598
- switch or a deleted reader falls back to the first rather than querying
599
- an id the site does not own. */
600
- selectedReaderId.value = resolveForSite(props.site, readers.value);
592
+ if (!selectedReaderId.value && readers.value.length) {
593
+ selectedReaderId.value = readers.value[0]._id;
594
+ }
601
595
  }
602
596
 
603
597
  async function loadDashboard() {
@@ -612,7 +606,7 @@ async function loadDashboard() {
612
606
  resetDashboardData();
613
607
  try {
614
608
  const isVisitorTab = activeTab.value === "visitor";
615
- const [logResponse, intercomLogResponse, hidAccessLogResponse, visitorResponse, readerUserResponse] = await Promise.all([
609
+ const [logResponse, hidAccessLogResponse, visitorResponse, readerUserResponse] = await Promise.all([
616
610
  loadDashboardSource(
617
611
  getLogs(selectedReaderId.value, {
618
612
  page: page.value,
@@ -621,20 +615,6 @@ async function loadDashboard() {
621
615
  null,
622
616
  "persisted access logs",
623
617
  ),
624
- // Door opens from the resident app during an intercom call. The reader
625
- // never logs a DTMF door open, so these exist only as persisted events
626
- // and are fetched by type: the general page above is shared with every
627
- // other event the reader emits and is dropped whenever the reader has
628
- // live logs, which it always has.
629
- loadDashboardSource(
630
- getLogs(selectedReaderId.value, {
631
- page: page.value,
632
- limit,
633
- type: INTERCOM_DOOR_OPEN_EVENT,
634
- }),
635
- null,
636
- "intercom door-open logs",
637
- ),
638
618
  loadDashboardSource(
639
619
  getReaderAccessLogs(selectedReaderId.value, {
640
620
  page: page.value,
@@ -701,14 +681,9 @@ async function loadDashboard() {
701
681
  // identity metadata here: it can contain a stale photo from another user.
702
682
  allIdentities.value = readerUsers;
703
683
  identities.value = readerUsers;
704
- const intercomItems = intercomLogResponse?.items ?? intercomLogResponse?.data?.items ?? intercomLogResponse?.data?.logs ?? [];
705
- logs.value = [
706
- ...(liveItems.length
707
- ? []
708
- : logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? []),
709
- // Always kept: the reader has no live counterpart for these.
710
- ...intercomItems,
711
- ];
684
+ logs.value = liveItems.length
685
+ ? []
686
+ : logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
712
687
  liveAccessLogs.value = liveItems;
713
688
  serverPages.value = Number(hidAccessLogResponse?.pages ?? hidAccessLogResponse?.data?.pages ?? 1);
714
689
  serverPageRange.value = String(
@@ -844,8 +819,6 @@ function getAccessMethod(identity: HidAccessRecord, log?: HidAccessRecord) {
844
819
  id_password: "ID and Password",
845
820
  pin: "PIN",
846
821
  card: "Card",
847
- // Set by the API on intercom_door_open events; no reader field implies it.
848
- intercom: "Intercom",
849
822
  };
850
823
  const normalizedMethod = String(payload.normalizedMethod || "");
851
824
  if (normalizedMethodLabels[normalizedMethod]) return normalizedMethodLabels[normalizedMethod];
@@ -860,36 +833,11 @@ function getAccessMethod(identity: HidAccessRecord, log?: HidAccessRecord) {
860
833
  return identity.metadata?.facialData || identity.metadata?.imageTimestamp ? "Facial" : "N/A";
861
834
  }
862
835
 
863
- /**
864
- * One shape for the whole Access Date and Time column: `15:18:43 23/9/2026`.
865
- *
866
- * Reader rows arrive as a number and went through `formatHidEpochDate`; an
867
- * intercom door open never touches the reader, so the API writes it as an ISO
868
- * string and this branch took `toLocaleString` instead - which put
869
- * "9/23/2026, 2:15:29 PM" in the same column, one row below "15:18:43
870
- * 23/9/2026". Both branches now build the same string.
871
- *
872
- * Local parts here, UTC parts there, on purpose: the reader encodes its own
873
- * wall clock as if it were UTC, while an ISO string is a real instant. Reading
874
- * each the way it was written is what makes both columns show the time that was
875
- * on the clock at the door.
876
- */
877
836
  function formatDate(value?: string | number) {
878
837
  if (!value) return "N/A";
879
838
  if (typeof value === "number") return formatHidEpochDate(value);
880
- const date = new Date(value);
881
- if (Number.isNaN(date.getTime())) return "N/A";
882
- return formatClock(
883
- [date.getHours(), date.getMinutes(), date.getSeconds()],
884
- date.getDate(),
885
- date.getMonth() + 1,
886
- date.getFullYear(),
887
- );
888
- }
889
-
890
- function formatClock(time: number[], day: number, month: number, year: number) {
891
- const clock = time.map((part) => String(part).padStart(2, "0")).join(":");
892
- return `${clock} ${day}/${month}/${year}`;
839
+ const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
840
+ return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
893
841
  }
894
842
 
895
843
  function parseDate(value?: string | number) {
@@ -919,12 +867,13 @@ function formatHidEpochDate(value: number) {
919
867
  const date = new Date(value * 1000);
920
868
  if (Number.isNaN(date.getTime())) return "N/A";
921
869
 
922
- return formatClock(
923
- [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
924
- date.getUTCDate(),
925
- date.getUTCMonth() + 1,
926
- date.getUTCFullYear(),
927
- );
870
+ const time = [
871
+ date.getUTCHours(),
872
+ date.getUTCMinutes(),
873
+ date.getUTCSeconds(),
874
+ ].map((part) => String(part).padStart(2, "0")).join(":");
875
+
876
+ return `${time} ${date.getUTCDate()}/${date.getUTCMonth() + 1}/${date.getUTCFullYear()}`;
928
877
  }
929
878
 
930
879
  function openHistoryDialog(row: AccessRow) {
@@ -1599,12 +1548,6 @@ function escapeHtml(value: unknown) {
1599
1548
  .replace(/"/g, "&quot;")
1600
1549
  .replace(/'/g, "&#039;");
1601
1550
  }
1602
-
1603
- /* Publish every change, including the seed above: a screen that had to fall
1604
- back to the first reader should leave the others agreeing with it. */
1605
- watch(selectedReaderId, (value) => {
1606
- if (value) selectHidReader(props.site, value);
1607
- });
1608
1551
  </script>
1609
1552
 
1610
1553
  <style scoped>