@7365admin1/layer-common 3.1.4-staging.53 → 3.2.0

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,17 +12,7 @@
12
12
  Add Contacts
13
13
  </v-btn>
14
14
 
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>
15
+ <div v-else class="top-placeholder" />
26
16
 
27
17
  <div class="toolbar-spacer" />
28
18
 
@@ -115,7 +105,6 @@
115
105
  <v-list-item title="Call Settings" @click="openCallDialog(row)" />
116
106
  <v-list-item title="Dial Code" @click="openDialCodeDialog(row)" />
117
107
  <v-list-item title="Make a call" @click="openMakeCallDialog(row)" />
118
- <v-list-item title="Web call" @click="openWebPhoneDialog(row)" />
119
108
  </v-list>
120
109
  </v-menu>
121
110
  </td>
@@ -242,6 +231,36 @@
242
231
  </v-card>
243
232
  </v-dialog>
244
233
 
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
+
245
264
  <v-dialog v-model="callDialog" max-width="360" persistent>
246
265
  <v-card class="form-dialog call-dialog" rounded="lg">
247
266
  <v-card-title class="dialog-title">Call Settings</v-card-title>
@@ -331,10 +350,10 @@
331
350
  <v-card-title class="dialog-title">Make a Call</v-card-title>
332
351
  <v-card-text class="dialog-body make-call-body">
333
352
  <div class="make-call-field">
334
- <div class="call-label">IP address to dial</div>
353
+ <div class="call-label">Address to dial</div>
335
354
  <v-text-field
336
355
  v-model="makeCallForm.target"
337
- placeholder="000.000.0.0"
356
+ placeholder="Enter extension, address, or contact"
338
357
  variant="outlined"
339
358
  density="compact"
340
359
  hide-details="auto"
@@ -369,192 +388,8 @@
369
388
  </div>
370
389
  </v-card-text>
371
390
  <v-card-actions class="dialog-actions pa-0">
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>
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>
558
393
  </v-card-actions>
559
394
  </v-card>
560
395
  </v-dialog>
@@ -696,25 +531,21 @@ const limit = 20;
696
531
  const selectedRow = ref<IntercomRow | null>(null);
697
532
  const selectedContact = ref<ContactRow | null>(null);
698
533
  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();
706
534
 
707
535
  const viewDialog = ref(false);
708
536
  const sipDialog = ref(false);
537
+ const rebootConfirmDialog = ref(false);
538
+ const rebootingDialog = ref(false);
709
539
  const callDialog = ref(false);
710
540
  const dialCodeDialog = ref(false);
711
541
  const makeCallDialog = ref(false);
712
- const webPhoneDialog = ref(false);
713
542
  const contactDialog = ref(false);
714
543
  const contactViewDialog = ref(false);
715
544
  const deleteContactDialog = ref(false);
716
545
 
717
546
  const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
547
+ const rebootCountdown = ref(59);
548
+ let rebootTimer: ReturnType<typeof setInterval> | undefined;
718
549
 
719
550
  const statusOptions = ["Status", "Connected", "Not connected", "Connecting", "Disabled", "Failed"];
720
551
 
