@7365admin1/layer-common 3.2.1-staging.72 → 3.2.2

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 (45) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/components/EntryPassInformation.vue +4 -4
  3. package/components/FeedbackMain.vue +7 -7
  4. package/components/HidAccessLogDashboard.vue +44 -119
  5. package/components/HidIdentityMapping.vue +975 -0
  6. package/components/HidIntercomManagement.vue +201 -808
  7. package/components/HidQrCodeConfiguration.vue +95 -907
  8. package/components/HidServiceSettingsPanel.vue +1 -20
  9. package/components/HidUserEnrollment.vue +81 -392
  10. package/components/Layout/NavigationDrawer.vue +2 -43
  11. package/components/NavigationItem.vue +0 -9
  12. package/components/Nfc/NFCPatrolRouteMain.vue +7 -52
  13. package/components/OnlineFormConfigurationForm.vue +224 -620
  14. package/components/OnlineFormFill.vue +8 -36
  15. package/components/OnlineFormsConfiguration.vue +2 -6
  16. package/components/QrTemplate/CreditCardLandscape.vue +12 -19
  17. package/components/QrTemplate/PrintDialog.vue +196 -209
  18. package/components/SiteSettings.vue +2 -4
  19. package/components/VisitorForm.vue +78 -201
  20. package/components/VisitorManagement.vue +2 -1
  21. package/composables/useComment.ts +3 -3
  22. package/composables/useHidAmico.ts +0 -127
  23. package/composables/useHidNavigation.ts +7 -0
  24. package/composables/useOnlineFormPages.ts +0 -2
  25. package/composables/useSiteSettings.ts +0 -50
  26. package/composables/useWebUsb.ts +37 -127
  27. package/middleware/member.ts +4 -0
  28. package/package.json +1 -2
  29. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +23 -0
  30. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  31. package/plugins/secure-member.client.ts +56 -21
  32. package/plugins/vuetify.ts +9 -37
  33. package/types/site.d.ts +0 -71
  34. package/.changeset/camera-wall-gated-endpoint.md +0 -47
  35. package/.changeset/camera-wall-plain-english.md +0 -60
  36. package/.changeset/camera-wall-selection-and-guidance.md +0 -65
  37. package/.changeset/dark-primary-contrast.md +0 -39
  38. package/components/CameraWall.vue +0 -1034
  39. package/components/CameraWallTile.vue +0 -818
  40. package/components/HidCameraCaptureDialog.vue +0 -199
  41. package/composables/useSipWebPhone.ts +0 -311
  42. package/utils/camera-wall.test.ts +0 -571
  43. package/utils/camera-wall.ts +0 -926
  44. package/utils/theme.test.ts +0 -201
  45. package/utils/theme.ts +0 -136
@@ -9,55 +9,65 @@
9
9
  </div>
10
10
 
11
11
  <div class="config-body">
12
- <div class="online-mode-row">
12
+ <div class="section-head">
13
13
  <div>
14
- <h3>Activate Online Mode</h3>
15
- <p>Enables real-time server communication, synchronizing live credential verification and access data across the network.</p>
14
+ <h3>HID QR Code Pass</h3>
15
+ <p>Allow users to generate and scan digital QR codes at compatible HID readers for touchless access control.</p>
16
16
  </div>
17
17
  <v-switch
18
- v-model="form.onlineMode"
19
- class="hid-config-switch"
18
+ v-model="form.enabled"
20
19
  color="success"
21
20
  hide-details
22
21
  inset
23
- :loading="savingSection === 'online-mode'"
24
- :disabled="Boolean(savingSection) || !form.readerId"
25
- @update:model-value="saveOnlineMode"
22
+ :loading="savingSection === 'enabled'"
23
+ @update:model-value="saveSection('enabled')"
24
+ />
25
+ </div>
26
+
27
+ <div class="form-section">
28
+ <div class="section-label">HID Reader</div>
29
+ <v-select
30
+ v-model="form.readerId"
31
+ :items="readerOptions"
32
+ item-title="title"
33
+ item-value="value"
34
+ placeholder="Select HID reader"
35
+ variant="outlined"
36
+ density="compact"
37
+ hide-details
38
+ class="reader-select"
26
39
  />
27
40
  </div>
28
41
 
29
42
  <v-divider />
30
43
 
31
- <div class="form-section identification-section">
32
- <div class="section-label">Enable/disable reader identification method</div>
33
- <p class="section-description">
34
- Toggles multi-factor access methods, allowing users to authenticate via biometric, physical card, or credential inputs.
35
- </p>
36
- <div class="methods-grid">
37
- <div
38
- v-for="method in identificationMethodOptions"
39
- :key="method.key"
40
- class="method-control"
41
- :class="{ 'method-control--disabled': !form.onlineMode }"
44
+ <div class="form-section">
45
+ <div class="section-label">QR Code Format</div>
46
+ <div class="validity-grid">
47
+ <v-select
48
+ v-model="form.qrFormat"
49
+ :items="qrFormatOptions"
50
+ item-title="title"
51
+ item-value="value"
52
+ placeholder="Select format"
53
+ variant="outlined"
54
+ density="compact"
55
+ hide-details
56
+ />
57
+ <v-btn
58
+ class="text-none save-button"
59
+ variant="flat"
60
+ prepend-icon="mdi-qrcode-edit"
61
+ height="40"
62
+ :loading="savingSection === 'qr-format'"
63
+ :disabled="!form.readerId"
64
+ @click="saveSection('qr-format')"
42
65
  >
