@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.
- package/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +46 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- 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
|
-
|
|
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" && !
|
|
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
|
|
1804
|
-
|
|
1805
|
-
|
|
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:
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2068
|
-
|
|
2231
|
+
} catch (error: unknown) {
|
|
2232
|
+
hidQrPreviewError.value = errorConverter(error) || "Unable to print HID QR Code.";
|
|
2069
2233
|
return false;
|
|
2070
2234
|
} finally {
|
|
2071
|
-
|
|
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>
|