@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
|
@@ -721,9 +721,9 @@ const {
|
|
|
721
721
|
_getFeedbacks({
|
|
722
722
|
page: page.value,
|
|
723
723
|
site: route.params.site as string,
|
|
724
|
-
provider: currentUser.value
|
|
725
|
-
...(currentUser.value
|
|
726
|
-
userId: currentUser.value
|
|
724
|
+
provider: currentUser.value?.serviceProvider,
|
|
725
|
+
...(currentUser.value?.type != "site" && {
|
|
726
|
+
userId: currentUser.value?._id,
|
|
727
727
|
}),
|
|
728
728
|
service: "Security",
|
|
729
729
|
dateFrom: moment(startDate.value, "DD/MM/YYYY").startOf("day"),
|
|
@@ -998,9 +998,9 @@ async function handleAccept() {
|
|
|
998
998
|
_id: feedbackData.value._id,
|
|
999
999
|
statusUpdate: {
|
|
1000
1000
|
status: "In-Progress",
|
|
1001
|
-
updatedById: currentUser.value
|
|
1002
|
-
updatedByName: `${currentUser.value
|
|
1003
|
-
assignee: currentUser.value
|
|
1001
|
+
updatedById: currentUser.value?._id,
|
|
1002
|
+
updatedByName: `${currentUser.value?.givenName} ${currentUser.value?.surname}`,
|
|
1003
|
+
assignee: currentUser.value?._id,
|
|
1004
1004
|
provider: currentUser.value?.serviceProvider,
|
|
1005
1005
|
},
|
|
1006
1006
|
};
|
|
@@ -1158,7 +1158,7 @@ async function submit() {
|
|
|
1158
1158
|
isSubmitting.value = true;
|
|
1159
1159
|
|
|
1160
1160
|
const result = await createFeedback({
|
|
1161
|
-
createdBy: currentUser.value
|
|
1161
|
+
createdBy: currentUser.value?._id,
|
|
1162
1162
|
description: _feedback.value.description,
|
|
1163
1163
|
attachments: _feedback.value.attachments,
|
|
1164
1164
|
site: route.params.site as string,
|
|
@@ -327,6 +327,8 @@ type AccessTab = "resident" | "visitor" | "administrator";
|
|
|
327
327
|
type AccessRow = {
|
|
328
328
|
key: string;
|
|
329
329
|
identityKey: string;
|
|
330
|
+
identityType?: string;
|
|
331
|
+
visitorId?: string;
|
|
330
332
|
name: string;
|
|
331
333
|
facialData: string;
|
|
332
334
|
faceImage?: string;
|
|
@@ -351,6 +353,7 @@ const { getVisitors } = useVisitor();
|
|
|
351
353
|
|
|
352
354
|
const readers = ref<Record<string, any>[]>([]);
|
|
353
355
|
const identities = ref<Record<string, any>[]>([]);
|
|
356
|
+
const allIdentities = ref<Record<string, any>[]>([]);
|
|
354
357
|
const visitorRecords = ref<Record<string, any>>({});
|
|
355
358
|
const logs = ref<Record<string, any>[]>([]);
|
|
356
359
|
const liveAccessLogs = ref<Record<string, any>[]>([]);
|
|
@@ -418,8 +421,17 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
418
421
|
if (!selectedHistoryRow.value) return [];
|
|
419
422
|
|
|
420
423
|
const selectedKey = selectedHistoryRow.value.identityKey;
|
|
421
|
-
const
|
|
422
|
-
const
|
|
424
|
+
const selectedIdentity = findIdentityByKey(selectedKey);
|
|
425
|
+
const matchedLogs = logs.value.filter((log) =>
|
|
426
|
+
selectedIdentity
|
|
427
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
428
|
+
: getLogIdentityKey(log) === selectedKey,
|
|
429
|
+
);
|
|
430
|
+
const matchedLiveLogs = liveAccessLogs.value.filter((log) =>
|
|
431
|
+
selectedIdentity
|
|
432
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
433
|
+
: getAccessLogIdentityKey(log) === selectedKey,
|
|
434
|
+
);
|
|
423
435
|
|
|
424
436
|
if (!matchedLogs.length && !matchedLiveLogs.length) {
|
|
425
437
|
return rows.value.filter((row) => row.identityKey === selectedKey);
|
|
@@ -549,28 +561,41 @@ async function loadDashboard() {
|
|
|
549
561
|
const isAdministratorTab = activeTab.value === "administrator";
|
|
550
562
|
const isVisitorTab = activeTab.value === "visitor";
|
|
551
563
|
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
+
loadDashboardSource(
|
|
565
|
+
getIdentities(selectedReaderId.value, {
|
|
566
|
+
page: 1,
|
|
567
|
+
limit: 500,
|
|
568
|
+
}),
|
|
569
|
+
null,
|
|
570
|
+
"identities",
|
|
571
|
+
),
|
|
572
|
+
loadDashboardSource(
|
|
573
|
+
getLogs(selectedReaderId.value, {
|
|
574
|
+
page: 1,
|
|
575
|
+
limit: 500,
|
|
576
|
+
}),
|
|
577
|
+
null,
|
|
578
|
+
"persisted access logs",
|
|
579
|
+
),
|
|
580
|
+
loadDashboardSource(loadLiveAccessLogs(selectedReaderId.value), [], "live access logs"),
|
|
581
|
+
loadDashboardSource(loadReaderConfiguration(selectedReaderId.value), {}, "reader configuration"),
|
|
582
|
+
isAdministratorTab
|
|
583
|
+
? loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles")
|
|
584
|
+
: Promise.resolve(null),
|
|
564
585
|
isVisitorTab && props.org
|
|
565
|
-
?
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
586
|
+
? loadDashboardSource(
|
|
587
|
+
getVisitors({
|
|
588
|
+
org: props.org,
|
|
589
|
+
site: props.site,
|
|
590
|
+
page: 1,
|
|
591
|
+
limit: 500,
|
|
592
|
+
order: "desc",
|
|
593
|
+
}),
|
|
594
|
+
null,
|
|
595
|
+
"visitor records",
|
|
596
|
+
)
|
|
572
597
|
: Promise.resolve(null),
|
|
573
|
-
loadReaderUsers(selectedReaderId.value),
|
|
598
|
+
loadDashboardSource(loadReaderUsers(selectedReaderId.value), [], "reader users"),
|
|
574
599
|
]);
|
|
575
600
|
|
|
576
601
|
if (sequence !== loadSequence.value) return;
|
|
@@ -578,18 +603,19 @@ async function loadDashboard() {
|
|
|
578
603
|
const loadedIdentities = identityResponse?.items ?? identityResponse?.data?.items ?? identityResponse?.data?.identities ?? [];
|
|
579
604
|
const mergedIdentities = mergeReaderUsersWithIdentities(readerUserResponse, loadedIdentities);
|
|
580
605
|
const loadedVisitors = visitorResponse?.items ?? visitorResponse?.data?.items ?? visitorResponse?.data?.visitors ?? [];
|
|
581
|
-
visitorRecords.value =
|
|
582
|
-
|
|
583
|
-
|
|
606
|
+
visitorRecords.value = Object.fromEntries(
|
|
607
|
+
loadedVisitors.map((visitor: Record<string, any>) => [String(visitor._id), visitor]),
|
|
608
|
+
);
|
|
584
609
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
610
|
+
allIdentities.value = mergedIdentities;
|
|
585
611
|
identities.value = isAdministratorTab
|
|
586
612
|
? mergedIdentities.filter((identity: Record<string, any>) => {
|
|
587
613
|
const hidUserId = toHidNumericId(identity.hidUserId);
|
|
588
614
|
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
589
615
|
})
|
|
590
|
-
:
|
|
591
|
-
? mergedIdentities.filter(
|
|
592
|
-
|
|
616
|
+
: activeTab.value === "visitor"
|
|
617
|
+
? mergedIdentities.filter(isVisitorIdentity)
|
|
618
|
+
: mergedIdentities.filter((identity: Record<string, any>) => !isVisitorIdentity(identity));
|
|
593
619
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
594
620
|
liveAccessLogs.value = hidAccessLogs;
|
|
595
621
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
@@ -601,6 +627,15 @@ async function loadDashboard() {
|
|
|
601
627
|
}
|
|
602
628
|
}
|
|
603
629
|
|
|
630
|
+
async function loadDashboardSource<T>(request: Promise<T>, fallback: T, label: string): Promise<T> {
|
|
631
|
+
try {
|
|
632
|
+
return await request;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.warn(`Unable to load HID ${label}`, error);
|
|
635
|
+
return fallback;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
604
639
|
async function loadUserImages(items: Record<string, any>[]) {
|
|
605
640
|
if (!selectedReaderId.value) return;
|
|
606
641
|
|
|
@@ -766,6 +801,7 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
766
801
|
function resetDashboardData() {
|
|
767
802
|
selectedHistoryRow.value = undefined;
|
|
768
803
|
identities.value = [];
|
|
804
|
+
allIdentities.value = [];
|
|
769
805
|
visitorRecords.value = {};
|
|
770
806
|
logs.value = [];
|
|
771
807
|
liveAccessLogs.value = [];
|
|
@@ -773,15 +809,11 @@ function resetDashboardData() {
|
|
|
773
809
|
administratorUserIds.value = new Set();
|
|
774
810
|
}
|
|
775
811
|
|
|
776
|
-
function getIdentityType(tab: AccessTab) {
|
|
777
|
-
if (tab === "administrator") return "admin";
|
|
778
|
-
return tab;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
812
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
782
813
|
const identity = findIdentityByKey(row.identityKey);
|
|
814
|
+
const isVisitor = row.identityType === "visitor" || Boolean(row.visitorId) || isVisitorIdentity(identity);
|
|
783
815
|
if (activeTab.value === "visitor") {
|
|
784
|
-
return
|
|
816
|
+
return isVisitor;
|
|
785
817
|
}
|
|
786
818
|
|
|
787
819
|
if (activeTab.value === "administrator") {
|
|
@@ -789,7 +821,11 @@ function isVisibleForActiveTab(row: AccessRow) {
|
|
|
789
821
|
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
790
822
|
}
|
|
791
823
|
|
|
792
|
-
return !
|
|
824
|
+
return !isVisitor;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
828
|
+
return Boolean(identity && (identity.type === "visitor" || identity.visitor));
|
|
793
829
|
}
|
|
794
830
|
|
|
795
831
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -835,13 +871,13 @@ function getIdentityKey(identity: Record<string, any>) {
|
|
|
835
871
|
return [
|
|
836
872
|
identity.hidUserId,
|
|
837
873
|
identity.registration,
|
|
838
|
-
identity.cardNo,
|
|
874
|
+
identity.cardNo || identity.metadata?.qrCodeValue,
|
|
839
875
|
].filter(Boolean).join("|");
|
|
840
876
|
}
|
|
841
877
|
|
|
842
878
|
function findIdentityByKey(identityKey: string) {
|
|
843
879
|
if (!identityKey) return undefined;
|
|
844
|
-
return
|
|
880
|
+
return allIdentities.value.find((identity) => getIdentityKey(identity) === identityKey);
|
|
845
881
|
}
|
|
846
882
|
|
|
847
883
|
function getLogIdentityKey(log: Record<string, any>) {
|
|
@@ -851,7 +887,7 @@ function getLogIdentityKey(log: Record<string, any>) {
|
|
|
851
887
|
return [
|
|
852
888
|
identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
|
|
853
889
|
identity.registration || lookup.registration,
|
|
854
|
-
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
|
|
890
|
+
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
855
891
|
].filter(Boolean).join("|");
|
|
856
892
|
}
|
|
857
893
|
|
|
@@ -1145,12 +1181,15 @@ function getRawAccessLogs(event: Record<string, any>) {
|
|
|
1145
1181
|
|
|
1146
1182
|
function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
|
|
1147
1183
|
const identity = findIdentityForAccessLog(rawLog, event);
|
|
1184
|
+
const eventIdentity = event.payload?.identity || event.payload?.identityLookup || {};
|
|
1148
1185
|
const rawAccessTime = getAccessLogTime(rawLog, event);
|
|
1149
1186
|
const identityKey = identity ? getIdentityKey(identity) : getLogIdentityKey({ ...event, payload: { ...event.payload, rawAccessLog: rawLog } });
|
|
1150
1187
|
|
|
1151
1188
|
return {
|
|
1152
1189
|
key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
|
|
1153
1190
|
identityKey,
|
|
1191
|
+
identityType: String(identity?.type || eventIdentity.type || "").toLowerCase() || undefined,
|
|
1192
|
+
visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
|
|
1154
1193
|
name: identity ? getName(identity) : getRawLogName(rawLog, event),
|
|
1155
1194
|
facialData: identity ? getFacialData(identity) : "N/A",
|
|
1156
1195
|
faceImage: identity ? getUserImageSrc(identity) : "",
|
|
@@ -1166,32 +1205,33 @@ function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, an
|
|
|
1166
1205
|
function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<string, any>) {
|
|
1167
1206
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1168
1207
|
if (eventIdentityId) {
|
|
1169
|
-
const byId =
|
|
1208
|
+
const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
1170
1209
|
if (byId) return byId;
|
|
1171
1210
|
}
|
|
1172
1211
|
|
|
1173
1212
|
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
1174
1213
|
const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
|
|
1175
1214
|
const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
|
|
1215
|
+
const qrCodeValue = rawLog.qrcode_value || rawLog.qrCode || rawLog.qrcode;
|
|
1176
1216
|
|
|
1177
|
-
return
|
|
1217
|
+
return allIdentities.value.find((identity) => {
|
|
1178
1218
|
return (
|
|
1179
1219
|
(hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
|
|
1180
1220
|
(registration && identity.registration === registration) ||
|
|
1181
|
-
(cardNo && String(identity.cardNo) === String(cardNo))
|
|
1221
|
+
(cardNo && String(identity.cardNo) === String(cardNo)) ||
|
|
1222
|
+
(qrCodeValue && String(identity.metadata?.qrCodeValue || "") === String(qrCodeValue))
|
|
1182
1223
|
);
|
|
1183
1224
|
});
|
|
1184
1225
|
}
|
|
1185
1226
|
|
|
1186
1227
|
function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
1187
|
-
const identityKey = getIdentityKey(identity);
|
|
1188
1228
|
const accessLogs = [
|
|
1189
1229
|
...liveAccessLogs.value,
|
|
1190
1230
|
...logs.value.flatMap((event) => getRawAccessLogs(event)),
|
|
1191
1231
|
];
|
|
1192
1232
|
|
|
1193
1233
|
return accessLogs
|
|
1194
|
-
.filter((log) =>
|
|
1234
|
+
.filter((log) => accessLogMatchesIdentity(log, identity))
|
|
1195
1235
|
.sort((a, b) => {
|
|
1196
1236
|
const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
|
|
1197
1237
|
const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
|
|
@@ -1199,11 +1239,46 @@ function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
|
1199
1239
|
})[0];
|
|
1200
1240
|
}
|
|
1201
1241
|
|
|
1242
|
+
function toUnknownRecord(value: unknown): Record<string, unknown> {
|
|
1243
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1244
|
+
? value as Record<string, unknown>
|
|
1245
|
+
: {};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function accessLogMatchesIdentity(log: unknown, identity: unknown) {
|
|
1249
|
+
const logRecord = toUnknownRecord(log);
|
|
1250
|
+
const identityRecord = toUnknownRecord(identity);
|
|
1251
|
+
const payload = toUnknownRecord(logRecord.payload);
|
|
1252
|
+
const rawLog = Object.keys(toUnknownRecord(payload.rawAccessLog)).length
|
|
1253
|
+
? toUnknownRecord(payload.rawAccessLog)
|
|
1254
|
+
: logRecord;
|
|
1255
|
+
const eventIdentity = Object.keys(toUnknownRecord(payload.identity)).length
|
|
1256
|
+
? toUnknownRecord(payload.identity)
|
|
1257
|
+
: toUnknownRecord(payload.identityLookup);
|
|
1258
|
+
const metadata = toUnknownRecord(identityRecord.metadata);
|
|
1259
|
+
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? eventIdentity.hidUserId;
|
|
1260
|
+
const registration = rawLog.registration || eventIdentity.registration;
|
|
1261
|
+
const credential =
|
|
1262
|
+
rawLog.card_value ||
|
|
1263
|
+
rawLog.cardNo ||
|
|
1264
|
+
rawLog.qrcode_value ||
|
|
1265
|
+
rawLog.qrCode ||
|
|
1266
|
+
rawLog.qrcode ||
|
|
1267
|
+
eventIdentity.cardNo;
|
|
1268
|
+
const identityCredential = identityRecord.cardNo || metadata.qrCodeValue;
|
|
1269
|
+
|
|
1270
|
+
return Boolean(
|
|
1271
|
+
(hidUserId !== undefined && hidUserId !== null && String(identityRecord.hidUserId) === String(hidUserId)) ||
|
|
1272
|
+
(registration && identityRecord.registration === registration) ||
|
|
1273
|
+
(credential && identityCredential && String(identityCredential) === String(credential)),
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1202
1277
|
function getAccessLogIdentityKey(rawLog: Record<string, any>) {
|
|
1203
1278
|
return [
|
|
1204
1279
|
rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
|
|
1205
1280
|
rawLog.registration,
|
|
1206
|
-
rawLog.card_value || rawLog.cardNo,
|
|
1281
|
+
rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
1207
1282
|
].filter(Boolean).join("|");
|
|
1208
1283
|
}
|
|
1209
1284
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog
|
|
3
|
+
:model-value="modelValue"
|
|
4
|
+
max-width="520"
|
|
5
|
+
persistent
|
|
6
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
7
|
+
>
|
|
8
|
+
<v-card class="camera-dialog" rounded="lg">
|
|
9
|
+
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
|
10
|
+
<span class="text-subtitle-1 font-weight-bold">Take Facial Photo</span>
|
|
11
|
+
<v-btn
|
|
12
|
+
icon="mdi-close"
|
|
13
|
+
variant="text"
|
|
14
|
+
size="small"
|
|
15
|
+
aria-label="Close camera"
|
|
16
|
+
@click="close"
|
|
17
|
+
/>
|
|
18
|
+
</v-card-title>
|
|
19
|
+
|
|
20
|
+
<v-divider />
|
|
21
|
+
|
|
22
|
+
<v-card-text class="pa-4">
|
|
23
|
+
<div class="camera-viewport">
|
|
24
|
+
<video
|
|
25
|
+
ref="videoElement"
|
|
26
|
+
class="camera-preview"
|
|
27
|
+
autoplay
|
|
28
|
+
muted
|
|
29
|
+
playsinline
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<div v-if="starting" class="camera-state">
|
|
33
|
+
<v-progress-circular indeterminate color="primary" />
|
|
34
|
+
<span class="mt-3">Starting camera...</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div v-else-if="cameraError" class="camera-state px-6 text-center">
|
|
38
|
+
<v-icon icon="mdi-camera-off-outline" size="42" color="error" />
|
|
39
|
+
<span class="mt-3 text-error">{{ cameraError }}</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<canvas ref="canvasElement" class="d-none" />
|
|
44
|
+
|
|
45
|
+
<div class="text-caption text-medium-emphasis mt-3 text-center">
|
|
46
|
+
Keep your face centered and make sure the area is well lit.
|
|
47
|
+
</div>
|
|
48
|
+
</v-card-text>
|
|
49
|
+
|
|
50
|
+
<v-divider />
|
|
51
|
+
|
|
52
|
+
<v-card-actions class="pa-4">
|
|
53
|
+
<v-btn variant="text" @click="close">Cancel</v-btn>
|
|
54
|
+
<v-spacer />
|
|
55
|
+
<v-btn
|
|
56
|
+
color="primary"
|
|
57
|
+
variant="flat"
|
|
58
|
+
prepend-icon="mdi-camera"
|
|
59
|
+
:disabled="starting || !!cameraError || !stream"
|
|
60
|
+
@click="capture"
|
|
61
|
+
>
|
|
62
|
+
Take Photo
|
|
63
|
+
</v-btn>
|
|
64
|
+
</v-card-actions>
|
|
65
|
+
</v-card>
|
|
66
|
+
</v-dialog>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
const props = defineProps<{ modelValue: boolean }>();
|
|
71
|
+
const emit = defineEmits<{
|
|
72
|
+
"update:modelValue": [value: boolean];
|
|
73
|
+
capture: [dataUrl: string];
|
|
74
|
+
}>();
|
|
75
|
+
|
|
76
|
+
const videoElement = ref<HTMLVideoElement | null>(null);
|
|
77
|
+
const canvasElement = ref<HTMLCanvasElement | null>(null);
|
|
78
|
+
const stream = shallowRef<MediaStream | null>(null);
|
|
79
|
+
const starting = ref(false);
|
|
80
|
+
const cameraError = ref("");
|
|
81
|
+
|
|
82
|
+
function stopCamera() {
|
|
83
|
+
stream.value?.getTracks().forEach((track) => track.stop());
|
|
84
|
+
stream.value = null;
|
|
85
|
+
if (videoElement.value) videoElement.value.srcObject = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getCameraErrorMessage(caught: unknown) {
|
|
89
|
+
if (caught instanceof DOMException) {
|
|
90
|
+
if (caught.name === "NotAllowedError") {
|
|
91
|
+
return "Camera permission was denied. Allow camera access and try again.";
|
|
92
|
+
}
|
|
93
|
+
if (caught.name === "NotFoundError") {
|
|
94
|
+
return "No camera was found on this device.";
|
|
95
|
+
}
|
|
96
|
+
if (caught.name === "NotReadableError") {
|
|
97
|
+
return "The camera is currently being used by another application.";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return "Unable to open the camera. Use HTTPS or check the browser camera permission.";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function startCamera() {
|
|
104
|
+
stopCamera();
|
|
105
|
+
cameraError.value = "";
|
|
106
|
+
starting.value = true;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
110
|
+
throw new Error("Camera API is unavailable");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
stream.value = await navigator.mediaDevices.getUserMedia({
|
|
114
|
+
audio: false,
|
|
115
|
+
video: {
|
|
116
|
+
facingMode: "user",
|
|
117
|
+
width: { ideal: 1280 },
|
|
118
|
+
height: { ideal: 960 },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await nextTick();
|
|
123
|
+
if (!videoElement.value) throw new Error("Camera preview is unavailable");
|
|
124
|
+
videoElement.value.srcObject = stream.value;
|
|
125
|
+
await videoElement.value.play();
|
|
126
|
+
} catch (caught: unknown) {
|
|
127
|
+
stopCamera();
|
|
128
|
+
cameraError.value = getCameraErrorMessage(caught);
|
|
129
|
+
} finally {
|
|
130
|
+
starting.value = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function capture() {
|
|
135
|
+
const video = videoElement.value;
|
|
136
|
+
const canvas = canvasElement.value;
|
|
137
|
+
if (!video || !canvas || !video.videoWidth || !video.videoHeight) return;
|
|
138
|
+
|
|
139
|
+
canvas.width = video.videoWidth;
|
|
140
|
+
canvas.height = video.videoHeight;
|
|
141
|
+
const context = canvas.getContext("2d");
|
|
142
|
+
if (!context) return;
|
|
143
|
+
|
|
144
|
+
context.translate(canvas.width, 0);
|
|
145
|
+
context.scale(-1, 1);
|
|
146
|
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
147
|
+
|
|
148
|
+
emit("capture", canvas.toDataURL("image/jpeg", 0.9));
|
|
149
|
+
close();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function close() {
|
|
153
|
+
stopCamera();
|
|
154
|
+
emit("update:modelValue", false);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
watch(
|
|
158
|
+
() => props.modelValue,
|
|
159
|
+
(isOpen) => {
|
|
160
|
+
if (isOpen) void startCamera();
|
|
161
|
+
else stopCamera();
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
onBeforeUnmount(stopCamera);
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<style scoped>
|
|
169
|
+
.camera-dialog {
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.camera-viewport {
|
|
174
|
+
position: relative;
|
|
175
|
+
width: 100%;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
aspect-ratio: 3 / 4;
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
background: #111722;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.camera-preview {
|
|
183
|
+
width: 100%;
|
|
184
|
+
height: 100%;
|
|
185
|
+
object-fit: cover;
|
|
186
|
+
transform: scaleX(-1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.camera-state {
|
|
190
|
+
position: absolute;
|
|
191
|
+
inset: 0;
|
|
192
|
+
display: flex;
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
align-items: center;
|
|
195
|
+
justify-content: center;
|
|
196
|
+
background: #111722;
|
|
197
|
+
color: #ffffff;
|
|
198
|
+
}
|
|
199
|
+
</style>
|