43
- <span>{{ method.label }}</span>
44
- <v-switch
45
- v-model="form.identificationMethods[method.key]"
46
- class="hid-config-switch"
47
- color="success"
48
- hide-details
49
- inset
50
- density="compact"
51
- :loading="savingSection === `method-${method.key}`"
52
- :disabled="Boolean(savingSection) || !form.onlineMode"
53
- @update:model-value="saveIdentificationMethod(method.key)"
54
- />
55
- </div>
66
+ Save Format
67
+ </v-btn>
56
68
  </div>
57
69
  </div>
58
70
 
59
- <v-divider />
60
-
61
71
  <div class="form-section">
62
72
  <div class="section-label">Printer Setup</div>
63
73
  <div class="printer-grid">
@@ -89,7 +99,6 @@
89
99
  variant="outlined"
90
100
  prepend-icon="mdi-printer-check"
91
101
  height="40"
92
- :loading="testingPrinter"
93
102
  :disabled="!hasPrinterConfig"
94
103
  @click="testPrint"
95
104
  >
@@ -175,88 +184,6 @@
175
184
  </v-btn>
176
185
  </div>
177
186
  </div>
178
-
179
- <v-divider />
180
-
181
- <div v-if="isPermissionManager" class="permission-section">
182
- <div class="permission-location">
183
- <div class="permission-copy">
184
- <div class="section-label">Permission Location</div>
185
- <p class="section-description">
186
- Permissions are scoped to the location configured on the HID Face Reader.
187
- </p>
188
- </div>
189
- <v-select
190
- v-model="permissionReaderId"
191
- class="permission-location__select"
192
- :items="permissionLocations"
193
- item-title="label"
194
- item-value="readerId"
195
- placeholder="Select a Face Reader location"
196
- variant="outlined"
197
- density="compact"
198
- hide-details
199
- :disabled="permissionLocations.length === 0"
200
- @update:model-value="loadPermissions"
201
- />
202
- </div>
203
-
204
- <v-alert
205
- v-if="permissionLocations.length === 0"
206
- class="mb-4"
207
- type="warning"
208
- variant="tonal"
209
- density="compact"
210
- >
211
- Add a Location to a Face Reader before assigning HID permissions.
212
- </v-alert>
213
-
214
- <v-divider />
215
-
216
- <div class="permission-row">
217
- <div class="permission-copy">
218
- <div class="section-label">Access Permission</div>
219
- <p class="section-description">
220
- Select who can use the enabled HID identification methods at this site.
221
- </p>
222
- </div>
223
- <div class="permission-actions">
224
- <v-btn
225
- v-for="option in accessPermissionOptions"
226
- :key="option.category"
227
- class="permission-button text-none"
228
- variant="outlined"
229
- :disabled="!form.onlineMode || !permissionReaderId"
230
- @click="openPermissionDialog(option.category)"
231
- >
232
- <span class="permission-count">{{ permissionCount(option.category) }}</span>
233
- <span>{{ option.label }}</span>
234
- </v-btn>
235
- </div>
236
- </div>
237
-
238
- <v-divider />
239
-
240
- <div class="permission-row permission-row--intercom">
241
- <div class="permission-copy">
242
- <div class="section-label">Access Intercom</div>
243
- <p class="section-description">
244
- Choose which authorized people and providers may use HID intercom access.
245
- </p>
246
- </div>
247
- <div class="permission-actions permission-actions--single">
248
- <v-btn
249
- class="permission-button text-none"
250
- variant="outlined"
251
- :disabled="!form.onlineMode || !permissionReaderId || permissionState.assignments.length === 0"
252
- @click="openPermissionDialog('intercom')"
253
- >
254
- <span class="permission-count">{{ permissionState.counts.intercom }}</span>
255
- <span>Intercom Permissions</span>
256
- </v-btn>
257
- </div>
258
- </div>
259
- </div>
260
187
  </div>
261
188
  </v-card>
262
189
 
@@ -295,158 +222,37 @@
295
222
  </v-card>
296
223
  </v-dialog>
297
224
 
