@7365admin1/layer-common 3.1.4-staging.53 → 3.2.0

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.
@@ -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,54 +184,6 @@
175
184
  </v-btn>
176
185
  </div>
177
186
  </div>
178
-
179
- <v-divider />
180
-
181
- <div class="permission-section">
182
- <div class="permission-row">
183
- <div class="permission-copy">
184
- <div class="section-label">Access Permission</div>
185
- <p class="section-description">
186
- Select who can use the enabled HID identification methods at this site.
187
- </p>
188
- </div>
189
- <div class="permission-actions">
190
- <v-btn
191
- v-for="option in accessPermissionOptions"
192
- :key="option.category"
193
- class="permission-button text-none"
194
- variant="outlined"
195
- :disabled="!form.onlineMode"
196
- @click="openPermissionDialog(option.category)"
197
- >
198
- <span class="permission-count">{{ permissionCount(option.category) }}</span>
199
- <span>{{ option.label }}</span>
200
- </v-btn>
201
- </div>
202
- </div>
203
-
204
- <v-divider />
205
-
206
- <div class="permission-row permission-row--intercom">
207
- <div class="permission-copy">
208
- <div class="section-label">Access Intercom</div>
209
- <p class="section-description">
210
- Choose which authorized people and providers may use HID intercom access.
211
- </p>
212
- </div>
213
- <div class="permission-actions permission-actions--single">
214
- <v-btn
215
- class="permission-button text-none"
216
- variant="outlined"
217
- :disabled="!form.onlineMode || permissionState.assignments.length === 0"
218
- @click="openPermissionDialog('intercom')"
219
- >
220
- <span class="permission-count">{{ permissionState.counts.intercom }}</span>
221
- <span>Intercom Permissions</span>
222
- </v-btn>
223
- </div>
224
- </div>
225
- </div>
226
187
  </div>
227
188
  </v-card>
228
189
 
@@ -261,109 +222,24 @@
261
222
  </v-card>
262
223
  </v-dialog>
263
224
 
264
- <v-dialog v-model="permissionDialog" max-width="640" persistent>
265
- <v-card class="permission-dialog">
266
- <v-card-title class="permission-dialog__header">
267
- <div>
268
- <div class="permission-dialog__title">{{ permissionDialogTitle }}</div>
269
- <div class="permission-dialog__subtitle">{{ permissionDialogDescription }}</div>
270
- </div>
271
- <v-btn
272
- icon="mdi-close"
273
- variant="text"
274
- density="comfortable"
275
- aria-label="Close permission dialog"
276
- @click="closePermissionDialog"
277
- />
278
- </v-card-title>
279
-
280
- <v-card-text class="permission-dialog__body">
281
- <v-text-field
282
- v-model="permissionSearch"
283
- prepend-inner-icon="mdi-magnify"
284
- placeholder="Search by name or details"
285
- variant="outlined"
286
- density="compact"
287
- hide-details
288
- clearable
289
- />
290
-
291
- <div class="permission-list" :class="{ 'permission-list--loading': loadingPermissionCandidates }">
292
- <div v-if="loadingPermissionCandidates" class="permission-empty">
293
- <v-progress-circular indeterminate color="primary" size="28" />
294
- <span>Loading permissions...</span>
295
- </div>
296
-
297
- <template v-else-if="visiblePermissionCandidates.length">
298
- <label
299
- v-for="candidate in visiblePermissionCandidates"
300
- :key="`${candidate.category}:${candidate.subjectId}`"
301
- class="permission-list-item"
302
- >
303
- <v-checkbox-btn
304
- :model-value="selectedPermissionIds.has(candidate.subjectId)"
305
- color="success"
306
- @update:model-value="togglePermissionCandidate(candidate.subjectId, Boolean($event))"
307
- />
308
- <span class="permission-list-item__content">
309
- <strong>{{ candidate.name }}</strong>
310
- <small>{{ candidate.subtitle || permissionCategoryLabel(candidate.category) }}</small>
311
- </span>
312
- <span class="permission-list-item__group">{{ permissionCategoryLabel(candidate.category) }}</span>
313
- </label>
314
- </template>
315
-
316
- <div v-else class="permission-empty">
317
- <v-icon icon="mdi-account-search-outline" size="30" />
318
- <span>{{ permissionEmptyMessage }}</span>
319
- </div>
320
- </div>
321
- </v-card-text>
322
-
323
- <v-card-actions class="permission-dialog__actions">
324
- <v-btn class="text-none" variant="text" @click="closePermissionDialog">Cancel</v-btn>
325
- <v-btn
326
- class="text-none save-button"
327
- variant="flat"
328
- min-width="140"
329
- :loading="savingPermissions"
330
- :disabled="loadingPermissionCandidates"
331
- @click="savePermissions"
332
- >
333
- Save Permissions
334
- </v-btn>
335
- </v-card-actions>
336
- </v-card>
337
- </v-dialog>
338
-
339
225
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
340
226
  </section>
