@7365admin1/layer-common 3.2.0 → 3.2.1-staging.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/components/CameraWall.vue +625 -0
  3. package/components/CameraWallTile.vue +333 -0
  4. package/components/EntryPassInformation.vue +4 -4
  5. package/components/HidAccessLogDashboard.vue +53 -8
  6. package/components/HidIntercomManagement.vue +842 -201
  7. package/components/HidQrCodeConfiguration.vue +907 -95
  8. package/components/HidServiceSettingsPanel.vue +20 -1
  9. package/components/HidUserEnrollment.vue +96 -2
  10. package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
  11. package/components/OnlineFormConfigurationForm.vue +620 -224
  12. package/components/OnlineFormFill.vue +36 -8
  13. package/components/OnlineFormsConfiguration.vue +6 -2
  14. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  15. package/components/QrTemplate/PrintDialog.vue +209 -196
  16. package/components/SiteSettings.vue +4 -2
  17. package/components/VisitorForm.vue +201 -78
  18. package/components/VisitorManagement.vue +0 -1
  19. package/composables/useHidAmico.ts +127 -0
  20. package/composables/useHidNavigation.ts +0 -7
  21. package/composables/useOnlineFormPages.ts +2 -0
  22. package/composables/useSipWebPhone.ts +332 -0
  23. package/composables/useSiteSettings.ts +28 -0
  24. package/composables/useWebUsb.ts +127 -37
  25. package/package.json +2 -1
  26. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  27. package/plugins/secure-member.client.ts +21 -56
  28. package/types/site.d.ts +71 -0
  29. package/utils/camera-wall.test.ts +149 -0
  30. package/utils/camera-wall.ts +299 -0
  31. package/components/HidIdentityMapping.vue +0 -975
  32. package/middleware/member.ts +0 -4
  33. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -0,0 +1,333 @@
