@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -12,7 +12,17 @@
|
|
|
12
12
|
Add Contacts
|
|
13
13
|
</v-btn>
|
|
14
14
|
|
|
15
|
-
<
|
|
15
|
+
<v-btn
|
|
16
|
+
v-else
|
|
17
|
+
rounded="xl"
|
|
18
|
+
class="text-none web-call-button"
|
|
19
|
+
:class="{ 'is-connected': webPhone.isRegistered.value, 'has-incoming-call': webPhone.callState.value === 'incoming' }"
|
|
20
|
+
variant="flat"
|
|
21
|
+
prepend-icon="mdi-phone-in-talk-outline"
|
|
22
|
+
@click="openWebPhoneDialog()"
|
|
23
|
+
>
|
|
24
|
+
{{ webPhone.callState.value === 'incoming' ? 'Incoming Call' : 'Web Call' }}
|
|
25
|
+
</v-btn>
|
|
16
26
|
|
|
17
27
|
<div class="toolbar-spacer" />
|
|
18
28
|
|
|
@@ -105,6 +115,7 @@
|
|
|
105
115
|
<v-list-item title="Call Settings" @click="openCallDialog(row)" />
|
|
106
116
|
<v-list-item title="Dial Code" @click="openDialCodeDialog(row)" />
|
|
107
117
|
<v-list-item title="Make a call" @click="openMakeCallDialog(row)" />
|
|
118
|
+
<v-list-item title="Web call" @click="openWebPhoneDialog(row)" />
|
|
108
119
|
</v-list>
|
|
109
120
|
</v-menu>
|
|
110
121
|
</td>
|
|
@@ -223,6 +234,13 @@
|
|
|
223
234
|
<v-radio label="UDP" value="UDP" />
|
|
224
235
|
<v-radio label="TCP" value="TCP" />
|
|
225
236
|
</v-radio-group>
|
|
237
|
+
<div class="sip-switch-row sip-section-switch">
|
|
238
|
+
<div>
|
|
239
|
+
<span>Enable video</span>
|
|
240
|
+
<small>Allow this reader to send its camera feed during SIP intercom calls.</small>
|
|
241
|
+
</div>
|
|
242
|
+
<v-switch v-model="sipForm.videoEnabled" :disabled="!sipForm.enabled" color="success" density="compact" hide-details />
|
|
243
|
+
</div>
|
|
226
244
|
</v-card-text>
|
|
227
245
|
<v-card-actions class="dialog-actions pa-0">
|
|
228
246
|
<v-btn class="text-none action-cancel" variant="flat" height="44" @click="sipDialog = false">Cancel</v-btn>
|
|
@@ -231,36 +249,6 @@
|
|
|
231
249
|
</v-card>
|
|
232
250
|
</v-dialog>
|
|
233
251
|
|
|
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
252
|
<v-dialog v-model="callDialog" max-width="360" persistent>
|
|
265
253
|
<v-card class="form-dialog call-dialog" rounded="lg">
|
|
266
254
|
<v-card-title class="dialog-title">Call Settings</v-card-title>
|
|
@@ -350,10 +338,10 @@
|
|
|
350
338
|
<v-card-title class="dialog-title">Make a Call</v-card-title>
|
|
351
339
|
<v-card-text class="dialog-body make-call-body">
|
|
352
340
|
<div class="make-call-field">
|
|
353
|
-
<div class="call-label">
|
|
341
|
+
<div class="call-label">Extension, SIP address, or peer IP</div>
|
|
354
342
|
<v-text-field
|
|
355
343
|
v-model="makeCallForm.target"
|
|
356
|
-
placeholder="
|
|
344
|
+
placeholder="7100 or user@example.com"
|
|
357
345
|
variant="outlined"
|
|
358
346
|
density="compact"
|
|
359
347
|
hide-details="auto"
|
|
@@ -388,12 +376,190 @@
|
|
|
388
376
|
</div>
|
|
389
377
|
</v-card-text>
|
|
390
378
|
<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>
|
|
379
|
+
<v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="makeCallDialog = false">Close</v-btn>
|
|
393
380
|
</v-card-actions>
|
|
394
381
|
</v-card>
|
|
395
382
|
</v-dialog>
|
|
396
383
|
|
|
384
|
+
<v-dialog v-model="webPhoneDialog" max-width="820" persistent eager>
|
|
385
|
+
<v-card class="web-phone-dialog" rounded="lg">
|
|
386
|
+
<v-card-title class="dialog-title web-phone-title">
|
|
387
|
+
<span>Web Call</span>
|
|
388
|
+
<div class="web-phone-title-actions">
|
|
389
|
+
<v-chip size="small" variant="flat" :class="webPhoneStatusClass">
|
|
390
|
+
{{ webPhoneStatusLabel }}
|
|
391
|
+
</v-chip>
|
|
392
|
+
<v-btn
|
|
393
|
+
icon="mdi-close"
|
|
394
|
+
size="small"
|
|
395
|
+
variant="text"
|
|
396
|
+
title="Close Web Call"
|
|
397
|
+
aria-label="Close Web Call"
|
|
398
|
+
@click="webPhoneDialog = false"
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
</v-card-title>
|
|
402
|
+
|
|
403
|
+
<v-card-text class="web-phone-body">
|
|
404
|
+
<div class="web-phone-settings">
|
|
405
|
+
<div class="web-phone-section-title">SIP Account</div>
|
|
406
|
+
<div v-if="sipAccountLoading" class="sip-account-state">
|
|
407
|
+
<v-progress-circular indeterminate color="primary" size="30" width="3" />
|
|
408
|
+
<span>Preparing your private SIP account...</span>
|
|
409
|
+
</div>
|
|
410
|
+
<div v-else-if="sipAccountError" class="sip-account-state is-error">
|
|
411
|
+
<v-icon icon="mdi-alert-circle-outline" size="28" />
|
|
412
|
+
<span>{{ sipAccountError }}</span>
|
|
413
|
+
<v-btn
|
|
414
|
+
class="text-none sip-account-retry"
|
|
415
|
+
variant="outlined"
|
|
416
|
+
prepend-icon="mdi-refresh"
|
|
417
|
+
@click="loadSipAccount"
|
|
418
|
+
>
|
|
419
|
+
Retry
|
|
420
|
+
</v-btn>
|
|
421
|
+
</div>
|
|
422
|
+
<div v-else-if="sipAccount" class="sip-account-summary">
|
|
423
|
+
<div>
|
|
424
|
+
<span class="sip-account-label">Assigned extension</span>
|
|
425
|
+
<strong class="sip-account-extension">{{ sipAccount.extension }}</strong>
|
|
426
|
+
</div>
|
|
427
|
+
<span class="sip-account-address">{{ sipAccount.sipAddress }}</span>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="call-toggle-row web-video-toggle">
|
|
430
|
+
<div>
|
|
431
|
+
<span>Receive HID video</span>
|
|
432
|
+
<small>Show the HID camera feed without sharing this device's camera.</small>
|
|
433
|
+
</div>
|
|
434
|
+
<v-switch
|
|
435
|
+
v-model="webPhoneForm.video"
|
|
436
|
+
color="success"
|
|
437
|
+
density="compact"
|
|
438
|
+
hide-details
|
|
439
|
+
:disabled="webPhone.isRegistered.value"
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
<v-btn
|
|
443
|
+
v-if="!webPhone.isRegistered.value"
|
|
444
|
+
block
|
|
445
|
+
class="text-none web-phone-connect"
|
|
446
|
+
variant="flat"
|
|
447
|
+
prepend-icon="mdi-lan-connect"
|
|
448
|
+
:loading="webPhoneConnecting"
|
|
449
|
+
:disabled="!sipAccount || sipAccountLoading"
|
|
450
|
+
@click="connectWebPhone"
|
|
451
|
+
>
|
|
452
|
+
Connect Web Phone
|
|
453
|
+
</v-btn>
|
|
454
|
+
<v-btn
|
|
455
|
+
v-else
|
|
456
|
+
block
|
|
457
|
+
class="text-none web-phone-disconnect"
|
|
458
|
+
variant="outlined"
|
|
459
|
+
prepend-icon="mdi-lan-disconnect"
|
|
460
|
+
:loading="webPhoneConnecting"
|
|
461
|
+
:disabled="webPhone.hasCall.value"
|
|
462
|
+
@click="disconnectWebPhone"
|
|
463
|
+
>
|
|
464
|
+
Disconnect
|
|
465
|
+
</v-btn>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<div class="web-phone-console">
|
|
469
|
+
<div
|
|
470
|
+
class="web-phone-media"
|
|
471
|
+
:class="{
|
|
472
|
+
'has-video': webPhoneForm.video && webPhone.callState.value === 'active',
|
|
473
|
+
'has-remote-video': webPhone.remoteVideoAvailable.value,
|
|
474
|
+
}"
|
|
475
|
+
>
|
|
476
|
+
<video ref="remoteVideoEl" autoplay playsinline class="remote-video" />
|
|
477
|
+
<audio ref="remoteAudioEl" autoplay />
|
|
478
|
+
<div
|
|
479
|
+
v-if="!webPhoneForm.video || webPhone.callState.value !== 'active' || !webPhone.remoteVideoAvailable.value"
|
|
480
|
+
class="audio-call-state"
|
|
481
|
+
>
|
|
482
|
+
<v-icon size="54">{{ webPhoneCallIcon }}</v-icon>
|
|
483
|
+
<strong>{{ webPhoneCallLabel }}</strong>
|
|
484
|
+
<span v-if="webPhone.currentTarget.value">{{ webPhone.currentTarget.value }}</span>
|
|
485
|
+
<span v-if="webPhoneForm.video && webPhone.callState.value === 'active' && !webPhone.remoteVideoAvailable.value">
|
|
486
|
+
Waiting for video from the HID device
|
|
487
|
+
</span>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<div v-if="webPhone.callState.value === 'incoming'" class="incoming-call-actions">
|
|
492
|
+
<v-btn
|
|
493
|
+
class="text-none answer-call"
|
|
494
|
+
variant="flat"
|
|
495
|
+
prepend-icon="mdi-phone"
|
|
496
|
+
:loading="webPhoneActionLoading"
|
|
497
|
+
@click="answerWebCall"
|
|
498
|
+
>
|
|
499
|
+
Answer
|
|
500
|
+
</v-btn>
|
|
501
|
+
<v-btn
|
|
502
|
+
class="text-none reject-call"
|
|
503
|
+
variant="flat"
|
|
504
|
+
prepend-icon="mdi-phone-hangup"
|
|
505
|
+
:disabled="webPhoneActionLoading"
|
|
506
|
+
@click="declineWebCall"
|
|
507
|
+
>
|
|
508
|
+
Decline
|
|
509
|
+
</v-btn>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<template v-else>
|
|
513
|
+
<div class="call-label">Extension or SIP Address</div>
|
|
514
|
+
<div class="web-phone-dial-row">
|
|
515
|
+
<v-text-field
|
|
516
|
+
v-model="webPhoneForm.destination"
|
|
517
|
+
placeholder="1001 or device@example.com"
|
|
518
|
+
variant="outlined"
|
|
519
|
+
density="compact"
|
|
520
|
+
hide-details="auto"
|
|
521
|
+
class="call-input mb-0"
|
|
522
|
+
:disabled="!webPhone.isRegistered.value || webPhone.hasCall.value"
|
|
523
|
+
@keyup.enter="startWebCall"
|
|
524
|
+
/>
|
|
525
|
+
<v-btn
|
|
526
|
+
icon="mdi-phone"
|
|
527
|
+
class="start-web-call"
|
|
528
|
+
variant="flat"
|
|
529
|
+
:loading="webPhoneActionLoading && webPhone.callState.value === 'calling'"
|
|
530
|
+
:disabled="!webPhone.isRegistered.value || webPhone.hasCall.value || !webPhoneForm.destination.trim()"
|
|
531
|
+
@click="startWebCall"
|
|
532
|
+
/>
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<div v-if="webPhone.hasCall.value" class="active-call-controls">
|
|
536
|
+
<v-btn
|
|
537
|
+
:icon="webPhone.muted.value ? 'mdi-microphone-off' : 'mdi-microphone'"
|
|
538
|
+
variant="outlined"
|
|
539
|
+
:disabled="webPhone.callState.value !== 'active'"
|
|
540
|
+
:title="webPhone.muted.value ? 'Unmute' : 'Mute'"
|
|
541
|
+
@click="toggleWebMute"
|
|
542
|
+
/>
|
|
543
|
+
<v-btn
|
|
544
|
+
icon="mdi-phone-hangup"
|
|
545
|
+
class="hangup-web-call"
|
|
546
|
+
variant="flat"
|
|
547
|
+
:loading="webPhoneActionLoading && webPhone.callState.value === 'ending'"
|
|
548
|
+
title="End call"
|
|
549
|
+
@click="hangupWebCall"
|
|
550
|
+
/>
|
|
551
|
+
</div>
|
|
552
|
+
</template>
|
|
553
|
+
|
|
554
|
+
<v-alert v-if="webPhone.errorMessage.value" type="error" variant="tonal" density="compact" class="web-phone-error">
|
|
555
|
+
{{ webPhone.errorMessage.value }}
|
|
556
|
+
</v-alert>
|
|
557
|
+
</div>
|
|
558
|
+
</v-card-text>
|
|
559
|
+
|
|
560
|
+
</v-card>
|
|
561
|
+
</v-dialog>
|
|
562
|
+
|
|
397
563
|
<v-dialog v-model="contactDialog" max-width="360" persistent>
|
|
398
564
|
<v-card class="form-dialog" rounded="lg">
|
|
399
565
|
<v-card-title class="dialog-title">{{ contactForm.id ? 'Edit Contact' : 'Add Contact' }}</v-card-title>
|
|
@@ -448,6 +614,10 @@
|
|
|
448
614
|
|
|
449
615
|
<script setup lang="ts">
|
|
450
616
|
const props = defineProps({
|
|
617
|
+
org: {
|
|
618
|
+
type: String,
|
|
619
|
+
required: true,
|
|
620
|
+
},
|
|
451
621
|
site: {
|
|
452
622
|
type: String,
|
|
453
623
|
required: true,
|
|
@@ -466,6 +636,19 @@ type ContactRow = {
|
|
|
466
636
|
dialCode: string;
|
|
467
637
|
};
|
|
468
638
|
|
|
639
|
+
type SipAccount = {
|
|
640
|
+
orgId: string;
|
|
641
|
+
site: string;
|
|
642
|
+
userId: string;
|
|
643
|
+
extension: string;
|
|
644
|
+
username: string;
|
|
645
|
+
password: string;
|
|
646
|
+
sipAddress: string;
|
|
647
|
+
websocketUrl: string;
|
|
648
|
+
status: "active";
|
|
649
|
+
provisionedAt: string;
|
|
650
|
+
};
|
|
651
|
+
|
|
469
652
|
type DetailItem = {
|
|
470
653
|
label: string;
|
|
471
654
|
value?: string | number | boolean;
|
|
@@ -474,7 +657,7 @@ type DetailItem = {
|
|
|
474
657
|
};
|
|
475
658
|
|
|
476
659
|
type IntercomRow = {
|
|
477
|
-
reader: Record<string,
|
|
660
|
+
reader: Record<string, unknown>;
|
|
478
661
|
name: string;
|
|
479
662
|
location: string;
|
|
480
663
|
serverAddress: string;
|
|
@@ -490,6 +673,7 @@ type IntercomRow = {
|
|
|
490
673
|
password: string;
|
|
491
674
|
operatingMode: string;
|
|
492
675
|
protocol: string;
|
|
676
|
+
videoEnabled: boolean;
|
|
493
677
|
dialMode: string;
|
|
494
678
|
dialModeLabel: string;
|
|
495
679
|
dialCode: string;
|
|
@@ -502,7 +686,7 @@ type IntercomRow = {
|
|
|
502
686
|
externalDialButton: boolean;
|
|
503
687
|
unknownContact: boolean;
|
|
504
688
|
callAfter: string;
|
|
505
|
-
pjsip: Record<string,
|
|
689
|
+
pjsip: Record<string, unknown>;
|
|
506
690
|
};
|
|
507
691
|
|
|
508
692
|
const {
|
|
@@ -513,6 +697,7 @@ const {
|
|
|
513
697
|
getIntercomStatus,
|
|
514
698
|
makeIntercomCall,
|
|
515
699
|
finalizeIntercomCall,
|
|
700
|
+
ensureSipAccount,
|
|
516
701
|
} = useHidAmico();
|
|
517
702
|
|
|
518
703
|
const activeTab = ref<IntercomTab>("intercom");
|
|
@@ -531,21 +716,26 @@ const limit = 20;
|
|
|
531
716
|
const selectedRow = ref<IntercomRow | null>(null);
|
|
532
717
|
const selectedContact = ref<ContactRow | null>(null);
|
|
533
718
|
const showSipPassword = ref(false);
|
|
719
|
+
const webPhoneConnecting = ref(false);
|
|
720
|
+
const webPhoneActionLoading = ref(false);
|
|
721
|
+
const sipAccount = ref<SipAccount | null>(null);
|
|
722
|
+
const sipAccountLoading = ref(false);
|
|
723
|
+
const sipAccountError = ref("");
|
|
724
|
+
const remoteVideoEl = ref<HTMLVideoElement | null>(null);
|
|
725
|
+
const remoteAudioEl = ref<HTMLAudioElement | null>(null);
|
|
726
|
+
const webPhone = useSipWebPhone();
|
|
534
727
|
|
|
535
728
|
const viewDialog = ref(false);
|
|
536
729
|
const sipDialog = ref(false);
|
|
537
|
-
const rebootConfirmDialog = ref(false);
|
|
538
|
-
const rebootingDialog = ref(false);
|
|
539
730
|
const callDialog = ref(false);
|
|
540
731
|
const dialCodeDialog = ref(false);
|
|
541
732
|
const makeCallDialog = ref(false);
|
|
733
|
+
const webPhoneDialog = ref(false);
|
|
542
734
|
const contactDialog = ref(false);
|
|
543
735
|
const contactViewDialog = ref(false);
|
|
544
736
|
const deleteContactDialog = ref(false);
|
|
545
737
|
|
|
546
738
|
const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
|
|
547
|
-
const rebootCountdown = ref(59);
|
|
548
|
-
let rebootTimer: ReturnType<typeof setInterval> | undefined;
|
|
549
739
|
|
|
550
740
|
const statusOptions = ["Status", "Connected", "Not connected", "Connecting", "Disabled", "Failed"];
|
|
551
741
|
|
|
@@ -582,13 +772,14 @@ const sipForm = reactive({
|
|
|
582
772
|
serverIp: "",
|
|
583
773
|
serverPort: "5060",
|
|
584
774
|
initialRtpPort: "10000",
|
|
585
|
-
finalRtpPort: "
|
|
775
|
+
finalRtpPort: "10100",
|
|
586
776
|
numericExtension: false,
|
|
587
777
|
extension: "",
|
|
588
778
|
login: "",
|
|
589
779
|
password: "",
|
|
590
780
|
operatingMode: "sip-client",
|
|
591
781
|
protocol: "UDP",
|
|
782
|
+
videoEnabled: false,
|
|
592
783
|
});
|
|
593
784
|
|
|
594
785
|
const callForm = reactive({
|
|
@@ -617,6 +808,11 @@ const makeCallForm = reactive({
|
|
|
617
808
|
statusType: "info" as MessageType,
|
|
618
809
|
});
|
|
619
810
|
|
|
811
|
+
const webPhoneForm = reactive({
|
|
812
|
+
destination: "",
|
|
813
|
+
video: false,
|
|
814
|
+
});
|
|
815
|
+
|
|
620
816
|
const contactForm = reactive({
|
|
621
817
|
id: undefined as number | undefined,
|
|
622
818
|
readerId: "",
|
|
@@ -694,6 +890,7 @@ const intercomDetailSections = computed(() => {
|
|
|
694
890
|
items: [
|
|
695
891
|
{ label: "Operating Mode", value: row?.operatingMode },
|
|
696
892
|
{ label: "Protocol", value: row?.protocol },
|
|
893
|
+
{ label: "Video", value: row?.videoEnabled ? "Enabled" : "Disabled" },
|
|
697
894
|
],
|
|
698
895
|
},
|
|
699
896
|
{
|
|
@@ -717,10 +914,45 @@ const contactDetailRows = computed<DetailItem[]>(() => [
|
|
|
717
914
|
{ label: "Server Address", value: selectedContact.value?.serverAddress, link: true },
|
|
718
915
|
{ label: "Location", value: selectedContact.value?.location },
|
|
719
916
|
]);
|
|
917
|
+
const webPhoneStatusLabel = computed(() => {
|
|
918
|
+
if (webPhone.callState.value === "incoming") return "Incoming call";
|
|
919
|
+
if (webPhone.callState.value === "calling") return "Calling";
|
|
920
|
+
if (webPhone.callState.value === "active") return "In call";
|
|
921
|
+
if (webPhone.callState.value === "ending") return "Ending call";
|
|
922
|
+
if (webPhone.connectionState.value === "connecting") return "Connecting";
|
|
923
|
+
if (webPhone.connectionState.value === "registered") return "Ready";
|
|
924
|
+
if (webPhone.connectionState.value === "error") return "Connection failed";
|
|
925
|
+
return "Disconnected";
|
|
926
|
+
});
|
|
927
|
+
const webPhoneStatusClass = computed(() => {
|
|
928
|
+
if (webPhone.callState.value === "incoming") return "web-phone-status incoming";
|
|
929
|
+
if (webPhone.callState.value === "active" || webPhone.connectionState.value === "registered") return "web-phone-status ready";
|
|
930
|
+
if (webPhone.connectionState.value === "error") return "web-phone-status failed";
|
|
931
|
+
return "web-phone-status idle";
|
|
932
|
+
});
|
|
933
|
+
const webPhoneCallLabel = computed(() => {
|
|
934
|
+
if (webPhone.callState.value === "incoming") return "Incoming call";
|
|
935
|
+
if (webPhone.callState.value === "calling") return "Calling...";
|
|
936
|
+
if (webPhone.callState.value === "active") return "Call connected";
|
|
937
|
+
if (webPhone.callState.value === "ending") return "Ending call...";
|
|
938
|
+
return webPhone.isRegistered.value ? "Ready to call" : "Connect a SIP account";
|
|
939
|
+
});
|
|
940
|
+
const webPhoneCallIcon = computed(() => {
|
|
941
|
+
if (webPhone.callState.value === "incoming") return "mdi-phone-incoming";
|
|
942
|
+
if (webPhone.callState.value === "calling") return "mdi-phone-outgoing";
|
|
943
|
+
if (webPhone.callState.value === "active") return "mdi-phone-in-talk";
|
|
944
|
+
return "mdi-phone-outline";
|
|
945
|
+
});
|
|
720
946
|
|
|
721
947
|
watch(
|
|
722
|
-
() => props.site,
|
|
723
|
-
() =>
|
|
948
|
+
[() => props.org, () => props.site],
|
|
949
|
+
() => {
|
|
950
|
+
if (webPhone.isRegistered.value) void webPhone.disconnect();
|
|
951
|
+
sipAccount.value = null;
|
|
952
|
+
sipAccountError.value = "";
|
|
953
|
+
webPhoneForm.destination = "";
|
|
954
|
+
void reload();
|
|
955
|
+
},
|
|
724
956
|
{ immediate: true },
|
|
725
957
|
);
|
|
726
958
|
|
|
@@ -732,9 +964,12 @@ watch(pages, () => {
|
|
|
732
964
|
if (page.value > pages.value) page.value = pages.value;
|
|
733
965
|
});
|
|
734
966
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
967
|
+
watch(
|
|
968
|
+
() => webPhone.callState.value,
|
|
969
|
+
(state) => {
|
|
970
|
+
if (state === "incoming") webPhoneDialog.value = true;
|
|
971
|
+
},
|
|
972
|
+
);
|
|
738
973
|
|
|
739
974
|
async function reload() {
|
|
740
975
|
if (!props.site) return;
|
|
@@ -798,19 +1033,27 @@ function paginate<T>(items: T[]) {
|
|
|
798
1033
|
return items.slice(start, start + limit);
|
|
799
1034
|
}
|
|
800
1035
|
|
|
801
|
-
function toIntercomRow(reader: Record<string,
|
|
1036
|
+
function toIntercomRow(reader: Record<string, unknown>, pjsip: Record<string, unknown>, status: string): IntercomRow {
|
|
802
1037
|
const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
|
|
803
1038
|
const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
|
|
1039
|
+
const initialRtpPort = toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A");
|
|
1040
|
+
const rtpPortRange = getPjsipValue(pjsip, ["server_outbound_port_range"]);
|
|
1041
|
+
const explicitFinalRtpPort = getPjsipValue(pjsip, ["finalRtpPort", "final_rtp_port"]);
|
|
1042
|
+
const initialRtpPortNumber = Number(initialRtpPort);
|
|
1043
|
+
const rtpPortRangeNumber = Number(rtpPortRange);
|
|
1044
|
+
const finalRtpPort = rtpPortRange !== undefined && Number.isFinite(initialRtpPortNumber) && Number.isFinite(rtpPortRangeNumber)
|
|
1045
|
+
? String(initialRtpPortNumber + rtpPortRangeNumber)
|
|
1046
|
+
: toText(explicitFinalRtpPort, "N/A");
|
|
804
1047
|
|
|
805
1048
|
return {
|
|
806
1049
|
reader,
|
|
807
|
-
name: reader.name || reader.deviceId
|
|
808
|
-
location: reader.location
|
|
1050
|
+
name: toText(reader.name || reader.deviceId, "HID Reader"),
|
|
1051
|
+
location: toText(reader.location, "N/A"),
|
|
809
1052
|
serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
|
|
810
1053
|
serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
|
|
811
|
-
initialRtpPort
|
|
812
|
-
finalRtpPort
|
|
813
|
-
monitorPath: reader.monitorPath
|
|
1054
|
+
initialRtpPort,
|
|
1055
|
+
finalRtpPort,
|
|
1056
|
+
monitorPath: toText(reader.monitorPath, "N/A"),
|
|
814
1057
|
status,
|
|
815
1058
|
sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
|
|
816
1059
|
numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
|
|
@@ -819,6 +1062,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
819
1062
|
password: toText(getPjsipValue(pjsip, ["password"]), ""),
|
|
820
1063
|
operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
|
|
821
1064
|
protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
|
|
1065
|
+
videoEnabled: toFlag(getPjsipValue(pjsip, ["video_enabled", "videoEnabled"])) === "1",
|
|
822
1066
|
dialMode,
|
|
823
1067
|
dialModeLabel: getDialModeLabel(dialMode),
|
|
824
1068
|
dialCode: toText(getPjsipValue(pjsip, ["open_door_command", "dialCode"]), "N/A"),
|
|
@@ -835,7 +1079,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
835
1079
|
};
|
|
836
1080
|
}
|
|
837
1081
|
|
|
838
|
-
function normalizePjsip(...sources: Record<string,
|
|
1082
|
+
function normalizePjsip(...sources: Record<string, unknown>[]) {
|
|
839
1083
|
for (const source of sources) {
|
|
840
1084
|
const candidate = findPjsipCandidate(source);
|
|
841
1085
|
if (candidate) return candidate;
|
|
@@ -843,45 +1087,46 @@ function normalizePjsip(...sources: Record<string, any>[]) {
|
|
|
843
1087
|
return {};
|
|
844
1088
|
}
|
|
845
1089
|
|
|
846
|
-
function findPjsipCandidate(source: Record<string,
|
|
1090
|
+
function findPjsipCandidate(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
847
1091
|
if (!source || typeof source !== "object") return undefined;
|
|
848
1092
|
const candidates = [
|
|
849
1093
|
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
|
|
1094
|
+
toRecord(source.pjsip),
|
|
1095
|
+
toRecord(toRecord(source.configuration).pjsip),
|
|
1096
|
+
toRecord(toRecord(source.result).pjsip),
|
|
1097
|
+
toRecord(toRecord(toRecord(source.result).configuration).pjsip),
|
|
1098
|
+
toRecord(toRecord(source.data).pjsip),
|
|
1099
|
+
toRecord(toRecord(toRecord(source.data).configuration).pjsip),
|
|
1100
|
+
toRecord(toRecord(toRecord(source.data).result).pjsip),
|
|
1101
|
+
toRecord(toRecord(toRecord(toRecord(source.data).result).configuration).pjsip),
|
|
1102
|
+
toRecord(toRecord(toRecord(source.data).data).pjsip),
|
|
1103
|
+
toRecord(toRecord(toRecord(toRecord(source.data).data).configuration).pjsip),
|
|
1104
|
+
toRecord(source.configuration),
|
|
1105
|
+
toRecord(toRecord(source.data).configuration),
|
|
1106
|
+
toRecord(toRecord(toRecord(source.data).data).configuration),
|
|
862
1107
|
source,
|
|
863
1108
|
];
|
|
864
1109
|
|
|
865
1110
|
return candidates.find((candidate) => isPjsipCandidate(candidate));
|
|
866
1111
|
}
|
|
867
1112
|
|
|
868
|
-
function isPjsipCandidate(value: unknown): value is Record<string,
|
|
1113
|
+
function isPjsipCandidate(value: unknown): value is Record<string, unknown> {
|
|
869
1114
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
870
1115
|
return pjsipKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)) ||
|
|
871
1116
|
["serverAddress", "serverIp", "serverPort", "extension", "peerToPeerEnabled"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
872
1117
|
}
|
|
873
1118
|
|
|
874
|
-
function getPjsipValue(source: Record<string,
|
|
1119
|
+
function getPjsipValue(source: Record<string, unknown>, keys: string[]) {
|
|
875
1120
|
for (const key of keys) {
|
|
876
1121
|
if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== "") return source[key];
|
|
877
1122
|
}
|
|
878
1123
|
return undefined;
|
|
879
1124
|
}
|
|
880
1125
|
|
|
881
|
-
function normalizeStatus(response: Record<string,
|
|
1126
|
+
function normalizeStatus(response: Record<string, unknown>, pjsip: Record<string, unknown>) {
|
|
882
1127
|
const status = Number(response?.status);
|
|
1128
|
+
if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
|
|
883
1129
|
if (status === 200) return "Connected";
|
|
884
|
-
if (status === -1 || toFlag(pjsip.enabled) === "0") return "Disabled";
|
|
885
1130
|
if (status === 0 || status === 100) return "Connecting";
|
|
886
1131
|
if ([401, 403, 408, 503].includes(status)) return "Failed";
|
|
887
1132
|
return "Not connected";
|
|
@@ -911,13 +1156,14 @@ function openSipDialog(row: IntercomRow) {
|
|
|
911
1156
|
sipForm.serverIp = row.serverAddress === "N/A" ? "" : row.serverAddress;
|
|
912
1157
|
sipForm.serverPort = row.serverPort === "N/A" ? "5060" : row.serverPort;
|
|
913
1158
|
sipForm.initialRtpPort = row.initialRtpPort === "N/A" ? "10000" : row.initialRtpPort;
|
|
914
|
-
sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "
|
|
1159
|
+
sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "10100" : row.finalRtpPort;
|
|
915
1160
|
sipForm.numericExtension = row.numericExtension;
|
|
916
1161
|
sipForm.extension = row.extension === "N/A" ? "" : row.extension;
|
|
917
1162
|
sipForm.login = row.login === "N/A" ? "" : row.login;
|
|
918
1163
|
sipForm.password = row.password;
|
|
919
1164
|
sipForm.operatingMode = row.operatingMode === "Peer-to-Peer" ? "peer-to-peer" : "sip-client";
|
|
920
1165
|
sipForm.protocol = row.protocol || "UDP";
|
|
1166
|
+
sipForm.videoEnabled = row.videoEnabled;
|
|
921
1167
|
sipDialog.value = true;
|
|
922
1168
|
}
|
|
923
1169
|
|
|
@@ -943,12 +1189,140 @@ function openDialCodeDialog(row: IntercomRow) {
|
|
|
943
1189
|
dialCodeDialog.value = true;
|
|
944
1190
|
}
|
|
945
1191
|
|
|
946
|
-
function openMakeCallDialog(row: IntercomRow) {
|
|
1192
|
+
async function openMakeCallDialog(row: IntercomRow) {
|
|
947
1193
|
selectedRow.value = row;
|
|
948
|
-
makeCallForm.target =
|
|
1194
|
+
makeCallForm.target = sipAccount.value?.extension || "";
|
|
949
1195
|
makeCallForm.status = "";
|
|
950
1196
|
makeCallForm.statusType = "info";
|
|
951
1197
|
makeCallDialog.value = true;
|
|
1198
|
+
if (!sipAccount.value && !sipAccountLoading.value) {
|
|
1199
|
+
await loadSipAccount();
|
|
1200
|
+
if (makeCallDialog.value && !makeCallForm.target) {
|
|
1201
|
+
makeCallForm.target = sipAccount.value?.extension || "";
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function openWebPhoneDialog(row?: IntercomRow) {
|
|
1207
|
+
const activeRow = row || selectedRow.value || intercomRows.value[0];
|
|
1208
|
+
if (activeRow) {
|
|
1209
|
+
selectedRow.value = activeRow;
|
|
1210
|
+
if (!webPhone.isRegistered.value && activeRow.extension !== "N/A") {
|
|
1211
|
+
webPhoneForm.destination = activeRow.extension;
|
|
1212
|
+
webPhoneForm.video = activeRow.videoEnabled;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
webPhoneDialog.value = true;
|
|
1216
|
+
if (!sipAccount.value && !sipAccountLoading.value) void loadSipAccount();
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function loadSipAccount() {
|
|
1220
|
+
if (!props.org || !props.site) return;
|
|
1221
|
+
|
|
1222
|
+
sipAccountLoading.value = true;
|
|
1223
|
+
sipAccountError.value = "";
|
|
1224
|
+
try {
|
|
1225
|
+
const response = await ensureSipAccount(props.site, props.org);
|
|
1226
|
+
sipAccount.value = normalizeSipAccount(response);
|
|
1227
|
+
} catch (error: unknown) {
|
|
1228
|
+
sipAccount.value = null;
|
|
1229
|
+
sipAccountError.value = getErrorMessage(error);
|
|
1230
|
+
} finally {
|
|
1231
|
+
sipAccountLoading.value = false;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
async function connectWebPhone() {
|
|
1236
|
+
const account = sipAccount.value;
|
|
1237
|
+
if (!account) {
|
|
1238
|
+
await loadSipAccount();
|
|
1239
|
+
if (!sipAccount.value) return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const activeAccount = sipAccount.value;
|
|
1243
|
+
if (!activeAccount) return;
|
|
1244
|
+
|
|
1245
|
+
webPhoneConnecting.value = true;
|
|
1246
|
+
try {
|
|
1247
|
+
await webPhone.connect({
|
|
1248
|
+
webSocketServer: activeAccount.websocketUrl,
|
|
1249
|
+
aor: activeAccount.sipAddress,
|
|
1250
|
+
authorizationUsername: activeAccount.username,
|
|
1251
|
+
authorizationPassword: activeAccount.password,
|
|
1252
|
+
displayName: "iService365 Web",
|
|
1253
|
+
video: webPhoneForm.video,
|
|
1254
|
+
}, {
|
|
1255
|
+
remoteVideo: remoteVideoEl.value,
|
|
1256
|
+
remoteAudio: remoteAudioEl.value,
|
|
1257
|
+
});
|
|
1258
|
+
showMessage("Web phone connected and ready to receive calls.", "success");
|
|
1259
|
+
} catch (error: unknown) {
|
|
1260
|
+
showMessage(getErrorMessage(error), "error");
|
|
1261
|
+
} finally {
|
|
1262
|
+
webPhoneConnecting.value = false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function disconnectWebPhone() {
|
|
1267
|
+
webPhoneConnecting.value = true;
|
|
1268
|
+
try {
|
|
1269
|
+
await webPhone.disconnect();
|
|
1270
|
+
showMessage("Web phone disconnected.", "info");
|
|
1271
|
+
} finally {
|
|
1272
|
+
webPhoneConnecting.value = false;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function startWebCall() {
|
|
1277
|
+
webPhoneActionLoading.value = true;
|
|
1278
|
+
try {
|
|
1279
|
+
await webPhone.call(webPhoneForm.destination);
|
|
1280
|
+
} catch (error: unknown) {
|
|
1281
|
+
showMessage(getErrorMessage(error), "error");
|
|
1282
|
+
} finally {
|
|
1283
|
+
webPhoneActionLoading.value = false;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function answerWebCall() {
|
|
1288
|
+
webPhoneActionLoading.value = true;
|
|
1289
|
+
try {
|
|
1290
|
+
await webPhone.answer();
|
|
1291
|
+
} catch (error: unknown) {
|
|
1292
|
+
showMessage(getErrorMessage(error), "error");
|
|
1293
|
+
} finally {
|
|
1294
|
+
webPhoneActionLoading.value = false;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function declineWebCall() {
|
|
1299
|
+
webPhoneActionLoading.value = true;
|
|
1300
|
+
try {
|
|
1301
|
+
await webPhone.decline();
|
|
1302
|
+
} catch (error: unknown) {
|
|
1303
|
+
showMessage(getErrorMessage(error), "error");
|
|
1304
|
+
} finally {
|
|
1305
|
+
webPhoneActionLoading.value = false;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async function hangupWebCall() {
|
|
1310
|
+
webPhoneActionLoading.value = true;
|
|
1311
|
+
try {
|
|
1312
|
+
await webPhone.hangup();
|
|
1313
|
+
} catch (error: unknown) {
|
|
1314
|
+
showMessage(getErrorMessage(error), "error");
|
|
1315
|
+
} finally {
|
|
1316
|
+
webPhoneActionLoading.value = false;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function toggleWebMute() {
|
|
1321
|
+
try {
|
|
1322
|
+
webPhone.toggleMute();
|
|
1323
|
+
} catch (error: unknown) {
|
|
1324
|
+
showMessage(getErrorMessage(error), "error");
|
|
1325
|
+
}
|
|
952
1326
|
}
|
|
953
1327
|
|
|
954
1328
|
function openContactDialog(contact?: ContactRow) {
|
|
@@ -974,21 +1348,35 @@ function openDeleteContact(contact: ContactRow) {
|
|
|
974
1348
|
async function saveSipSettings() {
|
|
975
1349
|
if (!selectedRow.value) return;
|
|
976
1350
|
const isPeerToPeer = sipForm.operatingMode === "peer-to-peer";
|
|
1351
|
+
const initialRtpPort = Number(sipForm.initialRtpPort);
|
|
1352
|
+
const finalRtpPort = Number(sipForm.finalRtpPort);
|
|
1353
|
+
|
|
1354
|
+
if (
|
|
1355
|
+
!Number.isInteger(initialRtpPort) ||
|
|
1356
|
+
!Number.isInteger(finalRtpPort) ||
|
|
1357
|
+
initialRtpPort < 1 ||
|
|
1358
|
+
finalRtpPort > 65535 ||
|
|
1359
|
+
finalRtpPort <= initialRtpPort
|
|
1360
|
+
) {
|
|
1361
|
+
showMessage("Final RTP Port must be greater than Initial RTP Port and both ports must be between 1 and 65535.", "warning");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
977
1365
|
const saved = await savePjsip(selectedRow.value.reader._id, {
|
|
978
1366
|
enabled: sipForm.enabled ? "1" : "0",
|
|
979
1367
|
server_ip: sipForm.serverIp,
|
|
980
1368
|
server_port: sipForm.serverPort,
|
|
981
|
-
server_outbound_port:
|
|
982
|
-
server_outbound_port_range:
|
|
1369
|
+
server_outbound_port: String(initialRtpPort),
|
|
1370
|
+
server_outbound_port_range: String(finalRtpPort - initialRtpPort),
|
|
983
1371
|
numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
|
|
984
1372
|
branch: isPeerToPeer ? "" : sipForm.extension,
|
|
985
1373
|
login: isPeerToPeer ? "" : sipForm.login,
|
|
986
1374
|
password: isPeerToPeer ? "" : sipForm.password,
|
|
987
1375
|
peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
|
|
988
|
-
|
|
1376
|
+
video_enabled: sipForm.videoEnabled ? "1" : "0",
|
|
1377
|
+
});
|
|
989
1378
|
if (saved) {
|
|
990
1379
|
sipDialog.value = false;
|
|
991
|
-
rebootConfirmDialog.value = true;
|
|
992
1380
|
}
|
|
993
1381
|
}
|
|
994
1382
|
|
|
@@ -1020,7 +1408,13 @@ async function runMakeCall() {
|
|
|
1020
1408
|
if (!selectedRow.value) return;
|
|
1021
1409
|
const target = makeCallForm.target.trim();
|
|
1022
1410
|
if (!target) {
|
|
1023
|
-
makeCallForm.status = "Please enter an address
|
|
1411
|
+
makeCallForm.status = "Please enter an extension, SIP address, or peer IP.";
|
|
1412
|
+
makeCallForm.statusType = "warning";
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (!isSipCallTarget(target)) {
|
|
1417
|
+
makeCallForm.status = "Enter a valid extension, SIP address, or IPv4 address.";
|
|
1024
1418
|
makeCallForm.statusType = "warning";
|
|
1025
1419
|
return;
|
|
1026
1420
|
}
|
|
@@ -1042,6 +1436,10 @@ async function runMakeCall() {
|
|
|
1042
1436
|
}
|
|
1043
1437
|
}
|
|
1044
1438
|
|
|
1439
|
+
function isSipCallTarget(value = "") {
|
|
1440
|
+
return /^(?:sip:)?[A-Za-z0-9_.+-]+(?:@[A-Za-z0-9.-]+(?::\d{1,5})?)?$/.test(value.trim());
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1045
1443
|
async function runHangupCall() {
|
|
1046
1444
|
if (!selectedRow.value) return;
|
|
1047
1445
|
hangingUp.value = true;
|
|
@@ -1078,30 +1476,6 @@ async function savePjsip(readerId: string, payload: Record<string, string>, opti
|
|
|
1078
1476
|
}
|
|
1079
1477
|
}
|
|
1080
1478
|
|
|
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
1479
|
async function saveContact() {
|
|
1106
1480
|
if (!contactForm.readerId || !contactForm.name.trim() || !contactForm.serverAddress.trim()) {
|
|
1107
1481
|
showMessage("Please complete the required contact fields.", "warning");
|
|
@@ -1182,8 +1556,42 @@ function showMessage(text: string, type: MessageType = "info") {
|
|
|
1182
1556
|
message.type = type;
|
|
1183
1557
|
}
|
|
1184
1558
|
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1559
|
+
function normalizeSipAccount(response: unknown): SipAccount {
|
|
1560
|
+
const responseRecord = toRecord(response);
|
|
1561
|
+
const payload = toRecord(responseRecord.data || responseRecord);
|
|
1562
|
+
const requiredFields = ["extension", "username", "password", "sipAddress", "websocketUrl"] as const;
|
|
1563
|
+
|
|
1564
|
+
if (requiredFields.some((field) => !toText(payload[field]))) {
|
|
1565
|
+
throw new Error("The SIP account response is incomplete. Please try again.");
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return {
|
|
1569
|
+
orgId: toText(payload.orgId, props.org),
|
|
1570
|
+
site: toText(payload.site, props.site),
|
|
1571
|
+
userId: toText(payload.userId),
|
|
1572
|
+
extension: toText(payload.extension),
|
|
1573
|
+
username: toText(payload.username),
|
|
1574
|
+
password: toText(payload.password),
|
|
1575
|
+
sipAddress: toText(payload.sipAddress),
|
|
1576
|
+
websocketUrl: toText(payload.websocketUrl),
|
|
1577
|
+
status: "active",
|
|
1578
|
+
provisionedAt: toText(payload.provisionedAt),
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function getErrorMessage(error: unknown) {
|
|
1583
|
+
const errorRecord = toRecord(error);
|
|
1584
|
+
const data = toRecord(errorRecord.data);
|
|
1585
|
+
const response = toRecord(errorRecord.response);
|
|
1586
|
+
const responseData = toRecord(response._data);
|
|
1587
|
+
return toText(data.message)
|
|
1588
|
+
|| toText(responseData.message)
|
|
1589
|
+
|| toText(errorRecord.message)
|
|
1590
|
+
|| "HID intercom request failed.";
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
1594
|
+
return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
|
|
1187
1595
|
}
|
|
1188
1596
|
|
|
1189
1597
|
function toText(value: unknown, fallback = "") {
|
|
@@ -1259,6 +1667,23 @@ export default {
|
|
|
1259
1667
|
width: 160px;
|
|
1260
1668
|
}
|
|
1261
1669
|
|
|
1670
|
+
.web-call-button {
|
|
1671
|
+
min-width: 160px;
|
|
1672
|
+
background: #e5e7eb;
|
|
1673
|
+
color: #0b2c40;
|
|
1674
|
+
font-size: 12px;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.web-call-button.is-connected {
|
|
1678
|
+
background: #d9f2e1;
|
|
1679
|
+
color: #16743a;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
.web-call-button.has-incoming-call {
|
|
1683
|
+
background: #1769aa;
|
|
1684
|
+
color: #fff;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1262
1687
|
.add-button {
|
|
1263
1688
|
min-width: 160px;
|
|
1264
1689
|
background: #dedede;
|
|
@@ -1468,6 +1893,20 @@ export default {
|
|
|
1468
1893
|
margin-bottom: 12px;
|
|
1469
1894
|
}
|
|
1470
1895
|
|
|
1896
|
+
.sip-section-switch > div {
|
|
1897
|
+
display: flex;
|
|
1898
|
+
min-width: 0;
|
|
1899
|
+
padding: 8px 12px 8px 0;
|
|
1900
|
+
flex-direction: column;
|
|
1901
|
+
gap: 2px;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
.sip-section-switch small {
|
|
1905
|
+
color: #667085;
|
|
1906
|
+
font-size: 10px;
|
|
1907
|
+
line-height: 1.4;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1471
1910
|
.sip-switch-row :deep(.v-switch) {
|
|
1472
1911
|
flex: 0 0 auto;
|
|
1473
1912
|
}
|
|
@@ -1630,98 +2069,6 @@ export default {
|
|
|
1630
2069
|
overflow: hidden;
|
|
1631
2070
|
}
|
|
1632
2071
|
|
|
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
2072
|
.call-dialog .dialog-body {
|
|
1726
2073
|
padding: 18px 24px 24px;
|
|
1727
2074
|
}
|
|
@@ -1931,6 +2278,274 @@ export default {
|
|
|
1931
2278
|
line-height: 1.45;
|
|
1932
2279
|
}
|
|
1933
2280
|
|
|
2281
|
+
.web-phone-dialog {
|
|
2282
|
+
display: flex;
|
|
2283
|
+
width: 820px;
|
|
2284
|
+
max-height: min(90vh, 760px);
|
|
2285
|
+
max-width: 94vw;
|
|
2286
|
+
overflow: hidden;
|
|
2287
|
+
background: #fff;
|
|
2288
|
+
flex-direction: column;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
.web-phone-title {
|
|
2292
|
+
flex: 0 0 auto;
|
|
2293
|
+
display: flex;
|
|
2294
|
+
align-items: center;
|
|
2295
|
+
justify-content: space-between;
|
|
2296
|
+
gap: 16px;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
.web-phone-title-actions {
|
|
2300
|
+
display: flex;
|
|
2301
|
+
align-items: center;
|
|
2302
|
+
gap: 8px;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
.web-phone-status {
|
|
2306
|
+
min-width: 92px;
|
|
2307
|
+
justify-content: center;
|
|
2308
|
+
font-size: 10px;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
.web-phone-status.ready {
|
|
2312
|
+
background: #d9f2e1;
|
|
2313
|
+
color: #16743a;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
.web-phone-status.incoming {
|
|
2317
|
+
background: #dcecff;
|
|
2318
|
+
color: #125c9a;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
.web-phone-status.failed {
|
|
2322
|
+
background: #fce2e2;
|
|
2323
|
+
color: #a92222;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
.web-phone-status.idle {
|
|
2327
|
+
background: #e5e7eb;
|
|
2328
|
+
color: #5d6673;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
.web-phone-body {
|
|
2332
|
+
display: grid;
|
|
2333
|
+
grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr);
|
|
2334
|
+
gap: 24px;
|
|
2335
|
+
min-height: 440px;
|
|
2336
|
+
overflow: auto;
|
|
2337
|
+
padding: 22px 24px 24px;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
.web-phone-settings {
|
|
2341
|
+
padding-right: 24px;
|
|
2342
|
+
border-right: 1px solid #e5e7eb;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.web-phone-section-title {
|
|
2346
|
+
margin-bottom: 14px;
|
|
2347
|
+
color: #0d1720;
|
|
2348
|
+
font-size: 13px;
|
|
2349
|
+
font-weight: 700;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
.sip-account-state {
|
|
2353
|
+
display: flex;
|
|
2354
|
+
min-height: 126px;
|
|
2355
|
+
align-items: center;
|
|
2356
|
+
justify-content: center;
|
|
2357
|
+
padding: 18px;
|
|
2358
|
+
border: 1px solid #dce2e8;
|
|
2359
|
+
border-radius: 6px;
|
|
2360
|
+
color: #5d6673;
|
|
2361
|
+
flex-direction: column;
|
|
2362
|
+
font-size: 12px;
|
|
2363
|
+
gap: 12px;
|
|
2364
|
+
text-align: center;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
.sip-account-state.is-error {
|
|
2368
|
+
border-color: #f0c7c7;
|
|
2369
|
+
background: #fff8f8;
|
|
2370
|
+
color: #a92222;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
.sip-account-retry {
|
|
2374
|
+
min-width: 104px;
|
|
2375
|
+
border-color: #aeb8c2;
|
|
2376
|
+
color: #344054;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
.sip-account-summary {
|
|
2380
|
+
display: grid;
|
|
2381
|
+
min-height: 96px;
|
|
2382
|
+
align-items: center;
|
|
2383
|
+
padding: 16px;
|
|
2384
|
+
border: 1px solid #dce2e8;
|
|
2385
|
+
border-radius: 6px;
|
|
2386
|
+
background: #f8fafc;
|
|
2387
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2388
|
+
gap: 18px;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
.sip-account-summary > div {
|
|
2392
|
+
display: flex;
|
|
2393
|
+
min-width: 0;
|
|
2394
|
+
flex-direction: column;
|
|
2395
|
+
gap: 4px;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
.sip-account-label {
|
|
2399
|
+
color: #667085;
|
|
2400
|
+
font-size: 11px;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
.sip-account-extension {
|
|
2404
|
+
color: #0d1720;
|
|
2405
|
+
font-size: 24px;
|
|
2406
|
+
line-height: 1.1;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
.sip-account-address {
|
|
2410
|
+
max-width: 170px;
|
|
2411
|
+
overflow: hidden;
|
|
2412
|
+
color: #536273;
|
|
2413
|
+
font-size: 11px;
|
|
2414
|
+
text-align: right;
|
|
2415
|
+
text-overflow: ellipsis;
|
|
2416
|
+
white-space: nowrap;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
.web-video-toggle {
|
|
2420
|
+
margin-top: 12px;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
.web-phone-connect {
|
|
2424
|
+
min-height: 42px;
|
|
2425
|
+
margin-top: 14px;
|
|
2426
|
+
background: #082b40;
|
|
2427
|
+
color: #fff;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
.web-phone-disconnect {
|
|
2431
|
+
min-height: 42px;
|
|
2432
|
+
margin-top: 14px;
|
|
2433
|
+
border-color: #aeb8c2;
|
|
2434
|
+
color: #344054;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
.web-phone-console {
|
|
2438
|
+
display: flex;
|
|
2439
|
+
min-width: 0;
|
|
2440
|
+
flex-direction: column;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
.web-phone-media {
|
|
2444
|
+
position: relative;
|
|
2445
|
+
display: grid;
|
|
2446
|
+
min-height: 260px;
|
|
2447
|
+
overflow: hidden;
|
|
2448
|
+
place-items: center;
|
|
2449
|
+
border: 1px solid #d7dde4;
|
|
2450
|
+
border-radius: 6px;
|
|
2451
|
+
background: #102a3b;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
.remote-video {
|
|
2455
|
+
display: none;
|
|
2456
|
+
width: 100%;
|
|
2457
|
+
height: 100%;
|
|
2458
|
+
object-fit: cover;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
.web-phone-media.has-remote-video .remote-video {
|
|
2462
|
+
display: block;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
.web-phone-media.has-remote-video .audio-call-state {
|
|
2466
|
+
display: none;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
.audio-call-state {
|
|
2470
|
+
display: flex;
|
|
2471
|
+
align-items: center;
|
|
2472
|
+
justify-content: center;
|
|
2473
|
+
padding: 28px;
|
|
2474
|
+
color: #fff;
|
|
2475
|
+
flex-direction: column;
|
|
2476
|
+
gap: 10px;
|
|
2477
|
+
text-align: center;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
.audio-call-state strong {
|
|
2481
|
+
font-size: 16px;
|
|
2482
|
+
font-weight: 600;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
.audio-call-state span {
|
|
2486
|
+
max-width: 100%;
|
|
2487
|
+
overflow: hidden;
|
|
2488
|
+
color: #c9d7e1;
|
|
2489
|
+
font-size: 11px;
|
|
2490
|
+
text-overflow: ellipsis;
|
|
2491
|
+
white-space: nowrap;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
.web-phone-dial-row {
|
|
2495
|
+
display: grid;
|
|
2496
|
+
grid-template-columns: minmax(0, 1fr) 42px;
|
|
2497
|
+
gap: 8px;
|
|
2498
|
+
align-items: start;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
.start-web-call,
|
|
2502
|
+
.hangup-web-call {
|
|
2503
|
+
width: 42px;
|
|
2504
|
+
height: 38px;
|
|
2505
|
+
border-radius: 4px;
|
|
2506
|
+
background: #2a9b50;
|
|
2507
|
+
color: #fff;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
.hangup-web-call,
|
|
2511
|
+
.reject-call {
|
|
2512
|
+
background: #c9293d;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
.incoming-call-actions {
|
|
2516
|
+
display: grid;
|
|
2517
|
+
grid-template-columns: 1fr 1fr;
|
|
2518
|
+
gap: 10px;
|
|
2519
|
+
margin-top: 14px;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
.answer-call,
|
|
2523
|
+
.reject-call {
|
|
2524
|
+
min-height: 42px;
|
|
2525
|
+
color: #fff;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
.answer-call {
|
|
2529
|
+
background: #2a9b50;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
.active-call-controls {
|
|
2533
|
+
display: flex;
|
|
2534
|
+
justify-content: center;
|
|
2535
|
+
gap: 12px;
|
|
2536
|
+
margin-top: 16px;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
.active-call-controls .v-btn {
|
|
2540
|
+
width: 46px;
|
|
2541
|
+
height: 46px;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
.web-phone-error {
|
|
2545
|
+
margin-top: 14px;
|
|
2546
|
+
font-size: 11px;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
1934
2549
|
.call-two-col {
|
|
1935
2550
|
gap: 12px;
|
|
1936
2551
|
}
|
|
@@ -2064,13 +2679,21 @@ export default {
|
|
|
2064
2679
|
max-width: none;
|
|
2065
2680
|
}
|
|
2066
2681
|
|
|
2067
|
-
.
|
|
2068
|
-
|
|
2069
|
-
|
|
2682
|
+
.web-call-button {
|
|
2683
|
+
width: 100%;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
.web-phone-body {
|
|
2687
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2688
|
+
min-height: 0;
|
|
2070
2689
|
}
|
|
2071
2690
|
|
|
2072
|
-
.
|
|
2073
|
-
|
|
2691
|
+
.web-phone-settings {
|
|
2692
|
+
padding-right: 0;
|
|
2693
|
+
padding-bottom: 20px;
|
|
2694
|
+
border-right: 0;
|
|
2695
|
+
border-bottom: 1px solid #e5e7eb;
|
|
2074
2696
|
}
|
|
2697
|
+
|
|
2075
2698
|
}
|
|
2076
2699
|
</style>
|