@7365admin1/layer-common 3.2.1 → 3.2.2-staging.100

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 (123) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/.changeset/camera-wall-plain-english.md +60 -0
  3. package/.changeset/camera-wall-selection-and-guidance.md +65 -0
  4. package/.changeset/dark-primary-contrast.md +39 -0
  5. package/.changeset/dashboard-dark-theme-contrast.md +15 -0
  6. package/.changeset/notification-preferences.md +17 -0
  7. package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
  8. package/.changeset/service-provider-invitation-actions.md +28 -0
  9. package/assets/css/primitives.css +680 -0
  10. package/assets/css/screens.css +541 -0
  11. package/assets/css/shell.css +258 -0
  12. package/assets/css/tokens.css +220 -0
  13. package/components/AccessCardQrTagging.vue +1 -1
  14. package/components/AccessManagement.vue +53 -72
  15. package/components/AppButton.vue +52 -0
  16. package/components/AppCard.vue +20 -0
  17. package/components/AppDateField.vue +35 -0
  18. package/components/AppField.vue +46 -0
  19. package/components/AppKpiTile.vue +65 -0
  20. package/components/AppSegmented.vue +38 -0
  21. package/components/AppSelect.vue +203 -0
  22. package/components/AreaChecklistHistoryMain.vue +6 -2
  23. package/components/AreaMain.vue +9 -17
  24. package/components/AttendanceMain.vue +4 -3
  25. package/components/Avatar/Main.vue +54 -2
  26. package/components/BillingMain.vue +26 -30
  27. package/components/BuildingManagement/buildings.vue +27 -38
  28. package/components/BuildingManagement/units.vue +26 -42
  29. package/components/BulletinBoardManagement.vue +23 -10
  30. package/components/CameraWall.vue +1034 -0
  31. package/components/CameraWallTile.vue +818 -0
  32. package/components/CardToolbar.vue +37 -0
  33. package/components/CleaningScheduleMain.vue +29 -46
  34. package/components/ClientMain.vue +90 -78
  35. package/components/DashboardEmptyState.vue +11 -1
  36. package/components/DashboardMain.vue +349 -275
  37. package/components/DashboardPanel.vue +1 -1
  38. package/components/DocumentManagement.vue +129 -93
  39. package/components/EntryPassInformation.vue +4 -4
  40. package/components/EntryPassMain.vue +49 -27
  41. package/components/EquipmentItemMain.vue +4 -6
  42. package/components/EquipmentManagementMain.vue +9 -7
  43. package/components/FeedbackMain.vue +42 -56
  44. package/components/FormDialog.vue +60 -21
  45. package/components/HidAccessLogDashboard.vue +119 -44
  46. package/components/HidCameraCaptureDialog.vue +199 -0
  47. package/components/HidIntercomManagement.vue +825 -202
  48. package/components/HidQrCodeConfiguration.vue +927 -93
  49. package/components/HidServiceSettingsPanel.vue +20 -1
  50. package/components/HidUserEnrollment.vue +454 -106
  51. package/components/InvitationMain.vue +46 -61
  52. package/components/Layout/Header.vue +289 -37
  53. package/components/Layout/NavigationDrawer.vue +340 -30
  54. package/components/ManageChecklistMain.vue +17 -17
  55. package/components/MemberMain.vue +79 -60
  56. package/components/MyAttendanceMain.vue +3 -5
  57. package/components/NavigationItem.vue +9 -0
  58. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  59. package/components/NotificationSettings.vue +361 -0
  60. package/components/OnlineFormConfigurationForm.vue +620 -224
  61. package/components/OnlineFormFill.vue +36 -8
  62. package/components/OnlineFormsConfiguration.vue +7 -3
  63. package/components/PageHeader.vue +43 -0
  64. package/components/PassInformation.vue +104 -45
  65. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  66. package/components/QrTemplate/PrintDialog.vue +209 -196
  67. package/components/RolePermissionMain.vue +184 -91
  68. package/components/ScanVisitorQRCode.vue +35 -7
  69. package/components/ScheduleAreaMain.vue +15 -49
  70. package/components/ScheduleTaskMain.vue +23 -26
  71. package/components/ScheduleTastTicketMain.vue +18 -43
  72. package/components/ServiceProviderInvitationPrompt.vue +150 -0
  73. package/components/ServiceProviderMain.vue +346 -157
  74. package/components/SiteSettings.vue +25 -10
  75. package/components/SpecificAttr.vue +10 -2
  76. package/components/StatusChip.vue +93 -0
  77. package/components/SwitchContext.vue +33 -1
  78. package/components/TableHygiene.vue +29 -46
  79. package/components/TableList.vue +1 -1
  80. package/components/TableListSecondary.vue +1 -1
  81. package/components/TableMain.vue +161 -77
  82. package/components/TableV3.vue +61 -66
  83. package/components/UnitMain.vue +9 -17
  84. package/components/VehicleManagement.vue +20 -9
  85. package/components/VisitorForm.vue +294 -78
  86. package/components/VisitorManagement.vue +221 -71
  87. package/components/VisitorSocketPopUp.vue +56 -2
  88. package/components/WorkOrder/Main.vue +42 -51
  89. package/composables/useComment.ts +3 -3
  90. package/composables/useHidAmico.ts +151 -0
  91. package/composables/useHidNavigation.ts +0 -7
  92. package/composables/useLocalAuth.ts +9 -36
  93. package/composables/useNotificationPreference.ts +87 -0
  94. package/composables/useOnlineFormPages.ts +2 -0
  95. package/composables/useOrg.ts +1 -1
  96. package/composables/useRole.ts +22 -0
  97. package/composables/useServiceProviderInvitation.ts +145 -0
  98. package/composables/useSipWebPhone.ts +336 -0
  99. package/composables/useSiteSettings.ts +50 -0
  100. package/composables/useThemePreference.ts +63 -0
  101. package/composables/useVisitor.ts +6 -1
  102. package/composables/useVisitorSocket.ts +26 -0
  103. package/composables/useWebUsb.ts +127 -37
  104. package/nuxt.config.ts +31 -0
  105. package/package.json +4 -1
  106. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  107. package/pages/notification-settings.vue +19 -0
  108. package/plugins/secure-member.client.ts +21 -56
  109. package/plugins/vuetify.ts +68 -9
  110. package/test/visitor-socket.test.mjs +36 -0
  111. package/types/org.d.ts +2 -0
  112. package/types/site.d.ts +71 -0
  113. package/utils/camera-wall.test.ts +571 -0
  114. package/utils/camera-wall.ts +926 -0
  115. package/utils/nav-sections.test.ts +190 -0
  116. package/utils/nav-sections.ts +151 -0
  117. package/utils/status.test.ts +77 -0
  118. package/utils/status.ts +90 -0
  119. package/utils/theme.test.ts +534 -0
  120. package/utils/theme.ts +294 -0
  121. package/components/HidIdentityMapping.vue +0 -975
  122. package/middleware/member.ts +0 -4
  123. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -496,10 +496,115 @@
