@7365admin1/layer-common 4.90.2-staging.491 → 4.90.3
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 +16 -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/InventoryItemDetail.vue +1 -2
- package/components/InventoryLinesDialog.vue +13 -91
- package/components/InventoryPhotoInput.vue +6 -44
- package/components/InventoryReportsTab.vue +3 -23
- package/components/InventoryRequestsTab.vue +4 -27
- package/components/InventoryStockTab.vue +0 -3
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +0 -12
- 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/useInventory.ts +1 -4
- 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/inventory.d.ts +0 -27
- 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/InventoryPoDialog.vue +0 -111
- 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/inventory-receiving.ts +0 -90
- 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
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<v-dialog v-model="open" max-width="760" scrollable>
|
|
3
|
-
<v-card class="hid-access-permissions">
|
|
4
|
-
<v-card-title class="hid-access-permissions__title">
|
|
5
|
-
<div>
|
|
6
|
-
<div class="text-h6">Access Permissions</div>
|
|
7
|
-
<div class="text-caption text-medium-emphasis">{{ readerLabel }}</div>
|
|
8
|
-
</div>
|
|
9
|
-
</v-card-title>
|
|
10
|
-
|
|
11
|
-
<v-divider />
|
|
12
|
-
|
|
13
|
-
<!-- The one precondition the server enforces before it will talk about
|
|
14
|
-
permissions at all. Saying it here beats letting the first request
|
|
15
|
-
fail with the same sentence in a snackbar. -->
|
|
16
|
-
<v-alert
|
|
17
|
-
v-if="!readerLocation"
|
|
18
|
-
type="warning"
|
|
19
|
-
variant="tonal"
|
|
20
|
-
density="compact"
|
|
21
|
-
class="ma-4"
|
|
22
|
-
>
|
|
23
|
-
Set a location on this HID Face Reader before managing permissions.
|
|
24
|
-
</v-alert>
|
|
25
|
-
|
|
26
|
-
<template v-else>
|
|
27
|
-
<div class="hid-access-permissions__intro">
|
|
28
|
-
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
29
|
-
Residents enrolled on this reader. Enrolling already grants access — switch
|
|
30
|
-
somebody off to close the door to them without removing their face, card
|
|
31
|
-
or PIN.
|
|
32
|
-
</p>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<v-card-text class="hid-access-permissions__body">
|
|
36
|
-
<TableMain
|
|
37
|
-
:headers="headers"
|
|
38
|
-
:items="rows"
|
|
39
|
-
:loading="loading"
|
|
40
|
-
:items-per-page="-1"
|
|
41
|
-
:page="page"
|
|
42
|
-
:pages="pages"
|
|
43
|
-
:page-range="pageRange"
|
|
44
|
-
item-value="rowKey"
|
|
45
|
-
@refresh="loadEnrolled"
|
|
46
|
-
@update:page="goToPage"
|
|
47
|
-
>
|
|
48
|
-
<template #extension>
|
|
49
|
-
<div class="app-filter-row">
|
|
50
|
-
<AppField
|
|
51
|
-
v-model="search"
|
|
52
|
-
search
|
|
53
|
-
placeholder="Search name, unit or registration"
|
|
54
|
-
@keyup.enter="searchFromFirstPage"
|
|
55
|
-
/>
|
|
56
|
-
</div>
|
|
57
|
-
</template>
|
|
58
|
-
|
|
59
|
-
<template #[`item.name`]="{ item }">
|
|
60
|
-
<div class="hid-access-permissions__person">
|
|
61
|
-
<div>{{ item.name || "—" }}</div>
|
|
62
|
-
<div class="text-caption text-medium-emphasis">{{ item.subtitle || "—" }}</div>
|
|
63
|
-
</div>
|
|
64
|
-
</template>
|
|
65
|
-
|
|
66
|
-
<template #[`item.selected`]="{ item }">
|
|
67
|
-
<div v-if="!item.assignable" class="hid-access-permissions__toggle">
|
|
68
|
-
<span class="text-caption text-medium-emphasis">{{ item.reason }}</span>
|
|
69
|
-
</div>
|
|
70
|
-
<div v-else class="hid-access-permissions__toggle">
|
|
71
|
-
<v-switch
|
|
72
|
-
:model-value="item.selected"
|
|
73
|
-
:disabled="saving"
|
|
74
|
-
color="primary"
|
|
75
|
-
density="compact"
|
|
76
|
-
hide-details
|
|
77
|
-
:aria-label="`Door access for ${item.name}`"
|
|
78
|
-
@update:model-value="(value) => toggle(item, value === true)"
|
|
79
|
-
/>
|
|
80
|
-
<span
|
|
81
|
-
class="text-caption"
|
|
82
|
-
:class="item.selected ? 'text-success' : 'text-medium-emphasis'"
|
|
83
|
-
>
|
|
84
|
-
{{ item.selected ? "Has access" : "No access" }}
|
|
85
|
-
</span>
|
|
86
|
-
</div>
|
|
87
|
-
</template>
|
|
88
|
-
</TableMain>
|
|
89
|
-
</v-card-text>
|
|
90
|
-
</template>
|
|
91
|
-
|
|
92
|
-
<v-divider />
|
|
93
|
-
|
|
94
|
-
<v-card-actions class="hid-access-permissions__actions">
|
|
95
|
-
<span v-if="readerLocation" class="text-caption text-medium-emphasis">
|
|
96
|
-
{{ assignedTotal }} with access on this reader
|
|
97
|
-
</span>
|
|
98
|
-
<v-spacer />
|
|
99
|
-
<AppButton variant="ghost" :disabled="saving" @click="open = false">Cancel</AppButton>
|
|
100
|
-
<AppButton
|
|
101
|
-
icon="mdi-content-save-outline"
|
|
102
|
-
:loading="saving"
|
|
103
|
-
:disabled="!readerLocation || !dirty || saving"
|
|
104
|
-
@click="save"
|
|
105
|
-
>
|
|
106
|
-
Save Permissions
|
|
107
|
-
</AppButton>
|
|
108
|
-
</v-card-actions>
|
|
109
|
-
</v-card>
|
|
110
|
-
|
|
111
|
-
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3500">
|
|
112
|
-
{{ snackbar.message }}
|
|
113
|
-
</v-snackbar>
|
|
114
|
-
</v-dialog>
|
|
115
|
-
</template>
|
|
116
|
-
|
|
117
|
-
<script setup lang="ts">
|
|
118
|
-
import {
|
|
119
|
-
applySelection,
|
|
120
|
-
hasChanges,
|
|
121
|
-
readCandidatePage,
|
|
122
|
-
seedAssignments,
|
|
123
|
-
setAssignment,
|
|
124
|
-
toAssignmentPayload,
|
|
125
|
-
type HidAssignmentState,
|
|
126
|
-
} from "../utils/hid-permission-assignments";
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* WHO MAY PASS THIS READER — a review and revoke screen, not a picker.
|
|
130
|
-
*
|
|
131
|
-
* Enrolling somebody now grants them access: `createIdentity` writes the
|
|
132
|
-
* assignment, and the reconcile puts them in the reader's group in the same
|
|
133
|
-
* pass. A property manager therefore has ONE thing to do, and this screen
|
|
134
|
-
* exists for the other direction — taking access away again.
|
|
135
|
-
*
|
|
136
|
-
* So it lists the people ENROLLED on this reader rather than everybody at the
|
|
137
|
-
* site. Switching somebody off removes their assignment, and the reconcile then
|
|
138
|
-
* deletes their `user_groups` row; their identity, face, card and PIN are left
|
|
139
|
-
* alone, so they stay recognisable at the door and are simply refused.
|
|
140
|
-
*
|
|
141
|
-
* The editing model lives in `utils/hid-permission-assignments.ts`, because the
|
|
142
|
-
* save is whole-reader while the list is one page at a time — and getting that
|
|
143
|
-
* wrong silently revokes everybody who is not on screen. See the tests there.
|
|
144
|
-
*/
|
|
145
|
-
const props = defineProps<{
|
|
146
|
-
modelValue: boolean;
|
|
147
|
-
site: string;
|
|
148
|
-
org: string;
|
|
149
|
-
reader: Record<string, unknown> | null;
|
|
150
|
-
}>();
|
|
151
|
-
|
|
152
|
-
const emit = defineEmits<{
|
|
153
|
-
(event: "update:modelValue", value: boolean): void;
|
|
154
|
-
(event: "saved"): void;
|
|
155
|
-
}>();
|
|
156
|
-
|
|
157
|
-
const { getSitePermissions, getIdentities, updateSitePermissions } = useHidAmico();
|
|
158
|
-
|
|
159
|
-
/* Name over unit in one cell rather than a "Unit" column — see `subtitleOf`. */
|
|
160
|
-
const headers = [
|
|
161
|
-
{ title: "Resident", value: "name", sortable: false },
|
|
162
|
-
{ title: "Door access", value: "selected", sortable: false, width: 190 },
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
const search = ref("");
|
|
166
|
-
const page = ref(1);
|
|
167
|
-
const pages = ref(1);
|
|
168
|
-
const pageRange = ref("");
|
|
169
|
-
const loading = ref(false);
|
|
170
|
-
const saving = ref(false);
|
|
171
|
-
const enrolled = ref<THidPermissionCandidate[]>([]);
|
|
172
|
-
const snackbar = reactive({ show: false, color: "success", message: "" });
|
|
173
|
-
|
|
174
|
-
/** What the server holds, and the working copy. The pair drives `dirty`. */
|
|
175
|
-
const savedState = ref<HidAssignmentState>(new Map());
|
|
176
|
-
const draft = ref<HidAssignmentState>(new Map());
|
|
177
|
-
|
|
178
|
-
const open = computed({
|
|
179
|
-
get: () => props.modelValue,
|
|
180
|
-
set: (value: boolean) => emit("update:modelValue", value),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const readerId = computed(() => String(props.reader?._id ?? ""));
|
|
184
|
-
const readerLocation = computed(() => String(props.reader?.location ?? "").trim());
|
|
185
|
-
const readerLabel = computed(() => {
|
|
186
|
-
const name = String(props.reader?.name ?? "").trim() || "HID Face Reader";
|
|
187
|
-
return readerLocation.value ? `${name} · ${readerLocation.value}` : name;
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const assignedTotal = computed(() => draft.value.size);
|
|
191
|
-
const dirty = computed(() => hasChanges(savedState.value, draft.value));
|
|
192
|
-
/* The draft is the truth once the dialog is open, so `selected` is recomputed
|
|
193
|
-
on every render rather than trusted from the row. */
|
|
194
|
-
const rows = computed(() => applySelection(enrolled.value, draft.value));
|
|
195
|
-
|
|
196
|
-
watch(
|
|
197
|
-
() => [props.modelValue, readerId.value] as const,
|
|
198
|
-
([isOpen]) => {
|
|
199
|
-
if (!isOpen || !readerId.value) return;
|
|
200
|
-
search.value = "";
|
|
201
|
-
page.value = 1;
|
|
202
|
-
void loadAll();
|
|
203
|
-
},
|
|
204
|
-
{ immediate: true },
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
async function loadAll() {
|
|
208
|
-
if (!readerLocation.value) return;
|
|
209
|
-
await loadAssignments();
|
|
210
|
-
await loadEnrolled();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* `getSitePermissions` returns the reader's assignments across ALL categories.
|
|
215
|
-
* That is what makes a whole-reader payload possible from a paged view, so it
|
|
216
|
-
* is loaded once per opening rather than once per page.
|
|
217
|
-
*/
|
|
218
|
-
async function loadAssignments() {
|
|
219
|
-
try {
|
|
220
|
-
const response = await getSitePermissions(props.site, props.org, readerId.value);
|
|
221
|
-
const assignments = response?.data?.assignments ?? [];
|
|
222
|
-
savedState.value = seedAssignments(assignments);
|
|
223
|
-
draft.value = seedAssignments(assignments);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
showToast(getErrorMessage(error, "Unable to load this reader's permissions."), "error");
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function toRecord(value: unknown): Record<string, unknown> {
|
|
230
|
-
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* The subject an identity names, in the order `createIdentity` validates them.
|
|
235
|
-
*
|
|
236
|
-
* A visitor returns null and is left out of the list: visitor access comes from
|
|
237
|
-
* `user_access_rules`, not from a group, so there is nothing here to switch
|
|
238
|
-
* off. An identity linked to nobody — a device user the reader holds that was
|
|
239
|
-
* never tied to a person — is dropped for the same reason.
|
|
240
|
-
*/
|
|
241
|
-
function subjectOf(identity: Record<string, unknown>) {
|
|
242
|
-
if (identity.person) {
|
|
243
|
-
return { subjectId: String(identity.person), category: "resident" as const };
|
|
244
|
-
}
|
|
245
|
-
if (identity.member) {
|
|
246
|
-
return { subjectId: String(identity.member), category: "property_management" as const };
|
|
247
|
-
}
|
|
248
|
-
if (identity.serviceProvider) {
|
|
249
|
-
return { subjectId: String(identity.serviceProvider), category: "service_provider" as const };
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Why a row cannot be switched, in words the reader of the screen can act on.
|
|
256
|
-
*
|
|
257
|
-
* The row is still DRAWN. Dropping one silently is what made the pager say
|
|
258
|
-
* "1-9 of 9" over seven rows: the count comes from the server and the rows came
|
|
259
|
-
* from a filtered list, so people vanished with nothing to say they had.
|
|
260
|
-
*
|
|
261
|
-
* What lands here is a resident identity holding only an account link - no
|
|
262
|
-
* person, member or service-provider record - which leaves no subject to
|
|
263
|
-
* assign. Visitors never reach it: they are excluded by the query above,
|
|
264
|
-
* because their access comes from `user_access_rules` rather than a group and
|
|
265
|
-
* there would be nothing here to switch.
|
|
266
|
-
*/
|
|
267
|
-
function unassignableReason() {
|
|
268
|
-
return "Not linked to a person";
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* The unit, when the enrolment captured one.
|
|
273
|
-
*
|
|
274
|
-
* `unitLabel` and NOT `block`/`level`/`unit`: those three hold ObjectIds, and
|
|
275
|
-
* the enrolment form resolves them through the occupancy tree before storing
|
|
276
|
-
* the readable label beside them. `buildUnitLabel` carries the warning this
|
|
277
|
-
* ignored once already — joining the raw values stamps
|
|
278
|
-
* "665f.../665f.../665f..." into the column.
|
|
279
|
-
*
|
|
280
|
-
* Falls back to the registration number, which is what the rows created by an
|
|
281
|
-
* assignment rather than an enrolment carry instead of a unit.
|
|
282
|
-
*/
|
|
283
|
-
function subtitleOf(identity: Record<string, unknown>) {
|
|
284
|
-
const metadata = toRecord(identity.metadata);
|
|
285
|
-
return String(metadata.unitLabel ?? "").trim()
|
|
286
|
-
|| String(identity.registration ?? "").trim();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function loadEnrolled() {
|
|
290
|
-
if (!readerId.value || !readerLocation.value) return;
|
|
291
|
-
loading.value = true;
|
|
292
|
-
try {
|
|
293
|
-
const response = await getIdentities(readerId.value, {
|
|
294
|
-
page: page.value,
|
|
295
|
-
limit: 10,
|
|
296
|
-
search: search.value.trim(),
|
|
297
|
-
// Residents only, and filtered by the SERVER rather than here. The pager's
|
|
298
|
-
// count comes back from the same query, so narrowing the list on this side
|
|
299
|
-
// would print a total that never matched the rows under it.
|
|
300
|
-
type: "resident",
|
|
301
|
-
});
|
|
302
|
-
const result = readCandidatePage(response);
|
|
303
|
-
/*
|
|
304
|
-
* EVERY row the server returned, including the ones this screen cannot act
|
|
305
|
-
* on. A visitor's access comes from `user_access_rules` rather than a group,
|
|
306
|
-
* and an identity linked to nobody has no subject to assign - but both are
|
|
307
|
-
* still enrolled on this reader, and the count in the pager counts them. They
|
|
308
|
-
* are drawn with the switch disabled and a reason, instead of disappearing.
|
|
309
|
-
*/
|
|
310
|
-
enrolled.value = result.items.map((row) => {
|
|
311
|
-
const identity = toRecord(row);
|
|
312
|
-
const subject = subjectOf(identity);
|
|
313
|
-
const metadata = toRecord(identity.metadata);
|
|
314
|
-
return {
|
|
315
|
-
subjectId: subject?.subjectId ?? "",
|
|
316
|
-
category: subject?.category ?? "resident",
|
|
317
|
-
// The row's own key. `subjectId` is empty for the rows below, and two
|
|
318
|
-
// empty keys would collapse into one row in the table.
|
|
319
|
-
rowKey: String(identity._id ?? identity.hidUserId ?? Math.random()),
|
|
320
|
-
assignable: Boolean(subject),
|
|
321
|
-
reason: subject ? "" : unassignableReason(),
|
|
322
|
-
name: String(metadata.name ?? "").trim()
|
|
323
|
-
|| String(identity.registration ?? "").trim()
|
|
324
|
-
|| "—",
|
|
325
|
-
subtitle: subtitleOf(identity),
|
|
326
|
-
intercom: false,
|
|
327
|
-
selected: false,
|
|
328
|
-
} as THidPermissionCandidate;
|
|
329
|
-
});
|
|
330
|
-
// `pages` only. The page number stays whatever the pager set — this
|
|
331
|
-
// endpoint's `paginate` never echoes it back, so reading one here pinned
|
|
332
|
-
// the screen to page one on every fetch.
|
|
333
|
-
pages.value = result.pages;
|
|
334
|
-
pageRange.value = result.pageRange;
|
|
335
|
-
} catch (error) {
|
|
336
|
-
enrolled.value = [];
|
|
337
|
-
pages.value = 1;
|
|
338
|
-
pageRange.value = "";
|
|
339
|
-
showToast(getErrorMessage(error, "Unable to load the people enrolled on this reader."), "error");
|
|
340
|
-
} finally {
|
|
341
|
-
loading.value = false;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function goToPage(value: number) {
|
|
346
|
-
page.value = Number(value) || 1;
|
|
347
|
-
void loadEnrolled();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function searchFromFirstPage() {
|
|
351
|
-
page.value = 1;
|
|
352
|
-
void loadEnrolled();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function toggle(item: THidPermissionCandidate, selected: boolean) {
|
|
356
|
-
if (!String(item.subjectId ?? "")) return;
|
|
357
|
-
draft.value = setAssignment(
|
|
358
|
-
draft.value,
|
|
359
|
-
savedState.value,
|
|
360
|
-
String(item.subjectId ?? ""),
|
|
361
|
-
item.category,
|
|
362
|
-
selected,
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async function save() {
|
|
367
|
-
if (!dirty.value || saving.value) return;
|
|
368
|
-
saving.value = true;
|
|
369
|
-
try {
|
|
370
|
-
await updateSitePermissions(
|
|
371
|
-
props.site,
|
|
372
|
-
props.org,
|
|
373
|
-
readerId.value,
|
|
374
|
-
toAssignmentPayload(draft.value),
|
|
375
|
-
);
|
|
376
|
-
// The server reconciles the device inside this call: anybody switched off
|
|
377
|
-
// loses their `user_groups` row before it returns, so a success here means
|
|
378
|
-
// the door has already been updated.
|
|
379
|
-
savedState.value = new Map(draft.value);
|
|
380
|
-
showToast("HID permissions updated.");
|
|
381
|
-
emit("saved");
|
|
382
|
-
open.value = false;
|
|
383
|
-
} catch (error) {
|
|
384
|
-
showToast(getErrorMessage(error, "Unable to update HID permissions."), "error");
|
|
385
|
-
} finally {
|
|
386
|
-
saving.value = false;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function showToast(message: string, color = "success") {
|
|
391
|
-
snackbar.message = message;
|
|
392
|
-
snackbar.color = color;
|
|
393
|
-
snackbar.show = true;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function getErrorMessage(error: unknown, fallback: string) {
|
|
397
|
-
const record = toRecord(error);
|
|
398
|
-
const data = toRecord(record.data);
|
|
399
|
-
return String(data.message ?? "") || String(record.message ?? "") || fallback;
|
|
400
|
-
}
|
|
401
|
-
</script>
|
|
402
|
-
|
|
403
|
-
<style scoped lang="scss">
|
|
404
|
-
.hid-access-permissions {
|
|
405
|
-
&__title { padding-block: 16px; }
|
|
406
|
-
|
|
407
|
-
&__intro { padding: 12px 16px 0; }
|
|
408
|
-
|
|
409
|
-
&__body {
|
|
410
|
-
padding-top: 8px;
|
|
411
|
-
min-height: 320px;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
&__actions { padding: 12px 16px; }
|
|
415
|
-
|
|
416
|
-
&__person > div:first-child { font-weight: 500; }
|
|
417
|
-
|
|
418
|
-
&__toggle {
|
|
419
|
-
display: flex;
|
|
420
|
-
align-items: center;
|
|
421
|
-
gap: 8px;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
</style>
|