@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.
- package/CHANGELOG.md +10 -4
- package/assets/css/primitives.css +0 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +6 -59
- package/components/AccessCardPreviewDialog.vue +53 -371
- package/components/AppSelect.vue +4 -143
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +292 -2255
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/MemberInformation.vue +4 -4
- package/components/PassInformation.vue +2 -2
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +19 -65
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAccessManagement.ts +1 -17
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useMember.ts +0 -12
- package/composables/useNFCPatrolReportFilters.ts +3 -21
- package/composables/useNFCPatrolRoute.ts +1 -8
- package/composables/usePeople.ts +0 -19
- package/composables/useServiceProvider.ts +2 -45
- package/composables/useSettingsPermission.ts +0 -14
- package/composables/useUser.ts +2 -6
- package/composables/useVehicle.ts +1 -40
- package/composables/useVerification.ts +0 -3
- package/middleware/01.auth.ts +7 -3
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/types/site.d.ts +1 -7
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/components/VehicleQrStickerDialog.vue +0 -249
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/hid-enrolment-subject.ts +0 -344
- package/utils/hid-permission-assignments.ts +0 -198
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/invite-group.ts +0 -61
- package/utils/invite-sites.ts +0 -25
- package/utils/occupancy-role.ts +0 -109
- package/utils/service-type.ts +0 -30
- 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:
|
|
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.
|
|
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="[
|
|
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(
|
|
576
|
-
|
|
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="
|
|
66
|
+
:close-on-content-click="false"
|
|
79
67
|
offset-y
|
|
80
|
-
width="
|
|
81
|
-
v-if="
|
|
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
|
|
88
|
-
|
|
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
|
-
|
|
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: "
|
|
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" &&
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
385
|
-
|
|
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 (!
|
|
318
|
+
if (!selectedInviteId.value) return;
|
|
391
319
|
|
|
392
320
|
cancelLoading.value = true;
|
|
393
321
|
try {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
327
|
+
selectedInviteId.value = null;
|
|
409
328
|
} catch (error: any) {
|
|
410
|
-
showMessage(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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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;
|
package/components/TableMain.vue
CHANGED
|
@@ -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
|
-
|
|
65
|
+
density="compact"
|
|
73
66
|
placeholder="Search"
|
|
74
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|