@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
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/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -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/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -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
|
-
qrFormat:
|
|
586
|
+
qrFormat: "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,183 @@ 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
|
+
identifier: ["pin_identification_enabled", "multi_factor_authentication"],
|
|
754
|
+
});
|
|
755
|
+
const configuration = getConfigurationData(response);
|
|
756
|
+
const general = isUnknownRecord(configuration.general) ? configuration.general : {};
|
|
757
|
+
const identifier = isUnknownRecord(configuration.identifier) ? configuration.identifier : {};
|
|
758
|
+
|
|
759
|
+
if (general.online !== undefined) {
|
|
760
|
+
form.onlineMode = isEnabledConfiguration(general.online);
|
|
761
|
+
}
|
|
762
|
+
if (identifier.pin_identification_enabled !== undefined) {
|
|
763
|
+
const pinEnabled = isEnabledConfiguration(identifier.pin_identification_enabled);
|
|
764
|
+
form.identificationMethods.pin = pinEnabled;
|
|
765
|
+
form.identificationMethods.idPassword = !pinEnabled;
|
|
766
|
+
}
|
|
767
|
+
} catch (error: unknown) {
|
|
768
|
+
console.warn("Unable to load HID reader configuration:", errorConverter(error));
|
|
769
|
+
}
|
|
325
770
|
}
|
|
326
771
|
|
|
327
772
|
function buildPayload() {
|
|
328
773
|
const metadata = {
|
|
329
774
|
...(siteData.value?.metadata || {}),
|
|
330
775
|
hidQrCodePass: {
|
|
331
|
-
enabled: form.
|
|
776
|
+
enabled: form.identificationMethods.qrCode,
|
|
777
|
+
onlineMode: form.onlineMode,
|
|
332
778
|
readerId: form.readerId,
|
|
333
|
-
|
|
779
|
+
// Visitor QR passes always use Amico alphanumeric qrcodes.
|
|
780
|
+
qrFormat: "0",
|
|
781
|
+
identificationMethods: { ...form.identificationMethods },
|
|
334
782
|
printer: {
|
|
335
783
|
vendorId: form.printer.vendorId.trim(),
|
|
336
784
|
productId: form.printer.productId.trim(),
|
|
@@ -352,17 +800,59 @@ async function saveSection(section: string) {
|
|
|
352
800
|
|
|
353
801
|
savingSection.value = section;
|
|
354
802
|
try {
|
|
355
|
-
|
|
803
|
+
await updateSitebyId(props.site, buildPayload());
|
|
804
|
+
await loadConfig();
|
|
805
|
+
showMessage("HID configuration saved.", "success");
|
|
806
|
+
} catch (error: unknown) {
|
|
807
|
+
showMessage(errorConverter(error), "error");
|
|
808
|
+
} finally {
|
|
809
|
+
savingSection.value = "";
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function saveOnlineMode() {
|
|
814
|
+
if (!props.site || !form.readerId) return;
|
|
815
|
+
|
|
816
|
+
savingSection.value = "online-mode";
|
|
817
|
+
try {
|
|
818
|
+
await setReaderConfiguration(form.readerId, {
|
|
819
|
+
general: { online: form.onlineMode ? "1" : "0" },
|
|
820
|
+
});
|
|
821
|
+
await updateSitebyId(props.site, buildPayload());
|
|
822
|
+
showMessage("Online mode updated.", "success");
|
|
823
|
+
} catch (error: unknown) {
|
|
824
|
+
await loadConfig();
|
|
825
|
+
showMessage(errorConverter(error), "error");
|
|
826
|
+
} finally {
|
|
827
|
+
savingSection.value = "";
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function saveIdentificationMethod(method: HidIdentificationMethod) {
|
|
832
|
+
if (!props.site) return;
|
|
833
|
+
|
|
834
|
+
form.enabled = form.identificationMethods.qrCode;
|
|
835
|
+
|
|
836
|
+
savingSection.value = `method-${method}`;
|
|
837
|
+
try {
|
|
838
|
+
if (method === "pin" || method === "idPassword") {
|
|
839
|
+
if (method === "pin" && form.identificationMethods.pin) {
|
|
840
|
+
form.identificationMethods.idPassword = false;
|
|
841
|
+
} else if (method === "idPassword" && form.identificationMethods.idPassword) {
|
|
842
|
+
form.identificationMethods.pin = false;
|
|
843
|
+
}
|
|
356
844
|
await setReaderConfiguration(form.readerId, {
|
|
357
|
-
|
|
358
|
-
|
|
845
|
+
identifier: {
|
|
846
|
+
pin_identification_enabled: form.identificationMethods.pin ? "1" : "0",
|
|
847
|
+
// Mode 1: PIN is an alternative credential, never a second factor.
|
|
848
|
+
multi_factor_authentication: "0",
|
|
359
849
|
},
|
|
360
850
|
});
|
|
361
851
|
}
|
|
362
852
|
await updateSitebyId(props.site, buildPayload());
|
|
853
|
+
showMessage(`${identificationMethodOptions.find((item) => item.key === method)?.label || "Identification method"} updated.`, "success");
|
|
854
|
+
} catch (error: unknown) {
|
|
363
855
|
await loadConfig();
|
|
364
|
-
showMessage("HID QR code settings saved.", "success");
|
|
365
|
-
} catch (error: any) {
|
|
366
856
|
showMessage(errorConverter(error), "error");
|
|
367
857
|
} finally {
|
|
368
858
|
savingSection.value = "";
|
|
@@ -376,19 +866,36 @@ async function scanPrinter() {
|
|
|
376
866
|
}
|
|
377
867
|
|
|
378
868
|
try {
|
|
379
|
-
const device = await (
|
|
869
|
+
const device = await requestPrinterDevice();
|
|
380
870
|
form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
|
|
381
871
|
form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
|
|
382
872
|
showMessage("Printer selected. Save settings to keep it.", "success");
|
|
383
|
-
} catch (error:
|
|
384
|
-
|
|
385
|
-
|
|
873
|
+
} catch (error: unknown) {
|
|
874
|
+
const message = errorConverter(error);
|
|
875
|
+
if (message !== "No device selected") {
|
|
876
|
+
showMessage(message || "Unable to scan printer.", "error");
|
|
386
877
|
}
|
|
387
878
|
}
|
|
388
879
|
}
|
|
389
880
|
|
|
390
|
-
function testPrint() {
|
|
391
|
-
|
|
881
|
+
async function testPrint() {
|
|
882
|
+
const vendorId = Number(form.printer.vendorId);
|
|
883
|
+
const productId = Number(form.printer.productId);
|
|
884
|
+
if (!vendorId || !productId) {
|
|
885
|
+
showMessage("Please scan a valid USB printer first.", "error");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
testingPrinter.value = true;
|
|
890
|
+
try {
|
|
891
|
+
const result = await testPrinterConnection(vendorId, productId);
|
|
892
|
+
showMessage(
|
|
893
|
+
result.success ? result.message : result.error || result.message,
|
|
894
|
+
result.success ? "success" : "error",
|
|
895
|
+
);
|
|
896
|
+
} finally {
|
|
897
|
+
testingPrinter.value = false;
|
|
898
|
+
}
|
|
392
899
|
}
|
|
393
900
|
|
|
394
901
|
function showMessage(text: string, color: string) {
|
|
@@ -432,15 +939,15 @@ function showMessage(text: string, color: string) {
|
|
|
432
939
|
padding: 30px 38px;
|
|
433
940
|
}
|
|
434
941
|
|
|
435
|
-
.
|
|
942
|
+
.online-mode-row {
|
|
436
943
|
display: flex;
|
|
437
|
-
align-items:
|
|
944
|
+
align-items: center;
|
|
438
945
|
justify-content: space-between;
|
|
439
946
|
gap: 24px;
|
|
440
|
-
|
|
947
|
+
padding: 0 0 20px;
|
|
441
948
|
}
|
|
442
949
|
|
|
443
|
-
.
|
|
950
|
+
.online-mode-row h3,
|
|
444
951
|
.section-label {
|
|
445
952
|
margin: 0 0 6px;
|
|
446
953
|
color: #233545;
|
|
@@ -448,16 +955,83 @@ function showMessage(text: string, color: string) {
|
|
|
448
955
|
font-weight: 700;
|
|
449
956
|
}
|
|
450
957
|
|
|
451
|
-
.
|
|
958
|
+
.online-mode-row p,
|
|
959
|
+
.section-description {
|
|
452
960
|
margin: 0;
|
|
453
961
|
color: #677280;
|
|
454
962
|
font-size: 12px;
|
|
963
|
+
line-height: 1.5;
|
|
455
964
|
}
|
|
456
965
|
|
|
457
966
|
.form-section {
|
|
458
967
|
padding: 16px 0;
|
|
459
968
|
}
|
|
460
969
|
|
|
970
|
+
.identification-section {
|
|
971
|
+
padding-bottom: 20px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.methods-grid {
|
|
975
|
+
display: grid;
|
|
976
|
+
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
|
977
|
+
gap: 18px;
|
|
978
|
+
margin-top: 16px;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.method-control {
|
|
982
|
+
display: flex;
|
|
983
|
+
align-items: center;
|
|
984
|
+
justify-content: space-between;
|
|
985
|
+
min-width: 0;
|
|
986
|
+
min-height: 42px;
|
|
987
|
+
padding: 0 10px 0 14px;
|
|
988
|
+
border: 1px solid #dce2e7;
|
|
989
|
+
border-radius: 6px;
|
|
990
|
+
color: #334454;
|
|
991
|
+
font-size: 12px;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.method-control span {
|
|
995
|
+
overflow: hidden;
|
|
996
|
+
text-overflow: ellipsis;
|
|
997
|
+
white-space: nowrap;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.method-control--disabled {
|
|
1001
|
+
background-color: #f5f6f7;
|
|
1002
|
+
color: #8b9297;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.method-control :deep(.v-switch) {
|
|
1006
|
+
flex: 0 0 auto;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.method-control :deep(.v-selection-control) {
|
|
1010
|
+
min-height: 32px;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.hid-config-switch :deep(.v-switch__track) {
|
|
1014
|
+
background-color: #8b9297;
|
|
1015
|
+
opacity: 1;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.hid-config-switch :deep(.v-selection-control--dirty .v-switch__track) {
|
|
1019
|
+
background-color: #2eaf55 !important;
|
|
1020
|
+
color: #2eaf55 !important;
|
|
1021
|
+
opacity: 1;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.hid-config-switch :deep(.v-switch__thumb) {
|
|
1025
|
+
background-color: #fff !important;
|
|
1026
|
+
color: #fff !important;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.hid-config-switch.v-input--disabled :deep(.v-switch__track) {
|
|
1030
|
+
background-color: #aeb4b8 !important;
|
|
1031
|
+
color: #aeb4b8 !important;
|
|
1032
|
+
opacity: 1;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
461
1035
|
.printer-grid {
|
|
462
1036
|
display: grid;
|
|
463
1037
|
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
|
|
@@ -483,6 +1057,231 @@ function showMessage(text: string, color: string) {
|
|
|
483
1057
|
align-items: center;
|
|
484
1058
|
}
|
|
485
1059
|
|
|
1060
|
+
.permission-section {
|
|
1061
|
+
padding-top: 2px;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.permission-location {
|
|
1065
|
+
display: grid;
|
|
1066
|
+
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1.35fr);
|
|
1067
|
+
gap: 36px;
|
|
1068
|
+
align-items: center;
|
|
1069
|
+
padding: 18px 0;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.permission-location__select {
|
|
1073
|
+
width: 100%;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.permission-row {
|
|
1077
|
+
display: grid;
|
|
1078
|
+
grid-template-columns: minmax(260px, 1fr) minmax(520px, 1.35fr);
|
|
1079
|
+
gap: 36px;
|
|
1080
|
+
align-items: center;
|
|
1081
|
+
padding: 18px 0;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.permission-row--intercom {
|
|
1085
|
+
padding-bottom: 4px;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.permission-copy {
|
|
1089
|
+
min-width: 0;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.permission-actions {
|
|
1093
|
+
display: grid;
|
|
1094
|
+
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
|
1095
|
+
gap: 12px;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.permission-actions--single {
|
|
1099
|
+
grid-template-columns: minmax(180px, 240px);
|
|
1100
|
+
justify-content: end;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.permission-button {
|
|
1104
|
+
justify-content: flex-start;
|
|
1105
|
+
min-height: 42px;
|
|
1106
|
+
padding-inline: 12px;
|
|
1107
|
+
border-color: #dce2e7;
|
|
1108
|
+
color: #334454;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.permission-count {
|
|
1112
|
+
display: inline-grid;
|
|
1113
|
+
place-items: center;
|
|
1114
|
+
width: 20px;
|
|
1115
|
+
height: 20px;
|
|
1116
|
+
margin-right: 8px;
|
|
1117
|
+
border-radius: 50%;
|
|
1118
|
+
background: #dce2e7;
|
|
1119
|
+
color: #526170;
|
|
1120
|
+
font-size: 11px;
|
|
1121
|
+
font-weight: 700;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.permission-button:not(:disabled):hover {
|
|
1125
|
+
border-color: #1473c9;
|
|
1126
|
+
color: #0a5da7;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.permission-dialog {
|
|
1130
|
+
overflow: hidden;
|
|
1131
|
+
border-radius: 6px;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.permission-dialog__header {
|
|
1135
|
+
display: flex;
|
|
1136
|
+
align-items: flex-start;
|
|
1137
|
+
justify-content: space-between;
|
|
1138
|
+
gap: 16px;
|
|
1139
|
+
padding: 16px 18px 14px;
|
|
1140
|
+
border-bottom: 1px solid #e5e9ed;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.permission-dialog__title {
|
|
1144
|
+
color: #172534;
|
|
1145
|
+
font-size: 18px;
|
|
1146
|
+
font-weight: 700;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.permission-dialog__subtitle {
|
|
1150
|
+
margin-top: 4px;
|
|
1151
|
+
color: #687583;
|
|
1152
|
+
font-size: 12px;
|
|
1153
|
+
white-space: normal;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.permission-dialog__body {
|
|
1157
|
+
display: grid;
|
|
1158
|
+
gap: 10px;
|
|
1159
|
+
padding: 14px 18px 16px;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.permission-search :deep(.v-field) {
|
|
1163
|
+
min-height: 44px;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.permission-list-summary {
|
|
1167
|
+
display: flex;
|
|
1168
|
+
align-items: center;
|
|
1169
|
+
justify-content: space-between;
|
|
1170
|
+
min-height: 22px;
|
|
1171
|
+
color: #778391;
|
|
1172
|
+
font-size: 11px;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.permission-list-summary__selected {
|
|
1176
|
+
color: #286c3c;
|
|
1177
|
+
font-weight: 700;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.permission-list {
|
|
1181
|
+
min-height: 180px;
|
|
1182
|
+
max-height: min(360px, 48vh);
|
|
1183
|
+
overflow-y: auto;
|
|
1184
|
+
border: 1px solid #dfe5ea;
|
|
1185
|
+
border-radius: 6px;
|
|
1186
|
+
background: #ffffff;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.permission-list--loading {
|
|
1190
|
+
display: grid;
|
|
1191
|
+
place-items: center;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.permission-list-item {
|
|
1195
|
+
display: grid;
|
|
1196
|
+
grid-template-columns: minmax(0, 1fr) max-content 32px;
|
|
1197
|
+
gap: 12px;
|
|
1198
|
+
align-items: center;
|
|
1199
|
+
min-height: 56px;
|
|
1200
|
+
padding: 7px 12px;
|
|
1201
|
+
border-bottom: 1px solid #edf0f3;
|
|
1202
|
+
cursor: pointer;
|
|
1203
|
+
transition: background-color 120ms ease;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.permission-list-item:last-child {
|
|
1207
|
+
border-bottom: 0;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.permission-list-item:hover {
|
|
1211
|
+
background: #f7fafc;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.permission-list-item--selected {
|
|
1215
|
+
background: #f1f8f3;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.permission-list-item--selected:hover {
|
|
1219
|
+
background: #eaf5ed;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.permission-list-item__content {
|
|
1223
|
+
display: grid;
|
|
1224
|
+
gap: 2px;
|
|
1225
|
+
min-width: 0;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.permission-list-item__content strong,
|
|
1229
|
+
.permission-list-item__content small {
|
|
1230
|
+
overflow: hidden;
|
|
1231
|
+
text-overflow: ellipsis;
|
|
1232
|
+
white-space: nowrap;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.permission-list-item__content strong {
|
|
1236
|
+
color: #243443;
|
|
1237
|
+
font-size: 13px;
|
|
1238
|
+
line-height: 1.35;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.permission-list-item__content small,
|
|
1242
|
+
.permission-list-item__group {
|
|
1243
|
+
color: #778391;
|
|
1244
|
+
font-size: 11px;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.permission-list-item__group {
|
|
1248
|
+
max-width: 170px;
|
|
1249
|
+
overflow: hidden;
|
|
1250
|
+
padding: 4px 9px;
|
|
1251
|
+
border-radius: 4px;
|
|
1252
|
+
background: #eef2f5;
|
|
1253
|
+
text-overflow: ellipsis;
|
|
1254
|
+
white-space: nowrap;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.permission-list-item__checkbox {
|
|
1258
|
+
justify-self: end;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.permission-list-item__checkbox :deep(.v-selection-control) {
|
|
1262
|
+
min-height: 32px;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.permission-empty {
|
|
1266
|
+
display: flex;
|
|
1267
|
+
flex-direction: column;
|
|
1268
|
+
align-items: center;
|
|
1269
|
+
justify-content: center;
|
|
1270
|
+
gap: 10px;
|
|
1271
|
+
min-height: 180px;
|
|
1272
|
+
padding: 24px;
|
|
1273
|
+
color: #75818c;
|
|
1274
|
+
font-size: 13px;
|
|
1275
|
+
text-align: center;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.permission-dialog__actions {
|
|
1279
|
+
justify-content: flex-end;
|
|
1280
|
+
gap: 8px;
|
|
1281
|
+
padding: 12px 18px;
|
|
1282
|
+
border-top: 1px solid #e5e9ed;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
486
1285
|
.save-button {
|
|
487
1286
|
background: #082e43;
|
|
488
1287
|
color: #ffffff;
|
|
@@ -592,11 +1391,21 @@ function showMessage(text: string, color: string) {
|
|
|
592
1391
|
}
|
|
593
1392
|
|
|
594
1393
|
@media (max-width: 1100px) {
|
|
1394
|
+
.methods-grid {
|
|
1395
|
+
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
|
1396
|
+
}
|
|
1397
|
+
|
|
595
1398
|
.printer-grid,
|
|
596
1399
|
.template-grid,
|
|
597
|
-
.validity-grid
|
|
1400
|
+
.validity-grid,
|
|
1401
|
+
.permission-location,
|
|
1402
|
+
.permission-row {
|
|
598
1403
|
grid-template-columns: 1fr 1fr;
|
|
599
1404
|
}
|
|
1405
|
+
|
|
1406
|
+
.permission-actions {
|
|
1407
|
+
grid-template-columns: 1fr;
|
|
1408
|
+
}
|
|
600
1409
|
}
|
|
601
1410
|
|
|
602
1411
|
@media (max-width: 700px) {
|
|
@@ -604,15 +1413,40 @@ function showMessage(text: string, color: string) {
|
|
|
604
1413
|
padding: 22px 16px;
|
|
605
1414
|
}
|
|
606
1415
|
|
|
607
|
-
.
|
|
1416
|
+
.online-mode-row,
|
|
608
1417
|
.printer-grid,
|
|
609
1418
|
.template-grid,
|
|
610
|
-
.validity-grid
|
|
1419
|
+
.validity-grid,
|
|
1420
|
+
.methods-grid,
|
|
1421
|
+
.permission-location,
|
|
1422
|
+
.permission-row,
|
|
1423
|
+
.permission-actions {
|
|
611
1424
|
grid-template-columns: 1fr;
|
|
612
1425
|
}
|
|
613
1426
|
|
|
614
|
-
.
|
|
1427
|
+
.online-mode-row {
|
|
615
1428
|
display: grid;
|
|
616
1429
|
}
|
|
1430
|
+
|
|
1431
|
+
.permission-actions--single {
|
|
1432
|
+
justify-content: stretch;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.permission-list-item {
|
|
1436
|
+
grid-template-columns: minmax(0, 1fr) 32px;
|
|
1437
|
+
gap: 8px;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.permission-list-item__group {
|
|
1441
|
+
grid-column: 1;
|
|
1442
|
+
grid-row: 2;
|
|
1443
|
+
justify-self: start;
|
|
1444
|
+
max-width: 100%;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.permission-list-item__checkbox {
|
|
1448
|
+
grid-column: 2;
|
|
1449
|
+
grid-row: 1 / span 2;
|
|
1450
|
+
}
|
|
617
1451
|
}
|
|
618
1452
|
</style>
|