@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.
@@ -0,0 +1,273 @@
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">Visitor Arrival</p>
6
+ <InputDatePicker v-model="visitor.expectedCheckIn" placeholder="DD/MM/YYYY" :rules="[requiredRule]" />
7
+ </v-col>
8
+
9
+ <v-col cols="6" class="mb-4 pr-1">
10
+ <v-text-field
11
+ v-model="visitor.arrivalTime"
12
+ type="time"
13
+ label="Time"
14
+ density="comfortable"
15
+ variant="outlined"
16
+ hide-details
17
+ />
18
+ </v-col>
19
+ <v-col cols="6" class="mb-4 pl-1">
20
+ <v-text-field
21
+ v-model.number="visitor.duration"
22
+ type="number"
23
+ min="0"
24
+ label="Duration Hour(s)"
25
+ density="comfortable"
26
+ variant="outlined"
27
+ hide-details
28
+ />
29
+ </v-col>
30
+
31
+ <v-col cols="12" class="mb-2">
32
+ <p class="text-caption text-medium-emphasis">Visitor Information</p>
33
+ </v-col>
34
+
35
+ <v-col cols="12" class="mb-4">
36
+ <v-text-field
37
+ v-model="visitor.name"
38
+ label="Name"
39
+ density="comfortable"
40
+ variant="outlined"
41
+ :rules="[requiredRule]"
42
+ />
43
+ </v-col>
44
+
45
+ <v-col cols="12" class="mb-1">
46
+ <InputPhoneNumberV2 v-model="visitor.contact" density="comfortable" :rules="[requiredRule]" />
47
+ </v-col>
48
+ <v-col v-if="isContactPickerSupported" cols="12" class="mb-4 text-right">
49
+ <a href="#" class="text-caption" @click.prevent="fillFromContactPicker">
50
+ or select contact in your phone book
51
+ </a>
52
+ </v-col>
53
+
54
+ <v-col cols="12" class="mb-4">
55
+ <PublicOnboardingLocationFields
56
+ :site="site"
57
+ v-model:block="visitor.block"
58
+ v-model:level="visitor.level"
59
+ v-model:unit="visitor.unit"
60
+ />
61
+ </v-col>
62
+
63
+ <v-col cols="12" class="mb-4">
64
+ <v-text-field
65
+ v-model="visitor.email"
66
+ type="email"
67
+ label="Email"
68
+ density="comfortable"
69
+ variant="outlined"
70
+ hide-details
71
+ />
72
+ </v-col>
73
+
74
+ <v-col cols="12" class="mb-4">
75
+ <div class="d-flex align-center justify-space-between pa-3 bordered-field">
76
+ <span>Overnight Parking</span>
77
+ <v-switch
78
+ :model-value="visitor.isOvernightParking"
79
+ color="primary"
80
+ base-color="grey-lighten-1"
81
+ hide-details
82
+ @update:model-value="onToggleOvernightParking"
83
+ />
84
+ </div>
85
+ </v-col>
86
+
87
+ <v-col cols="12" class="mb-4">
88
+ <div class="d-flex align-center justify-space-between pa-3 bordered-field">
89
+ <span>No. of passenger (Optional)</span>
90
+ <div class="d-flex align-center ga-3">
91
+ <v-btn icon size="small" variant="outlined" :disabled="!visitor.numberOfPassengers" @click="visitor.numberOfPassengers = Math.max(0, (visitor.numberOfPassengers || 0) - 1)">
92
+ <v-icon>mdi-minus</v-icon>
93
+ </v-btn>
94
+ <span>{{ visitor.numberOfPassengers || 0 }}</span>
95
+ <v-btn icon size="small" variant="outlined" @click="visitor.numberOfPassengers = (visitor.numberOfPassengers || 0) + 1">
96
+ <v-icon>mdi-plus</v-icon>
97
+ </v-btn>
98
+ </div>
99
+ </div>
100
+ </v-col>
101
+
102
+ <v-col cols="12" class="mb-4">
103
+ <v-textarea
104
+ v-model="visitor.purpose"
105
+ label="Enter purpose of visit"
106
+ density="comfortable"
107
+ variant="outlined"
108
+ rows="4"
109
+ auto-grow
110
+ />
111
+ </v-col>
112
+
113
+ <v-col v-if="submitError" cols="12" class="mb-4">
114
+ <v-alert type="error" variant="tonal" density="compact">{{ submitError }}</v-alert>
115
+ </v-col>
116
+
117
+ <v-col cols="12">
118
+ <v-btn color="primary" variant="flat" block size="large" :loading="submitting" :disabled="!formValid" type="submit">
119
+ Submit
120
+ </v-btn>
121
+ </v-col>
122
+ </v-row>
123
+
124
+ <v-dialog v-model="showOvernightDialog" max-width="420" persistent>
125
+ <v-card class="pa-6">
126
+ <p class="text-body-1 text-center mb-4">
127
+ Overnight Parking requests outside our below business hours will not
128
+ be guaranteed a response
129
+ </p>
130
+ <div class="mb-4">
131
+ <div v-for="day in overnightScheduleDisplay" :key="day.label" class="d-flex justify-space-between text-body-2 py-1">
132
+ <span class="text-capitalize">{{ day.label }}</span>
133
+ <span>{{ day.text }}</span>
134
+ </div>
135
+ </div>
136
+ <v-row no-gutters class="ga-2">
137
+ <v-col>
138
+ <v-btn color="success" variant="tonal" block @click="confirmOvernightParking(true)">Yes</v-btn>
139
+ </v-col>
140
+ <v-col>
141
+ <v-btn color="error" variant="tonal" block @click="confirmOvernightParking(false)">No</v-btn>
142
+ </v-col>
143
+ </v-row>
144
+ </v-card>
145
+ </v-dialog>
146
+ </v-form>
147
+ </template>
148
+
149
+ <script lang="ts" setup>
150
+ const prop = defineProps({
151
+ site: { type: String, required: true },
152
+ });
153
+
154
+ const emit = defineEmits(["done"]);
155
+
156
+ const { createVisitor, getOvernightParkingHours } = usePublicVisitorOnboarding();
157
+ const { isSupported: isContactPickerSupported, pickContact } = useContactPicker();
158
+
159
+ const requiredRule = (v: any) => (v !== null && v !== undefined && v !== "") || "This field is required";
160
+
161
+ const visitor = reactive({
162
+ expectedCheckIn: null as string | null,
163
+ arrivalTime: "",
164
+ duration: null as number | null,
165
+ name: "",
166
+ contact: "",
167
+ block: "",
168
+ level: "",
169
+ unit: "",
170
+ email: "",
171
+ isOvernightParking: false,
172
+ numberOfPassengers: 0,
173
+ purpose: "",
174
+ });
175
+
176
+ const formValid = ref(false);
177
+ const submitting = ref(false);
178
+ const submitError = ref("");
179
+
180
+ async function fillFromContactPicker() {
181
+ const contact = await pickContact();
182
+ if (!contact) return;
183
+ if (contact.name) visitor.name = contact.name;
184
+ if (contact.tel) visitor.contact = contact.tel;
185
+ }
186
+
187
+ const DAY_LABELS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
188
+ const overnightSchedule = ref<Record<string, any>>({});
189
+ const showOvernightDialog = ref(false);
190
+
191
+ const overnightScheduleDisplay = computed(() =>
192
+ DAY_LABELS.map((day) => {
193
+ const d = overnightSchedule.value[day];
194
+ return {
195
+ label: day,
196
+ text: d?.isEnabled && d?.startTime && d?.endTime ? `${d.startTime} - ${d.endTime}` : "Closed",
197
+ };
198
+ }),
199
+ );
200
+
201
+ async function onToggleOvernightParking(value: boolean | null) {
202
+ if (!value) {
203
+ visitor.isOvernightParking = false;
204
+ return;
205
+ }
206
+ try {
207
+ const res: any = await getOvernightParkingHours(prop.site);
208
+ overnightSchedule.value = res?.data ?? res ?? {};
209
+ } catch {
210
+ overnightSchedule.value = {};
211
+ }
212
+ showOvernightDialog.value = true;
213
+ }
214
+
215
+ function confirmOvernightParking(confirmed: boolean) {
216
+ visitor.isOvernightParking = confirmed;
217
+ showOvernightDialog.value = false;
218
+ }
219
+
220
+ async function submit() {
221
+ submitError.value = "";
222
+ submitting.value = true;
223
+ try {
224
+ const payload: Record<string, any> = {
225
+ type: "guest",
226
+ name: visitor.name,
227
+ contact: visitor.contact,
228
+ email: visitor.email || undefined,
229
+ block: visitor.block || undefined,
230
+ level: visitor.level || undefined,
231
+ unit: visitor.unit || undefined,
232
+ expectedCheckIn: visitor.expectedCheckIn || undefined,
233
+ arrivalTime: visitor.arrivalTime || undefined,
234
+ duration: visitor.duration ?? undefined,
235
+ isOvernightParking: visitor.isOvernightParking,
236
+ numberOfPassengers: visitor.numberOfPassengers || undefined,
237
+ purpose: visitor.purpose || undefined,
238
+ };
239
+
240
+ const res: any = await createVisitor(prop.site, payload);
241
+ emit("done", res?.status || "pending", res?._id);
242
+ } catch (error: any) {
243
+ submitError.value =
244
+ error?.data?.message || error?.message || "Unable to submit your registration. Please try again.";
245
+ } finally {
246
+ submitting.value = false;
247
+ }
248
+ }
249
+ </script>
250
+
251
+ <style scoped>
252
+ /* Matches the surrounding v-text-field/v-select "outlined" variant's own
253
+ border exactly (Vuetify's field border color/opacity/radius tokens),
254
+ rather than an approximated color - these two rows aren't real Vuetify
255
+ fields, so without this they were the only inconsistent borders on the
256
+ form (one had a mismatched color/radius, the other had none at all). */
257
+ .bordered-field {
258
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
259
+ border-radius: 4px;
260
+ }
261
+
262
+ /* InputDatePicker/InputPhoneNumberV2 hardcode `.filter-field` on their own
263
+ internal v-text-field/v-select, independent of anything on this form's
264
+ own markup - that class sets border-radius: var(--r-inner) globally
265
+ (assets/css/screens.css), which is why they never matched the other
266
+ plain fields' 4px default no matter what class this component's own
267
+ elements had. Scoped :deep() here targets just their rendering within
268
+ this form (not the shared components' other usages elsewhere in the
269
+ app) with higher specificity than that global rule. */
270
+ :deep(.filter-field .v-field) {
271
+ border-radius: 4px;
272
+ }
273
+ </style>
@@ -457,6 +457,15 @@
457
457
  >
