@7365admin1/layer-common 3.1.4-staging.49 → 3.1.4-staging.51

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.
@@ -12,7 +12,17 @@
12
12
  Add Contacts
13
13
  </v-btn>
14
14
 
15
- <div v-else class="top-placeholder" />
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>
@@ -231,36 +242,6 @@
231
242
  </v-card>
232
243
  </v-dialog>
233
244
 
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
245
  <v-dialog v-model="callDialog" max-width="360" persistent>
265
246
  <v-card class="form-dialog call-dialog" rounded="lg">
266
247
  <v-card-title class="dialog-title">Call Settings</v-card-title>
@@ -350,10 +331,10 @@
350
331
  <v-card-title class="dialog-title">Make a Call</v-card-title>
351
332
  <v-card-text class="dialog-body make-call-body">
352
333
  <div class="make-call-field">
353
- <div class="call-label">Address to dial</div>
334
+ <div class="call-label">IP address to dial</div>
354
335
  <v-text-field
355
336
  v-model="makeCallForm.target"
356
- placeholder="Enter extension, address, or contact"
337
+ placeholder="000.000.0.0"
357
338
  variant="outlined"
358
339
  density="compact"
359
340
  hide-details="auto"
@@ -388,8 +369,192 @@
388
369
  </div>
389
370
  </v-card-text>
390
371
  <v-card-actions class="dialog-actions pa-0">