496
496
  </v-card-actions>
497
497
  </v-card>
498
498
  </v-dialog>
499
+
500
+ <v-dialog v-model="hidQrPreviewDialog" max-width="440" persistent>
501
+ <v-card class="hid-qr-preview-card">
502
+ <v-toolbar color="transparent" density="comfortable" class="px-2">
503
+ <v-toolbar-title class="text-subtitle-1 font-weight-bold">
504
+ Visitor QR Code
505
+ </v-toolbar-title>
506
+ <v-btn icon="mdi-close" variant="text" title="Close" @click="closeHidQrPreview" />
507
+ </v-toolbar>
508
+
509
+ <v-card-text class="px-8 pb-6 text-center">
510
+ <div class="text-h6 font-weight-bold">
511
+ {{ hidQrTemplate.header || "Visitor Access Pass" }}
512
+ </div>
513
+ <div class="text-body-2 text-medium-emphasis mt-1">
514
+ {{ hidQrTemplate.subtext || "Scan at the HID reader for entry" }}
515
+ </div>
516
+
517
+ <div class="hid-qr-image-shell my-5">
518
+ <v-img v-if="hidQrVisible" :src="hidQrPreview.imageUrl" width="280" height="280" alt="Visitor access QR code" />
519
+ <button v-else type="button" class="hid-qr-reveal" aria-label="Show QR code" @click="hidQrVisible = true">
520
+ <v-icon icon="mdi-eye-outline" size="42" />
521
+ <span>Click to show QR code</span>
522
+ </button>
523
+ </div>
524
+
525
+ <v-btn
526
+ variant="text"
527
+ density="comfortable"
528
+ class="text-none"
529
+ :prepend-icon="hidQrVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
530
+ @click="hidQrVisible = !hidQrVisible"
531
+ >
532
+ {{ hidQrVisible ? "Hide QR code" : "Show QR code" }}
533
+ </v-btn>
534
+
535
+ <div class="text-h6 font-weight-bold">{{ visitor.name }}</div>
536
+ <div v-if="hidQrPreview.expiresAt" class="text-caption text-medium-emphasis mt-1">
537
+ Valid until {{ formatHidQrExpiry(hidQrPreview.expiresAt) }}
538
+ </div>
539
+ <v-chip
540
+ v-if="hidQrPreview.expiresAt"
541
+ class="mt-2"
542
+ size="small"
543
+ :color="hidQrExpired ? 'error' : 'success'"
544
+ variant="tonal"
545
+ >
546
+ {{ hidQrExpired ? "Expired" : `Expires in ${formatHidQrCountdown(hidQrRemainingSeconds)}` }}
547
+ </v-chip>
548
+
549
+ <v-btn
550
+ v-if="hidQrExpired"
551
+ block
552
+ class="mt-4 text-none"
553
+ color="primary-button"
554
+ variant="flat"
555
+ prepend-icon="mdi-refresh"
556
+ :loading="hidQrRegenerating"
557
+ @click="regenerateHidQrCode"
558
+ >
559
+ Generate QR again
560
+ </v-btn>
561
+
562
+ <v-alert v-if="hidQrPreviewError" type="error" variant="tonal" density="compact" class="mt-4 text-left">
563
+ {{ hidQrPreviewError }}
564
+ </v-alert>
565
+ </v-card-text>
566
+
567
+ <v-divider />
568
+ <v-card-actions class="pa-0">
569
+ <v-btn
570
+ variant="text"
571
+ height="52"
572
+ class="flex-grow-1 text-none"
573
+ prepend-icon="mdi-download"
574
+ @click="downloadHidQrCode"
575
+ >
576
+ Download
577
+ </v-btn>
578
+ <v-btn
579
+ v-if="hasHidQrPrinterConfig"
580
+ variant="flat"
581
+ color="primary-button"
582
+ height="52"
583
+ class="flex-grow-1 text-none"
584
+ prepend-icon="mdi-printer"
585
+ :loading="hidQrPrinting"
586
+ @click="printHidQrCodePass"
587
+ >
588
+ Print
589
+ </v-btn>
590
+ <v-btn
591
+ v-else
592
+ variant="flat"
593
+ color="primary-button"
594
+ height="52"
595
+ class="flex-grow-1 text-none"
596
+ @click="closeHidQrPreview"
597
+ >
598
+ Close
599
+ </v-btn>
600
+ </v-card-actions>
601
+ </v-card>
602
+ </v-dialog>
499
603
  </v-card>