458
458
  {{ item?.overnightParking?.status }}
459
459
  </v-card>
460
+ <v-card
461
+ v-else-if="activeTab === 'guests' && item?.status"
462
+ size="small"
463
+ class="rounded-xl text-capitalize text-center elevation-0 py-1 px-2"
464
+ :color="getVisitorStatusColor(item.status)"
465
+ style="max-width: 150px; margin: auto"
466
+ >
467
+ {{ getVisitorStatusLabel(item.status) }}
468
+ </v-card>
460
469
  <span v-else> N/A </span>
461
470
  </template>
462
471
 
@@ -536,6 +545,27 @@
536
545
  <v-divider />
537
546
  </v-card>
538
547
  </v-menu>
548
+ <v-menu v-else-if="activeTab === 'guests' && item?.status === 'pending' && canUpdateVisitor">
549
+ <template #activator="{ props }">
550
+ <v-avatar v-bind="props" class="rounded-xl border-md">
551
+ <v-icon icon="mdi-dots-vertical" />
552
+ </v-avatar>
553
+ </template>
554
+
555
+ <v-card>
556
+ <v-list-item @click.stop="openApproveRejectVisitorDialog(item, 'approved')">
557
+ <template #title>
558
+ <span class="text-caption">Approve</span>
559
+ </template>
560
+ </v-list-item>
561
+ <v-divider />
562
+ <v-list-item @click.stop="openApproveRejectVisitorDialog(item, 'rejected')">
563
+ <template #title>
564
+ <span class="text-caption">Reject</span>
565
+ </template>
566
+ </v-list-item>
567
+ </v-card>
568
+ </v-menu>
539
569
  </template>
