@7365admin1/layer-common 4.0.4-staging.241 → 4.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a45deb4: Add a canonical permission catalogue and a test that fails when the UI and the server stop agreeing
8
+
9
+ `constants/permissions.ts` exports the canonical spelling of every permission the estate uses, typed so a misspelt string is a build error rather than a check that is silently false forever, plus a map from each legacy spelling (four for work orders, two each for visitors, bulletin boards, buildings and service providers) to its canonical form.
10
+
11
+ Additive only: no gate is rewired and no existing check changes the string it asks for. `test/permission-vocabulary.test.mjs` fails when a new string appears in this package, or when the server's enforced set — snapshotted in `test/fixtures/server-permissions.json` by `tools/refresh-server-permissions.mjs` — contains something the catalogue has no name for.
12
+
3
13
  ## 4.0.3
4
14
 
5
15
  ### Patch Changes
@@ -280,11 +290,11 @@
280
290
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
281
291
  own rule from two endpoints the console already calls, unprojected:
282
292
 
283
- GET /api/members/user/:user/app/admin the Seven365 staff membership
284
- GET /api/roles/id/:role that membership's role document
293
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
294
+ GET /api/roles/id/:role that membership's role document
285
295
 
286
- owner = member.type === "admin" && role.type === "admin" && role.default === true
287
- staff = member.type === "admin" && role.type === "admin"
296
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
297
+ staff = member.type === "admin" && role.type === "admin"
288
298
 
289
299
  `role.default` is the marker because it is the only property of a platform
290
300
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <v-dialog
3
+ :model-value="modelValue"
4
+ transition="dialog-right-transition"
5
+ width="420"
6
+ persistent
7
+ @update:model-value="emit('update:modelValue', $event)"
8
+ >
9
+ <v-card class="screen-modal">
10
+ <div class="d-flex justify-end pa-2">
11
+ <v-btn
12
+ variant="text"
13
+ icon="mdi-close"
14
+ class="screen-modal__close"
15
+ aria-label="Close preview"
16
+ @click="emit('update:modelValue', false)"
17
+ ></v-btn>
18
+ </div>
19
+
20
+ <v-card-text class="screen-modal__body pa-0 pb-4">
21
+ <!-- Matches the printed A5 layout: one QR, a "scan" subtext, and a
22
+ dark banner carrying the site name - not the multi-copy receipt
23
+ `EntryPassQrTemplatePreview` renders for physical passes. -->
24
+ <div class="onboarding-qr-preview mx-6">
25
+ <img v-if="qrImage" :src="qrImage" width="240" height="240" class="d-block mx-auto" />
26
+ <p class="onboarding-qr-preview__subtext text-center mt-4">
27
+ {{ (subText || "SCAN QR CODE").toUpperCase() }}
28
+ </p>
29
+ </div>
30
+ <div class="onboarding-qr-preview__banner text-center mt-4 py-4">
31
+ {{ (header || "").toUpperCase() }}
32
+ </div>
33
+ </v-card-text>
34
+ </v-card>
35
+ </v-dialog>
36
+ </template>
37
+
38
+ <script lang="ts" setup>
39
+ import QRCode from "qrcode";
40
+
41
+ const props = defineProps({
42
+ modelValue: { type: Boolean, default: false },
43
+ header: { type: String, default: "" },
44
+ subText: { type: String, default: "" },
45
+ qrValue: { type: String, default: "" },
46
+ });
47
+
48
+ const emit = defineEmits(["update:modelValue"]);
49
+
50
+ const qrImage = ref<string>("");
51
+
52
+ async function renderQr() {
53
+ if (!props.qrValue) {
54
+ qrImage.value = "";
55
+ return;
56
+ }
57
+ qrImage.value = await QRCode.toDataURL(props.qrValue, { width: 240, margin: 1 });
58
+ }
59
+
60
+ watch(() => props.qrValue, renderQr);
61
+ onMounted(renderQr);
62
+ </script>
63
+
64
+ <style scoped>
65
+ .onboarding-qr-preview__subtext {
66
+ font-size: var(--fs-card-title);
67
+ font-weight: var(--fw-card-title);
68
+ color: var(--text);
69
+ letter-spacing: 0.5px;
70
+ }
71
+
72
+ .onboarding-qr-preview__banner {
73
+ background: #000;
74
+ color: #fff;
75
+ font-size: 22px;
76
+ font-weight: 700;
77
+ letter-spacing: 2px;
78
+ }
79
+ </style>
@@ -391,6 +391,157 @@
391
391
 
392
392
  </v-row>
393
393
  </v-window-item>
