@7365admin1/layer-common 4.90.2-staging.490 → 4.90.2

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 (67) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/assets/css/primitives.css +0 -57
  3. package/assets/css/screens.css +4 -8
  4. package/components/AccessCardDetailsDialog.vue +6 -59
  5. package/components/AccessCardPreviewDialog.vue +53 -371
  6. package/components/AppSelect.vue +4 -143
  7. package/components/BuildingUnitFormEdit.vue +0 -6
  8. package/components/Dialog/UpdateMoreAction.vue +1 -11
  9. package/components/DocumentForm.vue +112 -407
  10. package/components/DocumentManagement.vue +12 -213
  11. package/components/Facility/BookingSetup.vue +0 -63
  12. package/components/HidAccessLogDashboard.vue +17 -74
  13. package/components/HidIntercomManagement.vue +14 -183
  14. package/components/HidQrCodeConfiguration.vue +195 -17
  15. package/components/HidReaderManagement.vue +0 -398
  16. package/components/HidReaderUserRoster.vue +3 -44
  17. package/components/HidUserEnrollment.vue +292 -2255
  18. package/components/InvitationClientForm.vue +1 -19
  19. package/components/InvitationForm.vue +4 -31
  20. package/components/InvitationMain.vue +22 -124
  21. package/components/MemberInformation.vue +4 -4
  22. package/components/PassInformation.vue +2 -2
  23. package/components/SiteSettings.vue +0 -48
  24. package/components/TableMain.vue +7 -22
  25. package/components/VehicleManagement.vue +5 -80
  26. package/components/VisitorForm.vue +19 -65
  27. package/components/VisitorManagement.vue +5 -267
  28. package/composables/useAccessManagement.ts +1 -17
  29. package/composables/useDocument.ts +0 -7
  30. package/composables/useFacility.ts +0 -11
  31. package/composables/useHidAmico.ts +0 -165
  32. package/composables/useHidNavigation.ts +1 -25
  33. package/composables/useMember.ts +0 -12
  34. package/composables/useNFCPatrolReportFilters.ts +3 -21
  35. package/composables/useNFCPatrolRoute.ts +1 -8
  36. package/composables/usePeople.ts +0 -19
  37. package/composables/useServiceProvider.ts +2 -45
  38. package/composables/useSettingsPermission.ts +0 -14
  39. package/composables/useUser.ts +2 -6
  40. package/composables/useVehicle.ts +1 -40
  41. package/composables/useVerification.ts +0 -3
  42. package/middleware/01.auth.ts +7 -3
  43. package/package.json +1 -1
  44. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  45. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  46. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  47. package/types/document.d.ts +0 -14
  48. package/types/facility.d.ts +0 -4
  49. package/types/member.d.ts +0 -4
  50. package/types/people.d.ts +1 -28
  51. package/types/service-provider.d.ts +0 -5
  52. package/types/site.d.ts +1 -7
  53. package/components/BuildingUnitManagement.vue +0 -298
  54. package/components/HidAccessPermissions.vue +0 -424
  55. package/components/HidProfileQrCode.vue +0 -332
  56. package/components/VehicleQrStickerDialog.vue +0 -249
  57. package/composables/useHidReaderSelection.ts +0 -50
  58. package/composables/useSiteCategory.ts +0 -45
  59. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  60. package/utils/hid-enrolment-subject.ts +0 -344
  61. package/utils/hid-permission-assignments.ts +0 -198
  62. package/utils/hid-reader-selection.ts +0 -52
  63. package/utils/invite-group.ts +0 -61
  64. package/utils/invite-sites.ts +0 -25
  65. package/utils/occupancy-role.ts +0 -109
  66. package/utils/service-type.ts +0 -30
  67. package/utils/vehicle-qr-sticker.ts +0 -184
