@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,40 +1,26 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
HID INTERCOM MANAGEMENT - the readers' door intercoms and their contact list.
|
|
3
|
+
|
|
4
|
+
The largest of the six panels, and it was a private copy of the standard
|
|
5
|
+
table: an above-card toolbar, a `v-tabs` strip, a raw `v-table` with TWO
|
|
6
|
+
column sets, a refresh strip with a pager, a SECOND pager in the footer, an
|
|
7
|
+
empty state and its own chip palette - in about a hundred and forty literal
|
|
8
|
+
hexes. Those literals are why it had no dark theme: `#1f2937` on the dark
|
|
9
|
+
card measures 1.13:1, `white on #a7a7a7` 2.41:1 and the "Intercom" tab label
|
|
10
|
+
2.54:1.
|
|
11
|
+
|
|
12
|
+
It is the design's standard table screen with tabs, so it is drawn with the
|
|
13
|
+
shared one: `TableMain` supplies the heading, the card, the in-card toolbar,
|
|
14
|
+
the `app-tab` strip, the header row, the rows and ONE pager. The two column
|
|
15
|
+
sets stay two column sets - `tableHeaders` and `visibleRows` switch on the
|
|
16
|
+
active tab exactly as the two `<thead>` blocks did.
|
|
17
|
+
|
|
18
|
+
Every dialog, handler, SIP call and web-phone control is unchanged. Only the
|
|
19
|
+
chrome and the colours move; no call, connect, hangup or device path is
|
|
20
|
+
touched.
|
|
21
|
+
-->
|
|
1
22
|
<template>
|
|
2
23
|
<section class="hid-intercom-management">
|
|
3
|
-
<div class="top-bar">
|
|
4
|
-
<v-btn
|
|
5
|
-
v-if="activeTab === 'contacts'"
|
|
6
|
-
rounded="xl"
|
|
7
|
-
class="text-none add-button"
|
|
8
|
-
variant="flat"
|
|
9
|
-
prepend-icon="mdi-plus"
|
|
10
|
-
@click="openContactDialog()"
|
|
11
|
-
>
|
|
12
|
-
Add Contacts
|
|
13
|
-
</v-btn>
|
|
14
|
-
|
|
15
|
-
<div v-else class="top-placeholder" />
|
|
16
|
-
|
|
17
|
-
<div class="toolbar-spacer" />
|
|
18
|
-
|
|
19
|
-
<v-text-field
|
|
20
|
-
v-model="search"
|
|
21
|
-
placeholder="Search"
|
|
22
|
-
variant="outlined"
|
|
23
|
-
density="compact"
|
|
24
|
-
hide-details
|
|
25
|
-
class="search-input"
|
|
26
|
-
/>
|
|
27
|
-
|
|
28
|
-
<v-select
|
|
29
|
-
v-model="statusFilter"
|
|
30
|
-
:items="statusOptions"
|
|
31
|
-
variant="outlined"
|
|
32
|
-
density="compact"
|
|
33
|
-
hide-details
|
|
34
|
-
class="status-input"
|
|
35
|
-
/>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
24
|
<v-alert
|
|
39
25
|
v-if="message.text"
|
|
40
26
|
:type="message.type"
|
|
@@ -45,108 +31,153 @@
|
|
|
45
31
|
{{ message.text }}
|
|
46
32
|
</v-alert>
|
|
47
33
|
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<td>{{ contact.location }}</td>
|
|
118
|
-
<td>
|
|
119
|
-
<span :class="{ 'link-text': contact.serverAddress !== 'N/A' }">{{ contact.serverAddress }}</span>
|
|
120
|
-
</td>
|
|
121
|
-
<td>{{ contact.dialCode }}</td>
|
|
122
|
-
<td class="text-right">
|
|
123
|
-
<v-menu location="bottom end">
|
|
124
|
-
<template #activator="{ props: menuProps }">
|
|
125
|
-
<v-btn v-bind="menuProps" icon="mdi-dots-vertical" variant="text" density="comfortable" />
|
|
126
|
-
</template>
|
|
127
|
-
<v-list density="compact" class="row-menu">
|
|
128
|
-
<v-list-item title="View" @click="openContactView(contact)" />
|
|
129
|
-
<v-list-item title="Edit" @click="openContactDialog(contact)" />
|
|
130
|
-
<v-list-item title="Delete" @click="openDeleteContact(contact)" />
|
|
131
|
-
</v-list>
|
|
132
|
-
</v-menu>
|
|
133
|
-
</td>
|
|
134
|
-
</tr>
|
|
135
|
-
</template>
|
|
136
|
-
</tbody>
|
|
137
|
-
</v-table>
|
|
138
|
-
|
|
139
|
-
<div v-if="!visibleRows.length" class="empty-state">
|
|
140
|
-
No intercom records found.
|
|
34
|
+
<TableMain
|
|
35
|
+
title="Intercom"
|
|
36
|
+
:headers="tableHeaders"
|
|
37
|
+
:items="visiblePageRows"
|
|
38
|
+
:loading="loading"
|
|
39
|
+
:items-per-page="-1"
|
|
40
|
+
:page="page"
|
|
41
|
+
:pages="pages"
|
|
42
|
+
:page-range="pageRange"
|
|
43
|
+
item-value="_rowKey"
|
|
44
|
+
show-header
|
|
45
|
+
@refresh="reload"
|
|
46
|
+
@update:page="page = $event"
|
|
47
|
+
>
|
|
48
|
+
<!-- The action swaps with the tab, as it did in the old top bar. -->
|
|
49
|
+
<template #actions>
|
|
50
|
+
<AppButton
|
|
51
|
+
v-if="activeTab === 'contacts'"
|
|
52
|
+
icon="mdi-plus"
|
|
53
|
+
@click="openContactDialog()"
|
|
54
|
+
>
|
|
55
|
+
Add Contacts
|
|
56
|
+
</AppButton>
|
|
57
|
+
<AppButton
|
|
58
|
+
v-else
|
|
59
|
+
icon="mdi-phone-in-talk-outline"
|
|
60
|
+
class="web-call-button"
|
|
61
|
+
:class="webPhoneButtonClass"
|
|
62
|
+
@click="openWebPhoneDialog()"
|
|
63
|
+
>
|
|
64
|
+
{{ webPhone.callState.value === 'incoming' ? 'Incoming Call' : 'Web Call' }}
|
|
65
|
+
</AppButton>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<!-- Was `v-tabs`/`v-tab`, whose selected state is drawn from Vuetify's
|
|
69
|
+
`on-surface`. `.app-tab` is the shared strip. -->
|
|
70
|
+
<template #tabs>
|
|
71
|
+
<button
|
|
72
|
+
v-for="tab in tabOptions"
|
|
73
|
+
:key="tab.value"
|
|
74
|
+
type="button"
|
|
75
|
+
role="tab"
|
|
76
|
+
class="app-tab"
|
|
77
|
+
:class="{ 'app-tab--active': activeTab === tab.value }"
|
|
78
|
+
:aria-selected="activeTab === tab.value"
|
|
79
|
+
@click="selectTab(tab.value)"
|
|
80
|
+
>
|
|
81
|
+
{{ tab.label }}
|
|
82
|
+
</button>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<template #extension>
|
|
86
|
+
<div class="app-filter-row hid-intercom-filters">
|
|
87
|
+
<!-- Staging (1e6224e) added a reader picker here and filters both
|
|
88
|
+
tabs by it; it keeps that job, drawn as the shared select. -->
|
|
89
|
+
<AppSelect
|
|
90
|
+
v-if="readers.length"
|
|
91
|
+
v-model="selectedReaderId"
|
|
92
|
+
class="reader-select"
|
|
93
|
+
:items="readerOptions"
|
|
94
|
+
@update:model-value="page = 1"
|
|
95
|
+
/>
|
|
96
|
+
<AppField v-model="search" search placeholder="Search" />
|
|
97
|
+
<AppSelect
|
|
98
|
+
v-model="statusFilter"
|
|
99
|
+
:items="statusOptions"
|
|
100
|
+
placeholder="Status"
|
|
101
|
+
clearable
|
|
102
|
+
/>
|
|
141
103
|
</div>
|
|
142
|
-
</
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
<span>{{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<template #[`item.name`]="{ item }">
|
|
107
|
+
<span class="app-cell--strong">{{ item.name }}</span>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<template #[`item.location`]="{ item }">
|
|
111
|
+
<span class="app-cell--muted">{{ item.location }}</span>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
<template #[`item.serverAddress`]="{ item }">
|
|
115
|
+
<span :class="item.serverAddress !== 'N/A' ? 'link-text' : 'app-cell--muted'">
|
|
116
|
+
{{ item.serverAddress }}
|
|
117
|
+
</span>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template #[`item.operatingMode`]="{ item }">
|
|
121
|
+
<span class="app-cell--muted">{{ item.operatingMode }}</span>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<template #[`item.dialCode`]="{ item }">
|
|
125
|
+
<span class="app-cell--num">{{ item.dialCode }}</span>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<!-- The shared chip. This screen's `getStatusClass` was another private
|
|
129
|
+
copy of the status-to-colour map. -->
|
|
130
|
+
<template #[`item.status`]="{ item }">
|
|
131
|
+
<StatusChip :status="item.status" />
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<template #[`item.contactStatus`]="{ item }">
|
|
135
|
+
<v-btn
|
|
136
|
+
size="small"
|
|
137
|
+
variant="outlined"
|
|
138
|
+
class="text-none"
|
|
139
|
+
:loading="testingContactReaderId === item.readerId"
|
|
140
|
+
@click="testContactSipStatus(item)"
|
|
141
|
+
>
|
|
142
|
+
{{ getContactReaderStatus(item) }}
|
|
143
|
+
</v-btn>
|
|
144
|
+
</template>
|
|
145
|
+
|
|
146
|
+
<template #[`item.action-table`]="{ item }">
|
|
147
|
+
<v-menu location="bottom end">
|
|
148
|
+
<template #activator="{ props: menuProps }">
|
|
149
|
+
<AppButton v-bind="menuProps" variant="row" icon="mdi-dots-vertical" />
|
|
150
|
+
</template>
|
|
151
|
+
<v-list v-if="activeTab === 'intercom'" density="compact" class="row-menu">
|
|
152
|
+
<v-list-item title="View" @click="openViewDialog(item)" />
|
|
153
|
+
<v-list-item
|
|
154
|
+
title="Test Connection with SIP"
|
|
155
|
+
:disabled="testingSipReaderId === item.reader._id"
|
|
156
|
+
@click="testSipConnection(item)"
|
|
157
|
+
/>
|
|
158
|
+
<v-list-item title="Call Settings" @click="openCallDialog(item)" />
|
|
159
|
+
<v-list-item title="Dial Code" @click="openDialCodeDialog(item)" />
|
|
160
|
+
<v-list-item title="Make a call" @click="openMakeCallDialog(item)" />
|
|
161
|
+
<v-list-item title="Web call" @click="openWebPhoneDialog(item)" />
|
|
162
|
+
</v-list>
|
|
163
|
+
<v-list v-else density="compact" class="row-menu">
|
|
164
|
+
<v-list-item title="Call" prepend-icon="mdi-phone" @click="openContactCall(item)" />
|
|
165
|
+
<v-list-item title="Check reader SIP status" prepend-icon="mdi-lan-check" @click="testContactSipStatus(item)" />
|
|
166
|
+
<v-list-item title="View" @click="openContactView(item)" />
|
|
167
|
+
<v-list-item title="Edit" @click="openContactDialog(item)" />
|
|
168
|
+
<v-list-item title="Delete" @click="openDeleteContact(item)" />
|
|
169
|
+
</v-list>
|
|
170
|
+
</v-menu>
|
|
171
|
+
</template>
|
|
172
|
+
|
|
173
|
+
<!-- The screen's own sentence, kept. -->
|
|
174
|
+
<template #no-data>
|
|
175
|
+
<div class="table-card__empty">
|
|
176
|
+
<v-icon icon="mdi-deskphone" size="32" />
|
|
177
|
+
<span>No intercom records found.</span>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
180
|
+
</TableMain>
|
|
150
181
|
|
|
151
182
|
<v-dialog v-model="viewDialog" max-width="360">
|
|
152
183
|
<v-card class="detail-dialog" rounded="lg">
|
|
@@ -176,43 +207,53 @@
|
|
|
176
207
|
<v-card-text class="dialog-body">
|
|
177
208
|
<div class="sip-switch-row">
|
|
178
209
|
<span>Enable / Disable SIP</span>
|
|
179
|
-
<v-switch v-model="sipForm.enabled"
|
|
210
|
+
<v-switch v-model="sipForm.enabled" class="app-switch" density="compact" hide-details />
|
|
180
211
|
</div>
|
|
181
212
|
<div class="sip-label">Server Address <span>*</span></div>
|
|
182
|
-
<
|
|
213
|
+
<AppField v-model="sipForm.serverIp" :disabled="!sipForm.enabled" placeholder="SIP server address" class="sip-input" />
|
|
183
214
|
<div class="sip-label">Port <span>*</span></div>
|
|
184
|
-
<
|
|
215
|
+
<AppField v-model="sipForm.serverPort" :disabled="!sipForm.enabled" placeholder="0000" class="sip-input" />
|
|
185
216
|
<div class="form-two-col">
|
|
186
217
|
<div>
|
|
187
218
|
<div class="sip-label">Initial RTP Port <span>*</span></div>
|
|
188
|
-
<
|
|
219
|
+
<AppField v-model="sipForm.initialRtpPort" :disabled="!sipForm.enabled" placeholder="0000" class="sip-input" />
|
|
189
220
|
</div>
|
|
190
221
|
<div>
|
|
191
222
|
<div class="sip-label">Final RTP Port <span>*</span></div>
|
|
192
|
-
<
|
|
223
|
+
<AppField v-model="sipForm.finalRtpPort" :disabled="!sipForm.enabled" placeholder="0000" class="sip-input" />
|
|
193
224
|
</div>
|
|
194
225
|
</div>
|
|
195
226
|
<div class="sip-switch-row sip-section-switch">
|
|
196
227
|
<span>Numeric Extension</span>
|
|
197
|
-
<v-switch v-model="sipForm.numericExtension" :disabled="!sipForm.enabled"
|
|
228
|
+
<v-switch v-model="sipForm.numericExtension" :disabled="!sipForm.enabled" class="app-switch" density="compact" hide-details />
|
|
198
229
|
</div>
|
|
199
230
|
<div class="sip-label">Extension <span>*</span></div>
|
|
200
|
-
<
|
|
231
|
+
<AppField v-model="sipForm.extension" :disabled="areSipCredentialFieldsDisabled" placeholder="0000" class="sip-input" />
|
|
201
232
|
<div class="sip-label">Login <span>*</span></div>
|
|
202
|
-
<
|
|
233
|
+
<AppField v-model="sipForm.login" :disabled="areSipCredentialFieldsDisabled" placeholder="Enter username" class="sip-input" />
|
|
203
234
|
<div class="sip-label">Password <span>*</span></div>
|
|
204
|
-
|
|
235
|
+
<!-- The reveal eye moves into `AppField`'s `trailing` slot - the same
|
|
236
|
+
slot panel 4's access PIN uses, which is why it is a shared slot
|
|
237
|
+
rather than a private hack in two screens. -->
|
|
238
|
+
<AppField
|
|
205
239
|
v-model="sipForm.password"
|
|
240
|
+
:disabled="areSipCredentialFieldsDisabled"
|
|
206
241
|
placeholder="Enter password"
|
|
207
242
|
:type="showSipPassword ? 'text' : 'password'"
|
|
208
|
-
|
|
209
|
-
variant="outlined"
|
|
210
|
-
density="compact"
|
|
211
|
-
hide-details="auto"
|
|
243
|
+
autocomplete="new-password"
|
|
212
244
|
class="sip-input"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
>
|
|
246
|
+
<template #trailing>
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
class="app-field__trailing"
|
|
250
|
+
:aria-label="showSipPassword ? 'Hide SIP password' : 'Show SIP password'"
|
|
251
|
+
@click="showSipPassword = !showSipPassword"
|
|
252
|
+
>
|
|
253
|
+
<v-icon :icon="showSipPassword ? 'mdi-eye-off' : 'mdi-eye'" size="18" />
|
|
254
|
+
</button>
|
|
255
|
+
</template>
|
|
256
|
+
</AppField>
|
|
216
257
|
<div class="sip-label">Operating Mode <span>*</span></div>
|
|
217
258
|
<v-btn-toggle v-model="sipForm.operatingMode" :disabled="!sipForm.enabled" mandatory divided class="mode-toggle">
|
|
218
259
|
<v-btn value="sip-client" class="text-none">SIP Client</v-btn>
|
|
@@ -223,6 +264,13 @@
|
|
|
223
264
|
<v-radio label="UDP" value="UDP" />
|
|
224
265
|
<v-radio label="TCP" value="TCP" />
|
|
225
266
|
</v-radio-group>
|
|
267
|
+
<div class="sip-switch-row sip-section-switch">
|
|
268
|
+
<div>
|
|
269
|
+
<span>Enable video</span>
|
|
270
|
+
<small>Allow this reader to send its camera feed during SIP intercom calls.</small>
|
|
271
|
+
</div>
|
|
272
|
+
<v-switch v-model="sipForm.videoEnabled" :disabled="!sipForm.enabled" class="app-switch" density="compact" hide-details />
|
|
273
|
+
</div>
|
|
226
274
|
</v-card-text>
|
|
227
275
|
<v-card-actions class="dialog-actions pa-0">
|
|
228
276
|
<v-btn class="text-none action-cancel" variant="flat" height="44" @click="sipDialog = false">Cancel</v-btn>
|
|
@@ -231,36 +279,6 @@
|
|
|
231
279
|
</v-card>
|
|
232
280
|
</v-dialog>
|
|
233
281
|
|
|
234
|
-
<v-dialog v-model="rebootConfirmDialog" max-width="700" persistent>
|
|
235
|
-
<v-card class="reboot-dialog" rounded="0">
|
|
236
|
-
<button class="reboot-close" type="button" aria-label="Close" @click="rebootConfirmDialog = false">
|
|
237
|
-
<v-icon icon="mdi-close" />
|
|
238
|
-
</button>
|
|
239
|
-
<v-card-title class="reboot-title">Reboot is needed</v-card-title>
|
|
240
|
-
<v-card-text class="reboot-message">
|
|
241
|
-
It is necessary to restart the device after applying the configurations. Do you want to proceed?
|
|
242
|
-
</v-card-text>
|
|
243
|
-
<v-card-actions class="reboot-actions">
|
|
244
|
-
<v-btn class="text-none reboot-cancel" variant="text" @click="rebootConfirmDialog = false">
|
|
245
|
-
Cancel
|
|
246
|
-
</v-btn>
|
|
247
|
-
<v-btn class="text-none reboot-ok" variant="flat" @click="startRebootWait">
|
|
248
|
-
OK
|
|
249
|
-
</v-btn>
|
|
250
|
-
</v-card-actions>
|
|
251
|
-
</v-card>
|
|
252
|
-
</v-dialog>
|
|
253
|
-
|
|
254
|
-
<v-dialog v-model="rebootingDialog" max-width="900" persistent>
|
|
255
|
-
<v-card class="rebooting-card" rounded="0">
|
|
256
|
-
<div class="wait-word">Wait!</div>
|
|
257
|
-
<div class="rebooting-copy">
|
|
258
|
-
<h3>Rebooting Equipment</h3>
|
|
259
|
-
<p>Wait {{ rebootCountdown }} seconds for the equipment to reboot.</p>
|
|
260
|
-
</div>
|
|
261
|
-
</v-card>
|
|
262
|
-
</v-dialog>
|
|
263
|
-
|
|
264
282
|
<v-dialog v-model="callDialog" max-width="360" persistent>
|
|
265
283
|
<v-card class="form-dialog call-dialog" rounded="lg">
|
|
266
284
|
<v-card-title class="dialog-title">Call Settings</v-card-title>
|
|
@@ -273,37 +291,37 @@
|
|
|
273
291
|
</v-radio-group>
|
|
274
292
|
<div class="call-toggle-row">
|
|
275
293
|
<span>Automatic Answering</span>
|
|
276
|
-
<v-switch v-model="callForm.autoAnswer"
|
|
294
|
+
<v-switch v-model="callForm.autoAnswer" class="app-switch" density="compact" hide-details />
|
|
277
295
|
</div>
|
|
278
296
|
<div class="call-label">Automatic answering delay (seconds)</div>
|
|
279
|
-
<
|
|
297
|
+
<AppField v-model="callForm.autoAnswerDelay" :disabled="!callForm.autoAnswer" placeholder="0" class="call-input" />
|
|
280
298
|
<div class="form-two-col call-two-col">
|
|
281
299
|
<div>
|
|
282
300
|
<div class="call-label">Registration time (seconds)</div>
|
|
283
|
-
<
|
|
301
|
+
<AppField v-model="callForm.registrationTime" :disabled="!callForm.autoAnswer" placeholder="0" class="call-input" />
|
|
284
302
|
</div>
|
|
285
303
|
<div>
|
|
286
304
|
<div class="call-label">Keep-alive (seconds)</div>
|
|
287
|
-
<
|
|
305
|
+
<AppField v-model="callForm.keepAlive" :disabled="!callForm.autoAnswer" placeholder="0" class="call-input" />
|
|
288
306
|
</div>
|
|
289
307
|
</div>
|
|
290
308
|
<div class="call-label">Maximum call time (seconds)</div>
|
|
291
|
-
<
|
|
309
|
+
<AppField v-model="callForm.maxCallTime" :disabled="!callForm.autoAnswer" placeholder="0" class="call-input" />
|
|
292
310
|
<div class="call-toggle-row">
|
|
293
311
|
<span>Dial Button</span>
|
|
294
|
-
<v-switch v-model="callForm.dialButton"
|
|
312
|
+
<v-switch v-model="callForm.dialButton" class="app-switch" density="compact" hide-details />
|
|
295
313
|
</div>
|
|
296
314
|
<div class="call-toggle-row">
|
|
297
315
|
<span>External Dial Button</span>
|
|
298
|
-
<v-switch v-model="callForm.externalDialButton"
|
|
316
|
+
<v-switch v-model="callForm.externalDialButton" class="app-switch" density="compact" hide-details />
|
|
299
317
|
</div>
|
|
300
318
|
<div class="call-toggle-row">
|
|
301
319
|
<span>Facial identification during the call</span>
|
|
302
|
-
<v-switch v-model="callForm.faceDuringCall"
|
|
320
|
+
<v-switch v-model="callForm.faceDuringCall" class="app-switch" density="compact" hide-details />
|
|
303
321
|
</div>
|
|
304
322
|
<div class="call-toggle-row">
|
|
305
323
|
<span>Custom Icon</span>
|
|
306
|
-
<v-switch v-model="callForm.customIcon"
|
|
324
|
+
<v-switch v-model="callForm.customIcon" class="app-switch" density="compact" hide-details />
|
|
307
325
|
</div>
|
|
308
326
|
<div class="upload-row call-upload-row">
|
|
309
327
|
<div class="upload-drop" :class="{ 'is-disabled': !callForm.customIcon }"><v-icon size="16">mdi-cloud-upload-outline</v-icon> Drag and drop files here</div>
|
|
@@ -315,10 +333,10 @@
|
|
|
315
333
|
</div>
|
|
316
334
|
<div class="call-toggle-row">
|
|
317
335
|
<span>Automatic Call with unknown contact</span>
|
|
318
|
-
<v-switch v-model="callForm.unknownContact"
|
|
336
|
+
<v-switch v-model="callForm.unknownContact" class="app-switch" density="compact" hide-details />
|
|
319
337
|
</div>
|
|
320
338
|
<div class="call-label">Call after (seconds)</div>
|
|
321
|
-
<
|
|
339
|
+
<AppField v-model="callForm.callAfter" :disabled="!callForm.unknownContact" placeholder="0" class="call-input" />
|
|
322
340
|
</v-card-text>
|
|
323
341
|
<v-card-actions class="dialog-actions pa-0">
|
|
324
342
|
<v-btn class="text-none action-cancel" variant="flat" height="44" @click="callDialog = false">Cancel</v-btn>
|
|
@@ -333,10 +351,10 @@
|
|
|
333
351
|
<v-card-text class="dialog-body">
|
|
334
352
|
<div class="call-toggle-row dial-toggle-row">
|
|
335
353
|
<span>Dial Code</span>
|
|
336
|
-
<v-switch v-model="dialForm.enabled"
|
|
354
|
+
<v-switch v-model="dialForm.enabled" class="app-switch" density="compact" hide-details />
|
|
337
355
|
</div>
|
|
338
356
|
<div class="call-label">Dial code to unlock</div>
|
|
339
|
-
<
|
|
357
|
+
<AppField v-model="dialForm.code" :disabled="!dialForm.enabled" placeholder="0" class="call-input dial-input" />
|
|
340
358
|
</v-card-text>
|
|
341
359
|
<v-card-actions class="dialog-actions pa-0">
|
|
342
360
|
<v-btn class="text-none action-cancel" variant="flat" height="44" @click="dialCodeDialog = false">Cancel</v-btn>
|
|
@@ -350,16 +368,8 @@
|
|
|
350
368
|
<v-card-title class="dialog-title">Make a Call</v-card-title>
|
|
351
369
|
<v-card-text class="dialog-body make-call-body">
|
|
352
370
|
<div class="make-call-field">
|
|
353
|
-
<div class="call-label">
|
|
354
|
-
<v-
|
|
355
|
-
v-model="makeCallForm.target"
|
|
356
|
-
placeholder="Enter extension, address, or contact"
|
|
357
|
-
variant="outlined"
|
|
358
|
-
density="compact"
|
|
359
|
-
hide-details="auto"
|
|
360
|
-
class="call-input"
|
|
361
|
-
@keyup.enter="runMakeCall"
|
|
362
|
-
/>
|
|
371
|
+
<div class="call-label">Extension, SIP address, or peer IP</div>
|
|
372
|
+
<AppField v-model="makeCallForm.target" placeholder="7100 or user@example.com" @keyup.enter="runMakeCall" class="call-input" />
|
|
363
373
|
</div>
|
|
364
374
|
<div class="make-call-actions">
|
|
365
375
|
<v-btn
|
|
@@ -388,22 +398,195 @@
|
|
|
388
398
|
</div>
|
|
389
399
|
</v-card-text>
|
|
390
400
|
<v-card-actions class="dialog-actions pa-0">
|
|
391
|
-
<v-btn class="text-none action-
|
|
392
|
-
<v-btn class="text-none action-submit" variant="flat" height="44" @click="makeCallDialog = false">Close</v-btn>
|
|
401
|
+
<v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="makeCallDialog = false">Close</v-btn>
|
|
393
402
|
</v-card-actions>
|
|
394
403
|
</v-card>
|
|
395
404
|
</v-dialog>
|
|
396
405
|
|
|
406
|
+
<v-dialog v-model="webPhoneDialog" max-width="820" persistent eager>
|
|
407
|
+
<v-card class="web-phone-dialog" rounded="lg">
|
|
408
|
+
<v-card-title class="dialog-title web-phone-title">
|
|
409
|
+
<span>Web Call</span>
|
|
410
|
+
<div class="web-phone-title-actions">
|
|
411
|
+
<v-chip size="small" variant="flat" :class="webPhoneStatusClass">
|
|
412
|
+
{{ webPhoneStatusLabel }}
|
|
413
|
+
</v-chip>
|
|
414
|
+
<v-btn
|
|
415
|
+
icon="mdi-close"
|
|
416
|
+
size="small"
|
|
417
|
+
variant="text"
|
|
418
|
+
title="Close Web Call"
|
|
419
|
+
aria-label="Close Web Call"
|
|
420
|
+
@click="webPhoneDialog = false"
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
</v-card-title>
|
|
424
|
+
|
|
425
|
+
<v-card-text class="web-phone-body">
|
|
426
|
+
<div class="web-phone-settings">
|
|
427
|
+
<div class="web-phone-section-title">SIP Account</div>
|
|
428
|
+
<div v-if="sipAccountLoading" class="sip-account-state">
|
|
429
|
+
<v-progress-circular indeterminate color="primary" size="30" width="3" />
|
|
430
|
+
<span>Preparing your private SIP account...</span>
|
|
431
|
+
</div>
|
|
432
|
+
<div v-else-if="sipAccountError" class="sip-account-state is-error">
|
|
433
|
+
<v-icon icon="mdi-alert-circle-outline" size="28" />
|
|
434
|
+
<span>{{ sipAccountError }}</span>
|
|
435
|
+
<v-btn
|
|
436
|
+
class="text-none sip-account-retry"
|
|
437
|
+
variant="outlined"
|
|
438
|
+
prepend-icon="mdi-refresh"
|
|
439
|
+
@click="loadSipAccount"
|
|
440
|
+
>
|
|
441
|
+
Retry
|
|
442
|
+
</v-btn>
|
|
443
|
+
</div>
|
|
444
|
+
<div v-else-if="sipAccount" class="sip-account-summary">
|
|
445
|
+
<div>
|
|
446
|
+
<span class="sip-account-label">Assigned extension</span>
|
|
447
|
+
<strong class="sip-account-extension">{{ sipAccount.extension }}</strong>
|
|
448
|
+
</div>
|
|
449
|
+
<span class="sip-account-address">{{ sipAccount.sipAddress }}</span>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="call-toggle-row web-video-toggle">
|
|
452
|
+
<div>
|
|
453
|
+
<span>Receive HID video</span>
|
|
454
|
+
<small>Show the HID camera feed without sharing this device's camera.</small>
|
|
455
|
+
</div>
|
|
456
|
+
<v-switch
|
|
457
|
+
v-model="webPhoneForm.video"
|
|
458
|
+
class="app-switch"
|
|
459
|
+
density="compact"
|
|
460
|
+
hide-details
|
|
461
|
+
:disabled="webPhone.isRegistered.value"
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
<v-btn
|
|
465
|
+
v-if="!webPhone.isRegistered.value"
|
|
466
|
+
block
|
|
467
|
+
class="text-none web-phone-connect"
|
|
468
|
+
variant="flat"
|
|
469
|
+
prepend-icon="mdi-lan-connect"
|
|
470
|
+
:loading="webPhoneConnecting"
|
|
471
|
+
:disabled="!sipAccount || sipAccountLoading"
|
|
472
|
+
@click="connectWebPhone"
|
|
473
|
+
>
|
|
474
|
+
Connect Web Phone
|
|
475
|
+
</v-btn>
|
|
476
|
+
<v-btn
|
|
477
|
+
v-else
|
|
478
|
+
block
|
|
479
|
+
class="text-none web-phone-disconnect"
|
|
480
|
+
variant="outlined"
|
|
481
|
+
prepend-icon="mdi-lan-disconnect"
|
|
482
|
+
:loading="webPhoneConnecting"
|
|
483
|
+
:disabled="webPhone.hasCall.value"
|
|
484
|
+
@click="disconnectWebPhone"
|
|
485
|
+
>
|
|
486
|
+
Disconnect
|
|
487
|
+
</v-btn>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div class="web-phone-console">
|
|
491
|
+
<div
|
|
492
|
+
class="web-phone-media"
|
|
493
|
+
:class="{
|
|
494
|
+
'has-video': webPhoneForm.video && webPhone.callState.value === 'active',
|
|
495
|
+
'has-remote-video': webPhone.remoteVideoAvailable.value,
|
|
496
|
+
}"
|
|
497
|
+
>
|
|
498
|
+
<video ref="remoteVideoEl" autoplay playsinline class="remote-video" />
|
|
499
|
+
<audio ref="remoteAudioEl" autoplay />
|
|
500
|
+
<div
|
|
501
|
+
v-if="!webPhoneForm.video || webPhone.callState.value !== 'active' || !webPhone.remoteVideoAvailable.value"
|
|
502
|
+
class="audio-call-state"
|
|
503
|
+
>
|
|
504
|
+
<v-icon size="54">{{ webPhoneCallIcon }}</v-icon>
|
|
505
|
+
<strong>{{ webPhoneCallLabel }}</strong>
|
|
506
|
+
<span v-if="webPhone.currentTarget.value">{{ webPhone.currentTarget.value }}</span>
|
|
507
|
+
<span v-if="webPhoneForm.video && webPhone.callState.value === 'active' && !webPhone.remoteVideoAvailable.value">
|
|
508
|
+
Waiting for video from the HID device
|
|
509
|
+
</span>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div v-if="webPhone.callState.value === 'incoming'" class="incoming-call-actions">
|
|
514
|
+
<v-btn
|
|
515
|
+
class="text-none answer-call"
|
|
516
|
+
variant="flat"
|
|
517
|
+
prepend-icon="mdi-phone"
|
|
518
|
+
:loading="webPhoneActionLoading"
|
|
519
|
+
@click="answerWebCall"
|
|
520
|
+
>
|
|
521
|
+
Answer
|
|
522
|
+
</v-btn>
|
|
523
|
+
<v-btn
|
|
524
|
+
class="text-none reject-call"
|
|
525
|
+
variant="flat"
|
|
526
|
+
prepend-icon="mdi-phone-hangup"
|
|
527
|
+
:disabled="webPhoneActionLoading"
|
|
528
|
+
@click="declineWebCall"
|
|
529
|
+
>
|
|
530
|
+
Decline
|
|
531
|
+
</v-btn>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<template v-else>
|
|
535
|
+
<div class="call-label">Extension or SIP Address</div>
|
|
536
|
+
<div class="web-phone-dial-row">
|
|
537
|
+
<AppField v-model="webPhoneForm.destination" :disabled="!webPhone.isRegistered.value || webPhone.hasCall.value" placeholder="1001 or device@example.com" @keyup.enter="startWebCall" class="call-input mb-0" />
|
|
538
|
+
<v-btn
|
|
539
|
+
icon="mdi-phone"
|
|
540
|
+
class="start-web-call"
|
|
541
|
+
variant="flat"
|
|
542
|
+
:loading="webPhoneActionLoading && webPhone.callState.value === 'calling'"
|
|
543
|
+
:disabled="!webPhone.isRegistered.value || webPhone.hasCall.value || !webPhoneForm.destination.trim()"
|
|
544
|
+
@click="startWebCall"
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div v-if="webPhone.hasCall.value" class="active-call-controls">
|
|
549
|
+
<v-btn
|
|
550
|
+
:icon="webPhone.muted.value ? 'mdi-microphone-off' : 'mdi-microphone'"
|
|
551
|
+
variant="outlined"
|
|
552
|
+
:disabled="webPhone.callState.value !== 'active'"
|
|
553
|
+
:title="webPhone.muted.value ? 'Unmute' : 'Mute'"
|
|
554
|
+
@click="toggleWebMute"
|
|
555
|
+
/>
|
|
556
|
+
<v-btn
|
|
557
|
+
icon="mdi-phone-hangup"
|
|
558
|
+
class="hangup-web-call"
|
|
559
|
+
variant="flat"
|
|
560
|
+
:loading="webPhoneActionLoading && webPhone.callState.value === 'ending'"
|
|
561
|
+
title="End call"
|
|
562
|
+
@click="hangupWebCall"
|
|
563
|
+
/>
|
|
564
|
+
</div>
|
|
565
|
+
</template>
|
|
566
|
+
|
|
567
|
+
<v-alert v-if="webPhone.errorMessage.value" type="error" variant="tonal" density="compact" class="web-phone-error">
|
|
568
|
+
{{ webPhone.errorMessage.value }}
|
|
569
|
+
</v-alert>
|
|
570
|
+
</div>
|
|
571
|
+
</v-card-text>
|
|
572
|
+
|
|
573
|
+
</v-card>
|
|
574
|
+
</v-dialog>
|
|
575
|
+
|
|
397
576
|
<v-dialog v-model="contactDialog" max-width="360" persistent>
|
|
398
577
|
<v-card class="form-dialog" rounded="lg">
|
|
399
578
|
<v-card-title class="dialog-title">{{ contactForm.id ? 'Edit Contact' : 'Add Contact' }}</v-card-title>
|
|
400
579
|
<v-card-text class="dialog-body">
|
|
580
|
+
<FieldLabel text="HID Reader" required />
|
|
581
|
+
<AppSelect
|
|
582
|
+
v-model="contactForm.readerId"
|
|
583
|
+
:items="readerOptions"
|
|
584
|
+
:disabled="Boolean(contactForm.id)"
|
|
585
|
+
/>
|
|
401
586
|
<FieldLabel text="Name" required />
|
|
402
|
-
<
|
|
403
|
-
<FieldLabel text="
|
|
404
|
-
<
|
|
405
|
-
<FieldLabel text="Server Address" required />
|
|
406
|
-
<v-text-field v-model="contactForm.serverAddress" placeholder="192.168.1.1" variant="outlined" density="compact" hide-details="auto" />
|
|
587
|
+
<AppField v-model="contactForm.name" placeholder="Enter name" />
|
|
588
|
+
<FieldLabel text="SIP Number / Address" required />
|
|
589
|
+
<AppField v-model="contactForm.serverAddress" placeholder="7100 or user@example.com" />
|
|
407
590
|
</v-card-text>
|
|
408
591
|
<v-card-actions class="dialog-actions pa-0">
|
|
409
592
|
<v-btn class="text-none action-cancel" variant="flat" height="44" @click="contactDialog = false">Cancel</v-btn>
|
|
@@ -448,6 +631,10 @@
|
|
|
448
631
|
|
|
449
632
|
<script setup lang="ts">
|
|
450
633
|
const props = defineProps({
|
|
634
|
+
org: {
|
|
635
|
+
type: String,
|
|
636
|
+
required: true,
|
|
637
|
+
},
|
|
451
638
|
site: {
|
|
452
639
|
type: String,
|
|
453
640
|
required: true,
|
|
@@ -466,6 +653,19 @@ type ContactRow = {
|
|
|
466
653
|
dialCode: string;
|
|
467
654
|
};
|
|
468
655
|
|
|
656
|
+
type SipAccount = {
|
|
657
|
+
orgId: string;
|
|
658
|
+
site: string;
|
|
659
|
+
userId: string;
|
|
660
|
+
extension: string;
|
|
661
|
+
username: string;
|
|
662
|
+
password: string;
|
|
663
|
+
sipAddress: string;
|
|
664
|
+
websocketUrl: string;
|
|
665
|
+
status: "active";
|
|
666
|
+
provisionedAt: string;
|
|
667
|
+
};
|
|
668
|
+
|
|
469
669
|
type DetailItem = {
|
|
470
670
|
label: string;
|
|
471
671
|
value?: string | number | boolean;
|
|
@@ -474,7 +674,7 @@ type DetailItem = {
|
|
|
474
674
|
};
|
|
475
675
|
|
|
476
676
|
type IntercomRow = {
|
|
477
|
-
reader: Record<string,
|
|
677
|
+
reader: Record<string, unknown>;
|
|
478
678
|
name: string;
|
|
479
679
|
location: string;
|
|
480
680
|
serverAddress: string;
|
|
@@ -490,6 +690,7 @@ type IntercomRow = {
|
|
|
490
690
|
password: string;
|
|
491
691
|
operatingMode: string;
|
|
492
692
|
protocol: string;
|
|
693
|
+
videoEnabled: boolean;
|
|
493
694
|
dialMode: string;
|
|
494
695
|
dialModeLabel: string;
|
|
495
696
|
dialCode: string;
|
|
@@ -502,7 +703,7 @@ type IntercomRow = {
|
|
|
502
703
|
externalDialButton: boolean;
|
|
503
704
|
unknownContact: boolean;
|
|
504
705
|
callAfter: string;
|
|
505
|
-
pjsip: Record<string,
|
|
706
|
+
pjsip: Record<string, unknown>;
|
|
506
707
|
};
|
|
507
708
|
|
|
508
709
|
const {
|
|
@@ -513,41 +714,52 @@ const {
|
|
|
513
714
|
getIntercomStatus,
|
|
514
715
|
makeIntercomCall,
|
|
515
716
|
finalizeIntercomCall,
|
|
717
|
+
ensureSipAccount,
|
|
516
718
|
} = useHidAmico();
|
|
517
719
|
|
|
518
720
|
const activeTab = ref<IntercomTab>("intercom");
|
|
519
721
|
const readers = ref<Record<string, any>[]>([]);
|
|
722
|
+
const selectedReaderId = ref("");
|
|
723
|
+
const testingSipReaderId = ref("");
|
|
724
|
+
const testingContactReaderId = ref("");
|
|
520
725
|
const intercomRows = ref<IntercomRow[]>([]);
|
|
521
726
|
const contacts = ref<ContactRow[]>([]);
|
|
727
|
+
const intercomLoaded = ref(false);
|
|
728
|
+
const contactsLoaded = ref(false);
|
|
522
729
|
const loading = ref(false);
|
|
523
730
|
const saving = ref(false);
|
|
524
731
|
const contactsSaving = ref(false);
|
|
525
732
|
const calling = ref(false);
|
|
526
733
|
const hangingUp = ref(false);
|
|
527
734
|
const search = ref("");
|
|
528
|
-
const statusFilter = ref("
|
|
735
|
+
const statusFilter = ref("");
|
|
529
736
|
const page = ref(1);
|
|
530
737
|
const limit = 20;
|
|
531
738
|
const selectedRow = ref<IntercomRow | null>(null);
|
|
532
739
|
const selectedContact = ref<ContactRow | null>(null);
|
|
533
740
|
const showSipPassword = ref(false);
|
|
741
|
+
const webPhoneConnecting = ref(false);
|
|
742
|
+
const webPhoneActionLoading = ref(false);
|
|
743
|
+
const sipAccount = ref<SipAccount | null>(null);
|
|
744
|
+
const sipAccountLoading = ref(false);
|
|
745
|
+
const sipAccountError = ref("");
|
|
746
|
+
const remoteVideoEl = ref<HTMLVideoElement | null>(null);
|
|
747
|
+
const remoteAudioEl = ref<HTMLAudioElement | null>(null);
|
|
748
|
+
const webPhone = useSipWebPhone();
|
|
534
749
|
|
|
535
750
|
const viewDialog = ref(false);
|
|
536
751
|
const sipDialog = ref(false);
|
|
537
|
-
const rebootConfirmDialog = ref(false);
|
|
538
|
-
const rebootingDialog = ref(false);
|
|
539
752
|
const callDialog = ref(false);
|
|
540
753
|
const dialCodeDialog = ref(false);
|
|
541
754
|
const makeCallDialog = ref(false);
|
|
755
|
+
const webPhoneDialog = ref(false);
|
|
542
756
|
const contactDialog = ref(false);
|
|
543
757
|
const contactViewDialog = ref(false);
|
|
544
758
|
const deleteContactDialog = ref(false);
|
|
545
759
|
|
|
546
760
|
const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
|
|
547
|
-
const rebootCountdown = ref(59);
|
|
548
|
-
let rebootTimer: ReturnType<typeof setInterval> | undefined;
|
|
549
761
|
|
|
550
|
-
const statusOptions = ["
|
|
762
|
+
const statusOptions = ["Connected", "Not connected", "Connecting", "Disabled", "Failed"];
|
|
551
763
|
|
|
552
764
|
const pjsipKeys = [
|
|
553
765
|
"enabled",
|
|
@@ -582,13 +794,14 @@ const sipForm = reactive({
|
|
|
582
794
|
serverIp: "",
|
|
583
795
|
serverPort: "5060",
|
|
584
796
|
initialRtpPort: "10000",
|
|
585
|
-
finalRtpPort: "
|
|
797
|
+
finalRtpPort: "10100",
|
|
586
798
|
numericExtension: false,
|
|
587
799
|
extension: "",
|
|
588
800
|
login: "",
|
|
589
801
|
password: "",
|
|
590
802
|
operatingMode: "sip-client",
|
|
591
803
|
protocol: "UDP",
|
|
804
|
+
videoEnabled: false,
|
|
592
805
|
});
|
|
593
806
|
|
|
594
807
|
const callForm = reactive({
|
|
@@ -617,6 +830,11 @@ const makeCallForm = reactive({
|
|
|
617
830
|
statusType: "info" as MessageType,
|
|
618
831
|
});
|
|
619
832
|
|
|
833
|
+
const webPhoneForm = reactive({
|
|
834
|
+
destination: "",
|
|
835
|
+
video: false,
|
|
836
|
+
});
|
|
837
|
+
|
|
620
838
|
const contactForm = reactive({
|
|
621
839
|
id: undefined as number | undefined,
|
|
622
840
|
readerId: "",
|
|
@@ -625,6 +843,11 @@ const contactForm = reactive({
|
|
|
625
843
|
serverAddress: "",
|
|
626
844
|
});
|
|
627
845
|
|
|
846
|
+
const readerOptions = computed(() => readers.value.map((reader) => ({
|
|
847
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
848
|
+
value: reader._id,
|
|
849
|
+
})));
|
|
850
|
+
|
|
628
851
|
const visibleRows = computed(() => activeTab.value === "intercom" ? filteredIntercomRows.value : filteredContacts.value);
|
|
629
852
|
const pages = computed(() => Math.max(1, Math.ceil(visibleRows.value.length / limit)));
|
|
630
853
|
const pageRange = computed(() => {
|
|
@@ -638,6 +861,7 @@ const pageRange = computed(() => {
|
|
|
638
861
|
const filteredIntercomRows = computed(() => {
|
|
639
862
|
const query = search.value.trim().toLowerCase();
|
|
640
863
|
return intercomRows.value.filter((row) => {
|
|
864
|
+
const matchesReader = !selectedReaderId.value || row.reader._id === selectedReaderId.value;
|
|
641
865
|
const matchesSearch = !query || [
|
|
642
866
|
row.name,
|
|
643
867
|
row.location,
|
|
@@ -646,23 +870,83 @@ const filteredIntercomRows = computed(() => {
|
|
|
646
870
|
row.dialCode,
|
|
647
871
|
row.status,
|
|
648
872
|
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
649
|
-
const matchesStatus = statusFilter.value
|
|
650
|
-
return matchesSearch && matchesStatus;
|
|
873
|
+
const matchesStatus = !statusFilter.value || row.status === statusFilter.value;
|
|
874
|
+
return matchesReader && matchesSearch && matchesStatus;
|
|
651
875
|
});
|
|
652
876
|
});
|
|
653
877
|
|
|
654
878
|
const filteredContacts = computed(() => {
|
|
655
879
|
const query = search.value.trim().toLowerCase();
|
|
656
|
-
return contacts.value.filter((contact) =>
|
|
657
|
-
contact.
|
|
658
|
-
contact
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
880
|
+
return contacts.value.filter((contact) => {
|
|
881
|
+
const matchesReader = !selectedReaderId.value || contact.readerId === selectedReaderId.value;
|
|
882
|
+
const readerStatus = getContactReaderStatus(contact);
|
|
883
|
+
const matchesSearch = !query || [
|
|
884
|
+
contact.name,
|
|
885
|
+
contact.location,
|
|
886
|
+
contact.serverAddress,
|
|
887
|
+
contact.dialCode,
|
|
888
|
+
readerStatus,
|
|
889
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
890
|
+
const matchesStatus = !statusFilter.value || readerStatus === statusFilter.value;
|
|
891
|
+
return matchesReader && matchesSearch && matchesStatus;
|
|
892
|
+
});
|
|
662
893
|
});
|
|
663
894
|
|
|
664
895
|
const paginatedIntercomRows = computed(() => paginate(filteredIntercomRows.value));
|
|
665
896
|
const paginatedContacts = computed(() => paginate(filteredContacts.value));
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The two tabs, as data rather than two `v-tab` elements, so the shared
|
|
900
|
+
* `app-tab` strip can render them. Same values, same order.
|
|
901
|
+
*/
|
|
902
|
+
const tabOptions: Array<{ label: string; value: IntercomTab }> = [
|
|
903
|
+
{ label: "Intercom", value: "intercom" },
|
|
904
|
+
{ label: "Intercom Contacts", value: "contacts" },
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* The screen has TWO column sets, and it keeps them - this is the same pair the
|
|
909
|
+
* two `<thead>` blocks drew. `sortable: false` preserves the raw `v-table`'s
|
|
910
|
+
* behaviour, and no `weight` is set, so the layout is `TableMain`'s automatic
|
|
911
|
+
* one: the handoff draws no HID table and states no proportions.
|
|
912
|
+
*/
|
|
913
|
+
const tableHeaders = computed(() =>
|
|
914
|
+
activeTab.value === "intercom"
|
|
915
|
+
? [
|
|
916
|
+
{ title: "Name", value: "name", sortable: false },
|
|
917
|
+
{ title: "Location", value: "location", sortable: false },
|
|
918
|
+
{ title: "SIP Server Address", value: "serverAddress", sortable: false },
|
|
919
|
+
{ title: "Operating Mode", value: "operatingMode", sortable: false },
|
|
920
|
+
{ title: "Dial Code", value: "dialCode", sortable: false },
|
|
921
|
+
{ title: "SIP Status", value: "status", sortable: false },
|
|
922
|
+
{ title: "", value: "action-table", sortable: false },
|
|
923
|
+
]
|
|
924
|
+
: [
|
|
925
|
+
{ title: "Name", value: "name", sortable: false },
|
|
926
|
+
{ title: "Location", value: "location", sortable: false },
|
|
927
|
+
{ title: "SIP Number / Address", value: "serverAddress", sortable: false },
|
|
928
|
+
{ title: "Reader SIP Status", value: "contactStatus", sortable: false },
|
|
929
|
+
{ title: "", value: "action-table", sortable: false },
|
|
930
|
+
]
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* `TableMain` takes ONE item list, so the two `v-for` blocks become one. The
|
|
935
|
+
* intercom rows are keyed on their reader id (they have no `_rowKey` of their
|
|
936
|
+
* own, where contacts do), so one is supplied for the table's `item-value`.
|
|
937
|
+
*/
|
|
938
|
+
const visiblePageRows = computed(() =>
|
|
939
|
+
activeTab.value === "intercom"
|
|
940
|
+
? paginatedIntercomRows.value.map((row) => ({ ...row, _rowKey: row.reader?._id }))
|
|
941
|
+
: paginatedContacts.value
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
/** The web-call button turns green when registered and amber on an incoming
|
|
945
|
+
call - the same two states the old `.web-call-button` modifiers drew. */
|
|
946
|
+
const webPhoneButtonClass = computed(() => ({
|
|
947
|
+
"is-connected": webPhone.isRegistered.value,
|
|
948
|
+
"has-incoming-call": webPhone.callState.value === "incoming",
|
|
949
|
+
}));
|
|
666
950
|
const isPeerToPeerMode = computed(() => sipForm.operatingMode === "peer-to-peer");
|
|
667
951
|
const areSipCredentialFieldsDisabled = computed(() => !sipForm.enabled || isPeerToPeerMode.value);
|
|
668
952
|
const intercomDetailSections = computed(() => {
|
|
@@ -694,6 +978,7 @@ const intercomDetailSections = computed(() => {
|
|
|
694
978
|
items: [
|
|
695
979
|
{ label: "Operating Mode", value: row?.operatingMode },
|
|
696
980
|
{ label: "Protocol", value: row?.protocol },
|
|
981
|
+
{ label: "Video", value: row?.videoEnabled ? "Enabled" : "Disabled" },
|
|
697
982
|
],
|
|
698
983
|
},
|
|
699
984
|
{
|
|
@@ -717,10 +1002,45 @@ const contactDetailRows = computed<DetailItem[]>(() => [
|
|
|
717
1002
|
{ label: "Server Address", value: selectedContact.value?.serverAddress, link: true },
|
|
718
1003
|
{ label: "Location", value: selectedContact.value?.location },
|
|
719
1004
|
]);
|
|
1005
|
+
const webPhoneStatusLabel = computed(() => {
|
|
1006
|
+
if (webPhone.callState.value === "incoming") return "Incoming call";
|
|
1007
|
+
if (webPhone.callState.value === "calling") return "Calling";
|
|
1008
|
+
if (webPhone.callState.value === "active") return "In call";
|
|
1009
|
+
if (webPhone.callState.value === "ending") return "Ending call";
|
|
1010
|
+
if (webPhone.connectionState.value === "connecting") return "Connecting";
|
|
1011
|
+
if (webPhone.connectionState.value === "registered") return "Ready";
|
|
1012
|
+
if (webPhone.connectionState.value === "error") return "Connection failed";
|
|
1013
|
+
return "Disconnected";
|
|
1014
|
+
});
|
|
1015
|
+
const webPhoneStatusClass = computed(() => {
|
|
1016
|
+
if (webPhone.callState.value === "incoming") return "web-phone-status incoming";
|
|
1017
|
+
if (webPhone.callState.value === "active" || webPhone.connectionState.value === "registered") return "web-phone-status ready";
|
|
1018
|
+
if (webPhone.connectionState.value === "error") return "web-phone-status failed";
|
|
1019
|
+
return "web-phone-status idle";
|
|
1020
|
+
});
|
|
1021
|
+
const webPhoneCallLabel = computed(() => {
|
|
1022
|
+
if (webPhone.callState.value === "incoming") return "Incoming call";
|
|
1023
|
+
if (webPhone.callState.value === "calling") return "Calling...";
|
|
1024
|
+
if (webPhone.callState.value === "active") return "Call connected";
|
|
1025
|
+
if (webPhone.callState.value === "ending") return "Ending call...";
|
|
1026
|
+
return webPhone.isRegistered.value ? "Ready to call" : "Connect a SIP account";
|
|
1027
|
+
});
|
|
1028
|
+
const webPhoneCallIcon = computed(() => {
|
|
1029
|
+
if (webPhone.callState.value === "incoming") return "mdi-phone-incoming";
|
|
1030
|
+
if (webPhone.callState.value === "calling") return "mdi-phone-outgoing";
|
|
1031
|
+
if (webPhone.callState.value === "active") return "mdi-phone-in-talk";
|
|
1032
|
+
return "mdi-phone-outline";
|
|
1033
|
+
});
|
|
720
1034
|
|
|
721
1035
|
watch(
|
|
722
|
-
() => props.site,
|
|
723
|
-
() =>
|
|
1036
|
+
[() => props.org, () => props.site],
|
|
1037
|
+
() => {
|
|
1038
|
+
if (webPhone.isRegistered.value) void webPhone.disconnect();
|
|
1039
|
+
sipAccount.value = null;
|
|
1040
|
+
sipAccountError.value = "";
|
|
1041
|
+
webPhoneForm.destination = "";
|
|
1042
|
+
void reload();
|
|
1043
|
+
},
|
|
724
1044
|
{ immediate: true },
|
|
725
1045
|
);
|
|
726
1046
|
|
|
@@ -732,9 +1052,12 @@ watch(pages, () => {
|
|
|
732
1052
|
if (page.value > pages.value) page.value = pages.value;
|
|
733
1053
|
});
|
|
734
1054
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1055
|
+
watch(
|
|
1056
|
+
() => webPhone.callState.value,
|
|
1057
|
+
(state) => {
|
|
1058
|
+
if (state === "incoming") webPhoneDialog.value = true;
|
|
1059
|
+
},
|
|
1060
|
+
);
|
|
738
1061
|
|
|
739
1062
|
async function reload() {
|
|
740
1063
|
if (!props.site) return;
|
|
@@ -742,8 +1065,12 @@ async function reload() {
|
|
|
742
1065
|
try {
|
|
743
1066
|
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
744
1067
|
readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
745
|
-
|
|
746
|
-
|
|
1068
|
+
if (!readers.value.some((reader) => reader._id === selectedReaderId.value)) {
|
|
1069
|
+
selectedReaderId.value = readers.value[0]?._id || "";
|
|
1070
|
+
}
|
|
1071
|
+
intercomLoaded.value = false;
|
|
1072
|
+
contactsLoaded.value = false;
|
|
1073
|
+
await loadActiveTab();
|
|
747
1074
|
} catch (error: any) {
|
|
748
1075
|
showMessage(getErrorMessage(error), "error");
|
|
749
1076
|
} finally {
|
|
@@ -751,10 +1078,36 @@ async function reload() {
|
|
|
751
1078
|
}
|
|
752
1079
|
}
|
|
753
1080
|
|
|
754
|
-
async function
|
|
755
|
-
|
|
1081
|
+
async function loadActiveTab() {
|
|
1082
|
+
if (activeTab.value === "intercom") {
|
|
1083
|
+
await loadIntercomRows();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
await Promise.all([
|
|
1088
|
+
intercomLoaded.value ? Promise.resolve() : loadIntercomRows(),
|
|
1089
|
+
loadContacts(),
|
|
1090
|
+
]);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async function selectTab(tab: IntercomTab) {
|
|
1094
|
+
activeTab.value = tab;
|
|
1095
|
+
page.value = 1;
|
|
1096
|
+
|
|
1097
|
+
if ((tab === "intercom" && intercomLoaded.value) || (tab === "contacts" && contactsLoaded.value)) return;
|
|
1098
|
+
|
|
1099
|
+
loading.value = true;
|
|
1100
|
+
try {
|
|
1101
|
+
await loadActiveTab();
|
|
1102
|
+
} catch (error: unknown) {
|
|
1103
|
+
showMessage(getErrorMessage(error), "error");
|
|
1104
|
+
} finally {
|
|
1105
|
+
loading.value = false;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
756
1108
|
|
|
757
|
-
|
|
1109
|
+
async function loadIntercomRows() {
|
|
1110
|
+
const rows = await Promise.all(readers.value.map(async (reader): Promise<IntercomRow> => {
|
|
758
1111
|
const [configurationResult, statusResult] = await Promise.allSettled([
|
|
759
1112
|
getReaderConfiguration(reader._id, { pjsip: pjsipKeys }),
|
|
760
1113
|
getIntercomStatus(reader._id),
|
|
@@ -766,16 +1119,15 @@ async function loadIntercomRows() {
|
|
|
766
1119
|
const statusPayload = statusResponse?.data ?? statusResponse;
|
|
767
1120
|
const pjsip = normalizePjsip(configurationResponse, statusPayload);
|
|
768
1121
|
const status = normalizeStatus(statusPayload, pjsip);
|
|
769
|
-
|
|
770
|
-
}
|
|
1122
|
+
return toIntercomRow(reader, pjsip, status);
|
|
1123
|
+
}));
|
|
771
1124
|
|
|
772
1125
|
intercomRows.value = rows;
|
|
1126
|
+
intercomLoaded.value = true;
|
|
773
1127
|
}
|
|
774
1128
|
|
|
775
1129
|
async function loadContacts() {
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
for (const reader of readers.value) {
|
|
1130
|
+
const readerContacts = await Promise.all(readers.value.map(async (reader): Promise<ContactRow[]> => {
|
|
779
1131
|
try {
|
|
780
1132
|
const response = await runObjectOperation(reader._id, {
|
|
781
1133
|
operation: "load",
|
|
@@ -783,13 +1135,15 @@ async function loadContacts() {
|
|
|
783
1135
|
fields: ["id", "name", "number"],
|
|
784
1136
|
limit: 100,
|
|
785
1137
|
});
|
|
786
|
-
|
|
1138
|
+
return normalizeContacts(reader, response?.data ?? response);
|
|
787
1139
|
} catch {
|
|
788
1140
|
// Some firmware builds may not expose contacts until intercom is configured.
|
|
1141
|
+
return [];
|
|
789
1142
|
}
|
|
790
|
-
}
|
|
1143
|
+
}));
|
|
791
1144
|
|
|
792
|
-
contacts.value =
|
|
1145
|
+
contacts.value = readerContacts.flat();
|
|
1146
|
+
contactsLoaded.value = true;
|
|
793
1147
|
}
|
|
794
1148
|
|
|
795
1149
|
function paginate<T>(items: T[]) {
|
|
@@ -798,19 +1152,27 @@ function paginate<T>(items: T[]) {
|
|
|
798
1152
|
return items.slice(start, start + limit);
|
|
799
1153
|
}
|
|
800
1154
|
|
|
801
|
-
function toIntercomRow(reader: Record<string,
|
|
1155
|
+
function toIntercomRow(reader: Record<string, unknown>, pjsip: Record<string, unknown>, status: string): IntercomRow {
|
|
802
1156
|
const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
|
|
803
1157
|
const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
|
|
1158
|
+
const initialRtpPort = toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A");
|
|
1159
|
+
const rtpPortRange = getPjsipValue(pjsip, ["server_outbound_port_range"]);
|
|
1160
|
+
const explicitFinalRtpPort = getPjsipValue(pjsip, ["finalRtpPort", "final_rtp_port"]);
|
|
1161
|
+
const initialRtpPortNumber = Number(initialRtpPort);
|
|
1162
|
+
const rtpPortRangeNumber = Number(rtpPortRange);
|
|
1163
|
+
const finalRtpPort = rtpPortRange !== undefined && Number.isFinite(initialRtpPortNumber) && Number.isFinite(rtpPortRangeNumber)
|
|
1164
|
+
? String(initialRtpPortNumber + rtpPortRangeNumber)
|
|
1165
|
+
: toText(explicitFinalRtpPort, "N/A");
|
|
804
1166
|
|
|
805
1167
|
return {
|
|
806
1168
|
reader,
|
|
807
|
-
name: reader.name || reader.deviceId
|
|
808
|
-
location: reader.location
|
|
1169
|
+
name: toText(reader.name || reader.deviceId, "HID Reader"),
|
|
1170
|
+
location: toText(reader.location, "N/A"),
|
|
809
1171
|
serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
|
|
810
1172
|
serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
|
|
811
|
-
initialRtpPort
|
|
812
|
-
finalRtpPort
|
|
813
|
-
monitorPath: reader.monitorPath
|
|
1173
|
+
initialRtpPort,
|
|
1174
|
+
finalRtpPort,
|
|
1175
|
+
monitorPath: toText(reader.monitorPath, "N/A"),
|
|
814
1176
|
status,
|
|
815
1177
|
sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
|
|
816
1178
|
numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
|
|
@@ -819,6 +1181,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
819
1181
|
password: toText(getPjsipValue(pjsip, ["password"]), ""),
|
|
820
1182
|
operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
|
|
821
1183
|
protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
|
|
1184
|
+
videoEnabled: toFlag(getPjsipValue(pjsip, ["video_enabled", "videoEnabled"])) === "1",
|
|
822
1185
|
dialMode,
|
|
823
1186
|
dialModeLabel: getDialModeLabel(dialMode),
|
|
824
1187
|
dialCode: toText(getPjsipValue(pjsip, ["open_door_command", "dialCode"]), "N/A"),
|
|
@@ -835,7 +1198,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
835
1198
|
};
|
|
836
1199
|
}
|
|
837
1200
|
|
|
838
|
-
function normalizePjsip(...sources: Record<string,
|
|
1201
|
+
function normalizePjsip(...sources: Record<string, unknown>[]) {
|
|
839
1202
|
for (const source of sources) {
|
|
840
1203
|
const candidate = findPjsipCandidate(source);
|
|
841
1204
|
if (candidate) return candidate;
|
|
@@ -843,45 +1206,46 @@ function normalizePjsip(...sources: Record<string, any>[]) {
|
|
|
843
1206
|
return {};
|
|
844
1207
|
}
|
|
845
1208
|
|
|
846
|
-
function findPjsipCandidate(source: Record<string,
|
|
1209
|
+
function findPjsipCandidate(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
847
1210
|
if (!source || typeof source !== "object") return undefined;
|
|
848
1211
|
const candidates = [
|
|
849
1212
|
source.pjsip,
|
|
850
|
-
source.
|
|
851
|
-
source.
|
|
852
|
-
source.result
|
|
853
|
-
source.
|
|
854
|
-
source.data
|
|
855
|
-
source.data
|
|
856
|
-
source.data
|
|
857
|
-
source.data
|
|
858
|
-
source.data
|
|
859
|
-
source.configuration,
|
|
860
|
-
source.
|
|
861
|
-
source.data
|
|
1213
|
+
toRecord(source.pjsip),
|
|
1214
|
+
toRecord(toRecord(source.configuration).pjsip),
|
|
1215
|
+
toRecord(toRecord(source.result).pjsip),
|
|
1216
|
+
toRecord(toRecord(toRecord(source.result).configuration).pjsip),
|
|
1217
|
+
toRecord(toRecord(source.data).pjsip),
|
|
1218
|
+
toRecord(toRecord(toRecord(source.data).configuration).pjsip),
|
|
1219
|
+
toRecord(toRecord(toRecord(source.data).result).pjsip),
|
|
1220
|
+
toRecord(toRecord(toRecord(toRecord(source.data).result).configuration).pjsip),
|
|
1221
|
+
toRecord(toRecord(toRecord(source.data).data).pjsip),
|
|
1222
|
+
toRecord(toRecord(toRecord(toRecord(source.data).data).configuration).pjsip),
|
|
1223
|
+
toRecord(source.configuration),
|
|
1224
|
+
toRecord(toRecord(source.data).configuration),
|
|
1225
|
+
toRecord(toRecord(toRecord(source.data).data).configuration),
|
|
862
1226
|
source,
|
|
863
1227
|
];
|
|
864
1228
|
|
|
865
1229
|
return candidates.find((candidate) => isPjsipCandidate(candidate));
|
|
866
1230
|
}
|
|
867
1231
|
|
|
868
|
-
function isPjsipCandidate(value: unknown): value is Record<string,
|
|
1232
|
+
function isPjsipCandidate(value: unknown): value is Record<string, unknown> {
|
|
869
1233
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
870
1234
|
return pjsipKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)) ||
|
|
871
1235
|
["serverAddress", "serverIp", "serverPort", "extension", "peerToPeerEnabled"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
872
1236
|
}
|
|
873
1237
|
|
|
874
|
-
function getPjsipValue(source: Record<string,
|
|
1238
|
+
function getPjsipValue(source: Record<string, unknown>, keys: string[]) {
|
|
875
1239
|
for (const key of keys) {
|
|
876
1240
|
if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== "") return source[key];
|
|
877
1241
|
}
|
|
878
1242
|
return undefined;
|
|
879
1243
|
}
|
|
880
1244
|
|
|
881
|
-
function normalizeStatus(response: Record<string,
|
|
1245
|
+
function normalizeStatus(response: Record<string, unknown>, pjsip: Record<string, unknown>) {
|
|
882
1246
|
const status = Number(response?.status);
|
|
1247
|
+
if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
|
|
883
1248
|
if (status === 200) return "Connected";
|
|
884
|
-
if (status === -1 || toFlag(pjsip.enabled) === "0") return "Disabled";
|
|
885
1249
|
if (status === 0 || status === 100) return "Connecting";
|
|
886
1250
|
if ([401, 403, 408, 503].includes(status)) return "Failed";
|
|
887
1251
|
return "Not connected";
|
|
@@ -911,13 +1275,14 @@ function openSipDialog(row: IntercomRow) {
|
|
|
911
1275
|
sipForm.serverIp = row.serverAddress === "N/A" ? "" : row.serverAddress;
|
|
912
1276
|
sipForm.serverPort = row.serverPort === "N/A" ? "5060" : row.serverPort;
|
|
913
1277
|
sipForm.initialRtpPort = row.initialRtpPort === "N/A" ? "10000" : row.initialRtpPort;
|
|
914
|
-
sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "
|
|
1278
|
+
sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "10100" : row.finalRtpPort;
|
|
915
1279
|
sipForm.numericExtension = row.numericExtension;
|
|
916
1280
|
sipForm.extension = row.extension === "N/A" ? "" : row.extension;
|
|
917
1281
|
sipForm.login = row.login === "N/A" ? "" : row.login;
|
|
918
1282
|
sipForm.password = row.password;
|
|
919
1283
|
sipForm.operatingMode = row.operatingMode === "Peer-to-Peer" ? "peer-to-peer" : "sip-client";
|
|
920
1284
|
sipForm.protocol = row.protocol || "UDP";
|
|
1285
|
+
sipForm.videoEnabled = row.videoEnabled;
|
|
921
1286
|
sipDialog.value = true;
|
|
922
1287
|
}
|
|
923
1288
|
|
|
@@ -943,20 +1308,149 @@ function openDialCodeDialog(row: IntercomRow) {
|
|
|
943
1308
|
dialCodeDialog.value = true;
|
|
944
1309
|
}
|
|
945
1310
|
|
|
946
|
-
function openMakeCallDialog(row: IntercomRow) {
|
|
1311
|
+
async function openMakeCallDialog(row: IntercomRow) {
|
|
947
1312
|
selectedRow.value = row;
|
|
948
|
-
makeCallForm.target =
|
|
1313
|
+
makeCallForm.target = sipAccount.value?.extension || "";
|
|
949
1314
|
makeCallForm.status = "";
|
|
950
1315
|
makeCallForm.statusType = "info";
|
|
951
1316
|
makeCallDialog.value = true;
|
|
1317
|
+
if (!sipAccount.value && !sipAccountLoading.value) {
|
|
1318
|
+
await loadSipAccount();
|
|
1319
|
+
if (makeCallDialog.value && !makeCallForm.target) {
|
|
1320
|
+
makeCallForm.target = sipAccount.value?.extension || "";
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function openWebPhoneDialog(row?: IntercomRow) {
|
|
1326
|
+
const activeRow = row || selectedRow.value || intercomRows.value[0];
|
|
1327
|
+
if (activeRow) {
|
|
1328
|
+
selectedRow.value = activeRow;
|
|
1329
|
+
if (!webPhone.isRegistered.value && activeRow.extension !== "N/A") {
|
|
1330
|
+
webPhoneForm.destination = activeRow.extension;
|
|
1331
|
+
webPhoneForm.video = activeRow.videoEnabled;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
webPhoneDialog.value = true;
|
|
1335
|
+
if (!sipAccount.value && !sipAccountLoading.value) void loadSipAccount();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async function loadSipAccount() {
|
|
1339
|
+
if (!props.org || !props.site) return;
|
|
1340
|
+
|
|
1341
|
+
sipAccountLoading.value = true;
|
|
1342
|
+
sipAccountError.value = "";
|
|
1343
|
+
try {
|
|
1344
|
+
const response = await ensureSipAccount(props.site, props.org);
|
|
1345
|
+
sipAccount.value = normalizeSipAccount(response);
|
|
1346
|
+
} catch (error: unknown) {
|
|
1347
|
+
sipAccount.value = null;
|
|
1348
|
+
sipAccountError.value = getErrorMessage(error);
|
|
1349
|
+
} finally {
|
|
1350
|
+
sipAccountLoading.value = false;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function connectWebPhone() {
|
|
1355
|
+
const account = sipAccount.value;
|
|
1356
|
+
if (!account) {
|
|
1357
|
+
await loadSipAccount();
|
|
1358
|
+
if (!sipAccount.value) return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const activeAccount = sipAccount.value;
|
|
1362
|
+
if (!activeAccount) return;
|
|
1363
|
+
|
|
1364
|
+
webPhoneConnecting.value = true;
|
|
1365
|
+
try {
|
|
1366
|
+
await webPhone.connect({
|
|
1367
|
+
webSocketServer: activeAccount.websocketUrl,
|
|
1368
|
+
aor: activeAccount.sipAddress,
|
|
1369
|
+
authorizationUsername: activeAccount.username,
|
|
1370
|
+
authorizationPassword: activeAccount.password,
|
|
1371
|
+
displayName: "iService365 Web",
|
|
1372
|
+
video: webPhoneForm.video,
|
|
1373
|
+
}, {
|
|
1374
|
+
remoteVideo: remoteVideoEl.value,
|
|
1375
|
+
remoteAudio: remoteAudioEl.value,
|
|
1376
|
+
});
|
|
1377
|
+
showMessage("Web phone connected and ready to receive calls.", "success");
|
|
1378
|
+
} catch (error: unknown) {
|
|
1379
|
+
showMessage(getErrorMessage(error), "error");
|
|
1380
|
+
} finally {
|
|
1381
|
+
webPhoneConnecting.value = false;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async function disconnectWebPhone() {
|
|
1386
|
+
webPhoneConnecting.value = true;
|
|
1387
|
+
try {
|
|
1388
|
+
await webPhone.disconnect();
|
|
1389
|
+
showMessage("Web phone disconnected.", "info");
|
|
1390
|
+
} finally {
|
|
1391
|
+
webPhoneConnecting.value = false;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async function startWebCall() {
|
|
1396
|
+
webPhoneActionLoading.value = true;
|
|
1397
|
+
try {
|
|
1398
|
+
await webPhone.call(webPhoneForm.destination);
|
|
1399
|
+
} catch (error: unknown) {
|
|
1400
|
+
showMessage(getErrorMessage(error), "error");
|
|
1401
|
+
} finally {
|
|
1402
|
+
webPhoneActionLoading.value = false;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function answerWebCall() {
|
|
1407
|
+
webPhoneActionLoading.value = true;
|
|
1408
|
+
try {
|
|
1409
|
+
await webPhone.answer();
|
|
1410
|
+
} catch (error: unknown) {
|
|
1411
|
+
showMessage(getErrorMessage(error), "error");
|
|
1412
|
+
} finally {
|
|
1413
|
+
webPhoneActionLoading.value = false;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async function declineWebCall() {
|
|
1418
|
+
webPhoneActionLoading.value = true;
|
|
1419
|
+
try {
|
|
1420
|
+
await webPhone.decline();
|
|
1421
|
+
} catch (error: unknown) {
|
|
1422
|
+
showMessage(getErrorMessage(error), "error");
|
|
1423
|
+
} finally {
|
|
1424
|
+
webPhoneActionLoading.value = false;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function hangupWebCall() {
|
|
1429
|
+
webPhoneActionLoading.value = true;
|
|
1430
|
+
try {
|
|
1431
|
+
await webPhone.hangup();
|
|
1432
|
+
} catch (error: unknown) {
|
|
1433
|
+
showMessage(getErrorMessage(error), "error");
|
|
1434
|
+
} finally {
|
|
1435
|
+
webPhoneActionLoading.value = false;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function toggleWebMute() {
|
|
1440
|
+
try {
|
|
1441
|
+
webPhone.toggleMute();
|
|
1442
|
+
} catch (error: unknown) {
|
|
1443
|
+
showMessage(getErrorMessage(error), "error");
|
|
1444
|
+
}
|
|
952
1445
|
}
|
|
953
1446
|
|
|
954
1447
|
function openContactDialog(contact?: ContactRow) {
|
|
955
1448
|
selectedContact.value = contact || null;
|
|
956
1449
|
contactForm.id = contact?.id;
|
|
957
|
-
contactForm.readerId = contact?.readerId || readers.value[0]?._id || "";
|
|
1450
|
+
contactForm.readerId = contact?.readerId || selectedReaderId.value || readers.value[0]?._id || "";
|
|
958
1451
|
contactForm.name = contact?.name === "N/A" ? "" : contact?.name || "";
|
|
959
|
-
|
|
1452
|
+
const selectedReader = readers.value.find((reader) => reader._id === contactForm.readerId);
|
|
1453
|
+
contactForm.location = contact?.location === "N/A" ? "" : contact?.location || selectedReader?.location || "";
|
|
960
1454
|
contactForm.serverAddress = contact?.serverAddress === "N/A" ? "" : contact?.serverAddress || "";
|
|
961
1455
|
contactDialog.value = true;
|
|
962
1456
|
}
|
|
@@ -966,6 +1460,39 @@ function openContactView(contact: ContactRow) {
|
|
|
966
1460
|
contactViewDialog.value = true;
|
|
967
1461
|
}
|
|
968
1462
|
|
|
1463
|
+
function getContactReaderStatus(contact: ContactRow) {
|
|
1464
|
+
return intercomRows.value.find((row) => row.reader._id === contact.readerId)?.status || "Check status";
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async function testContactSipStatus(contact: ContactRow) {
|
|
1468
|
+
const reader = intercomRows.value.find((row) => row.reader._id === contact.readerId);
|
|
1469
|
+
if (!reader) {
|
|
1470
|
+
showMessage("The HID reader for this contact is not available.", "error");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
testingContactReaderId.value = contact.readerId;
|
|
1475
|
+
try {
|
|
1476
|
+
await testSipConnection(reader);
|
|
1477
|
+
} finally {
|
|
1478
|
+
testingContactReaderId.value = "";
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function openContactCall(contact: ContactRow) {
|
|
1483
|
+
const reader = intercomRows.value.find((row) => row.reader._id === contact.readerId);
|
|
1484
|
+
if (!reader) {
|
|
1485
|
+
showMessage("The HID reader for this contact is not available.", "error");
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
selectedRow.value = reader;
|
|
1490
|
+
makeCallForm.target = contact.serverAddress === "N/A" ? "" : contact.serverAddress;
|
|
1491
|
+
makeCallForm.status = "";
|
|
1492
|
+
makeCallForm.statusType = "info";
|
|
1493
|
+
makeCallDialog.value = true;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
969
1496
|
function openDeleteContact(contact: ContactRow) {
|
|
970
1497
|
selectedContact.value = contact;
|
|
971
1498
|
deleteContactDialog.value = true;
|
|
@@ -974,21 +1501,35 @@ function openDeleteContact(contact: ContactRow) {
|
|
|
974
1501
|
async function saveSipSettings() {
|
|
975
1502
|
if (!selectedRow.value) return;
|
|
976
1503
|
const isPeerToPeer = sipForm.operatingMode === "peer-to-peer";
|
|
1504
|
+
const initialRtpPort = Number(sipForm.initialRtpPort);
|
|
1505
|
+
const finalRtpPort = Number(sipForm.finalRtpPort);
|
|
1506
|
+
|
|
1507
|
+
if (
|
|
1508
|
+
!Number.isInteger(initialRtpPort) ||
|
|
1509
|
+
!Number.isInteger(finalRtpPort) ||
|
|
1510
|
+
initialRtpPort < 1 ||
|
|
1511
|
+
finalRtpPort > 65535 ||
|
|
1512
|
+
finalRtpPort <= initialRtpPort
|
|
1513
|
+
) {
|
|
1514
|
+
showMessage("Final RTP Port must be greater than Initial RTP Port and both ports must be between 1 and 65535.", "warning");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
977
1518
|
const saved = await savePjsip(selectedRow.value.reader._id, {
|
|
978
1519
|
enabled: sipForm.enabled ? "1" : "0",
|
|
979
1520
|
server_ip: sipForm.serverIp,
|
|
980
1521
|
server_port: sipForm.serverPort,
|
|
981
|
-
server_outbound_port:
|
|
982
|
-
server_outbound_port_range:
|
|
1522
|
+
server_outbound_port: String(initialRtpPort),
|
|
1523
|
+
server_outbound_port_range: String(finalRtpPort - initialRtpPort),
|
|
983
1524
|
numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
|
|
984
1525
|
branch: isPeerToPeer ? "" : sipForm.extension,
|
|
985
1526
|
login: isPeerToPeer ? "" : sipForm.login,
|
|
986
1527
|
password: isPeerToPeer ? "" : sipForm.password,
|
|
987
1528
|
peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
|
|
988
|
-
|
|
1529
|
+
video_enabled: sipForm.videoEnabled ? "1" : "0",
|
|
1530
|
+
});
|
|
989
1531
|
if (saved) {
|
|
990
1532
|
sipDialog.value = false;
|
|
991
|
-
rebootConfirmDialog.value = true;
|
|
992
1533
|
}
|
|
993
1534
|
}
|
|
994
1535
|
|
|
@@ -1020,7 +1561,13 @@ async function runMakeCall() {
|
|
|
1020
1561
|
if (!selectedRow.value) return;
|
|
1021
1562
|
const target = makeCallForm.target.trim();
|
|
1022
1563
|
if (!target) {
|
|
1023
|
-
makeCallForm.status = "Please enter an address
|
|
1564
|
+
makeCallForm.status = "Please enter an extension, SIP address, or peer IP.";
|
|
1565
|
+
makeCallForm.statusType = "warning";
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (!isSipCallTarget(target)) {
|
|
1570
|
+
makeCallForm.status = "Enter a valid extension, SIP address, or IPv4 address.";
|
|
1024
1571
|
makeCallForm.statusType = "warning";
|
|
1025
1572
|
return;
|
|
1026
1573
|
}
|
|
@@ -1042,6 +1589,10 @@ async function runMakeCall() {
|
|
|
1042
1589
|
}
|
|
1043
1590
|
}
|
|
1044
1591
|
|
|
1592
|
+
function isSipCallTarget(value = "") {
|
|
1593
|
+
return /^(?:sip:)?[A-Za-z0-9_.+-]+(?:@[A-Za-z0-9.-]+(?::\d{1,5})?)?$/.test(value.trim());
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1045
1596
|
async function runHangupCall() {
|
|
1046
1597
|
if (!selectedRow.value) return;
|
|
1047
1598
|
hangingUp.value = true;
|
|
@@ -1061,6 +1612,52 @@ async function runHangupCall() {
|
|
|
1061
1612
|
}
|
|
1062
1613
|
}
|
|
1063
1614
|
|
|
1615
|
+
async function testSipConnection(row: IntercomRow) {
|
|
1616
|
+
const readerId = toText(row.reader._id);
|
|
1617
|
+
if (!readerId || testingSipReaderId.value) return;
|
|
1618
|
+
|
|
1619
|
+
testingSipReaderId.value = readerId;
|
|
1620
|
+
try {
|
|
1621
|
+
const response = await getIntercomStatus(readerId);
|
|
1622
|
+
const responseRecord = toRecord(response);
|
|
1623
|
+
const statusPayload = toRecord(responseRecord.data || responseRecord);
|
|
1624
|
+
const status = Number(statusPayload.status);
|
|
1625
|
+
|
|
1626
|
+
await reload();
|
|
1627
|
+
|
|
1628
|
+
if (status === 200) {
|
|
1629
|
+
showMessage(`${row.name}: SIP is connected.`, "success");
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (status === -1) {
|
|
1633
|
+
showMessage(`${row.name}: SIP is disabled on the HID reader.`, "warning");
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (status === 0 || status === 100) {
|
|
1637
|
+
showMessage(`${row.name}: SIP is still connecting. Check the SIP settings on the HID reader.`, "warning");
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (status === 401 || status === 403) {
|
|
1641
|
+
showMessage(`${row.name}: SIP authentication failed. Check the account configured on the HID reader.`, "error");
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (status === 408) {
|
|
1645
|
+
showMessage(`${row.name}: The HID reader cannot reach the SIP server.`, "error");
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (status === 503) {
|
|
1649
|
+
showMessage(`${row.name}: The HID reader reported a SIP network failure.`, "error");
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
showMessage(`${row.name}: SIP is not connected. Configure SIP on the HID reader and test again.`, "warning");
|
|
1654
|
+
} catch (error: unknown) {
|
|
1655
|
+
showMessage(getErrorMessage(error), "error");
|
|
1656
|
+
} finally {
|
|
1657
|
+
testingSipReaderId.value = "";
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1064
1661
|
async function savePjsip(readerId: string, payload: Record<string, string>, options: { showSuccessMessage?: boolean } = {}) {
|
|
1065
1662
|
saving.value = true;
|
|
1066
1663
|
try {
|
|
@@ -1078,36 +1675,17 @@ async function savePjsip(readerId: string, payload: Record<string, string>, opti
|
|
|
1078
1675
|
}
|
|
1079
1676
|
}
|
|
1080
1677
|
|
|
1081
|
-
function startRebootWait() {
|
|
1082
|
-
rebootConfirmDialog.value = false;
|
|
1083
|
-
rebootCountdown.value = 59;
|
|
1084
|
-
rebootingDialog.value = true;
|
|
1085
|
-
clearRebootTimer();
|
|
1086
|
-
|
|
1087
|
-
rebootTimer = setInterval(() => {
|
|
1088
|
-
rebootCountdown.value -= 1;
|
|
1089
|
-
if (rebootCountdown.value <= 0) {
|
|
1090
|
-
clearRebootTimer();
|
|
1091
|
-
rebootingDialog.value = false;
|
|
1092
|
-
showMessage("Equipment reboot wait completed.", "success");
|
|
1093
|
-
reload();
|
|
1094
|
-
}
|
|
1095
|
-
}, 1000);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function clearRebootTimer() {
|
|
1099
|
-
if (rebootTimer) {
|
|
1100
|
-
clearInterval(rebootTimer);
|
|
1101
|
-
rebootTimer = undefined;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
1678
|
async function saveContact() {
|
|
1106
1679
|
if (!contactForm.readerId || !contactForm.name.trim() || !contactForm.serverAddress.trim()) {
|
|
1107
1680
|
showMessage("Please complete the required contact fields.", "warning");
|
|
1108
1681
|
return;
|
|
1109
1682
|
}
|
|
1110
1683
|
|
|
1684
|
+
if (!isSipCallTarget(contactForm.serverAddress)) {
|
|
1685
|
+
showMessage("Enter a valid extension, SIP address, or IPv4 address.", "warning");
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1111
1689
|
contactsSaving.value = true;
|
|
1112
1690
|
try {
|
|
1113
1691
|
if (contactForm.id) {
|
|
@@ -1159,13 +1737,6 @@ async function deleteContact() {
|
|
|
1159
1737
|
}
|
|
1160
1738
|
}
|
|
1161
1739
|
|
|
1162
|
-
function getStatusClass(status: string) {
|
|
1163
|
-
if (status === "Connected") return "status-success";
|
|
1164
|
-
if (status === "Disabled" || status === "Not connected") return "status-muted";
|
|
1165
|
-
if (status === "Failed") return "status-error";
|
|
1166
|
-
return "status-warning";
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
1740
|
function getDialModeLabel(value: string) {
|
|
1170
1741
|
if (value === "1") return "From the contact list";
|
|
1171
1742
|
if (value === "2") return "Numeric Keypad + Contacts";
|
|
@@ -1182,8 +1753,42 @@ function showMessage(text: string, type: MessageType = "info") {
|
|
|
1182
1753
|
message.type = type;
|
|
1183
1754
|
}
|
|
1184
1755
|
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1756
|
+
function normalizeSipAccount(response: unknown): SipAccount {
|
|
1757
|
+
const responseRecord = toRecord(response);
|
|
1758
|
+
const payload = toRecord(responseRecord.data || responseRecord);
|
|
1759
|
+
const requiredFields = ["extension", "username", "password", "sipAddress", "websocketUrl"] as const;
|
|
1760
|
+
|
|
1761
|
+
if (requiredFields.some((field) => !toText(payload[field]))) {
|
|
1762
|
+
throw new Error("The SIP account response is incomplete. Please try again.");
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
return {
|
|
1766
|
+
orgId: toText(payload.orgId, props.org),
|
|
1767
|
+
site: toText(payload.site, props.site),
|
|
1768
|
+
userId: toText(payload.userId),
|
|
1769
|
+
extension: toText(payload.extension),
|
|
1770
|
+
username: toText(payload.username),
|
|
1771
|
+
password: toText(payload.password),
|
|
1772
|
+
sipAddress: toText(payload.sipAddress),
|
|
1773
|
+
websocketUrl: toText(payload.websocketUrl),
|
|
1774
|
+
status: "active",
|
|
1775
|
+
provisionedAt: toText(payload.provisionedAt),
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function getErrorMessage(error: unknown) {
|
|
1780
|
+
const errorRecord = toRecord(error);
|
|
1781
|
+
const data = toRecord(errorRecord.data);
|
|
1782
|
+
const response = toRecord(errorRecord.response);
|
|
1783
|
+
const responseData = toRecord(response._data);
|
|
1784
|
+
return toText(data.message)
|
|
1785
|
+
|| toText(responseData.message)
|
|
1786
|
+
|| toText(errorRecord.message)
|
|
1787
|
+
|| "HID intercom request failed.";
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
1791
|
+
return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
|
|
1187
1792
|
}
|
|
1188
1793
|
|
|
1189
1794
|
function toText(value: unknown, fallback = "") {
|
|
@@ -1225,7 +1830,7 @@ export default {
|
|
|
1225
1830
|
<span>{{ label }}</span>
|
|
1226
1831
|
<v-switch
|
|
1227
1832
|
:model-value="modelValue"
|
|
1228
|
-
|
|
1833
|
+
class="app-switch"
|
|
1229
1834
|
density="compact"
|
|
1230
1835
|
hide-details
|
|
1231
1836
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
@@ -1244,116 +1849,66 @@ export default {
|
|
|
1244
1849
|
padding: 12px;
|
|
1245
1850
|
}
|
|
1246
1851
|
|
|
1247
|
-
.top-bar {
|
|
1248
|
-
display: flex;
|
|
1249
|
-
align-items: center;
|
|
1250
|
-
gap: 12px;
|
|
1251
|
-
margin-bottom: 12px;
|
|
1252
|
-
}
|
|
1253
1852
|
|
|
1254
1853
|
.toolbar-spacer {
|
|
1255
1854
|
flex: 1 1 auto;
|
|
1256
1855
|
}
|
|
1257
1856
|
|
|
1258
|
-
.top-placeholder {
|
|
1259
|
-
width: 160px;
|
|
1260
|
-
}
|
|
1261
1857
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1858
|
+
/*
|
|
1859
|
+
The filter row sits INSIDE the card's extension, so it needs the card's own
|
|
1860
|
+
side padding rather than the row's default bottom margin - same block as
|
|
1861
|
+
panels 2, 4 and 5.
|
|
1862
|
+
*/
|
|
1863
|
+
.hid-intercom-filters {
|
|
1864
|
+
margin: 0;
|
|
1865
|
+
padding: 10px 14px;
|
|
1267
1866
|
}
|
|
1268
1867
|
|
|
1269
|
-
.
|
|
1270
|
-
max-width:
|
|
1868
|
+
.hid-intercom-filters :deep(.app-field) {
|
|
1869
|
+
max-width: 240px;
|
|
1271
1870
|
}
|
|
1272
1871
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1872
|
+
/* The reader option now reads "Reader - Portal", so it needs more room than
|
|
1873
|
+
the other filters. `AppSelect`'s root class IS `app-field`, so this has to
|
|
1874
|
+
be more specific than the rule above rather than a `.app-select` one. */
|
|
1875
|
+
.hid-intercom-filters :deep(.app-field.reader-select) {
|
|
1876
|
+
max-width: 320px;
|
|
1275
1877
|
}
|
|
1276
1878
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1879
|
+
/*
|
|
1880
|
+
The web-call button is the card's PRIMARY action, so its resting state is
|
|
1881
|
+
`AppButton`'s own accent - it was a flat `#e5e7eb` grey, which read as
|
|
1882
|
+
disabled next to every other screen's primary. Only the two STATE colours
|
|
1883
|
+
are the screen's own, because the handoff draws no such states.
|
|
1884
|
+
*/
|
|
1885
|
+
.web-call-button {
|
|
1886
|
+
min-width: 160px;
|
|
1285
1887
|
}
|
|
1286
1888
|
|
|
1287
|
-
.
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
min-height: 42px;
|
|
1291
|
-
padding: 0 12px;
|
|
1292
|
-
background: #f6f6f6;
|
|
1293
|
-
color: #0b2c40;
|
|
1294
|
-
font-size: 12px;
|
|
1889
|
+
.web-call-button.is-connected {
|
|
1890
|
+
background: var(--ok);
|
|
1891
|
+
color: var(--on-accent-strong);
|
|
1295
1892
|
}
|
|
1296
1893
|
|
|
1297
|
-
.
|
|
1298
|
-
|
|
1894
|
+
.web-call-button.has-incoming-call {
|
|
1895
|
+
background: var(--warn);
|
|
1896
|
+
color: var(--on-accent-strong);
|
|
1299
1897
|
}
|
|
1300
1898
|
|
|
1301
|
-
.intercom-tabs :deep(.v-tab) {
|
|
1302
|
-
flex: 1 1 0;
|
|
1303
|
-
min-width: 0;
|
|
1304
|
-
font-size: 12px;
|
|
1305
|
-
text-transform: none;
|
|
1306
|
-
}
|
|
1307
1899
|
|
|
1308
|
-
.intercom-tabs :deep(.v-tab--selected) {
|
|
1309
|
-
color: #0b4ea2;
|
|
1310
|
-
}
|
|
1311
1900
|
|
|
1312
|
-
.table-body {
|
|
1313
|
-
position: relative;
|
|
1314
|
-
flex: 1 1 auto;
|
|
1315
|
-
min-height: 0;
|
|
1316
|
-
overflow: auto;
|
|
1317
|
-
}
|
|
1318
1901
|
|
|
1319
|
-
.table-body :deep(table) {
|
|
1320
|
-
table-layout: fixed;
|
|
1321
|
-
min-width: 840px;
|
|
1322
|
-
}
|
|
1323
1902
|
|
|
1324
|
-
.table-body :deep(th) {
|
|
1325
|
-
height: 48px;
|
|
1326
|
-
color: #0b2c40;
|
|
1327
|
-
font-size: 12px;
|
|
1328
|
-
font-weight: 500;
|
|
1329
|
-
}
|
|
1330
1903
|
|
|
1331
|
-
.table-body :deep(td) {
|
|
1332
|
-
height: 54px;
|
|
1333
|
-
color: #1f2937;
|
|
1334
|
-
font-size: 12px;
|
|
1335
|
-
}
|
|
1336
1904
|
|
|
1337
|
-
.table-footer {
|
|
1338
|
-
display: flex;
|
|
1339
|
-
flex: 0 0 auto;
|
|
1340
|
-
align-items: center;
|
|
1341
|
-
justify-content: flex-end;
|
|
1342
|
-
gap: 4px;
|
|
1343
|
-
min-height: 42px;
|
|
1344
|
-
padding: 0 12px;
|
|
1345
|
-
background: #f6f6f6;
|
|
1346
|
-
color: #0b2c40;
|
|
1347
|
-
font-size: 12px;
|
|
1348
|
-
}
|
|
1349
1905
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
|
|
1357
1912
|
|
|
1358
1913
|
.row-menu {
|
|
1359
1914
|
min-width: 160px;
|
|
@@ -1361,37 +1916,21 @@ export default {
|
|
|
1361
1916
|
}
|
|
1362
1917
|
|
|
1363
1918
|
.link-text {
|
|
1364
|
-
color:
|
|
1919
|
+
color: var(--accent-text);
|
|
1365
1920
|
}
|
|
1366
1921
|
|
|
1367
1922
|
.success-text {
|
|
1368
|
-
color:
|
|
1923
|
+
color: var(--ok);
|
|
1369
1924
|
}
|
|
1370
1925
|
|
|
1371
|
-
.status-success {
|
|
1372
|
-
background: #46b85a;
|
|
1373
|
-
color: #fff;
|
|
1374
|
-
}
|
|
1375
1926
|
|
|
1376
|
-
.status-muted {
|
|
1377
|
-
background: #a7a7a7;
|
|
1378
|
-
color: #fff;
|
|
1379
|
-
}
|
|
1380
1927
|
|
|
1381
|
-
.status-warning {
|
|
1382
|
-
background: #fff2cc;
|
|
1383
|
-
color: #8a5b00;
|
|
1384
|
-
}
|
|
1385
1928
|
|
|
1386
|
-
.status-error {
|
|
1387
|
-
background: #f9d0d0;
|
|
1388
|
-
color: #991b1b;
|
|
1389
|
-
}
|
|
1390
1929
|
|
|
1391
1930
|
.form-dialog,
|
|
1392
1931
|
.detail-dialog {
|
|
1393
1932
|
overflow: hidden;
|
|
1394
|
-
background:
|
|
1933
|
+
background: var(--card);
|
|
1395
1934
|
}
|
|
1396
1935
|
|
|
1397
1936
|
.sip-settings-dialog {
|
|
@@ -1402,8 +1941,8 @@ export default {
|
|
|
1402
1941
|
.dialog-title {
|
|
1403
1942
|
min-height: 48px;
|
|
1404
1943
|
padding: 14px 16px;
|
|
1405
|
-
background:
|
|
1406
|
-
color:
|
|
1944
|
+
background: var(--thead);
|
|
1945
|
+
color: var(--text);
|
|
1407
1946
|
font-size: 14px;
|
|
1408
1947
|
font-weight: 700;
|
|
1409
1948
|
}
|
|
@@ -1424,13 +1963,13 @@ export default {
|
|
|
1424
1963
|
|
|
1425
1964
|
.field-label {
|
|
1426
1965
|
margin: 12px 0 7px;
|
|
1427
|
-
color:
|
|
1966
|
+
color: var(--text2);
|
|
1428
1967
|
font-size: 10px;
|
|
1429
1968
|
font-weight: 500;
|
|
1430
1969
|
}
|
|
1431
1970
|
|
|
1432
1971
|
.field-label span {
|
|
1433
|
-
color:
|
|
1972
|
+
color: var(--err);
|
|
1434
1973
|
}
|
|
1435
1974
|
|
|
1436
1975
|
.sip-settings-dialog .field-label {
|
|
@@ -1439,14 +1978,14 @@ export default {
|
|
|
1439
1978
|
|
|
1440
1979
|
.sip-label {
|
|
1441
1980
|
margin: 12px 0 6px;
|
|
1442
|
-
color:
|
|
1981
|
+
color: var(--text);
|
|
1443
1982
|
font-size: 10px;
|
|
1444
1983
|
font-weight: 500;
|
|
1445
1984
|
line-height: 1.2;
|
|
1446
1985
|
}
|
|
1447
1986
|
|
|
1448
1987
|
.sip-label span {
|
|
1449
|
-
color:
|
|
1988
|
+
color: var(--err);
|
|
1450
1989
|
}
|
|
1451
1990
|
|
|
1452
1991
|
.sip-switch-row {
|
|
@@ -1456,10 +1995,10 @@ export default {
|
|
|
1456
1995
|
min-height: 40px;
|
|
1457
1996
|
margin-bottom: 12px;
|
|
1458
1997
|
padding: 0 12px;
|
|
1459
|
-
border: 1px solid
|
|
1998
|
+
border: 1px solid var(--border);
|
|
1460
1999
|
border-radius: 3px;
|
|
1461
|
-
background:
|
|
1462
|
-
color:
|
|
2000
|
+
background: var(--card);
|
|
2001
|
+
color: var(--text);
|
|
1463
2002
|
font-size: 12px;
|
|
1464
2003
|
}
|
|
1465
2004
|
|
|
@@ -1468,39 +2007,26 @@ export default {
|
|
|
1468
2007
|
margin-bottom: 12px;
|
|
1469
2008
|
}
|
|
1470
2009
|
|
|
1471
|
-
.sip-
|
|
1472
|
-
|
|
2010
|
+
.sip-section-switch > div {
|
|
2011
|
+
display: flex;
|
|
2012
|
+
min-width: 0;
|
|
2013
|
+
padding: 8px 12px 8px 0;
|
|
2014
|
+
flex-direction: column;
|
|
2015
|
+
gap: 2px;
|
|
1473
2016
|
}
|
|
1474
2017
|
|
|
1475
|
-
.sip-switch
|
|
1476
|
-
|
|
2018
|
+
.sip-section-switch small {
|
|
2019
|
+
color: var(--muted);
|
|
2020
|
+
font-size: 10px;
|
|
2021
|
+
line-height: 1.4;
|
|
1477
2022
|
}
|
|
1478
2023
|
|
|
1479
|
-
.sip-switch-row :deep(.v-selection-control__wrapper) {
|
|
1480
|
-
height: 28px;
|
|
1481
|
-
}
|
|
1482
2024
|
|
|
1483
|
-
.sip-switch-row :deep(.v-switch__track) {
|
|
1484
|
-
height: 24px;
|
|
1485
|
-
min-width: 42px;
|
|
1486
|
-
opacity: 1;
|
|
1487
|
-
}
|
|
1488
2025
|
|
|
1489
|
-
.sip-switch-row :deep(.v-switch__thumb) {
|
|
1490
|
-
background-color: #fff !important;
|
|
1491
|
-
color: #fff !important;
|
|
1492
|
-
height: 20px;
|
|
1493
|
-
width: 20px;
|
|
1494
|
-
}
|
|
1495
2026
|
|
|
1496
|
-
.sip-switch-row :deep(.v-selection-control--dirty .v-switch__thumb) {
|
|
1497
|
-
background-color: #fff !important;
|
|
1498
|
-
color: #fff !important;
|
|
1499
|
-
}
|
|
1500
2027
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
2028
|
+
|
|
2029
|
+
|
|
1504
2030
|
|
|
1505
2031
|
.toggle-field {
|
|
1506
2032
|
display: flex;
|
|
@@ -1509,27 +2035,27 @@ export default {
|
|
|
1509
2035
|
min-height: 38px;
|
|
1510
2036
|
margin-bottom: 8px;
|
|
1511
2037
|
padding: 0 10px;
|
|
1512
|
-
border: 1px solid
|
|
2038
|
+
border: 1px solid var(--border);
|
|
1513
2039
|
border-radius: 3px;
|
|
1514
|
-
color:
|
|
2040
|
+
color: var(--text2);
|
|
1515
2041
|
font-size: 12px;
|
|
1516
2042
|
}
|
|
1517
2043
|
|
|
1518
2044
|
.sip-input :deep(.v-field) {
|
|
1519
2045
|
min-height: 40px;
|
|
1520
2046
|
border-radius: 3px;
|
|
1521
|
-
background:
|
|
2047
|
+
background: var(--card);
|
|
1522
2048
|
font-size: 12px;
|
|
1523
2049
|
box-shadow: none;
|
|
1524
2050
|
}
|
|
1525
2051
|
|
|
1526
2052
|
.sip-input :deep(.v-field__outline) {
|
|
1527
2053
|
--v-field-border-opacity: 1;
|
|
1528
|
-
color:
|
|
2054
|
+
color: var(--border);
|
|
1529
2055
|
}
|
|
1530
2056
|
|
|
1531
2057
|
.sip-input :deep(.v-field--focused .v-field__outline) {
|
|
1532
|
-
color:
|
|
2058
|
+
color: var(--border);
|
|
1533
2059
|
}
|
|
1534
2060
|
|
|
1535
2061
|
.sip-input :deep(.v-field__outline__start),
|
|
@@ -1542,28 +2068,28 @@ export default {
|
|
|
1542
2068
|
padding-top: 0;
|
|
1543
2069
|
padding-bottom: 0;
|
|
1544
2070
|
padding-inline: 12px;
|
|
1545
|
-
color:
|
|
2071
|
+
color: var(--text2);
|
|
1546
2072
|
font-size: 12px;
|
|
1547
2073
|
}
|
|
1548
2074
|
|
|
1549
2075
|
.sip-input :deep(.v-field__append-inner) {
|
|
1550
2076
|
min-height: 40px;
|
|
1551
|
-
color:
|
|
2077
|
+
color: var(--muted);
|
|
1552
2078
|
}
|
|
1553
2079
|
|
|
1554
2080
|
.sip-input :deep(.v-field--disabled) {
|
|
1555
|
-
background:
|
|
2081
|
+
background: var(--hover);
|
|
1556
2082
|
opacity: 1;
|
|
1557
2083
|
}
|
|
1558
2084
|
|
|
1559
2085
|
.sip-input :deep(.v-field--disabled .v-field__input),
|
|
1560
2086
|
.sip-input :deep(.v-field--disabled input::placeholder) {
|
|
1561
|
-
color:
|
|
2087
|
+
color: var(--muted);
|
|
1562
2088
|
opacity: 1;
|
|
1563
2089
|
}
|
|
1564
2090
|
|
|
1565
2091
|
.sip-input :deep(input::placeholder) {
|
|
1566
|
-
color:
|
|
2092
|
+
color: var(--muted);
|
|
1567
2093
|
opacity: 1;
|
|
1568
2094
|
}
|
|
1569
2095
|
|
|
@@ -1578,7 +2104,7 @@ export default {
|
|
|
1578
2104
|
width: 100%;
|
|
1579
2105
|
grid-template-columns: 1fr 1fr;
|
|
1580
2106
|
margin-bottom: 6px;
|
|
1581
|
-
border: 1px solid
|
|
2107
|
+
border: 1px solid var(--border);
|
|
1582
2108
|
border-radius: 3px;
|
|
1583
2109
|
overflow: hidden;
|
|
1584
2110
|
}
|
|
@@ -1587,20 +2113,20 @@ export default {
|
|
|
1587
2113
|
min-width: 0;
|
|
1588
2114
|
min-height: 40px;
|
|
1589
2115
|
border-radius: 0;
|
|
1590
|
-
color:
|
|
2116
|
+
color: var(--muted);
|
|
1591
2117
|
font-size: 12px;
|
|
1592
2118
|
font-weight: 400;
|
|
1593
2119
|
letter-spacing: 0;
|
|
1594
2120
|
}
|
|
1595
2121
|
|
|
1596
2122
|
.mode-toggle :deep(.v-btn--active) {
|
|
1597
|
-
background:
|
|
1598
|
-
color:
|
|
2123
|
+
background: var(--text);
|
|
2124
|
+
color: var(--card);
|
|
1599
2125
|
}
|
|
1600
2126
|
|
|
1601
2127
|
.mode-toggle :deep(.v-btn--disabled) {
|
|
1602
|
-
background:
|
|
1603
|
-
color:
|
|
2128
|
+
background: var(--hover);
|
|
2129
|
+
color: var(--muted);
|
|
1604
2130
|
opacity: 1;
|
|
1605
2131
|
}
|
|
1606
2132
|
|
|
@@ -1608,13 +2134,9 @@ export default {
|
|
|
1608
2134
|
margin-top: 0;
|
|
1609
2135
|
}
|
|
1610
2136
|
|
|
1611
|
-
.sip-settings-dialog :deep(.v-selection-control-group--inline) {
|
|
1612
|
-
gap: 34px;
|
|
1613
|
-
min-height: 34px;
|
|
1614
|
-
}
|
|
1615
2137
|
|
|
1616
2138
|
.sip-settings-dialog :deep(.v-radio .v-label) {
|
|
1617
|
-
color:
|
|
2139
|
+
color: var(--text2);
|
|
1618
2140
|
font-size: 12px;
|
|
1619
2141
|
}
|
|
1620
2142
|
|
|
@@ -1630,112 +2152,20 @@ export default {
|
|
|
1630
2152
|
overflow: hidden;
|
|
1631
2153
|
}
|
|
1632
2154
|
|
|
1633
|
-
.reboot-dialog {
|
|
1634
|
-
position: relative;
|
|
1635
|
-
min-height: 220px;
|
|
1636
|
-
overflow: hidden;
|
|
1637
|
-
background: #fff;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
.reboot-close {
|
|
1641
|
-
position: absolute;
|
|
1642
|
-
top: 14px;
|
|
1643
|
-
right: 14px;
|
|
1644
|
-
display: grid;
|
|
1645
|
-
width: 40px;
|
|
1646
|
-
height: 40px;
|
|
1647
|
-
padding: 0;
|
|
1648
|
-
border: 0;
|
|
1649
|
-
place-items: center;
|
|
1650
|
-
background: transparent;
|
|
1651
|
-
color: #c7c7c7;
|
|
1652
|
-
cursor: pointer;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
.reboot-title {
|
|
1656
|
-
padding: 24px 20px 12px;
|
|
1657
|
-
color: #111;
|
|
1658
|
-
font-size: 28px;
|
|
1659
|
-
font-weight: 400;
|
|
1660
|
-
letter-spacing: 0;
|
|
1661
|
-
line-height: 1.2;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
.reboot-message {
|
|
1665
|
-
padding: 24px 20px 28px;
|
|
1666
|
-
color: #222;
|
|
1667
|
-
font-size: 17px;
|
|
1668
|
-
line-height: 1.45;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
.reboot-actions {
|
|
1672
|
-
display: flex;
|
|
1673
|
-
justify-content: flex-end;
|
|
1674
|
-
gap: 20px;
|
|
1675
|
-
padding: 0 20px 20px;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
.reboot-cancel {
|
|
1679
|
-
color: #075fb7;
|
|
1680
|
-
font-size: 16px;
|
|
1681
|
-
letter-spacing: 0;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
.reboot-ok {
|
|
1685
|
-
min-width: 64px;
|
|
1686
|
-
border-radius: 5px;
|
|
1687
|
-
background: #062f4d;
|
|
1688
|
-
color: #fff;
|
|
1689
|
-
font-size: 16px;
|
|
1690
|
-
letter-spacing: 0;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
.rebooting-card {
|
|
1694
|
-
display: grid;
|
|
1695
|
-
min-height: 240px;
|
|
1696
|
-
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
|
|
1697
|
-
align-items: center;
|
|
1698
|
-
gap: 24px;
|
|
1699
|
-
padding: 20px 54px;
|
|
1700
|
-
background: #fff;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
.wait-word {
|
|
1704
|
-
color: #347bd6;
|
|
1705
|
-
font-size: 120px;
|
|
1706
|
-
font-weight: 400;
|
|
1707
|
-
letter-spacing: 0;
|
|
1708
|
-
line-height: 0.95;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
.rebooting-copy h3 {
|
|
1712
|
-
margin: 0 0 18px;
|
|
1713
|
-
color: #000;
|
|
1714
|
-
font-size: 30px;
|
|
1715
|
-
font-weight: 400;
|
|
1716
|
-
letter-spacing: 0;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
.rebooting-copy p {
|
|
1720
|
-
margin: 0;
|
|
1721
|
-
color: #000;
|
|
1722
|
-
font-size: 16px;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
2155
|
.call-dialog .dialog-body {
|
|
1726
2156
|
padding: 18px 24px 24px;
|
|
1727
2157
|
}
|
|
1728
2158
|
|
|
1729
2159
|
.call-label {
|
|
1730
2160
|
margin: 8px 0 6px;
|
|
1731
|
-
color:
|
|
2161
|
+
color: var(--text);
|
|
1732
2162
|
font-size: 10px;
|
|
1733
2163
|
font-weight: 500;
|
|
1734
2164
|
letter-spacing: 0;
|
|
1735
2165
|
}
|
|
1736
2166
|
|
|
1737
2167
|
.call-label span {
|
|
1738
|
-
color:
|
|
2168
|
+
color: var(--err);
|
|
1739
2169
|
}
|
|
1740
2170
|
|
|
1741
2171
|
.call-radio-list {
|
|
@@ -1746,29 +2176,12 @@ export default {
|
|
|
1746
2176
|
width: 100%;
|
|
1747
2177
|
}
|
|
1748
2178
|
|
|
1749
|
-
.call-radio-list :deep(.v-selection-control-group) {
|
|
1750
|
-
gap: 8px;
|
|
1751
|
-
}
|
|
1752
2179
|
|
|
1753
|
-
.call-radio-list :deep(.v-selection-control) {
|
|
1754
|
-
min-height: 36px;
|
|
1755
|
-
margin-bottom: 0;
|
|
1756
|
-
padding: 0 10px;
|
|
1757
|
-
border: 1px solid #dfe3e8;
|
|
1758
|
-
border-radius: 3px;
|
|
1759
|
-
background: #fff;
|
|
1760
|
-
}
|
|
1761
2180
|
|
|
1762
|
-
.call-radio-list :deep(.v-selection-control__wrapper) {
|
|
1763
|
-
width: 24px;
|
|
1764
|
-
}
|
|
1765
2181
|
|
|
1766
|
-
.call-radio-list :deep(.v-selection-control__input) {
|
|
1767
|
-
transform: scale(0.9);
|
|
1768
|
-
}
|
|
1769
2182
|
|
|
1770
2183
|
.call-radio-list :deep(.v-label) {
|
|
1771
|
-
color:
|
|
2184
|
+
color: var(--text2);
|
|
1772
2185
|
font-size: 11px;
|
|
1773
2186
|
letter-spacing: 0;
|
|
1774
2187
|
opacity: 1;
|
|
@@ -1781,34 +2194,17 @@ export default {
|
|
|
1781
2194
|
min-height: 36px;
|
|
1782
2195
|
margin-bottom: 10px;
|
|
1783
2196
|
padding: 0 10px;
|
|
1784
|
-
border: 1px solid
|
|
2197
|
+
border: 1px solid var(--border);
|
|
1785
2198
|
border-radius: 3px;
|
|
1786
|
-
background:
|
|
1787
|
-
color:
|
|
2199
|
+
background: var(--card);
|
|
2200
|
+
color: var(--text);
|
|
1788
2201
|
font-size: 11px;
|
|
1789
2202
|
letter-spacing: 0;
|
|
1790
2203
|
}
|
|
1791
2204
|
|
|
1792
|
-
.call-toggle-row :deep(.v-selection-control) {
|
|
1793
|
-
min-height: 28px;
|
|
1794
|
-
}
|
|
1795
2205
|
|
|
1796
|
-
.call-toggle-row :deep(.v-selection-control__wrapper) {
|
|
1797
|
-
height: 28px;
|
|
1798
|
-
}
|
|
1799
2206
|
|
|
1800
|
-
.call-toggle-row :deep(.v-switch__track) {
|
|
1801
|
-
height: 22px;
|
|
1802
|
-
min-width: 40px;
|
|
1803
|
-
opacity: 1;
|
|
1804
|
-
}
|
|
1805
2207
|
|
|
1806
|
-
.call-toggle-row :deep(.v-switch__thumb) {
|
|
1807
|
-
width: 18px;
|
|
1808
|
-
height: 18px;
|
|
1809
|
-
background-color: #fff !important;
|
|
1810
|
-
color: #fff !important;
|
|
1811
|
-
}
|
|
1812
2208
|
|
|
1813
2209
|
.call-input {
|
|
1814
2210
|
margin-bottom: 8px;
|
|
@@ -1817,13 +2213,13 @@ export default {
|
|
|
1817
2213
|
.call-input :deep(.v-field) {
|
|
1818
2214
|
min-height: 38px;
|
|
1819
2215
|
border-radius: 3px;
|
|
1820
|
-
background:
|
|
2216
|
+
background: var(--card);
|
|
1821
2217
|
box-shadow: none;
|
|
1822
2218
|
}
|
|
1823
2219
|
|
|
1824
2220
|
.call-input :deep(.v-field__outline) {
|
|
1825
2221
|
--v-field-border-opacity: 1;
|
|
1826
|
-
color:
|
|
2222
|
+
color: var(--border);
|
|
1827
2223
|
}
|
|
1828
2224
|
|
|
1829
2225
|
.call-input :deep(.v-field__input) {
|
|
@@ -1831,19 +2227,19 @@ export default {
|
|
|
1831
2227
|
padding-top: 0;
|
|
1832
2228
|
padding-bottom: 0;
|
|
1833
2229
|
padding-inline: 12px;
|
|
1834
|
-
color:
|
|
2230
|
+
color: var(--text2);
|
|
1835
2231
|
font-size: 12px;
|
|
1836
2232
|
letter-spacing: 0;
|
|
1837
2233
|
}
|
|
1838
2234
|
|
|
1839
2235
|
.call-input :deep(.v-field--disabled) {
|
|
1840
|
-
background:
|
|
2236
|
+
background: var(--hover);
|
|
1841
2237
|
opacity: 1;
|
|
1842
2238
|
}
|
|
1843
2239
|
|
|
1844
2240
|
.call-input :deep(.v-field--disabled .v-field__input),
|
|
1845
2241
|
.call-input :deep(.v-field--disabled input::placeholder) {
|
|
1846
|
-
color:
|
|
2242
|
+
color: var(--muted);
|
|
1847
2243
|
opacity: 1;
|
|
1848
2244
|
}
|
|
1849
2245
|
|
|
@@ -1885,17 +2281,17 @@ export default {
|
|
|
1885
2281
|
.make-call-danger {
|
|
1886
2282
|
min-height: 46px;
|
|
1887
2283
|
border-radius: 0;
|
|
1888
|
-
color:
|
|
2284
|
+
color: var(--card);
|
|
1889
2285
|
font-size: 14px;
|
|
1890
2286
|
letter-spacing: 0;
|
|
1891
2287
|
}
|
|
1892
2288
|
|
|
1893
2289
|
.make-call-primary {
|
|
1894
|
-
background:
|
|
2290
|
+
background: var(--text);
|
|
1895
2291
|
}
|
|
1896
2292
|
|
|
1897
2293
|
.make-call-danger {
|
|
1898
|
-
background:
|
|
2294
|
+
background: var(--err);
|
|
1899
2295
|
}
|
|
1900
2296
|
|
|
1901
2297
|
.make-call-status {
|
|
@@ -1906,31 +2302,299 @@ export default {
|
|
|
1906
2302
|
}
|
|
1907
2303
|
|
|
1908
2304
|
.make-call-status.is-success {
|
|
1909
|
-
background:
|
|
1910
|
-
color:
|
|
2305
|
+
background: var(--ok-bg);
|
|
2306
|
+
color: var(--ok);
|
|
1911
2307
|
}
|
|
1912
2308
|
|
|
1913
2309
|
.make-call-status.is-warning {
|
|
1914
|
-
background:
|
|
1915
|
-
color:
|
|
2310
|
+
background: var(--warn-bg);
|
|
2311
|
+
color: var(--warn);
|
|
1916
2312
|
}
|
|
1917
2313
|
|
|
1918
2314
|
.make-call-status.is-error {
|
|
1919
|
-
background:
|
|
1920
|
-
color:
|
|
2315
|
+
background: var(--err-bg);
|
|
2316
|
+
color: var(--err);
|
|
1921
2317
|
}
|
|
1922
2318
|
|
|
1923
2319
|
.make-call-status.is-info {
|
|
1924
|
-
background:
|
|
1925
|
-
color:
|
|
2320
|
+
background: var(--accent-soft);
|
|
2321
|
+
color: var(--accent-text);
|
|
1926
2322
|
}
|
|
1927
2323
|
|
|
1928
2324
|
.make-call-note {
|
|
1929
|
-
color:
|
|
2325
|
+
color: var(--text2);
|
|
1930
2326
|
font-size: 13px;
|
|
1931
2327
|
line-height: 1.45;
|
|
1932
2328
|
}
|
|
1933
2329
|
|
|
2330
|
+
.web-phone-dialog {
|
|
2331
|
+
display: flex;
|
|
2332
|
+
width: 820px;
|
|
2333
|
+
max-height: min(90vh, 760px);
|
|
2334
|
+
max-width: 94vw;
|
|
2335
|
+
overflow: hidden;
|
|
2336
|
+
background: var(--card);
|
|
2337
|
+
flex-direction: column;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
.web-phone-title {
|
|
2341
|
+
flex: 0 0 auto;
|
|
2342
|
+
display: flex;
|
|
2343
|
+
align-items: center;
|
|
2344
|
+
justify-content: space-between;
|
|
2345
|
+
gap: 16px;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
.web-phone-title-actions {
|
|
2349
|
+
display: flex;
|
|
2350
|
+
align-items: center;
|
|
2351
|
+
gap: 8px;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
.web-phone-status {
|
|
2355
|
+
min-width: 92px;
|
|
2356
|
+
justify-content: center;
|
|
2357
|
+
font-size: 10px;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
.web-phone-status.ready {
|
|
2361
|
+
background: var(--ok-bg);
|
|
2362
|
+
color: var(--ok);
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
.web-phone-status.incoming {
|
|
2366
|
+
background: var(--accent-soft);
|
|
2367
|
+
color: var(--accent-text);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
.web-phone-status.failed {
|
|
2371
|
+
background: var(--err-bg);
|
|
2372
|
+
color: var(--err);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
.web-phone-status.idle {
|
|
2376
|
+
background: var(--border);
|
|
2377
|
+
color: var(--text2);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
.web-phone-body {
|
|
2381
|
+
display: grid;
|
|
2382
|
+
grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr);
|
|
2383
|
+
gap: 24px;
|
|
2384
|
+
min-height: 440px;
|
|
2385
|
+
overflow: auto;
|
|
2386
|
+
padding: 22px 24px 24px;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
.web-phone-settings {
|
|
2390
|
+
padding-right: 24px;
|
|
2391
|
+
border-right: 1px solid var(--border);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
.web-phone-section-title {
|
|
2395
|
+
margin-bottom: 14px;
|
|
2396
|
+
color: var(--text);
|
|
2397
|
+
font-size: 13px;
|
|
2398
|
+
font-weight: 700;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
.sip-account-state {
|
|
2402
|
+
display: flex;
|
|
2403
|
+
min-height: 126px;
|
|
2404
|
+
align-items: center;
|
|
2405
|
+
justify-content: center;
|
|
2406
|
+
padding: 18px;
|
|
2407
|
+
border: 1px solid var(--border);
|
|
2408
|
+
border-radius: 6px;
|
|
2409
|
+
color: var(--text2);
|
|
2410
|
+
flex-direction: column;
|
|
2411
|
+
font-size: 12px;
|
|
2412
|
+
gap: 12px;
|
|
2413
|
+
text-align: center;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
.sip-account-state.is-error {
|
|
2417
|
+
border-color: var(--err-bg);
|
|
2418
|
+
background: var(--err-bg);
|
|
2419
|
+
color: var(--err);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
.sip-account-retry {
|
|
2423
|
+
min-width: 104px;
|
|
2424
|
+
border-color: var(--muted);
|
|
2425
|
+
color: var(--text2);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
.sip-account-summary {
|
|
2429
|
+
display: grid;
|
|
2430
|
+
min-height: 96px;
|
|
2431
|
+
align-items: center;
|
|
2432
|
+
padding: 16px;
|
|
2433
|
+
border: 1px solid var(--border);
|
|
2434
|
+
border-radius: 6px;
|
|
2435
|
+
background: var(--hover);
|
|
2436
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2437
|
+
gap: 18px;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
.sip-account-summary > div {
|
|
2441
|
+
display: flex;
|
|
2442
|
+
min-width: 0;
|
|
2443
|
+
flex-direction: column;
|
|
2444
|
+
gap: 4px;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
.sip-account-label {
|
|
2448
|
+
color: var(--muted);
|
|
2449
|
+
font-size: 11px;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
.sip-account-extension {
|
|
2453
|
+
color: var(--text);
|
|
2454
|
+
font-size: 24px;
|
|
2455
|
+
line-height: 1.1;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
.sip-account-address {
|
|
2459
|
+
max-width: 170px;
|
|
2460
|
+
overflow: hidden;
|
|
2461
|
+
color: var(--text2);
|
|
2462
|
+
font-size: 11px;
|
|
2463
|
+
text-align: right;
|
|
2464
|
+
text-overflow: ellipsis;
|
|
2465
|
+
white-space: nowrap;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
.web-video-toggle {
|
|
2469
|
+
margin-top: 12px;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
.web-phone-connect {
|
|
2473
|
+
min-height: 42px;
|
|
2474
|
+
margin-top: 14px;
|
|
2475
|
+
background: var(--text);
|
|
2476
|
+
color: var(--card);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
.web-phone-disconnect {
|
|
2480
|
+
min-height: 42px;
|
|
2481
|
+
margin-top: 14px;
|
|
2482
|
+
border-color: var(--muted);
|
|
2483
|
+
color: var(--text2);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
.web-phone-console {
|
|
2487
|
+
display: flex;
|
|
2488
|
+
min-width: 0;
|
|
2489
|
+
flex-direction: column;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
.web-phone-media {
|
|
2493
|
+
position: relative;
|
|
2494
|
+
display: grid;
|
|
2495
|
+
min-height: 260px;
|
|
2496
|
+
overflow: hidden;
|
|
2497
|
+
place-items: center;
|
|
2498
|
+
border: 1px solid var(--border);
|
|
2499
|
+
border-radius: 6px;
|
|
2500
|
+
background: var(--text);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
.remote-video {
|
|
2504
|
+
display: none;
|
|
2505
|
+
width: 100%;
|
|
2506
|
+
height: 100%;
|
|
2507
|
+
object-fit: cover;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
.web-phone-media.has-remote-video .remote-video {
|
|
2511
|
+
display: block;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
.web-phone-media.has-remote-video .audio-call-state {
|
|
2515
|
+
display: none;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
.audio-call-state {
|
|
2519
|
+
display: flex;
|
|
2520
|
+
align-items: center;
|
|
2521
|
+
justify-content: center;
|
|
2522
|
+
padding: 28px;
|
|
2523
|
+
color: var(--card);
|
|
2524
|
+
flex-direction: column;
|
|
2525
|
+
gap: 10px;
|
|
2526
|
+
text-align: center;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
.audio-call-state strong {
|
|
2530
|
+
font-size: 16px;
|
|
2531
|
+
font-weight: 600;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
.audio-call-state span {
|
|
2535
|
+
max-width: 100%;
|
|
2536
|
+
overflow: hidden;
|
|
2537
|
+
color: var(--border);
|
|
2538
|
+
font-size: 11px;
|
|
2539
|
+
text-overflow: ellipsis;
|
|
2540
|
+
white-space: nowrap;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
.web-phone-dial-row {
|
|
2544
|
+
display: grid;
|
|
2545
|
+
grid-template-columns: minmax(0, 1fr) 42px;
|
|
2546
|
+
gap: 8px;
|
|
2547
|
+
align-items: start;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
.start-web-call,
|
|
2551
|
+
.hangup-web-call {
|
|
2552
|
+
width: 42px;
|
|
2553
|
+
height: 38px;
|
|
2554
|
+
border-radius: 4px;
|
|
2555
|
+
background: var(--ok);
|
|
2556
|
+
color: var(--card);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
.hangup-web-call,
|
|
2560
|
+
.reject-call {
|
|
2561
|
+
background: var(--err);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
.incoming-call-actions {
|
|
2565
|
+
display: grid;
|
|
2566
|
+
grid-template-columns: 1fr 1fr;
|
|
2567
|
+
gap: 10px;
|
|
2568
|
+
margin-top: 14px;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
.answer-call,
|
|
2572
|
+
.reject-call {
|
|
2573
|
+
min-height: 42px;
|
|
2574
|
+
color: var(--card);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
.answer-call {
|
|
2578
|
+
background: var(--ok);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
.active-call-controls {
|
|
2582
|
+
display: flex;
|
|
2583
|
+
justify-content: center;
|
|
2584
|
+
gap: 12px;
|
|
2585
|
+
margin-top: 16px;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
.active-call-controls .v-btn {
|
|
2589
|
+
width: 46px;
|
|
2590
|
+
height: 46px;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
.web-phone-error {
|
|
2594
|
+
margin-top: 14px;
|
|
2595
|
+
font-size: 11px;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
1934
2598
|
.call-two-col {
|
|
1935
2599
|
gap: 12px;
|
|
1936
2600
|
}
|
|
@@ -1953,28 +2617,28 @@ export default {
|
|
|
1953
2617
|
justify-content: center;
|
|
1954
2618
|
gap: 6px;
|
|
1955
2619
|
min-height: 38px;
|
|
1956
|
-
border: 1px dashed
|
|
1957
|
-
color:
|
|
2620
|
+
border: 1px dashed var(--border);
|
|
2621
|
+
color: var(--text2);
|
|
1958
2622
|
font-size: 11px;
|
|
1959
2623
|
letter-spacing: 0;
|
|
1960
2624
|
}
|
|
1961
2625
|
|
|
1962
2626
|
.upload-drop.is-disabled {
|
|
1963
|
-
background:
|
|
1964
|
-
color:
|
|
2627
|
+
background: var(--thead);
|
|
2628
|
+
color: var(--muted);
|
|
1965
2629
|
}
|
|
1966
2630
|
|
|
1967
2631
|
.upload-button {
|
|
1968
2632
|
width: 40px;
|
|
1969
2633
|
height: 38px;
|
|
1970
2634
|
border-radius: 3px;
|
|
1971
|
-
background:
|
|
1972
|
-
color:
|
|
2635
|
+
background: var(--accent-text);
|
|
2636
|
+
color: var(--card);
|
|
1973
2637
|
}
|
|
1974
2638
|
|
|
1975
2639
|
.upload-button.v-btn--disabled {
|
|
1976
|
-
background:
|
|
1977
|
-
color:
|
|
2640
|
+
background: var(--hover);
|
|
2641
|
+
color: var(--border);
|
|
1978
2642
|
opacity: 1;
|
|
1979
2643
|
}
|
|
1980
2644
|
|
|
@@ -1984,7 +2648,7 @@ export default {
|
|
|
1984
2648
|
justify-content: space-between;
|
|
1985
2649
|
min-height: 32px;
|
|
1986
2650
|
margin: 0 0 10px;
|
|
1987
|
-
color:
|
|
2651
|
+
color: var(--text2);
|
|
1988
2652
|
font-size: 11px;
|
|
1989
2653
|
}
|
|
1990
2654
|
|
|
@@ -1995,12 +2659,12 @@ export default {
|
|
|
1995
2659
|
}
|
|
1996
2660
|
|
|
1997
2661
|
.call-file-name .v-icon {
|
|
1998
|
-
color:
|
|
2662
|
+
color: var(--ok);
|
|
1999
2663
|
}
|
|
2000
2664
|
|
|
2001
2665
|
.detail-section-title {
|
|
2002
2666
|
margin: 14px 0 8px;
|
|
2003
|
-
color:
|
|
2667
|
+
color: var(--text);
|
|
2004
2668
|
font-size: 13px;
|
|
2005
2669
|
font-weight: 700;
|
|
2006
2670
|
}
|
|
@@ -2015,12 +2679,12 @@ export default {
|
|
|
2015
2679
|
justify-content: space-between;
|
|
2016
2680
|
gap: 16px;
|
|
2017
2681
|
min-height: 26px;
|
|
2018
|
-
color:
|
|
2682
|
+
color: var(--muted);
|
|
2019
2683
|
font-size: 11px;
|
|
2020
2684
|
}
|
|
2021
2685
|
|
|
2022
2686
|
.detail-row strong {
|
|
2023
|
-
color:
|
|
2687
|
+
color: var(--text);
|
|
2024
2688
|
font-weight: 500;
|
|
2025
2689
|
text-align: right;
|
|
2026
2690
|
}
|
|
@@ -2028,22 +2692,25 @@ export default {
|
|
|
2028
2692
|
.dialog-actions {
|
|
2029
2693
|
display: grid;
|
|
2030
2694
|
grid-template-columns: 1fr 1fr;
|
|
2031
|
-
border-top: 1px solid
|
|
2695
|
+
border-top: 1px solid var(--border);
|
|
2032
2696
|
}
|
|
2033
2697
|
|
|
2034
2698
|
.sip-settings-dialog .dialog-actions {
|
|
2035
2699
|
min-height: 46px;
|
|
2036
2700
|
}
|
|
2037
2701
|
|
|
2702
|
+
/* The split footer bar, as panels 2, 4 and 5 keep it: the SHAPE is the
|
|
2703
|
+
screen's own (the handoff draws no such bar); only the colours change. The
|
|
2704
|
+
navy was a fixed hex that sat on the dark card unchanged. */
|
|
2038
2705
|
.action-cancel {
|
|
2039
|
-
background:
|
|
2040
|
-
color:
|
|
2706
|
+
background: var(--card);
|
|
2707
|
+
color: var(--text2);
|
|
2041
2708
|
border-radius: 0;
|
|
2042
2709
|
}
|
|
2043
2710
|
|
|
2044
2711
|
.action-submit {
|
|
2045
|
-
background:
|
|
2046
|
-
color:
|
|
2712
|
+
background: var(--accent-strong);
|
|
2713
|
+
color: var(--on-accent-strong);
|
|
2047
2714
|
border-radius: 0;
|
|
2048
2715
|
}
|
|
2049
2716
|
|
|
@@ -2052,25 +2719,27 @@ export default {
|
|
|
2052
2719
|
}
|
|
2053
2720
|
|
|
2054
2721
|
@media (max-width: 900px) {
|
|
2055
|
-
.
|
|
2056
|
-
|
|
2057
|
-
|
|
2722
|
+
.hid-intercom-filters :deep(.app-field),
|
|
2723
|
+
.hid-intercom-filters :deep(.app-select) {
|
|
2724
|
+
width: 100%;
|
|
2725
|
+
max-width: none;
|
|
2058
2726
|
}
|
|
2059
2727
|
|
|
2060
|
-
.
|
|
2061
|
-
.search-input,
|
|
2062
|
-
.status-input {
|
|
2728
|
+
.web-call-button {
|
|
2063
2729
|
width: 100%;
|
|
2064
|
-
max-width: none;
|
|
2065
2730
|
}
|
|
2066
2731
|
|
|
2067
|
-
.
|
|
2068
|
-
grid-template-columns: 1fr;
|
|
2069
|
-
|
|
2732
|
+
.web-phone-body {
|
|
2733
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2734
|
+
min-height: 0;
|
|
2070
2735
|
}
|
|
2071
2736
|
|
|
2072
|
-
.
|
|
2073
|
-
|
|
2737
|
+
.web-phone-settings {
|
|
2738
|
+
padding-right: 0;
|
|
2739
|
+
padding-bottom: 20px;
|
|
2740
|
+
border-right: 0;
|
|
2741
|
+
border-bottom: 1px solid var(--border);
|
|
2074
2742
|
}
|
|
2743
|
+
|
|
2075
2744
|
}
|
|
2076
2745
|
</style>
|