391
- <v-btn class="text-none action-cancel" variant="flat" height="44" @click="makeCallDialog = false">Cancel</v-btn>
392
- <v-btn class="text-none action-submit" variant="flat" height="44" @click="makeCallDialog = false">Close</v-btn>
372
+ <v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="makeCallDialog = false">Close</v-btn>
373
+ </v-card-actions>
374
+ </v-card>
375
+ </v-dialog>
376
+
377
+ <v-dialog v-model="webPhoneDialog" max-width="820" persistent eager>
378
+ <v-card class="web-phone-dialog" rounded="lg">
379
+ <v-card-title class="dialog-title web-phone-title">
380
+ <span>Web Call</span>
381
+ <v-chip size="small" variant="flat" :class="webPhoneStatusClass">
382
+ {{ webPhoneStatusLabel }}
383
+ </v-chip>
384
+ </v-card-title>
385
+
386
+ <v-card-text class="web-phone-body">
387
+ <div class="web-phone-settings">
388
+ <div class="web-phone-section-title">SIP Account</div>
389
+ <div class="call-label">WebSocket Server <span>*</span></div>
390
+ <v-text-field
391
+ v-model="webPhoneForm.webSocketServer"
392
+ placeholder="wss://sip.example.com/ws"
393
+ variant="outlined"
394
+ density="compact"
395
+ hide-details="auto"
396
+ class="call-input"
397
+ :disabled="webPhone.isRegistered.value"
398
+ />
399
+ <div class="call-label">SIP Address <span>*</span></div>
400
+ <v-text-field
401
+ v-model="webPhoneForm.aor"
402
+ placeholder="operator@example.com"
403
+ variant="outlined"
404
+ density="compact"
405
+ hide-details="auto"
406
+ class="call-input"
407
+ :disabled="webPhone.isRegistered.value"
408
+ />
409
+ <div class="form-two-col">
410
+ <div>
411
+ <div class="call-label">Username <span>*</span></div>
412
+ <v-text-field
413
+ v-model="webPhoneForm.username"
414
+ placeholder="Username"
415
+ variant="outlined"
416
+ density="compact"
417
+ hide-details="auto"
418
+ class="call-input"
419
+ :disabled="webPhone.isRegistered.value"
420
+ />
421
+ </div>
422
+ <div>
423
+ <div class="call-label">Password <span>*</span></div>
424
+ <v-text-field
425
+ v-model="webPhoneForm.password"
426
+ placeholder="Password"
427
+ :type="showWebPhonePassword ? 'text' : 'password'"
428
+ :append-inner-icon="showWebPhonePassword ? 'mdi-eye-off' : 'mdi-eye'"
429
+ variant="outlined"
430
+ density="compact"
431
+ hide-details="auto"
432
+ class="call-input"
433
+ :disabled="webPhone.isRegistered.value"
434
+ @click:append-inner="showWebPhonePassword = !showWebPhonePassword"
435
+ />
436
+ </div>
437
+ </div>
438
+ <div class="call-toggle-row web-video-toggle">
439
+ <span>Enable video</span>
440
+ <v-switch
441
+ v-model="webPhoneForm.video"
442
+ color="success"
443
+ density="compact"
444
+ hide-details
445
+ :disabled="webPhone.isRegistered.value"
446
+ />
447
+ </div>
448
+ <v-btn
449
+ v-if="!webPhone.isRegistered.value"
450
+ block
451
+ class="text-none web-phone-connect"
452
+ variant="flat"
453
+ prepend-icon="mdi-lan-connect"
454
+ :loading="webPhoneConnecting"
455
+ @click="connectWebPhone"
456
+ >
457
+ Connect Web Phone
458
+ </v-btn>
459
+ <v-btn
460
+ v-else
461
+ block
462
+ class="text-none web-phone-disconnect"
463
+ variant="outlined"
464
+ prepend-icon="mdi-lan-disconnect"
465
+ :loading="webPhoneConnecting"
466
+ :disabled="webPhone.hasCall.value"
467
+ @click="disconnectWebPhone"
468
+ >
469
+ Disconnect
470
+ </v-btn>
471
+ </div>
472
+
473
+ <div class="web-phone-console">
474
+ <div class="web-phone-media" :class="{ 'has-video': webPhoneForm.video && webPhone.callState.value === 'active' }">
475
+ <video ref="remoteVideoEl" autoplay playsinline class="remote-video" />
476
+ <video ref="localVideoEl" autoplay muted playsinline class="local-video" />
477
+ <audio ref="remoteAudioEl" autoplay />
478
+ <div v-if="!webPhoneForm.video || webPhone.callState.value !== 'active'" class="audio-call-state">
479
+ <v-icon size="54">{{ webPhoneCallIcon }}</v-icon>
480
+ <strong>{{ webPhoneCallLabel }}</strong>
481
+ <span v-if="webPhone.currentTarget.value">{{ webPhone.currentTarget.value }}</span>
482
+ </div>
483
+ </div>
484
+
485
+ <div v-if="webPhone.callState.value === 'incoming'" class="incoming-call-actions">
486
+ <v-btn
487
+ class="text-none answer-call"
488
+ variant="flat"
489
+ prepend-icon="mdi-phone"
490
+ :loading="webPhoneActionLoading"
491
+ @click="answerWebCall"
492
+ >
493
+ Answer
494
+ </v-btn>
495
+ <v-btn
496
+ class="text-none reject-call"
497
+ variant="flat"
498
+ prepend-icon="mdi-phone-hangup"
499
+ :disabled="webPhoneActionLoading"
500
+ @click="declineWebCall"
501
+ >
502
+ Decline
503
+ </v-btn>
504
+ </div>
505
+
506
+ <template v-else>
507
+ <div class="call-label">Extension or SIP Address</div>
508
+ <div class="web-phone-dial-row">
509
+ <v-text-field
510
+ v-model="webPhoneForm.destination"
511
+ placeholder="1001 or device@example.com"
512
+ variant="outlined"
513
+ density="compact"
514
+ hide-details="auto"
515
+ class="call-input mb-0"
516
+ :disabled="!webPhone.isRegistered.value || webPhone.hasCall.value"
517
+ @keyup.enter="startWebCall"
518
+ />
519
+ <v-btn
520
+ icon="mdi-phone"
521
+ class="start-web-call"
522
+ variant="flat"
523
+ :loading="webPhoneActionLoading && webPhone.callState.value === 'calling'"
524
+ :disabled="!webPhone.isRegistered.value || webPhone.hasCall.value || !webPhoneForm.destination.trim()"
525
+ @click="startWebCall"
526
+ />
527
+ </div>
528
+
529
+ <div v-if="webPhone.hasCall.value" class="active-call-controls">
530
+ <v-btn
531
+ :icon="webPhone.muted.value ? 'mdi-microphone-off' : 'mdi-microphone'"
532
+ variant="outlined"
533
+ :disabled="webPhone.callState.value !== 'active'"
534
+ :title="webPhone.muted.value ? 'Unmute' : 'Mute'"
535
+ @click="webPhone.toggleMute"
536
+ />
537
+ <v-btn
538
+ icon="mdi-phone-hangup"
539
+ class="hangup-web-call"
540
+ variant="flat"
541
+ :loading="webPhoneActionLoading && webPhone.callState.value === 'ending'"
542
+ title="End call"
543
+ @click="hangupWebCall"
544
+ />
545
+ </div>
546
+ </template>
547
+
548
+ <v-alert v-if="webPhone.errorMessage.value" type="error" variant="tonal" density="compact" class="web-phone-error">
549
+ {{ webPhone.errorMessage.value }}
550
+ </v-alert>
551
+ </div>
552
+ </v-card-text>
553
+
554
+ <v-card-actions class="dialog-actions pa-0">
555
+ <v-btn class="text-none action-submit full-action" variant="flat" height="44" @click="webPhoneDialog = false">
556
+ Close
557
+ </v-btn>
393
558
  </v-card-actions>