1
+ <template>
2
+ <div
3
+ ref="root"
4
+ class="vms-tile"
5
+ :class="{ 'vms-tile--dense': dense, 'vms-tile--focused': focused, 'vms-tile--empty': !camera }"
6
+ :tabindex="camera ? 0 : -1"
7
+ :aria-label="camera ? `${camera.name || camera._id}, ${view.label}` : 'Empty position'"
8
+ @click="camera && $emit('focus', camera)"
9
+ @dblclick="camera && toggleFullscreen()"
10
+ @keydown.enter="camera && toggleFullscreen()"
11
+ >
12
+ <!--
13
+ The picture, when there is one to show. This is an embed of the video
14
+ service's own player page: the stored address IS the live view, and there
15
+ is no other transport for live web video today.
16
+ -->
17
+ <iframe
18
+ v-if="camera && view.showPlayer"
19
+ :key="camera.host"
20
+ :src="camera.host"
21
+ class="vms-tile__frame"
22
+ :title="`Live view of ${camera.name || 'camera'}`"
23
+ allowfullscreen
24
+ referrerpolicy="no-referrer"
25
+ @load="onPlayerLoad"
26
+ />
27
+
28
+ <!--
29
+ A tile with nothing to show says so across the whole tile, not in a
30
+ corner. At nine tiles a supervisor is scanning, and a one-line reason
31
+ tucked under a black rectangle is exactly the thing that gets missed.
32
+ -->
33
+ <div v-if="view.detail" class="vms-tile__overlay">
34
+ <p class="vms-tile__reason">{{ view.detail }}</p>
35
+ </div>
36
+
37
+ <!-- Chrome sits OVER the picture, never around it. -->
38
+ <div v-if="camera" class="vms-tile__status">
39
+ <span class="vms-tile__dot" :class="`vms-tile__dot--${view.state}`" />
40
+ <!--
41
+ At nine tiles the badge is the DOT ALONE: a status word beside a 6 px
42
+ dot on a small tile is unreadable at the distance a wall is scanned
43
+ from, and the dot is what is being read anyway. The word returns at
44
+ four tiles and one.
45
+ -->
46
+ <span v-if="!dense" class="vms-tile__state">{{ view.label }}</span>
47
+ </div>
48
+
49
+ <div v-if="camera" class="vms-tile__chrome">
50
+ <!-- The NAME wins the fight for room: it is the tile's entire job. -->
51
+ <span class="vms-tile__name">{{ camera.name || camera._id }}</span>
52
+ </div>
53
+
54
+ <button
55
+ v-if="camera"
56
+ type="button"
57
+ class="vms-tile__expand"
58
+ title="Fill the screen with this camera"
59
+ aria-label="Fill the screen with this camera"
60
+ @click.stop="toggleFullscreen()"
61
+ >
62
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
63
+ <path
64
+ d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width="2"
68
+ stroke-linecap="square"
69
+ />
70
+ </svg>
71
+ </button>
72
+ </div>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { computed, onBeforeUnmount, ref, watch } from "vue";
77
+
78
+ import {
79
+ LIVE_CONNECT_TIMEOUT_MS,
80
+ tileView,
81
+ type TPlayerState,
82
+ type TWallCamera,
83
+ } from "../utils/camera-wall";
84
+
85
+ /**
86
+ * ONE TILE OF THE CAMERA WALL.
87
+ *
88
+ * Everything this component decides comes from `utils/camera-wall.ts`; what is
89
+ * left here is the picture, the chrome over it, and the one thing only a
90
+ * browser can know — whether the player page ever loaded.
91
+ *
92
+ * ## The ceiling worth knowing before "improving" the live badge
93
+ *
94
+ * We can tell when the player PAGE loads. We cannot tell when a FRAME decodes:
95
+ * the page belongs to the video service, not to us, and learning more would
96
+ * mean reaching into another origin's document — which is both blocked by the
97
+ * browser and would break silently the day that page changes. So **"Live" means
98
+ * the player page is up**. If the service is up and the camera behind it is
99
+ * dead, the operator sees that service's own empty player, exactly as they do
100
+ * in the app they use today. Closing that honestly is a change on the video
101
+ * service's side, not here.
102
+ */
103
+
104
+ const props = withDefaults(
105
+ defineProps<{
106
+ camera: TWallCamera | null;
107
+ /** Nine tiles: drop the status word, keep the dot. */
108
+ dense?: boolean;
109
+ /** Browser connectivity. One dead tile and a dead browser look identical otherwise. */
110
+ online?: boolean;
111
+ focused?: boolean;
112
+ }>(),
113
+ { dense: false, online: true, focused: false }
114
+ );
115
+
116
+ defineEmits<{ (e: "focus", camera: TWallCamera): void }>();
117
+
118
+ const root = ref<HTMLElement | null>(null);
119
+ const player = ref<TPlayerState>("loading");
120
+ let timer: ReturnType<typeof setTimeout> | null = null;
121
+
122
+ const view = computed(() =>
123
+ tileView(props.camera, { online: props.online, player: player.value })
124
+ );
125
+
126
+ function onPlayerLoad() {
127
+ player.value = "ready";
128
+ clearTimer();
129
+ }
130
+
131
+ function clearTimer() {
132
+ if (timer) {
133
+ clearTimeout(timer);
134
+ timer = null;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Restart the clock whenever the address changes — a tile reused for a
140
+ * different camera must not inherit the previous one's verdict, in either
141
+ * direction.
142
+ */
143
+ watch(
144
+ () => props.camera?.host,
145
+ (host) => {
146
+ clearTimer();
147
+ player.value = "loading";
148
+ if (!host) return;
149
+ timer = setTimeout(() => {
150
+ if (player.value === "loading") player.value = "timeout";
151
+ }, LIVE_CONNECT_TIMEOUT_MS);
152
+ },
153
+ { immediate: true }
154
+ );
155
+
156
+ onBeforeUnmount(clearTimer);
157
+
158
+ /** The browser's own fullscreen. Nothing to build, and it survives the F11 key. */
159
+ async function toggleFullscreen() {
160
+ const el = root.value;
161
+ if (!el) return;
162
+ try {
163
+ if (document.fullscreenElement) await document.exitFullscreen();
164
+ else await el.requestFullscreen();
165
+ } catch {
166
+ // A browser that refuses fullscreen (permissions policy, an iframe, an old
167
+ // Safari) is not an error worth interrupting a supervisor over — the tile
168
+ // keeps working exactly as it did.
169
+ }
170
+ }
171
+ </script>
172
+
173
+ <style scoped>
174
+ .vms-tile {
175
+ position: relative;
176
+ overflow: hidden;
177
+ background: var(--vms-tile, #101418);
178
+ border-radius: var(--vms-radius, 3px);
179
+ outline: none;
180
+ min-height: 0;
181
+ }
182
+
183
+ .vms-tile--focused {
184
+ box-shadow: inset 0 0 0 2px var(--vms-accent, #5b9be0);
185
+ }
186
+
187
+ .vms-tile:focus-visible {
188
+ box-shadow: inset 0 0 0 2px var(--vms-accent, #5b9be0);
189
+ }
190
+
191
+ .vms-tile--empty {
192
+ background: transparent;
193
+ border: 1px dashed var(--vms-line, #232b36);
194
+ }
195
+
196
+ .vms-tile__frame {
197
+ width: 100%;
198
+ height: 100%;
199
+ border: none;
200
+ display: block;
201
+ background: #000;
202
+ }
203
+
204
+ .vms-tile__overlay {
205
+ position: absolute;
206
+ inset: 0;
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ padding: 10px;
211
+ background: var(--vms-scrim-solid, rgba(7, 9, 12, 0.82));
212
+ pointer-events: none;
213
+ }
214
+
215
+ .vms-tile__reason {
216
+ margin: 0;
217
+ text-align: center;
218
+ color: var(--vms-text-dim, #94a0b0);
219
+ font-size: 12px;
220
+ line-height: 16px;
221
+ max-width: 32ch;
222
+ }
223
+
224
+ .vms-tile--dense .vms-tile__reason {
225
+ font-size: 11px;
226
+ line-height: 15px;
227
+ }
228
+
229
+ .vms-tile__status {
230
+ position: absolute;
231
+ top: 6px;
232
+ right: 6px;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 5px;
236
+ padding: 2px 6px;
237
+ border-radius: var(--vms-radius, 3px);
238
+ background: var(--vms-scrim, rgba(7, 9, 12, 0.62));
239
+ pointer-events: none;
240
+ }
241
+
242
+ .vms-tile__dot {
243
+ width: 7px;
244
+ height: 7px;
245
+ border-radius: 50%;
246
+ background: var(--vms-text-faint, #5c6675);
247
+ flex: none;
248
+ }
249
+
250
+ /* State colours only. The chrome itself stays achromatic on purpose. */
251
+ .vms-tile__dot--live {
252
+ background: #4caf50;
253
+ }
254
+ .vms-tile__dot--connecting {
255
+ background: #fb8c00;
256
+ }
257
+ .vms-tile__dot--no-signal,
258
+ .vms-tile__dot--offline {
259
+ background: #e0241c;
260
+ }
261
+ .vms-tile__dot--unavailable {
262
+ background: var(--vms-text-faint, #5c6675);
263
+ }
264
+
265
+ .vms-tile__state {
266
+ color: var(--vms-text, #e8ecf1);
267
+ font-size: 11px;
268
+ line-height: 14px;
269
+ white-space: nowrap;
270
+ }
271
+
272
+ .vms-tile__chrome {
273
+ position: absolute;
274
+ left: 6px;
275
+ right: 6px;
276
+ bottom: 5px;
277
+ display: flex;
278
+ pointer-events: none;
279
+ }
280
+
281
+ .vms-tile__name {
282
+ color: var(--vms-text, #e8ecf1);
283
+ font-size: 12px;
284
+ line-height: 16px;
285
+ font-weight: 600;
286
+ padding: 1px 6px;
287
+ border-radius: var(--vms-radius, 3px);
288
+ background: var(--vms-scrim, rgba(7, 9, 12, 0.62));
289
+ overflow: hidden;
290
+ text-overflow: ellipsis;
291
+ white-space: nowrap;
292
+ }
293
+
294
+ .vms-tile--dense .vms-tile__name {
295
+ font-size: 11px;
296
+ line-height: 15px;
297
+ }
298
+
299
+ .vms-tile__expand {
300
+ position: absolute;
301
+ top: 6px;
302
+ left: 6px;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ width: 24px;
307
+ height: 24px;
308
+ border-radius: var(--vms-radius, 3px);
309
+ background: var(--vms-scrim, rgba(7, 9, 12, 0.62));
310
+ color: var(--vms-text, #e8ecf1);
311
+ border: none;
312
+ cursor: pointer;
313
+ opacity: 0;
314
+ transition: opacity 120ms ease;
315
+ }
316
+
317
+ /* The control is always THERE; it only stops competing with the picture. */
318
+ .vms-tile:hover .vms-tile__expand,
319
+ .vms-tile:focus-within .vms-tile__expand {
320
+ opacity: 1;
321
+ }
322
+
323
+ .vms-tile__expand:focus-visible {
324
+ opacity: 1;
325
+ outline: 2px solid var(--vms-accent, #5b9be0);
326
+ }
327
+
328
+ /* Fullscreen: the picture takes the whole screen, the chrome stays legible. */
329
+ .vms-tile:fullscreen {
330
+ border-radius: 0;
331
+ background: #000;
332
+ }
333
+ </style>
@@ -168,13 +168,13 @@
168
168
  <template v-if="passType === 'HID_QR'">
169
169
  <v-alert
170
170
  v-if="!hidQrPrinterConfigured"
171
- type="warning"
171
+ type="info"
172
172
  variant="tonal"
173
173
  density="compact"
174
174
  class="mt-3"
175
- icon="mdi-printer-alert"
175
+ icon="mdi-qrcode"
176
176
  >
177
- No printer is configured on HID settings. QR Pass may not print.
177
+ HID QR Code will be generated and shown on screen. You can download it after continuing.
178
178
  </v-alert>
179
179
  <v-alert
180
180
  v-else
@@ -184,7 +184,7 @@
184
184
  class="mt-3"
185
185
  icon="mdi-qrcode-scan"
186
186
  >
187
- HID QR Code will be generated and printed when you continue.
187
+ HID QR Code will be generated and shown on screen. Printing is also available.
188
188
  </v-alert>
189
189
  </template>
190
190
  </template>
@@ -418,8 +418,17 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
418
418
  if (!selectedHistoryRow.value) return [];
419
419
 
420
420
  const selectedKey = selectedHistoryRow.value.identityKey;
421
- const matchedLogs = logs.value.filter((log) => getLogIdentityKey(log) === selectedKey);
422
- const matchedLiveLogs = liveAccessLogs.value.filter((log) => getAccessLogIdentityKey(log) === selectedKey);
421
+ const selectedIdentity = findIdentityByKey(selectedKey);
422
+ const matchedLogs = logs.value.filter((log) =>
423
+ selectedIdentity
424
+ ? accessLogMatchesIdentity(log, selectedIdentity)
425
+ : getLogIdentityKey(log) === selectedKey,
426
+ );
427
+ const matchedLiveLogs = liveAccessLogs.value.filter((log) =>
428
+ selectedIdentity
429
+ ? accessLogMatchesIdentity(log, selectedIdentity)
430
+ : getAccessLogIdentityKey(log) === selectedKey,
431
+ );
423
432
 
424
433
  if (!matchedLogs.length && !matchedLiveLogs.length) {
425
434
  return rows.value.filter((row) => row.identityKey === selectedKey);
@@ -835,7 +844,7 @@ function getIdentityKey(identity: Record<string, any>) {
835
844
  return [
836
845
  identity.hidUserId,
837
846
  identity.registration,
838
- identity.cardNo,
847
+ identity.cardNo || identity.metadata?.qrCodeValue,
839
848
  ].filter(Boolean).join("|");
840
849
  }
841
850
 
@@ -851,7 +860,7 @@ function getLogIdentityKey(log: Record<string, any>) {
851
860
  return [
852
861
  identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
853
862
  identity.registration || lookup.registration,
854
- identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
863
+ identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
855
864
  ].filter(Boolean).join("|");
856
865
  }
857
866
 
@@ -1173,25 +1182,26 @@ function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<str
1173
1182
  const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
1174
1183
  const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
1175
1184
  const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
1185
+ const qrCodeValue = rawLog.qrcode_value || rawLog.qrCode || rawLog.qrcode;
1176
1186
 
1177
1187
  return identities.value.find((identity) => {
1178
1188
  return (
1179
1189
  (hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
1180
1190
  (registration && identity.registration === registration) ||
1181
- (cardNo && String(identity.cardNo) === String(cardNo))
1191
+ (cardNo && String(identity.cardNo) === String(cardNo)) ||
1192
+ (qrCodeValue && String(identity.metadata?.qrCodeValue || "") === String(qrCodeValue))
1182
1193
  );
1183
1194
  });
1184
1195
  }
1185
1196
 
1186
1197
  function getLatestAccessLogForIdentity(identity: Record<string, any>) {
1187
- const identityKey = getIdentityKey(identity);
1188
1198
  const accessLogs = [
1189
1199
  ...liveAccessLogs.value,
1190
1200
  ...logs.value.flatMap((event) => getRawAccessLogs(event)),
1191
1201
  ];
1192
1202
 
1193
1203
  return accessLogs
1194
- .filter((log) => getAccessLogIdentityKey(log) === identityKey)
1204
+ .filter((log) => accessLogMatchesIdentity(log, identity))
1195
1205
  .sort((a, b) => {
1196
1206
  const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
1197
1207
  const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
@@ -1199,11 +1209,46 @@ function getLatestAccessLogForIdentity(identity: Record<string, any>) {
1199
1209
  })[0];
1200
1210
  }
1201
1211
 
1212
+ function toUnknownRecord(value: unknown): Record<string, unknown> {
1213
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1214
+ ? value as Record<string, unknown>
1215
+ : {};
1216
+ }
1217
+
1218
+ function accessLogMatchesIdentity(log: unknown, identity: unknown) {
1219
+ const logRecord = toUnknownRecord(log);
1220
+ const identityRecord = toUnknownRecord(identity);
1221
+ const payload = toUnknownRecord(logRecord.payload);
1222
+ const rawLog = Object.keys(toUnknownRecord(payload.rawAccessLog)).length
1223
+ ? toUnknownRecord(payload.rawAccessLog)
1224
+ : logRecord;
1225
+ const eventIdentity = Object.keys(toUnknownRecord(payload.identity)).length
1226
+ ? toUnknownRecord(payload.identity)
1227
+ : toUnknownRecord(payload.identityLookup);
1228
+ const metadata = toUnknownRecord(identityRecord.metadata);
1229
+ const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? eventIdentity.hidUserId;
1230
+ const registration = rawLog.registration || eventIdentity.registration;
1231
+ const credential =
1232
+ rawLog.card_value ||
1233
+ rawLog.cardNo ||
1234
+ rawLog.qrcode_value ||
1235
+ rawLog.qrCode ||
1236
+ rawLog.qrcode ||
1237
+ eventIdentity.cardNo;
1238
+ const identityCredential = identityRecord.cardNo || metadata.qrCodeValue;
1239
+
1240
+ return Boolean(
1241
+ (hidUserId !== undefined && hidUserId !== null && String(identityRecord.hidUserId) === String(hidUserId)) ||
1242
+ (registration && identityRecord.registration === registration) ||
1243
+ (credential && identityCredential && String(identityCredential) === String(credential)),
1244
+ );
1245
+ }
1246
+
1202
1247
  function getAccessLogIdentityKey(rawLog: Record<string, any>) {
1203
1248
  return [
1204
1249
  rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
1205
1250
  rawLog.registration,
1206
- rawLog.card_value || rawLog.cardNo,
1251
+ rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
1207
1252
  ].filter(Boolean).join("|");
1208
1253
  }
1209
1254