500
604
  </template>
501
605
 
502
606
  <script setup lang="ts">
607
+ import QRCode from "qrcode";
503
608
  import { propsFactory } from "vuetify/lib/util/propsFactory.mjs";
504
609
  import useBuildingUnit from "../composables/useBuildingUnit";
505
610
  import useWebUsb from "../composables/useWebUsb";
@@ -511,6 +616,7 @@ import useVisitor from "../composables/useVisitor";
511
616
  import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
512
617
  import useAccessManagement from "../composables/useAccessManagement";
513
618
  import usePeople from "../composables/usePeople";
619
+ import useHidAmico from "../composables/useHidAmico";
514
620
 
515
621
  const prop = defineProps({
516
622
  type: {
@@ -562,6 +668,7 @@ const {
562
668
  const { getBySiteId: getEntryPassSettingsBySiteId } =
563
669
  useSiteEntryPassSettings();
564
670
  const { createVisitorPass, signQr } = useAccessManagement();
671
+ const { issueVisitorQr } = useHidAmico();
565
672
  const { testConnection } = useWebUsb();
566
673
  const {
567
674
  findPersonByNRIC,
@@ -610,6 +717,38 @@ const hidQrPrinted = ref(false);
610
717
  const isPhoneFieldFocused = ref(false);
611
718
  const isPlateNumberFieldFocused = ref(false);
612
719
  const isNricFieldFocused = ref(false);
720
+ const hidQrPrinting = ref(false);
721
+ const hidQrPreviewDialog = ref(false);
722
+ const hidQrPreviewError = ref("");
723
+ const hidQrVisible = ref(false);
724
+ const hidQrRegenerating = ref(false);
725
+ const hidQrVisitorId = ref("");
726
+ const hidQrNow = ref(Date.now());
727
+ let hidQrTimer: ReturnType<typeof setInterval> | null = null;
728
+ const hidQrPendingCompletion = ref<"done" | "done:more" | null>(null);
729
+ const hidQrPreview = reactive({
730
+ imageUrl: "",
731
+ issuedAt: "",
732
+ expiresAt: "",
733
+ });
734
+
735
+ const hidQrRemainingSeconds = computed(() => {
736
+ const expiry = new Date(hidQrPreview.expiresAt).getTime();
737
+ return Number.isFinite(expiry) ? Math.max(0, Math.ceil((expiry - hidQrNow.value) / 1000)) : 0;
738
+ });
739
+ const hidQrExpired = computed(() => Boolean(hidQrPreview.expiresAt) && hidQrRemainingSeconds.value === 0);
740
+
741
+ watch(hidQrPreviewDialog, (open) => {
742
+ if (hidQrTimer) clearInterval(hidQrTimer);
743
+ hidQrTimer = null;
744
+ if (!open) return;
745
+ hidQrNow.value = Date.now();
746
+ hidQrTimer = setInterval(() => { hidQrNow.value = Date.now(); }, 1000);
747
+ });
748
+
749
+ onBeforeUnmount(() => {
750
+ if (hidQrTimer) clearInterval(hidQrTimer);
751
+ });
613
752
 
614
753
  const registeredUnitCompanyName = ref("N/A");
615
754
 
@@ -710,7 +849,8 @@ const requireNRIC = computed(() => {
710
849
  });
711
850
 
712
851
  const hidQrCodePassConfig = computed(() => {
713
- return (siteData.value as any)?.metadata?.hidQrCodePass || {};
852
+ const site = siteData.value as TSite | null;
853
+ return site?.metadata?.hidQrCodePass;
714
854
  });
715
855
 
716
856
  const hidQrCodePassEnabled = computed(() => {
@@ -736,6 +876,8 @@ const hidQrValidityMinutes = computed(() => {
736
876
  return Number.isFinite(value) && value > 0 ? value : 60;
737
877
  });
738
878
 
879
+ const hidQrReaderId = computed(() => String(hidQrCodePassConfig.value?.readerId || ""));
880
+
739
881
  const hidQrCodePassAllowed = computed(() => {
740
882
  return (
741
883
  hidQrCodePassEnabled.value &&
@@ -1710,7 +1852,7 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1710
1852
  return;
1711
1853
  }
1712
1854
 
1713
- if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
1855
+ if (passType.value === "HID_QR" && !validateHidQrReaderConfig()) return;
1714
1856
  if (
1715
1857
  prop.mode === "add" &&
1716
1858
  !options.skipNonCheckedOutCheck &&
@@ -1800,11 +1942,10 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1800
1942
 
1801
1943
  if (res) {
1802
1944
  if (passType.value === "HID_QR") {
1803
- const printed = await printHidQrCodePass(
1804
- getVisitorIdFromResponse(res),
1805
- false
1806
- );
1807
- if (!printed) return;
1945
+ const previewReady = await prepareHidQrCodePreview(getVisitorIdFromResponse(res));
1946
+ if (!previewReady) return;
1947
+ hidQrPendingCompletion.value = createMore.value ? "done:more" : "done";
1948
+ return;
1808
1949
  }
1809
1950
 
1810
1951
  if (
@@ -1837,12 +1978,9 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1837
1978
  });
1838
1979
 
1839
1980
  if (passType.value === "QR") {
1840
- console.log("[QR Print] passRes:", passRes);
1841
1981
  const accessCards: any[] =
1842
1982
  (passRes as any)?.data?.accessCards ?? [];
1843
- console.log("[QR Print] accessCards:", accessCards);
1844
1983
  const printer = entryPassSettings.value?.data?.settings?.printer;
1845
- console.log("[QR Print] printer config:", printer);
1846
1984
  if (!printer?.vendorId || !printer?.productId) {
1847
1985
  console.warn("[QR Print] Aborted: no printer configured.");
1848
1986
  } else if (!accessCards.length) {
@@ -1863,31 +2001,14 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1863
2001
  const address = [blockLabel, levelLabel, unitLabel]
1864
2002
  .filter(Boolean)
1865
2003
  .join("/");
1866
- console.log(
1867
- "[QR Print] Printing",
1868
- accessCards.length,
1869
- "card(s). vendorId:",
1870
- vendorId,
1871
- "productId:",
1872
- productId,
1873
- "address:",
1874
- address
1875
- );
1876
2004
 
1877
2005
  await nextTick();
1878
2006
  for (const qrCode of accessCards) {
1879
- console.log("[QR Print] Signing QR for card:", qrCode._id);
1880
2007
  const signed = await signQr({
1881
2008
  cardId: qrCode._id,
1882
2009
  purpose: "vms",
1883
2010
  });
1884
2011
  const qrData = signed?.data;
1885
- console.log(
1886
- "[QR Print] signQr result:",
1887
- signed,
1888
- "| qrData:",
1889
- qrData
1890
- );
1891
2012
  if (!qrData) {
1892
2013
  console.warn(
1893
2014
  "[QR Print] Skipping card",
@@ -1947,17 +2068,23 @@ function parseUsbId(value: unknown) {
1947
2068
  : parseInt(text, 10);
1948
2069
  }
1949
2070
 
1950
- function getVisitorIdFromResponse(response: any) {
2071
+ function getVisitorIdFromResponse(response: unknown) {
1951
2072
  if (typeof response === "string") return response;
2073
+ if (!response || typeof response !== "object") return "";
2074
+
2075
+ const result = response as Record<string, unknown>;
2076
+ const nested = result.data && typeof result.data === "object"
2077
+ ? result.data as Record<string, unknown>
2078
+ : {};
1952
2079
  return (
1953
- response?._id ||
1954
- response?.id ||
1955
- response?.data?._id ||
1956
- response?.data?.id ||
1957
- response?.insertedId ||
1958
- response?.data?.insertedId ||
2080
+ result._id ||
2081
+ result.id ||
2082
+ nested._id ||
2083
+ nested.id ||
2084
+ result.insertedId ||
2085
+ nested.insertedId ||
1959
2086
  ""
1960
- );
2087
+ ).toString();
1961
2088
  }
1962
2089
 
1963
2090
  function getHidQrPrinterUsbIds() {
@@ -1966,7 +2093,19 @@ function getHidQrPrinterUsbIds() {
1966
2093
  return { vendorId, productId };
1967
2094
  }
1968
2095
 
2096
+ function validateHidQrReaderConfig() {
2097
+ if (!hidQrReaderId.value) {
2098
+ errorMessage.value =
2099
+ "No HID reader is configured for QR Code Pass. Please select a reader in Settings.";
2100
+ return false;
2101
+ }
2102
+
2103
+ return true;
2104
+ }
2105
+
1969
2106
  function validateHidQrPrinterConfig() {
2107
+ if (!validateHidQrReaderConfig()) return false;
2108
+
1970
2109
  if (!hasHidQrPrinterConfig.value) {
1971
2110
  errorMessage.value =
1972
2111
  "No printer is configured on HID settings. Please configure the HID QR Code printer before continuing.";
@@ -1984,42 +2123,6 @@ function validateHidQrPrinterConfig() {
1984
2123
  return true;
1985
2124
  }
1986
2125
 
1987
- function buildHidQrCodePayload(visitorId = "") {
1988
- if (hidQrPrintPayload.value) return hidQrPrintPayload.value;
1989
-
1990
- const issuedAt = new Date();
1991
- const expiresAt = new Date(
1992
- issuedAt.getTime() + hidQrValidityMinutes.value * 60 * 1000
1993
- );
1994
- const nonce =
1995
- typeof crypto !== "undefined" && "randomUUID" in crypto
1996
- ? crypto.randomUUID()
1997
- : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1998
-
1999
- hidQrPrintPayload.value = JSON.stringify({
2000
- purpose: "hid-amico-visitor-access",
2001
- visitorId,
2002
- site: prop.site,
2003
- org: prop.org,
2004
- visitorType: prop.type,
2005
- name: visitor.name || "",
2006
- company: visitor.company || "",
2007
- contact: visitor.contact || "",
2008
- nric: visitor.nric || "",
2009
- plateNumber: visitor.plateNumber || "",
2010
- block: visitor.block || "",
2011
- level: visitor.level || "",
2012
- unit: visitor.unit || "",
2013
- unitName: visitor.unitName || "",
2014
- address: buildVisitorAddress(),
2015
- issuedAt: issuedAt.toISOString(),
2016
- expiresAt: expiresAt.toISOString(),
2017
- nonce,
2018
- });
2019
-
2020
- return hidQrPrintPayload.value;
2021
- }
2022
-
2023
2126
  function buildVisitorAddress() {
2024
2127
  const blockLabel =
2025
2128
  blocksArray.value.find((b: any) => b.value === visitor.block)?.title || "";
@@ -2032,20 +2135,81 @@ function buildVisitorAddress() {
2032
2135
  return [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
2033
2136
  }
2034
2137
 
2035
- async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
2138
+ async function prepareHidQrCodePreview(visitorId: string) {
2139
+ hidQrPreviewError.value = "";
2140
+ if (!visitorId) {
2141
+ errorMessage.value = "Unable to identify the visitor for HID QR Code enrollment.";
2142
+ return false;
2143
+ }
2144
+
2145
+ try {
2146
+ hidQrVisitorId.value = visitorId;
2147
+ const response = await issueVisitorQr(hidQrReaderId.value, {
2148
+ visitorId,
2149
+ validityMinutes: hidQrValidityMinutes.value,
2150
+ });
2151
+ const credential = "data" in response ? response.data : response;
2152
+ hidQrPrintPayload.value = credential.qrValue;
2153
+ if (!hidQrPrintPayload.value) {
2154
+ errorMessage.value = "HID QR Code enrollment did not return a QR value.";
2155
+ return false;
2156
+ }
2157
+
2158
+ hidQrPreview.imageUrl = await QRCode.toDataURL(hidQrPrintPayload.value, {
2159
+ width: 360,
2160
+ margin: 2,
2161
+ errorCorrectionLevel: "M",
2162
+ });
2163
+ hidQrPreview.issuedAt = credential.issuedAt || "";
2164
+ hidQrPreview.expiresAt = credential.expiresAt || "";
2165
+ hidQrVisible.value = false;
2166
+ hidQrNow.value = Date.now();
2167
+ hidQrPreviewDialog.value = true;
2168
+ return true;
2169
+ } catch (error: unknown) {
2170
+ const message = errorConverter(error) || "Unable to generate HID QR Code.";
2171
+ if (hidQrPreviewDialog.value) hidQrPreviewError.value = message;
2172
+ else errorMessage.value = message;
2173
+ return false;
2174
+ }
2175
+ }
2176
+
2177
+ function formatHidQrCountdown(totalSeconds: number) {
2178
+ const safe = Math.max(0, totalSeconds);
2179
+ const hours = Math.floor(safe / 3600);
2180
+ const minutes = Math.floor((safe % 3600) / 60);
2181
+ const seconds = safe % 60;
2182
+ return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
2183
+ }
2184
+
2185
+ async function regenerateHidQrCode() {
2186
+ if (!hidQrVisitorId.value) return;
2187
+ hidQrRegenerating.value = true;
2188
+ hidQrPreviewError.value = "";
2189
+ try {
2190
+ await prepareHidQrCodePreview(hidQrVisitorId.value);
2191
+ } finally {
2192
+ hidQrRegenerating.value = false;
2193
+ }
2194
+ }
2195
+
2196
+ async function printHidQrCodePass() {
2036
2197
  if (hidQrPrinted.value) return true;
2037
2198
 
2038
2199
  if (!validateHidQrPrinterConfig()) return false;
2039
2200
 
2040
2201
  const { vendorId, productId } = getHidQrPrinterUsbIds();
2041
2202
 
2042
- if (manageProcessing) processing.value = true;
2203
+ hidQrPrinting.value = true;
2204
+ hidQrPreviewError.value = "";
2043
2205
  try {
2206
+ if (!hidQrPrintPayload.value) return false;
2207
+
2044
2208
  const printResult = await testConnection(
2045
2209
  vendorId,
2046
2210
  productId,
2047
2211
  true,
2048
- buildHidQrCodePayload(visitorId),
2212
+ hidQrPrintPayload.value,
2049
2213
  null,
2050
2214
  null,
2051
2215
  visitor.company || visitor.name || "",
@@ -2055,7 +2219,7 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
2055
2219
  );
2056
2220
 
2057
2221
  if (!printResult?.success || printResult?.printTest?.success === false) {
2058
- errorMessage.value =
2222
+ hidQrPreviewError.value =
2059
2223
  printResult?.printTest?.error ||
2060
2224
  printResult?.error ||
2061
2225
  "Unable to print HID QR Code. Please check the printer connection.";
@@ -2064,14 +2228,40 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
2064
2228
 
2065
2229
  hidQrPrinted.value = true;
2066
2230
  return true;
2067
- } catch (error: any) {
2068
- errorMessage.value = error?.message || "Unable to print HID QR Code.";
2231
+ } catch (error: unknown) {
2232
+ hidQrPreviewError.value = errorConverter(error) || "Unable to print HID QR Code.";
2069
2233
  return false;
2070
2234
  } finally {
2071
- if (manageProcessing) processing.value = false;
2235
+ hidQrPrinting.value = false;
2072
2236
  }
2073
2237
  }
2074
2238
 
2239
+ function formatHidQrExpiry(value: string) {
2240
+ const date = new Date(value);
2241
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
2242
+ }
2243
+
2244
+ function downloadHidQrCode() {
2245
+ if (!hidQrPreview.imageUrl) return;
2246
+ const safeName = String(visitor.name || "visitor")
2247
+ .trim()
2248
+ .replace(/[^a-z0-9]+/gi, "-")
2249
+ .replace(/^-|-$/g, "")
2250
+ .toLowerCase();
2251
+ const link = document.createElement("a");
2252
+ link.href = hidQrPreview.imageUrl;
2253
+ link.download = `hid-visitor-qr-${safeName || "visitor"}.png`;
2254
+ link.click();
2255
+ }
2256
+
2257
+ function closeHidQrPreview() {
2258
+ hidQrPreviewDialog.value = false;
2259
+ const completion = hidQrPendingCompletion.value;
2260
+ hidQrPendingCompletion.value = null;
2261
+ if (completion === "done:more") emit("done:more");
2262
+ else if (completion === "done") emit("done");
2263
+ }
2264
+
2075
2265
  watch(
2076
2266
  () => visitor.plateNumber,
2077
2267
  (newVal) => {
@@ -2212,4 +2402,30 @@ onMounted(() => {
2212
2402
  font-size: 0.95rem;
2213
2403
  opacity: 1;
2214
2404
  }
2405
+
2406
+ .hid-qr-preview-card {
2407
+ border-radius: 6px;
2408
+ }
2409
+
2410
+ .hid-qr-image-shell {
2411
+ display: inline-flex;
2412
+ padding: 12px;
2413
+ border: 1px solid #dce3e8;
2414
+ background: #ffffff;
2415
+ }
2416
+
2417
+ .hid-qr-reveal {
2418
+ display: flex;
2419
+ width: 280px;
2420
+ height: 280px;
2421
+ align-items: center;
2422
+ justify-content: center;
2423
+ border: 0;
2424
+ background: #f5f7f9;
2425
+ color: #344054;
2426
+ cursor: pointer;
2427
+ flex-direction: column;
2428
+ gap: 10px;
2429
+ font: inherit;
2430
+ }
2215
2431
  </style>