298
- <v-dialog v-model="permissionDialog" max-width="680" persistent>
299
- <v-card class="permission-dialog">
300
- <v-card-title class="permission-dialog__header">
301
- <div>
302
- <div class="permission-dialog__title">{{ permissionDialogTitle }}</div>
303
- <div class="permission-dialog__subtitle">{{ permissionDialogDescription }}</div>
304
- </div>
305
- <v-btn
306
- icon="mdi-close"
307
- variant="text"
308
- density="comfortable"
309
- aria-label="Close permission dialog"
310
- @click="closePermissionDialog"
311
- />
312
- </v-card-title>
313
-
314
- <v-card-text class="permission-dialog__body">
315
- <v-text-field
316
- v-model="permissionSearch"
317
- class="permission-search"
318
- prepend-inner-icon="mdi-magnify"
319
- placeholder="Search by name or details"
320
- variant="outlined"
321
- density="compact"
322
- hide-details
323
- clearable
324
- />
325
-
326
- <div class="permission-list-summary" aria-live="polite">
327
- <span>{{ visiblePermissionCandidates.length }} entries</span>
328
- <span class="permission-list-summary__selected">
329
- {{ selectedPermissionIds.size }} selected
330
- </span>
331
- </div>
332
-
333
- <div class="permission-list" :class="{ 'permission-list--loading': loadingPermissionCandidates }">
334
- <div v-if="loadingPermissionCandidates" class="permission-empty">
335
- <v-progress-circular indeterminate color="primary" size="28" />
336
- <span>Loading permissions...</span>
337
- </div>
338
-
339
- <template v-else-if="visiblePermissionCandidates.length">
340
- <label
341
- v-for="candidate in visiblePermissionCandidates"
342
- :key="`${candidate.category}:${candidate.subjectId}`"
343
- class="permission-list-item"
344
- :class="{ 'permission-list-item--selected': selectedPermissionIds.has(candidate.subjectId) }"
345
- >
346
- <span class="permission-list-item__content">
347
- <strong>{{ candidate.name }}</strong>
348
- <small>{{ candidate.subtitle || permissionCategoryLabel(candidate.category) }}</small>
349
- </span>
350
- <span class="permission-list-item__group">{{ permissionCategoryLabel(candidate.category) }}</span>
351
- <v-checkbox-btn
352
- class="permission-list-item__checkbox"
353
- :model-value="selectedPermissionIds.has(candidate.subjectId)"
354
- :aria-label="`Select ${candidate.name}`"
355
- color="success"
356
- @update:model-value="togglePermissionCandidate(candidate.subjectId, Boolean($event))"
357
- />
358
- </label>
359
- </template>
360
-
361
- <div v-else class="permission-empty">
362
- <v-icon icon="mdi-account-search-outline" size="30" />
363
- <span>{{ permissionEmptyMessage }}</span>
364
- </div>
365
- </div>
366
- </v-card-text>
367
-
368
- <v-card-actions class="permission-dialog__actions">
369
- <v-btn class="text-none" variant="text" @click="closePermissionDialog">Cancel</v-btn>
370
- <v-btn
371
- class="text-none save-button"
372
- variant="flat"
373
- min-width="140"
374
- :loading="savingPermissions"
375
- :disabled="loadingPermissionCandidates"
376
- @click="savePermissions"
377
- >
378
- Save Permissions
379
- </v-btn>
380
- </v-card-actions>
381
- </v-card>
382
- </v-dialog>
383
-
384
225
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
385
226
  </section>
386
227
  </template>
387
228
 
388
229
  <script setup lang="ts">
