@7365admin1/layer-common 4.0.4-staging.241 → 4.0.4-staging.243

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.
@@ -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;
@@ -0,0 +1,278 @@
1
+ <template>
2
+ <v-form v-model="formValid" @submit.prevent="submit">
3
+ <v-row no-gutters>
4
+ <v-col cols="12" class="mb-4">
5
+ <p class="text-caption text-medium-emphasis mb-2">Arrival Date</p>
6
+ <InputDatePicker v-model="visitor.expectedCheckIn" placeholder="DD/MM/YYYY" :rules="[requiredRule]" />
7
+ </v-col>
8
+
9
+ <v-col cols="12" class="mb-4">
10
+ <v-select
11
+ v-model="visitor.contractorType"
12
+ :items="contractorTypes"
13
+ item-title="title"
14
+ item-value="value"
15
+ label="Contractor Type"
16
+ density="comfortable"
17
+ variant="outlined"
18
+ hide-details
19
+ />
20
+ </v-col>
21
+
22
+ <v-col cols="12" class="mb-4">
23
+ <v-text-field
24
+ v-model="visitor.name"
25
+ label="Name"
26
+ density="comfortable"
27
+ variant="outlined"
28
+ :rules="[requiredRule]"
29
+ />
30
+ </v-col>
31
+
32
+ <v-col cols="12" class="mb-4">
33
+ <v-text-field
34
+ v-model="visitor.nric"
35
+ label="NRIC / Passport No. (Optional)"
36
+ density="comfortable"
37
+ variant="outlined"
38
+ hide-details
39
+ />
40
+ </v-col>
41
+
42
+ <v-col cols="12" class="mb-4">
43
+ <v-text-field
44
+ v-model="visitor.email"
45
+ type="email"
46
+ label="Email"
47
+ density="comfortable"
48
+ variant="outlined"
49
+ hide-details
50
+ />
51
+ </v-col>
52
+
53
+ <v-col cols="12" class="mb-1">
54
+ <InputPhoneNumberV2 v-model="visitor.contact" density="comfortable" :rules="[requiredRule]" />
55
+ </v-col>
56
+ <v-col v-if="isContactPickerSupported" cols="12" class="mb-4 text-right">
57
+ <a href="#" class="text-caption" @click.prevent="fillFromContactPicker">
58
+ or select contact in your phone book
59
+ </a>
60
+ </v-col>
61
+
62
+ <v-col cols="12" class="mb-4">
63
+ <PublicOnboardingLocationFields
64
+ :site="site"
65
+ v-model:block="visitor.block"
66
+ v-model:level="visitor.level"
67
+ v-model:unit="visitor.unit"
68
+ />
69
+ </v-col>
70
+
71
+ <v-col cols="12" class="mb-4">
72
+ <v-text-field
73
+ v-model="visitor.company"
74
+ label="Company Name"
75
+ density="comfortable"
76
+ variant="outlined"
77
+ :rules="[requiredRule]"
78
+ />
79
+ </v-col>
80
+
81
+ <v-col cols="12" class="mb-4">
82
+ <v-text-field
83
+ v-model="visitor.plateNumber"
84
+ label="Vehicle Number (Optional)"
85
+ density="comfortable"
86
+ variant="outlined"
87
+ hide-details
88
+ />
89
+ </v-col>
90
+
91
+ <v-col cols="12" class="mb-4">
92
+ <v-btn variant="outlined" block class="justify-space-between" append-icon="mdi-pencil-outline" @click="showMembersDialog = true">
93
+ {{ members.length > 0 ? `${members.length} Member(s) Added` : "Add Member" }}
94
+ </v-btn>
95
+ </v-col>
96
+
97
+ <v-col cols="12" class="mb-4">
98
+ <v-textarea
99
+ v-model="visitor.remarks"
100
+ label="Remarks (optional)"
101
+ density="comfortable"
102
+ variant="outlined"
103
+ rows="3"
104
+ auto-grow
105
+ />
106
+ </v-col>
107
+
108
+ <v-col v-if="submitError" cols="12" class="mb-4">
109
+ <v-alert type="error" variant="tonal" density="compact">{{ submitError }}</v-alert>
110
+ </v-col>
111
+
112
+ <v-col cols="12">
113
+ <v-btn color="primary" variant="flat" block size="large" :loading="submitting" :disabled="!formValid" type="submit">
114
+ Submit
115
+ </v-btn>
116
+ </v-col>
117
+ </v-row>
118
+
119
+ <v-dialog v-model="showMembersDialog" max-width="420" persistent>
120
+ <v-card class="pa-4">
121
+ <div class="d-flex align-center mb-4">
122
+ <v-btn icon variant="text" @click="showMembersDialog = false">
123
+ <v-icon>mdi-chevron-left</v-icon>
124
+ </v-btn>
125
+ <span class="text-subtitle-1 ml-2">Members</span>
126
+ </div>
127
+
128
+ <div
129
+ v-for="(member, index) in draftMembers"
130
+ :key="index"
131
+ class="pa-3 mb-3 bordered-field"
132
+ >
133
+ <v-text-field
134
+ v-model="member.name"
135
+ label="Name"
136
+ density="comfortable"
137
+ variant="outlined"
138
+ hide-details
139
+ class="mb-2"
140
+ />
141
+ <v-text-field
142
+ v-model="member.nric"
143
+ label="NRIC / Passport No. (Optional)"
144
+ density="comfortable"
145
+ variant="outlined"
146
+ hide-details
147
+ class="mb-2"
148
+ />
149
+ <InputPhoneNumberV2 v-model="member.contact" density="comfortable" hide-details />
150
+ </div>
151
+
152
+ <v-btn variant="outlined" block class="mb-3" prepend-icon="mdi-plus" @click="addDraftMember">
153
+ Add
154
+ </v-btn>
155
+ <v-btn color="primary" variant="flat" block @click="saveMembers">Save</v-btn>
156
+ </v-card>
157
+ </v-dialog>
158
+ </v-form>
159
+ </template>
160
+
161
+ <script lang="ts" setup>
162
+ const prop = defineProps({
163
+ site: { type: String, required: true },
164
+ });
165
+
166
+ const emit = defineEmits(["done"]);
167
+
168
+ const { createVisitor } = usePublicVisitorOnboarding();
169
+ const { isSupported: isContactPickerSupported, pickContact } = useContactPicker();
170
+
171
+ const requiredRule = (v: any) => (v !== null && v !== undefined && v !== "") || "This field is required";
172
+
173
+ const contractorTypes = [
174
+ { title: "Home Contractor", value: "home-contractor" },
175
+ { title: "House Mover", value: "house-mover" },
176
+ { title: "Estate Contractor", value: "estate-contractor" },
177
+ { title: "Property Agent", value: "property-agent" },
178
+ ];
179
+
180
+ const visitor = reactive({
181
+ expectedCheckIn: null as string | null,
182
+ contractorType: "",
183
+ name: "",
184
+ nric: "",
185
+ email: "",
186
+ contact: "",
187
+ block: "",
188
+ level: "",
189
+ unit: "",
190
+ company: "",
191
+ plateNumber: "",
192
+ remarks: "",
193
+ });
194
+
195
+ type TDraftMember = { name: string; nric: string; contact: string };
196
+ const members = ref<TDraftMember[]>([]);
197
+ const draftMembers = ref<TDraftMember[]>([]);
198
+ const showMembersDialog = ref(false);
199
+
200
+ watch(showMembersDialog, (open) => {
201
+ if (open) {
202
+ draftMembers.value = members.value.length
203
+ ? members.value.map((m) => ({ ...m }))
204
+ : [{ name: "", nric: "", contact: "" }];
205
+ }
206
+ });
207
+
208
+ function addDraftMember() {
209
+ draftMembers.value.push({ name: "", nric: "", contact: "" });
210
+ }
211
+
212
+ function saveMembers() {
213
+ members.value = draftMembers.value.filter((m) => m.name && m.contact);
214
+ showMembersDialog.value = false;
215
+ }
216
+
217
+ const formValid = ref(false);
218
+ const submitting = ref(false);
219
+ const submitError = ref("");
220
+
221
+ async function fillFromContactPicker() {
222
+ const contact = await pickContact();
223
+ if (!contact) return;
224
+ if (contact.name) visitor.name = contact.name;
225
+ if (contact.tel) visitor.contact = contact.tel;
226
+ }
227
+
228
+ async function submit() {
229
+ submitError.value = "";
230
+ submitting.value = true;
231
+ try {
232
+ const payload: Record<string, any> = {
233
+ type: "contractor",
234
+ contractorType: visitor.contractorType || undefined,
235
+ name: visitor.name,
236
+ contact: visitor.contact,
237
+ nric: visitor.nric || undefined,
238
+ email: visitor.email || undefined,
239
+ block: visitor.block || undefined,
240
+ level: visitor.level || undefined,
241
+ unit: visitor.unit || undefined,
242
+ company: visitor.company,
243
+ plateNumber: visitor.plateNumber || undefined,
244
+ remarks: visitor.remarks || undefined,
245
+ members: members.value.length ? members.value : undefined,
246
+ };
247
+
248
+ const res: any = await createVisitor(prop.site, payload);
249
+ emit("done", res?.status || "pending");
250
+ } catch (error: any) {
251
+ submitError.value =
252
+ error?.data?.message || error?.message || "Unable to submit your registration. Please try again.";
253
+ } finally {
254
+ submitting.value = false;
255
+ }
256
+ }
257
+ </script>
258
+
259
+ <style scoped>
260
+ /* Matches the surrounding v-text-field/InputPhoneNumberV2 fields' own
261
+ border color (Vuetify's border color/opacity token) - the per-member row
262
+ in the Members dialog isn't a real Vuetify field, so without this it was
263
+ the one inconsistent border on the form. */
264
+ .bordered-field {
265
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
266
+ border-radius: 4px;
267
+ }
268
+
269
+ /* InputDatePicker/InputPhoneNumberV2 hardcode `.filter-field` on their own
270
+ internal v-text-field/v-select, which sets border-radius: var(--r-inner)
271
+ globally (assets/css/screens.css) - overriding it just within this form
272
+ (including inside the teleported Members dialog; :deep()'s scoping
273
+ attribute stays on the element through teleport) rather than in the
274
+ shared components themselves, which are also used elsewhere. */
275
+ :deep(.filter-field .v-field) {
276
+ border-radius: 4px;
277
+ }
278
+ </style>