@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201
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/assets/css/primitives.css +1113 -0
- package/assets/css/screens.css +1097 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +236 -0
- package/components/AcceptDialog.vue +17 -18
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AccessCardAddForm.vue +42 -42
- package/components/AccessCardAssignToUnitForm.vue +13 -21
- package/components/AccessCardDeleteDialog.vue +41 -39
- package/components/AccessCardDetailsDialog.vue +125 -79
- package/components/AccessCardPreviewDialog.vue +58 -84
- package/components/AccessCardQrTagging.vue +60 -63
- package/components/AccessCardReplaceForm.vue +27 -23
- package/components/AccessManagement.vue +86 -85
- package/components/AddEqupmentForm.vue +18 -22
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppButton.vue +62 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +49 -0
- package/components/AppField.vue +76 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +247 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +14 -7
- package/components/AreaFormDialog.vue +71 -59
- package/components/AreaMain.vue +73 -107
- package/components/AttachmentsViewer.vue +84 -22
- package/components/AttendanceCheckInOutDialog.vue +85 -88
- package/components/AttendanceDetailsDialog.vue +57 -63
- package/components/AttendanceMain.vue +4 -3
- package/components/AttendanceMapSearchDialog.vue +42 -44
- package/components/AttendanceSettingsDialog.vue +79 -63
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BtnUploadFile.vue +53 -11
- package/components/BuildingForm.vue +34 -27
- package/components/BuildingManagement/buildings.vue +39 -39
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BuildingUnitFormAdd.vue +20 -23
- package/components/BuildingUnitFormEdit.vue +8 -4
- package/components/BulletinBoardForm.vue +19 -24
- package/components/BulletinBoardManagement.vue +49 -57
- package/components/BulletinBoardView.vue +9 -6
- package/components/BulletinExpirationChip.vue +13 -4
- package/components/Button/Close.vue +2 -2
- package/components/CameraForm.vue +21 -22
- package/components/CameraMain.vue +50 -49
- package/components/Card/DeleteConfirmation.vue +24 -22
- package/components/Card/MemberInfoSummary.vue +28 -3
- package/components/Card/Toggle.vue +15 -3
- package/components/CardToolbar.vue +37 -0
- package/components/Carousel.vue +56 -36
- package/components/Chat/Bubbles.vue +43 -14
- package/components/Chat/Information.vue +49 -32
- package/components/Chat/ListCard.vue +38 -9
- package/components/Chat/Message.vue +41 -15
- package/components/Chat/Navigation.vue +27 -15
- package/components/Chat/SkeletonLoader.vue +9 -2
- package/components/ChecklistItemRow.vue +25 -1
- package/components/CleaningScheduleMain.vue +44 -49
- package/components/ClientDetailForm.vue +81 -56
- package/components/ClientMain.vue +92 -82
- package/components/ConfirmDialog.vue +14 -8
- package/components/Container/Standard.vue +9 -7
- package/components/CreateSubsciptionPlan.vue +148 -89
- package/components/DashboardEmptyState.vue +48 -3
- package/components/DashboardMain.vue +924 -676
- package/components/DashboardPanel.vue +84 -21
- package/components/DeliveryCompany.vue +34 -20
- package/components/Dialog/DeleteConfirmation.vue +20 -21
- package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
- package/components/Dialog/ReusablePrompt.vue +30 -29
- package/components/Dialog/UpdateMoreAction.vue +62 -59
- package/components/DocumentForm.vue +14 -21
- package/components/DocumentManagement.vue +162 -108
- package/components/DocumentViewer.vue +29 -12
- package/components/DrawImage.vue +29 -21
- package/components/EntryPass/QrTemplatePreview.vue +27 -16
- package/components/EntryPassInformation.vue +40 -10
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +84 -97
- package/components/EquipmentManagementMain.vue +81 -103
- package/components/Facility/BookingSetup.vue +43 -52
- package/components/Feedback/Form.vue +26 -12
- package/components/FeedbackMain.vue +35 -49
- package/components/FileInput.vue +75 -58
- package/components/FileInputWithList.vue +59 -12
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +512 -490
- package/components/HidCameraCaptureDialog.vue +216 -0
- package/components/HidEnabledGate.vue +67 -9
- package/components/HidIntercomManagement.vue +1322 -653
- package/components/HidQrCodeConfiguration.vue +1030 -258
- package/components/HidReaderForm.vue +142 -59
- package/components/HidReaderManagement.vue +211 -323
- package/components/HidServiceSettingsPanel.vue +44 -13
- package/components/HidUserEnrollment.vue +1185 -412
- package/components/HygieneUpdateMoreAction.vue +44 -44
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/IncidentReport/IncidentInformation.vue +10 -10
- package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
- package/components/IncidentReport/affectedEntities.vue +41 -20
- package/components/Input/Date.vue +3 -0
- package/components/Input/DatePicker.vue +1 -2
- package/components/Input/DateTimePicker.vue +1 -1
- package/components/Input/File.vue +55 -10
- package/components/Input/FileV2.vue +39 -6
- package/components/Input/InputPhoneNumberV2.vue +2 -1
- package/components/Input/ListGroupSelection.vue +56 -8
- package/components/Input/NRICNumber.vue +1 -0
- package/components/Input/NewDate.vue +1 -0
- package/components/Input/Number.vue +1 -0
- package/components/Input/Password.vue +1 -0
- package/components/Input/PhoneNumber.vue +21 -1
- package/components/Input/VehicleNumber.vue +1 -0
- package/components/InputLabel.vue +2 -2
- package/components/InvitationClientForm.vue +21 -19
- package/components/InvitationForm.vue +15 -21
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +263 -36
- package/components/Layout/NavigationDrawer.vue +300 -30
- package/components/ListItem.vue +33 -3
- package/components/LocalPagination.vue +27 -3
- package/components/ManageChecklistMain.vue +136 -86
- package/components/MemberInformation.vue +32 -16
- package/components/MemberMain.vue +113 -93
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +83 -3
- package/components/Nfc/NFCPatrolReportMain.vue +47 -33
- package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/Nfc/NFCPatrolSettings.vue +14 -4
- package/components/Nfc/NFCTagForm.vue +22 -8
- package/components/Nfc/NFCTagMain.vue +41 -43
- package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
- package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
- package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
- package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
- package/components/NotificationSettings.vue +373 -0
- package/components/NumberSettingField.vue +9 -2
- package/components/OnlineFormConfigurationForm.vue +725 -237
- package/components/OnlineFormFill.vue +213 -106
- package/components/OnlineFormRenderPreview.vue +25 -30
- package/components/OnlineFormsConfiguration.vue +113 -109
- package/components/OrgViewDialog.vue +34 -16
- package/components/OvernightParkingAvailability.vue +49 -26
- package/components/OvernightParkingManagement.vue +78 -18
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +109 -53
- package/components/PasswordConfirmation.vue +44 -26
- package/components/PeopleForm.vue +41 -20
- package/components/PhotoUpload.vue +110 -48
- package/components/PlaceholderComponent.vue +35 -19
- package/components/PlanActionsMenu.vue +12 -2
- package/components/PlanStatusBadge.vue +7 -8
- package/components/PlateNumberDisplay.vue +9 -6
- package/components/QrTemplate/PrintDialog.vue +66 -43
- package/components/RolePermissionFormCreate.vue +104 -79
- package/components/RolePermissionFormPreviewUpdate.vue +111 -109
- package/components/RolePermissionMain.vue +183 -81
- package/components/ScanVisitorQRCode.vue +99 -35
- package/components/ScheduleAreaMain.vue +34 -59
- package/components/ScheduleTaskForm.vue +61 -42
- package/components/ScheduleTaskMain.vue +56 -69
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/ServiceProviderFormCreate.vue +29 -20
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +601 -432
- package/components/Signature.vue +40 -34
- package/components/SignaturePad.vue +5 -29
- package/components/SiteSettings.vue +41 -11
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SlideCardGroup.vue +35 -11
- package/components/Snackbar.vue +36 -3
- package/components/SpecificAttr.vue +18 -8
- package/components/StatusChip.vue +156 -0
- package/components/StockCard.vue +17 -12
- package/components/SubscriptionPlanTable.vue +207 -140
- package/components/SwitchContext.vue +201 -9
- package/components/TableHygiene.vue +36 -46
- package/components/TableList.vue +10 -10
- package/components/TableListSecondary.vue +105 -54
- package/components/TableMain.vue +251 -78
- package/components/TableV3.vue +61 -70
- package/components/TiptapEditor.vue +33 -40
- package/components/TiptapEditorQr.vue +5 -2
- package/components/Tooltip/Info.vue +69 -24
- package/components/UnitMain.vue +40 -60
- package/components/UnitPersonCard.vue +42 -13
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleManagement.vue +26 -12
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorForm.vue +774 -210
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorManagement.vue +239 -82
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSearchResultSummary.vue +60 -16
- package/components/VisitorSocketPopUp.vue +22 -14
- package/components/VisitorsReportPreview.vue +44 -29
- package/components/WorkOrder/Create.vue +26 -17
- package/components/WorkOrder/ListView.vue +38 -7
- package/components/WorkOrder/Main.vue +62 -59
- package/composables/useFeedback.ts +3 -3
- package/composables/useHidAmico.ts +269 -0
- package/composables/useHidNavigation.ts +2 -9
- package/composables/useMember.ts +3 -3
- package/composables/useNFCPatrolDailyReport.ts +13 -0
- package/composables/useNFCPatrolMonthlyReport.ts +22 -56
- package/composables/useNFCPatrolReport.ts +13 -0
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProvider.ts +3 -3
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSubscriptionPlan.ts +62 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useWebUsb.ts +127 -37
- package/composables/useWorkOrder.ts +17 -3
- package/error.vue +30 -6
- package/nuxt.config.ts +36 -0
- package/package.json +25 -4
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/pages/payment-method-linked.vue +34 -10
- package/pages/require-customer.vue +48 -10
- package/pages/require-organization-membership.vue +32 -8
- package/pages/unauthorized.vue +29 -4
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +69 -2
- package/types/nfc-patrol-report.ts +3 -2
- package/types/site.d.ts +71 -0
- package/types/subscription.ts +18 -11
- package/utils/breadcrumb.test.ts +108 -0
- package/utils/breadcrumb.ts +82 -0
- package/utils/dashboard.test.ts +118 -0
- package/utils/dashboard.ts +53 -0
- package/utils/data.test.ts +95 -0
- package/utils/data.ts +40 -2
- package/utils/hid-discover.test.ts +102 -0
- package/utils/list-page.test.ts +60 -0
- package/utils/list-page.ts +38 -0
- package/utils/materialSymbols.ts +363 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/nfc-patrol-report.test.ts +130 -0
- package/utils/nfc-patrol-report.ts +101 -0
- package/utils/role.test.ts +69 -0
- package/utils/role.ts +10 -0
- package/utils/route-name.test.ts +85 -0
- package/utils/route-name.ts +24 -0
- package/utils/status.test.ts +110 -0
- package/utils/status.ts +135 -0
- package/utils/table-card.test.ts +84 -0
- package/utils/theme-aa-ledger.ts +148 -0
- package/utils/theme.test.ts +485 -7
- package/utils/theme.ts +295 -110
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.editorconfig +0 -12
- package/.github/workflows/main.yml +0 -17
- package/.github/workflows/publish-staging.yml +0 -47
- package/.github/workflows/publish.yml +0 -39
- package/PUBLISHING.md +0 -269
- package/components/HidIdentityMapping.vue +0 -975
- package/components/ScheduleTaskAreaFormDialog.vue +0 -144
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
- package/components/ScheduleTastTicketMain.vue +0 -182
- package/components/TableWithButton.vue +0 -94
- package/test/visitor-socket.test.mjs +0 -36
|
@@ -1,203 +1,229 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
HID QR CODE CONFIGURATION - the reader's own settings, inside Site Settings.
|
|
3
|
+
|
|
4
|
+
It is mounted in `SiteSettings.vue`'s "HID Face Reader" expansion panel, next
|
|
5
|
+
to `HidServiceSettingsPanel`, so it takes the shape the handoff gives a
|
|
6
|
+
settings module BODY: section labels at 13px/700, explanatory lines at
|
|
7
|
+
12-13px/600 in `--muted`, the 38x22 toggle, 40px fields and 38px buttons.
|
|
8
|
+
|
|
9
|
+
Two pieces of legacy chrome are gone rather than restyled:
|
|
10
|
+
* the `#082e43` navy title bar. The design puts NO card inside an accordion
|
|
11
|
+
body - the accordion head ("HID Face Reader") is the card, and a second
|
|
12
|
+
titled panel inside it is a header the design does not draw. Its title is
|
|
13
|
+
kept as the body's first section label.
|
|
14
|
+
* the chevron button that sat on that bar. It had no `@click` and no model:
|
|
15
|
+
it looked like a collapse control and did nothing.
|
|
16
|
+
|
|
17
|
+
Every field, toggle, dialog, handler and save call is otherwise unchanged.
|
|
18
|
+
-->
|
|
1
19
|
<template>
|
|
2
20
|
<section class="hid-qr-config">
|
|
3
|
-
<
|
|
4
|
-
<div class="
|
|
21
|
+
<div class="config-body">
|
|
22
|
+
<div class="section-label config-body__title">HID Configuration</div>
|
|
23
|
+
|
|
24
|
+
<div class="online-mode-row">
|
|
5
25
|
<div>
|
|
6
|
-
<
|
|
26
|
+
<h3>Activate Online Mode</h3>
|
|
27
|
+
<p>Enables real-time server communication, synchronizing live credential verification and access data across the network.</p>
|
|
7
28
|
</div>
|
|
8
|
-
<v-
|
|
29
|
+
<v-switch
|
|
30
|
+
v-model="form.onlineMode"
|
|
31
|
+
class="app-switch"
|
|
32
|
+
hide-details
|
|
33
|
+
:loading="savingSection === 'online-mode'"
|
|
34
|
+
:disabled="Boolean(savingSection) || !form.readerId"
|
|
35
|
+
@update:model-value="saveOnlineMode"
|
|
36
|
+
/>
|
|
9
37
|
</div>
|
|
10
38
|
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
<v-divider />
|
|
40
|
+
|
|
41
|
+
<div class="form-section identification-section">
|
|
42
|
+
<div class="section-label">Enable/disable reader identification method</div>
|
|
43
|
+
<p class="section-description">
|
|
44
|
+
Toggles multi-factor access methods, allowing users to authenticate via biometric, physical card, or credential inputs.
|
|
45
|
+
</p>
|
|
46
|
+
<div class="methods-grid">
|
|
47
|
+
<div
|
|
48
|
+
v-for="method in identificationMethodOptions"
|
|
49
|
+
:key="method.key"
|
|
50
|
+
class="method-control"
|
|
51
|
+
:class="{ 'method-control--disabled': !form.onlineMode }"
|
|
52
|
+
>
|
|
53
|
+
<span>{{ method.label }}</span>
|
|
54
|
+
<v-switch
|
|
55
|
+
v-model="form.identificationMethods[method.key]"
|
|
56
|
+
class="app-switch"
|
|
57
|
+
hide-details
|
|
58
|
+
density="compact"
|
|
59
|
+
:loading="savingSection === `method-${method.key}`"
|
|
60
|
+
:disabled="Boolean(savingSection) || !form.onlineMode"
|
|
61
|
+
@update:model-value="saveIdentificationMethod(method.key)"
|
|
62
|
+
/>
|
|
16
63
|
</div>
|
|
17
|
-
<v-switch
|
|
18
|
-
v-model="form.enabled"
|
|
19
|
-
color="success"
|
|
20
|
-
hide-details
|
|
21
|
-
inset
|
|
22
|
-
:loading="savingSection === 'enabled'"
|
|
23
|
-
@update:model-value="saveSection('enabled')"
|
|
24
|
-
/>
|
|
25
64
|
</div>
|
|
65
|
+
</div>
|
|
26
66
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
<v-divider />
|
|
68
|
+
|
|
69
|
+
<div class="form-section">
|
|
70
|
+
<div class="section-label">Printer Setup</div>
|
|
71
|
+
<div class="printer-grid">
|
|
72
|
+
<AppField v-model="form.printer.vendorId" placeholder="Vendor" />
|
|
73
|
+
<AppField v-model="form.printer.productId" placeholder="Product Id" />
|
|
74
|
+
<AppButton variant="ghost" icon="mdi-magnify" @click="scanPrinter">
|
|
75
|
+
Scan Printer
|
|
76
|
+
</AppButton>
|
|
77
|
+
<AppButton
|
|
78
|
+
variant="ghost"
|
|
79
|
+
icon="mdi-printer-check"
|
|
80
|
+
:disabled="!hasPrinterConfig || testingPrinter"
|
|
81
|
+
@click="testPrint"
|
|
82
|
+
>
|
|
83
|
+
Test & Print
|
|
84
|
+
</AppButton>
|
|
85
|
+
<AppButton
|
|
86
|
+
icon="mdi-printer"
|
|
87
|
+
:disabled="savingSection === 'printer'"
|
|
88
|
+
@click="saveSection('printer')"
|
|
89
|
+
>
|
|
90
|
+
Save Settings
|
|
91
|
+
</AppButton>
|
|
40
92
|
</div>
|
|
93
|
+
</div>
|
|
41
94
|
|
|
42
|
-
|
|
95
|
+
<v-divider />
|
|
96
|
+
|
|
97
|
+
<div class="form-section">
|
|
98
|
+
<div class="section-label">HID QR Code Template</div>
|
|
99
|
+
<div class="template-grid">
|
|
100
|
+
<AppField v-model="form.template.header" placeholder="Header" />
|
|
101
|
+
<AppField v-model="form.template.subtext" placeholder="Subtext" />
|
|
102
|
+
<AppButton variant="ghost" icon="mdi-eye" @click="previewDialog = true">
|
|
103
|
+
Preview
|
|
104
|
+
</AppButton>
|
|
105
|
+
<AppButton
|
|
106
|
+
icon="mdi-qrcode-scan"
|
|
107
|
+
:disabled="savingSection === 'template'"
|
|
108
|
+
@click="saveSection('template')"
|
|
109
|
+
>
|
|
110
|
+
Save
|
|
111
|
+
</AppButton>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
43
114
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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')"
|
|
65
|
-
>
|
|
66
|
-
Save Format
|
|
67
|
-
</v-btn>
|
|
68
|
-
</div>
|
|
115
|
+
<v-divider />
|
|
116
|
+
|
|
117
|
+
<div class="form-section">
|
|
118
|
+
<div class="section-label">HID QR Code Validity</div>
|
|
119
|
+
<div class="validity-grid">
|
|
120
|
+
<AppField v-model="validityInput" type="number" placeholder="Validity (minutes)" />
|
|
121
|
+
<AppButton
|
|
122
|
+
icon="mdi-timer-outline"
|
|
123
|
+
:disabled="savingSection === 'validity'"
|
|
124
|
+
@click="saveSection('validity')"
|
|
125
|
+
>
|
|
126
|
+
Save
|
|
127
|
+
</AppButton>
|
|
69
128
|
</div>
|
|
129
|
+
</div>
|
|
70
130
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/>
|
|
81
|
-
<v-text-field
|
|
82
|
-
v-model="form.printer.productId"
|
|
83
|
-
placeholder="Product Id"
|
|
84
|
-
variant="outlined"
|
|
85
|
-
density="compact"
|
|
86
|
-
hide-details
|
|
87
|
-
/>
|
|
88
|
-
<v-btn
|
|
89
|
-
class="text-none utility-button"
|
|
90
|
-
variant="outlined"
|
|
91
|
-
prepend-icon="mdi-magnify"
|
|
92
|
-
height="40"
|
|
93
|
-
@click="scanPrinter"
|
|
94
|
-
>
|
|
95
|
-
Scan Printer
|
|
96
|
-
</v-btn>
|
|
97
|
-
<v-btn
|
|
98
|
-
class="text-none test-button"
|
|
99
|
-
variant="outlined"
|
|
100
|
-
prepend-icon="mdi-printer-check"
|
|
101
|
-
height="40"
|
|
102
|
-
:disabled="!hasPrinterConfig"
|
|
103
|
-
@click="testPrint"
|
|
104
|
-
>
|
|
105
|
-
Test & Print
|
|
106
|
-
</v-btn>
|
|
107
|
-
<v-btn
|
|
108
|
-
class="text-none save-button"
|
|
109
|
-
variant="flat"
|
|
110
|
-
prepend-icon="mdi-printer"
|
|
111
|
-
height="40"
|
|
112
|
-
:loading="savingSection === 'printer'"
|
|
113
|
-
@click="saveSection('printer')"
|
|
114
|
-
>
|
|
115
|
-
Save Settings
|
|
116
|
-
</v-btn>
|
|
131
|
+
<v-divider />
|
|
132
|
+
|
|
133
|
+
<div v-if="isPermissionManager" class="permission-section">
|
|
134
|
+
<div class="permission-location">
|
|
135
|
+
<div class="permission-copy">
|
|
136
|
+
<div class="section-label">Permission Location</div>
|
|
137
|
+
<p class="section-description">
|
|
138
|
+
Permissions are scoped to the location configured on the HID Face Reader.
|
|
139
|
+
</p>
|
|
117
140
|
</div>
|
|
141
|
+
<AppSelect
|
|
142
|
+
v-model="permissionReaderId"
|
|
143
|
+
class="permission-location__select"
|
|
144
|
+
:items="permissionLocations"
|
|
145
|
+
item-title="label"
|
|
146
|
+
item-value="readerId"
|
|
147
|
+
placeholder="Select a Face Reader location"
|
|
148
|
+
:disabled="permissionLocations.length === 0"
|
|
149
|
+
@update:model-value="loadPermissions"
|
|
150
|
+
/>
|
|
118
151
|
</div>
|
|
119
152
|
|
|
153
|
+
<v-alert
|
|
154
|
+
v-if="permissionLocations.length === 0"
|
|
155
|
+
class="mb-4"
|
|
156
|
+
type="warning"
|
|
157
|
+
variant="tonal"
|
|
158
|
+
density="compact"
|
|
159
|
+
>
|
|
160
|
+
Add a Location to a Face Reader before assigning HID permissions.
|
|
161
|
+
</v-alert>
|
|
162
|
+
|
|
120
163
|
<v-divider />
|
|
121
164
|
|
|
122
|
-
<div class="
|
|
123
|
-
<div class="
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
hide-details
|
|
138
|
-
/>
|
|
139
|
-
<v-btn
|
|
140
|
-
class="text-none preview-button"
|
|
141
|
-
variant="flat"
|
|
142
|
-
prepend-icon="mdi-eye"
|
|
143
|
-
height="40"
|
|
144
|
-
@click="previewDialog = true"
|
|
145
|
-
>
|
|
146
|
-
Preview
|
|
147
|
-
</v-btn>
|
|
148
|
-
<v-btn
|
|
149
|
-
class="text-none save-button"
|
|
150
|
-
variant="flat"
|
|
151
|
-
prepend-icon="mdi-qrcode-scan"
|
|
152
|
-
height="40"
|
|
153
|
-
:loading="savingSection === 'template'"
|
|
154
|
-
@click="saveSection('template')"
|
|
165
|
+
<div class="permission-row">
|
|
166
|
+
<div class="permission-copy">
|
|
167
|
+
<div class="section-label">Access Permission</div>
|
|
168
|
+
<p class="section-description">
|
|
169
|
+
Select who can use the enabled HID identification methods at this site.
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="permission-actions">
|
|
173
|
+
<AppButton
|
|
174
|
+
v-for="option in accessPermissionOptions"
|
|
175
|
+
:key="option.category"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
class="permission-button"
|
|
178
|
+
:disabled="!form.onlineMode || !permissionReaderId"
|
|
179
|
+
@click="openPermissionDialog(option.category)"
|
|
155
180
|
>
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
<span class="permission-count">{{ permissionCount(option.category) }}</span>
|
|
182
|
+
<span>{{ option.label }}</span>
|
|
183
|
+
</AppButton>
|
|
158
184
|
</div>
|
|
159
185
|
</div>
|
|
160
186
|
|
|
161
187
|
<v-divider />
|
|
162
188
|
|
|
163
|
-
<div class="
|
|
164
|
-
<div class="
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
class="text-none save-button"
|
|
177
|
-
variant="flat"
|
|
178
|
-
prepend-icon="mdi-timer-outline"
|
|
179
|
-
height="40"
|
|
180
|
-
:loading="savingSection === 'validity'"
|
|
181
|
-
@click="saveSection('validity')"
|
|
189
|
+
<div class="permission-row permission-row--intercom">
|
|
190
|
+
<div class="permission-copy">
|
|
191
|
+
<div class="section-label">Access Intercom</div>
|
|
192
|
+
<p class="section-description">
|
|
193
|
+
Choose which authorized people and providers may use HID intercom access.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="permission-actions permission-actions--single">
|
|
197
|
+
<AppButton
|
|
198
|
+
variant="ghost"
|
|
199
|
+
class="permission-button"
|
|
200
|
+
:disabled="!form.onlineMode || !permissionReaderId || permissionState.assignments.length === 0"
|
|
201
|
+
@click="openPermissionDialog('intercom')"
|
|
182
202
|
>
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
<span class="permission-count">{{ permissionState.counts.intercom }}</span>
|
|
204
|
+
<span>Intercom Permissions</span>
|
|
205
|
+
</AppButton>
|
|
185
206
|
</div>
|
|
186
207
|
</div>
|
|
187
208
|
</div>
|
|
188
|
-
</
|
|
209
|
+
</div>
|
|
189
210
|
|
|
190
211
|
<v-dialog v-model="previewDialog" max-width="420">
|
|
191
212
|
<v-card class="preview-shell">
|
|
192
|
-
<
|
|
213
|
+
<AppButton
|
|
214
|
+
variant="icon"
|
|
193
215
|
class="preview-close"
|
|
194
216
|
icon="mdi-close"
|
|
195
|
-
variant="text"
|
|
196
|
-
density="comfortable"
|
|
197
217
|
@click="previewDialog = false"
|
|
198
218
|
/>
|
|
199
219
|
<v-card-text>
|
|
200
220
|
<div class="preview-title">QR code Preview</div>
|
|
221
|
+
<!--
|
|
222
|
+
THE PRINTED PASS. Deliberately NOT themed: this is a preview of a
|
|
223
|
+
piece of card that comes out of a printer, so it is white with black
|
|
224
|
+
ink on both themes, the way the physical pass is. Everything AROUND
|
|
225
|
+
it sits on the dialog scrim, which is dark in both themes.
|
|
226
|
+
-->
|
|
201
227
|
<div class="pass-preview">
|
|
202
228
|
<div class="pass-heading">{{ form.template.header || "Header" }}</div>
|
|
203
229
|
<div class="pass-subtext">{{ form.template.subtext || "Subtext" }}</div>
|
|
@@ -222,37 +248,148 @@
|
|
|
222
248
|
</v-card>
|
|
223
249
|
</v-dialog>
|
|
224
250
|
|
|
251
|
+
<v-dialog v-model="permissionDialog" max-width="680" persistent>
|
|
252
|
+
<v-card class="permission-dialog">
|
|
253
|
+
<v-card-title class="permission-dialog__header">
|
|
254
|
+
<div>
|
|
255
|
+
<div class="permission-dialog__title">{{ permissionDialogTitle }}</div>
|
|
256
|
+
<div class="permission-dialog__subtitle">{{ permissionDialogDescription }}</div>
|
|
257
|
+
</div>
|
|
258
|
+
<AppButton
|
|
259
|
+
variant="icon"
|
|
260
|
+
icon="mdi-close"
|
|
261
|
+
aria-label="Close permission dialog"
|
|
262
|
+
@click="closePermissionDialog"
|
|
263
|
+
/>
|
|
264
|
+
</v-card-title>
|
|
265
|
+
|
|
266
|
+
<v-card-text class="permission-dialog__body">
|
|
267
|
+
<AppField
|
|
268
|
+
v-model="permissionSearch"
|
|
269
|
+
search
|
|
270
|
+
placeholder="Search by name or details"
|
|
271
|
+
/>
|
|
272
|
+
|
|
273
|
+
<div class="permission-list-summary" aria-live="polite">
|
|
274
|
+
<span>{{ visiblePermissionCandidates.length }} entries</span>
|
|
275
|
+
<span class="permission-list-summary__selected">
|
|
276
|
+
{{ selectedPermissionIds.size }} selected
|
|
277
|
+
</span>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div class="permission-list" :class="{ 'permission-list--loading': loadingPermissionCandidates }">
|
|
281
|
+
<div v-if="loadingPermissionCandidates" class="permission-empty">
|
|
282
|
+
<v-progress-circular indeterminate class="permission-spinner" size="28" />
|
|
283
|
+
<span>Loading permissions...</span>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<template v-else-if="visiblePermissionCandidates.length">
|
|
287
|
+
<label
|
|
288
|
+
v-for="candidate in visiblePermissionCandidates"
|
|
289
|
+
:key="`${candidate.category}:${candidate.subjectId}`"
|
|
290
|
+
class="permission-list-item"
|
|
291
|
+
:class="{ 'permission-list-item--selected': selectedPermissionIds.has(candidate.subjectId) }"
|
|
292
|
+
>
|
|
293
|
+
<span class="permission-list-item__content">
|
|
294
|
+
<strong>{{ candidate.name }}</strong>
|
|
295
|
+
<small>{{ candidate.subtitle || permissionCategoryLabel(candidate.category) }}</small>
|
|
296
|
+
</span>
|
|
297
|
+
<span class="permission-list-item__group">{{ permissionCategoryLabel(candidate.category) }}</span>
|
|
298
|
+
<v-checkbox-btn
|
|
299
|
+
class="permission-list-item__checkbox"
|
|
300
|
+
:model-value="selectedPermissionIds.has(candidate.subjectId)"
|
|
301
|
+
:aria-label="`Select ${candidate.name}`"
|
|
302
|
+
@update:model-value="togglePermissionCandidate(candidate.subjectId, Boolean($event))"
|
|
303
|
+
/>
|
|
304
|
+
</label>
|
|
305
|
+
</template>
|
|
306
|
+
|
|
307
|
+
<div v-else class="permission-empty">
|
|
308
|
+
<v-icon icon="mdi-account-search-outline" size="30" />
|
|
309
|
+
<span>{{ permissionEmptyMessage }}</span>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</v-card-text>
|
|
313
|
+
|
|
314
|
+
<v-card-actions class="permission-dialog__actions">
|
|
315
|
+
<AppButton variant="ghost" @click="closePermissionDialog">Cancel</AppButton>
|
|
316
|
+
<AppButton
|
|
317
|
+
:disabled="loadingPermissionCandidates || savingPermissions"
|
|
318
|
+
@click="savePermissions"
|
|
319
|
+
>
|
|
320
|
+
Save Permissions
|
|
321
|
+
</AppButton>
|
|
322
|
+
</v-card-actions>
|
|
323
|
+
</v-card>
|
|
324
|
+
</v-dialog>
|
|
325
|
+
|
|
225
326
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
226
327
|
</section>
|
|
227
328
|
</template>
|
|
228
329
|
|
|
330
|
+
|
|
229
331
|
<script setup lang="ts">
|
|
230
|
-
type
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
332
|
+
type HidIdentificationMethod = keyof NonNullable<THidQrCodePassConfig["identificationMethods"]>;
|
|
333
|
+
|
|
334
|
+
type HidQrConfig = Omit<
|
|
335
|
+
THidQrCodePassConfig,
|
|
336
|
+
"identificationMethods" | "onlineMode" | "updatedAt"
|
|
337
|
+
> & {
|
|
338
|
+
onlineMode: boolean;
|
|
339
|
+
identificationMethods: Record<HidIdentificationMethod, boolean>;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
type HidReaderOption = {
|
|
343
|
+
_id: string;
|
|
344
|
+
name?: string;
|
|
345
|
+
deviceId?: string;
|
|
346
|
+
location?: string;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
type HidReaderListResponse = {
|
|
350
|
+
items?: HidReaderOption[];
|
|
351
|
+
readers?: HidReaderOption[];
|
|
352
|
+
data?: {
|
|
353
|
+
items?: HidReaderOption[];
|
|
354
|
+
readers?: HidReaderOption[];
|
|
241
355
|
};
|
|
242
|
-
validityMinutes: number | null;
|
|
243
356
|
};
|
|
244
357
|
|
|
245
358
|
const props = defineProps<{
|
|
246
359
|
site: string;
|
|
360
|
+
org: string;
|
|
247
361
|
}>();
|
|
248
362
|
|
|
363
|
+
const runtimeConfig = useRuntimeConfig();
|
|
364
|
+
const isPermissionManager = computed(() => runtimeConfig.public.APP === "security_agency");
|
|
365
|
+
|
|
249
366
|
const { getSiteById, updateSitebyId } = useSiteSettings();
|
|
250
|
-
const {
|
|
367
|
+
const {
|
|
368
|
+
getReaders,
|
|
369
|
+
getReaderConfiguration,
|
|
370
|
+
setReaderConfiguration,
|
|
371
|
+
getSitePermissions,
|
|
372
|
+
getPermissionCandidates,
|
|
373
|
+
updateSitePermissions,
|
|
374
|
+
} = useHidAmico();
|
|
375
|
+
const {
|
|
376
|
+
requestDevice: requestPrinterDevice,
|
|
377
|
+
testConnection: testPrinterConnection,
|
|
378
|
+
} = useWebUsb();
|
|
251
379
|
|
|
252
380
|
const defaultConfig: HidQrConfig = {
|
|
253
381
|
enabled: false,
|
|
382
|
+
onlineMode: false,
|
|
254
383
|
readerId: "",
|
|
255
384
|
qrFormat: "0",
|
|
385
|
+
identificationMethods: {
|
|
386
|
+
facial: true,
|
|
387
|
+
card: true,
|
|
388
|
+
qrCode: false,
|
|
389
|
+
idPassword: true,
|
|
390
|
+
pin: false,
|
|
391
|
+
bluetooth: false,
|
|
392
|
+
},
|
|
256
393
|
printer: {
|
|
257
394
|
vendorId: "",
|
|
258
395
|
productId: "",
|
|
@@ -264,36 +401,123 @@ const defaultConfig: HidQrConfig = {
|
|
|
264
401
|
validityMinutes: null,
|
|
265
402
|
};
|
|
266
403
|
|
|
267
|
-
const siteData = ref<
|
|
268
|
-
const readers = ref<
|
|
404
|
+
const siteData = ref<TSite | null>(null);
|
|
405
|
+
const readers = ref<HidReaderOption[]>([]);
|
|
269
406
|
const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
|
|
270
407
|
const previewDialog = ref(false);
|
|
271
408
|
const savingSection = ref("");
|
|
409
|
+
const testingPrinter = ref(false);
|
|
272
410
|
const message = ref("");
|
|
273
411
|
const messageColor = ref("success");
|
|
274
412
|
const messageSnackbar = ref(false);
|
|
413
|
+
const permissionState = reactive<THidSitePermissions>({
|
|
414
|
+
orgId: props.org,
|
|
415
|
+
site: props.site,
|
|
416
|
+
readerId: "",
|
|
417
|
+
location: "",
|
|
418
|
+
assignments: [],
|
|
419
|
+
counts: {
|
|
420
|
+
resident: 0,
|
|
421
|
+
propertyManagement: 0,
|
|
422
|
+
serviceProvider: 0,
|
|
423
|
+
intercom: 0,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
const permissionDialog = ref(false);
|
|
427
|
+
const permissionReaderId = ref("");
|
|
428
|
+
const permissionDialogCategory = ref<THidPermissionCategory | "intercom">("resident");
|
|
429
|
+
const permissionCandidates = ref<THidPermissionCandidate[]>([]);
|
|
430
|
+
const selectedPermissionIds = ref(new Set<string>());
|
|
431
|
+
const permissionSearch = ref("");
|
|
432
|
+
const loadingPermissionCandidates = ref(false);
|
|
433
|
+
const savingPermissions = ref(false);
|
|
275
434
|
|
|
276
435
|
const hasPrinterConfig = computed(() =>
|
|
277
436
|
Boolean(form.printer.vendorId && form.printer.productId),
|
|
278
437
|
);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
{
|
|
286
|
-
{
|
|
438
|
+
const identificationMethodOptions: Array<{
|
|
439
|
+
key: HidIdentificationMethod;
|
|
440
|
+
label: string;
|
|
441
|
+
}> = [
|
|
442
|
+
{ key: "facial", label: "Facial" },
|
|
443
|
+
{ key: "card", label: "Card" },
|
|
444
|
+
{ key: "qrCode", label: "QR Code" },
|
|
445
|
+
{ key: "idPassword", label: "ID Password" },
|
|
446
|
+
{ key: "pin", label: "PIN" },
|
|
447
|
+
];
|
|
448
|
+
const accessPermissionOptions: Array<{
|
|
449
|
+
category: THidPermissionCategory;
|
|
450
|
+
label: string;
|
|
451
|
+
}> = [
|
|
452
|
+
{ category: "resident", label: "Resident" },
|
|
453
|
+
{ category: "property_management", label: "Property Management" },
|
|
454
|
+
{ category: "service_provider", label: "Service Provider" },
|
|
287
455
|
];
|
|
288
456
|
|
|
457
|
+
const permissionLocations = computed(() => {
|
|
458
|
+
const seen = new Set<string>();
|
|
459
|
+
return readers.value.reduce<Array<{ readerId: string; location: string; label: string }>>((items, reader) => {
|
|
460
|
+
const location = reader.location?.trim();
|
|
461
|
+
const key = location?.toLocaleLowerCase();
|
|
462
|
+
if (!location || !key || seen.has(key)) return items;
|
|
463
|
+
seen.add(key);
|
|
464
|
+
items.push({
|
|
465
|
+
readerId: reader._id,
|
|
466
|
+
location,
|
|
467
|
+
label: location,
|
|
468
|
+
});
|
|
469
|
+
return items;
|
|
470
|
+
}, []);
|
|
471
|
+
});
|
|
472
|
+
const selectedPermissionLocation = computed(() => (
|
|
473
|
+
permissionLocations.value.find((item) => item.readerId === permissionReaderId.value)?.location
|
|
474
|
+
|| "the selected location"
|
|
475
|
+
));
|
|
476
|
+
|
|
289
477
|
const siteName = computed(() => siteData.value?.name || "Site");
|
|
290
478
|
const siteLabel = computed(() => siteData.value?.code || siteData.value?.block || props.site);
|
|
291
479
|
const validityText = computed(() =>
|
|
292
480
|
form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
|
|
293
481
|
);
|
|
294
482
|
|
|
483
|
+
/**
|
|
484
|
+
* `AppField` is a native `<input>` and its model is a string, where
|
|
485
|
+
* `form.validityMinutes` is a `number | null` on the config type. This keeps
|
|
486
|
+
* the field's value exactly the number `v-model.number` used to put there -
|
|
487
|
+
* empty stays `null`, not `NaN` and not `""`.
|
|
488
|
+
*/
|
|
489
|
+
const validityInput = computed({
|
|
490
|
+
get: () => (form.validityMinutes === null || form.validityMinutes === undefined ? "" : String(form.validityMinutes)),
|
|
491
|
+
set: (value: string) => {
|
|
492
|
+
form.validityMinutes = value === "" ? null : Number(value);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
const visiblePermissionCandidates = computed(() => {
|
|
496
|
+
const search = permissionSearch.value.trim().toLowerCase();
|
|
497
|
+
if (!search) return permissionCandidates.value;
|
|
498
|
+
return permissionCandidates.value.filter((candidate) => (
|
|
499
|
+
candidate.name.toLowerCase().includes(search)
|
|
500
|
+
|| candidate.subtitle?.toLowerCase().includes(search)
|
|
501
|
+
));
|
|
502
|
+
});
|
|
503
|
+
const permissionDialogTitle = computed(() => (
|
|
504
|
+
permissionDialogCategory.value === "intercom"
|
|
505
|
+
? "Intercom Permissions"
|
|
506
|
+
: `${permissionCategoryLabel(permissionDialogCategory.value)} Access Permission`
|
|
507
|
+
));
|
|
508
|
+
const permissionDialogDescription = computed(() => (
|
|
509
|
+
permissionDialogCategory.value === "intercom"
|
|
510
|
+
? `Select from people and providers who already have HID access permission at ${selectedPermissionLocation.value}.`
|
|
511
|
+
: `Select the ${permissionCategoryLabel(permissionDialogCategory.value).toLowerCase()} entries allowed to use HID access at ${selectedPermissionLocation.value}.`
|
|
512
|
+
));
|
|
513
|
+
const permissionEmptyMessage = computed(() => (
|
|
514
|
+
permissionDialogCategory.value === "intercom" && permissionState.assignments.length === 0
|
|
515
|
+
? "Add an access permission before assigning intercom access."
|
|
516
|
+
: "No matching entries found."
|
|
517
|
+
));
|
|
518
|
+
|
|
295
519
|
watch(
|
|
296
|
-
() => props.site,
|
|
520
|
+
() => [props.org, props.site],
|
|
297
521
|
() => loadConfig(),
|
|
298
522
|
{ immediate: true },
|
|
299
523
|
);
|
|
@@ -305,13 +529,24 @@ async function loadConfig() {
|
|
|
305
529
|
getSiteById(props.site),
|
|
306
530
|
getReaders({ site: props.site, page: 1, limit: 100 }),
|
|
307
531
|
]);
|
|
308
|
-
siteData.value = site;
|
|
309
|
-
|
|
532
|
+
siteData.value = site as TSite;
|
|
533
|
+
const readerList = readerResponse as HidReaderListResponse;
|
|
534
|
+
readers.value = readerList.items ?? readerList.data?.items ?? readerList.data?.readers ?? [];
|
|
310
535
|
const config = siteData.value?.metadata?.hidQrCodePass || {};
|
|
536
|
+
const configuredMethods = config.identificationMethods;
|
|
311
537
|
Object.assign(form, {
|
|
312
538
|
enabled: Boolean(config.enabled),
|
|
539
|
+
onlineMode: Boolean(config.onlineMode),
|
|
313
540
|
readerId: String(config.readerId || readers.value[0]?._id || ""),
|
|
314
|
-
qrFormat:
|
|
541
|
+
qrFormat: "0",
|
|
542
|
+
identificationMethods: {
|
|
543
|
+
facial: configuredMethods?.facial ?? true,
|
|
544
|
+
card: configuredMethods?.card ?? true,
|
|
545
|
+
qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
|
|
546
|
+
idPassword: configuredMethods?.idPassword ?? true,
|
|
547
|
+
pin: configuredMethods?.pin ?? false,
|
|
548
|
+
bluetooth: false,
|
|
549
|
+
},
|
|
315
550
|
printer: {
|
|
316
551
|
vendorId: String(config.printer?.vendorId || ""),
|
|
317
552
|
productId: String(config.printer?.productId || ""),
|
|
@@ -322,15 +557,192 @@ async function loadConfig() {
|
|
|
322
557
|
},
|
|
323
558
|
validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
|
|
324
559
|
});
|
|
560
|
+
|
|
561
|
+
permissionReaderId.value = permissionLocations.value.find((item) => item.readerId === form.readerId)?.readerId
|
|
562
|
+
|| permissionLocations.value[0]?.readerId
|
|
563
|
+
|| "";
|
|
564
|
+
|
|
565
|
+
await Promise.all([
|
|
566
|
+
loadReaderConfiguration(),
|
|
567
|
+
isPermissionManager.value ? loadPermissions() : Promise.resolve(),
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function loadPermissions() {
|
|
572
|
+
if (!props.org || !props.site || !permissionReaderId.value) {
|
|
573
|
+
Object.assign(permissionState, {
|
|
574
|
+
orgId: props.org,
|
|
575
|
+
site: props.site,
|
|
576
|
+
readerId: "",
|
|
577
|
+
location: "",
|
|
578
|
+
assignments: [],
|
|
579
|
+
counts: { resident: 0, propertyManagement: 0, serviceProvider: 0, intercom: 0 },
|
|
580
|
+
});
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const response = await getSitePermissions(props.site, props.org, permissionReaderId.value);
|
|
585
|
+
Object.assign(permissionState, response.data);
|
|
586
|
+
} catch (error: unknown) {
|
|
587
|
+
console.error("Unable to load HID permissions:", errorConverter(error));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function permissionCategoryLabel(category: THidPermissionCategory) {
|
|
592
|
+
return accessPermissionOptions.find((option) => option.category === category)?.label || "Permission";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function permissionCount(category: THidPermissionCategory) {
|
|
596
|
+
if (category === "property_management") return permissionState.counts.propertyManagement;
|
|
597
|
+
if (category === "service_provider") return permissionState.counts.serviceProvider;
|
|
598
|
+
return permissionState.counts.resident;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function openPermissionDialog(category: THidPermissionCategory | "intercom") {
|
|
602
|
+
if (!props.org || !permissionReaderId.value) return;
|
|
603
|
+
permissionDialogCategory.value = category;
|
|
604
|
+
permissionSearch.value = "";
|
|
605
|
+
permissionCandidates.value = [];
|
|
606
|
+
selectedPermissionIds.value = new Set<string>();
|
|
607
|
+
permissionDialog.value = true;
|
|
608
|
+
loadingPermissionCandidates.value = true;
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const response = await getPermissionCandidates(props.site, {
|
|
612
|
+
orgId: props.org,
|
|
613
|
+
readerId: permissionReaderId.value,
|
|
614
|
+
category,
|
|
615
|
+
page: 1,
|
|
616
|
+
limit: 500,
|
|
617
|
+
});
|
|
618
|
+
permissionCandidates.value = response.items || [];
|
|
619
|
+
selectedPermissionIds.value = new Set(
|
|
620
|
+
permissionCandidates.value
|
|
621
|
+
.filter((candidate) => category === "intercom" ? candidate.intercom : candidate.selected)
|
|
622
|
+
.map((candidate) => candidate.subjectId),
|
|
623
|
+
);
|
|
624
|
+
} catch (error: unknown) {
|
|
625
|
+
showMessage(errorConverter(error), "error");
|
|
626
|
+
} finally {
|
|
627
|
+
loadingPermissionCandidates.value = false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function closePermissionDialog() {
|
|
632
|
+
if (savingPermissions.value) return;
|
|
633
|
+
permissionDialog.value = false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function togglePermissionCandidate(subjectId: string, selected: boolean) {
|
|
637
|
+
const next = new Set(selectedPermissionIds.value);
|
|
638
|
+
if (selected) next.add(subjectId);
|
|
639
|
+
else next.delete(subjectId);
|
|
640
|
+
selectedPermissionIds.value = next;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function savePermissions() {
|
|
644
|
+
const category = permissionDialogCategory.value;
|
|
645
|
+
let assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>;
|
|
646
|
+
|
|
647
|
+
if (category === "intercom") {
|
|
648
|
+
assignments = permissionState.assignments.map((assignment) => ({
|
|
649
|
+
subjectId: assignment.subjectId,
|
|
650
|
+
category: assignment.category,
|
|
651
|
+
intercom: selectedPermissionIds.value.has(assignment.subjectId),
|
|
652
|
+
}));
|
|
653
|
+
} else {
|
|
654
|
+
const retained = permissionState.assignments
|
|
655
|
+
.filter((assignment) => assignment.category !== category)
|
|
656
|
+
.map((assignment) => ({
|
|
657
|
+
subjectId: assignment.subjectId,
|
|
658
|
+
category: assignment.category,
|
|
659
|
+
intercom: assignment.intercom,
|
|
660
|
+
}));
|
|
661
|
+
const selected = permissionCandidates.value
|
|
662
|
+
.filter((candidate) => selectedPermissionIds.value.has(candidate.subjectId))
|
|
663
|
+
.map((candidate) => ({
|
|
664
|
+
subjectId: candidate.subjectId,
|
|
665
|
+
category: candidate.category,
|
|
666
|
+
intercom: candidate.intercom,
|
|
667
|
+
}));
|
|
668
|
+
assignments = [...retained, ...selected];
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
savingPermissions.value = true;
|
|
672
|
+
try {
|
|
673
|
+
const response = await updateSitePermissions(
|
|
674
|
+
props.site,
|
|
675
|
+
props.org,
|
|
676
|
+
permissionReaderId.value,
|
|
677
|
+
assignments,
|
|
678
|
+
);
|
|
679
|
+
Object.assign(permissionState, response.data);
|
|
680
|
+
permissionDialog.value = false;
|
|
681
|
+
showMessage("HID permissions updated.", "success");
|
|
682
|
+
} catch (error: unknown) {
|
|
683
|
+
showMessage(errorConverter(error), "error");
|
|
684
|
+
} finally {
|
|
685
|
+
savingPermissions.value = false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
690
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function getConfigurationData(response: unknown) {
|
|
694
|
+
if (!isUnknownRecord(response)) return {};
|
|
695
|
+
return isUnknownRecord(response.data) ? response.data : response;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function isEnabledConfiguration(value: unknown) {
|
|
699
|
+
return value === true || value === 1 || value === "1";
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function loadReaderConfiguration() {
|
|
703
|
+
if (!form.readerId) return;
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
const response = await getReaderConfiguration(form.readerId, {
|
|
707
|
+
general: ["online"],
|
|
708
|
+
identifier: [
|
|
709
|
+
"card_identification_enabled",
|
|
710
|
+
"pin_identification_enabled",
|
|
711
|
+
"multi_factor_authentication",
|
|
712
|
+
],
|
|
713
|
+
});
|
|
714
|
+
const configuration = getConfigurationData(response);
|
|
715
|
+
const general = isUnknownRecord(configuration.general) ? configuration.general : {};
|
|
716
|
+
const identifier = isUnknownRecord(configuration.identifier) ? configuration.identifier : {};
|
|
717
|
+
|
|
718
|
+
if (general.online !== undefined) {
|
|
719
|
+
form.onlineMode = isEnabledConfiguration(general.online);
|
|
720
|
+
}
|
|
721
|
+
if (identifier.pin_identification_enabled !== undefined) {
|
|
722
|
+
const pinEnabled = isEnabledConfiguration(identifier.pin_identification_enabled);
|
|
723
|
+
form.identificationMethods.pin = pinEnabled;
|
|
724
|
+
form.identificationMethods.idPassword = !pinEnabled;
|
|
725
|
+
}
|
|
726
|
+
if (identifier.card_identification_enabled !== undefined) {
|
|
727
|
+
form.identificationMethods.card = isEnabledConfiguration(
|
|
728
|
+
identifier.card_identification_enabled,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
} catch (error: unknown) {
|
|
732
|
+
console.warn("Unable to load HID reader configuration:", errorConverter(error));
|
|
733
|
+
}
|
|
325
734
|
}
|
|
326
735
|
|
|
327
736
|
function buildPayload() {
|
|
328
737
|
const metadata = {
|
|
329
738
|
...(siteData.value?.metadata || {}),
|
|
330
739
|
hidQrCodePass: {
|
|
331
|
-
enabled: form.
|
|
740
|
+
enabled: form.identificationMethods.qrCode,
|
|
741
|
+
onlineMode: form.onlineMode,
|
|
332
742
|
readerId: form.readerId,
|
|
333
|
-
|
|
743
|
+
// Visitor QR passes always use Amico alphanumeric qrcodes.
|
|
744
|
+
qrFormat: "0",
|
|
745
|
+
identificationMethods: { ...form.identificationMethods },
|
|
334
746
|
printer: {
|
|
335
747
|
vendorId: form.printer.vendorId.trim(),
|
|
336
748
|
productId: form.printer.productId.trim(),
|
|
@@ -352,17 +764,65 @@ async function saveSection(section: string) {
|
|
|
352
764
|
|
|
353
765
|
savingSection.value = section;
|
|
354
766
|
try {
|
|
355
|
-
|
|
767
|
+
await updateSitebyId(props.site, buildPayload());
|
|
768
|
+
await loadConfig();
|
|
769
|
+
showMessage("HID configuration saved.", "success");
|
|
770
|
+
} catch (error: unknown) {
|
|
771
|
+
showMessage(errorConverter(error), "error");
|
|
772
|
+
} finally {
|
|
773
|
+
savingSection.value = "";
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function saveOnlineMode() {
|
|
778
|
+
if (!props.site || !form.readerId) return;
|
|
779
|
+
|
|
780
|
+
savingSection.value = "online-mode";
|
|
781
|
+
try {
|
|
782
|
+
await setReaderConfiguration(form.readerId, {
|
|
783
|
+
general: { online: form.onlineMode ? "1" : "0" },
|
|
784
|
+
});
|
|
785
|
+
await updateSitebyId(props.site, buildPayload());
|
|
786
|
+
showMessage("Online mode updated.", "success");
|
|
787
|
+
} catch (error: unknown) {
|
|
788
|
+
await loadConfig();
|
|
789
|
+
showMessage(errorConverter(error), "error");
|
|
790
|
+
} finally {
|
|
791
|
+
savingSection.value = "";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function saveIdentificationMethod(method: HidIdentificationMethod) {
|
|
796
|
+
if (!props.site) return;
|
|
797
|
+
|
|
798
|
+
form.enabled = form.identificationMethods.qrCode;
|
|
799
|
+
|
|
800
|
+
savingSection.value = `method-${method}`;
|
|
801
|
+
try {
|
|
802
|
+
if (method === "card") {
|
|
356
803
|
await setReaderConfiguration(form.readerId, {
|
|
357
|
-
|
|
358
|
-
|
|
804
|
+
identifier: {
|
|
805
|
+
card_identification_enabled: form.identificationMethods.card ? "1" : "0",
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
} else if (method === "pin" || method === "idPassword") {
|
|
809
|
+
if (method === "pin" && form.identificationMethods.pin) {
|
|
810
|
+
form.identificationMethods.idPassword = false;
|
|
811
|
+
} else if (method === "idPassword" && form.identificationMethods.idPassword) {
|
|
812
|
+
form.identificationMethods.pin = false;
|
|
813
|
+
}
|
|
814
|
+
await setReaderConfiguration(form.readerId, {
|
|
815
|
+
identifier: {
|
|
816
|
+
pin_identification_enabled: form.identificationMethods.pin ? "1" : "0",
|
|
817
|
+
// Mode 1: PIN is an alternative credential, never a second factor.
|
|
818
|
+
multi_factor_authentication: "0",
|
|
359
819
|
},
|
|
360
820
|
});
|
|
361
821
|
}
|
|
362
822
|
await updateSitebyId(props.site, buildPayload());
|
|
823
|
+
showMessage(`${identificationMethodOptions.find((item) => item.key === method)?.label || "Identification method"} updated.`, "success");
|
|
824
|
+
} catch (error: unknown) {
|
|
363
825
|
await loadConfig();
|
|
364
|
-
showMessage("HID QR code settings saved.", "success");
|
|
365
|
-
} catch (error: any) {
|
|
366
826
|
showMessage(errorConverter(error), "error");
|
|
367
827
|
} finally {
|
|
368
828
|
savingSection.value = "";
|
|
@@ -376,19 +836,36 @@ async function scanPrinter() {
|
|
|
376
836
|
}
|
|
377
837
|
|
|
378
838
|
try {
|
|
379
|
-
const device = await (
|
|
839
|
+
const device = await requestPrinterDevice();
|
|
380
840
|
form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
|
|
381
841
|
form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
|
|
382
842
|
showMessage("Printer selected. Save settings to keep it.", "success");
|
|
383
|
-
} catch (error:
|
|
384
|
-
|
|
385
|
-
|
|
843
|
+
} catch (error: unknown) {
|
|
844
|
+
const message = errorConverter(error);
|
|
845
|
+
if (message !== "No device selected") {
|
|
846
|
+
showMessage(message || "Unable to scan printer.", "error");
|
|
386
847
|
}
|
|
387
848
|
}
|
|
388
849
|
}
|
|
389
850
|
|
|
390
|
-
function testPrint() {
|
|
391
|
-
|
|
851
|
+
async function testPrint() {
|
|
852
|
+
const vendorId = Number(form.printer.vendorId);
|
|
853
|
+
const productId = Number(form.printer.productId);
|
|
854
|
+
if (!vendorId || !productId) {
|
|
855
|
+
showMessage("Please scan a valid USB printer first.", "error");
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
testingPrinter.value = true;
|
|
860
|
+
try {
|
|
861
|
+
const result = await testPrinterConnection(vendorId, productId);
|
|
862
|
+
showMessage(
|
|
863
|
+
result.success ? result.message : result.error || result.message,
|
|
864
|
+
result.success ? "success" : "error",
|
|
865
|
+
);
|
|
866
|
+
} finally {
|
|
867
|
+
testingPrinter.value = false;
|
|
868
|
+
}
|
|
392
869
|
}
|
|
393
870
|
|
|
394
871
|
function showMessage(text: string, color: string) {
|
|
@@ -398,71 +875,121 @@ function showMessage(text: string, color: string) {
|
|
|
398
875
|
}
|
|
399
876
|
</script>
|
|
400
877
|
|
|
878
|
+
|
|
401
879
|
<style scoped>
|
|
880
|
+
/*
|
|
881
|
+
EVERY COLOUR IS A TOKEN NOW.
|
|
882
|
+
|
|
883
|
+
This file carried 38 distinct literal hexes - a navy `#082e43` bar, greens
|
|
884
|
+
`#2eaf55` / `#35b84b` / `#34b84a` / `#2da643`, greys `#233545` / `#677280` /
|
|
885
|
+
`#334454` / `#8b9297` / `#dce2e7` / `#e6eaee` and a dozen more. A literal is
|
|
886
|
+
the same colour on both themes, which is why this screen had no dark theme at
|
|
887
|
+
all. The LAYOUT (the grids, the breakpoints, the 42px method boxes) is the
|
|
888
|
+
screen's own and is kept; only the palette moved.
|
|
889
|
+
|
|
890
|
+
The one deliberate exception is `.pass-preview` - see the template comment.
|
|
891
|
+
*/
|
|
402
892
|
.hid-qr-config {
|
|
403
893
|
width: 100%;
|
|
404
894
|
min-width: 0;
|
|
405
895
|
padding: 8px 0 28px;
|
|
406
896
|
}
|
|
407
897
|
|
|
408
|
-
.config-
|
|
409
|
-
|
|
410
|
-
border-radius: 6px;
|
|
411
|
-
overflow: hidden;
|
|
412
|
-
background: #ffffff;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.panel-header {
|
|
416
|
-
display: flex;
|
|
417
|
-
align-items: center;
|
|
418
|
-
justify-content: space-between;
|
|
419
|
-
min-height: 48px;
|
|
420
|
-
padding: 0 18px;
|
|
421
|
-
background: #082e43;
|
|
422
|
-
color: #ffffff;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.panel-header h2 {
|
|
426
|
-
margin: 0;
|
|
427
|
-
font-size: 13px;
|
|
428
|
-
font-weight: 700;
|
|
898
|
+
.config-body {
|
|
899
|
+
padding: 4px 0 0;
|
|
429
900
|
}
|
|
430
901
|
|
|
431
|
-
.config-
|
|
432
|
-
|
|
902
|
+
.config-body__title {
|
|
903
|
+
margin-bottom: 14px;
|
|
433
904
|
}
|
|
434
905
|
|
|
435
|
-
.
|
|
906
|
+
.online-mode-row {
|
|
436
907
|
display: flex;
|
|
437
|
-
align-items:
|
|
908
|
+
align-items: center;
|
|
438
909
|
justify-content: space-between;
|
|
439
910
|
gap: 24px;
|
|
440
|
-
|
|
911
|
+
padding: 0 0 20px;
|
|
441
912
|
}
|
|
442
913
|
|
|
443
|
-
|
|
914
|
+
/* The prototype's settings body: a 13px/700 label over a 12-13px/600 muted
|
|
915
|
+
line. */
|
|
916
|
+
.online-mode-row h3,
|
|
444
917
|
.section-label {
|
|
445
918
|
margin: 0 0 6px;
|
|
446
|
-
color:
|
|
919
|
+
color: var(--text);
|
|
920
|
+
font-family: var(--font-sans);
|
|
447
921
|
font-size: 13px;
|
|
448
922
|
font-weight: 700;
|
|
449
923
|
}
|
|
450
924
|
|
|
451
|
-
.
|
|
925
|
+
.online-mode-row p,
|
|
926
|
+
.section-description {
|
|
452
927
|
margin: 0;
|
|
453
|
-
color:
|
|
928
|
+
color: var(--muted);
|
|
929
|
+
font-family: var(--font-sans);
|
|
454
930
|
font-size: 12px;
|
|
931
|
+
font-weight: 600;
|
|
932
|
+
line-height: 1.5;
|
|
455
933
|
}
|
|
456
934
|
|
|
457
935
|
.form-section {
|
|
458
936
|
padding: 16px 0;
|
|
459
937
|
}
|
|
460
938
|
|
|
939
|
+
.identification-section {
|
|
940
|
+
padding-bottom: 20px;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.methods-grid {
|
|
944
|
+
display: grid;
|
|
945
|
+
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
|
946
|
+
gap: 18px;
|
|
947
|
+
margin-top: 16px;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.method-control {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: space-between;
|
|
954
|
+
min-width: 0;
|
|
955
|
+
min-height: 42px;
|
|
956
|
+
padding: 0 10px 0 14px;
|
|
957
|
+
border: 1px solid var(--border);
|
|
958
|
+
border-radius: var(--r-inner);
|
|
959
|
+
color: var(--text2);
|
|
960
|
+
font-family: var(--font-sans);
|
|
961
|
+
font-size: 12px;
|
|
962
|
+
font-weight: 700;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.method-control span {
|
|
966
|
+
overflow: hidden;
|
|
967
|
+
text-overflow: ellipsis;
|
|
968
|
+
white-space: nowrap;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.method-control--disabled {
|
|
972
|
+
background-color: var(--hover);
|
|
973
|
+
color: var(--muted);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.method-control :deep(.v-switch) {
|
|
977
|
+
flex: 0 0 auto;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.method-control :deep(.v-selection-control) {
|
|
981
|
+
min-height: 32px;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/* The three `.hid-config-switch` rules are gone: `.app-switch` in
|
|
985
|
+
primitives.css is the handoff's toggle, and it is shared with the other two
|
|
986
|
+
HID panels that have one. */
|
|
987
|
+
|
|
461
988
|
.printer-grid {
|
|
462
989
|
display: grid;
|
|
463
990
|
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
|
|
464
991
|
gap: 14px;
|
|
465
|
-
align-items:
|
|
992
|
+
align-items: end;
|
|
466
993
|
}
|
|
467
994
|
|
|
468
995
|
.template-grid {
|
|
@@ -471,37 +998,249 @@ function showMessage(text: string, color: string) {
|
|
|
471
998
|
gap: 14px;
|
|
472
999
|
}
|
|
473
1000
|
|
|
474
|
-
.template-grid .preview-button,
|
|
475
|
-
.template-grid .save-button {
|
|
476
|
-
width: 100%;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
1001
|
.validity-grid {
|
|
480
1002
|
display: grid;
|
|
481
1003
|
grid-template-columns: minmax(180px, 260px) minmax(140px, 260px);
|
|
482
1004
|
gap: 14px;
|
|
1005
|
+
align-items: end;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.permission-section {
|
|
1009
|
+
padding-top: 2px;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.permission-location {
|
|
1013
|
+
display: grid;
|
|
1014
|
+
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1.35fr);
|
|
1015
|
+
gap: 36px;
|
|
483
1016
|
align-items: center;
|
|
1017
|
+
padding: 18px 0;
|
|
484
1018
|
}
|
|
485
1019
|
|
|
486
|
-
.
|
|
487
|
-
|
|
488
|
-
color: #ffffff;
|
|
1020
|
+
.permission-location__select {
|
|
1021
|
+
width: 100%;
|
|
489
1022
|
}
|
|
490
1023
|
|
|
491
|
-
.
|
|
492
|
-
|
|
493
|
-
|
|
1024
|
+
.permission-row {
|
|
1025
|
+
display: grid;
|
|
1026
|
+
grid-template-columns: minmax(260px, 1fr) minmax(520px, 1.35fr);
|
|
1027
|
+
gap: 36px;
|
|
1028
|
+
align-items: center;
|
|
1029
|
+
padding: 18px 0;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.permission-row--intercom {
|
|
1033
|
+
padding-bottom: 4px;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.permission-copy {
|
|
1037
|
+
min-width: 0;
|
|
494
1038
|
}
|
|
495
1039
|
|
|
496
|
-
.
|
|
497
|
-
|
|
498
|
-
|
|
1040
|
+
.permission-actions {
|
|
1041
|
+
display: grid;
|
|
1042
|
+
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
|
1043
|
+
gap: 12px;
|
|
499
1044
|
}
|
|
500
1045
|
|
|
501
|
-
.
|
|
502
|
-
|
|
1046
|
+
.permission-actions--single {
|
|
1047
|
+
grid-template-columns: minmax(180px, 240px);
|
|
1048
|
+
justify-content: end;
|
|
503
1049
|
}
|
|
504
1050
|
|
|
1051
|
+
/* A ghost button carrying a count badge. `AppButton --ghost` supplies the
|
|
1052
|
+
height, radius, border and type; only the alignment is local. */
|
|
1053
|
+
.permission-button {
|
|
1054
|
+
justify-content: flex-start;
|
|
1055
|
+
min-height: 42px;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.permission-count {
|
|
1059
|
+
display: inline-grid;
|
|
1060
|
+
place-items: center;
|
|
1061
|
+
width: 20px;
|
|
1062
|
+
height: 20px;
|
|
1063
|
+
margin-right: 8px;
|
|
1064
|
+
border-radius: 50%;
|
|
1065
|
+
background: var(--accent-soft);
|
|
1066
|
+
color: var(--accent-text);
|
|
1067
|
+
font-size: 11px;
|
|
1068
|
+
font-weight: 700;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.permission-button:not(:disabled):hover {
|
|
1072
|
+
border-color: var(--accent);
|
|
1073
|
+
color: var(--accent-text);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.permission-dialog {
|
|
1077
|
+
overflow: hidden;
|
|
1078
|
+
border-radius: var(--r-modal);
|
|
1079
|
+
background: var(--card);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.permission-dialog__header {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
align-items: flex-start;
|
|
1085
|
+
justify-content: space-between;
|
|
1086
|
+
gap: 16px;
|
|
1087
|
+
padding: 16px 18px 14px;
|
|
1088
|
+
border-bottom: 1px solid var(--border);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.permission-dialog__title {
|
|
1092
|
+
color: var(--text);
|
|
1093
|
+
font-family: var(--font-sans);
|
|
1094
|
+
font-size: 18px;
|
|
1095
|
+
font-weight: 800;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.permission-dialog__subtitle {
|
|
1099
|
+
margin-top: 4px;
|
|
1100
|
+
color: var(--muted);
|
|
1101
|
+
font-size: 12px;
|
|
1102
|
+
font-weight: 600;
|
|
1103
|
+
white-space: normal;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.permission-dialog__body {
|
|
1107
|
+
display: grid;
|
|
1108
|
+
gap: 10px;
|
|
1109
|
+
padding: 14px 18px 16px;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.permission-list-summary {
|
|
1113
|
+
display: flex;
|
|
1114
|
+
align-items: center;
|
|
1115
|
+
justify-content: space-between;
|
|
1116
|
+
min-height: 22px;
|
|
1117
|
+
color: var(--muted);
|
|
1118
|
+
font-size: 11px;
|
|
1119
|
+
font-weight: 600;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.permission-list-summary__selected {
|
|
1123
|
+
color: var(--ok);
|
|
1124
|
+
font-weight: 700;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.permission-list {
|
|
1128
|
+
min-height: 180px;
|
|
1129
|
+
max-height: min(360px, 48vh);
|
|
1130
|
+
overflow-y: auto;
|
|
1131
|
+
border: 1px solid var(--border);
|
|
1132
|
+
border-radius: var(--r-inner);
|
|
1133
|
+
background: var(--card);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.permission-list--loading {
|
|
1137
|
+
display: grid;
|
|
1138
|
+
place-items: center;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.permission-list-item {
|
|
1142
|
+
display: grid;
|
|
1143
|
+
grid-template-columns: minmax(0, 1fr) max-content 32px;
|
|
1144
|
+
gap: 12px;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
min-height: 56px;
|
|
1147
|
+
padding: 7px 12px;
|
|
1148
|
+
border-bottom: 1px solid var(--border);
|
|
1149
|
+
cursor: pointer;
|
|
1150
|
+
transition: background-color var(--motion) ease;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.permission-list-item:last-child {
|
|
1154
|
+
border-bottom: 0;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.permission-list-item:hover {
|
|
1158
|
+
background: var(--hover);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.permission-list-item--selected,
|
|
1162
|
+
.permission-list-item--selected:hover {
|
|
1163
|
+
background: var(--accent-soft);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.permission-list-item__content {
|
|
1167
|
+
display: grid;
|
|
1168
|
+
gap: 2px;
|
|
1169
|
+
min-width: 0;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.permission-list-item__content strong,
|
|
1173
|
+
.permission-list-item__content small {
|
|
1174
|
+
overflow: hidden;
|
|
1175
|
+
text-overflow: ellipsis;
|
|
1176
|
+
white-space: nowrap;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.permission-list-item__content strong {
|
|
1180
|
+
color: var(--text);
|
|
1181
|
+
font-size: 13px;
|
|
1182
|
+
font-weight: 700;
|
|
1183
|
+
line-height: 1.35;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.permission-list-item__content small,
|
|
1187
|
+
.permission-list-item__group {
|
|
1188
|
+
color: var(--muted);
|
|
1189
|
+
font-size: 11px;
|
|
1190
|
+
font-weight: 600;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.permission-list-item__group {
|
|
1194
|
+
max-width: 170px;
|
|
1195
|
+
overflow: hidden;
|
|
1196
|
+
padding: 4px 9px;
|
|
1197
|
+
border-radius: var(--r-tag);
|
|
1198
|
+
background: var(--hover);
|
|
1199
|
+
text-overflow: ellipsis;
|
|
1200
|
+
white-space: nowrap;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.permission-list-item__checkbox {
|
|
1204
|
+
justify-self: end;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.permission-list-item__checkbox :deep(.v-selection-control) {
|
|
1208
|
+
min-height: 32px;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
.permission-empty {
|
|
1212
|
+
display: flex;
|
|
1213
|
+
flex-direction: column;
|
|
1214
|
+
align-items: center;
|
|
1215
|
+
justify-content: center;
|
|
1216
|
+
gap: 10px;
|
|
1217
|
+
min-height: 180px;
|
|
1218
|
+
padding: 24px;
|
|
1219
|
+
color: var(--muted);
|
|
1220
|
+
font-size: 13px;
|
|
1221
|
+
text-align: center;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.permission-spinner {
|
|
1225
|
+
color: var(--accent);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.permission-dialog__actions {
|
|
1229
|
+
justify-content: flex-end;
|
|
1230
|
+
gap: 8px;
|
|
1231
|
+
padding: 12px 18px;
|
|
1232
|
+
border-top: 1px solid var(--border);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/*
|
|
1236
|
+
THE PRINTED PASS - the one thing here that is NOT themed.
|
|
1237
|
+
|
|
1238
|
+
It previews a physical card that comes out of a badge printer, so it stays
|
|
1239
|
+
white with dark ink on both themes; theming it would show the operator a
|
|
1240
|
+
preview of something the printer cannot produce. Everything around it sits on
|
|
1241
|
+
the dialog scrim, which is dark on both themes - which is why the title above
|
|
1242
|
+
it is white.
|
|
1243
|
+
*/
|
|
505
1244
|
.preview-shell {
|
|
506
1245
|
position: relative;
|
|
507
1246
|
overflow: visible;
|
|
@@ -514,13 +1253,15 @@ function showMessage(text: string, color: string) {
|
|
|
514
1253
|
top: 38px;
|
|
515
1254
|
right: 18px;
|
|
516
1255
|
z-index: 2;
|
|
1256
|
+
color: #ffffff;
|
|
517
1257
|
}
|
|
518
1258
|
|
|
519
1259
|
.preview-title {
|
|
520
1260
|
margin: 0 0 12px;
|
|
521
1261
|
color: #ffffff;
|
|
1262
|
+
font-family: var(--font-sans);
|
|
522
1263
|
font-size: 18px;
|
|
523
|
-
font-weight:
|
|
1264
|
+
font-weight: 800;
|
|
524
1265
|
}
|
|
525
1266
|
|
|
526
1267
|
.pass-preview {
|
|
@@ -592,27 +1333,58 @@ function showMessage(text: string, color: string) {
|
|
|
592
1333
|
}
|
|
593
1334
|
|
|
594
1335
|
@media (max-width: 1100px) {
|
|
1336
|
+
.methods-grid {
|
|
1337
|
+
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
595
1340
|
.printer-grid,
|
|
596
1341
|
.template-grid,
|
|
597
|
-
.validity-grid
|
|
1342
|
+
.validity-grid,
|
|
1343
|
+
.permission-location,
|
|
1344
|
+
.permission-row {
|
|
598
1345
|
grid-template-columns: 1fr 1fr;
|
|
599
1346
|
}
|
|
600
|
-
}
|
|
601
1347
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
padding: 22px 16px;
|
|
1348
|
+
.permission-actions {
|
|
1349
|
+
grid-template-columns: 1fr;
|
|
605
1350
|
}
|
|
1351
|
+
}
|
|
606
1352
|
|
|
607
|
-
|
|
1353
|
+
@media (max-width: 700px) {
|
|
1354
|
+
.online-mode-row,
|
|
608
1355
|
.printer-grid,
|
|
609
1356
|
.template-grid,
|
|
610
|
-
.validity-grid
|
|
1357
|
+
.validity-grid,
|
|
1358
|
+
.methods-grid,
|
|
1359
|
+
.permission-location,
|
|
1360
|
+
.permission-row,
|
|
1361
|
+
.permission-actions {
|
|
611
1362
|
grid-template-columns: 1fr;
|
|
612
1363
|
}
|
|
613
1364
|
|
|
614
|
-
.
|
|
1365
|
+
.online-mode-row {
|
|
615
1366
|
display: grid;
|
|
616
1367
|
}
|
|
1368
|
+
|
|
1369
|
+
.permission-actions--single {
|
|
1370
|
+
justify-content: stretch;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
.permission-list-item {
|
|
1374
|
+
grid-template-columns: minmax(0, 1fr) 32px;
|
|
1375
|
+
gap: 8px;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.permission-list-item__group {
|
|
1379
|
+
grid-column: 1;
|
|
1380
|
+
grid-row: 2;
|
|
1381
|
+
justify-self: start;
|
|
1382
|
+
max-width: 100%;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
.permission-list-item__checkbox {
|
|
1386
|
+
grid-column: 2;
|
|
1387
|
+
grid-row: 1 / span 2;
|
|
1388
|
+
}
|
|
617
1389
|
}
|
|
618
1390
|
</style>
|