@7365admin1/layer-common 3.0.8 → 3.0.9

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 (31) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/components/DashboardMain.vue +1894 -844
  3. package/components/EntryPassInformation.vue +60 -0
  4. package/components/HidAccessLogDashboard.vue +1691 -0
  5. package/components/HidEnabledGate.vue +51 -0
  6. package/components/HidIdentityMapping.vue +975 -0
  7. package/components/HidQrCodeConfiguration.vue +618 -0
  8. package/components/HidReaderForm.vue +246 -0
  9. package/components/HidReaderManagement.vue +1233 -0
  10. package/components/HidServiceSettingsPanel.vue +150 -0
  11. package/components/HidUserEnrollment.vue +1274 -0
  12. package/components/Layout/NavigationDrawer.vue +31 -1
  13. package/components/NavigationItem.vue +11 -4
  14. package/components/SiteSettings.vue +24 -1
  15. package/components/VisitorForm.vue +195 -4
  16. package/components/VisitorFormSelection.vue +12 -2
  17. package/composables/useDashboard.ts +29 -2
  18. package/composables/useHidAmico.ts +152 -0
  19. package/composables/useHidNavigation.ts +106 -0
  20. package/composables/useMember.ts +7 -1
  21. package/composables/useOptionalServices.ts +118 -0
  22. package/composables/useSiteSettings.ts +1 -1
  23. package/composables/useUser.ts +1 -2
  24. package/nuxt.config.ts +1 -1
  25. package/package.json +2 -1
  26. package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
  27. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
  28. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
  29. package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
  30. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
  31. package/types/local.d.ts +2 -1