394
+
395
+ <!-- ─── Tab 5: Self-Service ───────────────────────────── -->
396
+ <v-window-item value="onboarding">
397
+ <v-row no-gutters class="px-6 pt-4">
398
+
399
+ <!-- Self-Service Auto-Approvals -->
400
+ <v-col cols="12">
401
+ <div class="d-flex mt-3 pb-5 pr-5">
402
+ <p class="mr-6 font-weight-medium flex-grow-1">
403
+ Self-Service Auto-Approvals
404
+ <span class="d-block text-caption screen-hint mt-1">
405
+ Automatically issue active passes the moment a contractor or
406
+ visitor submits their registration form via the self-service
407
+ web portal.
408
+ </span>
409
+ </p>
410
+ <v-switch
411
+ color="success"
412
+ hide-details
413
+ v-model="currentEntryPassSettings.onboardingEnabled"
414
+ @update:modelValue="updateOnboardingEnabled()"
415
+ />
416
+ </div>
417
+ </v-col>
418
+
419
+ <v-divider />
420
+
421
+ <!-- Printer Setup -->
422
+ <v-col cols="12">
423
+ <div class="mt-5 pb-5">
424
+ <p class="font-weight-medium mb-3">Printer Setup</p>
425
+ <v-row align="center">
426
+ <v-col cols="12" md="3">
427
+ <v-text-field
428
+ label="Vendor ID"
429
+ hide-details
430
+ clearable
431
+ v-model="currentEntryPassSettings.onboardingPrinterVendorId"
432
+ />
433
+ </v-col>
434
+ <v-col cols="12" md="3">
435
+ <v-text-field
436
+ label="Product ID"
437
+ hide-details
438
+ clearable
439
+ v-model="currentEntryPassSettings.onboardingPrinterProductId"
440
+ />
441
+ </v-col>
442
+ <v-col cols="12" md="3" class="d-flex align-end">
443
+ <v-btn
444
+ color="primary"
445
+ variant="outlined"
446
+ prepend-icon="mdi-magnify"
447
+ block
448
+ height="48"
449
+ :loading="scanningOnboardingPrinter"
450
+ @click="scanOnboardingPrinter()"
451
+ >
452
+ Scan Printer
453
+ </v-btn>
454
+ </v-col>
455
+ <v-col cols="12" md="3" class="d-flex align-end pr-0">
456
+ <v-btn
457
+ color="success"
458
+ variant="outlined"
459
+ prepend-icon="mdi-printer-check"
460
+ block
461
+ height="48"
462
+ :loading="testingOnboardingPrinter"
463
+ @click="testAndPrintOnboarding()"
464
+ >
465
+ Test & Print
466
+ </v-btn>
467
+ </v-col>
468
+ </v-row>
469
+ <v-row class="mt-4">
470
+ <v-col cols="12" md="3">
471
+ <v-btn
472
+ color="primary"
473
+ variant="flat"
474
+ prepend-icon="mdi-content-save"
475
+ block
476
+ @click="saveOnboardingSettings()"
477
+ >
478
+ Save Settings
479
+ </v-btn>
480
+ </v-col>
481
+ </v-row>
482
+ </div>
483
+ </v-col>
484
+
485
+ <v-divider />
486
+
487
+ <!-- Self-Service Pass QR Code -->
488
+ <v-col cols="12">
489
+ <div class="mt-5 pb-5">
490
+ <p class="font-weight-medium mb-3">Self-Service Pass QR Code</p>
491
+ <v-row no-gutters>
492
+ <v-col cols="12" md="6" class="pr-md-2">
493
+ <v-text-field
494
+ label="Header"
495
+ placeholder="Enter QR code header"
496
+ hide-details
497
+ density="comfortable"
498
+ clearable
499
+ v-model="currentEntryPassSettings.onboardingHeader"
500
+ />
501
+ </v-col>
502
+ <v-col cols="12" md="6" class="pl-md-2 mt-3 mt-md-0">
503
+ <v-text-field
504
+ label="Subtext"
505
+ placeholder="Enter QR code subtext"
506
+ hide-details
507
+ density="comfortable"
508
+ clearable
509
+ v-model="currentEntryPassSettings.onboardingSubText"
510
+ />
511
+ </v-col>
512
+ <v-col cols="12">
513
+ <v-row class="mt-3">
514
+ <v-col cols="12" md="4">
515
+ <v-btn
516
+ color="success"
517
+ variant="flat"
518
+ prepend-icon="mdi-eye"
519
+ block
520
+ @click="isShowOnboardingPreview = true"
521
+ >
522
+ Preview
523
+ </v-btn>
524
+ </v-col>
525
+ <v-col cols="12" md="4">
526
+ <v-btn
527
+ color="primary"
528
+ variant="flat"
529
+ prepend-icon="mdi-printer"
530
+ block
531
+ :loading="printingOnboardingQr"
532
+ @click="printOnboardingQrCode()"
533
+ >
534
+ Print QR Code
535
+ </v-btn>
536
+ </v-col>
537
+ </v-row>
538
+ </v-col>
539
+ </v-row>
540
+ </div>
541
+ </v-col>
542
+
543
+ </v-row>
544
+ </v-window-item>
394
545
  </v-window>
