@7365admin1/layer-common 3.2.0 → 3.2.1-staging.59
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/components/CameraWall.vue +625 -0
- package/components/CameraWallTile.vue +333 -0
- package/components/EntryPassInformation.vue +4 -4
- package/components/HidAccessLogDashboard.vue +53 -8
- package/components/HidIntercomManagement.vue +842 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +96 -2
- 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/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +0 -1
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useSipWebPhone.ts +332 -0
- package/composables/useSiteSettings.ts +28 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +2 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +149 -0
- package/utils/camera-wall.ts +299 -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>
|
|
@@ -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
|
}
|
|
@@ -28,6 +28,46 @@ type HidIdentityPayload = {
|
|
|
28
28
|
metadata?: Record<string, any>;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
type HidVisitorQrData = {
|
|
32
|
+
visitorId: string;
|
|
33
|
+
readerId: string;
|
|
34
|
+
hidUserId: number;
|
|
35
|
+
qrValue: string;
|
|
36
|
+
qrFormat: "0" | "1" | "2";
|
|
37
|
+
issuedAt: string;
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type HidVisitorQrResponse = HidVisitorQrData | { data: HidVisitorQrData };
|
|
42
|
+
|
|
43
|
+
type HidPermissionListResponse = {
|
|
44
|
+
items: THidPermissionCandidate[];
|
|
45
|
+
page: number;
|
|
46
|
+
pages: number;
|
|
47
|
+
pageRange: string;
|
|
48
|
+
limit: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type HidFacialEnrollmentResult = {
|
|
52
|
+
user_id?: number;
|
|
53
|
+
success?: boolean;
|
|
54
|
+
scores?: Record<string, unknown>;
|
|
55
|
+
errors?: Array<{ code?: number; message?: string }>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type HidSipAccountData = {
|
|
59
|
+
orgId: string;
|
|
60
|
+
site: string;
|
|
61
|
+
userId: string;
|
|
62
|
+
extension: string;
|
|
63
|
+
username: string;
|
|
64
|
+
password: string;
|
|
65
|
+
sipAddress: string;
|
|
66
|
+
websocketUrl: string;
|
|
67
|
+
status: "active";
|
|
68
|
+
provisionedAt: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
31
71
|
export default function useHidAmico() {
|
|
32
72
|
const basePath = "/api/access-management/hid";
|
|
33
73
|
|
|
@@ -78,6 +118,16 @@ export default function useHidAmico() {
|
|
|
78
118
|
});
|
|
79
119
|
}
|
|
80
120
|
|
|
121
|
+
function issueVisitorQr(
|
|
122
|
+
readerId: string,
|
|
123
|
+
payload: { visitorId: string; validityMinutes: number; qrFormat: "0" | "1" | "2" },
|
|
124
|
+
) {
|
|
125
|
+
return useNuxtApp().$api<HidVisitorQrResponse>(`${basePath}/readers/${readerId}/visitor-qr`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
body: payload,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
81
131
|
function getReaderConfiguration(readerId: string, payload: Record<string, string[]> = {}) {
|
|
82
132
|
return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/configuration`, {
|
|
83
133
|
method: "POST",
|
|
@@ -112,6 +162,33 @@ export default function useHidAmico() {
|
|
|
112
162
|
});
|
|
113
163
|
}
|
|
114
164
|
|
|
165
|
+
function setUserImage(
|
|
166
|
+
readerId: string,
|
|
167
|
+
hidUserId: string | number,
|
|
168
|
+
image: Blob,
|
|
169
|
+
options: { timestamp?: number; match?: boolean } = {},
|
|
170
|
+
) {
|
|
171
|
+
return useNuxtApp().$api<{ data: HidFacialEnrollmentResult }>(
|
|
172
|
+
`${basePath}/readers/${readerId}/users/${hidUserId}/image`,
|
|
173
|
+
{
|
|
174
|
+
method: "PUT",
|
|
175
|
+
query: {
|
|
176
|
+
...(options.timestamp ? { timestamp: options.timestamp } : {}),
|
|
177
|
+
match: options.match ?? true,
|
|
178
|
+
},
|
|
179
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
180
|
+
body: image,
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function deleteUserImage(readerId: string, hidUserId: string | number) {
|
|
186
|
+
return useNuxtApp().$api<Record<string, unknown>>(
|
|
187
|
+
`${basePath}/readers/${readerId}/users/${hidUserId}/image`,
|
|
188
|
+
{ method: "DELETE" },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
115
192
|
function createIdentity(readerId: string, payload: HidIdentityPayload) {
|
|
116
193
|
return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/identities`, {
|
|
117
194
|
method: "POST",
|
|
@@ -151,6 +228,49 @@ export default function useHidAmico() {
|
|
|
151
228
|
});
|
|
152
229
|
}
|
|
153
230
|
|
|
231
|
+
function getSitePermissions(siteId: string, orgId: string, readerId: string) {
|
|
232
|
+
return useNuxtApp().$api<{ data: THidSitePermissions }>(
|
|
233
|
+
`${basePath}/sites/${siteId}/permissions`,
|
|
234
|
+
{ method: "GET", query: { orgId, readerId } },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ensureSipAccount(siteId: string, orgId: string) {
|
|
239
|
+
return useNuxtApp().$api<{ data: HidSipAccountData }>(
|
|
240
|
+
`${basePath}/sites/${siteId}/intercom/sip-account`,
|
|
241
|
+
{ method: "POST", body: { orgId } },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getPermissionCandidates(
|
|
246
|
+
siteId: string,
|
|
247
|
+
query: {
|
|
248
|
+
orgId: string;
|
|
249
|
+
readerId: string;
|
|
250
|
+
category: THidPermissionCategory | "intercom";
|
|
251
|
+
search?: string;
|
|
252
|
+
page?: number;
|
|
253
|
+
limit?: number;
|
|
254
|
+
},
|
|
255
|
+
) {
|
|
256
|
+
return useNuxtApp().$api<HidPermissionListResponse>(
|
|
257
|
+
`${basePath}/sites/${siteId}/permission-candidates`,
|
|
258
|
+
{ method: "GET", query },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function updateSitePermissions(
|
|
263
|
+
siteId: string,
|
|
264
|
+
orgId: string,
|
|
265
|
+
readerId: string,
|
|
266
|
+
assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>,
|
|
267
|
+
) {
|
|
268
|
+
return useNuxtApp().$api<{ data: THidSitePermissions; message: string }>(
|
|
269
|
+
`${basePath}/sites/${siteId}/permissions`,
|
|
270
|
+
{ method: "PUT", body: { orgId, readerId, assignments } },
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
154
274
|
return {
|
|
155
275
|
getReaders,
|
|
156
276
|
createReader,
|
|
@@ -159,10 +279,13 @@ export default function useHidAmico() {
|
|
|
159
279
|
testReader,
|
|
160
280
|
syncReader,
|
|
161
281
|
runObjectOperation,
|
|
282
|
+
issueVisitorQr,
|
|
162
283
|
getReaderConfiguration,
|
|
163
284
|
setReaderConfiguration,
|
|
164
285
|
getLogs,
|
|
165
286
|
getUserImage,
|
|
287
|
+
setUserImage,
|
|
288
|
+
deleteUserImage,
|
|
166
289
|
getIdentities,
|
|
167
290
|
createIdentity,
|
|
168
291
|
updateIdentity,
|
|
@@ -170,5 +293,9 @@ export default function useHidAmico() {
|
|
|
170
293
|
getIntercomStatus,
|
|
171
294
|
makeIntercomCall,
|
|
172
295
|
finalizeIntercomCall,
|
|
296
|
+
ensureSipAccount,
|
|
297
|
+
getSitePermissions,
|
|
298
|
+
getPermissionCandidates,
|
|
299
|
+
updateSitePermissions,
|
|
173
300
|
};
|
|
174
301
|
}
|
|
@@ -41,13 +41,6 @@ export default function useHidNavigation() {
|
|
|
41
41
|
params: { org, site },
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
|
-
{
|
|
45
|
-
title: "Identity Mapping",
|
|
46
|
-
route: {
|
|
47
|
-
name: "org-site-access-mgmt-identity-mapping",
|
|
48
|
-
params: { org, site },
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
44
|
{
|
|
52
45
|
title: "Access Logs",
|
|
53
46
|
route: {
|
|
@@ -20,6 +20,8 @@ export type FormBlock =
|
|
|
20
20
|
| { type: "checkbox-group"; title?: string; single?: boolean; items: CheckboxItemDef[] }
|
|
21
21
|
| { type: "paragraph"; text: string }
|
|
22
22
|
| { type: "section-header"; text: string }
|
|
23
|
+
| { type: "ordered-list"; title?: string; items: string[] }
|
|
24
|
+
| { type: "unordered-list"; title?: string; items: string[] }
|
|
23
25
|
| { type: "divider" }
|
|
24
26
|
| { type: "signature-row"; label: string; key: string }
|
|
25
27
|
| { type: "acknowledgement"; text: string }
|