540
570
 
541
571
  <template v-slot:item.checkInRemarks="{ item }">
@@ -1179,6 +1209,78 @@
1179
1209
  </v-card>
1180
1210
  </v-dialog>
1181
1211
 
1212
+ <v-dialog
1213
+ v-model="dialog.approveRejectVisitorDialog"
1214
+ transition="dialog-bottom-transition"
1215
+ persistent
1216
+ width="60vh"
1217
+ >
1218
+ <v-card>
1219
+ <v-toolbar density="compact">
1220
+ <v-row
1221
+ no-gutters
1222
+ class="d-flex fill-height justify-space-between align-center px-4"
1223
+ >
1224
+ <span class="font-weight-bold">{{ approveRejectVisitorDialogTitle }}</span>
1225
+ <v-btn
1226
+ icon="mdi-close"
1227
+ variant="text"
1228
+ @click="closeApproveRejectVisitorDialog"
1229
+ />
1230
+ </v-row>
1231
+ </v-toolbar>
1232
+ <v-card-text class="px-4 pb-6">
1233
+ <v-row no-gutters justify="center" align-content="center">
1234
+ <v-col cols="12" class="text-h6 text-center">
1235
+ {{
1236
+ `Are you sure you want to ${approveRejectVisitorDialog.text} the ${approveRejectVisitorDialogTitle}?`
1237
+ }}
1238
+ </v-col>
1239
+
1240
+ <v-col cols="12" md="8" class="mt-4">
1241
+ <v-textarea
1242
+ v-model="visitorApproveRejectRemarks"
1243
+ label="Remarks"
1244
+ placeholder="Enter remarks"
1245
+ outlined
1246
+ rows="3"
1247
+ clearable
1248
+ />
1249
+ </v-col>
1250
+ </v-row>
1251
+ </v-card-text>
1252
+ <v-toolbar class="pa-0" density="compact">
1253
+ <v-row no-gutters>
1254
+ <v-col cols="6">
1255
+ <v-btn
1256
+ text="No"
1257
+ color="grey-lighten-3"
1258
+ variant="flat"
1259
+ height="48"
1260
+ tile
1261
+ block
1262
+ :disabled="isApprovingRejectingVisitor"
1263
+ @click="closeApproveRejectVisitorDialog"
1264
+ />
1265
+ </v-col>
1266
+ <v-col cols="6">
1267
+ <v-btn
1268
+ text="Yes"
1269
+ color="success"
1270
+ variant="flat"
1271
+ height="48"
1272
+ tile
1273
+ block
1274
+ :disabled="!visitorApproveRejectRemarks || isApprovingRejectingVisitor"
1275
+ @click="updateVisitorApprovalStatus"
1276
+ :loading="isApprovingRejectingVisitor"
1277
+ />
1278
+ </v-col>
1279
+ </v-row>
1280
+ </v-toolbar>
1281
+ </v-card>
1282
+ </v-dialog>
1283
+
1182
1284
  <v-dialog v-model="hidQrDialog" max-width="440" persistent>