395
546
  </AppCard>
396
547
 
@@ -401,6 +552,13 @@
401
552
  :header="currentEntryPassSettings.qrCodeTemplateHeader"
402
553
  :sub-text="currentEntryPassSettings.qrCodeTemplateHeaderSubText"
403
554
  />
555
+
556
+ <EntryPassSelfServiceQrPreview
557
+ v-model="isShowOnboardingPreview"
558
+ :header="currentEntryPassSettings.onboardingHeader"
559
+ :sub-text="currentEntryPassSettings.onboardingSubText"
560
+ :qr-value="onboardingQrValue"
561
+ />
404
562
  </div>
405
563
  </template>
406
564
 
@@ -420,6 +578,7 @@ const entryPassTabs = [
420
578
  { value: "qrcode", label: "QR Code", icon: "mdi-qrcode" },
421
579
  { value: "printer", label: "Printer", icon: "mdi-printer" },
422
580
  { value: "entrypass", label: "Entrypass", icon: "mdi-link-variant" },
581
+ { value: "onboarding", label: "Self-Service", icon: "mdi-account-multiple-check-outline" },
423
582
  ];
424
583
 
425
584
  const currentEntryPassSettings = ref<Record<string, any>>({
@@ -445,6 +604,11 @@ const currentEntryPassSettings = ref<Record<string, any>>({
445
604
  disclaimerMessage: "",
446
605
  url: "",
447
606
  template: { id: "", name: "" },
607
+ onboardingEnabled: false,
608
+ onboardingHeader: "",
609
+ onboardingSubText: "",
610
+ onboardingPrinterVendorId: "",
611
+ onboardingPrinterProductId: "",
448
612
  status: "",
449
613
  createdBy: "",
450
614
  updatedBy: "",
@@ -474,6 +638,18 @@ const isShowTemplatePreview = ref(false);
474
638
  const uploadingTemplate = ref(false);
475
639
  const templateFileInput = ref<HTMLInputElement | null>(null);
476
640
 
641
+ const scanningOnboardingPrinter = ref(false);
642
+ const testingOnboardingPrinter = ref(false);
643
+ const printingOnboardingQr = ref(false);
644
+ const isShowOnboardingPreview = ref(false);
645
+
646
+ /** The URL printed on the self-service QR code — the public, unauthenticated
647
+ page a walk-up visitor lands on after scanning it at the gate. */
648
+ const runtimeConfig = useRuntimeConfig().public as Record<string, any>;
649
+ const onboardingQrValue = computed(
650
+ () => `${runtimeConfig.APP_PROPERTY_MANAGEMENT}/public-view/visitor-onboarding/${siteId}`
651
+ );
652
+
477
653
  function triggerTemplateUpload() {
478
654
  templateFileInput.value?.click();
479
655
  }
@@ -614,6 +790,15 @@ watchEffect(() => {
614
790
  disclaimerMessage: data.settings?.disclaimerMessage ?? "",
615
791
  url: data.settings?.url ?? "",
616
792
  template: data.settings?.template ?? { id: "", name: "" },
793
+ onboardingEnabled: data.settings?.onboarding?.enabled ?? false,
794
+ onboardingHeader: data.settings?.onboarding?.header ?? "",
795
+ onboardingSubText: data.settings?.onboarding?.subText ?? "",
796
+ onboardingPrinterVendorId: data.settings?.onboarding?.printer?.vendorId
797
+ ? String(data.settings.onboarding.printer.vendorId)
798
+ : "",
799
+ onboardingPrinterProductId: data.settings?.onboarding?.printer?.productId
800
+ ? String(data.settings.onboarding.printer.productId)
801
+ : "",
617
802
  createdAt: data.createdAt ?? "",
618
803
  updatedAt: data.updatedAt ?? "",
619
804
  };
@@ -651,6 +836,15 @@ function buildPayload() {
651
836
  template: currentEntryPassSettings.value.template?.id
652
837
  ? currentEntryPassSettings.value.template
653
838
  : null,
839
+ onboarding: {
840
+ enabled: currentEntryPassSettings.value.onboardingEnabled ?? false,
841
+ header: currentEntryPassSettings.value.onboardingHeader || "",
842
+ subText: currentEntryPassSettings.value.onboardingSubText || "",
843
+ printer: {
844
+ vendorId: Number(currentEntryPassSettings.value.onboardingPrinterVendorId) || null,
845
+ productId: Number(currentEntryPassSettings.value.onboardingPrinterProductId) || null,
846
+ },
847
+ },
654
848
  },
655
849
  };
656
850
  }
@@ -693,6 +887,101 @@ async function saveEntryPassUrl() {
693
887
  getSiteEntryPassSettings();
694
888
  }
695
889
 
890
+ async function updateOnboardingEnabled() {
891
+ await _save(buildPayload());
892
+ showMessage("Self-service auto-approval updated!", "success");
893
+ getSiteEntryPassSettings();
894
+ }
895
+
896
+ async function scanOnboardingPrinter() {
897
+ if (!isWebUsbSupported.value) {
898
+ showMessage("Web USB is not supported in this browser", "error");
899
+ return;
900
+ }
901
+ try {
902
+ scanningOnboardingPrinter.value = true;
903
+ const device = await requestDevice();
904
+ currentEntryPassSettings.value.onboardingPrinterVendorId = String(device.vendorId);
905
+ currentEntryPassSettings.value.onboardingPrinterProductId = String(device.productId);
906
+ showMessage(
907
+ `Printer found: ${device.productName} (${device.vendorId}:${device.productId})`,
908
+ "success",
909
+ );
910
+ } catch (error: any) {
911
+ if (error.message !== "No device selected") {
912
+ showMessage(error.message || "Failed to scan printer", "error");
913
+ }
914
+ } finally {
915
+ scanningOnboardingPrinter.value = false;
916
+ }
917
+ }
918
+
919
+ async function testAndPrintOnboarding() {
920
+ if (!isWebUsbSupported.value) {
921
+ showMessage("Web USB is not supported in this browser", "error");
922
+ return;
923
+ }
924
+ const vendorId = Number(currentEntryPassSettings.value.onboardingPrinterVendorId);
925
+ const productId = Number(currentEntryPassSettings.value.onboardingPrinterProductId);
926
+ if (!vendorId || !productId) {
927
+ showMessage("Please set a valid Vendor ID and Product ID first", "warning");
928
+ return;
929
+ }
930
+ try {
931
+ testingOnboardingPrinter.value = true;
932
+ const result = await testConnection(vendorId, productId);
933
+ showMessage(result.message, result.success ? "success" : "error");
934
+ } catch (error: any) {
935
+ showMessage(error.message || "Test print failed", "error");
936
+ } finally {
937
+ testingOnboardingPrinter.value = false;
938
+ }
939
+ }
940
+
941
+ async function saveOnboardingSettings() {
942
+ await _save(buildPayload());
943
+ showMessage("Self-service settings saved successfully!", "success");
944
+ getSiteEntryPassSettings();
945
+ }
946
+
947
+ async function printOnboardingQrCode() {
948
+ if (!isWebUsbSupported.value) {
949
+ showMessage("Web USB is not supported in this browser", "error");
950
+ return;
951
+ }
952
+ const vendorId = Number(currentEntryPassSettings.value.onboardingPrinterVendorId);
953
+ const productId = Number(currentEntryPassSettings.value.onboardingPrinterProductId);
954
+ if (!vendorId || !productId) {
955
+ showMessage("Please set up and save a printer first", "warning");
956
+ return;
957
+ }
958
+ try {
959
+ printingOnboardingQr.value = true;
960
+ // Header/subtext are printed onto the physical QR, so persist them
961
+ // before sending to the printer - otherwise a printed pass could show
962
+ // text the settings screen never actually saved.
963
+ await _save(buildPayload());
964
+ const result = await testConnection(
965
+ vendorId,
966
+ productId,
967
+ true,
968
+ onboardingQrValue.value,
969
+ null,
970
+ null,
971
+ undefined,
972
+ undefined,
973
+ currentEntryPassSettings.value.onboardingHeader,
974
+ currentEntryPassSettings.value.onboardingSubText,
975
+ "onboarding",
976
+ );
977
+ showMessage(result.message, result.success ? "success" : "error");
978
+ } catch (error: any) {
979
+ showMessage(error.message || "Failed to print QR code", "error");
980
+ } finally {
981
+ printingOnboardingQr.value = false;
982
+ }
983
+ }
984
+
696
985
  function showMessage(msg: string, color: string) {
697
986
  message.value = msg;
698
987
  messageColor.value = color;