@7365admin1/layer-common 3.2.0 → 3.2.1-staging.60
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/components/CameraWall.vue +625 -0
- package/components/CameraWallTile.vue +333 -0
- package/components/EntryPassInformation.vue +4 -4
- package/components/HidAccessLogDashboard.vue +53 -8
- package/components/HidIntercomManagement.vue +842 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +96 -2
- package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +6 -2
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +0 -1
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useSipWebPhone.ts +332 -0
- package/composables/useSiteSettings.ts +28 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +2 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +149 -0
- package/utils/camera-wall.ts +299 -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 and receive video during SIP 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">IP address to dial</div>
|
|
354
342
|
<v-text-field
|
|
355
343
|
v-model="makeCallForm.target"
|
|
356
|
-
placeholder="
|
|
344
|
+
placeholder="000.000.0.0"
|
|
357
345
|
variant="outlined"
|
|
358
346
|
density="compact"
|
|
359
347
|
hide-details="auto"
|
|
@@ -388,8 +376,188 @@
|
|
|
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
|
-
|
|
379
|
+
<v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="makeCallDialog = false">Close</v-btn>
|
|
380
|
+
</v-card-actions>
|
|
381
|
+
</v-card>
|
|
382
|
+
</v-dialog>
|
|
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
|
+
<v-chip size="small" variant="flat" :class="webPhoneStatusClass">
|
|
389
|
+
{{ webPhoneStatusLabel }}
|
|
390
|
+
</v-chip>
|
|
391
|
+
</v-card-title>
|
|
392
|
+
|
|
393
|
+
<v-card-text class="web-phone-body">
|
|
394
|
+
<div class="web-phone-settings">
|
|
395
|
+
<div class="web-phone-section-title">SIP Account</div>
|
|
396
|
+
<div v-if="sipAccountLoading" class="sip-account-state">
|
|
397
|
+
<v-progress-circular indeterminate color="primary" size="30" width="3" />
|
|
398
|
+
<span>Preparing your private SIP account...</span>
|
|
399
|
+
</div>
|
|
400
|
+
<div v-else-if="sipAccountError" class="sip-account-state is-error">
|
|
401
|
+
<v-icon icon="mdi-alert-circle-outline" size="28" />
|
|
402
|
+
<span>{{ sipAccountError }}</span>
|
|
403
|
+
<v-btn
|
|
404
|
+
class="text-none sip-account-retry"
|
|
405
|
+
variant="outlined"
|
|
406
|
+
prepend-icon="mdi-refresh"
|
|
407
|
+
@click="loadSipAccount"
|
|
408
|
+
>
|
|
409
|
+
Retry
|
|
410
|
+
</v-btn>
|
|
411
|
+
</div>
|
|
412
|
+
<div v-else-if="sipAccount" class="sip-account-summary">
|
|
413
|
+
<div>
|
|
414
|
+
<span class="sip-account-label">Assigned extension</span>
|
|
415
|
+
<strong class="sip-account-extension">{{ sipAccount.extension }}</strong>
|
|
416
|
+
</div>
|
|
417
|
+
<span class="sip-account-address">{{ sipAccount.sipAddress }}</span>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="call-toggle-row web-video-toggle">
|
|
420
|
+
<span>Enable video</span>
|
|
421
|
+
<v-switch
|
|
422
|
+
v-model="webPhoneForm.video"
|
|
423
|
+
color="success"
|
|
424
|
+
density="compact"
|
|
425
|
+
hide-details
|
|
426
|
+
:disabled="webPhone.isRegistered.value"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
<v-btn
|
|
430
|
+
v-if="!webPhone.isRegistered.value"
|
|
431
|
+
block
|
|
432
|
+
class="text-none web-phone-connect"
|
|
433
|
+
variant="flat"
|
|
434
|
+
prepend-icon="mdi-lan-connect"
|
|
435
|
+
:loading="webPhoneConnecting"
|
|
436
|
+
:disabled="!sipAccount || sipAccountLoading"
|
|
437
|
+
@click="connectWebPhone"
|
|
438
|
+
>
|
|
439
|
+
Connect Web Phone
|
|
440
|
+
</v-btn>
|
|
441
|
+
<v-btn
|
|
442
|
+
v-else
|
|
443
|
+
block
|
|
444
|
+
class="text-none web-phone-disconnect"
|
|
445
|
+
variant="outlined"
|
|
446
|
+
prepend-icon="mdi-lan-disconnect"
|
|
447
|
+
:loading="webPhoneConnecting"
|
|
448
|
+
:disabled="webPhone.hasCall.value"
|
|
449
|
+
@click="disconnectWebPhone"
|
|
450
|
+
>
|
|
451
|
+
Disconnect
|
|
452
|
+
</v-btn>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div class="web-phone-console">
|
|
456
|
+
<div
|
|
457
|
+
class="web-phone-media"
|
|
458
|
+
:class="{
|
|
459
|
+
'has-video': webPhoneForm.video && webPhone.callState.value === 'active',
|
|
460
|
+
'has-local-video': webPhone.localVideoAvailable.value,
|
|
461
|
+
'has-remote-video': webPhone.remoteVideoAvailable.value,
|
|
462
|
+
}"
|
|
463
|
+
>
|
|
464
|
+
<video ref="remoteVideoEl" autoplay playsinline class="remote-video" />
|
|
465
|
+
<video ref="localVideoEl" autoplay muted playsinline class="local-video" />
|
|
466
|
+
<audio ref="remoteAudioEl" autoplay />
|
|
467
|
+
<div
|
|
468
|
+
v-if="!webPhoneForm.video || webPhone.callState.value !== 'active' || !webPhone.remoteVideoAvailable.value"
|
|
469
|
+
class="audio-call-state"
|
|
470
|
+
>
|
|
471
|
+
<v-icon size="54">{{ webPhoneCallIcon }}</v-icon>
|
|
472
|
+
<strong>{{ webPhoneCallLabel }}</strong>
|
|
473
|
+
<span v-if="webPhone.currentTarget.value">{{ webPhone.currentTarget.value }}</span>
|
|
474
|
+
<span v-if="webPhoneForm.video && webPhone.callState.value === 'active' && !webPhone.remoteVideoAvailable.value">
|
|
475
|
+
Waiting for video from the other device
|
|
476
|
+
</span>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div v-if="webPhone.callState.value === 'incoming'" class="incoming-call-actions">
|
|
481
|
+
<v-btn
|
|
482
|
+
class="text-none answer-call"
|
|
483
|
+
variant="flat"
|
|
484
|
+
prepend-icon="mdi-phone"
|
|
485
|
+
:loading="webPhoneActionLoading"
|
|
486
|
+
@click="answerWebCall"
|
|
487
|
+
>
|
|
488
|
+
Answer
|
|
489
|
+
</v-btn>
|
|
490
|
+
<v-btn
|
|
491
|
+
class="text-none reject-call"
|
|
492
|
+
variant="flat"
|
|
493
|
+
prepend-icon="mdi-phone-hangup"
|
|
494
|
+
:disabled="webPhoneActionLoading"
|
|
495
|
+
@click="declineWebCall"
|
|
496
|
+
>
|
|
497
|
+
Decline
|
|
498
|
+
</v-btn>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<template v-else>
|
|
502
|
+
<div class="call-label">Extension or SIP Address</div>
|
|
503
|
+
<div class="web-phone-dial-row">
|
|
504
|
+
<v-text-field
|
|
505
|
+
v-model="webPhoneForm.destination"
|
|
506
|
+
placeholder="1001 or device@example.com"
|
|
507
|
+
variant="outlined"
|
|
508
|
+
density="compact"
|
|
509
|
+
hide-details="auto"
|
|
510
|
+
class="call-input mb-0"
|
|
511
|
+
:disabled="!webPhone.isRegistered.value || webPhone.hasCall.value"
|
|
512
|
+
@keyup.enter="startWebCall"
|
|
513
|
+
/>
|
|
514
|
+
<v-btn
|
|
515
|
+
icon="mdi-phone"
|
|
516
|
+
class="start-web-call"
|
|
517
|
+
variant="flat"
|
|
518
|
+
:loading="webPhoneActionLoading && webPhone.callState.value === 'calling'"
|
|
519
|
+
:disabled="!webPhone.isRegistered.value || webPhone.hasCall.value || !webPhoneForm.destination.trim()"
|
|
520
|
+
@click="startWebCall"
|
|
521
|
+
/>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<div v-if="webPhone.hasCall.value" class="active-call-controls">
|
|
525
|
+
<v-btn
|
|
526
|
+
:icon="webPhone.muted.value ? 'mdi-microphone-off' : 'mdi-microphone'"
|
|
527
|
+
variant="outlined"
|
|
528
|
+
:disabled="webPhone.callState.value !== 'active'"
|
|
529
|
+
:title="webPhone.muted.value ? 'Unmute' : 'Mute'"
|
|
530
|
+
@click="toggleWebMute"
|
|
531
|
+
/>
|
|
532
|
+
<v-btn
|
|
533
|
+
v-if="webPhoneForm.video"
|
|
534
|
+
:icon="webPhone.cameraEnabled.value ? 'mdi-video' : 'mdi-video-off'"
|
|
535
|
+
variant="outlined"
|
|
536
|
+
:disabled="webPhone.callState.value !== 'active' || !webPhone.localVideoAvailable.value"
|
|
537
|
+
:title="webPhone.cameraEnabled.value ? 'Turn camera off' : 'Turn camera on'"
|
|
538
|
+
@click="toggleWebCamera"
|
|
539
|
+
/>
|
|
540
|
+
<v-btn
|
|
541
|
+
icon="mdi-phone-hangup"
|
|
542
|
+
class="hangup-web-call"
|
|
543
|
+
variant="flat"
|
|
544
|
+
:loading="webPhoneActionLoading && webPhone.callState.value === 'ending'"
|
|
545
|
+
title="End call"
|
|
546
|
+
@click="hangupWebCall"
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
</template>
|
|
550
|
+
|
|
551
|
+
<v-alert v-if="webPhone.errorMessage.value" type="error" variant="tonal" density="compact" class="web-phone-error">
|
|
552
|
+
{{ webPhone.errorMessage.value }}
|
|
553
|
+
</v-alert>
|
|
554
|
+
</div>
|
|
555
|
+
</v-card-text>
|
|
556
|
+
|
|
557
|
+
<v-card-actions class="dialog-actions pa-0">
|
|
558
|
+
<v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="webPhoneDialog = false">
|
|
559
|
+
Close
|
|
560
|
+
</v-btn>
|
|
393
561
|
</v-card-actions>
|
|
394
562
|
</v-card>
|
|
395
563
|
</v-dialog>
|
|
@@ -448,6 +616,10 @@
|
|
|
448
616
|
|
|
449
617
|
<script setup lang="ts">
|
|
450
618
|
const props = defineProps({
|
|
619
|
+
org: {
|
|
620
|
+
type: String,
|
|
621
|
+
required: true,
|
|
622
|
+
},
|
|
451
623
|
site: {
|
|
452
624
|
type: String,
|
|
453
625
|
required: true,
|
|
@@ -466,6 +638,19 @@ type ContactRow = {
|
|
|
466
638
|
dialCode: string;
|
|
467
639
|
};
|
|
468
640
|
|
|
641
|
+
type SipAccount = {
|
|
642
|
+
orgId: string;
|
|
643
|
+
site: string;
|
|
644
|
+
userId: string;
|
|
645
|
+
extension: string;
|
|
646
|
+
username: string;
|
|
647
|
+
password: string;
|
|
648
|
+
sipAddress: string;
|
|
649
|
+
websocketUrl: string;
|
|
650
|
+
status: "active";
|
|
651
|
+
provisionedAt: string;
|
|
652
|
+
};
|
|
653
|
+
|
|
469
654
|
type DetailItem = {
|
|
470
655
|
label: string;
|
|
471
656
|
value?: string | number | boolean;
|
|
@@ -474,7 +659,7 @@ type DetailItem = {
|
|
|
474
659
|
};
|
|
475
660
|
|
|
476
661
|
type IntercomRow = {
|
|
477
|
-
reader: Record<string,
|
|
662
|
+
reader: Record<string, unknown>;
|
|
478
663
|
name: string;
|
|
479
664
|
location: string;
|
|
480
665
|
serverAddress: string;
|
|
@@ -490,6 +675,7 @@ type IntercomRow = {
|
|
|
490
675
|
password: string;
|
|
491
676
|
operatingMode: string;
|
|
492
677
|
protocol: string;
|
|
678
|
+
videoEnabled: boolean;
|
|
493
679
|
dialMode: string;
|
|
494
680
|
dialModeLabel: string;
|
|
495
681
|
dialCode: string;
|
|
@@ -502,7 +688,7 @@ type IntercomRow = {
|
|
|
502
688
|
externalDialButton: boolean;
|
|
503
689
|
unknownContact: boolean;
|
|
504
690
|
callAfter: string;
|
|
505
|
-
pjsip: Record<string,
|
|
691
|
+
pjsip: Record<string, unknown>;
|
|
506
692
|
};
|
|
507
693
|
|
|
508
694
|
const {
|
|
@@ -513,6 +699,7 @@ const {
|
|
|
513
699
|
getIntercomStatus,
|
|
514
700
|
makeIntercomCall,
|
|
515
701
|
finalizeIntercomCall,
|
|
702
|
+
ensureSipAccount,
|
|
516
703
|
} = useHidAmico();
|
|
517
704
|
|
|
518
705
|
const activeTab = ref<IntercomTab>("intercom");
|
|
@@ -531,21 +718,27 @@ const limit = 20;
|
|
|
531
718
|
const selectedRow = ref<IntercomRow | null>(null);
|
|
532
719
|
const selectedContact = ref<ContactRow | null>(null);
|
|
533
720
|
const showSipPassword = ref(false);
|
|
721
|
+
const webPhoneConnecting = ref(false);
|
|
722
|
+
const webPhoneActionLoading = ref(false);
|
|
723
|
+
const sipAccount = ref<SipAccount | null>(null);
|
|
724
|
+
const sipAccountLoading = ref(false);
|
|
725
|
+
const sipAccountError = ref("");
|
|
726
|
+
const localVideoEl = ref<HTMLVideoElement | null>(null);
|
|
727
|
+
const remoteVideoEl = ref<HTMLVideoElement | null>(null);
|
|
728
|
+
const remoteAudioEl = ref<HTMLAudioElement | null>(null);
|
|
729
|
+
const webPhone = useSipWebPhone();
|
|
534
730
|
|
|
535
731
|
const viewDialog = ref(false);
|
|
536
732
|
const sipDialog = ref(false);
|
|
537
|
-
const rebootConfirmDialog = ref(false);
|
|
538
|
-
const rebootingDialog = ref(false);
|
|
539
733
|
const callDialog = ref(false);
|
|
540
734
|
const dialCodeDialog = ref(false);
|
|
541
735
|
const makeCallDialog = ref(false);
|
|
736
|
+
const webPhoneDialog = ref(false);
|
|
542
737
|
const contactDialog = ref(false);
|
|
543
738
|
const contactViewDialog = ref(false);
|
|
544
739
|
const deleteContactDialog = ref(false);
|
|
545
740
|
|
|
546
741
|
const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
|
|
547
|
-
const rebootCountdown = ref(59);
|
|
548
|
-
let rebootTimer: ReturnType<typeof setInterval> | undefined;
|
|
549
742
|
|
|
550
743
|
const statusOptions = ["Status", "Connected", "Not connected", "Connecting", "Disabled", "Failed"];
|
|
551
744
|
|
|
@@ -582,13 +775,14 @@ const sipForm = reactive({
|
|
|
582
775
|
serverIp: "",
|
|
583
776
|
serverPort: "5060",
|
|
584
777
|
initialRtpPort: "10000",
|
|
585
|
-
finalRtpPort: "
|
|
778
|
+
finalRtpPort: "10100",
|
|
586
779
|
numericExtension: false,
|
|
587
780
|
extension: "",
|
|
588
781
|
login: "",
|
|
589
782
|
password: "",
|
|
590
783
|
operatingMode: "sip-client",
|
|
591
784
|
protocol: "UDP",
|
|
785
|
+
videoEnabled: false,
|
|
592
786
|
});
|
|
593
787
|
|
|
594
788
|
const callForm = reactive({
|
|
@@ -617,6 +811,11 @@ const makeCallForm = reactive({
|
|
|
617
811
|
statusType: "info" as MessageType,
|
|
618
812
|
});
|
|
619
813
|
|
|
814
|
+
const webPhoneForm = reactive({
|
|
815
|
+
destination: "",
|
|
816
|
+
video: false,
|
|
817
|
+
});
|
|
818
|
+
|
|
620
819
|
const contactForm = reactive({
|
|
621
820
|
id: undefined as number | undefined,
|
|
622
821
|
readerId: "",
|
|
@@ -694,6 +893,7 @@ const intercomDetailSections = computed(() => {
|
|
|
694
893
|
items: [
|
|
695
894
|
{ label: "Operating Mode", value: row?.operatingMode },
|
|
696
895
|
{ label: "Protocol", value: row?.protocol },
|
|
896
|
+
{ label: "Video", value: row?.videoEnabled ? "Enabled" : "Disabled" },
|
|
697
897
|
],
|
|
698
898
|
},
|
|
699
899
|
{
|
|
@@ -717,10 +917,45 @@ const contactDetailRows = computed<DetailItem[]>(() => [
|
|
|
717
917
|
{ label: "Server Address", value: selectedContact.value?.serverAddress, link: true },
|
|
718
918
|
{ label: "Location", value: selectedContact.value?.location },
|
|
719
919
|
]);
|
|
920
|
+
const webPhoneStatusLabel = computed(() => {
|
|
921
|
+
if (webPhone.callState.value === "incoming") return "Incoming call";
|
|
922
|
+
if (webPhone.callState.value === "calling") return "Calling";
|
|
923
|
+
if (webPhone.callState.value === "active") return "In call";
|
|
924
|
+
if (webPhone.callState.value === "ending") return "Ending call";
|
|
925
|
+
if (webPhone.connectionState.value === "connecting") return "Connecting";
|
|
926
|
+
if (webPhone.connectionState.value === "registered") return "Ready";
|
|
927
|
+
if (webPhone.connectionState.value === "error") return "Connection failed";
|
|
928
|
+
return "Disconnected";
|
|
929
|
+
});
|
|
930
|
+
const webPhoneStatusClass = computed(() => {
|
|
931
|
+
if (webPhone.callState.value === "incoming") return "web-phone-status incoming";
|
|
932
|
+
if (webPhone.callState.value === "active" || webPhone.connectionState.value === "registered") return "web-phone-status ready";
|
|
933
|
+
if (webPhone.connectionState.value === "error") return "web-phone-status failed";
|
|
934
|
+
return "web-phone-status idle";
|
|
935
|
+
});
|
|
936
|
+
const webPhoneCallLabel = computed(() => {
|
|
937
|
+
if (webPhone.callState.value === "incoming") return "Incoming call";
|
|
938
|
+
if (webPhone.callState.value === "calling") return "Calling...";
|
|
939
|
+
if (webPhone.callState.value === "active") return "Call connected";
|
|
940
|
+
if (webPhone.callState.value === "ending") return "Ending call...";
|
|
941
|
+
return webPhone.isRegistered.value ? "Ready to call" : "Connect a SIP account";
|
|
942
|
+
});
|
|
943
|
+
const webPhoneCallIcon = computed(() => {
|
|
944
|
+
if (webPhone.callState.value === "incoming") return "mdi-phone-incoming";
|
|
945
|
+
if (webPhone.callState.value === "calling") return "mdi-phone-outgoing";
|
|
946
|
+
if (webPhone.callState.value === "active") return "mdi-phone-in-talk";
|
|
947
|
+
return "mdi-phone-outline";
|
|
948
|
+
});
|
|
720
949
|
|
|
721
950
|
watch(
|
|
722
|
-
() => props.site,
|
|
723
|
-
() =>
|
|
951
|
+
[() => props.org, () => props.site],
|
|
952
|
+
() => {
|
|
953
|
+
if (webPhone.isRegistered.value) void webPhone.disconnect();
|
|
954
|
+
sipAccount.value = null;
|
|
955
|
+
sipAccountError.value = "";
|
|
956
|
+
webPhoneForm.destination = "";
|
|
957
|
+
void reload();
|
|
958
|
+
},
|
|
724
959
|
{ immediate: true },
|
|
725
960
|
);
|
|
726
961
|
|
|
@@ -732,9 +967,12 @@ watch(pages, () => {
|
|
|
732
967
|
if (page.value > pages.value) page.value = pages.value;
|
|
733
968
|
});
|
|
734
969
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
970
|
+
watch(
|
|
971
|
+
() => webPhone.callState.value,
|
|
972
|
+
(state) => {
|
|
973
|
+
if (state === "incoming") webPhoneDialog.value = true;
|
|
974
|
+
},
|
|
975
|
+
);
|
|
738
976
|
|
|
739
977
|
async function reload() {
|
|
740
978
|
if (!props.site) return;
|
|
@@ -798,19 +1036,27 @@ function paginate<T>(items: T[]) {
|
|
|
798
1036
|
return items.slice(start, start + limit);
|
|
799
1037
|
}
|
|
800
1038
|
|
|
801
|
-
function toIntercomRow(reader: Record<string,
|
|
1039
|
+
function toIntercomRow(reader: Record<string, unknown>, pjsip: Record<string, unknown>, status: string): IntercomRow {
|
|
802
1040
|
const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
|
|
803
1041
|
const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
|
|
1042
|
+
const initialRtpPort = toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A");
|
|
1043
|
+
const rtpPortRange = getPjsipValue(pjsip, ["server_outbound_port_range"]);
|
|
1044
|
+
const explicitFinalRtpPort = getPjsipValue(pjsip, ["finalRtpPort", "final_rtp_port"]);
|
|
1045
|
+
const initialRtpPortNumber = Number(initialRtpPort);
|
|
1046
|
+
const rtpPortRangeNumber = Number(rtpPortRange);
|
|
1047
|
+
const finalRtpPort = rtpPortRange !== undefined && Number.isFinite(initialRtpPortNumber) && Number.isFinite(rtpPortRangeNumber)
|
|
1048
|
+
? String(initialRtpPortNumber + rtpPortRangeNumber)
|
|
1049
|
+
: toText(explicitFinalRtpPort, "N/A");
|
|
804
1050
|
|
|
805
1051
|
return {
|
|
806
1052
|
reader,
|
|
807
|
-
name: reader.name || reader.deviceId
|
|
808
|
-
location: reader.location
|
|
1053
|
+
name: toText(reader.name || reader.deviceId, "HID Reader"),
|
|
1054
|
+
location: toText(reader.location, "N/A"),
|
|
809
1055
|
serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
|
|
810
1056
|
serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
|
|
811
|
-
initialRtpPort
|
|
812
|
-
finalRtpPort
|
|
813
|
-
monitorPath: reader.monitorPath
|
|
1057
|
+
initialRtpPort,
|
|
1058
|
+
finalRtpPort,
|
|
1059
|
+
monitorPath: toText(reader.monitorPath, "N/A"),
|
|
814
1060
|
status,
|
|
815
1061
|
sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
|
|
816
1062
|
numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
|
|
@@ -819,6 +1065,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
819
1065
|
password: toText(getPjsipValue(pjsip, ["password"]), ""),
|
|
820
1066
|
operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
|
|
821
1067
|
protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
|
|
1068
|
+
videoEnabled: toFlag(getPjsipValue(pjsip, ["video_enabled", "videoEnabled"])) === "1",
|
|
822
1069
|
dialMode,
|
|
823
1070
|
dialModeLabel: getDialModeLabel(dialMode),
|
|
824
1071
|
dialCode: toText(getPjsipValue(pjsip, ["open_door_command", "dialCode"]), "N/A"),
|
|
@@ -835,7 +1082,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
|
|
|
835
1082
|
};
|
|
836
1083
|
}
|
|
837
1084
|
|
|
838
|
-
function normalizePjsip(...sources: Record<string,
|
|
1085
|
+
function normalizePjsip(...sources: Record<string, unknown>[]) {
|
|
839
1086
|
for (const source of sources) {
|
|
840
1087
|
const candidate = findPjsipCandidate(source);
|
|
841
1088
|
if (candidate) return candidate;
|
|
@@ -843,45 +1090,46 @@ function normalizePjsip(...sources: Record<string, any>[]) {
|
|
|
843
1090
|
return {};
|
|
844
1091
|
}
|
|
845
1092
|
|
|
846
|
-
function findPjsipCandidate(source: Record<string,
|
|
1093
|
+
function findPjsipCandidate(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
847
1094
|
if (!source || typeof source !== "object") return undefined;
|
|
848
1095
|
const candidates = [
|
|
849
1096
|
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
|
|
1097
|
+
toRecord(source.pjsip),
|
|
1098
|
+
toRecord(toRecord(source.configuration).pjsip),
|
|
1099
|
+
toRecord(toRecord(source.result).pjsip),
|
|
1100
|
+
toRecord(toRecord(toRecord(source.result).configuration).pjsip),
|
|
1101
|
+
toRecord(toRecord(source.data).pjsip),
|
|
1102
|
+
toRecord(toRecord(toRecord(source.data).configuration).pjsip),
|
|
1103
|
+
toRecord(toRecord(toRecord(source.data).result).pjsip),
|
|
1104
|
+
toRecord(toRecord(toRecord(toRecord(source.data).result).configuration).pjsip),
|
|
1105
|
+
toRecord(toRecord(toRecord(source.data).data).pjsip),
|
|
1106
|
+
toRecord(toRecord(toRecord(toRecord(source.data).data).configuration).pjsip),
|
|
1107
|
+
toRecord(source.configuration),
|
|
1108
|
+
toRecord(toRecord(source.data).configuration),
|
|
1109
|
+
toRecord(toRecord(toRecord(source.data).data).configuration),
|
|
862
1110
|
source,
|
|
863
1111
|
];
|
|
864
1112
|
|
|
865
1113
|
return candidates.find((candidate) => isPjsipCandidate(candidate));
|
|
866
1114
|
}
|
|
867
1115
|
|
|
868
|
-
function isPjsipCandidate(value: unknown): value is Record<string,
|
|
1116
|
+
function isPjsipCandidate(value: unknown): value is Record<string, unknown> {
|
|
869
1117
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
870
1118
|
return pjsipKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)) ||
|
|
871
1119
|
["serverAddress", "serverIp", "serverPort", "extension", "peerToPeerEnabled"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
872
1120
|
}
|
|
873
1121
|
|
|
874
|
-
function getPjsipValue(source: Record<string,
|
|
1122
|
+
function getPjsipValue(source: Record<string, unknown>, keys: string[]) {
|
|
875
1123
|
for (const key of keys) {
|
|
876
1124
|
if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== "") return source[key];
|
|
877
1125
|
}
|
|
878
1126
|
return undefined;
|
|
879
1127
|
}
|
|
880
1128
|
|
|
881
|
-
function normalizeStatus(response: Record<string,
|
|
1129
|
+
function normalizeStatus(response: Record<string, unknown>, pjsip: Record<string, unknown>) {
|
|
882
1130
|
const status = Number(response?.status);
|
|
1131
|
+
if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
|
|
883
1132
|
if (status === 200) return "Connected";
|
|
884
|
-
if (status === -1 || toFlag(pjsip.enabled) === "0") return "Disabled";
|
|
885
1133
|
if (status === 0 || status === 100) return "Connecting";
|
|
886
1134
|
if ([401, 403, 408, 503].includes(status)) return "Failed";
|
|
887
1135
|
return "Not connected";
|
|
@@ -911,13 +1159,14 @@ function openSipDialog(row: IntercomRow) {
|
|
|
911
1159
|
sipForm.serverIp = row.serverAddress === "N/A" ? "" : row.serverAddress;
|
|
912
1160
|
sipForm.serverPort = row.serverPort === "N/A" ? "5060" : row.serverPort;
|
|
913
1161
|
sipForm.initialRtpPort = row.initialRtpPort === "N/A" ? "10000" : row.initialRtpPort;
|
|
914
|
-
sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "
|
|
1162
|
+
sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "10100" : row.finalRtpPort;
|
|
915
1163
|
sipForm.numericExtension = row.numericExtension;
|
|
916
1164
|
sipForm.extension = row.extension === "N/A" ? "" : row.extension;
|
|
917
1165
|
sipForm.login = row.login === "N/A" ? "" : row.login;
|
|
918
1166
|
sipForm.password = row.password;
|
|
919
1167
|
sipForm.operatingMode = row.operatingMode === "Peer-to-Peer" ? "peer-to-peer" : "sip-client";
|
|
920
1168
|
sipForm.protocol = row.protocol || "UDP";
|
|
1169
|
+
sipForm.videoEnabled = row.videoEnabled;
|
|
921
1170
|
sipDialog.value = true;
|
|
922
1171
|
}
|
|
923
1172
|
|
|
@@ -945,12 +1194,143 @@ function openDialCodeDialog(row: IntercomRow) {
|
|
|
945
1194
|
|
|
946
1195
|
function openMakeCallDialog(row: IntercomRow) {
|
|
947
1196
|
selectedRow.value = row;
|
|
948
|
-
makeCallForm.target = row.
|
|
1197
|
+
makeCallForm.target = isIpAddress(row.serverAddress) ? row.serverAddress : "";
|
|
949
1198
|
makeCallForm.status = "";
|
|
950
1199
|
makeCallForm.statusType = "info";
|
|
951
1200
|
makeCallDialog.value = true;
|
|
952
1201
|
}
|
|
953
1202
|
|
|
1203
|
+
function openWebPhoneDialog(row?: IntercomRow) {
|
|
1204
|
+
const activeRow = row || selectedRow.value || intercomRows.value[0];
|
|
1205
|
+
if (activeRow) {
|
|
1206
|
+
selectedRow.value = activeRow;
|
|
1207
|
+
if (!webPhone.isRegistered.value && activeRow.extension !== "N/A") {
|
|
1208
|
+
webPhoneForm.destination = activeRow.extension;
|
|
1209
|
+
webPhoneForm.video = activeRow.videoEnabled;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
webPhoneDialog.value = true;
|
|
1213
|
+
if (!sipAccount.value && !sipAccountLoading.value) void loadSipAccount();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
async function loadSipAccount() {
|
|
1217
|
+
if (!props.org || !props.site) return;
|
|
1218
|
+
|
|
1219
|
+
sipAccountLoading.value = true;
|
|
1220
|
+
sipAccountError.value = "";
|
|
1221
|
+
try {
|
|
1222
|
+
const response = await ensureSipAccount(props.site, props.org);
|
|
1223
|
+
sipAccount.value = normalizeSipAccount(response);
|
|
1224
|
+
} catch (error: unknown) {
|
|
1225
|
+
sipAccount.value = null;
|
|
1226
|
+
sipAccountError.value = getErrorMessage(error);
|
|
1227
|
+
} finally {
|
|
1228
|
+
sipAccountLoading.value = false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function connectWebPhone() {
|
|
1233
|
+
const account = sipAccount.value;
|
|
1234
|
+
if (!account) {
|
|
1235
|
+
await loadSipAccount();
|
|
1236
|
+
if (!sipAccount.value) return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const activeAccount = sipAccount.value;
|
|
1240
|
+
if (!activeAccount) return;
|
|
1241
|
+
|
|
1242
|
+
webPhoneConnecting.value = true;
|
|
1243
|
+
try {
|
|
1244
|
+
await webPhone.connect({
|
|
1245
|
+
webSocketServer: activeAccount.websocketUrl,
|
|
1246
|
+
aor: activeAccount.sipAddress,
|
|
1247
|
+
authorizationUsername: activeAccount.username,
|
|
1248
|
+
authorizationPassword: activeAccount.password,
|
|
1249
|
+
displayName: "iService365 Web",
|
|
1250
|
+
video: webPhoneForm.video,
|
|
1251
|
+
}, {
|
|
1252
|
+
localVideo: localVideoEl.value,
|
|
1253
|
+
remoteVideo: remoteVideoEl.value,
|
|
1254
|
+
remoteAudio: remoteAudioEl.value,
|
|
1255
|
+
});
|
|
1256
|
+
showMessage("Web phone connected and ready to receive calls.", "success");
|
|
1257
|
+
} catch (error: unknown) {
|
|
1258
|
+
showMessage(getErrorMessage(error), "error");
|
|
1259
|
+
} finally {
|
|
1260
|
+
webPhoneConnecting.value = false;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async function disconnectWebPhone() {
|
|
1265
|
+
webPhoneConnecting.value = true;
|
|
1266
|
+
try {
|
|
1267
|
+
await webPhone.disconnect();
|
|
1268
|
+
showMessage("Web phone disconnected.", "info");
|
|
1269
|
+
} finally {
|
|
1270
|
+
webPhoneConnecting.value = false;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function startWebCall() {
|
|
1275
|
+
webPhoneActionLoading.value = true;
|
|
1276
|
+
try {
|
|
1277
|
+
await webPhone.call(webPhoneForm.destination);
|
|
1278
|
+
} catch (error: unknown) {
|
|
1279
|
+
showMessage(getErrorMessage(error), "error");
|
|
1280
|
+
} finally {
|
|
1281
|
+
webPhoneActionLoading.value = false;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function answerWebCall() {
|
|
1286
|
+
webPhoneActionLoading.value = true;
|
|
1287
|
+
try {
|
|
1288
|
+
await webPhone.answer();
|
|
1289
|
+
} catch (error: unknown) {
|
|
1290
|
+
showMessage(getErrorMessage(error), "error");
|
|
1291
|
+
} finally {
|
|
1292
|
+
webPhoneActionLoading.value = false;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
async function declineWebCall() {
|
|
1297
|
+
webPhoneActionLoading.value = true;
|
|
1298
|
+
try {
|
|
1299
|
+
await webPhone.decline();
|
|
1300
|
+
} catch (error: unknown) {
|
|
1301
|
+
showMessage(getErrorMessage(error), "error");
|
|
1302
|
+
} finally {
|
|
1303
|
+
webPhoneActionLoading.value = false;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function hangupWebCall() {
|
|
1308
|
+
webPhoneActionLoading.value = true;
|
|
1309
|
+
try {
|
|
1310
|
+
await webPhone.hangup();
|
|
1311
|
+
} catch (error: unknown) {
|
|
1312
|
+
showMessage(getErrorMessage(error), "error");
|
|
1313
|
+
} finally {
|
|
1314
|
+
webPhoneActionLoading.value = false;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function toggleWebCamera() {
|
|
1319
|
+
try {
|
|
1320
|
+
webPhone.toggleCamera();
|
|
1321
|
+
} catch (error: unknown) {
|
|
1322
|
+
showMessage(getErrorMessage(error), "error");
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function toggleWebMute() {
|
|
1327
|
+
try {
|
|
1328
|
+
webPhone.toggleMute();
|
|
1329
|
+
} catch (error: unknown) {
|
|
1330
|
+
showMessage(getErrorMessage(error), "error");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
954
1334
|
function openContactDialog(contact?: ContactRow) {
|
|
955
1335
|
selectedContact.value = contact || null;
|
|
956
1336
|
contactForm.id = contact?.id;
|
|
@@ -974,21 +1354,35 @@ function openDeleteContact(contact: ContactRow) {
|
|
|
974
1354
|
async function saveSipSettings() {
|
|
975
1355
|
if (!selectedRow.value) return;
|
|
976
1356
|
const isPeerToPeer = sipForm.operatingMode === "peer-to-peer";
|
|
1357
|
+
const initialRtpPort = Number(sipForm.initialRtpPort);
|
|
1358
|
+
const finalRtpPort = Number(sipForm.finalRtpPort);
|
|
1359
|
+
|
|
1360
|
+
if (
|
|
1361
|
+
!Number.isInteger(initialRtpPort) ||
|
|
1362
|
+
!Number.isInteger(finalRtpPort) ||
|
|
1363
|
+
initialRtpPort < 1 ||
|
|
1364
|
+
finalRtpPort > 65535 ||
|
|
1365
|
+
finalRtpPort <= initialRtpPort
|
|
1366
|
+
) {
|
|
1367
|
+
showMessage("Final RTP Port must be greater than Initial RTP Port and both ports must be between 1 and 65535.", "warning");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
977
1371
|
const saved = await savePjsip(selectedRow.value.reader._id, {
|
|
978
1372
|
enabled: sipForm.enabled ? "1" : "0",
|
|
979
1373
|
server_ip: sipForm.serverIp,
|
|
980
1374
|
server_port: sipForm.serverPort,
|
|
981
|
-
server_outbound_port:
|
|
982
|
-
server_outbound_port_range:
|
|
1375
|
+
server_outbound_port: String(initialRtpPort),
|
|
1376
|
+
server_outbound_port_range: String(finalRtpPort - initialRtpPort),
|
|
983
1377
|
numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
|
|
984
1378
|
branch: isPeerToPeer ? "" : sipForm.extension,
|
|
985
1379
|
login: isPeerToPeer ? "" : sipForm.login,
|
|
986
1380
|
password: isPeerToPeer ? "" : sipForm.password,
|
|
987
1381
|
peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
|
|
988
|
-
|
|
1382
|
+
video_enabled: sipForm.videoEnabled ? "1" : "0",
|
|
1383
|
+
});
|
|
989
1384
|
if (saved) {
|
|
990
1385
|
sipDialog.value = false;
|
|
991
|
-
rebootConfirmDialog.value = true;
|
|
992
1386
|
}
|
|
993
1387
|
}
|
|
994
1388
|
|
|
@@ -1020,7 +1414,13 @@ async function runMakeCall() {
|
|
|
1020
1414
|
if (!selectedRow.value) return;
|
|
1021
1415
|
const target = makeCallForm.target.trim();
|
|
1022
1416
|
if (!target) {
|
|
1023
|
-
makeCallForm.status = "Please enter an address to dial.";
|
|
1417
|
+
makeCallForm.status = "Please enter an IP address to dial.";
|
|
1418
|
+
makeCallForm.statusType = "warning";
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (!isIpAddress(target)) {
|
|
1423
|
+
makeCallForm.status = "Address to dial must be an IP address.";
|
|
1024
1424
|
makeCallForm.statusType = "warning";
|
|
1025
1425
|
return;
|
|
1026
1426
|
}
|
|
@@ -1042,6 +1442,15 @@ async function runMakeCall() {
|
|
|
1042
1442
|
}
|
|
1043
1443
|
}
|
|
1044
1444
|
|
|
1445
|
+
function isIpAddress(value = "") {
|
|
1446
|
+
const parts = value.trim().split(".");
|
|
1447
|
+
return parts.length === 4 && parts.every((part) => {
|
|
1448
|
+
if (!/^\d{1,3}$/.test(part)) return false;
|
|
1449
|
+
const value = Number(part);
|
|
1450
|
+
return value >= 0 && value <= 255;
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1045
1454
|
async function runHangupCall() {
|
|
1046
1455
|
if (!selectedRow.value) return;
|
|
1047
1456
|
hangingUp.value = true;
|
|
@@ -1078,30 +1487,6 @@ async function savePjsip(readerId: string, payload: Record<string, string>, opti
|
|
|
1078
1487
|
}
|
|
1079
1488
|
}
|
|
1080
1489
|
|
|
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
1490
|
async function saveContact() {
|
|
1106
1491
|
if (!contactForm.readerId || !contactForm.name.trim() || !contactForm.serverAddress.trim()) {
|
|
1107
1492
|
showMessage("Please complete the required contact fields.", "warning");
|
|
@@ -1182,8 +1567,42 @@ function showMessage(text: string, type: MessageType = "info") {
|
|
|
1182
1567
|
message.type = type;
|
|
1183
1568
|
}
|
|
1184
1569
|
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1570
|
+
function normalizeSipAccount(response: unknown): SipAccount {
|
|
1571
|
+
const responseRecord = toRecord(response);
|
|
1572
|
+
const payload = toRecord(responseRecord.data || responseRecord);
|
|
1573
|
+
const requiredFields = ["extension", "username", "password", "sipAddress", "websocketUrl"] as const;
|
|
1574
|
+
|
|
1575
|
+
if (requiredFields.some((field) => !toText(payload[field]))) {
|
|
1576
|
+
throw new Error("The SIP account response is incomplete. Please try again.");
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
return {
|
|
1580
|
+
orgId: toText(payload.orgId, props.org),
|
|
1581
|
+
site: toText(payload.site, props.site),
|
|
1582
|
+
userId: toText(payload.userId),
|
|
1583
|
+
extension: toText(payload.extension),
|
|
1584
|
+
username: toText(payload.username),
|
|
1585
|
+
password: toText(payload.password),
|
|
1586
|
+
sipAddress: toText(payload.sipAddress),
|
|
1587
|
+
websocketUrl: toText(payload.websocketUrl),
|
|
1588
|
+
status: "active",
|
|
1589
|
+
provisionedAt: toText(payload.provisionedAt),
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function getErrorMessage(error: unknown) {
|
|
1594
|
+
const errorRecord = toRecord(error);
|
|
1595
|
+
const data = toRecord(errorRecord.data);
|
|
1596
|
+
const response = toRecord(errorRecord.response);
|
|
1597
|
+
const responseData = toRecord(response._data);
|
|
1598
|
+
return toText(data.message)
|
|
1599
|
+
|| toText(responseData.message)
|
|
1600
|
+
|| toText(errorRecord.message)
|
|
1601
|
+
|| "HID intercom request failed.";
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
1605
|
+
return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
|
|
1187
1606
|
}
|
|
1188
1607
|
|
|
1189
1608
|
function toText(value: unknown, fallback = "") {
|
|
@@ -1259,6 +1678,23 @@ export default {
|
|
|
1259
1678
|
width: 160px;
|
|
1260
1679
|
}
|
|
1261
1680
|
|
|
1681
|
+
.web-call-button {
|
|
1682
|
+
min-width: 160px;
|
|
1683
|
+
background: #e5e7eb;
|
|
1684
|
+
color: #0b2c40;
|
|
1685
|
+
font-size: 12px;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.web-call-button.is-connected {
|
|
1689
|
+
background: #d9f2e1;
|
|
1690
|
+
color: #16743a;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.web-call-button.has-incoming-call {
|
|
1694
|
+
background: #1769aa;
|
|
1695
|
+
color: #fff;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1262
1698
|
.add-button {
|
|
1263
1699
|
min-width: 160px;
|
|
1264
1700
|
background: #dedede;
|
|
@@ -1468,6 +1904,20 @@ export default {
|
|
|
1468
1904
|
margin-bottom: 12px;
|
|
1469
1905
|
}
|
|
1470
1906
|
|
|
1907
|
+
.sip-section-switch > div {
|
|
1908
|
+
display: flex;
|
|
1909
|
+
min-width: 0;
|
|
1910
|
+
padding: 8px 12px 8px 0;
|
|
1911
|
+
flex-direction: column;
|
|
1912
|
+
gap: 2px;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
.sip-section-switch small {
|
|
1916
|
+
color: #667085;
|
|
1917
|
+
font-size: 10px;
|
|
1918
|
+
line-height: 1.4;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1471
1921
|
.sip-switch-row :deep(.v-switch) {
|
|
1472
1922
|
flex: 0 0 auto;
|
|
1473
1923
|
}
|
|
@@ -1630,98 +2080,6 @@ export default {
|
|
|
1630
2080
|
overflow: hidden;
|
|
1631
2081
|
}
|
|
1632
2082
|
|
|
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
2083
|
.call-dialog .dialog-body {
|
|
1726
2084
|
padding: 18px 24px 24px;
|
|
1727
2085
|
}
|
|
@@ -1931,6 +2289,280 @@ export default {
|
|
|
1931
2289
|
line-height: 1.45;
|
|
1932
2290
|
}
|
|
1933
2291
|
|
|
2292
|
+
.web-phone-dialog {
|
|
2293
|
+
width: 820px;
|
|
2294
|
+
max-width: 94vw;
|
|
2295
|
+
overflow: hidden;
|
|
2296
|
+
background: #fff;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
.web-phone-title {
|
|
2300
|
+
display: flex;
|
|
2301
|
+
align-items: center;
|
|
2302
|
+
justify-content: space-between;
|
|
2303
|
+
gap: 16px;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
.web-phone-status {
|
|
2307
|
+
min-width: 92px;
|
|
2308
|
+
justify-content: center;
|
|
2309
|
+
font-size: 10px;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
.web-phone-status.ready {
|
|
2313
|
+
background: #d9f2e1;
|
|
2314
|
+
color: #16743a;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
.web-phone-status.incoming {
|
|
2318
|
+
background: #dcecff;
|
|
2319
|
+
color: #125c9a;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
.web-phone-status.failed {
|
|
2323
|
+
background: #fce2e2;
|
|
2324
|
+
color: #a92222;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
.web-phone-status.idle {
|
|
2328
|
+
background: #e5e7eb;
|
|
2329
|
+
color: #5d6673;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
.web-phone-body {
|
|
2333
|
+
display: grid;
|
|
2334
|
+
grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr);
|
|
2335
|
+
gap: 24px;
|
|
2336
|
+
min-height: 440px;
|
|
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
|
+
.local-video {
|
|
2466
|
+
position: absolute;
|
|
2467
|
+
right: 12px;
|
|
2468
|
+
bottom: 12px;
|
|
2469
|
+
display: none;
|
|
2470
|
+
width: 108px;
|
|
2471
|
+
aspect-ratio: 3 / 4;
|
|
2472
|
+
border: 2px solid #fff;
|
|
2473
|
+
border-radius: 4px;
|
|
2474
|
+
background: #263b49;
|
|
2475
|
+
object-fit: cover;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
.web-phone-media.has-local-video .local-video {
|
|
2479
|
+
display: block;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
.web-phone-media.has-remote-video .audio-call-state {
|
|
2483
|
+
display: none;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
.audio-call-state {
|
|
2487
|
+
display: flex;
|
|
2488
|
+
align-items: center;
|
|
2489
|
+
justify-content: center;
|
|
2490
|
+
padding: 28px;
|
|
2491
|
+
color: #fff;
|
|
2492
|
+
flex-direction: column;
|
|
2493
|
+
gap: 10px;
|
|
2494
|
+
text-align: center;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.audio-call-state strong {
|
|
2498
|
+
font-size: 16px;
|
|
2499
|
+
font-weight: 600;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
.audio-call-state span {
|
|
2503
|
+
max-width: 100%;
|
|
2504
|
+
overflow: hidden;
|
|
2505
|
+
color: #c9d7e1;
|
|
2506
|
+
font-size: 11px;
|
|
2507
|
+
text-overflow: ellipsis;
|
|
2508
|
+
white-space: nowrap;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
.web-phone-dial-row {
|
|
2512
|
+
display: grid;
|
|
2513
|
+
grid-template-columns: minmax(0, 1fr) 42px;
|
|
2514
|
+
gap: 8px;
|
|
2515
|
+
align-items: start;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
.start-web-call,
|
|
2519
|
+
.hangup-web-call {
|
|
2520
|
+
width: 42px;
|
|
2521
|
+
height: 38px;
|
|
2522
|
+
border-radius: 4px;
|
|
2523
|
+
background: #2a9b50;
|
|
2524
|
+
color: #fff;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
.hangup-web-call,
|
|
2528
|
+
.reject-call {
|
|
2529
|
+
background: #c9293d;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
.incoming-call-actions {
|
|
2533
|
+
display: grid;
|
|
2534
|
+
grid-template-columns: 1fr 1fr;
|
|
2535
|
+
gap: 10px;
|
|
2536
|
+
margin-top: 14px;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
.answer-call,
|
|
2540
|
+
.reject-call {
|
|
2541
|
+
min-height: 42px;
|
|
2542
|
+
color: #fff;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
.answer-call {
|
|
2546
|
+
background: #2a9b50;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
.active-call-controls {
|
|
2550
|
+
display: flex;
|
|
2551
|
+
justify-content: center;
|
|
2552
|
+
gap: 12px;
|
|
2553
|
+
margin-top: 16px;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
.active-call-controls .v-btn {
|
|
2557
|
+
width: 46px;
|
|
2558
|
+
height: 46px;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
.web-phone-error {
|
|
2562
|
+
margin-top: 14px;
|
|
2563
|
+
font-size: 11px;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
1934
2566
|
.call-two-col {
|
|
1935
2567
|
gap: 12px;
|
|
1936
2568
|
}
|
|
@@ -2064,13 +2696,22 @@ export default {
|
|
|
2064
2696
|
max-width: none;
|
|
2065
2697
|
}
|
|
2066
2698
|
|
|
2067
|
-
.
|
|
2068
|
-
|
|
2069
|
-
|
|
2699
|
+
.web-call-button {
|
|
2700
|
+
width: 100%;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
.web-phone-body {
|
|
2704
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2705
|
+
max-height: 78vh;
|
|
2706
|
+
overflow: auto;
|
|
2070
2707
|
}
|
|
2071
2708
|
|
|
2072
|
-
.
|
|
2073
|
-
|
|
2709
|
+
.web-phone-settings {
|
|
2710
|
+
padding-right: 0;
|
|
2711
|
+
padding-bottom: 20px;
|
|
2712
|
+
border-right: 0;
|
|
2713
|
+
border-bottom: 1px solid #e5e7eb;
|
|
2074
2714
|
}
|
|
2715
|
+
|
|
2075
2716
|
}
|
|
2076
2717
|
</style>
|