@7365admin1/layer-common 3.1.4-staging.49 → 3.1.4-staging.51
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/components/EntryPassInformation.vue +4 -4
- package/components/HidAccessLogDashboard.vue +53 -8
- package/components/HidIntercomManagement.vue +621 -174
- package/components/HidQrCodeConfiguration.vue +742 -94
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +2 -0
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +0 -1
- package/composables/useHidAmico.ts +66 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useSipWebPhone.ts +236 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +2 -1
- package/plugins/secure-member.client.ts +21 -56
- package/types/site.d.ts +65 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -9,65 +9,55 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
<div class="config-body">
|
|
12
|
-
<div class="
|
|
12
|
+
<div class="online-mode-row">
|
|
13
13
|
<div>
|
|
14
|
-
<h3>
|
|
15
|
-
<p>
|
|
14
|
+
<h3>Activate Online Mode</h3>
|
|
15
|
+
<p>Enables real-time server communication, synchronizing live credential verification and access data across the network.</p>
|
|
16
16
|
</div>
|
|
17
17
|
<v-switch
|
|
18
|
-
v-model="form.
|
|
18
|
+
v-model="form.onlineMode"
|
|
19
|
+
class="hid-config-switch"
|
|
19
20
|
color="success"
|
|
20
21
|
hide-details
|
|
21
22
|
inset
|
|
22
|
-
:loading="savingSection === '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<div class="form-section">
|
|
28
|
-
<div class="section-label">HID Reader</div>
|
|
29
|
-
<v-select
|
|
30
|
-
v-model="form.readerId"
|
|
31
|
-
:items="readerOptions"
|
|
32
|
-
item-title="title"
|
|
33
|
-
item-value="value"
|
|
34
|
-
placeholder="Select HID reader"
|
|
35
|
-
variant="outlined"
|
|
36
|
-
density="compact"
|
|
37
|
-
hide-details
|
|
38
|
-
class="reader-select"
|
|
23
|
+
:loading="savingSection === 'online-mode'"
|
|
24
|
+
:disabled="Boolean(savingSection) || !form.readerId"
|
|
25
|
+
@update:model-value="saveOnlineMode"
|
|
39
26
|
/>
|
|
40
27
|
</div>
|
|
41
28
|
|
|
42
29
|
<v-divider />
|
|
43
30
|
|
|
44
|
-
<div class="form-section">
|
|
45
|
-
<div class="section-label">
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
hide-details
|
|
56
|
-
/>
|
|
57
|
-
<v-btn
|
|
58
|
-
class="text-none save-button"
|
|
59
|
-
variant="flat"
|
|
60
|
-
prepend-icon="mdi-qrcode-edit"
|
|
61
|
-
height="40"
|
|
62
|
-
:loading="savingSection === 'qr-format'"
|
|
63
|
-
:disabled="!form.readerId"
|
|
64
|
-
@click="saveSection('qr-format')"
|
|
31
|
+
<div class="form-section identification-section">
|
|
32
|
+
<div class="section-label">Enable/disable reader identification method</div>
|
|
33
|
+
<p class="section-description">
|
|
34
|
+
Toggles multi-factor access methods, allowing users to authenticate via biometric, physical card, or credential inputs.
|
|
35
|
+
</p>
|
|
36
|
+
<div class="methods-grid">
|
|
37
|
+
<div
|
|
38
|
+
v-for="method in identificationMethodOptions"
|
|
39
|
+
:key="method.key"
|
|
40
|
+
class="method-control"
|
|
41
|
+
:class="{ 'method-control--disabled': !form.onlineMode }"
|
|
65
42
|
>
|
|
66
|
-
|
|
67
|
-
|
|
43
|
+
<span>{{ method.label }}</span>
|
|
44
|
+
<v-switch
|
|
45
|
+
v-model="form.identificationMethods[method.key]"
|
|
46
|
+
class="hid-config-switch"
|
|
47
|
+
color="success"
|
|
48
|
+
hide-details
|
|
49
|
+
inset
|
|
50
|
+
density="compact"
|
|
51
|
+
:loading="savingSection === `method-${method.key}`"
|
|
52
|
+
:disabled="Boolean(savingSection) || !form.onlineMode"
|
|
53
|
+
@update:model-value="saveIdentificationMethod(method.key)"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
68
56
|
</div>
|
|
69
57
|
</div>
|
|
70
58
|
|
|
59
|
+
<v-divider />
|
|
60
|
+
|
|
71
61
|
<div class="form-section">
|
|
72
62
|
<div class="section-label">Printer Setup</div>
|
|
73
63
|
<div class="printer-grid">
|
|
@@ -99,6 +89,7 @@
|
|
|
99
89
|
variant="outlined"
|
|
100
90
|
prepend-icon="mdi-printer-check"
|
|
101
91
|
height="40"
|
|
92
|
+
:loading="testingPrinter"
|
|
102
93
|
:disabled="!hasPrinterConfig"
|
|
103
94
|
@click="testPrint"
|
|
104
95
|
>
|
|
@@ -184,6 +175,54 @@
|
|
|
184
175
|
</v-btn>
|
|
185
176
|
</div>
|
|
186
177
|
</div>
|
|
178
|
+
|
|
179
|
+
<v-divider />
|
|
180
|
+
|
|
181
|
+
<div class="permission-section">
|
|
182
|
+
<div class="permission-row">
|
|
183
|
+
<div class="permission-copy">
|
|
184
|
+
<div class="section-label">Access Permission</div>
|
|
185
|
+
<p class="section-description">
|
|
186
|
+
Select who can use the enabled HID identification methods at this site.
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="permission-actions">
|
|
190
|
+
<v-btn
|
|
191
|
+
v-for="option in accessPermissionOptions"
|
|
192
|
+
:key="option.category"
|
|
193
|
+
class="permission-button text-none"
|
|
194
|
+
variant="outlined"
|
|
195
|
+
:disabled="!form.onlineMode"
|
|
196
|
+
@click="openPermissionDialog(option.category)"
|
|
197
|
+
>
|
|
198
|
+
<span class="permission-count">{{ permissionCount(option.category) }}</span>
|
|
199
|
+
<span>{{ option.label }}</span>
|
|
200
|
+
</v-btn>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<v-divider />
|
|
205
|
+
|
|
206
|
+
<div class="permission-row permission-row--intercom">
|
|
207
|
+
<div class="permission-copy">
|
|
208
|
+
<div class="section-label">Access Intercom</div>
|
|
209
|
+
<p class="section-description">
|
|
210
|
+
Choose which authorized people and providers may use HID intercom access.
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="permission-actions permission-actions--single">
|
|
214
|
+
<v-btn
|
|
215
|
+
class="permission-button text-none"
|
|
216
|
+
variant="outlined"
|
|
217
|
+
:disabled="!form.onlineMode || permissionState.assignments.length === 0"
|
|
218
|
+
@click="openPermissionDialog('intercom')"
|
|
219
|
+
>
|
|
220
|
+
<span class="permission-count">{{ permissionState.counts.intercom }}</span>
|
|
221
|
+
<span>Intercom Permissions</span>
|
|
222
|
+
</v-btn>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
187
226
|
</div>
|
|
188
227
|
</v-card>
|
|
189
228
|
|
|
@@ -222,24 +261,109 @@
|
|
|
222
261
|
</v-card>
|
|
223
262
|
</v-dialog>
|
|
224
263
|
|
|
264
|
+
<v-dialog v-model="permissionDialog" max-width="640" persistent>
|
|
265
|
+
<v-card class="permission-dialog">
|
|
266
|
+
<v-card-title class="permission-dialog__header">
|
|
267
|
+
<div>
|
|
268
|
+
<div class="permission-dialog__title">{{ permissionDialogTitle }}</div>
|
|
269
|
+
<div class="permission-dialog__subtitle">{{ permissionDialogDescription }}</div>
|
|
270
|
+
</div>
|
|
271
|
+
<v-btn
|
|
272
|
+
icon="mdi-close"
|
|
273
|
+
variant="text"
|
|
274
|
+
density="comfortable"
|
|
275
|
+
aria-label="Close permission dialog"
|
|
276
|
+
@click="closePermissionDialog"
|
|
277
|
+
/>
|
|
278
|
+
</v-card-title>
|
|
279
|
+
|
|
280
|
+
<v-card-text class="permission-dialog__body">
|
|
281
|
+
<v-text-field
|
|
282
|
+
v-model="permissionSearch"
|
|
283
|
+
prepend-inner-icon="mdi-magnify"
|
|
284
|
+
placeholder="Search by name or details"
|
|
285
|
+
variant="outlined"
|
|
286
|
+
density="compact"
|
|
287
|
+
hide-details
|
|
288
|
+
clearable
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
<div class="permission-list" :class="{ 'permission-list--loading': loadingPermissionCandidates }">
|
|
292
|
+
<div v-if="loadingPermissionCandidates" class="permission-empty">
|
|
293
|
+
<v-progress-circular indeterminate color="primary" size="28" />
|
|
294
|
+
<span>Loading permissions...</span>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<template v-else-if="visiblePermissionCandidates.length">
|
|
298
|
+
<label
|
|
299
|
+
v-for="candidate in visiblePermissionCandidates"
|
|
300
|
+
:key="`${candidate.category}:${candidate.subjectId}`"
|
|
301
|
+
class="permission-list-item"
|
|
302
|
+
>
|
|
303
|
+
<v-checkbox-btn
|
|
304
|
+
:model-value="selectedPermissionIds.has(candidate.subjectId)"
|
|
305
|
+
color="success"
|
|
306
|
+
@update:model-value="togglePermissionCandidate(candidate.subjectId, Boolean($event))"
|
|
307
|
+
/>
|
|
308
|
+
<span class="permission-list-item__content">
|
|
309
|
+
<strong>{{ candidate.name }}</strong>
|
|
310
|
+
<small>{{ candidate.subtitle || permissionCategoryLabel(candidate.category) }}</small>
|
|
311
|
+
</span>
|
|
312
|
+
<span class="permission-list-item__group">{{ permissionCategoryLabel(candidate.category) }}</span>
|
|
313
|
+
</label>
|
|
314
|
+
</template>
|
|
315
|
+
|
|
316
|
+
<div v-else class="permission-empty">
|
|
317
|
+
<v-icon icon="mdi-account-search-outline" size="30" />
|
|
318
|
+
<span>{{ permissionEmptyMessage }}</span>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</v-card-text>
|
|
322
|
+
|
|
323
|
+
<v-card-actions class="permission-dialog__actions">
|
|
324
|
+
<v-btn class="text-none" variant="text" @click="closePermissionDialog">Cancel</v-btn>
|
|
325
|
+
<v-btn
|
|
326
|
+
class="text-none save-button"
|
|
327
|
+
variant="flat"
|
|
328
|
+
min-width="140"
|
|
329
|
+
:loading="savingPermissions"
|
|
330
|
+
:disabled="loadingPermissionCandidates"
|
|
331
|
+
@click="savePermissions"
|
|
332
|
+
>
|
|
333
|
+
Save Permissions
|
|
334
|
+
</v-btn>
|
|
335
|
+
</v-card-actions>
|
|
336
|
+
</v-card>
|
|
337
|
+
</v-dialog>
|
|
338
|
+
|
|
225
339
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
226
340
|
</section>
|
|
227
341
|
</template>
|
|
228
342
|
|
|
229
343
|
<script setup lang="ts">
|
|
230
|
-
type
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
344
|
+
type HidIdentificationMethod = keyof NonNullable<THidQrCodePassConfig["identificationMethods"]>;
|
|
345
|
+
|
|
346
|
+
type HidQrConfig = Omit<
|
|
347
|
+
THidQrCodePassConfig,
|
|
348
|
+
"identificationMethods" | "onlineMode" | "updatedAt"
|
|
349
|
+
> & {
|
|
350
|
+
onlineMode: boolean;
|
|
351
|
+
identificationMethods: Record<HidIdentificationMethod, boolean>;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
type HidReaderOption = {
|
|
355
|
+
_id: string;
|
|
356
|
+
name?: string;
|
|
357
|
+
deviceId?: string;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
type HidReaderListResponse = {
|
|
361
|
+
items?: HidReaderOption[];
|
|
362
|
+
readers?: HidReaderOption[];
|
|
363
|
+
data?: {
|
|
364
|
+
items?: HidReaderOption[];
|
|
365
|
+
readers?: HidReaderOption[];
|
|
241
366
|
};
|
|
242
|
-
validityMinutes: number | null;
|
|
243
367
|
};
|
|
244
368
|
|
|
245
369
|
const props = defineProps<{
|
|
@@ -247,12 +371,32 @@ const props = defineProps<{
|
|
|
247
371
|
}>();
|
|
248
372
|
|
|
249
373
|
const { getSiteById, updateSitebyId } = useSiteSettings();
|
|
250
|
-
const {
|
|
374
|
+
const {
|
|
375
|
+
getReaders,
|
|
376
|
+
getReaderConfiguration,
|
|
377
|
+
setReaderConfiguration,
|
|
378
|
+
getSitePermissions,
|
|
379
|
+
getPermissionCandidates,
|
|
380
|
+
updateSitePermissions,
|
|
381
|
+
} = useHidAmico();
|
|
382
|
+
const {
|
|
383
|
+
requestDevice: requestPrinterDevice,
|
|
384
|
+
testConnection: testPrinterConnection,
|
|
385
|
+
} = useWebUsb();
|
|
251
386
|
|
|
252
387
|
const defaultConfig: HidQrConfig = {
|
|
253
388
|
enabled: false,
|
|
389
|
+
onlineMode: false,
|
|
254
390
|
readerId: "",
|
|
255
391
|
qrFormat: "0",
|
|
392
|
+
identificationMethods: {
|
|
393
|
+
facial: true,
|
|
394
|
+
card: true,
|
|
395
|
+
qrCode: false,
|
|
396
|
+
idPassword: true,
|
|
397
|
+
pin: false,
|
|
398
|
+
bluetooth: false,
|
|
399
|
+
},
|
|
256
400
|
printer: {
|
|
257
401
|
vendorId: "",
|
|
258
402
|
productId: "",
|
|
@@ -264,26 +408,54 @@ const defaultConfig: HidQrConfig = {
|
|
|
264
408
|
validityMinutes: null,
|
|
265
409
|
};
|
|
266
410
|
|
|
267
|
-
const siteData = ref<
|
|
268
|
-
const readers = ref<
|
|
411
|
+
const siteData = ref<TSite | null>(null);
|
|
412
|
+
const readers = ref<HidReaderOption[]>([]);
|
|
269
413
|
const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
|
|
270
414
|
const previewDialog = ref(false);
|
|
271
415
|
const savingSection = ref("");
|
|
416
|
+
const testingPrinter = ref(false);
|
|
272
417
|
const message = ref("");
|
|
273
418
|
const messageColor = ref("success");
|
|
274
419
|
const messageSnackbar = ref(false);
|
|
420
|
+
const permissionState = reactive<THidSitePermissions>({
|
|
421
|
+
site: props.site,
|
|
422
|
+
assignments: [],
|
|
423
|
+
counts: {
|
|
424
|
+
resident: 0,
|
|
425
|
+
propertyManagement: 0,
|
|
426
|
+
serviceProvider: 0,
|
|
427
|
+
intercom: 0,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
const permissionDialog = ref(false);
|
|
431
|
+
const permissionDialogCategory = ref<THidPermissionCategory | "intercom">("resident");
|
|
432
|
+
const permissionCandidates = ref<THidPermissionCandidate[]>([]);
|
|
433
|
+
const selectedPermissionIds = ref(new Set<string>());
|
|
434
|
+
const permissionSearch = ref("");
|
|
435
|
+
const loadingPermissionCandidates = ref(false);
|
|
436
|
+
const savingPermissions = ref(false);
|
|
275
437
|
|
|
276
438
|
const hasPrinterConfig = computed(() =>
|
|
277
439
|
Boolean(form.printer.vendorId && form.printer.productId),
|
|
278
440
|
);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
{
|
|
286
|
-
{
|
|
441
|
+
const identificationMethodOptions: Array<{
|
|
442
|
+
key: HidIdentificationMethod;
|
|
443
|
+
label: string;
|
|
444
|
+
}> = [
|
|
445
|
+
{ key: "facial", label: "Facial" },
|
|
446
|
+
{ key: "card", label: "Card" },
|
|
447
|
+
{ key: "qrCode", label: "QR Code" },
|
|
448
|
+
{ key: "idPassword", label: "ID Password" },
|
|
449
|
+
{ key: "pin", label: "PIN" },
|
|
450
|
+
{ key: "bluetooth", label: "Bluetooth" },
|
|
451
|
+
];
|
|
452
|
+
const accessPermissionOptions: Array<{
|
|
453
|
+
category: THidPermissionCategory;
|
|
454
|
+
label: string;
|
|
455
|
+
}> = [
|
|
456
|
+
{ category: "resident", label: "Resident" },
|
|
457
|
+
{ category: "property_management", label: "Property Management" },
|
|
458
|
+
{ category: "service_provider", label: "Service Provider" },
|
|
287
459
|
];
|
|
288
460
|
|
|
289
461
|
const siteName = computed(() => siteData.value?.name || "Site");
|
|
@@ -291,6 +463,29 @@ const siteLabel = computed(() => siteData.value?.code || siteData.value?.block |
|
|
|
291
463
|
const validityText = computed(() =>
|
|
292
464
|
form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
|
|
293
465
|
);
|
|
466
|
+
const visiblePermissionCandidates = computed(() => {
|
|
467
|
+
const search = permissionSearch.value.trim().toLowerCase();
|
|
468
|
+
if (!search) return permissionCandidates.value;
|
|
469
|
+
return permissionCandidates.value.filter((candidate) => (
|
|
470
|
+
candidate.name.toLowerCase().includes(search)
|
|
471
|
+
|| candidate.subtitle?.toLowerCase().includes(search)
|
|
472
|
+
));
|
|
473
|
+
});
|
|
474
|
+
const permissionDialogTitle = computed(() => (
|
|
475
|
+
permissionDialogCategory.value === "intercom"
|
|
476
|
+
? "Intercom Permissions"
|
|
477
|
+
: `${permissionCategoryLabel(permissionDialogCategory.value)} Access Permission`
|
|
478
|
+
));
|
|
479
|
+
const permissionDialogDescription = computed(() => (
|
|
480
|
+
permissionDialogCategory.value === "intercom"
|
|
481
|
+
? "Select from people and providers who already have HID access permission."
|
|
482
|
+
: `Select the ${permissionCategoryLabel(permissionDialogCategory.value).toLowerCase()} entries allowed to use HID access at this site.`
|
|
483
|
+
));
|
|
484
|
+
const permissionEmptyMessage = computed(() => (
|
|
485
|
+
permissionDialogCategory.value === "intercom" && permissionState.assignments.length === 0
|
|
486
|
+
? "Add an access permission before assigning intercom access."
|
|
487
|
+
: "No matching entries found."
|
|
488
|
+
));
|
|
294
489
|
|
|
295
490
|
watch(
|
|
296
491
|
() => props.site,
|
|
@@ -305,13 +500,24 @@ async function loadConfig() {
|
|
|
305
500
|
getSiteById(props.site),
|
|
306
501
|
getReaders({ site: props.site, page: 1, limit: 100 }),
|
|
307
502
|
]);
|
|
308
|
-
siteData.value = site;
|
|
309
|
-
|
|
503
|
+
siteData.value = site as TSite;
|
|
504
|
+
const readerList = readerResponse as HidReaderListResponse;
|
|
505
|
+
readers.value = readerList.items ?? readerList.data?.items ?? readerList.data?.readers ?? [];
|
|
310
506
|
const config = siteData.value?.metadata?.hidQrCodePass || {};
|
|
507
|
+
const configuredMethods = config.identificationMethods;
|
|
311
508
|
Object.assign(form, {
|
|
312
509
|
enabled: Boolean(config.enabled),
|
|
510
|
+
onlineMode: Boolean(config.onlineMode),
|
|
313
511
|
readerId: String(config.readerId || readers.value[0]?._id || ""),
|
|
314
512
|
qrFormat: ["0", "1", "2"].includes(String(config.qrFormat)) ? String(config.qrFormat) as "0" | "1" | "2" : "0",
|
|
513
|
+
identificationMethods: {
|
|
514
|
+
facial: configuredMethods?.facial ?? true,
|
|
515
|
+
card: configuredMethods?.card ?? true,
|
|
516
|
+
qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
|
|
517
|
+
idPassword: configuredMethods?.idPassword ?? true,
|
|
518
|
+
pin: configuredMethods?.pin ?? false,
|
|
519
|
+
bluetooth: configuredMethods?.bluetooth ?? false,
|
|
520
|
+
},
|
|
315
521
|
printer: {
|
|
316
522
|
vendorId: String(config.printer?.vendorId || ""),
|
|
317
523
|
productId: String(config.printer?.productId || ""),
|
|
@@ -322,15 +528,150 @@ async function loadConfig() {
|
|
|
322
528
|
},
|
|
323
529
|
validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
|
|
324
530
|
});
|
|
531
|
+
|
|
532
|
+
await Promise.all([loadReaderConfiguration(), loadPermissions()]);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function loadPermissions() {
|
|
536
|
+
if (!props.site) return;
|
|
537
|
+
try {
|
|
538
|
+
const response = await getSitePermissions(props.site);
|
|
539
|
+
Object.assign(permissionState, response.data);
|
|
540
|
+
} catch (error: unknown) {
|
|
541
|
+
console.error("Unable to load HID permissions:", errorConverter(error));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function permissionCategoryLabel(category: THidPermissionCategory) {
|
|
546
|
+
return accessPermissionOptions.find((option) => option.category === category)?.label || "Permission";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function permissionCount(category: THidPermissionCategory) {
|
|
550
|
+
if (category === "property_management") return permissionState.counts.propertyManagement;
|
|
551
|
+
if (category === "service_provider") return permissionState.counts.serviceProvider;
|
|
552
|
+
return permissionState.counts.resident;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function openPermissionDialog(category: THidPermissionCategory | "intercom") {
|
|
556
|
+
permissionDialogCategory.value = category;
|
|
557
|
+
permissionSearch.value = "";
|
|
558
|
+
permissionCandidates.value = [];
|
|
559
|
+
selectedPermissionIds.value = new Set<string>();
|
|
560
|
+
permissionDialog.value = true;
|
|
561
|
+
loadingPermissionCandidates.value = true;
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const response = await getPermissionCandidates(props.site, {
|
|
565
|
+
category,
|
|
566
|
+
page: 1,
|
|
567
|
+
limit: 500,
|
|
568
|
+
});
|
|
569
|
+
permissionCandidates.value = response.items || [];
|
|
570
|
+
selectedPermissionIds.value = new Set(
|
|
571
|
+
permissionCandidates.value
|
|
572
|
+
.filter((candidate) => category === "intercom" ? candidate.intercom : candidate.selected)
|
|
573
|
+
.map((candidate) => candidate.subjectId),
|
|
574
|
+
);
|
|
575
|
+
} catch (error: unknown) {
|
|
576
|
+
showMessage(errorConverter(error), "error");
|
|
577
|
+
} finally {
|
|
578
|
+
loadingPermissionCandidates.value = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function closePermissionDialog() {
|
|
583
|
+
if (savingPermissions.value) return;
|
|
584
|
+
permissionDialog.value = false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function togglePermissionCandidate(subjectId: string, selected: boolean) {
|
|
588
|
+
const next = new Set(selectedPermissionIds.value);
|
|
589
|
+
if (selected) next.add(subjectId);
|
|
590
|
+
else next.delete(subjectId);
|
|
591
|
+
selectedPermissionIds.value = next;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function savePermissions() {
|
|
595
|
+
const category = permissionDialogCategory.value;
|
|
596
|
+
let assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>;
|
|
597
|
+
|
|
598
|
+
if (category === "intercom") {
|
|
599
|
+
assignments = permissionState.assignments.map((assignment) => ({
|
|
600
|
+
subjectId: assignment.subjectId,
|
|
601
|
+
category: assignment.category,
|
|
602
|
+
intercom: selectedPermissionIds.value.has(assignment.subjectId),
|
|
603
|
+
}));
|
|
604
|
+
} else {
|
|
605
|
+
const retained = permissionState.assignments
|
|
606
|
+
.filter((assignment) => assignment.category !== category)
|
|
607
|
+
.map((assignment) => ({
|
|
608
|
+
subjectId: assignment.subjectId,
|
|
609
|
+
category: assignment.category,
|
|
610
|
+
intercom: assignment.intercom,
|
|
611
|
+
}));
|
|
612
|
+
const selected = permissionCandidates.value
|
|
613
|
+
.filter((candidate) => selectedPermissionIds.value.has(candidate.subjectId))
|
|
614
|
+
.map((candidate) => ({
|
|
615
|
+
subjectId: candidate.subjectId,
|
|
616
|
+
category: candidate.category,
|
|
617
|
+
intercom: candidate.intercom,
|
|
618
|
+
}));
|
|
619
|
+
assignments = [...retained, ...selected];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
savingPermissions.value = true;
|
|
623
|
+
try {
|
|
624
|
+
const response = await updateSitePermissions(props.site, assignments);
|
|
625
|
+
Object.assign(permissionState, response.data);
|
|
626
|
+
permissionDialog.value = false;
|
|
627
|
+
showMessage("HID permissions updated.", "success");
|
|
628
|
+
} catch (error: unknown) {
|
|
629
|
+
showMessage(errorConverter(error), "error");
|
|
630
|
+
} finally {
|
|
631
|
+
savingPermissions.value = false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
636
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function getConfigurationData(response: unknown) {
|
|
640
|
+
if (!isUnknownRecord(response)) return {};
|
|
641
|
+
return isUnknownRecord(response.data) ? response.data : response;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function isEnabledConfiguration(value: unknown) {
|
|
645
|
+
return value === true || value === 1 || value === "1";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function loadReaderConfiguration() {
|
|
649
|
+
if (!form.readerId) return;
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const response = await getReaderConfiguration(form.readerId, {
|
|
653
|
+
general: ["online"],
|
|
654
|
+
});
|
|
655
|
+
const configuration = getConfigurationData(response);
|
|
656
|
+
const general = isUnknownRecord(configuration.general) ? configuration.general : {};
|
|
657
|
+
|
|
658
|
+
if (general.online !== undefined) {
|
|
659
|
+
form.onlineMode = isEnabledConfiguration(general.online);
|
|
660
|
+
}
|
|
661
|
+
} catch (error: unknown) {
|
|
662
|
+
console.warn("Unable to load HID reader configuration:", errorConverter(error));
|
|
663
|
+
}
|
|
325
664
|
}
|
|
326
665
|
|
|
327
666
|
function buildPayload() {
|
|
328
667
|
const metadata = {
|
|
329
668
|
...(siteData.value?.metadata || {}),
|
|
330
669
|
hidQrCodePass: {
|
|
331
|
-
enabled: form.
|
|
670
|
+
enabled: form.identificationMethods.qrCode,
|
|
671
|
+
onlineMode: form.onlineMode,
|
|
332
672
|
readerId: form.readerId,
|
|
333
673
|
qrFormat: form.qrFormat,
|
|
674
|
+
identificationMethods: { ...form.identificationMethods },
|
|
334
675
|
printer: {
|
|
335
676
|
vendorId: form.printer.vendorId.trim(),
|
|
336
677
|
productId: form.printer.productId.trim(),
|
|
@@ -352,17 +693,45 @@ async function saveSection(section: string) {
|
|
|
352
693
|
|
|
353
694
|
savingSection.value = section;
|
|
354
695
|
try {
|
|
355
|
-
if (section === "qr-format") {
|
|
356
|
-
await setReaderConfiguration(form.readerId, {
|
|
357
|
-
face_id: {
|
|
358
|
-
qrcode_legacy_mode_enabled: form.qrFormat,
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
696
|
await updateSitebyId(props.site, buildPayload());
|
|
363
697
|
await loadConfig();
|
|
364
|
-
showMessage("HID
|
|
365
|
-
} catch (error:
|
|
698
|
+
showMessage("HID configuration saved.", "success");
|
|
699
|
+
} catch (error: unknown) {
|
|
700
|
+
showMessage(errorConverter(error), "error");
|
|
701
|
+
} finally {
|
|
702
|
+
savingSection.value = "";
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function saveOnlineMode() {
|
|
707
|
+
if (!props.site || !form.readerId) return;
|
|
708
|
+
|
|
709
|
+
savingSection.value = "online-mode";
|
|
710
|
+
try {
|
|
711
|
+
await setReaderConfiguration(form.readerId, {
|
|
712
|
+
general: { online: form.onlineMode ? "1" : "0" },
|
|
713
|
+
});
|
|
714
|
+
await updateSitebyId(props.site, buildPayload());
|
|
715
|
+
showMessage("Online mode updated.", "success");
|
|
716
|
+
} catch (error: unknown) {
|
|
717
|
+
await loadConfig();
|
|
718
|
+
showMessage(errorConverter(error), "error");
|
|
719
|
+
} finally {
|
|
720
|
+
savingSection.value = "";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function saveIdentificationMethod(method: HidIdentificationMethod) {
|
|
725
|
+
if (!props.site) return;
|
|
726
|
+
|
|
727
|
+
form.enabled = form.identificationMethods.qrCode;
|
|
728
|
+
|
|
729
|
+
savingSection.value = `method-${method}`;
|
|
730
|
+
try {
|
|
731
|
+
await updateSitebyId(props.site, buildPayload());
|
|
732
|
+
showMessage(`${identificationMethodOptions.find((item) => item.key === method)?.label || "Identification method"} updated.`, "success");
|
|
733
|
+
} catch (error: unknown) {
|
|
734
|
+
await loadConfig();
|
|
366
735
|
showMessage(errorConverter(error), "error");
|
|
367
736
|
} finally {
|
|
368
737
|
savingSection.value = "";
|
|
@@ -376,19 +745,36 @@ async function scanPrinter() {
|
|
|
376
745
|
}
|
|
377
746
|
|
|
378
747
|
try {
|
|
379
|
-
const device = await (
|
|
748
|
+
const device = await requestPrinterDevice();
|
|
380
749
|
form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
|
|
381
750
|
form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
|
|
382
751
|
showMessage("Printer selected. Save settings to keep it.", "success");
|
|
383
|
-
} catch (error:
|
|
384
|
-
|
|
385
|
-
|
|
752
|
+
} catch (error: unknown) {
|
|
753
|
+
const message = errorConverter(error);
|
|
754
|
+
if (message !== "No device selected") {
|
|
755
|
+
showMessage(message || "Unable to scan printer.", "error");
|
|
386
756
|
}
|
|
387
757
|
}
|
|
388
758
|
}
|
|
389
759
|
|
|
390
|
-
function testPrint() {
|
|
391
|
-
|
|
760
|
+
async function testPrint() {
|
|
761
|
+
const vendorId = Number(form.printer.vendorId);
|
|
762
|
+
const productId = Number(form.printer.productId);
|
|
763
|
+
if (!vendorId || !productId) {
|
|
764
|
+
showMessage("Please scan a valid USB printer first.", "error");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
testingPrinter.value = true;
|
|
769
|
+
try {
|
|
770
|
+
const result = await testPrinterConnection(vendorId, productId);
|
|
771
|
+
showMessage(
|
|
772
|
+
result.success ? result.message : result.error || result.message,
|
|
773
|
+
result.success ? "success" : "error",
|
|
774
|
+
);
|
|
775
|
+
} finally {
|
|
776
|
+
testingPrinter.value = false;
|
|
777
|
+
}
|
|
392
778
|
}
|
|
393
779
|
|
|
394
780
|
function showMessage(text: string, color: string) {
|
|
@@ -432,15 +818,15 @@ function showMessage(text: string, color: string) {
|
|
|
432
818
|
padding: 30px 38px;
|
|
433
819
|
}
|
|
434
820
|
|
|
435
|
-
.
|
|
821
|
+
.online-mode-row {
|
|
436
822
|
display: flex;
|
|
437
|
-
align-items:
|
|
823
|
+
align-items: center;
|
|
438
824
|
justify-content: space-between;
|
|
439
825
|
gap: 24px;
|
|
440
|
-
|
|
826
|
+
padding: 0 0 20px;
|
|
441
827
|
}
|
|
442
828
|
|
|
443
|
-
.
|
|
829
|
+
.online-mode-row h3,
|
|
444
830
|
.section-label {
|
|
445
831
|
margin: 0 0 6px;
|
|
446
832
|
color: #233545;
|
|
@@ -448,16 +834,83 @@ function showMessage(text: string, color: string) {
|
|
|
448
834
|
font-weight: 700;
|
|
449
835
|
}
|
|
450
836
|
|
|
451
|
-
.
|
|
837
|
+
.online-mode-row p,
|
|
838
|
+
.section-description {
|
|
452
839
|
margin: 0;
|
|
453
840
|
color: #677280;
|
|
454
841
|
font-size: 12px;
|
|
842
|
+
line-height: 1.5;
|
|
455
843
|
}
|
|
456
844
|
|
|
457
845
|
.form-section {
|
|
458
846
|
padding: 16px 0;
|
|
459
847
|
}
|
|
460
848
|
|
|
849
|
+
.identification-section {
|
|
850
|
+
padding-bottom: 20px;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.methods-grid {
|
|
854
|
+
display: grid;
|
|
855
|
+
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
|
856
|
+
gap: 18px;
|
|
857
|
+
margin-top: 16px;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.method-control {
|
|
861
|
+
display: flex;
|
|
862
|
+
align-items: center;
|
|
863
|
+
justify-content: space-between;
|
|
864
|
+
min-width: 0;
|
|
865
|
+
min-height: 42px;
|
|
866
|
+
padding: 0 10px 0 14px;
|
|
867
|
+
border: 1px solid #dce2e7;
|
|
868
|
+
border-radius: 6px;
|
|
869
|
+
color: #334454;
|
|
870
|
+
font-size: 12px;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.method-control span {
|
|
874
|
+
overflow: hidden;
|
|
875
|
+
text-overflow: ellipsis;
|
|
876
|
+
white-space: nowrap;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.method-control--disabled {
|
|
880
|
+
background-color: #f5f6f7;
|
|
881
|
+
color: #8b9297;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.method-control :deep(.v-switch) {
|
|
885
|
+
flex: 0 0 auto;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.method-control :deep(.v-selection-control) {
|
|
889
|
+
min-height: 32px;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.hid-config-switch :deep(.v-switch__track) {
|
|
893
|
+
background-color: #8b9297;
|
|
894
|
+
opacity: 1;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.hid-config-switch :deep(.v-selection-control--dirty .v-switch__track) {
|
|
898
|
+
background-color: #2eaf55 !important;
|
|
899
|
+
color: #2eaf55 !important;
|
|
900
|
+
opacity: 1;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.hid-config-switch :deep(.v-switch__thumb) {
|
|
904
|
+
background-color: #fff !important;
|
|
905
|
+
color: #fff !important;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.hid-config-switch.v-input--disabled :deep(.v-switch__track) {
|
|
909
|
+
background-color: #aeb4b8 !important;
|
|
910
|
+
color: #aeb4b8 !important;
|
|
911
|
+
opacity: 1;
|
|
912
|
+
}
|
|
913
|
+
|
|
461
914
|
.printer-grid {
|
|
462
915
|
display: grid;
|
|
463
916
|
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
|
|
@@ -483,6 +936,177 @@ function showMessage(text: string, color: string) {
|
|
|
483
936
|
align-items: center;
|
|
484
937
|
}
|
|
485
938
|
|
|
939
|
+
.permission-section {
|
|
940
|
+
padding-top: 2px;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.permission-row {
|
|
944
|
+
display: grid;
|
|
945
|
+
grid-template-columns: minmax(260px, 1fr) minmax(520px, 1.35fr);
|
|
946
|
+
gap: 36px;
|
|
947
|
+
align-items: center;
|
|
948
|
+
padding: 18px 0;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.permission-row--intercom {
|
|
952
|
+
padding-bottom: 4px;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.permission-copy {
|
|
956
|
+
min-width: 0;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.permission-actions {
|
|
960
|
+
display: grid;
|
|
961
|
+
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
|
962
|
+
gap: 12px;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.permission-actions--single {
|
|
966
|
+
grid-template-columns: minmax(180px, 240px);
|
|
967
|
+
justify-content: end;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.permission-button {
|
|
971
|
+
justify-content: flex-start;
|
|
972
|
+
min-height: 42px;
|
|
973
|
+
padding-inline: 12px;
|
|
974
|
+
border-color: #dce2e7;
|
|
975
|
+
color: #334454;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.permission-count {
|
|
979
|
+
display: inline-grid;
|
|
980
|
+
place-items: center;
|
|
981
|
+
width: 20px;
|
|
982
|
+
height: 20px;
|
|
983
|
+
margin-right: 8px;
|
|
984
|
+
border-radius: 50%;
|
|
985
|
+
background: #dce2e7;
|
|
986
|
+
color: #526170;
|
|
987
|
+
font-size: 11px;
|
|
988
|
+
font-weight: 700;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.permission-button:not(:disabled):hover {
|
|
992
|
+
border-color: #1473c9;
|
|
993
|
+
color: #0a5da7;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.permission-dialog {
|
|
997
|
+
overflow: hidden;
|
|
998
|
+
border-radius: 6px;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.permission-dialog__header {
|
|
1002
|
+
display: flex;
|
|
1003
|
+
align-items: flex-start;
|
|
1004
|
+
justify-content: space-between;
|
|
1005
|
+
gap: 20px;
|
|
1006
|
+
padding: 20px 22px 16px;
|
|
1007
|
+
border-bottom: 1px solid #e5e9ed;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.permission-dialog__title {
|
|
1011
|
+
color: #172534;
|
|
1012
|
+
font-size: 18px;
|
|
1013
|
+
font-weight: 700;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.permission-dialog__subtitle {
|
|
1017
|
+
margin-top: 4px;
|
|
1018
|
+
color: #687583;
|
|
1019
|
+
font-size: 12px;
|
|
1020
|
+
white-space: normal;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.permission-dialog__body {
|
|
1024
|
+
display: grid;
|
|
1025
|
+
gap: 16px;
|
|
1026
|
+
padding: 18px 22px;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.permission-list {
|
|
1030
|
+
min-height: 240px;
|
|
1031
|
+
max-height: 390px;
|
|
1032
|
+
overflow-y: auto;
|
|
1033
|
+
border: 1px solid #dfe5ea;
|
|
1034
|
+
border-radius: 6px;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.permission-list--loading {
|
|
1038
|
+
display: grid;
|
|
1039
|
+
place-items: center;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.permission-list-item {
|
|
1043
|
+
display: grid;
|
|
1044
|
+
grid-template-columns: 34px minmax(0, 1fr) auto;
|
|
1045
|
+
gap: 10px;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
min-height: 62px;
|
|
1048
|
+
padding: 8px 14px 8px 8px;
|
|
1049
|
+
border-bottom: 1px solid #edf0f3;
|
|
1050
|
+
cursor: pointer;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.permission-list-item:last-child {
|
|
1054
|
+
border-bottom: 0;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.permission-list-item:hover {
|
|
1058
|
+
background: #f7fafc;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.permission-list-item__content {
|
|
1062
|
+
display: grid;
|
|
1063
|
+
min-width: 0;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.permission-list-item__content strong,
|
|
1067
|
+
.permission-list-item__content small {
|
|
1068
|
+
overflow: hidden;
|
|
1069
|
+
text-overflow: ellipsis;
|
|
1070
|
+
white-space: nowrap;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.permission-list-item__content strong {
|
|
1074
|
+
color: #243443;
|
|
1075
|
+
font-size: 13px;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.permission-list-item__content small,
|
|
1079
|
+
.permission-list-item__group {
|
|
1080
|
+
color: #778391;
|
|
1081
|
+
font-size: 11px;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.permission-list-item__group {
|
|
1085
|
+
padding: 4px 8px;
|
|
1086
|
+
border-radius: 4px;
|
|
1087
|
+
background: #eef2f5;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.permission-empty {
|
|
1091
|
+
display: flex;
|
|
1092
|
+
flex-direction: column;
|
|
1093
|
+
align-items: center;
|
|
1094
|
+
justify-content: center;
|
|
1095
|
+
gap: 10px;
|
|
1096
|
+
min-height: 240px;
|
|
1097
|
+
padding: 24px;
|
|
1098
|
+
color: #75818c;
|
|
1099
|
+
font-size: 13px;
|
|
1100
|
+
text-align: center;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.permission-dialog__actions {
|
|
1104
|
+
justify-content: flex-end;
|
|
1105
|
+
gap: 8px;
|
|
1106
|
+
padding: 14px 22px;
|
|
1107
|
+
border-top: 1px solid #e5e9ed;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
486
1110
|
.save-button {
|
|
487
1111
|
background: #082e43;
|
|
488
1112
|
color: #ffffff;
|
|
@@ -592,11 +1216,20 @@ function showMessage(text: string, color: string) {
|
|
|
592
1216
|
}
|
|
593
1217
|
|
|
594
1218
|
@media (max-width: 1100px) {
|
|
1219
|
+
.methods-grid {
|
|
1220
|
+
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
595
1223
|
.printer-grid,
|
|
596
1224
|
.template-grid,
|
|
597
|
-
.validity-grid
|
|
1225
|
+
.validity-grid,
|
|
1226
|
+
.permission-row {
|
|
598
1227
|
grid-template-columns: 1fr 1fr;
|
|
599
1228
|
}
|
|
1229
|
+
|
|
1230
|
+
.permission-actions {
|
|
1231
|
+
grid-template-columns: 1fr;
|
|
1232
|
+
}
|
|
600
1233
|
}
|
|
601
1234
|
|
|
602
1235
|
@media (max-width: 700px) {
|
|
@@ -604,15 +1237,30 @@ function showMessage(text: string, color: string) {
|
|
|
604
1237
|
padding: 22px 16px;
|
|
605
1238
|
}
|
|
606
1239
|
|
|
607
|
-
.
|
|
1240
|
+
.online-mode-row,
|
|
608
1241
|
.printer-grid,
|
|
609
1242
|
.template-grid,
|
|
610
|
-
.validity-grid
|
|
1243
|
+
.validity-grid,
|
|
1244
|
+
.methods-grid,
|
|
1245
|
+
.permission-row,
|
|
1246
|
+
.permission-actions {
|
|
611
1247
|
grid-template-columns: 1fr;
|
|
612
1248
|
}
|
|
613
1249
|
|
|
614
|
-
.
|
|
1250
|
+
.online-mode-row {
|
|
615
1251
|
display: grid;
|
|
616
1252
|
}
|
|
1253
|
+
|
|
1254
|
+
.permission-actions--single {
|
|
1255
|
+
justify-content: stretch;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
.permission-list-item {
|
|
1259
|
+
grid-template-columns: 34px minmax(0, 1fr);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.permission-list-item__group {
|
|
1263
|
+
display: none;
|
|
1264
|
+
}
|
|
617
1265
|
}
|
|
618
1266
|
</style>
|