394
559
  </v-card>
395
560
  </v-dialog>
@@ -531,21 +696,25 @@ const limit = 20;
531
696
  const selectedRow = ref<IntercomRow | null>(null);
532
697
  const selectedContact = ref<ContactRow | null>(null);
533
698
  const showSipPassword = ref(false);
699
+ const showWebPhonePassword = ref(false);
700
+ const webPhoneConnecting = ref(false);
701
+ const webPhoneActionLoading = ref(false);
702
+ const localVideoEl = ref<HTMLVideoElement | null>(null);
703
+ const remoteVideoEl = ref<HTMLVideoElement | null>(null);
704
+ const remoteAudioEl = ref<HTMLAudioElement | null>(null);
705
+ const webPhone = useSipWebPhone();
534
706
 
535
707
  const viewDialog = ref(false);
536
708
  const sipDialog = ref(false);
537
- const rebootConfirmDialog = ref(false);
538
- const rebootingDialog = ref(false);
539
709
  const callDialog = ref(false);
540
710
  const dialCodeDialog = ref(false);
541
711
  const makeCallDialog = ref(false);
712
+ const webPhoneDialog = ref(false);
542
713
  const contactDialog = ref(false);
543
714
  const contactViewDialog = ref(false);
544
715
  const deleteContactDialog = ref(false);
545
716
 
546
717
  const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
547
- const rebootCountdown = ref(59);
548
- let rebootTimer: ReturnType<typeof setInterval> | undefined;
549
718
 
550
719
  const statusOptions = ["Status", "Connected", "Not connected", "Connecting", "Disabled", "Failed"];
551
720
 