389
- type HidIdentificationMethod = keyof NonNullable<THidQrCodePassConfig["identificationMethods"]>;
390
-
391
- type HidQrConfig = Omit<
392
- THidQrCodePassConfig,
393
- "identificationMethods" | "onlineMode" | "updatedAt"
394
- > & {
395
- onlineMode: boolean;
396
- identificationMethods: Record<HidIdentificationMethod, boolean>;
397
- };
398
-
399
- type HidReaderOption = {
400
- _id: string;
401
- name?: string;
402
- deviceId?: string;
403
- location?: string;
404
- };
405
-
406
- type HidReaderListResponse = {
407
- items?: HidReaderOption[];
408
- readers?: HidReaderOption[];
409
- data?: {
410
- items?: HidReaderOption[];
411
- readers?: HidReaderOption[];
230
+ type HidQrConfig = {
231
+ enabled: boolean;
232
+ readerId: string;
233
+ qrFormat: "0" | "1" | "2";
234
+ printer: {
235
+ vendorId: string;
236
+ productId: string;
412
237
  };
238
+ template: {
239
+ header: string;
240
+ subtext: string;
241
+ };
242
+ validityMinutes: number | null;
413
243
  };
414
244
 
415
245
  const props = defineProps<{
416
246
  site: string;
417
- org: string;
418
247
  }>();
419
248
 
420
- const runtimeConfig = useRuntimeConfig();
421
- const isPermissionManager = computed(() => runtimeConfig.public.APP === "security_agency");
422
-
423
249
  const { getSiteById, updateSitebyId } = useSiteSettings();
424
- const {
425
- getReaders,
426
- getReaderConfiguration,
427
- setReaderConfiguration,
428
- getSitePermissions,
429
- getPermissionCandidates,
430
- updateSitePermissions,
431
- } = useHidAmico();
432
- const {
433
- requestDevice: requestPrinterDevice,
434
- testConnection: testPrinterConnection,
435
- } = useWebUsb();
250
+ const { getReaders, setReaderConfiguration } = useHidAmico();
436
251
 
437
252
  const defaultConfig: HidQrConfig = {
438
253
  enabled: false,
439
- onlineMode: false,
440
254
  readerId: "",
441
255
  qrFormat: "0",
442
- identificationMethods: {
443
- facial: true,
444
- card: true,
445
- qrCode: false,
446
- idPassword: true,
447
- pin: false,
448
- bluetooth: false,
449
- },
450
256
  printer: {
451
257
  vendorId: "",
452
258
  productId: "",
@@ -458,111 +264,36 @@ const defaultConfig: HidQrConfig = {
458
264
  validityMinutes: null,
459
265
  };
460
266
 
461
- const siteData = ref<TSite | null>(null);
462
- const readers = ref<HidReaderOption[]>([]);
267
+ const siteData = ref<Record<string, any> | null>(null);
268
+ const readers = ref<Record<string, any>[]>([]);
463
269
  const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
464
270
  const previewDialog = ref(false);
465
271
  const savingSection = ref("");
466
- const testingPrinter = ref(false);
467
272
  const message = ref("");
468
273
  const messageColor = ref("success");
469
274
  const messageSnackbar = ref(false);
470
- const permissionState = reactive<THidSitePermissions>({
471
- orgId: props.org,
472
- site: props.site,
473
- readerId: "",
474
- location: "",
475
- assignments: [],
476
- counts: {
477
- resident: 0,
478
- propertyManagement: 0,
479
- serviceProvider: 0,
480
- intercom: 0,
481
- },
482
- });
483
- const permissionDialog = ref(false);
484
- const permissionReaderId = ref("");
485
- const permissionDialogCategory = ref<THidPermissionCategory | "intercom">("resident");
486
- const permissionCandidates = ref<THidPermissionCandidate[]>([]);
487
- const selectedPermissionIds = ref(new Set<string>());
488
- const permissionSearch = ref("");
489
- const loadingPermissionCandidates = ref(false);
490
- const savingPermissions = ref(false);
491
275
 
492
276
  const hasPrinterConfig = computed(() =>
493
277
  Boolean(form.printer.vendorId && form.printer.productId),
494
278
  );
495
- const identificationMethodOptions: Array<{
496
- key: HidIdentificationMethod;
497
- label: string;
498
- }> = [
499
- { key: "facial", label: "Facial" },
500
- { key: "card", label: "Card" },
501
- { key: "qrCode", label: "QR Code" },
502
- { key: "idPassword", label: "ID Password" },
503
- { key: "pin", label: "PIN" },
504
- { key: "bluetooth", label: "Bluetooth" },
505
- ];
506
- const accessPermissionOptions: Array<{
507
- category: THidPermissionCategory;
508
- label: string;
509
- }> = [
510
- { category: "resident", label: "Resident" },
511
- { category: "property_management", label: "Property Management" },
512
- { category: "service_provider", label: "Service Provider" },
279
+ const readerOptions = computed(() => readers.value.map((reader) => ({
280
+ title: reader.name || reader.deviceId || reader._id,
281
+ value: reader._id,
282
+ })));
283
+ const qrFormatOptions = [
284
+ { title: "Alphanumeric", value: "0" },
285
+ { title: "Numeric only", value: "1" },
286
+ { title: "Hexadecimal numeric", value: "2" },
513
287
  ];
514
288
 
515
- const permissionLocations = computed(() => {
516
- const seen = new Set<string>();
517
- return readers.value.reduce<Array<{ readerId: string; location: string; label: string }>>((items, reader) => {
518
- const location = reader.location?.trim();
519
- const key = location?.toLocaleLowerCase();
520
- if (!location || !key || seen.has(key)) return items;
521
- seen.add(key);
522
- items.push({
523
- readerId: reader._id,
524
- location,
525
- label: location,
526
- });
527
- return items;
528
- }, []);
529
- });
530
- const selectedPermissionLocation = computed(() => (
531
- permissionLocations.value.find((item) => item.readerId === permissionReaderId.value)?.location
532
- || "the selected location"
533
- ));
534
-
535
289
  const siteName = computed(() => siteData.value?.name || "Site");
536
290
  const siteLabel = computed(() => siteData.value?.code || siteData.value?.block || props.site);
537
291
  const validityText = computed(() =>
538
292
  form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
539
293
  );
540
- const visiblePermissionCandidates = computed(() => {
541
- const search = permissionSearch.value.trim().toLowerCase();
542
- if (!search) return permissionCandidates.value;
543
- return permissionCandidates.value.filter((candidate) => (
544
- candidate.name.toLowerCase().includes(search)
545
- || candidate.subtitle?.toLowerCase().includes(search)
546
- ));
547
- });
548
- const permissionDialogTitle = computed(() => (
549
- permissionDialogCategory.value === "intercom"
550
- ? "Intercom Permissions"
551
- : `${permissionCategoryLabel(permissionDialogCategory.value)} Access Permission`
552
- ));
553
- const permissionDialogDescription = computed(() => (
554
- permissionDialogCategory.value === "intercom"
555
- ? `Select from people and providers who already have HID access permission at ${selectedPermissionLocation.value}.`
556
- : `Select the ${permissionCategoryLabel(permissionDialogCategory.value).toLowerCase()} entries allowed to use HID access at ${selectedPermissionLocation.value}.`
557
- ));
558
- const permissionEmptyMessage = computed(() => (
559
- permissionDialogCategory.value === "intercom" && permissionState.assignments.length === 0
560
- ? "Add an access permission before assigning intercom access."
561
- : "No matching entries found."
562
- ));
563
294
 
564
295
  watch(
565
- () => [props.org, props.site],
296
+ () => props.site,
566
297
  () => loadConfig(),
567
298
  { immediate: true },
568
299
  );
@@ -574,24 +305,13 @@ async function loadConfig() {
574
305
  getSiteById(props.site),
575
306
  getReaders({ site: props.site, page: 1, limit: 100 }),
576
307
  ]);
577
- siteData.value = site as TSite;
578
- const readerList = readerResponse as HidReaderListResponse;
579
- readers.value = readerList.items ?? readerList.data?.items ?? readerList.data?.readers ?? [];
308
+ siteData.value = site;
309
+ readers.value = readerResponse?.items ?? readerResponse?.data?.items ?? readerResponse?.data?.readers ?? [];
580
310
  const config = siteData.value?.metadata?.hidQrCodePass || {};
581
- const configuredMethods = config.identificationMethods;
582
311
  Object.assign(form, {
583
312
  enabled: Boolean(config.enabled),
584
- onlineMode: Boolean(config.onlineMode),
585
313
  readerId: String(config.readerId || readers.value[0]?._id || ""),
586
314
  qrFormat: ["0", "1", "2"].includes(String(config.qrFormat)) ? String(config.qrFormat) as "0" | "1" | "2" : "0",
587
- identificationMethods: {
588
- facial: configuredMethods?.facial ?? true,
589
- card: configuredMethods?.card ?? true,
590
- qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
591
- idPassword: configuredMethods?.idPassword ?? true,
592
- pin: configuredMethods?.pin ?? false,
593
- bluetooth: configuredMethods?.bluetooth ?? false,
594
- },
595
315
  printer: {
596
316
  vendorId: String(config.printer?.vendorId || ""),
597
317
  productId: String(config.printer?.productId || ""),
@@ -602,175 +322,15 @@ async function loadConfig() {
602
322
  },
603
323
  validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
604
324
  });
605
-
606
- permissionReaderId.value = permissionLocations.value.find((item) => item.readerId === form.readerId)?.readerId
607
- || permissionLocations.value[0]?.readerId
608
- || "";
609
-
610
- await Promise.all([
611
- loadReaderConfiguration(),
612
- isPermissionManager.value ? loadPermissions() : Promise.resolve(),
613
- ]);
614
- }
615
-
616
- async function loadPermissions() {
617
- if (!props.org || !props.site || !permissionReaderId.value) {
618
- Object.assign(permissionState, {
619
- orgId: props.org,
620
- site: props.site,
621
- readerId: "",
622
- location: "",
623
- assignments: [],
624
- counts: { resident: 0, propertyManagement: 0, serviceProvider: 0, intercom: 0 },
625
- });
626
- return;
627
- }
628
- try {
629
- const response = await getSitePermissions(props.site, props.org, permissionReaderId.value);
630
- Object.assign(permissionState, response.data);
631
- } catch (error: unknown) {
632
- console.error("Unable to load HID permissions:", errorConverter(error));
633
- }
634
- }
635
-
636
- function permissionCategoryLabel(category: THidPermissionCategory) {
637
- return accessPermissionOptions.find((option) => option.category === category)?.label || "Permission";
638
- }
639
-
640
- function permissionCount(category: THidPermissionCategory) {
641
- if (category === "property_management") return permissionState.counts.propertyManagement;
642
- if (category === "service_provider") return permissionState.counts.serviceProvider;
643
- return permissionState.counts.resident;
644
- }
645
-
646
- async function openPermissionDialog(category: THidPermissionCategory | "intercom") {
647
- if (!props.org || !permissionReaderId.value) return;
648
- permissionDialogCategory.value = category;
649
- permissionSearch.value = "";
650
- permissionCandidates.value = [];
651
- selectedPermissionIds.value = new Set<string>();
652
- permissionDialog.value = true;
653
- loadingPermissionCandidates.value = true;
654
-
655
- try {
656
- const response = await getPermissionCandidates(props.site, {
657
- orgId: props.org,
658
- readerId: permissionReaderId.value,
659
- category,
660
- page: 1,
661
- limit: 500,
662
- });
663
- permissionCandidates.value = response.items || [];
664
- selectedPermissionIds.value = new Set(
665
- permissionCandidates.value
666
- .filter((candidate) => category === "intercom" ? candidate.intercom : candidate.selected)
667
- .map((candidate) => candidate.subjectId),
668
- );
669
- } catch (error: unknown) {
670
- showMessage(errorConverter(error), "error");
671
- } finally {
672
- loadingPermissionCandidates.value = false;
673
- }
674
- }
675
-
676
- function closePermissionDialog() {
677
- if (savingPermissions.value) return;
678
- permissionDialog.value = false;
679
- }
680
-
681
- function togglePermissionCandidate(subjectId: string, selected: boolean) {
682
- const next = new Set(selectedPermissionIds.value);
683
- if (selected) next.add(subjectId);
684
- else next.delete(subjectId);
685
- selectedPermissionIds.value = next;
686
- }
687
-
688
- async function savePermissions() {
689
- const category = permissionDialogCategory.value;
690
- let assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>;
691
-
692
- if (category === "intercom") {
693
- assignments = permissionState.assignments.map((assignment) => ({
694
- subjectId: assignment.subjectId,
695
- category: assignment.category,
696
- intercom: selectedPermissionIds.value.has(assignment.subjectId),
697
- }));
698
- } else {
699
- const retained = permissionState.assignments
700
- .filter((assignment) => assignment.category !== category)
701
- .map((assignment) => ({
702
- subjectId: assignment.subjectId,
703
- category: assignment.category,
704
- intercom: assignment.intercom,
705
- }));
706
- const selected = permissionCandidates.value
707
- .filter((candidate) => selectedPermissionIds.value.has(candidate.subjectId))
708
- .map((candidate) => ({
709
- subjectId: candidate.subjectId,
710
- category: candidate.category,
711
- intercom: candidate.intercom,
712
- }));
713
- assignments = [...retained, ...selected];
714
- }
715
-
716
- savingPermissions.value = true;
717
- try {
718
- const response = await updateSitePermissions(
719
- props.site,
720
- props.org,
721
- permissionReaderId.value,
722
- assignments,
723
- );
724
- Object.assign(permissionState, response.data);
725
- permissionDialog.value = false;
726
- showMessage("HID permissions updated.", "success");
727
- } catch (error: unknown) {
728
- showMessage(errorConverter(error), "error");
729
- } finally {
730
- savingPermissions.value = false;
731
- }
732
- }
733
-
734
- function isUnknownRecord(value: unknown): value is Record<string, unknown> {
735
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
736
- }
737
-
738
- function getConfigurationData(response: unknown) {
739
- if (!isUnknownRecord(response)) return {};
740
- return isUnknownRecord(response.data) ? response.data : response;
741
- }
742
-
743
- function isEnabledConfiguration(value: unknown) {
744
- return value === true || value === 1 || value === "1";
745
- }
746
-
747
- async function loadReaderConfiguration() {
748
- if (!form.readerId) return;
749
-
750
- try {
751
- const response = await getReaderConfiguration(form.readerId, {
752
- general: ["online"],
753
- });
754
- const configuration = getConfigurationData(response);
755
- const general = isUnknownRecord(configuration.general) ? configuration.general : {};
756
-
757
- if (general.online !== undefined) {
758
- form.onlineMode = isEnabledConfiguration(general.online);
759
- }
760
- } catch (error: unknown) {
761
- console.warn("Unable to load HID reader configuration:", errorConverter(error));
762
- }
763
325
  }
764
326
 
765
327
  function buildPayload() {
766
328
  const metadata = {
767
329
  ...(siteData.value?.metadata || {}),
768
330
  hidQrCodePass: {
769
- enabled: form.identificationMethods.qrCode,
770
- onlineMode: form.onlineMode,
331
+ enabled: form.enabled,
771
332
  readerId: form.readerId,
772
333
  qrFormat: form.qrFormat,
773
- identificationMethods: { ...form.identificationMethods },
774
334
  printer: {
775
335
  vendorId: form.printer.vendorId.trim(),
776
336
  productId: form.printer.productId.trim(),
@@ -792,45 +352,17 @@ async function saveSection(section: string) {
792
352
 
793
353
  savingSection.value = section;
794
354
  try {
355
+ if (section === "qr-format") {
356
+ await setReaderConfiguration(form.readerId, {
357
+ face_id: {
358
+ qrcode_legacy_mode_enabled: form.qrFormat,
359
+ },
360
+ });
361
+ }
795
362
  await updateSitebyId(props.site, buildPayload());
796
363
  await loadConfig();
797
- showMessage("HID configuration saved.", "success");
798
- } catch (error: unknown) {
799
- showMessage(errorConverter(error), "error");
800
- } finally {
801
- savingSection.value = "";
802
- }
803
- }
804
-
805
- async function saveOnlineMode() {
806
- if (!props.site || !form.readerId) return;
807
-
808
- savingSection.value = "online-mode";
809
- try {
810
- await setReaderConfiguration(form.readerId, {
811
- general: { online: form.onlineMode ? "1" : "0" },
812
- });
813
- await updateSitebyId(props.site, buildPayload());
814
- showMessage("Online mode updated.", "success");
815
- } catch (error: unknown) {
816
- await loadConfig();
817
- showMessage(errorConverter(error), "error");
818
- } finally {
819
- savingSection.value = "";
820
- }
821
- }
822
-
823
- async function saveIdentificationMethod(method: HidIdentificationMethod) {
824
- if (!props.site) return;
825
-
826
- form.enabled = form.identificationMethods.qrCode;
827
-
828
- savingSection.value = `method-${method}`;
829
- try {
830
- await updateSitebyId(props.site, buildPayload());
831
- showMessage(`${identificationMethodOptions.find((item) => item.key === method)?.label || "Identification method"} updated.`, "success");
832
- } catch (error: unknown) {
833
- await loadConfig();
364
+ showMessage("HID QR code settings saved.", "success");
365
+ } catch (error: any) {
834
366
  showMessage(errorConverter(error), "error");
835
367
  } finally {
836
368
  savingSection.value = "";
@@ -844,36 +376,19 @@ async function scanPrinter() {
844
376
  }
845
377
 
846
378
  try {
847
- const device = await requestPrinterDevice();
379
+ const device = await (navigator as any).usb.requestDevice({ filters: [] });
848
380
  form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
849
381
  form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
850
382
  showMessage("Printer selected. Save settings to keep it.", "success");
851
- } catch (error: unknown) {
852
- const message = errorConverter(error);
853
- if (message !== "No device selected") {
854
- showMessage(message || "Unable to scan printer.", "error");
383
+ } catch (error: any) {
384
+ if (error?.name !== "NotFoundError") {
385
+ showMessage(error?.message || "Unable to scan printer.", "error");
855
386
  }
856
387
  }
857
388
  }
858
389
 
859
- async function testPrint() {
860
- const vendorId = Number(form.printer.vendorId);
861
- const productId = Number(form.printer.productId);
862
- if (!vendorId || !productId) {
863
- showMessage("Please scan a valid USB printer first.", "error");
864
- return;
865
- }
866
-
867
- testingPrinter.value = true;
868
- try {
869
- const result = await testPrinterConnection(vendorId, productId);
870
- showMessage(
871
- result.success ? result.message : result.error || result.message,
872
- result.success ? "success" : "error",
873
- );
874
- } finally {
875
- testingPrinter.value = false;
876
- }
390
+ function testPrint() {
391
+ showMessage("Printer settings are ready. Connect print command when HID QR payload generation is available.", "success");
877
392
  }
878
393
 
879
394
  function showMessage(text: string, color: string) {
@@ -917,15 +432,15 @@ function showMessage(text: string, color: string) {
917
432
  padding: 30px 38px;
918
433
  }
919
434
 
920
- .online-mode-row {
435
+ .section-head {
921
436
  display: flex;
922
- align-items: center;
437
+ align-items: flex-start;
923
438
  justify-content: space-between;
924
439
  gap: 24px;
925
- padding: 0 0 20px;
440
+ margin-bottom: 24px;
926
441
  }
927
442
 
928
- .online-mode-row h3,
443
+ .section-head h3,
929
444
  .section-label {
930
445
  margin: 0 0 6px;
931
446
  color: #233545;
@@ -933,83 +448,16 @@ function showMessage(text: string, color: string) {
933
448
  font-weight: 700;
934
449
  }
935
450
 
936
- .online-mode-row p,
937
- .section-description {
451
+ .section-head p {
938
452
  margin: 0;
939
453
  color: #677280;
940
454
  font-size: 12px;
941
- line-height: 1.5;
942
455
  }
943
456
 
944
457
  .form-section {
945
458
  padding: 16px 0;
946
459
  }
947
460
 
948
- .identification-section {
949
- padding-bottom: 20px;
950
- }
951
-
952
- .methods-grid {
953
- display: grid;
954
- grid-template-columns: repeat(6, minmax(120px, 1fr));
955
- gap: 18px;
956
- margin-top: 16px;
957
- }
958
-
959
- .method-control {
960
- display: flex;
961
- align-items: center;
962
- justify-content: space-between;
963
- min-width: 0;
964
- min-height: 42px;
965
- padding: 0 10px 0 14px;
966
- border: 1px solid #dce2e7;
967
- border-radius: 6px;
968
- color: #334454;
969
- font-size: 12px;
970
- }
971
-
972
- .method-control span {
973
- overflow: hidden;
974
- text-overflow: ellipsis;
975
- white-space: nowrap;
976
- }
977
-
978
- .method-control--disabled {
979
- background-color: #f5f6f7;
980
- color: #8b9297;
981
- }
982
-
983
- .method-control :deep(.v-switch) {
984
- flex: 0 0 auto;
985
- }
986
-
987
- .method-control :deep(.v-selection-control) {
988
- min-height: 32px;
989
- }
990
-
991
- .hid-config-switch :deep(.v-switch__track) {
992
- background-color: #8b9297;
993
- opacity: 1;
994
- }
995
-
996
- .hid-config-switch :deep(.v-selection-control--dirty .v-switch__track) {
997
- background-color: #2eaf55 !important;
998
- color: #2eaf55 !important;
999
- opacity: 1;
1000
- }
1001
-
1002
- .hid-config-switch :deep(.v-switch__thumb) {
1003
- background-color: #fff !important;
1004
- color: #fff !important;
1005
- }
1006
-
1007
- .hid-config-switch.v-input--disabled :deep(.v-switch__track) {
1008
- background-color: #aeb4b8 !important;
1009
- color: #aeb4b8 !important;
1010
- opacity: 1;
1011
- }
1012
-
1013
461
  .printer-grid {
1014
462
  display: grid;
1015
463
  grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
@@ -1035,231 +483,6 @@ function showMessage(text: string, color: string) {
1035
483
  align-items: center;
1036
484
  }
1037
485
 
1038
- .permission-section {
1039
- padding-top: 2px;
1040
- }
1041
-
1042
- .permission-location {
1043
- display: grid;
1044
- grid-template-columns: minmax(260px, 1fr) minmax(320px, 1.35fr);
1045
- gap: 36px;
1046
- align-items: center;
1047
- padding: 18px 0;
1048
- }
1049
-
1050
- .permission-location__select {
1051
- width: 100%;
1052
- }
1053
-
1054
- .permission-row {
1055
- display: grid;
1056
- grid-template-columns: minmax(260px, 1fr) minmax(520px, 1.35fr);
1057
- gap: 36px;
1058
- align-items: center;
1059
- padding: 18px 0;
1060
- }
1061
-
1062
- .permission-row--intercom {
1063
- padding-bottom: 4px;
1064
- }
1065
-
1066
- .permission-copy {
1067
- min-width: 0;
1068
- }
1069
-
1070
- .permission-actions {
1071
- display: grid;
1072
- grid-template-columns: repeat(3, minmax(150px, 1fr));
1073
- gap: 12px;
1074
- }
1075
-
1076
- .permission-actions--single {
1077
- grid-template-columns: minmax(180px, 240px);
1078
- justify-content: end;
1079
- }
1080
-
1081
- .permission-button {
1082
- justify-content: flex-start;
1083
- min-height: 42px;
1084
- padding-inline: 12px;
1085
- border-color: #dce2e7;
1086
- color: #334454;
1087
- }
1088
-
1089
- .permission-count {
1090
- display: inline-grid;
1091
- place-items: center;
1092
- width: 20px;
1093
- height: 20px;
1094
- margin-right: 8px;
1095
- border-radius: 50%;
1096
- background: #dce2e7;
1097
- color: #526170;
1098
- font-size: 11px;
1099
- font-weight: 700;
1100
- }
1101
-
1102
- .permission-button:not(:disabled):hover {
1103
- border-color: #1473c9;
1104
- color: #0a5da7;
1105
- }
1106
-
1107
- .permission-dialog {
1108
- overflow: hidden;
1109
- border-radius: 6px;
1110
- }
1111
-
1112
- .permission-dialog__header {
1113
- display: flex;
1114
- align-items: flex-start;
1115
- justify-content: space-between;
1116
- gap: 16px;
1117
- padding: 16px 18px 14px;
1118
- border-bottom: 1px solid #e5e9ed;
1119
- }
1120
-
1121
- .permission-dialog__title {
1122
- color: #172534;
1123
- font-size: 18px;
1124
- font-weight: 700;
1125
- }
1126
-
1127
- .permission-dialog__subtitle {
1128
- margin-top: 4px;
1129
- color: #687583;
1130
- font-size: 12px;
1131
- white-space: normal;
1132
- }
1133
-
1134
- .permission-dialog__body {
1135
- display: grid;
1136
- gap: 10px;
1137
- padding: 14px 18px 16px;
1138
- }
1139
-
1140
- .permission-search :deep(.v-field) {
1141
- min-height: 44px;
1142
- }
1143
-
1144
- .permission-list-summary {
1145
- display: flex;
1146
- align-items: center;
1147
- justify-content: space-between;
1148
- min-height: 22px;
1149
- color: #778391;
1150
- font-size: 11px;
1151
- }
1152
-
1153
- .permission-list-summary__selected {
1154
- color: #286c3c;
1155
- font-weight: 700;
1156
- }
1157
-
1158
- .permission-list {
1159
- min-height: 180px;
1160
- max-height: min(360px, 48vh);
1161
- overflow-y: auto;
1162
- border: 1px solid #dfe5ea;
1163
- border-radius: 6px;
1164
- background: #ffffff;
1165
- }
1166
-
1167
- .permission-list--loading {
1168
- display: grid;
1169
- place-items: center;
1170
- }
1171
-
1172
- .permission-list-item {
1173
- display: grid;
1174
- grid-template-columns: minmax(0, 1fr) max-content 32px;
1175
- gap: 12px;
1176
- align-items: center;
1177
- min-height: 56px;
1178
- padding: 7px 12px;
1179
- border-bottom: 1px solid #edf0f3;
1180
- cursor: pointer;
1181
- transition: background-color 120ms ease;
1182
- }
1183
-
1184
- .permission-list-item:last-child {
1185
- border-bottom: 0;
1186
- }
1187
-
1188
- .permission-list-item:hover {
1189
- background: #f7fafc;
1190
- }
1191
-
1192
- .permission-list-item--selected {
1193
- background: #f1f8f3;
1194
- }
1195
-
1196
- .permission-list-item--selected:hover {
1197
- background: #eaf5ed;
1198
- }
1199
-
1200
- .permission-list-item__content {
1201
- display: grid;
1202
- gap: 2px;
1203
- min-width: 0;
1204
- }
1205
-
1206
- .permission-list-item__content strong,
1207
- .permission-list-item__content small {
1208
- overflow: hidden;
1209
- text-overflow: ellipsis;
1210
- white-space: nowrap;
1211
- }
1212
-
1213
- .permission-list-item__content strong {
1214
- color: #243443;
1215
- font-size: 13px;
1216
- line-height: 1.35;
1217
- }
1218
-
1219
- .permission-list-item__content small,
1220
- .permission-list-item__group {
1221
- color: #778391;
1222
- font-size: 11px;
1223
- }
1224
-
1225
- .permission-list-item__group {
1226
- max-width: 170px;
1227
- overflow: hidden;
1228
- padding: 4px 9px;
1229
- border-radius: 4px;
1230
- background: #eef2f5;
1231
- text-overflow: ellipsis;
1232
- white-space: nowrap;
1233
- }
1234
-
1235
- .permission-list-item__checkbox {
1236
- justify-self: end;
1237
- }
1238
-
1239
- .permission-list-item__checkbox :deep(.v-selection-control) {
1240
- min-height: 32px;
1241
- }
1242
-
1243
- .permission-empty {
1244
- display: flex;
1245
- flex-direction: column;
1246
- align-items: center;
1247
- justify-content: center;
1248
- gap: 10px;
1249
- min-height: 180px;
1250
- padding: 24px;
1251
- color: #75818c;
1252
- font-size: 13px;
1253
- text-align: center;
1254
- }
1255
-
1256
- .permission-dialog__actions {
1257
- justify-content: flex-end;
1258
- gap: 8px;
1259
- padding: 12px 18px;
1260
- border-top: 1px solid #e5e9ed;
1261
- }
1262
-
1263
486
  .save-button {
1264
487
  background: #082e43;
1265
488
  color: #ffffff;
@@ -1369,21 +592,11 @@ function showMessage(text: string, color: string) {
1369
592
  }
1370
593
 
1371
594
  @media (max-width: 1100px) {
1372
- .methods-grid {
1373
- grid-template-columns: repeat(3, minmax(140px, 1fr));
1374
- }
1375
-
1376
595
  .printer-grid,
1377
596
  .template-grid,
1378
- .validity-grid,
1379
- .permission-location,
1380
- .permission-row {
597
+ .validity-grid {
1381
598
  grid-template-columns: 1fr 1fr;
1382
599
  }
1383
-
1384
- .permission-actions {
1385
- grid-template-columns: 1fr;
1386
- }
1387
600
  }
1388
601
 
1389
602
  @media (max-width: 700px) {
@@ -1391,40 +604,15 @@ function showMessage(text: string, color: string) {
1391
604
  padding: 22px 16px;
1392
605
  }
1393
606
 
1394
- .online-mode-row,
607
+ .section-head,
1395
608
  .printer-grid,
1396
609
  .template-grid,
1397
- .validity-grid,
1398
- .methods-grid,
1399
- .permission-location,
1400
- .permission-row,
1401
- .permission-actions {
610
+ .validity-grid {
1402
611
  grid-template-columns: 1fr;
1403
612
  }
1404
613
 
1405
- .online-mode-row {
614
+ .section-head {
1406
615
  display: grid;
1407
616
  }
1408
-
1409
- .permission-actions--single {
1410
- justify-content: stretch;
1411
- }
1412
-
1413
- .permission-list-item {
1414
- grid-template-columns: minmax(0, 1fr) 32px;
1415
- gap: 8px;
1416
- }
1417
-
1418
- .permission-list-item__group {
1419
- grid-column: 1;
1420
- grid-row: 2;
1421
- justify-self: start;
1422
- max-width: 100%;
1423
- }
1424
-
1425
- .permission-list-item__checkbox {
1426
- grid-column: 2;
1427
- grid-row: 1 / span 2;
1428
- }
1429
617
  }
1430
618
  </style>