@7365admin1/layer-common 3.1.4-staging.56 → 3.1.4-staging.58

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.
@@ -234,6 +234,13 @@
234
234
  <v-radio label="UDP" value="UDP" />
235
235
  <v-radio label="TCP" value="TCP" />
236
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>
237
244
  </v-card-text>
238
245
  <v-card-actions class="dialog-actions pa-0">
239
246
  <v-btn class="text-none action-cancel" variant="flat" height="44" @click="sipDialog = false">Cancel</v-btn>
@@ -386,54 +393,28 @@
386
393
  <v-card-text class="web-phone-body">
387
394
  <div class="web-phone-settings">
388
395
  <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>
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">
422
413
  <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
- />
414
+ <span class="sip-account-label">Assigned extension</span>
415
+ <strong class="sip-account-extension">{{ sipAccount.extension }}</strong>
436
416
  </div>
417
+ <span class="sip-account-address">{{ sipAccount.sipAddress }}</span>
437
418
  </div>
438
419
  <div class="call-toggle-row web-video-toggle">
439
420
  <span>Enable video</span>
@@ -452,6 +433,7 @@
452
433
  variant="flat"
453
434
  prepend-icon="mdi-lan-connect"
454
435
  :loading="webPhoneConnecting"
436
+ :disabled="!sipAccount || sipAccountLoading"
455
437
  @click="connectWebPhone"
456
438
  >
457
439
  Connect Web Phone
@@ -471,14 +453,27 @@
471
453
  </div>
472
454
 
473
455
  <div class="web-phone-console">
474
- <div class="web-phone-media" :class="{ 'has-video': webPhoneForm.video && webPhone.callState.value === 'active' }">
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
+ >
475
464
  <video ref="remoteVideoEl" autoplay playsinline class="remote-video" />
476
465
  <video ref="localVideoEl" autoplay muted playsinline class="local-video" />
477
466
  <audio ref="remoteAudioEl" autoplay />
478
- <div v-if="!webPhoneForm.video || webPhone.callState.value !== 'active'" class="audio-call-state">
467
+ <div
468
+ v-if="!webPhoneForm.video || webPhone.callState.value !== 'active' || !webPhone.remoteVideoAvailable.value"
469
+ class="audio-call-state"
470
+ >
479
471
  <v-icon size="54">{{ webPhoneCallIcon }}</v-icon>
480
472
  <strong>{{ webPhoneCallLabel }}</strong>
481
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>
482
477
  </div>
483
478
  </div>
484
479
 
@@ -532,7 +527,15 @@
532
527
  variant="outlined"
533
528
  :disabled="webPhone.callState.value !== 'active'"
534
529
  :title="webPhone.muted.value ? 'Unmute' : 'Mute'"
535
- @click="webPhone.toggleMute"
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"
536
539
  />
537
540
  <v-btn
538
541
  icon="mdi-phone-hangup"
@@ -613,6 +616,10 @@
613
616
 
614
617
  <script setup lang="ts">
