@7365admin1/layer-common 3.2.1 → 3.2.2-staging.77
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/service-provider-invitation-actions.md +28 -0
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/DashboardMain.vue +84 -88
- package/components/DashboardPanel.vue +1 -1
- package/components/EntryPassInformation.vue +4 -4
- package/components/FeedbackMain.vue +7 -7
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +808 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +392 -81
- package/components/Layout/NavigationDrawer.vue +43 -2
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +6 -2
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +258 -14
- package/components/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +2 -3
- package/components/VisitorSocketPopUp.vue +56 -2
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +311 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +3 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +37 -9
- package/test/visitor-socket.test.mjs +36 -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/theme.test.ts +216 -0
- package/utils/theme.ts +163 -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,79 @@
|
|
|
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 :src="hidQrPreview.imageUrl" width="280" height="280" alt="Visitor access QR code" />
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<div class="text-h6 font-weight-bold">{{ visitor.name }}</div>
|
|
522
|
+
<div v-if="hidQrPreview.expiresAt" class="text-caption text-medium-emphasis mt-1">
|
|
523
|
+
Valid until {{ formatHidQrExpiry(hidQrPreview.expiresAt) }}
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<v-alert v-if="hidQrPreviewError" type="error" variant="tonal" density="compact" class="mt-4 text-left">
|
|
527
|
+
{{ hidQrPreviewError }}
|
|
528
|
+
</v-alert>
|
|
529
|
+
</v-card-text>
|
|
530
|
+
|
|
531
|
+
<v-divider />
|
|
532
|
+
<v-card-actions class="pa-0">
|
|
533
|
+
<v-btn
|
|
534
|
+
variant="text"
|
|
535
|
+
height="52"
|
|
536
|
+
class="flex-grow-1 text-none"
|
|
537
|
+
prepend-icon="mdi-download"
|
|
538
|
+
@click="downloadHidQrCode"
|
|
539
|
+
>
|
|
540
|
+
Download
|
|
541
|
+
</v-btn>
|
|
542
|
+
<v-btn
|
|
543
|
+
v-if="hasHidQrPrinterConfig"
|
|
544
|
+
variant="flat"
|
|
545
|
+
color="primary-button"
|
|
546
|
+
height="52"
|
|
547
|
+
class="flex-grow-1 text-none"
|
|
548
|
+
prepend-icon="mdi-printer"
|
|
549
|
+
:loading="hidQrPrinting"
|
|
550
|
+
@click="printHidQrCodePass"
|
|
551
|
+
>
|
|
552
|
+
Print
|
|
553
|
+
</v-btn>
|
|
554
|
+
<v-btn
|
|
555
|
+
v-else
|
|
556
|
+
variant="flat"
|
|
557
|
+
color="primary-button"
|
|
558
|
+
height="52"
|
|
559
|
+
class="flex-grow-1 text-none"
|
|
560
|
+
@click="closeHidQrPreview"
|
|
561
|
+
>
|
|
562
|
+
Close
|
|
563
|
+
</v-btn>
|
|
564
|
+
</v-card-actions>
|
|
565
|
+
</v-card>
|
|
566
|
+
</v-dialog>
|
|
499
567
|
</v-card>
|
|
500
568
|
</template>
|
|
501
569
|
|
|
502
570
|
<script setup lang="ts">
|
|
571
|
+
import QRCode from "qrcode";
|
|
503
572
|
import { propsFactory } from "vuetify/lib/util/propsFactory.mjs";
|
|
504
573
|
import useBuildingUnit from "../composables/useBuildingUnit";
|
|
505
574
|
import useWebUsb from "../composables/useWebUsb";
|
|
@@ -511,6 +580,7 @@ import useVisitor from "../composables/useVisitor";
|
|
|
511
580
|
import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
|
|
512
581
|
import useAccessManagement from "../composables/useAccessManagement";
|
|
513
582
|
import usePeople from "../composables/usePeople";
|
|
583
|
+
import useHidAmico from "../composables/useHidAmico";
|
|
514
584
|
|
|
515
585
|
const prop = defineProps({
|
|
516
586
|
type: {
|
|
@@ -562,6 +632,7 @@ const {
|
|
|
562
632
|
const { getBySiteId: getEntryPassSettingsBySiteId } =
|
|
563
633
|
useSiteEntryPassSettings();
|
|
564
634
|
const { createVisitorPass, signQr } = useAccessManagement();
|
|
635
|
+
const { issueVisitorQr } = useHidAmico();
|
|
565
636
|
const { testConnection } = useWebUsb();
|
|
566
637
|
const {
|
|
567
638
|
findPersonByNRIC,
|
|
@@ -610,6 +681,15 @@ const hidQrPrinted = ref(false);
|
|
|
610
681
|
const isPhoneFieldFocused = ref(false);
|
|
611
682
|
const isPlateNumberFieldFocused = ref(false);
|
|
612
683
|
const isNricFieldFocused = ref(false);
|
|
684
|
+
const hidQrPrinting = ref(false);
|
|
685
|
+
const hidQrPreviewDialog = ref(false);
|
|
686
|
+
const hidQrPreviewError = ref("");
|
|
687
|
+
const hidQrPendingCompletion = ref<"done" | "done:more" | null>(null);
|
|
688
|
+
const hidQrPreview = reactive({
|
|
689
|
+
imageUrl: "",
|
|
690
|
+
issuedAt: "",
|
|
691
|
+
expiresAt: "",
|
|
692
|
+
});
|
|
613
693
|
|
|
614
694
|
const registeredUnitCompanyName = ref("N/A");
|
|
615
695
|
|
|
@@ -710,7 +790,8 @@ const requireNRIC = computed(() => {
|
|
|
710
790
|
});
|
|
711
791
|
|
|
712
792
|
const hidQrCodePassConfig = computed(() => {
|
|
713
|
-
|
|
793
|
+
const site = siteData.value as TSite | null;
|
|
794
|
+
return site?.metadata?.hidQrCodePass;
|
|
714
795
|
});
|
|
715
796
|
|
|
716
797
|
const hidQrCodePassEnabled = computed(() => {
|
|
@@ -736,6 +817,12 @@ const hidQrValidityMinutes = computed(() => {
|
|
|
736
817
|
return Number.isFinite(value) && value > 0 ? value : 60;
|
|
737
818
|
});
|
|
738
819
|
|
|
820
|
+
const hidQrReaderId = computed(() => String(hidQrCodePassConfig.value?.readerId || ""));
|
|
821
|
+
const hidQrFormat = computed<"0" | "1" | "2">(() => {
|
|
822
|
+
const value = String(hidQrCodePassConfig.value?.qrFormat || "0");
|
|
823
|
+
return value === "1" || value === "2" ? value : "0";
|
|
824
|
+
});
|
|
825
|
+
|
|
739
826
|
const hidQrCodePassAllowed = computed(() => {
|
|
740
827
|
return (
|
|
741
828
|
hidQrCodePassEnabled.value &&
|
|
@@ -1710,7 +1797,7 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
|
|
|
1710
1797
|
return;
|
|
1711
1798
|
}
|
|
1712
1799
|
|
|
1713
|
-
if (passType.value === "HID_QR" && !
|
|
1800
|
+
if (passType.value === "HID_QR" && !validateHidQrReaderConfig()) return;
|
|
1714
1801
|
if (
|
|
1715
1802
|
prop.mode === "add" &&
|
|
1716
1803
|
!options.skipNonCheckedOutCheck &&
|
|
@@ -1800,11 +1887,10 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
|
|
|
1800
1887
|
|
|
1801
1888
|
if (res) {
|
|
1802
1889
|
if (passType.value === "HID_QR") {
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
if (!printed) return;
|
|
1890
|
+
const previewReady = await prepareHidQrCodePreview(getVisitorIdFromResponse(res));
|
|
1891
|
+
if (!previewReady) return;
|
|
1892
|
+
hidQrPendingCompletion.value = createMore.value ? "done:more" : "done";
|
|
1893
|
+
return;
|
|
1808
1894
|
}
|
|
1809
1895
|
|
|
1810
1896
|
if (
|
|
@@ -1837,12 +1923,9 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
|
|
|
1837
1923
|
});
|
|
1838
1924
|
|
|
1839
1925
|
if (passType.value === "QR") {
|
|
1840
|
-
console.log("[QR Print] passRes:", passRes);
|
|
1841
1926
|
const accessCards: any[] =
|
|
1842
1927
|
(passRes as any)?.data?.accessCards ?? [];
|
|
1843
|
-
console.log("[QR Print] accessCards:", accessCards);
|
|
1844
1928
|
const printer = entryPassSettings.value?.data?.settings?.printer;
|
|
1845
|
-
console.log("[QR Print] printer config:", printer);
|
|
1846
1929
|
if (!printer?.vendorId || !printer?.productId) {
|
|
1847
1930
|
console.warn("[QR Print] Aborted: no printer configured.");
|
|
1848
1931
|
} else if (!accessCards.length) {
|
|
@@ -1863,31 +1946,14 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
|
|
|
1863
1946
|
const address = [blockLabel, levelLabel, unitLabel]
|
|
1864
1947
|
.filter(Boolean)
|
|
1865
1948
|
.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
1949
|
|
|
1877
1950
|
await nextTick();
|
|
1878
1951
|
for (const qrCode of accessCards) {
|
|
1879
|
-
console.log("[QR Print] Signing QR for card:", qrCode._id);
|
|
1880
1952
|
const signed = await signQr({
|
|
1881
1953
|
cardId: qrCode._id,
|
|
1882
1954
|
purpose: "vms",
|
|
1883
1955
|
});
|
|
1884
1956
|
const qrData = signed?.data;
|
|
1885
|
-
console.log(
|
|
1886
|
-
"[QR Print] signQr result:",
|
|
1887
|
-
signed,
|
|
1888
|
-
"| qrData:",
|
|
1889
|
-
qrData
|
|
1890
|
-
);
|
|
1891
1957
|
if (!qrData) {
|
|
1892
1958
|
console.warn(
|
|
1893
1959
|
"[QR Print] Skipping card",
|
|
@@ -1947,17 +2013,23 @@ function parseUsbId(value: unknown) {
|
|
|
1947
2013
|
: parseInt(text, 10);
|
|
1948
2014
|
}
|
|
1949
2015
|
|
|
1950
|
-
function getVisitorIdFromResponse(response:
|
|
2016
|
+
function getVisitorIdFromResponse(response: unknown) {
|
|
1951
2017
|
if (typeof response === "string") return response;
|
|
2018
|
+
if (!response || typeof response !== "object") return "";
|
|
2019
|
+
|
|
2020
|
+
const result = response as Record<string, unknown>;
|
|
2021
|
+
const nested = result.data && typeof result.data === "object"
|
|
2022
|
+
? result.data as Record<string, unknown>
|
|
2023
|
+
: {};
|
|
1952
2024
|
return (
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2025
|
+
result._id ||
|
|
2026
|
+
result.id ||
|
|
2027
|
+
nested._id ||
|
|
2028
|
+
nested.id ||
|
|
2029
|
+
result.insertedId ||
|
|
2030
|
+
nested.insertedId ||
|
|
1959
2031
|
""
|
|
1960
|
-
);
|
|
2032
|
+
).toString();
|
|
1961
2033
|
}
|
|
1962
2034
|
|
|
1963
2035
|
function getHidQrPrinterUsbIds() {
|
|
@@ -1966,7 +2038,19 @@ function getHidQrPrinterUsbIds() {
|
|
|
1966
2038
|
return { vendorId, productId };
|
|
1967
2039
|
}
|
|
1968
2040
|
|
|
2041
|
+
function validateHidQrReaderConfig() {
|
|
2042
|
+
if (!hidQrReaderId.value) {
|
|
2043
|
+
errorMessage.value =
|
|
2044
|
+
"No HID reader is configured for QR Code Pass. Please select a reader in Settings.";
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1969
2051
|
function validateHidQrPrinterConfig() {
|
|
2052
|
+
if (!validateHidQrReaderConfig()) return false;
|
|
2053
|
+
|
|
1970
2054
|
if (!hasHidQrPrinterConfig.value) {
|
|
1971
2055
|
errorMessage.value =
|
|
1972
2056
|
"No printer is configured on HID settings. Please configure the HID QR Code printer before continuing.";
|
|
@@ -1984,42 +2068,6 @@ function validateHidQrPrinterConfig() {
|
|
|
1984
2068
|
return true;
|
|
1985
2069
|
}
|
|
1986
2070
|
|
|
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
2071
|
function buildVisitorAddress() {
|
|
2024
2072
|
const blockLabel =
|
|
2025
2073
|
blocksArray.value.find((b: any) => b.value === visitor.block)?.title || "";
|
|
@@ -2032,20 +2080,58 @@ function buildVisitorAddress() {
|
|
|
2032
2080
|
return [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
|
|
2033
2081
|
}
|
|
2034
2082
|
|
|
2035
|
-
async function
|
|
2083
|
+
async function prepareHidQrCodePreview(visitorId: string) {
|
|
2084
|
+
hidQrPreviewError.value = "";
|
|
2085
|
+
if (!visitorId) {
|
|
2086
|
+
errorMessage.value = "Unable to identify the visitor for HID QR Code enrollment.";
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
try {
|
|
2091
|
+
const response = await issueVisitorQr(hidQrReaderId.value, {
|
|
2092
|
+
visitorId,
|
|
2093
|
+
validityMinutes: hidQrValidityMinutes.value,
|
|
2094
|
+
qrFormat: hidQrFormat.value,
|
|
2095
|
+
});
|
|
2096
|
+
const credential = "data" in response ? response.data : response;
|
|
2097
|
+
hidQrPrintPayload.value = credential.qrValue;
|
|
2098
|
+
if (!hidQrPrintPayload.value) {
|
|
2099
|
+
errorMessage.value = "HID QR Code enrollment did not return a QR value.";
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
hidQrPreview.imageUrl = await QRCode.toDataURL(hidQrPrintPayload.value, {
|
|
2104
|
+
width: 360,
|
|
2105
|
+
margin: 2,
|
|
2106
|
+
errorCorrectionLevel: "M",
|
|
2107
|
+
});
|
|
2108
|
+
hidQrPreview.issuedAt = credential.issuedAt || "";
|
|
2109
|
+
hidQrPreview.expiresAt = credential.expiresAt || "";
|
|
2110
|
+
hidQrPreviewDialog.value = true;
|
|
2111
|
+
return true;
|
|
2112
|
+
} catch (error: unknown) {
|
|
2113
|
+
errorMessage.value = errorConverter(error) || "Unable to generate HID QR Code.";
|
|
2114
|
+
return false;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
async function printHidQrCodePass() {
|
|
2036
2119
|
if (hidQrPrinted.value) return true;
|
|
2037
2120
|
|
|
2038
2121
|
if (!validateHidQrPrinterConfig()) return false;
|
|
2039
2122
|
|
|
2040
2123
|
const { vendorId, productId } = getHidQrPrinterUsbIds();
|
|
2041
2124
|
|
|
2042
|
-
|
|
2125
|
+
hidQrPrinting.value = true;
|
|
2126
|
+
hidQrPreviewError.value = "";
|
|
2043
2127
|
try {
|
|
2128
|
+
if (!hidQrPrintPayload.value) return false;
|
|
2129
|
+
|
|
2044
2130
|
const printResult = await testConnection(
|
|
2045
2131
|
vendorId,
|
|
2046
2132
|
productId,
|
|
2047
2133
|
true,
|
|
2048
|
-
|
|
2134
|
+
hidQrPrintPayload.value,
|
|
2049
2135
|
null,
|
|
2050
2136
|
null,
|
|
2051
2137
|
visitor.company || visitor.name || "",
|
|
@@ -2055,7 +2141,7 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
|
|
|
2055
2141
|
);
|
|
2056
2142
|
|
|
2057
2143
|
if (!printResult?.success || printResult?.printTest?.success === false) {
|
|
2058
|
-
|
|
2144
|
+
hidQrPreviewError.value =
|
|
2059
2145
|
printResult?.printTest?.error ||
|
|
2060
2146
|
printResult?.error ||
|
|
2061
2147
|
"Unable to print HID QR Code. Please check the printer connection.";
|
|
@@ -2064,14 +2150,40 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
|
|
|
2064
2150
|
|
|
2065
2151
|
hidQrPrinted.value = true;
|
|
2066
2152
|
return true;
|
|
2067
|
-
} catch (error:
|
|
2068
|
-
|
|
2153
|
+
} catch (error: unknown) {
|
|
2154
|
+
hidQrPreviewError.value = errorConverter(error) || "Unable to print HID QR Code.";
|
|
2069
2155
|
return false;
|
|
2070
2156
|
} finally {
|
|
2071
|
-
|
|
2157
|
+
hidQrPrinting.value = false;
|
|
2072
2158
|
}
|
|
2073
2159
|
}
|
|
2074
2160
|
|
|
2161
|
+
function formatHidQrExpiry(value: string) {
|
|
2162
|
+
const date = new Date(value);
|
|
2163
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function downloadHidQrCode() {
|
|
2167
|
+
if (!hidQrPreview.imageUrl) return;
|
|
2168
|
+
const safeName = String(visitor.name || "visitor")
|
|
2169
|
+
.trim()
|
|
2170
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
2171
|
+
.replace(/^-|-$/g, "")
|
|
2172
|
+
.toLowerCase();
|
|
2173
|
+
const link = document.createElement("a");
|
|
2174
|
+
link.href = hidQrPreview.imageUrl;
|
|
2175
|
+
link.download = `hid-visitor-qr-${safeName || "visitor"}.png`;
|
|
2176
|
+
link.click();
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function closeHidQrPreview() {
|
|
2180
|
+
hidQrPreviewDialog.value = false;
|
|
2181
|
+
const completion = hidQrPendingCompletion.value;
|
|
2182
|
+
hidQrPendingCompletion.value = null;
|
|
2183
|
+
if (completion === "done:more") emit("done:more");
|
|
2184
|
+
else if (completion === "done") emit("done");
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2075
2187
|
watch(
|
|
2076
2188
|
() => visitor.plateNumber,
|
|
2077
2189
|
(newVal) => {
|
|
@@ -2212,4 +2324,15 @@ onMounted(() => {
|
|
|
2212
2324
|
font-size: 0.95rem;
|
|
2213
2325
|
opacity: 1;
|
|
2214
2326
|
}
|
|
2327
|
+
|
|
2328
|
+
.hid-qr-preview-card {
|
|
2329
|
+
border-radius: 6px;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
.hid-qr-image-shell {
|
|
2333
|
+
display: inline-flex;
|
|
2334
|
+
padding: 12px;
|
|
2335
|
+
border: 1px solid #dce3e8;
|
|
2336
|
+
background: #ffffff;
|
|
2337
|
+
}
|
|
2215
2338
|
</style>
|
|
@@ -588,7 +588,7 @@
|
|
|
588
588
|
:org="orgId"
|
|
589
589
|
:site="siteId"
|
|
590
590
|
:visitor-data="selectedVisitorDataObject"
|
|
591
|
-
:current-user="currentUser
|
|
591
|
+
:current-user="currentUser?._id"
|
|
592
592
|
:type="activeVisitorFormType"
|
|
593
593
|
:enable-unregistered="enableUnregistered"
|
|
594
594
|
@back="handleClickBack"
|
|
@@ -1601,7 +1601,6 @@ const {
|
|
|
1601
1601
|
watch(getVisitorReq, (newData: any) => {
|
|
1602
1602
|
if (newData) {
|
|
1603
1603
|
items.value = newData.items ?? [];
|
|
1604
|
-
// items.value = [{ _id: "testid", name: "John Doe", type: "Contractor", company: "ABC Corp", location: "Block A, Level 1, Unit 101", contact: "12345678", plateNumber: "SGX1234A", checkIn: new Date().toISOString(), checkOut: null, status: "registered", visitorPass: [{ _id: "1", keyId: "pass1", prefixAndName: "VIP Pass" }, { _id: "2", keyId: "pass2", prefixAndName: "VIP Pass 2" }], passKeys: [{ _id: "1", keyId: "key2", prefixAndName: "Master Key" }] }];
|
|
1605
1604
|
pages.value = newData.pages ?? 0;
|
|
1606
1605
|
pageRange.value = newData?.pageRange ?? "-- - -- of --";
|
|
1607
1606
|
}
|
|
@@ -1708,7 +1707,7 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
|
|
|
1708
1707
|
site: visitorData.site ?? siteId,
|
|
1709
1708
|
},
|
|
1710
1709
|
update: {
|
|
1711
|
-
updatedBy: currentUser.value
|
|
1710
|
+
updatedBy: currentUser.value?._id,
|
|
1712
1711
|
visitorPass: visitorPass.find(
|
|
1713
1712
|
(i: Record<string, any>) => i.keyId === null
|
|
1714
1713
|
)
|
|
@@ -66,6 +66,29 @@
|
|
|
66
66
|
</template>
|
|
67
67
|
</v-snackbar>
|
|
68
68
|
|
|
69
|
+
<!--
|
|
70
|
+
Camera health is its own alert, deliberately separate from the plate
|
|
71
|
+
snackbar above: it is not a visitor event, it must not be silenced by the
|
|
72
|
+
unregistered-visitor switch, and it must not be hidden on the unregistered
|
|
73
|
+
tab. Several failing cameras collapse into one line rather than one
|
|
74
|
+
snackbar each.
|
|
75
|
+
-->
|
|
76
|
+
<v-snackbar
|
|
77
|
+
v-if="canViewVisitor"
|
|
78
|
+
v-model="cameraAlert.modal"
|
|
79
|
+
:color="cameraFaults.size ? 'warning' : 'success'"
|
|
80
|
+
:timeout="10000"
|
|
81
|
+
location="top right"
|
|
82
|
+
class="rounded-xl"
|
|
83
|
+
multi-line
|
|
84
|
+
:z-index="100000000"
|
|
85
|
+
>
|
|
86
|
+
<span class="text-1-4rem">{{ cameraAlertText }}</span>
|
|
87
|
+
<template #actions>
|
|
88
|
+
<v-btn icon="mdi-close" variant="text" @click="cameraAlert.modal = false" />
|
|
89
|
+
</template>
|
|
90
|
+
</v-snackbar>
|
|
91
|
+
|
|
69
92
|
<v-dialog v-model="permanentMessageDialog.modal" max-width="480" persistent>
|
|
70
93
|
<v-card>
|
|
71
94
|
<v-card-title class="text-h6">Notice</v-card-title>
|
|
@@ -132,6 +155,36 @@ const permanentMessageDialog = reactive({
|
|
|
132
155
|
text: "",
|
|
133
156
|
});
|
|
134
157
|
|
|
158
|
+
// Outstanding camera faults for this site, keyed by camera id so a repeat for
|
|
159
|
+
// the same camera replaces its line instead of adding another.
|
|
160
|
+
const cameraFaults = reactive(new Map<string, string>());
|
|
161
|
+
const cameraRecovery = ref("");
|
|
162
|
+
const cameraAlert = reactive({ modal: false });
|
|
163
|
+
|
|
164
|
+
const cameraAlertText = computed(
|
|
165
|
+
() => cameraAlertMessage([...cameraFaults.values()]) || cameraRecovery.value
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const handleCameraHealth = (data: TVisitorSocketData) => {
|
|
169
|
+
// Older API-core builds send the fault text with no `event`, so a bare
|
|
170
|
+
// message is a fault.
|
|
171
|
+
if (data?.event === "camera-recovered") {
|
|
172
|
+
// Without a camera id there is no way to tell which fault cleared.
|
|
173
|
+
if (data?.camera) cameraFaults.delete(data.camera);
|
|
174
|
+
else cameraFaults.clear();
|
|
175
|
+
// Alerts are now infrequent, so the operator is told it came back —
|
|
176
|
+
// otherwise the only signal a camera recovered is silence.
|
|
177
|
+
cameraRecovery.value = cameraFaults.size ? "" : data?.message || "";
|
|
178
|
+
cameraAlert.modal = Boolean(cameraAlertText.value);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!data?.message) return;
|
|
183
|
+
cameraRecovery.value = "";
|
|
184
|
+
cameraFaults.set(data?.camera || data.message, data.message);
|
|
185
|
+
cameraAlert.modal = true;
|
|
186
|
+
};
|
|
187
|
+
|
|
135
188
|
watch(
|
|
136
189
|
() => props.siteId,
|
|
137
190
|
async (siteId) => {
|
|
@@ -233,8 +286,9 @@ const connectSocket = () => {
|
|
|
233
286
|
}
|
|
234
287
|
|
|
235
288
|
|
|
236
|
-
|
|
237
|
-
|
|
289
|
+
// Camera health, not a visitor event: its own alert, its own gate.
|
|
290
|
+
if (data?.message || data?.event) {
|
|
291
|
+
handleCameraHealth(data);
|
|
238
292
|
}
|
|
239
293
|
|
|
240
294
|
if (data?.messagePermanent) {
|
|
@@ -89,13 +89,13 @@ export default function useComment() {
|
|
|
89
89
|
|
|
90
90
|
comments.value = _comments.items
|
|
91
91
|
.map((comment: any) => {
|
|
92
|
-
if (comment.createdBy === currentUser.value
|
|
92
|
+
if (comment.createdBy === currentUser.value?._id) {
|
|
93
93
|
comment.justify = "end";
|
|
94
94
|
} else {
|
|
95
95
|
comment.justify = "start";
|
|
96
96
|
if (
|
|
97
97
|
((Array.isArray(comment.seenBy) &&
|
|
98
|
-
!comment.seenBy.includes(currentUser.value
|
|
98
|
+
!comment.seenBy.includes(currentUser.value?._id)) ||
|
|
99
99
|
!comment?.seenBy) &&
|
|
100
100
|
comment?._id
|
|
101
101
|
) {
|
|
@@ -111,7 +111,7 @@ export default function useComment() {
|
|
|
111
111
|
);
|
|
112
112
|
|
|
113
113
|
if (Array.isArray(updateSeenIds) && updateSeenIds.length > 0) {
|
|
114
|
-
const seenBy = await updateSeenBy(updateSeenIds, currentUser.value
|
|
114
|
+
const seenBy = await updateSeenBy(updateSeenIds, currentUser.value?._id);
|
|
115
115
|
}
|
|
116
116
|
} catch (error) {
|
|
117
117
|
console.log("error :", error);
|