@@ -388,27 +388,9 @@ watch(
388
388
  const roles = ref<any[]>([])
389
389
  const { getRoles } = useRole()
390
390
 
391
- /*
392
- * `type` is the ROLE type, not the service the client subscribes to, and the
393
- * only caller (`ClientMain.vue` "Invite Client") passes no `app` prop -- so
394
- * this sent `type: ""` and the endpoint applied no type filter at all. The
395
- * dropdown therefore listed every org-less role of every type, which includes
396
- * the platform's OWN staff roles (`type: "admin"` -- the two rows the console's
397
- * `/super-admin/role-permissions` screen edits). Offering a Seven365 console
398
- * staff role on a CLIENT invitation is the defect.
399
- *
400
- * A client admin's role is `type: "organization"`, which is what the client
401
- * roles screen (`org/[organization]/roles-permissions.vue`) already lists. This
402
- * narrows to exactly that. It is a narrowing of this one picker only: no role
403
- * changes, no permission changes, and the org-less `organization` roles that
404
- * were already being picked are all still offered.
405
- *
406
- * Keying off the selected service instead would empty the dropdown -- no
407
- * org-less role carries a service type -- so `props.app` is not the filter.
408
- */
409
391
  const { data: roleData, refresh: refreshRoles } = await useLazyAsyncData(
410
392
  "roles",
411
- () => getRoles({ org: org.value?._id, type: "organization", limit: 50 }),
393
+ () => getRoles({ org: org.value?._id, type: props.app, limit: 50 }),
412
394
  { watch: [() => props.app] }
413
395
  )
414
396
 
@@ -88,24 +88,13 @@
88
88
  persistent-hint
89
89
  data-test="invite-fixed-site"
90
90
  ></v-text-field>
91
- <!--
92
- Several sites (owner request 2026-10-02): one invitation per
93
- site, one e-mail. One role for all of them, so 2+ sites offer
94
- organisation roles only (see `roleSiteFor`).
95
- -->
96
91
  <v-autocomplete
97
92
  v-else
98
- v-model="invite.sites"
93
+ v-model="invite.site"
99
94
  :items="sites"
100
- multiple
101
- chips
102
- closable-chips
103
95
  class="filter-field"
104
96
  density="comfortable"
105
- :rules="[sitesRule]"
106
- :hint="invite.sites.length > 1 ? severalSitesHint : ''"
107
- :persistent-hint="invite.sites.length > 1"
108
- data-test="invite-sites"
97
+ :rules="[requiredRule]"
109
98
  @update:model-value="handleUpdateSite"
110
99
  ></v-autocomplete>
111
100
  </v-col>
@@ -174,7 +163,6 @@ import { useLocalSetup } from '../composables/useLocalSetup';
174
163
  import useRole from '../composables/useRole';
175
164
  import useSite from '../composables/useSite';
176
165
  import { inviteRoleOptions } from '../utils/invite-role-options';
177
- import { roleSiteFor } from '../utils/invite-sites';
178
166
  import useUser from '../composables/useUser';
179
167
  import useUtils from '../composables/useUtils';
180
168
 
@@ -288,8 +276,6 @@ const invite = ref<Record<string, any>>({
288
276
  role: "",
289
277
  org: "",
290
278
  site: "",
291
- // the sites picked in the Site field (several allowed); `site` follows it
292
- sites: [] as string[],
293
279
  siteName: "",
294
280
  isMemberInvite: false,
295
281
  });
@@ -515,17 +501,9 @@ watchEffect(() => {
515
501
 
516
502
  const roleOptions = computed(() => inviteRoleOptions(roles.value));
517
503
 
518
- const severalSitesHint =
519
- "Several sites: pick an organisation role - it applies at each site. For a different role per site, invite one site at a time.";
520
-
521
- const sitesRule = (v: string[]) =>
522
- (Array.isArray(v) && v.length > 0) || "Pick at least one site";
523
-
524
504
  const noRolesText = computed(() =>
525
505
  rolesError?.value
526
506
  ? "The roles could not be loaded. You may not have access to this organisation's roles; refresh, or sign in again."
527
- : !fixedSite.value && invite.value.sites.length > 1
528
- ? "No organisation roles for this service yet. Create one in Roles & Permissions (Roles for <service>, use at any site), or invite one site at a time."
529
507
  : fixedSite.value
530
508
  ? "No roles have been created for this site yet. Create one in Roles & Permissions first."
531
509
  : "No roles have been created yet. Create one in Roles & Permissions first."
@@ -547,7 +525,6 @@ const rolesPageLink = computed(() => {
547
525
  async function handleUpdateApp(value: string) {
548
526
  invite.value.role = "";
549
527
  if (!fixedSite.value) {
550
- invite.value.sites = [];
551
528
  invite.value.site = "";
552
529
  invite.value.siteName = "";
553
530
  }
@@ -567,16 +544,12 @@ function resetInvite() {
567
544
  invite.value.role = "";
568
545
  // Same site as before: a fixed site stays fixed, a picked one is cleared.
569
546
  invite.value.site = fixedSite.value;
570
- invite.value.sites = [];
571
547
  if (!invite.value.site) invite.value.siteName = "";
572
548
  message.value = "";
573
549
  }
574
550
 
575
- function handleUpdateSite(siteIds: string[]) {
576
- // The roles list follows `site`: the one chosen site, or "" for several
577
- // (organisation roles only); the role watch clears a role no longer offered.
578
- invite.value.site = roleSiteFor(siteIds);
579
- const obj = sites.value.find((x) => x?.value === invite.value.site);
551
+ function handleUpdateSite(siteId: string) {
552
+ const obj = sites.value.find((x) => x?.value === siteId);
580
553
  invite.value.siteName = obj?.title || "";
581
554
  }
582
555
 
@@ -61,37 +61,19 @@
61
61
  <template #item.createdAt="{ item }">
62
62
  {{ item.createdAt ? formatDate(item.createdAt) : "" }}
63
63
  </template>
64
- <template #item.siteLabel="{ item }">
65
- <a
66
- v-if="siteCountOf(item) > 1"
67
- href="#"
68
- class="text-primary"
69
- :title="item.sites?.map((s) => s.siteName).join(', ')"
70
- @click.prevent="detailItem = item"
71
- >
72
- {{ siteLabel(item) }}
73
- </a>
74
- <span v-else>{{ siteLabel(item) }}</span>
75
- </template>
76
64
  <template #item.action-table="{ item }">
77
65
  <v-menu
78
- :close-on-content-click="true"
66
+ :close-on-content-click="false"
79
67
  offset-y
80
- width="170"
81
- v-if="item.status === 'pending' && (props.cancelInvitation || props.inviteMember)"
68
+ width="150"
69
+ v-if="props.cancelInvitation"
82
70
  >
83
71
  <template v-slot:activator="{ props }">
84
72
  <v-icon v-bind="props">mdi-dots-vertical</v-icon>
85
73
  </template>
86
74
  <v-list>
87
- <v-list-item v-if="props.inviteMember" @click="onResend(item)">
88
- Resend
89
- </v-list-item>
90
- <v-list-item
91
- v-if="props.cancelInvitation"
92
- @click="openConfirmDialog(groupIds(item))"
93
- >
94
- {{ siteCountOf(item) > 1 ? "Cancel all" : "Cancel Invite" }}
75
+ <v-list-item @click="openConfirmDialog(item._id ?? '')" v-if="item.status === 'pending'">
76
+ Cancel Invite
95
77
  </v-list-item>
96
78
  </v-list>
97
79
  </v-menu>
@@ -103,34 +85,6 @@
103
85
  </AppCard>
104
86
  </v-col>
105
87
 
106
- <v-dialog :model-value="!!detailItem" max-width="520" @update:model-value="detailItem = null">
107
- <v-card v-if="detailItem" class="pa-4">
108
- <div class="text-h6 mb-1">{{ siteLabel(detailItem) }}</div>
109
- <div class="text-caption mb-3">
110
- {{ detailItem.email }} - one invitation e-mail; accepting it adds every site below.
111
- </div>
112
- <v-list density="compact" style="max-height: 50vh; overflow-y: auto">
113
- <v-list-item v-for="s in detailItem.sites" :key="s._id" :title="s.siteName || 'Site'">
114
- <template #append>
115
- <span class="text-caption me-3">{{ formatStatus(s.status || "") }}</span>
116
- <v-btn
117
- v-if="s.status === 'pending' && props.cancelInvitation"
118
- size="small"
119
- variant="text"
120
- color="error"
121
- @click="openConfirmDialog([s._id])"
122
- >
123
- Cancel
124
- </v-btn>
125
- </template>
126
- </v-list-item>
127
- </v-list>
128
- <div class="d-flex justify-end mt-3">
129
- <v-btn variant="text" @click="detailItem = null">Close</v-btn>
130
- </div>
131
- </v-card>
132
- </v-dialog>
133
-
134
88
  <ConfirmDialog
135
89
  v-model="confirmDialog"
136
90
  :loading="cancelLoading"
@@ -141,12 +95,8 @@
141
95
  </template>
142
96
  <template #description>
143
97
  <p class="text-subtitle-2">
144
- {{
145
- selectedInviteIds.length > 1
146
- ? `Are you sure you want to cancel all ${selectedInviteIds.length} site invitations?`
147
- : "Are you sure you want to cancel this invitation?"
148
- }}
149
- This action cannot be undone.
98
+ Are you sure you want to cancel this invitation? This action cannot be
99
+ undone.
150
100
  </p>
151
101
  </template>
152
102
  <template #footer>
@@ -190,13 +140,6 @@
190
140
  <script setup lang="ts">
191
141
  // Relative import: this layer's `utils/` is not auto-imported into consumers.
192
142
  import { invitationsEmptyText } from "../utils/invitations-empty-text";
193
- import {
194
- groupIds,
195
- resendArgs,
196
- siteCountOf,
197
- siteLabel,
198
- } from "../utils/invite-group";
199
- import useUser from "../composables/useUser";
200
143
 
201
144
  /** The same two tabs, same labels, same routes. */
202
145
  const invitationTabs = [
@@ -262,8 +205,7 @@ const headers = computed(() => {
262
205
  },
263
206
  {
264
207
  title: "Site Invited To",
265
- value: "siteLabel",
266
- sortable: false,
208
+ value: "metadata.siteName",
267
209
  },
268
210
  {
269
211
  title: "Status",
@@ -272,7 +214,7 @@ const headers = computed(() => {
272
214
 
273
215
  ];
274
216
 
275
- if (props.status === "pending" && (props.cancelInvitation || props.inviteMember)) {
217
+ if (props.status === "pending" && props.cancelInvitation) {
276
218
  baseHeaders.push({
277
219
  title: "Action",
278
220
  value: "action-table",
@@ -284,7 +226,6 @@ const headers = computed(() => {
284
226
 
285
227
  const { getVerifications: _getVerifications, cancelUserInvitation } =
286
228
  useVerification();
287
- const { inviteMemberClient } = useUser();
288
229
 
289
230
  const page = ref(1);
290
231
  const pages = ref(0);
@@ -314,31 +255,20 @@ const {
314
255
  pageRange: string;
315
256
  }>(
316
257
  `get-verifications-${props.app}-${props.org}-${props.siteId}-${props.status}`,
317
- async () => {
318
- const query = {
258
+ () =>
259
+ _getVerifications({
319
260
  page: page.value,
320
261
  status: props.status,
321
262
  search: headerSearch.value,
322
263
  // type: "user-invite,member-invite",
323
264
  type: "member-invite",
324
- // The org console (organization app, no site) lists every invitation of
325
- // the organisation, including the site invitations sent from there
326
- // (owner request 2026-10-02); the server's list scoping is unchanged.
327
- app: props.app === "organization" && !props.siteId ? "" : props.app,
265
+ app: props.app,
328
266
  orgId: props.org,
329
267
  // A site app lists only THIS site's invitations; the server checks the
330
268
  // caller reaches it (owner report 2026-09-28: LakeGarden's invitations
331
269
  // showed on every Wing Tai site). The org console passes no site.
332
270
  siteId: props.siteId,
333
- };
334
- // One row per multi-site invitation. An API without `group` refuses the
335
- // key: fall back to the plain list (one row per site, as before).
336
- try {
337
- return await _getVerifications({ ...query, group: true });
338
- } catch {
339
- return await _getVerifications(query);
340
- }
341
- },
271
+ }),
342
272
  {
343
273
  watch: [() => props.status]
344
274
  }
@@ -376,63 +306,31 @@ watch([headerSearch, () => props.status], () => {
376
306
  });
377
307
 
378
308
  const confirmDialog = ref(false);
379
- const selectedInviteIds = ref<string[]>([]);
309
+ const selectedInviteId = ref<string | null>(null);
380
310
  const cancelLoading = ref(false);
381
- const detailItem = ref<any>(null);
382
- const resending = ref(false);
383
311
 
384
- function openConfirmDialog(ids: string[]) {
385
- selectedInviteIds.value = ids;
312
+ function openConfirmDialog(id: string) {
313
+ selectedInviteId.value = id;
386
314
  confirmDialog.value = true;
387
315
  }
388
316
 
389
317
  async function onConfirmCancel() {
390
- if (!selectedInviteIds.value.length) return;
318
+ if (!selectedInviteId.value) return;
391
319
 
392
320
  cancelLoading.value = true;
393
321
  try {
394
- let res: Record<string, any> = {};
395
- for (const id of selectedInviteIds.value) {
396
- res = await cancelUserInvitation(id);
397
- }
398
-
399
- showMessage(
400
- selectedInviteIds.value.length > 1
401
- ? "All site invitations cancelled"
402
- : res.message || "Invitation cancelled successfully",
403
- "success",
404
- );
405
- detailItem.value = null;
322
+ const res = await cancelUserInvitation(selectedInviteId.value);
323
+
324
+ showMessage(res.message || "Invitation cancelled successfully", "success");
406
325
  await getVerifications();
407
326
  confirmDialog.value = false;
408
- selectedInviteIds.value = [];
327
+ selectedInviteId.value = null;
409
328
  } catch (error: any) {
410
- showMessage(error?.data?.message || error?.message || "Failed to cancel invitation", "error");
411
- await getVerifications();
329
+ showMessage(error?.message || "Failed to cancel invitation", "error");
412
330
  }
413
331
  cancelLoading.value = false;
414
332
  }
415
333
 
416
- // ONE request = ONE e-mail for the whole group (the server re-uses the
417
- // pending rows and names every site).
418
- async function onResend(item: any) {
419
- const args = resendArgs(item);
420
- if (!args) {
421
- showMessage("This invitation cannot be re-sent from here.", "error");
422
- return;
423
- }
424
- if (resending.value) return;
425
- resending.value = true;
426
- try {
427
- await inviteMemberClient(args);
428
- showMessage("Invitation re-sent (one e-mail).", "success");
429
- await getVerifications();
430
- } catch (error: any) {
431
- showMessage(error?.data?.message || error?.message || "Failed to resend invitation", "error");
432
- }
433
- resending.value = false;
434
- }
435
-
436
334
  function handleSuccess(closeForm: boolean) {
437
335
  getVerifications();
438
336
  if (closeForm) {
@@ -10,12 +10,12 @@
10
10
  <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorScanPassMessage = ''">
11
11
  <v-row no-gutters class="pt-5 ga-2">
12
12
  <v-col cols="12">
13
- <InputLabel class="text-capitalize" title="Full Name (Required)" required />
13
+ <InputLabel class="text-capitalize" title="Full Name" required />
14
14
  <v-text-field v-model.trim="memberForm.name" density="compact" hide-details :rules="[requiredRule]" />
15
15
  </v-col>
16
16
 
17
17
  <v-col cols="12">
18
- <InputLabel class="text-capitalize" title="NRIC (Required)" required />
18
+ <InputLabel class="text-capitalize" title="NRIC" required />
19
19
  <v-menu v-model="memberNRICSearchMenu" location="bottom" offset-y :close-on-content-click="false">
20
20
  <template #activator="{ props: menuProps }">
21
21
  <InputNRICNumber v-bind="menuProps" v-model.trim="memberForm.nric" density="compact" hide-details
@@ -34,7 +34,7 @@
34
34
  </v-col>
35
35
 
36
36
  <v-col cols="12">
37
- <InputLabel class="text-capitalize" title="Phone Number (Required)" required />
37
+ <InputLabel class="text-capitalize" title="Phone Number" required />
38
38
  <v-menu v-model="memberPhoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
39
39
  <template #activator="{ props: menuProps }">
40
40
  <InputPhoneNumberV2 v-bind="menuProps" v-model="memberForm.contact" density="compact"
@@ -54,7 +54,7 @@
54
54
 
55
55
  <v-col cols="12">
56
56
  <v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false" class="mt-3"
57
- :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id" label="Pass (Optional)"
57
+ :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id" label="Pass (optional)"
58
58
  variant="outlined" density="compact" :error-messages="errorScanPassMessage" persistent-hint small-chips
59
59
  :clearable="clearable" @click="fetchPasses">
60
60
  <template v-slot:no-data>
@@ -19,7 +19,7 @@
19
19
  :rules="props.passRules"
20
20
  item-title="prefixAndName"
21
21
  item-value="_id"
22
- label="Pass (Optional)"
22
+ label="Pass"
23
23
  variant="outlined"
24
24
  hide-details
25
25
  persistent-hint
@@ -48,7 +48,7 @@
48
48
  :rules="props.passRules"
49
49
  item-title="prefixAndName"
50
50
  item-value="_id"
51
- label="Keys (Optional)"
51
+ label="Keys"
52
52
  multiple
53
53
  variant="outlined"
54
54
  hide-details
@@ -199,22 +199,6 @@
199
199
  </v-expansion-panel-text>
200
200
  </v-expansion-panel>
201
201
 
202
- <v-expansion-panel v-if="canManageVehicleQr">
203
- <v-expansion-panel-title>
204
- <v-icon class="mr-2">mdi-qrcode</v-icon>
205
- Vehicle QR stickers
206
- </v-expansion-panel-title>
207
- <v-expansion-panel-text>
208
- <v-switch v-model="vehicleQrEnabled" :loading="vehicleQrSaving" :disabled="vehicleQrSaving"
209
- label="Vehicle QR stickers" color="primary" hide-details data-test="vehicle-qr-switch"
210
- @update:model-value="saveVehicleQr" />
211
- <p class="text-caption text-medium-emphasis mt-2 mb-0">
212
- When on, staff can print a QR sticker for each vehicle from Vehicle Mgmt, and guards can scan it to see
213
- which unit the vehicle belongs to.
214
- </p>
215
- </v-expansion-panel-text>
216
- </v-expansion-panel>
217
-
218
202
  <slot
219
203
  name="panels"
220
204
  :can-manage="canManageSiteSettings"
@@ -231,7 +215,6 @@ import useFile from "../composables/useFile";
231
215
  import useSiteSettings from "../composables/useSiteSettings";
232
216
  import useUtils from "../composables/useUtils";
233
217
  import useANPRSettings from "../composables/useANPRSettings";
234
- import useVehicle from "../composables/useVehicle";
235
218
 
236
219
  const props = defineProps({
237
220
  siteId: { type: String, required: true },
@@ -250,7 +233,6 @@ const { canManageSiteInformation,
250
233
  canManageCCTVCamera,
251
234
  canManageDeliveryCompanies,
252
235
  canManageResidentVisitors,
253
- canManageVehicleQr,
254
236
  } = useSettingsPermission();
255
237
 
256
238
  const { getSiteById, updateSitebyId, updateSiteInformation } =
@@ -423,36 +405,6 @@ function handleUpdateCompanies(value: string[]) {
423
405
 
424
406
  const route = useRoute();
425
407
 
426
- // Vehicle QR stickers switch (off until someone turns it on).
427
- const { getVehicleQrSettings, setVehicleQrSettings } = useVehicle();
428
- const vehicleQrEnabled = ref(false);
429
- const vehicleQrSaving = ref(false);
430
-
431
- await useLazyAsyncData(`vehicle-qr-${props.siteId}`, async () => {
432
- if (!canManageVehicleQr.value) return null;
433
- try {
434
- vehicleQrEnabled.value = !!(await getVehicleQrSettings(props.siteId))?.enabled;
435
- } catch {
436
- vehicleQrEnabled.value = false;
437
- }
438
- return null;
439
- });
440
-
441
- async function saveVehicleQr(value: boolean | null) {
442
- const enabled = !!value;
443
- vehicleQrSaving.value = true;
444
- try {
445
- const res = await setVehicleQrSettings(props.siteId, enabled);
446
- vehicleQrEnabled.value = !!res?.enabled;
447
- show(enabled ? "Vehicle QR stickers turned on" : "Vehicle QR stickers turned off", "success");
448
- } catch (e: any) {
449
- vehicleQrEnabled.value = !enabled;
450
- show(e?.data?.message || "Could not change the Vehicle QR stickers setting", "error");
451
- } finally {
452
- vehicleQrSaving.value = false;
453
- }
454
- }
455
-
456
408
  function show(message: string, color: string) {
457
409
  toast.show = true;
458
410
  toast.message = message;
@@ -59,32 +59,17 @@
59
59
  >
60
60
  {{ createLabel }}
61
61
  </v-btn>
62
- <!--
63
- The design's search field. This was a `v-text-field` with only a
64
- `max-width`, so inside the actions row it shrank to its minimum
65
- and showed "Se" and a magnifier, at a different height and
66
- border from the button beside it. Same `search` model; clearing
67
- now sets "" instead of Vuetify's null.
68
- -->
69
- <AppField
62
+ <v-text-field
70
63
  v-if="canSearch"
71
64
  v-model="searchInput"
72
- search
65
+ density="compact"
73
66
  placeholder="Search"
74
- aria-label="Search"
67
+ clearable
68
+ max-width="300"
69
+ append-inner-icon="mdi-magnify"
70
+ hide-details
75
71
  class="table-card__search"
76
- >
77
- <template v-if="searchInput" #trailing>
78
- <button
79
- type="button"
80
- class="app-field__trailing"
81
- aria-label="Clear search"
82
- @click="searchInput = ''"
83
- >
84
- <v-icon icon="mdi-close" size="16" />
85
- </button>
86
- </template>
87
- </AppField>
72
+ />
88
73
  <!-- Ghost/secondary, as the design specifies for Scan QR Code. -->
89
74
  <v-btn
90
75
  v-if="canScanVisitorQRCode"
@@ -8,9 +8,6 @@
8
8
  <v-col cols="12">
9
9
  <PageHeader title="Vehicle Mgmt">
10
10
  <template #actions>
11
- <AppButton v-if="qrStickersOn" icon="mdi-qrcode" variant="ghost" @click="openStickerDialog()">
12
- Print QR stickers
13
- </AppButton>
14
11
  <AppButton v-if="canCreateVehicle" icon="mdi-plus" @click="handleAddVehicleClick">
15
12
  Add Vehicle
16
13
  </AppButton>
@@ -85,19 +82,17 @@
85
82
  be drawn.
86
83
  -->
87
84
  <template #item.action="{ item }">
88
- <v-menu v-if="rowHasVehicleActions(item, vehiclePermissions) || qrStickersOn" location="bottom end">
85
+ <v-menu v-if="rowHasVehicleActions(item, vehiclePermissions)" location="bottom end">
89
86
  <template #activator="{ props: menuProps }">
90
87
  <AppButton v-bind="menuProps" variant="row" icon="mdi-dots-vertical" aria-label="Vehicle actions"
91
88
  @click.stop />
92
89
  </template>
93
90
  <v-list density="compact">
94
91
  <template v-for="plate in rowPlates(item)" :key="plate._id ?? plate.plateNumber">
95
- <template v-if="vehiclePlateActions(plate, vehiclePermissions).length || qrStickersOn">
92
+ <template v-if="vehiclePlateActions(plate, vehiclePermissions).length">
96
93
  <v-list-subheader v-if="rowPlates(item).length > 1">
97
94
  {{ plate.plateNumber }}
98
95
  </v-list-subheader>
99
- <v-list-item v-if="qrStickersOn" title="Re-issue sticker" prepend-icon="mdi-qrcode-edit"
100
- @click="askReissue(plate as TPlateNumber)" />
101
96
  <v-list-item v-for="action in vehiclePlateActions(plate, vehiclePermissions)" :key="action.key"
102
97
  :title="action.title" :prepend-icon="action.icon" :base-color="action.color"
103
98
  @click="runVehicleAction(action.key, plate as TPlateNumber)" />
@@ -217,13 +212,11 @@
217
212
  v-tooltip:top="'History'" @click="openHistory(plateNumberItem as TPlateNumber)" />
218
213
  </template>
219
214
  <template #item.action="{ item: plateNumberItem }">
220
- <v-menu v-if="vehiclePlateActions(plateNumberItem, vehiclePermissions).length || qrStickersOn">
215
+ <v-menu v-if="vehiclePlateActions(plateNumberItem, vehiclePermissions).length">
221
216
  <template #activator="{ props: menuProps }">
222
217
  <v-btn icon="mdi-dots-vertical" v-bind="menuProps" flat size="x-small" />
223
218
  </template>
224
219
  <v-list density="compact">
225
- <v-list-item v-if="qrStickersOn" title="Re-issue sticker" prepend-icon="mdi-qrcode-edit"
226
- @click="askReissue(plateNumberItem as TPlateNumber)" />
227
220
  <v-list-item
228
221
  v-for="action in vehiclePlateActions(plateNumberItem, vehiclePermissions)"
229
222
  :key="action.key" :title="action.title" :prepend-icon="action.icon"
@@ -348,26 +341,6 @@
348
341
  </v-card-text>
349
342
  </v-card>
350
343
  </v-dialog>
351
- <VehicleQrStickerDialog v-if="qrStickersOn" v-model="dialog.qrStickers" :site="props.site" :rows="items"
352
- :view="activeTab" :search="searchInput" :vehicle-ids="qrVehicleIds" />
353
-
354
- <v-dialog v-model="dialog.qrReissue" width="460">
355
- <v-card>
356
- <v-card-title>Re-issue sticker?</v-card-title>
357
- <v-card-text>
358
- A new QR sticker will be made for {{ qrReissuePlate?.plateNumber }}. The old sticker will stop working, so
359
- the new one must be pasted on the car.
360
- <v-alert v-if="qrReissueError" type="error" variant="tonal" density="compact" class="mt-3">{{ qrReissueError }}</v-alert>
361
- </v-card-text>
362
- <v-card-actions>
363
- <v-spacer />
364
- <v-btn variant="text" class="text-none" @click="dialog.qrReissue = false">Cancel</v-btn>
365
- <v-btn color="primary" variant="flat" class="text-none" :loading="qrReissuing" @click="confirmReissue">
366
- Re-issue
367
- </v-btn>
368
- </v-card-actions>
369
- </v-card>
370
- </v-dialog>
371
344
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
372
345
  </v-row>
373
346
  </template>
@@ -487,53 +460,7 @@ const activeTab = ref<TVehicleTab>("residents");
487
460
 
488
461
 
489
462
  const { formatCamelCaseToWords, formatDate, debounce, maskNRIC } = useUtils();
490
- const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, blockVehicle, unblockVehicle, getSpecificVehicleById, getActiveCameraCounts, getVehicleQrSettings, regenerateVehicleQr } = useVehicle();
491
-
492
- // Vehicle QR stickers: the site's switch (Settings) decides whether the print
493
- // and re-issue actions exist; only people who may update vehicles get them.
494
- const qrEnabled = ref(false);
495
- const qrStickersOn = computed(() => qrEnabled.value && props.canUpdateVehicle);
496
- const qrVehicleIds = ref<string[]>([]);
497
- const qrReissuePlate = ref<TPlateNumber | null>(null);
498
- const qrReissuing = ref(false);
499
- const qrReissueError = ref("");
500
-
501
- async function loadQrSetting() {
502
- try {
503
- qrEnabled.value = !!(await getVehicleQrSettings(props.site))?.enabled;
504
- } catch {
505
- qrEnabled.value = false;
506
- }
507
- }
508
- onMounted(loadQrSetting);
509
-
510
- function openStickerDialog(ids: string[] = []) {
511
- qrVehicleIds.value = ids;
512
- dialog.qrStickers = true;
513
- }
514
-
515
- function askReissue(plate: TPlateNumber) {
516
- qrReissuePlate.value = plate || null;
517
- qrReissueError.value = "";
518
- dialog.qrReissue = true;
519
- }
520
-
521
- async function confirmReissue() {
522
- const id = qrReissuePlate.value?._id;
523
- if (!id) return;
524
- qrReissuing.value = true;
525
- qrReissueError.value = "";
526
- try {
527
- await regenerateVehicleQr(String(id));
528
- dialog.qrReissue = false;
529
- dialog.showMoreActions = false;
530
- openStickerDialog([String(id)]);
531
- } catch (e: any) {
532
- qrReissueError.value = errorConverter(e);
533
- } finally {
534
- qrReissuing.value = false;
535
- }
536
- }
463
+ const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, blockVehicle, unblockVehicle, getSpecificVehicleById, getActiveCameraCounts } = useVehicle();
537
464
 
538
465
  /**
539
466
  * Active ANPR cameras per list at the site. A live plate on fewer cameras
@@ -616,9 +543,7 @@ const dialog = reactive({
616
543
  blockVehicle: false,
617
544
  unblockVehicle: false,
618
545
  history: false,
619
- bulkVehicle: false,
620
- qrStickers: false,
621
- qrReissue: false,
546
+ bulkVehicle: false
622
547
  });
623
548
 
624
549