341
227
  </template>
342
228
 
343
229
  <script setup lang="ts">
344
- type HidIdentificationMethod = keyof NonNullable<THidQrCodePassConfig["identificationMethods"]>;
345
-
346
- type HidQrConfig = Omit<
347
- THidQrCodePassConfig,
348
- "identificationMethods" | "onlineMode" | "updatedAt"
349
- > & {
350
- onlineMode: boolean;
351
- identificationMethods: Record<HidIdentificationMethod, boolean>;
352
- };
353
-
354
- type HidReaderOption = {
355
- _id: string;
356
- name?: string;
357
- deviceId?: string;
358
- };
359
-
360
- type HidReaderListResponse = {
361
- items?: HidReaderOption[];
362
- readers?: HidReaderOption[];
363
- data?: {
364
- items?: HidReaderOption[];
365
- readers?: HidReaderOption[];
230
+ type HidQrConfig = {
231
+ enabled: boolean;
232
+ readerId: string;
233
+ qrFormat: "0" | "1" | "2";
234
+ printer: {
235
+ vendorId: string;
236
+ productId: string;
237
+ };
238
+ template: {
239
+ header: string;
240
+ subtext: string;
366
241
  };
242
+ validityMinutes: number | null;
367
243
  };
368
244
 
369
245
  const props = defineProps<{
@@ -371,32 +247,12 @@ const props = defineProps<{
371
247
  }>();
372
248
 
373
249
  const { getSiteById, updateSitebyId } = useSiteSettings();
374
- const {
375
- getReaders,
376
- getReaderConfiguration,
377
- setReaderConfiguration,
378
- getSitePermissions,
379
- getPermissionCandidates,
380
- updateSitePermissions,
381
- } = useHidAmico();
382
- const {
383
- requestDevice: requestPrinterDevice,
384
- testConnection: testPrinterConnection,
385
- } = useWebUsb();
250
+ const { getReaders, setReaderConfiguration } = useHidAmico();
386
251
 
387
252
  const defaultConfig: HidQrConfig = {
388
253
  enabled: false,
389
- onlineMode: false,
390
254
  readerId: "",
391
255
  qrFormat: "0",
392
- identificationMethods: {
393
- facial: true,
394
- card: true,
395
- qrCode: false,
396
- idPassword: true,
397
- pin: false,
398
- bluetooth: false,
399
- },
400
256
  printer: {
401
257
  vendorId: "",
402
258
  productId: "",
@@ -408,54 +264,26 @@ const defaultConfig: HidQrConfig = {
408
264
  validityMinutes: null,
409
265
  };
410
266
 
411
- const siteData = ref<TSite | null>(null);
412
- const readers = ref<HidReaderOption[]>([]);
267
+ const siteData = ref<Record<string, any> | null>(null);
268
+ const readers = ref<Record<string, any>[]>([]);
413
269
  const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
414
270
  const previewDialog = ref(false);
415
271
  const savingSection = ref("");
416
- const testingPrinter = ref(false);
417
272
  const message = ref("");
418
273
  const messageColor = ref("success");
419
274
  const messageSnackbar = ref(false);
420
- const permissionState = reactive<THidSitePermissions>({
421
- site: props.site,
422
- assignments: [],
423
- counts: {
424
- resident: 0,
425
- propertyManagement: 0,
426
- serviceProvider: 0,
427
- intercom: 0,
428
- },
429
- });
430
- const permissionDialog = ref(false);
431
- const permissionDialogCategory = ref<THidPermissionCategory | "intercom">("resident");
432
- const permissionCandidates = ref<THidPermissionCandidate[]>([]);
433
- const selectedPermissionIds = ref(new Set<string>());
434
- const permissionSearch = ref("");
435
- const loadingPermissionCandidates = ref(false);
436
- const savingPermissions = ref(false);
437
275
 
438
276
  const hasPrinterConfig = computed(() =>
439
277
  Boolean(form.printer.vendorId && form.printer.productId),
440
278
  );
441
- const identificationMethodOptions: Array<{
442
- key: HidIdentificationMethod;
443
- label: string;
444
- }> = [
445
- { key: "facial", label: "Facial" },
446
- { key: "card", label: "Card" },
447
- { key: "qrCode", label: "QR Code" },
448
- { key: "idPassword", label: "ID Password" },
449
- { key: "pin", label: "PIN" },
450
- { key: "bluetooth", label: "Bluetooth" },
451
- ];
452
- const accessPermissionOptions: Array<{
453
- category: THidPermissionCategory;
454
- label: string;
455
- }> = [
456
- { category: "resident", label: "Resident" },
457
- { category: "property_management", label: "Property Management" },
458
- { 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" },
459
287
  ];
460
288
 
461
289
  const siteName = computed(() => siteData.value?.name || "Site");
@@ -463,29 +291,6 @@ const siteLabel = computed(() => siteData.value?.code || siteData.value?.block |
463
291
  const validityText = computed(() =>
464
292
  form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
465
293
  );
466
- const visiblePermissionCandidates = computed(() => {
467
- const search = permissionSearch.value.trim().toLowerCase();
468
- if (!search) return permissionCandidates.value;
469
- return permissionCandidates.value.filter((candidate) => (
470
- candidate.name.toLowerCase().includes(search)
471
- || candidate.subtitle?.toLowerCase().includes(search)
472
- ));
473
- });
474
- const permissionDialogTitle = computed(() => (
475
- permissionDialogCategory.value === "intercom"
476
- ? "Intercom Permissions"
477
- : `${permissionCategoryLabel(permissionDialogCategory.value)} Access Permission`
478
- ));
479
- const permissionDialogDescription = computed(() => (
480
- permissionDialogCategory.value === "intercom"
481
- ? "Select from people and providers who already have HID access permission."
482
- : `Select the ${permissionCategoryLabel(permissionDialogCategory.value).toLowerCase()} entries allowed to use HID access at this site.`
483
- ));
484
- const permissionEmptyMessage = computed(() => (
485
- permissionDialogCategory.value === "intercom" && permissionState.assignments.length === 0
486
- ? "Add an access permission before assigning intercom access."
487
- : "No matching entries found."
488
- ));
489
294
 
490
295
  watch(
491
296
  () => props.site,
@@ -500,24 +305,13 @@ async function loadConfig() {
500
305
  getSiteById(props.site),
501
306
  getReaders({ site: props.site, page: 1, limit: 100 }),
502
307
  ]);
503
- siteData.value = site as TSite;
504
- const readerList = readerResponse as HidReaderListResponse;
505
- 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 ?? [];
506
310
  const config = siteData.value?.metadata?.hidQrCodePass || {};
507
- const configuredMethods = config.identificationMethods;
508
311
  Object.assign(form, {
509
312
  enabled: Boolean(config.enabled),
510
- onlineMode: Boolean(config.onlineMode),
511
313
  readerId: String(config.readerId || readers.value[0]?._id || ""),
512
314
  qrFormat: ["0", "1", "2"].includes(String(config.qrFormat)) ? String(config.qrFormat) as "0" | "1" | "2" : "0",
513
- identificationMethods: {
514
- facial: configuredMethods?.facial ?? true,
515
- card: configuredMethods?.card ?? true,
516
- qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
517
- idPassword: configuredMethods?.idPassword ?? true,
518
- pin: configuredMethods?.pin ?? false,
519
- bluetooth: configuredMethods?.bluetooth ?? false,
520
- },
521
315
  printer: {
522
316
  vendorId: String(config.printer?.vendorId || ""),
523
317
  productId: String(config.printer?.productId || ""),
@@ -528,150 +322,15 @@ async function loadConfig() {
528
322
  },
529
323
  validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
530
324
  });
531
-
532
- await Promise.all([loadReaderConfiguration(), loadPermissions()]);
533
- }
534
-
535
- async function loadPermissions() {
536
- if (!props.site) return;
537
- try {
538
- const response = await getSitePermissions(props.site);
539
- Object.assign(permissionState, response.data);
540
- } catch (error: unknown) {
541
- console.error("Unable to load HID permissions:", errorConverter(error));
542
- }
543
- }
544
-
545
- function permissionCategoryLabel(category: THidPermissionCategory) {
546
- return accessPermissionOptions.find((option) => option.category === category)?.label || "Permission";
547
- }
548
-
549
- function permissionCount(category: THidPermissionCategory) {
550
- if (category === "property_management") return permissionState.counts.propertyManagement;
551
- if (category === "service_provider") return permissionState.counts.serviceProvider;
552
- return permissionState.counts.resident;
553
- }
554
-
555
- async function openPermissionDialog(category: THidPermissionCategory | "intercom") {
556
- permissionDialogCategory.value = category;
557
- permissionSearch.value = "";
558
- permissionCandidates.value = [];
559
- selectedPermissionIds.value = new Set<string>();
560
- permissionDialog.value = true;
561
- loadingPermissionCandidates.value = true;
562
-
563
- try {
564
- const response = await getPermissionCandidates(props.site, {
565
- category,
566
- page: 1,
567
- limit: 500,
568
- });
569
- permissionCandidates.value = response.items || [];
570
- selectedPermissionIds.value = new Set(
571
- permissionCandidates.value
572
- .filter((candidate) => category === "intercom" ? candidate.intercom : candidate.selected)
573
- .map((candidate) => candidate.subjectId),
574
- );
575
- } catch (error: unknown) {
576
- showMessage(errorConverter(error), "error");
577
- } finally {
578
- loadingPermissionCandidates.value = false;
579
- }
580
- }
581
-
582
- function closePermissionDialog() {
583
- if (savingPermissions.value) return;
584
- permissionDialog.value = false;
585
- }
586
-
587
- function togglePermissionCandidate(subjectId: string, selected: boolean) {
588
- const next = new Set(selectedPermissionIds.value);
589
- if (selected) next.add(subjectId);
590
- else next.delete(subjectId);
591
- selectedPermissionIds.value = next;
592
- }
593
-
594
- async function savePermissions() {
595
- const category = permissionDialogCategory.value;
596
- let assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>;
597
-
598
- if (category === "intercom") {
599
- assignments = permissionState.assignments.map((assignment) => ({
600
- subjectId: assignment.subjectId,
601
- category: assignment.category,
602
- intercom: selectedPermissionIds.value.has(assignment.subjectId),
603
- }));
604
- } else {
605
- const retained = permissionState.assignments
606
- .filter((assignment) => assignment.category !== category)
607
- .map((assignment) => ({
608
- subjectId: assignment.subjectId,
609
- category: assignment.category,
610
- intercom: assignment.intercom,
611
- }));
612
- const selected = permissionCandidates.value
613
- .filter((candidate) => selectedPermissionIds.value.has(candidate.subjectId))
614
- .map((candidate) => ({
615
- subjectId: candidate.subjectId,
616
- category: candidate.category,
617
- intercom: candidate.intercom,
618
- }));
619
- assignments = [...retained, ...selected];
620
- }
621
-
622
- savingPermissions.value = true;
623
- try {
624
- const response = await updateSitePermissions(props.site, assignments);
625
- Object.assign(permissionState, response.data);
626
- permissionDialog.value = false;
627
- showMessage("HID permissions updated.", "success");
628
- } catch (error: unknown) {
629
- showMessage(errorConverter(error), "error");
630
- } finally {
631
- savingPermissions.value = false;
632
- }
633
- }
634
-
635
- function isUnknownRecord(value: unknown): value is Record<string, unknown> {
636
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
637
- }
638
-
639
- function getConfigurationData(response: unknown) {
640
- if (!isUnknownRecord(response)) return {};
641
- return isUnknownRecord(response.data) ? response.data : response;
642
- }
643
-
644
- function isEnabledConfiguration(value: unknown) {
645
- return value === true || value === 1 || value === "1";
646
- }
647
-
648
- async function loadReaderConfiguration() {
649
- if (!form.readerId) return;
650
-
651
- try {
652
- const response = await getReaderConfiguration(form.readerId, {
653
- general: ["online"],
654
- });
655
- const configuration = getConfigurationData(response);
656
- const general = isUnknownRecord(configuration.general) ? configuration.general : {};
657
-
658
- if (general.online !== undefined) {
659
- form.onlineMode = isEnabledConfiguration(general.online);
660
- }
661
- } catch (error: unknown) {
662
- console.warn("Unable to load HID reader configuration:", errorConverter(error));
663
- }
664
325
  }
665
326
 
666
327
  function buildPayload() {
667
328
  const metadata = {
668
329
  ...(siteData.value?.metadata || {}),
669
330
  hidQrCodePass: {
670
- enabled: form.identificationMethods.qrCode,
671
- onlineMode: form.onlineMode,
331
+ enabled: form.enabled,
672
332
  readerId: form.readerId,
673
333
  qrFormat: form.qrFormat,
674
- identificationMethods: { ...form.identificationMethods },
675
334
  printer: {
676
335
  vendorId: form.printer.vendorId.trim(),
677
336
  productId: form.printer.productId.trim(),
@@ -693,45 +352,17 @@ async function saveSection(section: string) {
693
352
 
694
353
  savingSection.value = section;
695
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
+ }
696
362
  await updateSitebyId(props.site, buildPayload());
697
363
  await loadConfig();
698
- showMessage("HID configuration saved.", "success");
699
- } catch (error: unknown) {
700
- showMessage(errorConverter(error), "error");
701
- } finally {
702
- savingSection.value = "";
703
- }
704
- }
705
-
706
- async function saveOnlineMode() {
707
- if (!props.site || !form.readerId) return;
708
-
709
- savingSection.value = "online-mode";
710
- try {
711
- await setReaderConfiguration(form.readerId, {
712
- general: { online: form.onlineMode ? "1" : "0" },
713
- });
714
- await updateSitebyId(props.site, buildPayload());
715
- showMessage("Online mode updated.", "success");
716
- } catch (error: unknown) {
717
- await loadConfig();
718
- showMessage(errorConverter(error), "error");
719
- } finally {
720
- savingSection.value = "";
721
- }
722
- }
723
-
724
- async function saveIdentificationMethod(method: HidIdentificationMethod) {
725
- if (!props.site) return;
726
-
727
- form.enabled = form.identificationMethods.qrCode;
728
-
729
- savingSection.value = `method-${method}`;
730
- try {
731
- await updateSitebyId(props.site, buildPayload());
732
- showMessage(`${identificationMethodOptions.find((item) => item.key === method)?.label || "Identification method"} updated.`, "success");
733
- } catch (error: unknown) {
734
- await loadConfig();
364
+ showMessage("HID QR code settings saved.", "success");
365
+ } catch (error: any) {
735
366
  showMessage(errorConverter(error), "error");
736
367
  } finally {
737
368
  savingSection.value = "";
@@ -745,36 +376,19 @@ async function scanPrinter() {
745
376
  }
746
377
 
747
378
  try {
748
- const device = await requestPrinterDevice();
379
+ const device = await (navigator as any).usb.requestDevice({ filters: [] });
749
380
  form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
750
381
  form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
751
382
  showMessage("Printer selected. Save settings to keep it.", "success");
752
- } catch (error: unknown) {
753
- const message = errorConverter(error);
754
- if (message !== "No device selected") {
755
- 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");
756
386
  }
757
387
  }
758
388
  }
759
389
 
760
- async function testPrint() {
761
- const vendorId = Number(form.printer.vendorId);
762
- const productId = Number(form.printer.productId);
763
- if (!vendorId || !productId) {
764
- showMessage("Please scan a valid USB printer first.", "error");
765
- return;
766
- }
767
-
768
- testingPrinter.value = true;
769
- try {
770
- const result = await testPrinterConnection(vendorId, productId);
771
- showMessage(
772
- result.success ? result.message : result.error || result.message,
773
- result.success ? "success" : "error",
774
- );
775
- } finally {
776
- testingPrinter.value = false;
777
- }
390
+ function testPrint() {
391
+ showMessage("Printer settings are ready. Connect print command when HID QR payload generation is available.", "success");
778
392
  }
779
393
 
780
394
  function showMessage(text: string, color: string) {
@@ -818,15 +432,15 @@ function showMessage(text: string, color: string) {
818
432
  padding: 30px 38px;
819
433
  }
820
434
 
821
- .online-mode-row {
435
+ .section-head {
822
436
  display: flex;
823
- align-items: center;
437
+ align-items: flex-start;
824
438
  justify-content: space-between;
825
439
  gap: 24px;
826
- padding: 0 0 20px;
440
+ margin-bottom: 24px;
827
441
  }
828
442
 
829
- .online-mode-row h3,
443
+ .section-head h3,
830
444
  .section-label {
831
445
  margin: 0 0 6px;
832
446
  color: #233545;
@@ -834,83 +448,16 @@ function showMessage(text: string, color: string) {
834
448
  font-weight: 700;
835
449
  }
836
450
 
837
- .online-mode-row p,
838
- .section-description {
451
+ .section-head p {
839
452
  margin: 0;
840
453
  color: #677280;
841
454
  font-size: 12px;
842
- line-height: 1.5;
843
455
  }
844
456
 
845
457
  .form-section {
846
458
  padding: 16px 0;
847
459
  }
848
460
 
849
- .identification-section {
850
- padding-bottom: 20px;
851
- }
852
-
853
- .methods-grid {
854
- display: grid;
855
- grid-template-columns: repeat(6, minmax(120px, 1fr));
856
- gap: 18px;
857
- margin-top: 16px;
858
- }
859
-
860
- .method-control {
861
- display: flex;
862
- align-items: center;
863
- justify-content: space-between;
864
- min-width: 0;
865
- min-height: 42px;
866
- padding: 0 10px 0 14px;
867
- border: 1px solid #dce2e7;
868
- border-radius: 6px;
869
- color: #334454;
870
- font-size: 12px;
871
- }
872
-
873
- .method-control span {
874
- overflow: hidden;
875
- text-overflow: ellipsis;
876
- white-space: nowrap;
877
- }
878
-
879
- .method-control--disabled {
880
- background-color: #f5f6f7;
881
- color: #8b9297;
882
- }
883
-
884
- .method-control :deep(.v-switch) {
885
- flex: 0 0 auto;
886
- }
887
-
888
- .method-control :deep(.v-selection-control) {
889
- min-height: 32px;
890
- }
891
-
892
- .hid-config-switch :deep(.v-switch__track) {
893
- background-color: #8b9297;
894
- opacity: 1;
895
- }
896
-
897
- .hid-config-switch :deep(.v-selection-control--dirty .v-switch__track) {
898
- background-color: #2eaf55 !important;
899
- color: #2eaf55 !important;
900
- opacity: 1;
901
- }
902
-
903
- .hid-config-switch :deep(.v-switch__thumb) {
904
- background-color: #fff !important;
905
- color: #fff !important;
906
- }
907
-
908
- .hid-config-switch.v-input--disabled :deep(.v-switch__track) {
909
- background-color: #aeb4b8 !important;
910
- color: #aeb4b8 !important;
911
- opacity: 1;
912
- }
913
-
914
461
  .printer-grid {
915
462
  display: grid;
916
463
  grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
@@ -936,177 +483,6 @@ function showMessage(text: string, color: string) {
936
483
  align-items: center;
937
484
  }
938
485
 
939
- .permission-section {
940
- padding-top: 2px;
941
- }
942
-
943
- .permission-row {
944
- display: grid;
945
- grid-template-columns: minmax(260px, 1fr) minmax(520px, 1.35fr);
946
- gap: 36px;
947
- align-items: center;
948
- padding: 18px 0;
949
- }
950
-
951
- .permission-row--intercom {
952
- padding-bottom: 4px;
953
- }
954
-
955
- .permission-copy {
956
- min-width: 0;
957
- }
958
-
959
- .permission-actions {
960
- display: grid;
961
- grid-template-columns: repeat(3, minmax(150px, 1fr));
962
- gap: 12px;
963
- }
964
-
965
- .permission-actions--single {
966
- grid-template-columns: minmax(180px, 240px);
967
- justify-content: end;
968
- }
969
-
970
- .permission-button {
971
- justify-content: flex-start;
972
- min-height: 42px;
973
- padding-inline: 12px;
974
- border-color: #dce2e7;
975
- color: #334454;
976
- }
977
-
978
- .permission-count {
979
- display: inline-grid;
980
- place-items: center;
981
- width: 20px;
982
- height: 20px;
983
- margin-right: 8px;
984
- border-radius: 50%;
985
- background: #dce2e7;
986
- color: #526170;
987
- font-size: 11px;
988
- font-weight: 700;
989
- }
990
-
991
- .permission-button:not(:disabled):hover {
992
- border-color: #1473c9;
993
- color: #0a5da7;
994
- }
995
-
996
- .permission-dialog {
997
- overflow: hidden;
998
- border-radius: 6px;
999
- }
1000
-
1001
- .permission-dialog__header {
1002
- display: flex;
1003
- align-items: flex-start;
1004
- justify-content: space-between;
1005
- gap: 20px;
1006
- padding: 20px 22px 16px;
1007
- border-bottom: 1px solid #e5e9ed;
1008
- }
1009
-
1010
- .permission-dialog__title {
1011
- color: #172534;
1012
- font-size: 18px;
1013
- font-weight: 700;
1014
- }
1015
-
1016
- .permission-dialog__subtitle {
1017
- margin-top: 4px;
1018
- color: #687583;
1019
- font-size: 12px;
1020
- white-space: normal;
1021
- }
1022
-
1023
- .permission-dialog__body {
1024
- display: grid;
1025
- gap: 16px;
1026
- padding: 18px 22px;
1027
- }
1028
-
1029
- .permission-list {
1030
- min-height: 240px;
1031
- max-height: 390px;
1032
- overflow-y: auto;
1033
- border: 1px solid #dfe5ea;
1034
- border-radius: 6px;
1035
- }
1036
-
1037
- .permission-list--loading {
1038
- display: grid;
1039
- place-items: center;
1040
- }
1041
-
1042
- .permission-list-item {
1043
- display: grid;
1044
- grid-template-columns: 34px minmax(0, 1fr) auto;
1045
- gap: 10px;
1046
- align-items: center;
1047
- min-height: 62px;
1048
- padding: 8px 14px 8px 8px;
1049
- border-bottom: 1px solid #edf0f3;
1050
- cursor: pointer;
1051
- }
1052
-
1053
- .permission-list-item:last-child {
1054
- border-bottom: 0;
1055
- }
1056
-
1057
- .permission-list-item:hover {
1058
- background: #f7fafc;
1059
- }
1060
-
1061
- .permission-list-item__content {
1062
- display: grid;
1063
- min-width: 0;
1064
- }
1065
-
1066
- .permission-list-item__content strong,
1067
- .permission-list-item__content small {
1068
- overflow: hidden;
1069
- text-overflow: ellipsis;
1070
- white-space: nowrap;
1071
- }
1072
-
1073
- .permission-list-item__content strong {
1074
- color: #243443;
1075
- font-size: 13px;
1076
- }
1077
-
1078
- .permission-list-item__content small,
1079
- .permission-list-item__group {
1080
- color: #778391;
1081
- font-size: 11px;
1082
- }
1083
-
1084
- .permission-list-item__group {
1085
- padding: 4px 8px;
1086
- border-radius: 4px;
1087
- background: #eef2f5;
1088
- }
1089
-
1090
- .permission-empty {
1091
- display: flex;
1092
- flex-direction: column;
1093
- align-items: center;
1094
- justify-content: center;
1095
- gap: 10px;
1096
- min-height: 240px;
1097
- padding: 24px;
1098
- color: #75818c;
1099
- font-size: 13px;
1100
- text-align: center;
1101
- }
1102
-
1103
- .permission-dialog__actions {
1104
- justify-content: flex-end;
1105
- gap: 8px;
1106
- padding: 14px 22px;
1107
- border-top: 1px solid #e5e9ed;
1108
- }
1109
-
1110
486
  .save-button {
1111
487
  background: #082e43;
1112
488
  color: #ffffff;
@@ -1216,20 +592,11 @@ function showMessage(text: string, color: string) {
1216
592
  }
1217
593
 
1218
594
  @media (max-width: 1100px) {
1219
- .methods-grid {
1220
- grid-template-columns: repeat(3, minmax(140px, 1fr));
1221
- }
1222
-
1223
595
  .printer-grid,
1224
596
  .template-grid,
1225
- .validity-grid,
1226
- .permission-row {
597
+ .validity-grid {
1227
598
  grid-template-columns: 1fr 1fr;
1228
599
  }
1229
-
1230
- .permission-actions {
1231
- grid-template-columns: 1fr;
1232
- }
1233
600
  }
1234
601
 
1235
602
  @media (max-width: 700px) {
@@ -1237,30 +604,15 @@ function showMessage(text: string, color: string) {
1237
604
  padding: 22px 16px;
1238
605
  }
1239
606
 
1240
- .online-mode-row,
607
+ .section-head,
1241
608
  .printer-grid,
1242
609
  .template-grid,
1243
- .validity-grid,
1244
- .methods-grid,
1245
- .permission-row,
1246
- .permission-actions {
610
+ .validity-grid {
1247
611
  grid-template-columns: 1fr;
1248
612
  }
1249
613
 
1250
- .online-mode-row {
614
+ .section-head {
1251
615
  display: grid;
1252
616
  }
1253
-
1254
- .permission-actions--single {
1255
- justify-content: stretch;
1256
- }
1257
-
1258
- .permission-list-item {
1259
- grid-template-columns: 34px minmax(0, 1fr);
1260
- }
1261
-
1262
- .permission-list-item__group {
1263
- display: none;
1264
- }
1265
617
  }
1266
618
  </style>