1183
1285
  <v-card>
1184
1286
  <v-toolbar color="transparent" density="comfortable" class="px-2">
@@ -1436,6 +1538,7 @@ const dialog = reactive({
1436
1538
  showVisitorDataFromScannedQRCode: false,
1437
1539
  approveRejectOvernightParkingRequestDialog: false,
1438
1540
  returnNfcCard: false,
1541
+ approveRejectVisitorDialog: false,
1439
1542
  });
1440
1543
 
1441
1544
  const snapshotImageUrl = ref("");
@@ -1488,6 +1591,18 @@ const headers = computed(() => {
1488
1591
  list.push({ title: "Action", value: "action" });
1489
1592
  }
1490
1593
 
1594
+ // Guests is the only non-overnight-parking tab whose rows carry a
1595
+ // top-level status (approved/pending/rejected) and an Approve/Reject
1596
+ // action - without these two, the item.status/item.action template slots
1597
+ // have no header to render against and v-data-table never shows them,
1598
+ // no matter what their own v-if says.
1599
+ if (tab === "guests") {
1600
+ list.push(
1601
+ { title: "Status", value: "status" },
1602
+ { title: "Action", value: "action" },
1603
+ );
1604
+ }
1605
+
1491
1606
  return list;
1492
1607
  });
1493
1608
 