615
618
  const props = defineProps({
619
+ org: {
620
+ type: String,
621
+ required: true,
622
+ },
616
623
  site: {
617
624
  type: String,
618
625
  required: true,
@@ -631,6 +638,19 @@ type ContactRow = {
631
638
  dialCode: string;
632
639
  };
633
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
+
634
654
  type DetailItem = {
635
655
  label: string;
636
656
  value?: string | number | boolean;
@@ -639,7 +659,7 @@ type DetailItem = {
639
659
  };
640
660
 
641
661
  type IntercomRow = {
642
- reader: Record<string, any>;
662
+ reader: Record<string, unknown>;
643
663
  name: string;
644
664
  location: string;
645
665
  serverAddress: string;
@@ -655,6 +675,7 @@ type IntercomRow = {
655
675
  password: string;
656
676
  operatingMode: string;
657
677
  protocol: string;
678
+ videoEnabled: boolean;
658
679
  dialMode: string;
659
680
  dialModeLabel: string;
660
681
  dialCode: string;
@@ -667,7 +688,7 @@ type IntercomRow = {
667
688
  externalDialButton: boolean;
668
689
  unknownContact: boolean;
669
690
  callAfter: string;
670
- pjsip: Record<string, any>;
691
+ pjsip: Record<string, unknown>;
671
692
  };
672
693
 
673
694
  const {
@@ -678,6 +699,7 @@ const {
678
699
  getIntercomStatus,
679
700
  makeIntercomCall,
680
701
  finalizeIntercomCall,
702
+ ensureSipAccount,
681
703
  } = useHidAmico();
682
704
 
683
705
  const activeTab = ref<IntercomTab>("intercom");
@@ -696,9 +718,11 @@ const limit = 20;
696
718
  const selectedRow = ref<IntercomRow | null>(null);
697
719
  const selectedContact = ref<ContactRow | null>(null);
698
720
  const showSipPassword = ref(false);
699
- const showWebPhonePassword = ref(false);
700
721
  const webPhoneConnecting = ref(false);
701
722
  const webPhoneActionLoading = ref(false);
723
+ const sipAccount = ref<SipAccount | null>(null);
724
+ const sipAccountLoading = ref(false);
725
+ const sipAccountError = ref("");
702
726
  const localVideoEl = ref<HTMLVideoElement | null>(null);
703
727
  const remoteVideoEl = ref<HTMLVideoElement | null>(null);
704
728
  const remoteAudioEl = ref<HTMLAudioElement | null>(null);
@@ -758,6 +782,7 @@ const sipForm = reactive({
758
782
  password: "",
759
783
  operatingMode: "sip-client",
760
784
  protocol: "UDP",
785
+ videoEnabled: false,
761
786
  });
762
787
 
763
788
  const callForm = reactive({
@@ -787,10 +812,6 @@ const makeCallForm = reactive({
787
812
  });
788
813
 
789
814
  const webPhoneForm = reactive({
790
- webSocketServer: "",
791
- aor: "",
792
- username: "",
793
- password: "",
794
815
  destination: "",
795
816
  video: false,
796
817
  });
@@ -872,6 +893,7 @@ const intercomDetailSections = computed(() => {
872
893
  items: [
873
894
  { label: "Operating Mode", value: row?.operatingMode },
874
895
  { label: "Protocol", value: row?.protocol },
896
+ { label: "Video", value: row?.videoEnabled ? "Enabled" : "Disabled" },
875
897
  ],
876
898
  },
877
899
  {
@@ -926,8 +948,14 @@ const webPhoneCallIcon = computed(() => {
926
948
  });
927
949
 
928
950
  watch(
929
- () => props.site,
930
- () => reload(),
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
+ },
931
959
  { immediate: true },
932
960
  );
933
961
 
@@ -1008,7 +1036,7 @@ function paginate<T>(items: T[]) {
1008
1036
  return items.slice(start, start + limit);
1009
1037
  }
1010
1038
 
1011
- function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>, status: string): IntercomRow {
1039
+ function toIntercomRow(reader: Record<string, unknown>, pjsip: Record<string, unknown>, status: string): IntercomRow {
1012
1040
  const peerToPeer = toFlag(getPjsipValue(pjsip, ["peer_to_peer_enabled", "peerToPeerEnabled"])) === "1";
1013
1041
  const dialMode = toText(getPjsipValue(pjsip, ["dialing_display_mode", "dialingDisplayMode"]), "0");
1014
1042
  const initialRtpPort = toText(getPjsipValue(pjsip, ["server_outbound_port", "initialRtpPort", "initial_rtp_port"]), "N/A");
@@ -1022,13 +1050,13 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
1022
1050
 
1023
1051
  return {
1024
1052
  reader,
1025
- name: reader.name || reader.deviceId || "HID Reader",
1026
- location: reader.location || "N/A",
1053
+ name: toText(reader.name || reader.deviceId, "HID Reader"),
1054
+ location: toText(reader.location, "N/A"),
1027
1055
  serverAddress: toText(getPjsipValue(pjsip, ["server_ip", "serverAddress", "server_address", "serverIp"]), "N/A"),
1028
1056
  serverPort: toText(getPjsipValue(pjsip, ["server_port", "serverPort", "port"]), "N/A"),
1029
1057
  initialRtpPort,
1030
1058
  finalRtpPort,
1031
- monitorPath: reader.monitorPath || "N/A",
1059
+ monitorPath: toText(reader.monitorPath, "N/A"),
1032
1060
  status,
1033
1061
  sipEnabled: toFlag(getPjsipValue(pjsip, ["enabled"])) === "1",
1034
1062
  numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
@@ -1037,6 +1065,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
1037
1065
  password: toText(getPjsipValue(pjsip, ["password"]), ""),
1038
1066
  operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
1039
1067
  protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
1068
+ videoEnabled: toFlag(getPjsipValue(pjsip, ["video_enabled", "videoEnabled"])) === "1",
1040
1069
  dialMode,
1041
1070
  dialModeLabel: getDialModeLabel(dialMode),
1042
1071
  dialCode: toText(getPjsipValue(pjsip, ["open_door_command", "dialCode"]), "N/A"),
@@ -1053,7 +1082,7 @@ function toIntercomRow(reader: Record<string, any>, pjsip: Record<string, any>,
1053
1082
  };
1054
1083
  }
1055
1084
 
1056
- function normalizePjsip(...sources: Record<string, any>[]) {
1085
+ function normalizePjsip(...sources: Record<string, unknown>[]) {
1057
1086
  for (const source of sources) {
1058
1087
  const candidate = findPjsipCandidate(source);
1059
1088
  if (candidate) return candidate;
@@ -1061,42 +1090,43 @@ function normalizePjsip(...sources: Record<string, any>[]) {
1061
1090
  return {};
1062
1091
  }
1063
1092
 
1064
- function findPjsipCandidate(source: Record<string, any> | undefined): Record<string, any> | undefined {
1093
+ function findPjsipCandidate(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
1065
1094
  if (!source || typeof source !== "object") return undefined;
1066
1095
  const candidates = [
1067
1096
  source.pjsip,
1068
- source.configuration?.pjsip,
1069
- source.result?.pjsip,
1070
- source.result?.configuration?.pjsip,
1071
- source.data?.pjsip,
1072
- source.data?.configuration?.pjsip,
1073
- source.data?.result?.pjsip,
1074
- source.data?.result?.configuration?.pjsip,
1075
- source.data?.data?.pjsip,
1076
- source.data?.data?.configuration?.pjsip,
1077
- source.configuration,
1078
- source.data?.configuration,
1079
- source.data?.data?.configuration,
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),
1080
1110
  source,
1081
1111
  ];
1082
1112
 
1083
1113
  return candidates.find((candidate) => isPjsipCandidate(candidate));
1084
1114
  }
1085
1115
 
1086
- function isPjsipCandidate(value: unknown): value is Record<string, any> {
1116
+ function isPjsipCandidate(value: unknown): value is Record<string, unknown> {
1087
1117
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1088
1118
  return pjsipKeys.some((key) => Object.prototype.hasOwnProperty.call(value, key)) ||
1089
1119
  ["serverAddress", "serverIp", "serverPort", "extension", "peerToPeerEnabled"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
1090
1120
  }
1091
1121
 
1092
- function getPjsipValue(source: Record<string, any>, keys: string[]) {
1122
+ function getPjsipValue(source: Record<string, unknown>, keys: string[]) {
1093
1123
  for (const key of keys) {
1094
1124
  if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== "") return source[key];
1095
1125
  }
1096
1126
  return undefined;
1097
1127
  }
1098
1128
 
1099
- function normalizeStatus(response: Record<string, any>, pjsip: Record<string, any>) {
1129
+ function normalizeStatus(response: Record<string, unknown>, pjsip: Record<string, unknown>) {
1100
1130
  const status = Number(response?.status);
1101
1131
  if (toFlag(pjsip.enabled) === "0" || status === -1) return "Disabled";
1102
1132
  if (status === 200) return "Connected";
@@ -1136,6 +1166,7 @@ function openSipDialog(row: IntercomRow) {
1136
1166
  sipForm.password = row.password;
1137
1167
  sipForm.operatingMode = row.operatingMode === "Peer-to-Peer" ? "peer-to-peer" : "sip-client";
1138
1168
  sipForm.protocol = row.protocol || "UDP";
1169
+ sipForm.videoEnabled = row.videoEnabled;
1139
1170
  sipDialog.value = true;
1140
1171
  }
1141
1172
 
@@ -1175,19 +1206,46 @@ function openWebPhoneDialog(row?: IntercomRow) {
1175
1206
  selectedRow.value = activeRow;
1176
1207
  if (!webPhone.isRegistered.value && activeRow.extension !== "N/A") {
1177
1208
  webPhoneForm.destination = activeRow.extension;
1209
+ webPhoneForm.video = activeRow.videoEnabled;
1178
1210
  }
1179
1211
  }
1180
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
+ }
1181
1230
  }
1182
1231
 
1183
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
+
1184
1242
  webPhoneConnecting.value = true;
1185
1243
  try {
1186
1244
  await webPhone.connect({
1187
- webSocketServer: webPhoneForm.webSocketServer,
1188
- aor: webPhoneForm.aor,
1189
- authorizationUsername: webPhoneForm.username,
1190
- authorizationPassword: webPhoneForm.password,
1245
+ webSocketServer: activeAccount.websocketUrl,
1246
+ aor: activeAccount.sipAddress,
1247
+ authorizationUsername: activeAccount.username,
1248
+ authorizationPassword: activeAccount.password,
1191
1249
  displayName: "iService365 Web",
1192
1250
  video: webPhoneForm.video,
1193
1251
  }, {
@@ -1196,7 +1254,7 @@ async function connectWebPhone() {
1196
1254
  remoteAudio: remoteAudioEl.value,
1197
1255
  });
1198
1256
  showMessage("Web phone connected and ready to receive calls.", "success");
1199
- } catch (error: any) {
1257
+ } catch (error: unknown) {
1200
1258
  showMessage(getErrorMessage(error), "error");
1201
1259
  } finally {
1202
1260
  webPhoneConnecting.value = false;
@@ -1217,7 +1275,7 @@ async function startWebCall() {
1217
1275
  webPhoneActionLoading.value = true;
1218
1276
  try {
1219
1277
  await webPhone.call(webPhoneForm.destination);
1220
- } catch (error: any) {
1278
+ } catch (error: unknown) {
1221
1279
  showMessage(getErrorMessage(error), "error");
1222
1280
  } finally {
1223
1281
  webPhoneActionLoading.value = false;
@@ -1228,7 +1286,7 @@ async function answerWebCall() {
1228
1286
  webPhoneActionLoading.value = true;
1229
1287
  try {
1230
1288
  await webPhone.answer();
1231
- } catch (error: any) {
1289
+ } catch (error: unknown) {
1232
1290
  showMessage(getErrorMessage(error), "error");
1233
1291
  } finally {
1234
1292
  webPhoneActionLoading.value = false;
@@ -1239,7 +1297,7 @@ async function declineWebCall() {
1239
1297
  webPhoneActionLoading.value = true;
1240
1298
  try {
1241
1299
  await webPhone.decline();
1242
- } catch (error: any) {
1300
+ } catch (error: unknown) {
1243
1301
  showMessage(getErrorMessage(error), "error");
1244
1302
  } finally {
1245
1303
  webPhoneActionLoading.value = false;
@@ -1250,13 +1308,29 @@ async function hangupWebCall() {
1250
1308
  webPhoneActionLoading.value = true;
1251
1309
  try {
1252
1310
  await webPhone.hangup();
1253
- } catch (error: any) {
1311
+ } catch (error: unknown) {
1254
1312
  showMessage(getErrorMessage(error), "error");
1255
1313
  } finally {
1256
1314
  webPhoneActionLoading.value = false;
1257
1315
  }
1258
1316
  }
1259
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
+
1260
1334
  function openContactDialog(contact?: ContactRow) {
1261
1335
  selectedContact.value = contact || null;
1262
1336
  contactForm.id = contact?.id;
@@ -1305,6 +1379,7 @@ async function saveSipSettings() {
1305
1379
  login: isPeerToPeer ? "" : sipForm.login,
1306
1380
  password: isPeerToPeer ? "" : sipForm.password,
1307
1381
  peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
1382
+ video_enabled: sipForm.videoEnabled ? "1" : "0",
1308
1383
  });
1309
1384
  if (saved) {
1310
1385
  sipDialog.value = false;
@@ -1492,8 +1567,42 @@ function showMessage(text: string, type: MessageType = "info") {
1492
1567
  message.type = type;
1493
1568
  }
1494
1569
 
1495
- function getErrorMessage(error: any) {
1496
- return error?.data?.message || error?.response?._data?.message || error?.message || "HID intercom request failed.";
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> : {};
1497
1606
  }
1498
1607
 
1499
1608
  function toText(value: unknown, fallback = "") {
@@ -1795,6 +1904,20 @@ export default {
1795
1904
  margin-bottom: 12px;
1796
1905
  }
1797
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
+
1798
1921
  .sip-switch-row :deep(.v-switch) {
1799
1922
  flex: 0 0 auto;
1800
1923
  }
@@ -2226,6 +2349,73 @@ export default {
2226
2349
  font-weight: 700;
2227
2350
  }
2228
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
+
2229
2419
  .web-video-toggle {
2230
2420
  margin-top: 12px;
2231
2421
  }
@@ -2268,7 +2458,7 @@ export default {
2268
2458
  object-fit: cover;
2269
2459
  }
2270
2460
 
2271
- .web-phone-media.has-video .remote-video {
2461
+ .web-phone-media.has-remote-video .remote-video {
2272
2462
  display: block;
2273
2463
  }
2274
2464
 
@@ -2285,10 +2475,14 @@ export default {
2285
2475
  object-fit: cover;
2286
2476
  }
2287
2477
 
2288
- .web-phone-media.has-video .local-video {
2478
+ .web-phone-media.has-local-video .local-video {
2289
2479
  display: block;
2290
2480
  }
2291
2481
 
2482
+ .web-phone-media.has-remote-video .audio-call-state {
2483
+ display: none;
2484
+ }
2485
+
2292
2486
  .audio-call-state {
2293
2487
  display: flex;
2294
2488
  align-items: center;