@@ -751,7 +582,7 @@ const sipForm = reactive({
751
582
  serverIp: "",
752
583
  serverPort: "5060",
753
584
  initialRtpPort: "10000",
754
- finalRtpPort: "10100",
585
+ finalRtpPort: "1000",
755
586
  numericExtension: false,
756
587
  extension: "",
757
588
  login: "",
@@ -786,15 +617,6 @@ const makeCallForm = reactive({
786
617
  statusType: "info" as MessageType,
787
618
  });
788
619
 
789
- const webPhoneForm = reactive({
790
- webSocketServer: "",
791
- aor: "",
792
- username: "",
793
- password: "",
794
- destination: "",
795
- video: false,
796
- });
797
-
798
620
  const contactForm = reactive({
799
621
  id: undefined as number | undefined,
800
622
  readerId: "",
@@ -895,35 +717,6 @@ const contactDetailRows = computed<DetailItem[]>(() => [
895
717
  { label: "Server Address", value: selectedContact.value?.serverAddress, link: true },
896
718
  { label: "Location", value: selectedContact.value?.location },
897
719
  ]);
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
- });
927
720
 
928
721
  watch(
929
722
  () => props.site,
@@ -939,12 +732,9 @@ watch(pages, () => {
939
732
  if (page.value > pages.value) page.value = pages.value;
940
733
  });
941
734
 
942
- watch(
943
- () => webPhone.callState.value,
944
- (state) => {
945
- if (state === "incoming") webPhoneDialog.value = true;
946
- },
947
- );
735
+ onUnmounted(() => {
736
+ clearRebootTimer();
737
+ });
948
738
 
949
739
  async function reload() {
950
740
  if (!props.site) return;
@@ -1011,14 +801,6 @@ function paginate<T>(items: T[]) {
1011
801
  function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>, status: string): IntercomRow {
1012
802
  const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
1013
803
  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");
1022
804
 
1023
805
  return {
1024
806
  reader,
@@ -1026,8 +808,8 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
1026
808
  location: reader.location || "N/A",
1027
809
  serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
1028
810
  serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
1029
- initialRtpPort,
1030
- finalRtpPort,
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"),
1031
813
  monitorPath: reader.monitorPath || "N/A",
1032
814
  status,
1033
815
  sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
@@ -1098,8 +880,8 @@ function getPjsipValue(source: Record<string, any>, keys: string[]) {
1098
880
 
1099
881
  function normalizeStatus(response: Record<string, any>, pjsip: Record<string, any>) {
1100
882
  const status = Number(response?.status);
1101
- if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
1102
883
  if (status === 200) return "Connected";
884
+ if (status === -1 || toFlag(pjsip.enabled) === "0") return "Disabled";
1103
885
  if (status === 0 || status === 100) return "Connecting";
1104
886
  if ([401, 403, 408, 503].includes(status)) return "Failed";
1105
887
  return "Not connected";
@@ -1129,7 +911,7 @@ function openSipDialog(row: IntercomRow) {
1129
911
  sipForm.serverIp = row.serverAddress === "N/A" ? "" : row.serverAddress;
1130
912
  sipForm.serverPort = row.serverPort === "N/A" ? "5060" : row.serverPort;
1131
913
  sipForm.initialRtpPort = row.initialRtpPort === "N/A" ? "10000" : row.initialRtpPort;
1132
- sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "10100" : row.finalRtpPort;
914
+ sipForm.finalRtpPort = row.finalRtpPort === "N/A" ? "1000" : row.finalRtpPort;
1133
915
  sipForm.numericExtension = row.numericExtension;
1134
916
  sipForm.extension = row.extension === "N/A" ? "" : row.extension;
1135
917
  sipForm.login = row.login === "N/A" ? "" : row.login;
@@ -1163,100 +945,12 @@ function openDialCodeDialog(row: IntercomRow) {
1163
945
 
1164
946
  function openMakeCallDialog(row: IntercomRow) {
1165
947
  selectedRow.value = row;
1166
- makeCallForm.target = isIpAddress(row.serverAddress) ? row.serverAddress : "";
948
+ makeCallForm.target = row.extension !== "N/A" ? row.extension : "";
1167
949
  makeCallForm.status = "";
1168
950
  makeCallForm.statusType = "info";
1169
951
  makeCallDialog.value = true;
1170
952
  }
1171
953
 
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
-
1260
954
  function openContactDialog(contact?: ContactRow) {
1261
955
  selectedContact.value = contact || null;
1262
956
  contactForm.id = contact?.id;
@@ -1280,34 +974,21 @@ function openDeleteContact(contact: ContactRow) {
1280
974
  async function saveSipSettings() {
1281
975
  if (!selectedRow.value) return;
1282
976
  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
-
1297
977
  const saved = await savePjsip(selectedRow.value.reader._id, {
1298
978
  enabled: sipForm.enabled ? "1" : "0",
1299
979
  server_ip: sipForm.serverIp,
1300
980
  server_port: sipForm.serverPort,
1301
- server_outbound_port: String(initialRtpPort),
1302
- server_outbound_port_range: String(finalRtpPort - initialRtpPort),
981
+ server_outbound_port: sipForm.initialRtpPort,
982
+ server_outbound_port_range: sipForm.finalRtpPort,
1303
983
  numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
1304
984
  branch: isPeerToPeer ? "" : sipForm.extension,
1305
985
  login: isPeerToPeer ? "" : sipForm.login,
1306
986
  password: isPeerToPeer ? "" : sipForm.password,
1307
987
  peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
1308
- });
988
+ }, { showSuccessMessage: false });
1309
989
  if (saved) {
1310
990
  sipDialog.value = false;
991
+ rebootConfirmDialog.value = true;
1311
992
  }
1312
993
  }
1313
994
 
@@ -1339,13 +1020,7 @@ async function runMakeCall() {
1339
1020
  if (!selectedRow.value) return;
1340
1021
  const target = makeCallForm.target.trim();
1341
1022
  if (!target) {
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.";
1023
+ makeCallForm.status = "Please enter an address to dial.";
1349
1024
  makeCallForm.statusType = "warning";
1350
1025
  return;
1351
1026
  }
@@ -1367,15 +1042,6 @@ async function runMakeCall() {
1367
1042
  }
1368
1043
  }
1369
1044
 
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
-
1379
1045
  async function runHangupCall() {
1380
1046
  if (!selectedRow.value) return;
1381
1047
  hangingUp.value = true;
@@ -1412,6 +1078,30 @@ async function savePjsip(readerId: string, payload: Record<string, string>, opti
1412
1078
  }
1413
1079
  }
1414
1080
 
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
+
1415
1105
  async function saveContact() {
1416
1106
  if (!contactForm.readerId || !contactForm.name.trim() || !contactForm.serverAddress.trim()) {
1417
1107
  showMessage("Please complete the required contact fields.", "warning");
@@ -1569,23 +1259,6 @@ export default {
1569
1259
  width: 160px;
1570
1260
  }
1571
1261
 
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
-
1589
1262
  .add-button {
1590
1263
  min-width: 160px;
1591
1264
  background: #dedede;
@@ -1957,6 +1630,98 @@ export default {
1957
1630
  overflow: hidden;
1958
1631
  }
1959
1632
 
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
+
1960
1725
  .call-dialog .dialog-body {
1961
1726
  padding: 18px 24px 24px;
1962
1727
  }
@@ -2166,209 +1931,6 @@ export default {
2166
1931
  line-height: 1.45;
2167
1932
  }
2168
1933
 
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
-
2372
1934
  .call-two-col {
2373
1935
  gap: 12px;
2374
1936
  }
@@ -2502,22 +2064,13 @@ export default {
2502
2064
  max-width: none;
2503
2065
  }
2504
2066
 
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;
2067
+ .rebooting-card {
2068
+ grid-template-columns: 1fr;
2069
+ padding: 28px;
2513
2070
  }
2514
2071
 
2515
- .web-phone-settings {
2516
- padding-right: 0;
2517
- padding-bottom: 20px;
2518
- border-right: 0;
2519
- border-bottom: 1px solid #e5e7eb;
2072
+ .wait-word {
2073
+ font-size: 80px;
2520
2074
  }
2521
-
2522
2075
  }
2523
2076
  </style>