@7365admin1/layer-common 3.2.1 → 3.2.2-staging.76
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/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/DashboardMain.vue +84 -88
- package/components/DashboardPanel.vue +1 -1
- package/components/EntryPassInformation.vue +4 -4
- package/components/FeedbackMain.vue +7 -7
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +808 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +392 -81
- package/components/Layout/NavigationDrawer.vue +43 -2
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +6 -2
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +258 -14
- package/components/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +2 -3
- package/components/VisitorSocketPopUp.vue +56 -2
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +311 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +3 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +37 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/theme.test.ts +216 -0
- package/utils/theme.ts +163 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- 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,88 @@
|
|
|
184
175
|
</v-btn>
|
|
185
176
|
</div>
|
|
186
177
|
</div>
|
|
178
|
+
|
|
179
|
+
<v-divider />
|
|
180
|
+
|
|
181
|
+
<div v-if="isPermissionManager" class="permission-section">
|
|
182
|
+
<div class="permission-location">
|
|
183
|
+
<div class="permission-copy">
|
|
184
|
+
<div class="section-label">Permission Location</div>
|
|
185
|
+
<p class="section-description">
|
|
186
|
+
Permissions are scoped to the location configured on the HID Face Reader.
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<v-select
|
|
190
|
+
v-model="permissionReaderId"
|
|
191
|
+
class="permission-location__select"
|
|
192
|
+
:items="permissionLocations"
|
|
193
|
+
item-title="label"
|
|
194
|
+
item-value="readerId"
|
|
195
|
+
placeholder="Select a Face Reader location"
|
|
196
|
+
variant="outlined"
|
|
197
|
+
density="compact"
|
|
198
|
+
hide-details
|
|
199
|
+
:disabled="permissionLocations.length === 0"
|
|
200
|
+
@update:model-value="loadPermissions"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<v-alert
|
|
205
|
+
v-if="permissionLocations.length === 0"
|
|
206
|
+
class="mb-4"
|
|
207
|
+
type="warning"
|
|
208
|
+
variant="tonal"
|
|
209
|
+
density="compact"
|
|
210
|
+
>
|
|
211
|
+
Add a Location to a Face Reader before assigning HID permissions.
|
|
212
|
+
</v-alert>
|
|
213
|
+
|
|
214
|
+
<v-divider />
|
|
215
|
+
|
|
216
|
+
<div class="permission-row">
|
|
217
|
+
<div class="permission-copy">
|
|
218
|
+
<div class="section-label">Access Permission</div>
|
|
219
|
+
<p class="section-description">
|
|
220
|
+
Select who can use the enabled HID identification methods at this site.
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="permission-actions">
|
|
224
|
+
<v-btn
|
|
225
|
+
v-for="option in accessPermissionOptions"
|
|
226
|
+
:key="option.category"
|
|
227
|
+
class="permission-button text-none"
|
|
228
|
+
variant="outlined"
|
|
229
|
+
:disabled="!form.onlineMode || !permissionReaderId"
|
|
230
|
+
@click="openPermissionDialog(option.category)"
|
|
231
|
+
>
|
|
232
|
+
<span class="permission-count">{{ permissionCount(option.category) }}</span>
|
|
233
|
+
<span>{{ option.label }}</span>
|
|
234
|
+
</v-btn>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<v-divider />
|
|
239
|
+
|
|
240
|
+
<div class="permission-row permission-row--intercom">
|
|
241
|
+
<div class="permission-copy">
|
|
242
|
+
<div class="section-label">Access Intercom</div>
|
|
243
|
+
<p class="section-description">
|
|
244
|
+
Choose which authorized people and providers may use HID intercom access.
|
|
245
|
+
</p>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="permission-actions permission-actions--single">
|
|
248
|
+
<v-btn
|
|
249
|
+
class="permission-button text-none"
|
|
250
|
+
variant="outlined"
|
|
251
|
+
:disabled="!form.onlineMode || !permissionReaderId || permissionState.assignments.length === 0"
|
|
252
|
+
@click="openPermissionDialog('intercom')"
|
|
253
|
+
>
|
|
254
|
+
<span class="permission-count">{{ permissionState.counts.intercom }}</span>
|
|
255
|
+
<span>Intercom Permissions</span>
|
|
256
|
+
</v-btn>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
187
260
|
</div>
|
|
188
261
|
</v-card>
|
|
189
262
|
|
|
@@ -222,37 +295,158 @@
|
|
|
222
295
|
</v-card>
|
|
223
296
|
</v-dialog>
|
|
224
297
|
|
|
298
|
+
<v-dialog v-model="permissionDialog" max-width="680" persistent>
|
|
299
|
+
<v-card class="permission-dialog">
|
|
300
|
+
<v-card-title class="permission-dialog__header">
|
|
301
|
+
<div>
|
|
302
|
+
<div class="permission-dialog__title">{{ permissionDialogTitle }}</div>
|
|
303
|
+
<div class="permission-dialog__subtitle">{{ permissionDialogDescription }}</div>
|
|
304
|
+
</div>
|
|
305
|
+
<v-btn
|
|
306
|
+
icon="mdi-close"
|
|
307
|
+
variant="text"
|
|
308
|
+
density="comfortable"
|
|
309
|
+
aria-label="Close permission dialog"
|
|
310
|
+
@click="closePermissionDialog"
|
|
311
|
+
/>
|
|
312
|
+
</v-card-title>
|
|
313
|
+
|
|
314
|
+
<v-card-text class="permission-dialog__body">
|
|
315
|
+
<v-text-field
|
|
316
|
+
v-model="permissionSearch"
|
|
317
|
+
class="permission-search"
|
|
318
|
+
prepend-inner-icon="mdi-magnify"
|
|
319
|
+
placeholder="Search by name or details"
|
|
320
|
+
variant="outlined"
|
|
321
|
+
density="compact"
|
|
322
|
+
hide-details
|
|
323
|
+
clearable
|
|
324
|
+
/>
|
|
325
|
+
|
|
326
|
+
<div class="permission-list-summary" aria-live="polite">
|
|
327
|
+
<span>{{ visiblePermissionCandidates.length }} entries</span>
|
|
328
|
+
<span class="permission-list-summary__selected">
|
|
329
|
+
{{ selectedPermissionIds.size }} selected
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div class="permission-list" :class="{ 'permission-list--loading': loadingPermissionCandidates }">
|
|
334
|
+
<div v-if="loadingPermissionCandidates" class="permission-empty">
|
|
335
|
+
<v-progress-circular indeterminate color="primary" size="28" />
|
|
336
|
+
<span>Loading permissions...</span>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<template v-else-if="visiblePermissionCandidates.length">
|
|
340
|
+
<label
|
|
341
|
+
v-for="candidate in visiblePermissionCandidates"
|
|
342
|
+
:key="`${candidate.category}:${candidate.subjectId}`"
|
|
343
|
+
class="permission-list-item"
|
|
344
|
+
:class="{ 'permission-list-item--selected': selectedPermissionIds.has(candidate.subjectId) }"
|
|
345
|
+
>
|
|
346
|
+
<span class="permission-list-item__content">
|
|
347
|
+
<strong>{{ candidate.name }}</strong>
|
|
348
|
+
<small>{{ candidate.subtitle || permissionCategoryLabel(candidate.category) }}</small>
|
|
349
|
+
</span>
|
|
350
|
+
<span class="permission-list-item__group">{{ permissionCategoryLabel(candidate.category) }}</span>
|
|
351
|
+
<v-checkbox-btn
|
|
352
|
+
class="permission-list-item__checkbox"
|
|
353
|
+
:model-value="selectedPermissionIds.has(candidate.subjectId)"
|
|
354
|
+
:aria-label="`Select ${candidate.name}`"
|
|
355
|
+
color="success"
|
|
356
|
+
@update:model-value="togglePermissionCandidate(candidate.subjectId, Boolean($event))"
|
|
357
|
+
/>
|
|
358
|
+
</label>
|
|
359
|
+
</template>
|
|
360
|
+
|
|
361
|
+
<div v-else class="permission-empty">
|
|
362
|
+
<v-icon icon="mdi-account-search-outline" size="30" />
|
|
363
|
+
<span>{{ permissionEmptyMessage }}</span>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</v-card-text>
|
|
367
|
+
|
|
368
|
+
<v-card-actions class="permission-dialog__actions">
|
|
369
|
+
<v-btn class="text-none" variant="text" @click="closePermissionDialog">Cancel</v-btn>
|
|
370
|
+
<v-btn
|
|
371
|
+
class="text-none save-button"
|
|
372
|
+
variant="flat"
|
|
373
|
+
min-width="140"
|
|
374
|
+
:loading="savingPermissions"
|
|
375
|
+
:disabled="loadingPermissionCandidates"
|
|
376
|
+
@click="savePermissions"
|
|
377
|
+
>
|
|
378
|
+
Save Permissions
|
|
379
|
+
</v-btn>
|
|
380
|
+
</v-card-actions>
|
|
381
|
+
</v-card>
|
|
382
|
+
</v-dialog>
|
|
383
|
+
|
|
225
384
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
226
385
|
</section>
|
|
227
386
|
</template>
|
|
228
387
|
|
|
229
388
|
<script setup lang="ts">
|
|
230
|
-
type
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
389
|
+
type HidIdentificationMethod = keyof NonNullable<THidQrCodePassConfig["identificationMethods"]>;
|
|
390
|
+
|
|
391
|
+
type HidQrConfig = Omit<
|
|
392
|
+
THidQrCodePassConfig,
|
|
393
|
+
"identificationMethods" | "onlineMode" | "updatedAt"
|
|
394
|
+
> & {
|
|
395
|
+
onlineMode: boolean;
|
|
396
|
+
identificationMethods: Record<HidIdentificationMethod, boolean>;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
type HidReaderOption = {
|
|
400
|
+
_id: string;
|
|
401
|
+
name?: string;
|
|
402
|
+
deviceId?: string;
|
|
403
|
+
location?: string;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
type HidReaderListResponse = {
|
|
407
|
+
items?: HidReaderOption[];
|
|
408
|
+
readers?: HidReaderOption[];
|
|
409
|
+
data?: {
|
|
410
|
+
items?: HidReaderOption[];
|
|
411
|
+
readers?: HidReaderOption[];
|
|
241
412
|
};
|
|
242
|
-
validityMinutes: number | null;
|
|
243
413
|
};
|
|
244
414
|
|
|
245
415
|
const props = defineProps<{
|
|
246
416
|
site: string;
|
|
417
|
+
org: string;
|
|
247
418
|
}>();
|
|
248
419
|
|
|
420
|
+
const runtimeConfig = useRuntimeConfig();
|
|
421
|
+
const isPermissionManager = computed(() => runtimeConfig.public.APP === "security_agency");
|
|
422
|
+
|
|
249
423
|
const { getSiteById, updateSitebyId } = useSiteSettings();
|
|
250
|
-
const {
|
|
424
|
+
const {
|
|
425
|
+
getReaders,
|
|
426
|
+
getReaderConfiguration,
|
|
427
|
+
setReaderConfiguration,
|
|
428
|
+
getSitePermissions,
|
|
429
|
+
getPermissionCandidates,
|
|
430
|
+
updateSitePermissions,
|
|
431
|
+
} = useHidAmico();
|
|
432
|
+
const {
|
|
433
|
+
requestDevice: requestPrinterDevice,
|
|
434
|
+
testConnection: testPrinterConnection,
|
|
435
|
+
} = useWebUsb();
|
|
251
436
|
|
|
252
437
|
const defaultConfig: HidQrConfig = {
|
|
253
438
|
enabled: false,
|
|
439
|
+
onlineMode: false,
|
|
254
440
|
readerId: "",
|
|
255
441
|
qrFormat: "0",
|
|
442
|
+
identificationMethods: {
|
|
443
|
+
facial: true,
|
|
444
|
+
card: true,
|
|
445
|
+
qrCode: false,
|
|
446
|
+
idPassword: true,
|
|
447
|
+
pin: false,
|
|
448
|
+
bluetooth: false,
|
|
449
|
+
},
|
|
256
450
|
printer: {
|
|
257
451
|
vendorId: "",
|
|
258
452
|
productId: "",
|
|
@@ -264,36 +458,111 @@ const defaultConfig: HidQrConfig = {
|
|
|
264
458
|
validityMinutes: null,
|
|
265
459
|
};
|
|
266
460
|
|
|
267
|
-
const siteData = ref<
|
|
268
|
-
const readers = ref<
|
|
461
|
+
const siteData = ref<TSite | null>(null);
|
|
462
|
+
const readers = ref<HidReaderOption[]>([]);
|
|
269
463
|
const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
|
|
270
464
|
const previewDialog = ref(false);
|
|
271
465
|
const savingSection = ref("");
|
|
466
|
+
const testingPrinter = ref(false);
|
|
272
467
|
const message = ref("");
|
|
273
468
|
const messageColor = ref("success");
|
|
274
469
|
const messageSnackbar = ref(false);
|
|
470
|
+
const permissionState = reactive<THidSitePermissions>({
|
|
471
|
+
orgId: props.org,
|
|
472
|
+
site: props.site,
|
|
473
|
+
readerId: "",
|
|
474
|
+
location: "",
|
|
475
|
+
assignments: [],
|
|
476
|
+
counts: {
|
|
477
|
+
resident: 0,
|
|
478
|
+
propertyManagement: 0,
|
|
479
|
+
serviceProvider: 0,
|
|
480
|
+
intercom: 0,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
const permissionDialog = ref(false);
|
|
484
|
+
const permissionReaderId = ref("");
|
|
485
|
+
const permissionDialogCategory = ref<THidPermissionCategory | "intercom">("resident");
|
|
486
|
+
const permissionCandidates = ref<THidPermissionCandidate[]>([]);
|
|
487
|
+
const selectedPermissionIds = ref(new Set<string>());
|
|
488
|
+
const permissionSearch = ref("");
|
|
489
|
+
const loadingPermissionCandidates = ref(false);
|
|
490
|
+
const savingPermissions = ref(false);
|
|
275
491
|
|
|
276
492
|
const hasPrinterConfig = computed(() =>
|
|
277
493
|
Boolean(form.printer.vendorId && form.printer.productId),
|
|
278
494
|
);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
{
|
|
286
|
-
{
|
|
495
|
+
const identificationMethodOptions: Array<{
|
|
496
|
+
key: HidIdentificationMethod;
|
|
497
|
+
label: string;
|
|
498
|
+
}> = [
|
|
499
|
+
{ key: "facial", label: "Facial" },
|
|
500
|
+
{ key: "card", label: "Card" },
|
|
501
|
+
{ key: "qrCode", label: "QR Code" },
|
|
502
|
+
{ key: "idPassword", label: "ID Password" },
|
|
503
|
+
{ key: "pin", label: "PIN" },
|
|
504
|
+
{ key: "bluetooth", label: "Bluetooth" },
|
|
505
|
+
];
|
|
506
|
+
const accessPermissionOptions: Array<{
|
|
507
|
+
category: THidPermissionCategory;
|
|
508
|
+
label: string;
|
|
509
|
+
}> = [
|
|
510
|
+
{ category: "resident", label: "Resident" },
|
|
511
|
+
{ category: "property_management", label: "Property Management" },
|
|
512
|
+
{ category: "service_provider", label: "Service Provider" },
|
|
287
513
|
];
|
|
288
514
|
|
|
515
|
+
const permissionLocations = computed(() => {
|
|
516
|
+
const seen = new Set<string>();
|
|
517
|
+
return readers.value.reduce<Array<{ readerId: string; location: string; label: string }>>((items, reader) => {
|
|
518
|
+
const location = reader.location?.trim();
|
|
519
|
+
const key = location?.toLocaleLowerCase();
|
|
520
|
+
if (!location || !key || seen.has(key)) return items;
|
|
521
|
+
seen.add(key);
|
|
522
|
+
items.push({
|
|
523
|
+
readerId: reader._id,
|
|
524
|
+
location,
|
|
525
|
+
label: location,
|
|
526
|
+
});
|
|
527
|
+
return items;
|
|
528
|
+
}, []);
|
|
529
|
+
});
|
|
530
|
+
const selectedPermissionLocation = computed(() => (
|
|
531
|
+
permissionLocations.value.find((item) => item.readerId === permissionReaderId.value)?.location
|
|
532
|
+
|| "the selected location"
|
|
533
|
+
));
|
|
534
|
+
|
|
289
535
|
const siteName = computed(() => siteData.value?.name || "Site");
|
|
290
536
|
const siteLabel = computed(() => siteData.value?.code || siteData.value?.block || props.site);
|
|
291
537
|
const validityText = computed(() =>
|
|
292
538
|
form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
|
|
293
539
|
);
|
|
540
|
+
const visiblePermissionCandidates = computed(() => {
|
|
541
|
+
const search = permissionSearch.value.trim().toLowerCase();
|
|
542
|
+
if (!search) return permissionCandidates.value;
|
|
543
|
+
return permissionCandidates.value.filter((candidate) => (
|
|
544
|
+
candidate.name.toLowerCase().includes(search)
|
|
545
|
+
|| candidate.subtitle?.toLowerCase().includes(search)
|
|
546
|
+
));
|
|
547
|
+
});
|
|
548
|
+
const permissionDialogTitle = computed(() => (
|
|
549
|
+
permissionDialogCategory.value === "intercom"
|
|
550
|
+
? "Intercom Permissions"
|
|
551
|
+
: `${permissionCategoryLabel(permissionDialogCategory.value)} Access Permission`
|
|
552
|
+
));
|
|
553
|
+
const permissionDialogDescription = computed(() => (
|
|
554
|
+
permissionDialogCategory.value === "intercom"
|
|
555
|
+
? `Select from people and providers who already have HID access permission at ${selectedPermissionLocation.value}.`
|
|
556
|
+
: `Select the ${permissionCategoryLabel(permissionDialogCategory.value).toLowerCase()} entries allowed to use HID access at ${selectedPermissionLocation.value}.`
|
|
557
|
+
));
|
|
558
|
+
const permissionEmptyMessage = computed(() => (
|
|
559
|
+
permissionDialogCategory.value === "intercom" && permissionState.assignments.length === 0
|
|
560
|
+
? "Add an access permission before assigning intercom access."
|
|
561
|
+
: "No matching entries found."
|
|
562
|
+
));
|
|
294
563
|
|
|
295
564
|
watch(
|
|
296
|
-
() => props.site,
|
|
565
|
+
() => [props.org, props.site],
|
|
297
566
|
() => loadConfig(),
|
|
298
567
|
{ immediate: true },
|
|
299
568
|
);
|
|
@@ -305,13 +574,24 @@ async function loadConfig() {
|
|
|
305
574
|
getSiteById(props.site),
|
|
306
575
|
getReaders({ site: props.site, page: 1, limit: 100 }),
|
|
307
576
|
]);
|
|
308
|
-
siteData.value = site;
|
|
309
|
-
|
|
577
|
+
siteData.value = site as TSite;
|
|
578
|
+
const readerList = readerResponse as HidReaderListResponse;
|
|
579
|
+
readers.value = readerList.items ?? readerList.data?.items ?? readerList.data?.readers ?? [];
|
|
310
580
|
const config = siteData.value?.metadata?.hidQrCodePass || {};
|
|
581
|
+
const configuredMethods = config.identificationMethods;
|
|
311
582
|
Object.assign(form, {
|
|
312
583
|
enabled: Boolean(config.enabled),
|
|
584
|
+
onlineMode: Boolean(config.onlineMode),
|
|
313
585
|
readerId: String(config.readerId || readers.value[0]?._id || ""),
|
|
314
586
|
qrFormat: ["0", "1", "2"].includes(String(config.qrFormat)) ? String(config.qrFormat) as "0" | "1" | "2" : "0",
|
|
587
|
+
identificationMethods: {
|
|
588
|
+
facial: configuredMethods?.facial ?? true,
|
|
589
|
+
card: configuredMethods?.card ?? true,
|
|
590
|
+
qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
|
|
591
|
+
idPassword: configuredMethods?.idPassword ?? true,
|
|
592
|
+
pin: configuredMethods?.pin ?? false,
|
|
593
|
+
bluetooth: configuredMethods?.bluetooth ?? false,
|
|
594
|
+
},
|
|
315
595
|
printer: {
|
|
316
596
|
vendorId: String(config.printer?.vendorId || ""),
|
|
317
597
|
productId: String(config.printer?.productId || ""),
|
|
@@ -322,15 +602,175 @@ async function loadConfig() {
|
|
|
322
602
|
},
|
|
323
603
|
validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
|
|
324
604
|
});
|
|
605
|
+
|
|
606
|
+
permissionReaderId.value = permissionLocations.value.find((item) => item.readerId === form.readerId)?.readerId
|
|
607
|
+
|| permissionLocations.value[0]?.readerId
|
|
608
|
+
|| "";
|
|
609
|
+
|
|
610
|
+
await Promise.all([
|
|
611
|
+
loadReaderConfiguration(),
|
|
612
|
+
isPermissionManager.value ? loadPermissions() : Promise.resolve(),
|
|
613
|
+
]);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function loadPermissions() {
|
|
617
|
+
if (!props.org || !props.site || !permissionReaderId.value) {
|
|
618
|
+
Object.assign(permissionState, {
|
|
619
|
+
orgId: props.org,
|
|
620
|
+
site: props.site,
|
|
621
|
+
readerId: "",
|
|
622
|
+
location: "",
|
|
623
|
+
assignments: [],
|
|
624
|
+
counts: { resident: 0, propertyManagement: 0, serviceProvider: 0, intercom: 0 },
|
|
625
|
+
});
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const response = await getSitePermissions(props.site, props.org, permissionReaderId.value);
|
|
630
|
+
Object.assign(permissionState, response.data);
|
|
631
|
+
} catch (error: unknown) {
|
|
632
|
+
console.error("Unable to load HID permissions:", errorConverter(error));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function permissionCategoryLabel(category: THidPermissionCategory) {
|
|
637
|
+
return accessPermissionOptions.find((option) => option.category === category)?.label || "Permission";
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function permissionCount(category: THidPermissionCategory) {
|
|
641
|
+
if (category === "property_management") return permissionState.counts.propertyManagement;
|
|
642
|
+
if (category === "service_provider") return permissionState.counts.serviceProvider;
|
|
643
|
+
return permissionState.counts.resident;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function openPermissionDialog(category: THidPermissionCategory | "intercom") {
|
|
647
|
+
if (!props.org || !permissionReaderId.value) return;
|
|
648
|
+
permissionDialogCategory.value = category;
|
|
649
|
+
permissionSearch.value = "";
|
|
650
|
+
permissionCandidates.value = [];
|
|
651
|
+
selectedPermissionIds.value = new Set<string>();
|
|
652
|
+
permissionDialog.value = true;
|
|
653
|
+
loadingPermissionCandidates.value = true;
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const response = await getPermissionCandidates(props.site, {
|
|
657
|
+
orgId: props.org,
|
|
658
|
+
readerId: permissionReaderId.value,
|
|
659
|
+
category,
|
|
660
|
+
page: 1,
|
|
661
|
+
limit: 500,
|
|
662
|
+
});
|
|
663
|
+
permissionCandidates.value = response.items || [];
|
|
664
|
+
selectedPermissionIds.value = new Set(
|
|
665
|
+
permissionCandidates.value
|
|
666
|
+
.filter((candidate) => category === "intercom" ? candidate.intercom : candidate.selected)
|
|
667
|
+
.map((candidate) => candidate.subjectId),
|
|
668
|
+
);
|
|
669
|
+
} catch (error: unknown) {
|
|
670
|
+
showMessage(errorConverter(error), "error");
|
|
671
|
+
} finally {
|
|
672
|
+
loadingPermissionCandidates.value = false;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function closePermissionDialog() {
|
|
677
|
+
if (savingPermissions.value) return;
|
|
678
|
+
permissionDialog.value = false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function togglePermissionCandidate(subjectId: string, selected: boolean) {
|
|
682
|
+
const next = new Set(selectedPermissionIds.value);
|
|
683
|
+
if (selected) next.add(subjectId);
|
|
684
|
+
else next.delete(subjectId);
|
|
685
|
+
selectedPermissionIds.value = next;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function savePermissions() {
|
|
689
|
+
const category = permissionDialogCategory.value;
|
|
690
|
+
let assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>;
|
|
691
|
+
|
|
692
|
+
if (category === "intercom") {
|
|
693
|
+
assignments = permissionState.assignments.map((assignment) => ({
|
|
694
|
+
subjectId: assignment.subjectId,
|
|
695
|
+
category: assignment.category,
|
|
696
|
+
intercom: selectedPermissionIds.value.has(assignment.subjectId),
|
|
697
|
+
}));
|
|
698
|
+
} else {
|
|
699
|
+
const retained = permissionState.assignments
|
|
700
|
+
.filter((assignment) => assignment.category !== category)
|
|
701
|
+
.map((assignment) => ({
|
|
702
|
+
subjectId: assignment.subjectId,
|
|
703
|
+
category: assignment.category,
|
|
704
|
+
intercom: assignment.intercom,
|
|
705
|
+
}));
|
|
706
|
+
const selected = permissionCandidates.value
|
|
707
|
+
.filter((candidate) => selectedPermissionIds.value.has(candidate.subjectId))
|
|
708
|
+
.map((candidate) => ({
|
|
709
|
+
subjectId: candidate.subjectId,
|
|
710
|
+
category: candidate.category,
|
|
711
|
+
intercom: candidate.intercom,
|
|
712
|
+
}));
|
|
713
|
+
assignments = [...retained, ...selected];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
savingPermissions.value = true;
|
|
717
|
+
try {
|
|
718
|
+
const response = await updateSitePermissions(
|
|
719
|
+
props.site,
|
|
720
|
+
props.org,
|
|
721
|
+
permissionReaderId.value,
|
|
722
|
+
assignments,
|
|
723
|
+
);
|
|
724
|
+
Object.assign(permissionState, response.data);
|
|
725
|
+
permissionDialog.value = false;
|
|
726
|
+
showMessage("HID permissions updated.", "success");
|
|
727
|
+
} catch (error: unknown) {
|
|
728
|
+
showMessage(errorConverter(error), "error");
|
|
729
|
+
} finally {
|
|
730
|
+
savingPermissions.value = false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
735
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function getConfigurationData(response: unknown) {
|
|
739
|
+
if (!isUnknownRecord(response)) return {};
|
|
740
|
+
return isUnknownRecord(response.data) ? response.data : response;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function isEnabledConfiguration(value: unknown) {
|
|
744
|
+
return value === true || value === 1 || value === "1";
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function loadReaderConfiguration() {
|
|
748
|
+
if (!form.readerId) return;
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
const response = await getReaderConfiguration(form.readerId, {
|
|
752
|
+
general: ["online"],
|
|
753
|
+
});
|
|
754
|
+
const configuration = getConfigurationData(response);
|
|
755
|
+
const general = isUnknownRecord(configuration.general) ? configuration.general : {};
|
|
756
|
+
|
|
757
|
+
if (general.online !== undefined) {
|
|
758
|
+
form.onlineMode = isEnabledConfiguration(general.online);
|
|
759
|
+
}
|
|
760
|
+
} catch (error: unknown) {
|
|
761
|
+
console.warn("Unable to load HID reader configuration:", errorConverter(error));
|
|
762
|
+
}
|
|
325
763
|
}
|
|
326
764
|
|
|
327
765
|
function buildPayload() {
|
|
328
766
|
const metadata = {
|
|
329
767
|
...(siteData.value?.metadata || {}),
|
|
330
768
|
hidQrCodePass: {
|
|
331
|
-
enabled: form.
|
|
769
|
+
enabled: form.identificationMethods.qrCode,
|
|
770
|
+
onlineMode: form.onlineMode,
|
|
332
771
|
readerId: form.readerId,
|
|
333
772
|
qrFormat: form.qrFormat,
|
|
773
|
+
identificationMethods: { ...form.identificationMethods },
|
|
334
774
|
printer: {
|
|
335
775
|
vendorId: form.printer.vendorId.trim(),
|
|
336
776
|
productId: form.printer.productId.trim(),
|
|
@@ -352,17 +792,45 @@ async function saveSection(section: string) {
|
|
|
352
792
|
|
|
353
793
|
savingSection.value = section;
|
|
354
794
|
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
795
|
await updateSitebyId(props.site, buildPayload());
|
|
363
796
|
await loadConfig();
|
|
364
|
-
showMessage("HID
|
|
365
|
-
} catch (error:
|
|
797
|
+
showMessage("HID configuration saved.", "success");
|
|
798
|
+
} catch (error: unknown) {
|
|
799
|
+
showMessage(errorConverter(error), "error");
|
|
800
|
+
} finally {
|
|
801
|
+
savingSection.value = "";
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function saveOnlineMode() {
|
|
806
|
+
if (!props.site || !form.readerId) return;
|
|
807
|
+
|
|
808
|
+
savingSection.value = "online-mode";
|
|
809
|
+
try {
|
|
810
|
+
await setReaderConfiguration(form.readerId, {
|
|
811
|
+
general: { online: form.onlineMode ? "1" : "0" },
|
|
812
|
+
});
|
|
813
|
+
await updateSitebyId(props.site, buildPayload());
|
|
814
|
+
showMessage("Online mode updated.", "success");
|
|
815
|
+
} catch (error: unknown) {
|
|
816
|
+
await loadConfig();
|
|
817
|
+
showMessage(errorConverter(error), "error");
|
|
818
|
+
} finally {
|
|
819
|
+
savingSection.value = "";
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function saveIdentificationMethod(method: HidIdentificationMethod) {
|
|
824
|
+
if (!props.site) return;
|
|
825
|
+
|
|
826
|
+
form.enabled = form.identificationMethods.qrCode;
|
|
827
|
+
|
|
828
|
+
savingSection.value = `method-${method}`;
|
|
829
|
+
try {
|
|
830
|
+
await updateSitebyId(props.site, buildPayload());
|
|
831
|
+
showMessage(`${identificationMethodOptions.find((item) => item.key === method)?.label || "Identification method"} updated.`, "success");
|
|
832
|
+
} catch (error: unknown) {
|
|
833
|
+
await loadConfig();
|
|
366
834
|
showMessage(errorConverter(error), "error");
|
|
367
835
|
} finally {
|
|
368
836
|
savingSection.value = "";
|
|
@@ -376,19 +844,36 @@ async function scanPrinter() {
|
|
|
376
844
|
}
|
|
377
845
|
|
|
378
846
|
try {
|
|
379
|
-
const device = await (
|
|
847
|
+
const device = await requestPrinterDevice();
|
|
380
848
|
form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
|
|
381
849
|
form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
|
|
382
850
|
showMessage("Printer selected. Save settings to keep it.", "success");
|
|
383
|
-
} catch (error:
|
|
384
|
-
|
|
385
|
-
|
|
851
|
+
} catch (error: unknown) {
|
|
852
|
+
const message = errorConverter(error);
|
|
853
|
+
if (message !== "No device selected") {
|
|
854
|
+
showMessage(message || "Unable to scan printer.", "error");
|
|
386
855
|
}
|
|
387
856
|
}
|
|
388
857
|
}
|
|
389
858
|
|
|
390
|
-
function testPrint() {
|
|
391
|
-
|
|
859
|
+
async function testPrint() {
|
|
860
|
+
const vendorId = Number(form.printer.vendorId);
|
|
861
|
+
const productId = Number(form.printer.productId);
|
|
862
|
+
if (!vendorId || !productId) {
|
|
863
|
+
showMessage("Please scan a valid USB printer first.", "error");
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
testingPrinter.value = true;
|
|
868
|
+
try {
|
|
869
|
+
const result = await testPrinterConnection(vendorId, productId);
|
|
870
|
+
showMessage(
|
|
871
|
+
result.success ? result.message : result.error || result.message,
|
|
872
|
+
result.success ? "success" : "error",
|
|
873
|
+
);
|
|
874
|
+
} finally {
|
|
875
|
+
testingPrinter.value = false;
|
|
876
|
+
}
|
|
392
877
|
}
|
|
393
878
|
|
|
394
879
|
function showMessage(text: string, color: string) {
|
|
@@ -432,15 +917,15 @@ function showMessage(text: string, color: string) {
|
|
|
432
917
|
padding: 30px 38px;
|
|
433
918
|
}
|
|
434
919
|
|
|
435
|
-
.
|
|
920
|
+
.online-mode-row {
|
|
436
921
|
display: flex;
|
|
437
|
-
align-items:
|
|
922
|
+
align-items: center;
|
|
438
923
|
justify-content: space-between;
|
|
439
924
|
gap: 24px;
|
|
440
|
-
|
|
925
|
+
padding: 0 0 20px;
|
|
441
926
|
}
|
|
442
927
|
|
|
443
|
-
.
|
|
928
|
+
.online-mode-row h3,
|
|
444
929
|
.section-label {
|
|
445
930
|
margin: 0 0 6px;
|
|
446
931
|
color: #233545;
|
|
@@ -448,16 +933,83 @@ function showMessage(text: string, color: string) {
|
|
|
448
933
|
font-weight: 700;
|
|
449
934
|
}
|
|
450
935
|
|
|
451
|
-
.
|
|
936
|
+
.online-mode-row p,
|
|
937
|
+
.section-description {
|
|
452
938
|
margin: 0;
|
|
453
939
|
color: #677280;
|
|
454
940
|
font-size: 12px;
|
|
941
|
+
line-height: 1.5;
|
|
455
942
|
}
|
|
456
943
|
|
|
457
944
|
.form-section {
|
|
458
945
|
padding: 16px 0;
|
|
459
946
|
}
|
|
460
947
|
|
|
948
|
+
.identification-section {
|
|
949
|
+
padding-bottom: 20px;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.methods-grid {
|
|
953
|
+
display: grid;
|
|
954
|
+
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
|
955
|
+
gap: 18px;
|
|
956
|
+
margin-top: 16px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.method-control {
|
|
960
|
+
display: flex;
|
|
961
|
+
align-items: center;
|
|
962
|
+
justify-content: space-between;
|
|
963
|
+
min-width: 0;
|
|
964
|
+
min-height: 42px;
|
|
965
|
+
padding: 0 10px 0 14px;
|
|
966
|
+
border: 1px solid #dce2e7;
|
|
967
|
+
border-radius: 6px;
|
|
968
|
+
color: #334454;
|
|
969
|
+
font-size: 12px;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.method-control span {
|
|
973
|
+
overflow: hidden;
|
|
974
|
+
text-overflow: ellipsis;
|
|
975
|
+
white-space: nowrap;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.method-control--disabled {
|
|
979
|
+
background-color: #f5f6f7;
|
|
980
|
+
color: #8b9297;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.method-control :deep(.v-switch) {
|
|
984
|
+
flex: 0 0 auto;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.method-control :deep(.v-selection-control) {
|
|
988
|
+
min-height: 32px;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.hid-config-switch :deep(.v-switch__track) {
|
|
992
|
+
background-color: #8b9297;
|
|
993
|
+
opacity: 1;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.hid-config-switch :deep(.v-selection-control--dirty .v-switch__track) {
|
|
997
|
+
background-color: #2eaf55 !important;
|
|
998
|
+
color: #2eaf55 !important;
|
|
999
|
+
opacity: 1;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.hid-config-switch :deep(.v-switch__thumb) {
|
|
1003
|
+
background-color: #fff !important;
|
|
1004
|
+
color: #fff !important;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.hid-config-switch.v-input--disabled :deep(.v-switch__track) {
|
|
1008
|
+
background-color: #aeb4b8 !important;
|
|
1009
|
+
color: #aeb4b8 !important;
|
|
1010
|
+
opacity: 1;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
461
1013
|
.printer-grid {
|
|
462
1014
|
display: grid;
|
|
463
1015
|
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
|
|
@@ -483,6 +1035,231 @@ function showMessage(text: string, color: string) {
|
|
|
483
1035
|
align-items: center;
|
|
484
1036
|
}
|
|
485
1037
|
|
|
1038
|
+
.permission-section {
|
|
1039
|
+
padding-top: 2px;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.permission-location {
|
|
1043
|
+
display: grid;
|
|
1044
|
+
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1.35fr);
|
|
1045
|
+
gap: 36px;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
padding: 18px 0;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.permission-location__select {
|
|
1051
|
+
width: 100%;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.permission-row {
|
|
1055
|
+
display: grid;
|
|
1056
|
+
grid-template-columns: minmax(260px, 1fr) minmax(520px, 1.35fr);
|
|
1057
|
+
gap: 36px;
|
|
1058
|
+
align-items: center;
|
|
1059
|
+
padding: 18px 0;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.permission-row--intercom {
|
|
1063
|
+
padding-bottom: 4px;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.permission-copy {
|
|
1067
|
+
min-width: 0;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.permission-actions {
|
|
1071
|
+
display: grid;
|
|
1072
|
+
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
|
1073
|
+
gap: 12px;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.permission-actions--single {
|
|
1077
|
+
grid-template-columns: minmax(180px, 240px);
|
|
1078
|
+
justify-content: end;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.permission-button {
|
|
1082
|
+
justify-content: flex-start;
|
|
1083
|
+
min-height: 42px;
|
|
1084
|
+
padding-inline: 12px;
|
|
1085
|
+
border-color: #dce2e7;
|
|
1086
|
+
color: #334454;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.permission-count {
|
|
1090
|
+
display: inline-grid;
|
|
1091
|
+
place-items: center;
|
|
1092
|
+
width: 20px;
|
|
1093
|
+
height: 20px;
|
|
1094
|
+
margin-right: 8px;
|
|
1095
|
+
border-radius: 50%;
|
|
1096
|
+
background: #dce2e7;
|
|
1097
|
+
color: #526170;
|
|
1098
|
+
font-size: 11px;
|
|
1099
|
+
font-weight: 700;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.permission-button:not(:disabled):hover {
|
|
1103
|
+
border-color: #1473c9;
|
|
1104
|
+
color: #0a5da7;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.permission-dialog {
|
|
1108
|
+
overflow: hidden;
|
|
1109
|
+
border-radius: 6px;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.permission-dialog__header {
|
|
1113
|
+
display: flex;
|
|
1114
|
+
align-items: flex-start;
|
|
1115
|
+
justify-content: space-between;
|
|
1116
|
+
gap: 16px;
|
|
1117
|
+
padding: 16px 18px 14px;
|
|
1118
|
+
border-bottom: 1px solid #e5e9ed;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.permission-dialog__title {
|
|
1122
|
+
color: #172534;
|
|
1123
|
+
font-size: 18px;
|
|
1124
|
+
font-weight: 700;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.permission-dialog__subtitle {
|
|
1128
|
+
margin-top: 4px;
|
|
1129
|
+
color: #687583;
|
|
1130
|
+
font-size: 12px;
|
|
1131
|
+
white-space: normal;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.permission-dialog__body {
|
|
1135
|
+
display: grid;
|
|
1136
|
+
gap: 10px;
|
|
1137
|
+
padding: 14px 18px 16px;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.permission-search :deep(.v-field) {
|
|
1141
|
+
min-height: 44px;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.permission-list-summary {
|
|
1145
|
+
display: flex;
|
|
1146
|
+
align-items: center;
|
|
1147
|
+
justify-content: space-between;
|
|
1148
|
+
min-height: 22px;
|
|
1149
|
+
color: #778391;
|
|
1150
|
+
font-size: 11px;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.permission-list-summary__selected {
|
|
1154
|
+
color: #286c3c;
|
|
1155
|
+
font-weight: 700;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.permission-list {
|
|
1159
|
+
min-height: 180px;
|
|
1160
|
+
max-height: min(360px, 48vh);
|
|
1161
|
+
overflow-y: auto;
|
|
1162
|
+
border: 1px solid #dfe5ea;
|
|
1163
|
+
border-radius: 6px;
|
|
1164
|
+
background: #ffffff;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.permission-list--loading {
|
|
1168
|
+
display: grid;
|
|
1169
|
+
place-items: center;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.permission-list-item {
|
|
1173
|
+
display: grid;
|
|
1174
|
+
grid-template-columns: minmax(0, 1fr) max-content 32px;
|
|
1175
|
+
gap: 12px;
|
|
1176
|
+
align-items: center;
|
|
1177
|
+
min-height: 56px;
|
|
1178
|
+
padding: 7px 12px;
|
|
1179
|
+
border-bottom: 1px solid #edf0f3;
|
|
1180
|
+
cursor: pointer;
|
|
1181
|
+
transition: background-color 120ms ease;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.permission-list-item:last-child {
|
|
1185
|
+
border-bottom: 0;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.permission-list-item:hover {
|
|
1189
|
+
background: #f7fafc;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.permission-list-item--selected {
|
|
1193
|
+
background: #f1f8f3;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.permission-list-item--selected:hover {
|
|
1197
|
+
background: #eaf5ed;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.permission-list-item__content {
|
|
1201
|
+
display: grid;
|
|
1202
|
+
gap: 2px;
|
|
1203
|
+
min-width: 0;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.permission-list-item__content strong,
|
|
1207
|
+
.permission-list-item__content small {
|
|
1208
|
+
overflow: hidden;
|
|
1209
|
+
text-overflow: ellipsis;
|
|
1210
|
+
white-space: nowrap;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.permission-list-item__content strong {
|
|
1214
|
+
color: #243443;
|
|
1215
|
+
font-size: 13px;
|
|
1216
|
+
line-height: 1.35;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.permission-list-item__content small,
|
|
1220
|
+
.permission-list-item__group {
|
|
1221
|
+
color: #778391;
|
|
1222
|
+
font-size: 11px;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.permission-list-item__group {
|
|
1226
|
+
max-width: 170px;
|
|
1227
|
+
overflow: hidden;
|
|
1228
|
+
padding: 4px 9px;
|
|
1229
|
+
border-radius: 4px;
|
|
1230
|
+
background: #eef2f5;
|
|
1231
|
+
text-overflow: ellipsis;
|
|
1232
|
+
white-space: nowrap;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.permission-list-item__checkbox {
|
|
1236
|
+
justify-self: end;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.permission-list-item__checkbox :deep(.v-selection-control) {
|
|
1240
|
+
min-height: 32px;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.permission-empty {
|
|
1244
|
+
display: flex;
|
|
1245
|
+
flex-direction: column;
|
|
1246
|
+
align-items: center;
|
|
1247
|
+
justify-content: center;
|
|
1248
|
+
gap: 10px;
|
|
1249
|
+
min-height: 180px;
|
|
1250
|
+
padding: 24px;
|
|
1251
|
+
color: #75818c;
|
|
1252
|
+
font-size: 13px;
|
|
1253
|
+
text-align: center;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.permission-dialog__actions {
|
|
1257
|
+
justify-content: flex-end;
|
|
1258
|
+
gap: 8px;
|
|
1259
|
+
padding: 12px 18px;
|
|
1260
|
+
border-top: 1px solid #e5e9ed;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
486
1263
|
.save-button {
|
|
487
1264
|
background: #082e43;
|
|
488
1265
|
color: #ffffff;
|
|
@@ -592,11 +1369,21 @@ function showMessage(text: string, color: string) {
|
|
|
592
1369
|
}
|
|
593
1370
|
|
|
594
1371
|
@media (max-width: 1100px) {
|
|
1372
|
+
.methods-grid {
|
|
1373
|
+
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
|
1374
|
+
}
|
|
1375
|
+
|
|
595
1376
|
.printer-grid,
|
|
596
1377
|
.template-grid,
|
|
597
|
-
.validity-grid
|
|
1378
|
+
.validity-grid,
|
|
1379
|
+
.permission-location,
|
|
1380
|
+
.permission-row {
|
|
598
1381
|
grid-template-columns: 1fr 1fr;
|
|
599
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
.permission-actions {
|
|
1385
|
+
grid-template-columns: 1fr;
|
|
1386
|
+
}
|
|
600
1387
|
}
|
|
601
1388
|
|
|
602
1389
|
@media (max-width: 700px) {
|
|
@@ -604,15 +1391,40 @@ function showMessage(text: string, color: string) {
|
|
|
604
1391
|
padding: 22px 16px;
|
|
605
1392
|
}
|
|
606
1393
|
|
|
607
|
-
.
|
|
1394
|
+
.online-mode-row,
|
|
608
1395
|
.printer-grid,
|
|
609
1396
|
.template-grid,
|
|
610
|
-
.validity-grid
|
|
1397
|
+
.validity-grid,
|
|
1398
|
+
.methods-grid,
|
|
1399
|
+
.permission-location,
|
|
1400
|
+
.permission-row,
|
|
1401
|
+
.permission-actions {
|
|
611
1402
|
grid-template-columns: 1fr;
|
|
612
1403
|
}
|
|
613
1404
|
|
|
614
|
-
.
|
|
1405
|
+
.online-mode-row {
|
|
615
1406
|
display: grid;
|
|
616
1407
|
}
|
|
1408
|
+
|
|
1409
|
+
.permission-actions--single {
|
|
1410
|
+
justify-content: stretch;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.permission-list-item {
|
|
1414
|
+
grid-template-columns: minmax(0, 1fr) 32px;
|
|
1415
|
+
gap: 8px;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.permission-list-item__group {
|
|
1419
|
+
grid-column: 1;
|
|
1420
|
+
grid-row: 2;
|
|
1421
|
+
justify-self: start;
|
|
1422
|
+
max-width: 100%;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.permission-list-item__checkbox {
|
|
1426
|
+
grid-column: 2;
|
|
1427
|
+
grid-row: 1 / span 2;
|
|
1428
|
+
}
|
|
617
1429
|
}
|
|
618
1430
|
</style>
|