@@ -0,0 +1,618 @@
1
+ <template>
2
+ <section class="hid-qr-config">
3
+ <v-card flat class="config-panel">
4
+ <div class="panel-header">
5
+ <div>
6
+ <h2>HID Configuration</h2>
7
+ </div>
8
+ <v-btn icon="mdi-chevron-up" variant="text" density="comfortable" />
9
+ </div>
10
+
11
+ <div class="config-body">
12
+ <div class="section-head">
13
+ <div>
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
+ </div>
17
+ <v-switch
18
+ v-model="form.enabled"
19
+ color="success"
20
+ hide-details
21
+ inset
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"
39
+ />
40
+ </div>
41
+
42
+ <v-divider />
43
+
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')"
65
+ >
66
+ Save Format
67
+ </v-btn>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="form-section">
72
+ <div class="section-label">Printer Setup</div>
73
+ <div class="printer-grid">
74
+ <v-text-field
75
+ v-model="form.printer.vendorId"
76
+ placeholder="Vendor"
77
+ variant="outlined"
78
+ density="compact"
79
+ hide-details
80
+ />
81
+ <v-text-field
82
+ v-model="form.printer.productId"
83
+ placeholder="Product Id"
84
+ variant="outlined"
85
+ density="compact"
86
+ hide-details
87
+ />
88
+ <v-btn
89
+ class="text-none utility-button"
90
+ variant="outlined"
91
+ prepend-icon="mdi-magnify"
92
+ height="40"
93
+ @click="scanPrinter"
94
+ >
95
+ Scan Printer
96
+ </v-btn>
97
+ <v-btn
98
+ class="text-none test-button"
99
+ variant="outlined"
100
+ prepend-icon="mdi-printer-check"
101
+ height="40"
102
+ :disabled="!hasPrinterConfig"
103
+ @click="testPrint"
104
+ >
105
+ Test & Print
106
+ </v-btn>
107
+ <v-btn
108
+ class="text-none save-button"
109
+ variant="flat"
110
+ prepend-icon="mdi-printer"
111
+ height="40"
112
+ :loading="savingSection === 'printer'"
113
+ @click="saveSection('printer')"
114
+ >
115
+ Save Settings
116
+ </v-btn>
117
+ </div>
118
+ </div>
119
+
120
+ <v-divider />
121
+
122
+ <div class="form-section">
123
+ <div class="section-label">HID QR Code Template</div>
124
+ <div class="template-grid">
125
+ <v-text-field
126
+ v-model="form.template.header"
127
+ placeholder="Header"
128
+ variant="outlined"
129
+ density="compact"
130
+ hide-details
131
+ />
132
+ <v-text-field
133
+ v-model="form.template.subtext"
134
+ placeholder="Subtext"
135
+ variant="outlined"
136
+ density="compact"
137
+ hide-details
138
+ />
139
+ <v-btn
140
+ class="text-none preview-button"
141
+ variant="flat"
142
+ prepend-icon="mdi-eye"
143
+ height="40"
144
+ @click="previewDialog = true"
145
+ >
146
+ Preview
147
+ </v-btn>
148
+ <v-btn
149
+ class="text-none save-button"
150
+ variant="flat"
151
+ prepend-icon="mdi-qrcode-scan"
152
+ height="40"
153
+ :loading="savingSection === 'template'"
154
+ @click="saveSection('template')"
155
+ >
156
+ Save
157
+ </v-btn>
158
+ </div>
159
+ </div>
160
+
161
+ <v-divider />
162
+
163
+ <div class="form-section">
164
+ <div class="section-label">HID QR Code Validity</div>
165
+ <div class="validity-grid">
166
+ <v-text-field
167
+ v-model.number="form.validityMinutes"
168
+ placeholder="Validity (minutes)"
169
+ variant="outlined"
170
+ density="compact"
171
+ hide-details
172
+ min="1"
173
+ type="number"
174
+ />
175
+ <v-btn
176
+ class="text-none save-button"
177
+ variant="flat"
178
+ prepend-icon="mdi-timer-outline"
179
+ height="40"
180
+ :loading="savingSection === 'validity'"
181
+ @click="saveSection('validity')"
182
+ >
183
+ Save
184
+ </v-btn>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </v-card>
189
+
190
+ <v-dialog v-model="previewDialog" max-width="420">
191
+ <v-card class="preview-shell">
192
+ <v-btn
193
+ class="preview-close"
194
+ icon="mdi-close"
195
+ variant="text"
196
+ density="comfortable"
197
+ @click="previewDialog = false"
198
+ />
199
+ <v-card-text>
200
+ <div class="preview-title">QR code Preview</div>
201
+ <div class="pass-preview">
202
+ <div class="pass-heading">{{ form.template.header || "Header" }}</div>
203
+ <div class="pass-subtext">{{ form.template.subtext || "Subtext" }}</div>
204
+
205
+ <div class="qr-grid">
206
+ <div class="qr-placeholder small"><span>No QR</span></div>
207
+ <div class="qr-placeholder small"><span>No QR</span></div>
208
+ <div class="qr-placeholder large"><span>No QR payload</span></div>
209
+ <div class="qr-placeholder small"><span>No QR</span></div>
210
+ <div class="qr-placeholder small"><span>No QR</span></div>
211
+ </div>
212
+
213
+ <div class="pass-footer">
214
+ <strong>{{ siteName }}</strong>
215
+ <strong>{{ siteLabel }}</strong>
216
+ <span>Validity: {{ validityText }}</span>
217
+ <span>Access: Door</span>
218
+ <span>Generated QR payload will render here.</span>
219
+ </div>
220
+ </div>
221
+ </v-card-text>
222
+ </v-card>
223
+ </v-dialog>
224
+
225
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
226
+ </section>
227
+ </template>
228
+
229
+ <script setup lang="ts">
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;
241
+ };
242
+ validityMinutes: number | null;
243
+ };
244
+
245
+ const props = defineProps<{
246
+ site: string;
247
+ }>();
248
+
249
+ const { getSiteById, updateSitebyId } = useSiteSettings();
250
+ const { getReaders, setReaderConfiguration } = useHidAmico();
251
+
252
+ const defaultConfig: HidQrConfig = {
253
+ enabled: false,
254
+ readerId: "",
255
+ qrFormat: "0",
256
+ printer: {
257
+ vendorId: "",
258
+ productId: "",
259
+ },
260
+ template: {
261
+ header: "",
262
+ subtext: "",
263
+ },
264
+ validityMinutes: null,
265
+ };
266
+
267
+ const siteData = ref<Record<string, any> | null>(null);
268
+ const readers = ref<Record<string, any>[]>([]);
269
+ const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
270
+ const previewDialog = ref(false);
271
+ const savingSection = ref("");
272
+ const message = ref("");
273
+ const messageColor = ref("success");
274
+ const messageSnackbar = ref(false);
275
+
276
+ const hasPrinterConfig = computed(() =>
277
+ Boolean(form.printer.vendorId && form.printer.productId),
278
+ );
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" },
287
+ ];
288
+
289
+ const siteName = computed(() => siteData.value?.name || "Site");
290
+ const siteLabel = computed(() => siteData.value?.code || siteData.value?.block || props.site);
291
+ const validityText = computed(() =>
292
+ form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
293
+ );
294
+
295
+ watch(
296
+ () => props.site,
297
+ () => loadConfig(),
298
+ { immediate: true },
299
+ );
300
+
301
+ async function loadConfig() {
302
+ if (!props.site) return;
303
+
304
+ const [site, readerResponse] = await Promise.all([
305
+ getSiteById(props.site),
306
+ getReaders({ site: props.site, page: 1, limit: 100 }),
307
+ ]);
308
+ siteData.value = site;
309
+ readers.value = readerResponse?.items ?? readerResponse?.data?.items ?? readerResponse?.data?.readers ?? [];
310
+ const config = siteData.value?.metadata?.hidQrCodePass || {};
311
+ Object.assign(form, {
312
+ enabled: Boolean(config.enabled),
313
+ readerId: String(config.readerId || readers.value[0]?._id || ""),
314
+ qrFormat: ["0", "1", "2"].includes(String(config.qrFormat)) ? String(config.qrFormat) as "0" | "1" | "2" : "0",
315
+ printer: {
316
+ vendorId: String(config.printer?.vendorId || ""),
317
+ productId: String(config.printer?.productId || ""),
318
+ },
319
+ template: {
320
+ header: String(config.template?.header || ""),
321
+ subtext: String(config.template?.subtext || ""),
322
+ },
323
+ validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
324
+ });
325
+ }
326
+
327
+ function buildPayload() {
328
+ const metadata = {
329
+ ...(siteData.value?.metadata || {}),
330
+ hidQrCodePass: {
331
+ enabled: form.enabled,
332
+ readerId: form.readerId,
333
+ qrFormat: form.qrFormat,
334
+ printer: {
335
+ vendorId: form.printer.vendorId.trim(),
336
+ productId: form.printer.productId.trim(),
337
+ },
338
+ template: {
339
+ header: form.template.header.trim(),
340
+ subtext: form.template.subtext.trim(),
341
+ },
342
+ validityMinutes: form.validityMinutes ? Number(form.validityMinutes) : null,
343
+ updatedAt: new Date().toISOString(),
344
+ },
345
+ };
346
+
347
+ return { metadata };
348
+ }
349
+
350
+ async function saveSection(section: string) {
351
+ if (!props.site) return;
352
+
353
+ savingSection.value = section;
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
+ }
362
+ await updateSitebyId(props.site, buildPayload());
363
+ await loadConfig();
364
+ showMessage("HID QR code settings saved.", "success");
365
+ } catch (error: any) {
366
+ showMessage(errorConverter(error), "error");
367
+ } finally {
368
+ savingSection.value = "";
369
+ }
370
+ }
371
+
372
+ async function scanPrinter() {
373
+ if (!("usb" in navigator)) {
374
+ showMessage("USB printer scan is not available in this browser.", "error");
375
+ return;
376
+ }
377
+
378
+ try {
379
+ const device = await (navigator as any).usb.requestDevice({ filters: [] });
380
+ form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
381
+ form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
382
+ showMessage("Printer selected. Save settings to keep it.", "success");
383
+ } catch (error: any) {
384
+ if (error?.name !== "NotFoundError") {
385
+ showMessage(error?.message || "Unable to scan printer.", "error");
386
+ }
387
+ }
388
+ }
389
+
390
+ function testPrint() {
391
+ showMessage("Printer settings are ready. Connect print command when HID QR payload generation is available.", "success");
392
+ }
393
+
394
+ function showMessage(text: string, color: string) {
395
+ message.value = text;
396
+ messageColor.value = color;
397
+ messageSnackbar.value = true;
398
+ }
399
+ </script>
400
+
401
+ <style scoped>
402
+ .hid-qr-config {
403
+ width: 100%;
404
+ min-width: 0;
405
+ padding: 8px 0 28px;
406
+ }
407
+
408
+ .config-panel {
409
+ border: 1px solid #e6eaee;
410
+ border-radius: 6px;
411
+ overflow: hidden;
412
+ background: #ffffff;
413
+ }
414
+
415
+ .panel-header {
416
+ display: flex;
417
+ align-items: center;
418
+ justify-content: space-between;
419
+ min-height: 48px;
420
+ padding: 0 18px;
421
+ background: #082e43;
422
+ color: #ffffff;
423
+ }
424
+
425
+ .panel-header h2 {
426
+ margin: 0;
427
+ font-size: 13px;
428
+ font-weight: 700;
429
+ }
430
+
431
+ .config-body {
432
+ padding: 30px 38px;
433
+ }
434
+
435
+ .section-head {
436
+ display: flex;
437
+ align-items: flex-start;
438
+ justify-content: space-between;
439
+ gap: 24px;
440
+ margin-bottom: 24px;
441
+ }
442
+
443
+ .section-head h3,
444
+ .section-label {
445
+ margin: 0 0 6px;
446
+ color: #233545;
447
+ font-size: 13px;
448
+ font-weight: 700;
449
+ }
450
+
451
+ .section-head p {
452
+ margin: 0;
453
+ color: #677280;
454
+ font-size: 12px;
455
+ }
456
+
457
+ .form-section {
458
+ padding: 16px 0;
459
+ }
460
+
461
+ .printer-grid {
462
+ display: grid;
463
+ grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
464
+ gap: 14px;
465
+ align-items: center;
466
+ }
467
+
468
+ .template-grid {
469
+ display: grid;
470
+ grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr);
471
+ gap: 14px;
472
+ }
473
+
474
+ .template-grid .preview-button,
475
+ .template-grid .save-button {
476
+ width: 100%;
477
+ }
478
+
479
+ .validity-grid {
480
+ display: grid;
481
+ grid-template-columns: minmax(180px, 260px) minmax(140px, 260px);
482
+ gap: 14px;
483
+ align-items: center;
484
+ }
485
+
486
+ .save-button {
487
+ background: #082e43;
488
+ color: #ffffff;
489
+ }
490
+
491
+ .preview-button {
492
+ background: #35b84b;
493
+ color: #ffffff;
494
+ }
495
+
496
+ .test-button {
497
+ border-color: #34b84a;
498
+ color: #2da643;
499
+ }
500
+
501
+ .utility-button {
502
+ color: #1f3448;
503
+ }
504
+
505
+ .preview-shell {
506
+ position: relative;
507
+ overflow: visible;
508
+ background: transparent;
509
+ box-shadow: none;
510
+ }
511
+
512
+ .preview-close {
513
+ position: absolute;
514
+ top: 38px;
515
+ right: 18px;
516
+ z-index: 2;
517
+ }
518
+
519
+ .preview-title {
520
+ margin: 0 0 12px;
521
+ color: #ffffff;
522
+ font-size: 18px;
523
+ font-weight: 700;
524
+ }
525
+
526
+ .pass-preview {
527
+ width: 244px;
528
+ min-height: 360px;
529
+ margin: 0 auto;
530
+ padding: 22px 28px;
531
+ background: #ffffff;
532
+ color: #252f3b;
533
+ text-align: center;
534
+ }
535
+
536
+ .pass-heading {
537
+ min-height: 20px;
538
+ font-size: 16px;
539
+ font-weight: 700;
540
+ }
541
+
542
+ .pass-subtext {
543
+ min-height: 18px;
544
+ color: #6b7280;
545
+ font-size: 11px;
546
+ }
547
+
548
+ .qr-grid {
549
+ display: grid;
550
+ grid-template-columns: 1fr 1fr;
551
+ gap: 14px 36px;
552
+ justify-items: center;
553
+ margin: 16px 0;
554
+ }
555
+
556
+ .qr-placeholder {
557
+ display: grid;
558
+ place-items: center;
559
+ border: 1px dashed #aab3bd;
560
+ background:
561
+ linear-gradient(45deg, #f4f6f8 25%, transparent 25%),
562
+ linear-gradient(-45deg, #f4f6f8 25%, transparent 25%),
563
+ linear-gradient(45deg, transparent 75%, #f4f6f8 75%),
564
+ linear-gradient(-45deg, transparent 75%, #f4f6f8 75%);
565
+ background-size: 10px 10px;
566
+ background-position: 0 0, 0 5px, 5px -5px, -5px 0;
567
+ color: #6b7280;
568
+ font-size: 9px;
569
+ }
570
+
571
+ .qr-placeholder.small {
572
+ width: 54px;
573
+ height: 54px;
574
+ }
575
+
576
+ .qr-placeholder.large {
577
+ grid-column: 1 / -1;
578
+ width: 96px;
579
+ height: 96px;
580
+ }
581
+
582
+ .pass-footer {
583
+ display: grid;
584
+ gap: 4px;
585
+ color: #4b5563;
586
+ font-size: 10px;
587
+ }
588
+
589
+ .pass-footer strong {
590
+ color: #263241;
591
+ font-size: 15px;
592
+ }
593
+
594
+ @media (max-width: 1100px) {
595
+ .printer-grid,
596
+ .template-grid,
597
+ .validity-grid {
598
+ grid-template-columns: 1fr 1fr;
599
+ }
600
+ }
601
+
602
+ @media (max-width: 700px) {
603
+ .config-body {
604
+ padding: 22px 16px;
605
+ }
606
+
607
+ .section-head,
608
+ .printer-grid,
609
+ .template-grid,
610
+ .validity-grid {
611
+ grid-template-columns: 1fr;
612
+ }
613
+
614
+ .section-head {
615
+ display: grid;
616
+ }
617
+ }
618
+ </style>