@@ -582,7 +751,7 @@ const sipForm = reactive({
582
751
  serverIp: "",
583
752
  serverPort: "5060",
584
753
  initialRtpPort: "10000",
585
- finalRtpPort: "1000",
754
+ finalRtpPort: "10100",
586
755
  numericExtension: false,
587
756
  extension: "",
588
757
  login: "",
@@ -617,6 +786,15 @@ const makeCallForm = reactive({
617
786
  statusType: "info" as MessageType,
618
787
  });
619
788
 
789
+ const webPhoneForm = reactive({
790
+ webSocketServer: "",
791
+ aor: "",
792
+ username: "",
793
+ password: "",
794
+ destination: "",
795
+ video: false,
796
+ });
797
+
620
798
  const contactForm = reactive({
621
799
  id: undefined as number | undefined,
622
800
  readerId: "",
@@ -717,6 +895,35 @@ const contactDetailRows = computed<DetailItem[]>(() => [
717
895
  { label: "Server Address", value: selectedContact.value?.serverAddress, link: true },
718
896
  { label: "Location", value: selectedContact.value?.location },
719
897
  ]);
898
+ const webPhoneStatusLabel = computed(() => {
899
+ if (webPhone.callState.value === "incoming") return "Incoming call";
900
+ if (webPhone.callState.value === "calling") return "Calling";
901
+ if (webPhone.callState.value === "active") return "In call";
902
+ if (webPhone.callState.value === "ending") return "Ending call";
903
+ if (webPhone.connectionState.value === "connecting") return "Connecting";
904
+ if (webPhone.connectionState.value === "registered") return "Ready";
905
+ if (webPhone.connectionState.value === "error") return "Connection failed";
906
+ return "Disconnected";
907
+ });
908
+ const webPhoneStatusClass = computed(() => {
909
+ if (webPhone.callState.value === "incoming") return "web-phone-status incoming";
910
+ if (webPhone.callState.value === "active" || webPhone.connectionState.value === "registered") return "web-phone-status ready";
911
+ if (webPhone.connectionState.value === "error") return "web-phone-status failed";
912
+ return "web-phone-status idle";
913
+ });
914
+ const webPhoneCallLabel = computed(() => {
915
+ if (webPhone.callState.value === "incoming") return "Incoming call";
916
+ if (webPhone.callState.value === "calling") return "Calling...";
917
+ if (webPhone.callState.value === "active") return "Call connected";
918
+ if (webPhone.callState.value === "ending") return "Ending call...";
919
+ return webPhone.isRegistered.value ? "Ready to call" : "Connect a SIP account";
920
+ });
921
+ const webPhoneCallIcon = computed(() => {
922
+ if (webPhone.callState.value === "incoming") return "mdi-phone-incoming";
923
+ if (webPhone.callState.value === "calling") return "mdi-phone-outgoing";
924
+ if (webPhone.callState.value === "active") return "mdi-phone-in-talk";
925
+ return "mdi-phone-outline";
926
+ });
720
927
 
721
928
  watch(
722
929
  () => props.site,
@@ -732,9 +939,12 @@ watch(pages, () => {
732
939
  if (page.value > pages.value) page.value = pages.value;
733
940
  });
734
941
 
735
- onUnmounted(() => {
736
- clearRebootTimer();
737
- });
942
+ watch(
943
+ () => webPhone.callState.value,
944
+ (state) => {
945
+ if (state === "incoming") webPhoneDialog.value = true;
946
+ },
947
+ );
738
948
 
739
949
  async function reload() {
740
950
  if (!props.site) return;
@@ -801,6 +1011,14 @@ function paginate<T>(items: T[]) {
801
1011
  function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>, status: string): IntercomRow {
802
1012
  const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
803
1013
  const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
1014
+ const initialRtpPort = toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A");
1015
+ const rtpPortRange = getPjsipValue(pjsip, ["server_outbound_port_range"]);
1016
+ const explicitFinalRtpPort = getPjsipValue(pjsip, ["finalRtpPort", "final_rtp_port"]);
1017
+ const initialRtpPortNumber = Number(initialRtpPort);
1018
+ const rtpPortRangeNumber = Number(rtpPortRange);
1019
+ const finalRtpPort = rtpPortRange !== undefined && Number.isFinite(initialRtpPortNumber) && Number.isFinite(rtpPortRangeNumber)
1020
+ ? String(initialRtpPortNumber + rtpPortRangeNumber)
1021
+ : toText(explicitFinalRtpPort, "N/A");
804
1022
 
805
1023
  return {
806
1024
  reader,
@@ -808,8 +1026,8 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
808
1026
  location: reader.location || "N/A",
809
1027
  serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
810
1028
  serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
811
- initialRtpPort: toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A"),
812
- finalRtpPort: toText(getPjsipValue(pjsip, ["server_outbound_port_range", "finalRtpPort", "final_rtp_port"]), "N/A"),
1029
+ initialRtpPort,
1030
+ finalRtpPort,
813
1031
  monitorPath: reader.monitorPath || "N/A",
814
1032
  status,
815
1033
  sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
@@ -880,8 +1098,8 @@ function getPjsipValue(source: Record<string, any>, keys: string[]) {
880
1098
 
881
1099
  function normalizeStatus(response: Record<string, any>, pjsip: Record<string, any>) {
882
1100
  const status = Number(response?.status);
1101
+ if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
883
1102
  if (status === 200) return "Connected";
884
- if (status === -1 || toFlag(pjsip.enabled) === "0") return "Disabled";
885
1103
  if (status === 0 || status === 100) return "Connecting";
886
1104
  if ([401, 403, 408, 503].includes(status)) return "Failed";
887
1105
  return "Not connected";
@@ -911,7 +1129,7 @@ function openSipDialog(row: IntercomRow) {
911
1129
  sipForm.serverIp = row.serverAddress === "N/A" ? "" : row.serverAddress;
912
1130
  sipForm.serverPort = row.serverPort === "N/A" ? "5060" : row.serverPort;
913
1131
  sipForm.initialRtpPort = row.initialRtpPort === "N/A" ? "10000" : row.initialRtpPort;
914
- sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "1000" : row.finalRtpPort;
1132
+ sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "10100" : row.finalRtpPort;
915
1133
  sipForm.numericExtension = row.numericExtension;
916
1134
  sipForm.extension = row.extension === "N/A" ? "" : row.extension;
917
1135
  sipForm.login = row.login === "N/A" ? "" : row.login;
@@ -945,12 +1163,100 @@ function openDialCodeDialog(row: IntercomRow) {
945
1163
 
946
1164
  function openMakeCallDialog(row: IntercomRow) {
947
1165
  selectedRow.value = row;
948
- makeCallForm.target = row.extension !== "N/A" ? row.extension : "";
1166
+ makeCallForm.target = isIpAddress(row.serverAddress) ? row.serverAddress : "";
949
1167
  makeCallForm.status = "";
950
1168
  makeCallForm.statusType = "info";
951
1169
  makeCallDialog.value = true;
952
1170
  }
953
1171
 
1172
+ function openWebPhoneDialog(row?: IntercomRow) {
1173
+ const activeRow = row || selectedRow.value || intercomRows.value[0];
1174
+ if (activeRow) {
1175
+ selectedRow.value = activeRow;
1176
+ if (!webPhone.isRegistered.value && activeRow.extension !== "N/A") {
1177
+ webPhoneForm.destination = activeRow.extension;
1178
+ }
1179
+ }
1180
+ webPhoneDialog.value = true;
1181
+ }
1182
+
1183
+ async function connectWebPhone() {
1184
+ webPhoneConnecting.value = true;
1185
+ try {
1186
+ await webPhone.connect({
1187
+ webSocketServer: webPhoneForm.webSocketServer,
1188
+ aor: webPhoneForm.aor,
1189
+ authorizationUsername: webPhoneForm.username,
1190
+ authorizationPassword: webPhoneForm.password,
1191
+ displayName: "iService365 Web",
1192
+ video: webPhoneForm.video,
1193
+ }, {
1194
+ localVideo: localVideoEl.value,
1195
+ remoteVideo: remoteVideoEl.value,
1196
+ remoteAudio: remoteAudioEl.value,
1197
+ });
1198
+ showMessage("Web phone connected and ready to receive calls.", "success");
1199
+ } catch (error: any) {
1200
+ showMessage(getErrorMessage(error), "error");
1201
+ } finally {
1202
+ webPhoneConnecting.value = false;
1203
+ }
1204
+ }
1205
+
1206
+ async function disconnectWebPhone() {
1207
+ webPhoneConnecting.value = true;
1208
+ try {
1209
+ await webPhone.disconnect();
1210
+ showMessage("Web phone disconnected.", "info");
1211
+ } finally {
1212
+ webPhoneConnecting.value = false;
1213
+ }
1214
+ }
1215
+
1216
+ async function startWebCall() {
1217
+ webPhoneActionLoading.value = true;
1218
+ try {
1219
+ await webPhone.call(webPhoneForm.destination);
1220
+ } catch (error: any) {
1221
+ showMessage(getErrorMessage(error), "error");
1222
+ } finally {
1223
+ webPhoneActionLoading.value = false;
1224
+ }
1225
+ }
1226
+
1227
+ async function answerWebCall() {
1228
+ webPhoneActionLoading.value = true;
1229
+ try {
1230
+ await webPhone.answer();
1231
+ } catch (error: any) {
1232
+ showMessage(getErrorMessage(error), "error");
1233
+ } finally {
1234
+ webPhoneActionLoading.value = false;
1235
+ }
1236
+ }
1237
+
1238
+ async function declineWebCall() {
1239
+ webPhoneActionLoading.value = true;
1240
+ try {
1241
+ await webPhone.decline();
1242
+ } catch (error: any) {
1243
+ showMessage(getErrorMessage(error), "error");
1244
+ } finally {
1245
+ webPhoneActionLoading.value = false;
1246
+ }
1247
+ }
1248
+
1249
+ async function hangupWebCall() {
1250
+ webPhoneActionLoading.value = true;
1251
+ try {
1252
+ await webPhone.hangup();
1253
+ } catch (error: any) {
1254
+ showMessage(getErrorMessage(error), "error");
1255
+ } finally {
1256
+ webPhoneActionLoading.value = false;
1257
+ }
1258
+ }
1259
+
954
1260
  function openContactDialog(contact?: ContactRow) {
955
1261
  selectedContact.value = contact || null;
956
1262
  contactForm.id = contact?.id;
@@ -974,21 +1280,34 @@ function openDeleteContact(contact: ContactRow) {
974
1280
  async function saveSipSettings() {
975
1281
  if (!selectedRow.value) return;
976
1282
  const isPeerToPeer = sipForm.operatingMode === "peer-to-peer";
1283
+ const initialRtpPort = Number(sipForm.initialRtpPort);
1284
+ const finalRtpPort = Number(sipForm.finalRtpPort);
1285
+
1286
+ if (
1287
+ !Number.isInteger(initialRtpPort) ||
1288
+ !Number.isInteger(finalRtpPort) ||
1289
+ initialRtpPort < 1 ||
1290
+ finalRtpPort > 65535 ||
1291
+ finalRtpPort <= initialRtpPort
1292
+ ) {
1293
+ showMessage("Final RTP Port must be greater than Initial RTP Port and both ports must be between 1 and 65535.", "warning");
1294
+ return;
1295
+ }
1296
+
977
1297
  const saved = await savePjsip(selectedRow.value.reader._id, {
978
1298
  enabled: sipForm.enabled ? "1" : "0",
979
1299
  server_ip: sipForm.serverIp,
980
1300
  server_port: sipForm.serverPort,
981
- server_outbound_port: sipForm.initialRtpPort,
982
- server_outbound_port_range: sipForm.finalRtpPort,
1301
+ server_outbound_port: String(initialRtpPort),
1302
+ server_outbound_port_range: String(finalRtpPort - initialRtpPort),
983
1303
  numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
984
1304
  branch: isPeerToPeer ? "" : sipForm.extension,
985
1305
  login: isPeerToPeer ? "" : sipForm.login,
986
1306
  password: isPeerToPeer ? "" : sipForm.password,
987
1307
  peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
988
- }, { showSuccessMessage: false });
1308
+ });
989
1309
  if (saved) {
990
1310
  sipDialog.value = false;
991
- rebootConfirmDialog.value = true;
992
1311
  }
993
1312
  }
994
1313
 
@@ -1020,7 +1339,13 @@ async function runMakeCall() {
1020
1339
  if (!selectedRow.value) return;
1021
1340
  const target = makeCallForm.target.trim();
1022
1341
  if (!target) {
1023
- makeCallForm.status = "Please enter an address to dial.";
1342
+ makeCallForm.status = "Please enter an IP address to dial.";
1343
+ makeCallForm.statusType = "warning";
1344
+ return;
1345
+ }
1346
+
1347
+ if (!isIpAddress(target)) {
1348
+ makeCallForm.status = "Address to dial must be an IP address.";
1024
1349
  makeCallForm.statusType = "warning";
1025
1350
  return;
1026
1351
  }
@@ -1042,6 +1367,15 @@ async function runMakeCall() {
1042
1367
  }
1043
1368
  }
1044
1369
 
1370
+ function isIpAddress(value = "") {
1371
+ const parts = value.trim().split(".");
1372
+ return parts.length === 4 && parts.every((part) => {
1373
+ if (!/^\d{1,3}$/.test(part)) return false;
1374
+ const value = Number(part);
1375
+ return value >= 0 && value <= 255;
1376
+ });
1377
+ }
1378
+
1045
1379
  async function runHangupCall() {
1046
1380
  if (!selectedRow.value) return;
1047
1381
  hangingUp.value = true;
@@ -1078,30 +1412,6 @@ async function savePjsip(readerId: string, payload: Record<string, string>, opti
1078
1412
  }
1079
1413
  }
1080
1414
 
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
1415
  async function saveContact() {
1106
1416
  if (!contactForm.readerId || !contactForm.name.trim() || !contactForm.serverAddress.trim()) {
1107
1417
  showMessage("Please complete the required contact fields.", "warning");
@@ -1259,6 +1569,23 @@ export default {
1259
1569
  width: 160px;
1260
1570
  }
1261
1571
 
1572
+ .web-call-button {
1573
+ min-width: 160px;
1574
+ background: #e5e7eb;
1575
+ color: #0b2c40;
1576
+ font-size: 12px;
1577
+ }
1578
+
1579
+ .web-call-button.is-connected {
1580
+ background: #d9f2e1;
1581
+ color: #16743a;
1582
+ }
1583
+
1584
+ .web-call-button.has-incoming-call {
1585
+ background: #1769aa;
1586
+ color: #fff;
1587
+ }
1588
+
1262
1589
  .add-button {
1263
1590
  min-width: 160px;
1264
1591
  background: #dedede;
@@ -1630,98 +1957,6 @@ export default {
1630
1957
  overflow: hidden;
1631
1958
  }
1632
1959
 
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
1960
  .call-dialog .dialog-body {
1726
1961
  padding: 18px 24px 24px;
1727
1962
  }
@@ -1931,6 +2166,209 @@ export default {
1931
2166
  line-height: 1.45;
1932
2167
  }
1933
2168
 
2169
+ .web-phone-dialog {
2170
+ width: 820px;
2171
+ max-width: 94vw;
2172
+ overflow: hidden;
2173
+ background: #fff;
2174
+ }
2175
+
2176
+ .web-phone-title {
2177
+ display: flex;
2178
+ align-items: center;
2179
+ justify-content: space-between;
2180
+ gap: 16px;
2181
+ }
2182
+
2183
+ .web-phone-status {
2184
+ min-width: 92px;
2185
+ justify-content: center;
2186
+ font-size: 10px;
2187
+ }
2188
+
2189
+ .web-phone-status.ready {
2190
+ background: #d9f2e1;
2191
+ color: #16743a;
2192
+ }
2193
+
2194
+ .web-phone-status.incoming {
2195
+ background: #dcecff;
2196
+ color: #125c9a;
2197
+ }
2198
+
2199
+ .web-phone-status.failed {
2200
+ background: #fce2e2;
2201
+ color: #a92222;
2202
+ }
2203
+
2204
+ .web-phone-status.idle {
2205
+ background: #e5e7eb;
2206
+ color: #5d6673;
2207
+ }
2208
+
2209
+ .web-phone-body {
2210
+ display: grid;
2211
+ grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr);
2212
+ gap: 24px;
2213
+ min-height: 440px;
2214
+ padding: 22px 24px 24px;
2215
+ }
2216
+
2217
+ .web-phone-settings {
2218
+ padding-right: 24px;
2219
+ border-right: 1px solid #e5e7eb;
2220
+ }
2221
+
2222
+ .web-phone-section-title {
2223
+ margin-bottom: 14px;
2224
+ color: #0d1720;
2225
+ font-size: 13px;
2226
+ font-weight: 700;
2227
+ }
2228
+
2229
+ .web-video-toggle {
2230
+ margin-top: 12px;
2231
+ }
2232
+
2233
+ .web-phone-connect {
2234
+ min-height: 42px;
2235
+ margin-top: 14px;
2236
+ background: #082b40;
2237
+ color: #fff;
2238
+ }
2239
+
2240
+ .web-phone-disconnect {
2241
+ min-height: 42px;
2242
+ margin-top: 14px;
2243
+ border-color: #aeb8c2;
2244
+ color: #344054;
2245
+ }
2246
+
2247
+ .web-phone-console {
2248
+ display: flex;
2249
+ min-width: 0;
2250
+ flex-direction: column;
2251
+ }
2252
+
2253
+ .web-phone-media {
2254
+ position: relative;
2255
+ display: grid;
2256
+ min-height: 260px;
2257
+ overflow: hidden;
2258
+ place-items: center;
2259
+ border: 1px solid #d7dde4;
2260
+ border-radius: 6px;
2261
+ background: #102a3b;
2262
+ }
2263
+
2264
+ .remote-video {
2265
+ display: none;
2266
+ width: 100%;
2267
+ height: 100%;
2268
+ object-fit: cover;
2269
+ }
2270
+
2271
+ .web-phone-media.has-video .remote-video {
2272
+ display: block;
2273
+ }
2274
+
2275
+ .local-video {
2276
+ position: absolute;
2277
+ right: 12px;
2278
+ bottom: 12px;
2279
+ display: none;
2280
+ width: 108px;
2281
+ aspect-ratio: 3 / 4;
2282
+ border: 2px solid #fff;
2283
+ border-radius: 4px;
2284
+ background: #263b49;
2285
+ object-fit: cover;
2286
+ }
2287
+
2288
+ .web-phone-media.has-video .local-video {
2289
+ display: block;
2290
+ }
2291
+
2292
+ .audio-call-state {
2293
+ display: flex;
2294
+ align-items: center;
2295
+ justify-content: center;
2296
+ padding: 28px;
2297
+ color: #fff;
2298
+ flex-direction: column;
2299
+ gap: 10px;
2300
+ text-align: center;
2301
+ }
2302
+
2303
+ .audio-call-state strong {
2304
+ font-size: 16px;
2305
+ font-weight: 600;
2306
+ }
2307
+
2308
+ .audio-call-state span {
2309
+ max-width: 100%;
2310
+ overflow: hidden;
2311
+ color: #c9d7e1;
2312
+ font-size: 11px;
2313
+ text-overflow: ellipsis;
2314
+ white-space: nowrap;
2315
+ }
2316
+
2317
+ .web-phone-dial-row {
2318
+ display: grid;
2319
+ grid-template-columns: minmax(0, 1fr) 42px;
2320
+ gap: 8px;
2321
+ align-items: start;
2322
+ }
2323
+
2324
+ .start-web-call,
2325
+ .hangup-web-call {
2326
+ width: 42px;
2327
+ height: 38px;
2328
+ border-radius: 4px;
2329
+ background: #2a9b50;
2330
+ color: #fff;
2331
+ }
2332
+
2333
+ .hangup-web-call,
2334
+ .reject-call {
2335
+ background: #c9293d;
2336
+ }
2337
+
2338
+ .incoming-call-actions {
2339
+ display: grid;
2340
+ grid-template-columns: 1fr 1fr;
2341
+ gap: 10px;
2342
+ margin-top: 14px;
2343
+ }
2344
+
2345
+ .answer-call,
2346
+ .reject-call {
2347
+ min-height: 42px;
2348
+ color: #fff;
2349
+ }
2350
+
2351
+ .answer-call {
2352
+ background: #2a9b50;
2353
+ }
2354
+
2355
+ .active-call-controls {
2356
+ display: flex;
2357
+ justify-content: center;
2358
+ gap: 12px;
2359
+ margin-top: 16px;
2360
+ }
2361
+
2362
+ .active-call-controls .v-btn {
2363
+ width: 46px;
2364
+ height: 46px;
2365
+ }
2366
+
2367
+ .web-phone-error {
2368
+ margin-top: 14px;
2369
+ font-size: 11px;
2370
+ }
2371
+
1934
2372
  .call-two-col {
1935
2373
  gap: 12px;
1936
2374
  }
@@ -2064,13 +2502,22 @@ export default {
2064
2502
  max-width: none;
2065
2503
  }
2066
2504
 
2067
- .rebooting-card {
2068
- grid-template-columns: 1fr;
2069
- padding: 28px;
2505
+ .web-call-button {
2506
+ width: 100%;
2507
+ }
2508
+
2509
+ .web-phone-body {
2510
+ grid-template-columns: minmax(0, 1fr);
2511
+ max-height: 78vh;
2512
+ overflow: auto;
2070
2513
  }
2071
2514
 
2072
- .wait-word {
2073
- font-size: 80px;
2515
+ .web-phone-settings {
2516
+ padding-right: 0;
2517
+ padding-bottom: 20px;
2518
+ border-right: 0;
2519
+ border-bottom: 1px solid #e5e7eb;
2074
2520
  }
2521
+
2075
2522
  }
2076
2523
  </style>