@@ -1590,6 +1705,77 @@ function closeApproveRejectOvernightParkingDialog() {
1590
1705
  overNightParkingRequestApproveRejectRemarks.value = "";
1591
1706
  }
1592
1707
 
1708
+ /** Self-service submissions arrive with `status: "pending"` when the site's
1709
+ Self-Service Auto-Approvals toggle is off - Approve/Reject here is what
1710
+ actually decides the visitor's outcome, mirroring the Overnight Parking
1711
+ approve/reject pattern above but against the top-level visitor status
1712
+ (via the existing staff update endpoint) rather than a nested field. */
1713
+ function getVisitorStatusColor(status: string) {
1714
+ switch (status) {
1715
+ case "approved":
1716
+ return "success";
1717
+ case "rejected":
1718
+ return "error";
1719
+ case "pending":
1720
+ return "warning";
1721
+ default:
1722
+ return "grey";
1723
+ }
1724
+ }
1725
+
1726
+ function getVisitorStatusLabel(status: string) {
1727
+ return status === "pending" ? "Pending Approval" : status;
1728
+ }
1729
+
1730
+ const selectedVisitorForApproval = ref<Record<string, any>>({});
1731
+ const visitorApproveRejectRemarks = ref("");
1732
+ const approveRejectVisitorDialog = ref({ text: "", status: "" });
1733
+ const isApprovingRejectingVisitor = ref(false);
1734
+
1735
+ const approveRejectVisitorDialogTitle = computed(() => {
1736
+ const type = selectedVisitorForApproval.value?.type as string | undefined;
1737
+ const label = type ? type.replace("-", " ") : "Visitor";
1738
+ return `${label.charAt(0).toUpperCase()}${label.slice(1)} Registration Request`;
1739
+ });
1740
+
1741
+ function openApproveRejectVisitorDialog(visitor: Record<string, any>, status: "approved" | "rejected") {
1742
+ selectedVisitorForApproval.value = visitor;
1743
+ approveRejectVisitorDialog.value = {
1744
+ text: status === "approved" ? "Approve" : "Reject",
1745
+ status,
1746
+ };
1747
+ visitorApproveRejectRemarks.value = "";
1748
+ dialog.approveRejectVisitorDialog = true;
1749
+ }
1750
+
1751
+ function closeApproveRejectVisitorDialog() {
1752
+ dialog.approveRejectVisitorDialog = false;
1753
+ selectedVisitorForApproval.value = {};
1754
+ approveRejectVisitorDialog.value = { text: "", status: "" };
1755
+ visitorApproveRejectRemarks.value = "";
1756
+ }
1757
+
1758
+ async function updateVisitorApprovalStatus() {
1759
+ isApprovingRejectingVisitor.value = true;
1760
+ try {
1761
+ await updateVisitor(selectedVisitorForApproval.value._id, {
1762
+ status: approveRejectVisitorDialog.value.status,
1763
+ remarks: visitorApproveRejectRemarks.value,
1764
+ updatedBy: currentUser.value?._id,
1765
+ } as any);
1766
+ showMessage(
1767
+ `Visitor registration successfully ${approveRejectVisitorDialog.value.status}`,
1768
+ "success"
1769
+ );
1770
+ await getVisitorRefresh();
1771
+ closeApproveRejectVisitorDialog();
1772
+ } catch (error) {
1773
+ showMessage(errorConverter(error), "error");
1774
+ } finally {
1775
+ isApprovingRejectingVisitor.value = false;
1776
+ }
1777
+ }
1778
+
1593
1779
  function mappedAttachments(attachments: string[]) {
1594
1780
  return attachments.map((x, index) => {
1595
1781
  return {
@@ -1665,17 +1851,12 @@ function toRoute(tab: any) {
1665
1851
 
1666
1852
  const obj = tabOptions.value.find((x) => x.value === tab);
1667
1853
  if (!obj) return;
1854
+ // Don't call getVisitorRefresh() here - activeTab is already in the
1855
+ // useLazyAsyncData `watch` array below, so changing it (which already
1856
+ // happened via v-model before this handler runs) already triggers a
1857
+ // refetch on its own. Calling refresh() again here fired the same
1858
+ // request a second time on every tab click.
1668
1859
  page.value = 1;
1669
- getVisitorRefresh();
1670
- // navigateTo({
1671
- // name: routeName,
1672
- // params: {
1673
- // org: orgId,
1674
- // },
1675
- // // query: {
1676
- // // tab
1677
- // // },
1678
- // });
1679
1860
  }
1680
1861
  const {
1681
1862
  data: getVisitorReq,
@@ -1702,8 +1883,15 @@ const {
1702
1883
  };
1703
1884
 
1704
1885
  if (activeTab.value === "guests") {
1705
- params.type = "guest";
1706
- params.status = "approved";
1886
+ // Self-service Contractor submissions land here too, not just Visitor
1887
+ // (type: "guest") - the "Registered" tab hard-filters status="registered",
1888
+ // which a pending/approved self-service contractor never has, so without
1889
+ // this it had no tab that would ever show it.
1890
+ params.type = "guest,contractor";
1891
+ // A self-service submission awaiting Approve/Reject (Self-Service
1892
+ // Auto-Approvals off) lands here as "pending" alongside already
1893
+ // "approved" guests, rather than being hidden until someone acts on it.
1894
+ params.status = "approved,pending";
1707
1895
  } else if (activeTab.value === "resident-transactions") {
1708
1896
  params.status = "registered";
1709
1897
  params.type = "resident,tenant";
@@ -2237,8 +2425,11 @@ function buildReportParams(): any {
2237
2425
  };
2238
2426
 
2239
2427
  if (activeTab.value === "guests") {
2240
- params.type = "guest";
2241
- params.status = "approved";
2428
+ // Kept in sync with the Guests tab's own list query above - otherwise a
2429
+ // report generated from this tab would silently omit contractors and
2430
+ // pending rows the screen is actually showing.
2431
+ params.type = "guest,contractor";
2432
+ params.status = "approved,pending";
2242
2433
  } else if (activeTab.value === "resident-transactions") {
2243
2434
  params.status = "registered";
2244
2435
  params.type = "resident,tenant";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Wraps the browser's Contact Picker API ("or select contact in your phone
3
+ * book" on the self-service onboarding form). Support is narrow - Android
4
+ * Chrome/Edge over HTTPS only, not iOS Safari or desktop - so `isSupported`
5
+ * must gate whether the UI even offers this, rather than showing a button
6
+ * that silently does nothing everywhere else.
7
+ */
8
+ export default function useContactPicker() {
9
+ const isSupported = computed(() => {
10
+ return (
11
+ typeof navigator !== "undefined" &&
12
+ "contacts" in navigator &&
13
+ "ContactsManager" in window
14
+ );
15
+ });
16
+
17
+ async function pickContact(): Promise<{ name?: string; tel?: string } | null> {
18
+ if (!isSupported.value) return null;
19
+ try {
20
+ const props = ["name", "tel"];
21
+ const contacts = await (navigator as any).contacts.select(props, { multiple: false });
22
+ const contact = contacts?.[0];
23
+ if (!contact) return null;
24
+ return {
25
+ name: contact.name?.[0],
26
+ tel: contact.tel?.[0],
27
+ };
28
+ } catch {
29
+ // User cancelled the picker, or the browser refused - either way this
30
+ // is not an error worth surfacing, the visitor can just type instead.
31
+ return null;
32
+ }
33
+ }
34
+
35
+ return { isSupported, pickContact };
36
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Public, unauthenticated calls for the self-service visitor onboarding page
3
+ * (reached by scanning the gate QR code printed from Site Settings > Entry
4
+ * Pass > Self-Service). Every endpoint here is deliberately a separate,
5
+ * narrower public route from its staff-facing equivalent in `useBuilding`/
6
+ * `useVisitor`/`useSiteEntryPassSettings` - see the server-side route
7
+ * comments for why each one is safe to leave unauthenticated.
8
+ */
9
+ export default function usePublicVisitorOnboarding() {
10
+ function getOnboardingSettings(siteId: string) {
11
+ return useNuxtApp().$api<Record<string, any>>(
12
+ `/api/access-management/settings/${siteId}/onboarding`,
13
+ { method: "GET" },
14
+ );
15
+ }
16
+
17
+ function getBlocks(siteId: string) {
18
+ return useNuxtApp().$api<Record<string, any>>(
19
+ `/api/buildings/list/site/resident/${siteId}`,
20
+ { method: "GET" },
21
+ );
22
+ }
23
+
24
+ function getLevels(siteId: string, blockId: string) {
25
+ return useNuxtApp().$api<Record<string, any>>(
26
+ `/api/building-levels/list/site/resident/${siteId}`,
27
+ { method: "GET", query: { block: blockId } },
28
+ );
29
+ }
30
+
31
+ function getUnits(siteId: string, block: string, level: string) {
32
+ return useNuxtApp().$api<Record<string, any>>(
33
+ `/api/building-units/resident/site/${siteId}/block/${block}/level/${level}`,
34
+ { method: "GET" },
35
+ );
36
+ }
37
+
38
+ function createVisitor(siteId: string, payload: Record<string, any>) {
39
+ return useNuxtApp().$api<Record<string, any>>(
40
+ `/api/visitor-transactions/self-service/${siteId}`,
41
+ { method: "POST", body: payload },
42
+ );
43
+ }
44
+
45
+ // The "2" collection is the one actually populated/used by
46
+ // OvernightParkingAvailability.vue - the un-suffixed v1 collection this
47
+ // used to read from is a separate, unpopulated legacy store.
48
+ function getOvernightParkingHours(siteId: string) {
49
+ return useNuxtApp().$api<Record<string, any>>(
50
+ `/api/overnight-parking-approval-settings2/site/${siteId}/public`,
51
+ { method: "GET" },
52
+ );
53
+ }
54
+
55
+ // Backs the visitor's own preview/QR page, linked from their registration
56
+ // email so it still works after they've closed the browser.
57
+ function getSelfServicePreview(id: string) {
58
+ return useNuxtApp().$api<Record<string, any>>(
59
+ `/api/visitor-transactions/self-service/preview/${id}`,
60
+ { method: "GET" },
61
+ );
62
+ }
63
+
64
+ return {
65
+ getOnboardingSettings,
66
+ getBlocks,
67
+ getLevels,
68
+ getUnits,
69
+ createVisitor,
70
+ getOvernightParkingHours,
71
+ getSelfServicePreview,
72
+ };
73
+ }