callix-dialer-widget 2.8.3 → 2.8.5

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.
@@ -43754,6 +43754,7 @@ const INITIAL_TRANSCRIPT = { segments: [], status: null, insight: null, interims
43754
43754
  const MAX_SEGMENTS = 300;
43755
43755
  const RECONNECT_MIN_MS = 500;
43756
43756
  const RECONNECT_MAX_MS = 5e3;
43757
+ const ENGINE_VIEW_TIMEOUT_MS = 15e3;
43757
43758
  const PENDING_COMMANDS_MAX = 10;
43758
43759
  const PENDING_COMMANDS_TTL_MS = 15e3;
43759
43760
  function engineWsUrl(baseUrl) {
@@ -43775,6 +43776,9 @@ class EngineClient {
43775
43776
  this.reconnectDelay = RECONNECT_MIN_MS;
43776
43777
  this.pendingCommands = [];
43777
43778
  this.reconnectTimer = null;
43779
+ this.viewTimer = null;
43780
+ this.lastSnapshotAt = null;
43781
+ this.connectionError = null;
43778
43782
  this.closed = false;
43779
43783
  this.iceServers = [{ urls: "stun:stun.l.google.com:19302" }];
43780
43784
  this.pc = null;
@@ -43828,13 +43832,20 @@ class EngineClient {
43828
43832
  if (this.closed) this.closed = false;
43829
43833
  if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return;
43830
43834
  const Impl = this.options.WebSocketImpl ?? WebSocket;
43831
- this.setState({ wsState: "connecting" });
43835
+ this.setState({ wsState: "connecting", tabId: null, view: null });
43832
43836
  const ws = new Impl(engineWsUrl(this.options.url));
43833
43837
  this.ws = ws;
43838
+ this.watchForView(ws);
43834
43839
  ws.onopen = () => {
43840
+ var _a2, _b;
43835
43841
  if (this.ws !== ws) return;
43842
+ if (this.pendingLogoffToken !== void 0 && ((_b = (_a2 = this.options).sessionToken) == null ? void 0 : _b.call(_a2)) !== this.pendingLogoffToken) {
43843
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command.name !== "logoff");
43844
+ this.rejectWaiters(new EngineAccessError("session-changed", "A sessão mudou antes de confirmar a saída."));
43845
+ }
43836
43846
  this.reconnectDelay = RECONNECT_MIN_MS;
43837
43847
  this.setState({ wsState: "open" });
43848
+ this.watchForView(ws);
43838
43849
  const sessionToken = !this.session && this.options.sessionToken ? this.options.sessionToken() : null;
43839
43850
  const hello = {
43840
43851
  type: "hello",
@@ -43860,24 +43871,14 @@ class EngineClient {
43860
43871
  this.handle(message);
43861
43872
  };
43862
43873
  ws.onclose = () => {
43863
- if (this.ws !== ws) return;
43864
- this.ws = null;
43865
- const wasEngaged = this.state.engaged;
43866
- this.setState({ wsState: "closed", tabId: null, engaged: false });
43867
- if (wasEngaged) this.stopAudio();
43868
- if (this.closed) return;
43869
- this.log(`conexão com o motor caiu; nova tentativa em ${this.reconnectDelay} ms`);
43870
- this.reconnectTimer = setTimeout(() => {
43871
- this.reconnectTimer = null;
43872
- this.connect();
43873
- }, this.reconnectDelay);
43874
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
43874
+ this.disconnected(ws, "Conexão com o motor interrompida. Reconectando...");
43875
43875
  };
43876
43876
  ws.onerror = () => void 0;
43877
43877
  }
43878
43878
  /** fecha de vez (sem reconectar) e solta o áudio */
43879
43879
  close() {
43880
43880
  this.closed = true;
43881
+ this.clearViewTimer();
43881
43882
  if (this.reconnectTimer) {
43882
43883
  clearTimeout(this.reconnectTimer);
43883
43884
  this.reconnectTimer = null;
@@ -43889,8 +43890,41 @@ class EngineClient {
43889
43890
  ws.onclose = null;
43890
43891
  ws.close();
43891
43892
  }
43892
- for (const waiter of this.viewWaiters.splice(0)) clearTimeout(waiter.timer);
43893
- this.setState({ wsState: "closed", tabId: null, engaged: false });
43893
+ this.pendingCommands = [];
43894
+ this.rejectWaiters(new Error("a conexão com o motor foi encerrada"));
43895
+ this.userSignal.set(void 0);
43896
+ this.setState({ wsState: "closed", tabId: null, view: null, engaged: false });
43897
+ }
43898
+ clearViewTimer() {
43899
+ if (this.viewTimer) clearTimeout(this.viewTimer);
43900
+ this.viewTimer = null;
43901
+ }
43902
+ watchForView(ws) {
43903
+ var _a2, _b;
43904
+ this.clearViewTimer();
43905
+ this.viewTimer = setTimeout(() => {
43906
+ if (this.ws !== ws) return;
43907
+ this.disconnected(ws, "O motor parou de atualizar o estado. Reconectando...");
43908
+ ws.close();
43909
+ }, ENGINE_VIEW_TIMEOUT_MS);
43910
+ (_b = (_a2 = this.viewTimer).unref) == null ? void 0 : _b.call(_a2);
43911
+ }
43912
+ disconnected(ws, message) {
43913
+ if (this.ws !== ws) return;
43914
+ this.ws = null;
43915
+ this.clearViewTimer();
43916
+ this.userSignal.set(void 0);
43917
+ this.connectionError = message;
43918
+ this.setState({ wsState: "closed", tabId: null, view: null, engaged: false, lastError: message });
43919
+ this.stopAudio();
43920
+ if (this.closed) return;
43921
+ this.log(`conexão com o motor caiu; nova tentativa em ${this.reconnectDelay} ms`);
43922
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
43923
+ this.reconnectTimer = setTimeout(() => {
43924
+ this.reconnectTimer = null;
43925
+ this.connect();
43926
+ }, this.reconnectDelay);
43927
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
43894
43928
  }
43895
43929
  /**
43896
43930
  * A view atual, ou a próxima que o host mandar (welcome/view). Com `accept`, espera uma
@@ -43900,7 +43934,7 @@ class EngineClient {
43900
43934
  waitForView(timeoutMs = 15e3, accept, options) {
43901
43935
  const rejected = this.state.rejected;
43902
43936
  if (rejected) return Promise.reject(new EngineAccessError(rejected.code, rejected.message));
43903
- if (!(options == null ? void 0 : options.fresh) && this.state.view && this.state.tabId && (!accept || accept(this.state.view))) return Promise.resolve(this.state.view);
43937
+ if (!(options == null ? void 0 : options.fresh) && this.state.wsState === "open" && this.state.view && this.state.tabId && (!accept || accept(this.state.view))) return Promise.resolve(this.state.view);
43904
43938
  return new Promise((resolve, reject) => {
43905
43939
  const timer = setTimeout(() => {
43906
43940
  const index = this.viewWaiters.findIndex((w) => w.timer === timer);
@@ -43924,6 +43958,7 @@ class EngineClient {
43924
43958
  case "welcome": {
43925
43959
  if (Array.isArray(message.iceServers) && message.iceServers.length > 0) this.iceServers = message.iceServers;
43926
43960
  this.setState({ tabId: message.tabId, rejected: null });
43961
+ if (this.ws) this.watchForView(this.ws);
43927
43962
  this.flushPendingCommands();
43928
43963
  this.applyView(message.view);
43929
43964
  this.log(`sou a aba ${message.tabId}`);
@@ -43951,6 +43986,10 @@ class EngineClient {
43951
43986
  break;
43952
43987
  }
43953
43988
  if (message.code === "session-changed") {
43989
+ if (this.pendingLogoffToken !== void 0) {
43990
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command.name !== "logoff");
43991
+ this.rejectWaiters(new EngineAccessError(message.code, message.message));
43992
+ }
43954
43993
  this.session = null;
43955
43994
  this.log("a sessão deste agente foi trocada por um login novo; reconectando com a credencial atual");
43956
43995
  break;
@@ -43977,21 +44016,34 @@ class EngineClient {
43977
44016
  }
43978
44017
  }
43979
44018
  applyView(view) {
43980
- var _a2, _b;
44019
+ var _a2, _b, _c;
43981
44020
  const previous = this.state.view;
44021
+ const snapshotAt = (_a2 = view.snapshot) == null ? void 0 : _a2.at;
44022
+ if (view.engine === "stopped" || view.engine === "crashed") {
44023
+ this.clearViewTimer();
44024
+ } else if (this.ws && (snapshotAt !== void 0 && snapshotAt !== this.lastSnapshotAt || (previous == null ? void 0 : previous.engine) === "stopped" || (previous == null ? void 0 : previous.engine) === "crashed")) {
44025
+ this.watchForView(this.ws);
44026
+ }
44027
+ if (snapshotAt !== void 0) this.lastSnapshotAt = snapshotAt;
43982
44028
  const lost = view.sessionLost ?? null;
43983
44029
  const newlyLost = Boolean(lost) && !(previous == null ? void 0 : previous.sessionLost);
43984
44030
  if (newlyLost) {
43985
44031
  this.session = null;
43986
44032
  this.log(`o Callix derrubou a sessão: ${(lost == null ? void 0 : lost.message) ?? ""}`);
43987
44033
  }
43988
- const user = (_a2 = view.snapshot) == null ? void 0 : _a2.leg.user;
44034
+ const user = (_b = view.snapshot) == null ? void 0 : _b.leg.user;
43989
44035
  this.userSignal.set(user ? { ...user, sessionId: 0 } : void 0);
43990
- const call = (_b = view.snapshot) == null ? void 0 : _b.leg.call;
44036
+ const call = (_c = view.snapshot) == null ? void 0 : _c.leg.call;
43991
44037
  const patch = { view, sessionLost: lost ? { originator: lost.originator, message: lost.message } : null };
44038
+ if (this.connectionError && this.state.lastError === this.connectionError) patch.lastError = null;
44039
+ this.connectionError = null;
43992
44040
  if (call && typeof call.muted === "boolean" && call.muted !== this.state.inputMuted && call.state !== "ended") patch.inputMuted = call.muted;
43993
44041
  this.setState(patch);
43994
44042
  for (const waiter of this.viewWaiters.splice(0)) {
44043
+ if (this.state.wsState !== "open" || !this.state.tabId) {
44044
+ this.viewWaiters.push(waiter);
44045
+ continue;
44046
+ }
43995
44047
  if (waiter.accept && !waiter.accept(view)) {
43996
44048
  this.viewWaiters.push(waiter);
43997
44049
  continue;
@@ -44014,6 +44066,10 @@ class EngineClient {
44014
44066
  * (é assim que um login novo substitui a sessão de outro navegador).
44015
44067
  */
44016
44068
  setSession(session) {
44069
+ if (this.pendingLogoffToken !== void 0) {
44070
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command.name !== "logoff");
44071
+ this.rejectWaiters(new EngineAccessError("session-changed", "Uma sessão nova substituiu o pedido de saída anterior."));
44072
+ }
44017
44073
  this.session = session;
44018
44074
  this.setState({ rejected: null, sessionLost: null });
44019
44075
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
@@ -44042,8 +44098,38 @@ class EngineClient {
44042
44098
  release() {
44043
44099
  this.send({ type: "release" });
44044
44100
  }
44101
+ /** Só confirma a saída quando o host terminou o encerramento do SDK e da sessão. */
44102
+ async logoff(timeoutMs = 4e4) {
44103
+ var _a2, _b;
44104
+ if (this.state.rejected) throw new EngineAccessError(this.state.rejected.code, this.state.rejected.message);
44105
+ this.session = null;
44106
+ this.pendingCommands = [];
44107
+ this.pendingLogoffToken = (_b = (_a2 = this.options).sessionToken) == null ? void 0 : _b.call(_a2);
44108
+ const command = { name: "logoff" };
44109
+ let acknowledged = false;
44110
+ const confirmation = this.waitForView(timeoutMs, (view) => {
44111
+ if (view.loggingOff) {
44112
+ acknowledged = true;
44113
+ return false;
44114
+ }
44115
+ return view.engine === "stopped" || view.engine === "crashed" || acknowledged && Boolean(view.logoffError);
44116
+ }, { fresh: true });
44117
+ this.command(command);
44118
+ try {
44119
+ const view = await confirmation;
44120
+ if (view.logoffError) throw new Error(view.logoffError);
44121
+ if (!acknowledged || view.engine !== "stopped") throw new Error("O motor parou sem confirmar o encerramento da sessão no Callix.");
44122
+ } catch (error2) {
44123
+ if (error2 instanceof EngineAccessError) throw error2;
44124
+ throw new Error(error2 instanceof Error && error2.message !== "o motor não respondeu" ? error2.message : "O motor não confirmou o encerramento da sessão.");
44125
+ } finally {
44126
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command !== command);
44127
+ this.pendingLogoffToken = void 0;
44128
+ }
44129
+ }
44045
44130
  command(command) {
44046
44131
  if (this.ws && this.ws.readyState === WebSocket.OPEN && this.state.tabId) {
44132
+ this.setState({ lastError: null });
44047
44133
  this.send({ type: "command", command });
44048
44134
  this.log(`comando: ${command.name}${command.phone ? ` ${command.phone}` : ""}`);
44049
44135
  return;
@@ -44051,7 +44137,8 @@ class EngineClient {
44051
44137
  this.pendingCommands.push({ command, at: Date.now() });
44052
44138
  if (this.pendingCommands.length > PENDING_COMMANDS_MAX) this.pendingCommands.shift();
44053
44139
  this.log(`comando: ${command.name} guardado até a conexão com o motor voltar`);
44054
- this.setState({ lastError: "Sem conexão com o motor neste momento; o comando será enviado assim que ela voltar." });
44140
+ this.connectionError = "Sem conexão com o motor neste momento; o comando será enviado assim que ela voltar.";
44141
+ this.setState({ lastError: this.connectionError });
44055
44142
  this.connect();
44056
44143
  }
44057
44144
  /** manda os comandos guardados que ainda fazem sentido (os velhos poderiam atingir outra chamada) */
@@ -44313,13 +44400,13 @@ class EngineCall {
44313
44400
  return Math.max(0, Math.floor((Date.now() - from) / 1e3));
44314
44401
  }
44315
44402
  answer() {
44316
- this.client.command({ name: "answer" });
44403
+ engineCommand(this.client, { name: "answer" });
44317
44404
  }
44318
44405
  reject() {
44319
- this.client.command({ name: "reject" });
44406
+ engineCommand(this.client, { name: "reject" });
44320
44407
  }
44321
44408
  hangup() {
44322
- this.client.command({ name: "hangup" });
44409
+ engineCommand(this.client, { name: "hangup" });
44323
44410
  }
44324
44411
  isOnHold() {
44325
44412
  return { local: false, remote: false };
@@ -44338,7 +44425,7 @@ class EngineCall {
44338
44425
  this.client.setInputMuted(false);
44339
44426
  }
44340
44427
  sendDtmf(tones) {
44341
- this.client.command({ name: "dtmf", tones });
44428
+ engineCommand(this.client, { name: "dtmf", tones });
44342
44429
  }
44343
44430
  on(event, listener) {
44344
44431
  let set = this.listeners.get(event);
@@ -44374,10 +44461,12 @@ class EngineCall {
44374
44461
  }
44375
44462
  }
44376
44463
  function operatorStateFromView(view, call) {
44377
- var _a2, _b, _c;
44464
+ var _a2, _b, _c, _d, _e;
44378
44465
  if (!view) return void 0;
44379
- if (((_a2 = view.sessionLost) == null ? void 0 : _a2.originator) === "anotherDevice") return { state: "startupError", error: "userIsAlreadyConnectedOnAnotherDevice" };
44380
- const leg = (_b = view.snapshot) == null ? void 0 : _b.leg;
44466
+ if (view.logoffError || ((_a2 = view.snapshot) == null ? void 0 : _a2.leg.logoffError)) return { state: "startupError", error: "unknownError" };
44467
+ if (view.loggingOff || ((_b = view.snapshot) == null ? void 0 : _b.leg.loggingOff)) return { state: "starting" };
44468
+ if (((_c = view.sessionLost) == null ? void 0 : _c.originator) === "anotherDevice") return { state: "startupError", error: "userIsAlreadyConnectedOnAnotherDevice" };
44469
+ const leg = (_d = view.snapshot) == null ? void 0 : _d.leg;
44381
44470
  if (!leg) {
44382
44471
  if (view.engine === "starting") return { state: "starting" };
44383
44472
  if (view.engine === "running") return { state: "starting" };
@@ -44392,7 +44481,7 @@ function operatorStateFromView(view, call) {
44392
44481
  case "ringing":
44393
44482
  return { state: "callRinging", call: asCall };
44394
44483
  case "dialing":
44395
- return ((_c = leg.call) == null ? void 0 : _c.state) === "ringing" ? { state: "manualCallRinging", call: asCall } : { state: "manualCallSetup", call: asCall };
44484
+ return ((_e = leg.call) == null ? void 0 : _e.state) === "ringing" ? { state: "manualCallRinging", call: asCall } : { state: "manualCallSetup", call: asCall };
44396
44485
  case "inCall":
44397
44486
  return { state: "callInProgress", call: asCall };
44398
44487
  case "afterCall":
@@ -44405,6 +44494,30 @@ function operatorStateFromView(view, call) {
44405
44494
  return { state: "startupError", error: "unknownError" };
44406
44495
  }
44407
44496
  }
44497
+ function operatorStateFromClientState(state, call) {
44498
+ if (state.wsState !== "open" || !state.tabId || !state.view) return { state: "starting" };
44499
+ if (state.rejected) return { state: "startupError", error: "unknownError" };
44500
+ return operatorStateFromView(state.view, call);
44501
+ }
44502
+ function assertEngineCommand(client2, allowDisconnected = false) {
44503
+ var _a2, _b, _c, _d;
44504
+ const state = client2.getState();
44505
+ const view = state.view;
44506
+ const error2 = ((_a2 = state.rejected) == null ? void 0 : _a2.message) ?? ((_b = state.sessionLost) == null ? void 0 : _b.message) ?? (view == null ? void 0 : view.logoffError) ?? ((_c = view == null ? void 0 : view.snapshot) == null ? void 0 : _c.leg.logoffError);
44507
+ if (error2) throw new Error(error2);
44508
+ if ((view == null ? void 0 : view.loggingOff) || ((_d = view == null ? void 0 : view.snapshot) == null ? void 0 : _d.leg.loggingOff)) throw new Error("Aguarde o encerramento da sessão antes de usar o discador.");
44509
+ if (state.wsState !== "open" || !state.tabId || !view) {
44510
+ if (allowDisconnected) return;
44511
+ throw new Error("Sem conexão com o motor. Aguarde a reconexão antes de usar o discador.");
44512
+ }
44513
+ if (view.engine !== "running" || !view.snapshot || view.snapshot.leg.status === "offline" || view.snapshot.leg.status === "error" || view.snapshot.leg.status === "starting") {
44514
+ throw new Error("O atendimento ainda não está conectado ao Callix. Aguarde antes de usar o discador.");
44515
+ }
44516
+ }
44517
+ function engineCommand(client2, command) {
44518
+ assertEngineCommand(client2, command.name === "hangup");
44519
+ client2.command(command);
44520
+ }
44408
44521
  function callInfoFromView(view) {
44409
44522
  var _a2;
44410
44523
  const leg = (_a2 = view == null ? void 0 : view.snapshot) == null ? void 0 : _a2.leg;
@@ -44436,9 +44549,9 @@ function callInfoFromView(view) {
44436
44549
  return { type: "manual", info: { successQualifications, discardQualifications } };
44437
44550
  }
44438
44551
  function useEngineCall(client2) {
44439
- var _a2, _b;
44552
+ var _a2, _b, _c, _d, _e, _f, _g, _h;
44440
44553
  const state = useEngineState(client2);
44441
- const snapshot = ((_b = (_a2 = state.view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.call) ?? null;
44554
+ const snapshot = state.wsState === "open" && state.tabId && !((_a2 = state.view) == null ? void 0 : _a2.logoffError) && !((_b = state.view) == null ? void 0 : _b.loggingOff) && !((_d = (_c = state.view) == null ? void 0 : _c.snapshot) == null ? void 0 : _d.leg.logoffError) && !((_f = (_e = state.view) == null ? void 0 : _e.snapshot) == null ? void 0 : _f.leg.loggingOff) ? ((_h = (_g = state.view) == null ? void 0 : _g.snapshot) == null ? void 0 : _h.leg.call) ?? null : null;
44442
44555
  const ref = useRef(null);
44443
44556
  if (!snapshot || snapshot.state === "ended") {
44444
44557
  ref.current = null;
@@ -44452,7 +44565,7 @@ function useEngineCall(client2) {
44452
44565
  function useEngineOperatorState(client2) {
44453
44566
  const state = useEngineState(client2);
44454
44567
  const call = useEngineCall(client2);
44455
- return useMemo(() => operatorStateFromView(state.view, call), [state.view, call]);
44568
+ return useMemo(() => operatorStateFromClientState(state, call), [state, call]);
44456
44569
  }
44457
44570
  function useEngineCurrentCall(client2) {
44458
44571
  const call = useEngineCall(client2);
@@ -44477,15 +44590,16 @@ function useEngineControls(client2) {
44477
44590
  () => ({
44478
44591
  becomeAvailable: () => {
44479
44592
  var _a2, _b;
44593
+ assertEngineCommand(client2);
44480
44594
  const status = (_b = (_a2 = client2.getState().view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.status;
44481
- if (status === "onBreak") client2.command({ name: "resume" });
44595
+ if (status === "onBreak") engineCommand(client2, { name: "resume" });
44482
44596
  },
44483
44597
  goOffline: () => void 0,
44484
- enterOnBreak: (breakId) => client2.command(pauseCommand(breakId)),
44485
- makeManualCall: (phoneNumber) => client2.command({ name: "dial", phone: phoneNumber }),
44598
+ enterOnBreak: (breakId) => engineCommand(client2, pauseCommand(breakId)),
44599
+ makeManualCall: (phoneNumber) => engineCommand(client2, { name: "dial", phone: phoneNumber }),
44486
44600
  finishAfterCall: (data) => {
44487
- if (typeof data.qualificationId === "number") client2.command({ name: "qualify", qualificationId: data.qualificationId, result: data.result === "failure" ? "discard" : "success" });
44488
- else client2.command({ name: "finish" });
44601
+ if (typeof data.qualificationId === "number") engineCommand(client2, { name: "qualify", qualificationId: data.qualificationId, result: data.result === "failure" ? "discard" : "success" });
44602
+ else engineCommand(client2, { name: "finish" });
44489
44603
  }
44490
44604
  }),
44491
44605
  [client2]
@@ -44494,9 +44608,9 @@ function useEngineControls(client2) {
44494
44608
  function useEngineCurrentCallControls(client2) {
44495
44609
  return useMemo(
44496
44610
  () => ({
44497
- answer: () => client2.command({ name: "answer" }),
44498
- reject: () => client2.command({ name: "reject" }),
44499
- hangup: () => client2.command({ name: "hangup" }),
44611
+ answer: () => engineCommand(client2, { name: "answer" }),
44612
+ reject: () => engineCommand(client2, { name: "reject" }),
44613
+ hangup: () => engineCommand(client2, { name: "hangup" }),
44500
44614
  isOnHold: () => ({ local: false, remote: false }),
44501
44615
  hold: async () => void 0,
44502
44616
  unhold: async () => void 0,
@@ -44549,13 +44663,14 @@ function engineOperator(client2) {
44549
44663
  goOffline: () => void 0,
44550
44664
  becomeAvailable: () => {
44551
44665
  var _a2, _b;
44552
- if (((_b = (_a2 = client2.getState().view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.status) === "onBreak") client2.command({ name: "resume" });
44666
+ assertEngineCommand(client2);
44667
+ if (((_b = (_a2 = client2.getState().view) == null ? void 0 : _a2.snapshot) == null ? void 0 : _b.leg.status) === "onBreak") engineCommand(client2, { name: "resume" });
44553
44668
  },
44554
- enterOnBreak: (breakId) => client2.command(pauseCommand(breakId)),
44555
- makeManualCall: (phoneNumber) => client2.command({ name: "dial", phone: phoneNumber }),
44669
+ enterOnBreak: (breakId) => engineCommand(client2, pauseCommand(breakId)),
44670
+ makeManualCall: (phoneNumber) => engineCommand(client2, { name: "dial", phone: phoneNumber }),
44556
44671
  finishAfterCall: (data) => {
44557
- if (typeof data.qualificationId === "number") client2.command({ name: "qualify", qualificationId: data.qualificationId, result: data.result === "failure" ? "discard" : "success" });
44558
- else client2.command({ name: "finish" });
44672
+ if (typeof data.qualificationId === "number") engineCommand(client2, { name: "qualify", qualificationId: data.qualificationId, result: data.result === "failure" ? "discard" : "success" });
44673
+ else engineCommand(client2, { name: "finish" });
44559
44674
  }
44560
44675
  };
44561
44676
  operators.set(client2, operator);
@@ -57829,8 +57944,14 @@ function LiveCaptionPanelInner({ client: client2, maxLines }) {
57829
57944
  ] });
57830
57945
  }
57831
57946
  function useDialerNotices(options = {}) {
57947
+ var _a2, _b, _c;
57832
57948
  const operatorState = useCallOperatorState();
57833
57949
  const currentCall = useCallOperatorCurrentCall();
57950
+ const engine = useEngineClient();
57951
+ const subscribeEngine = useCallback((listener) => engine ? engine.subscribe(listener) : () => {
57952
+ }, [engine]);
57953
+ const readEngine = useCallback(() => (engine == null ? void 0 : engine.getState()) ?? null, [engine]);
57954
+ const engineState = useSyncExternalStore(subscribeEngine, readEngine, readEngine);
57834
57955
  const [deviceIssue, setDeviceIssue] = useState(null);
57835
57956
  const [sessionDrop, setSessionDrop] = useState(null);
57836
57957
  const [callFailure, setCallFailure] = useState(null);
@@ -57843,7 +57964,7 @@ function useDialerNotices(options = {}) {
57843
57964
  setDeviceIssue(issue);
57844
57965
  }, [options.checkDevices]);
57845
57966
  useEffect(() => {
57846
- var _a2;
57967
+ var _a3;
57847
57968
  if (options.checkDevices === false) return;
57848
57969
  let cancelled = false;
57849
57970
  const run = async () => {
@@ -57853,11 +57974,11 @@ function useDialerNotices(options = {}) {
57853
57974
  void run();
57854
57975
  const mediaDevices = typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
57855
57976
  const handler = () => void run();
57856
- (_a2 = mediaDevices == null ? void 0 : mediaDevices.addEventListener) == null ? void 0 : _a2.call(mediaDevices, "devicechange", handler);
57977
+ (_a3 = mediaDevices == null ? void 0 : mediaDevices.addEventListener) == null ? void 0 : _a3.call(mediaDevices, "devicechange", handler);
57857
57978
  return () => {
57858
- var _a3;
57979
+ var _a4;
57859
57980
  cancelled = true;
57860
- (_a3 = mediaDevices == null ? void 0 : mediaDevices.removeEventListener) == null ? void 0 : _a3.call(mediaDevices, "devicechange", handler);
57981
+ (_a4 = mediaDevices == null ? void 0 : mediaDevices.removeEventListener) == null ? void 0 : _a4.call(mediaDevices, "devicechange", handler);
57861
57982
  };
57862
57983
  }, [options.checkDevices]);
57863
57984
  useCallOperatorEventListener("userSessionDropped", (data) => {
@@ -57866,8 +57987,8 @@ function useDialerNotices(options = {}) {
57866
57987
  useEffect(() => {
57867
57988
  if (!(currentCall == null ? void 0 : currentCall.on)) return;
57868
57989
  const handleFailed = (event) => {
57869
- var _a2;
57870
- const failure = (event == null ? void 0 : event.failure) ?? ((_a2 = currentCall.toCallData) == null ? void 0 : _a2.call(currentCall).failure) ?? null;
57990
+ var _a3;
57991
+ const failure = (event == null ? void 0 : event.failure) ?? ((_a3 = currentCall.toCallData) == null ? void 0 : _a3.call(currentCall).failure) ?? null;
57871
57992
  setCallFailure(failure ?? { cause: null });
57872
57993
  setDismissedCode(null);
57873
57994
  if (failureTimer.current) clearTimeout(failureTimer.current);
@@ -57875,14 +57996,14 @@ function useDialerNotices(options = {}) {
57875
57996
  };
57876
57997
  currentCall.on("failed", handleFailed);
57877
57998
  return () => {
57878
- var _a2;
57879
- (_a2 = currentCall.off) == null ? void 0 : _a2.call(currentCall, "failed", handleFailed);
57999
+ var _a3;
58000
+ (_a3 = currentCall.off) == null ? void 0 : _a3.call(currentCall, "failed", handleFailed);
57880
58001
  };
57881
58002
  }, [currentCall, ttl]);
57882
58003
  useEffect(() => {
57883
- var _a2, _b;
58004
+ var _a3, _b2;
57884
58005
  if ((operatorState == null ? void 0 : operatorState.state) !== "manualCallFailed") return;
57885
- const failure = ((_b = (_a2 = operatorState.call) == null ? void 0 : _a2.toCallData) == null ? void 0 : _b.call(_a2).failure) ?? null;
58006
+ const failure = ((_b2 = (_a3 = operatorState.call) == null ? void 0 : _a3.toCallData) == null ? void 0 : _b2.call(_a3).failure) ?? null;
57886
58007
  setCallFailure((current) => current ?? failure ?? { cause: null });
57887
58008
  setDismissedCode(null);
57888
58009
  }, [operatorState]);
@@ -57895,13 +58016,26 @@ function useDialerNotices(options = {}) {
57895
58016
  useEffect(() => () => {
57896
58017
  if (failureTimer.current) clearTimeout(failureTimer.current);
57897
58018
  }, []);
57898
- const notice = pickNotice({
58019
+ let notice = pickNotice({
57899
58020
  deviceIssue,
57900
58021
  operatorState: (operatorState == null ? void 0 : operatorState.state) ?? null,
57901
58022
  startupError: (operatorState == null ? void 0 : operatorState.state) === "startupError" ? operatorState.error ?? null : null,
57902
58023
  sessionDrop,
57903
58024
  callFailure
57904
58025
  });
58026
+ const logoffError = ((_a2 = engineState == null ? void 0 : engineState.view) == null ? void 0 : _a2.logoffError) ?? ((_c = (_b = engineState == null ? void 0 : engineState.view) == null ? void 0 : _b.snapshot) == null ? void 0 : _c.leg.logoffError);
58027
+ const reconnecting = Boolean(engineState && (engineState.wsState !== "open" || !engineState.tabId));
58028
+ const engineError = logoffError ?? (engineState == null ? void 0 : engineState.lastError);
58029
+ if (engineState && !sessionDrop && (engineError || reconnecting)) {
58030
+ notice = {
58031
+ level: "error",
58032
+ source: "offline",
58033
+ title: logoffError ? "Saída não concluída" : reconnecting ? "Reconectando ao discador" : "Comando não concluído",
58034
+ detail: engineError || "Aguardando a conexão e o estado atual do motor.",
58035
+ action: logoffError ? "Clique em Sair novamente para concluir o encerramento da sessão." : reconnecting ? "Aguarde a conexão voltar antes de ligar." : "Confira o estado do discador e tente novamente.",
58036
+ code: logoffError ? "engine:logoff" : reconnecting ? "engine:reconnecting" : "engine:command"
58037
+ };
58038
+ }
57905
58039
  const visible = notice && notice.code !== dismissedCode ? notice : null;
57906
58040
  const dismiss = useCallback(() => {
57907
58041
  if (!notice) return;
@@ -58735,7 +58869,7 @@ function AuthenticatedMobileDialer({
58735
58869
  showClientInfoModal = false,
58736
58870
  density
58737
58871
  }) {
58738
- var _a2, _b, _c, _d, _e, _f;
58872
+ var _a2, _b, _c, _d, _e;
58739
58873
  const { skipManualQualification } = useDialerConfig();
58740
58874
  const [dialValue, setDialValue] = useState("");
58741
58875
  const [activeScreen, setActiveScreen] = useState("dialer");
@@ -58749,7 +58883,6 @@ function AuthenticatedMobileDialer({
58749
58883
  const [isClientInfoModalOpen, setIsClientInfoModalOpen] = useState(false);
58750
58884
  const [isDebugPanelOpen, setIsDebugPanelOpen] = useState(false);
58751
58885
  useDebugLogger();
58752
- const [currentBreakName, setCurrentBreakName] = useState(null);
58753
58886
  const [pauseStartTime, setPauseStartTime] = useState(null);
58754
58887
  const [pauseDurationSeconds, setPauseDurationSeconds] = useState(0);
58755
58888
  const [contactData, setContactData] = useState({
@@ -58785,6 +58918,10 @@ function AuthenticatedMobileDialer({
58785
58918
  const breaks = useSignal(operator == null ? void 0 : operator.breaks) || [];
58786
58919
  const userData = useSignal(operator == null ? void 0 : operator.user);
58787
58920
  const isOnBreak = (operatorState == null ? void 0 : operatorState.state) === "onBreak";
58921
+ const activeBreak = (operatorState == null ? void 0 : operatorState.state) === "onBreak" ? operatorState.break : void 0;
58922
+ const activeBreakId = activeBreak == null ? void 0 : activeBreak.id;
58923
+ const hasConfirmedPauseState = isOnBreak || ["idle", "manualCallSetup", "manualCallRinging", "manualCallFailed", "callRinging", "callInProgress", "afterCall"].includes((operatorState == null ? void 0 : operatorState.state) ?? "");
58924
+ const previousBreakRef = useRef({ isOnBreak, id: activeBreakId, confirmed: hasConfirmedPauseState });
58788
58925
  const [inputMuted, setInputMuted] = useCallOperatorAudioInputMuted();
58789
58926
  const haptic = useHaptic();
58790
58927
  const dtmf = useDTMF({ enabled: true });
@@ -58884,8 +59021,12 @@ function AuthenticatedMobileDialer({
58884
59021
  const INITIAL_BREAK_ID = 5;
58885
59022
  const INITIAL_BREAK_NAME = "Reunião";
58886
59023
  const callState = operatorState == null ? void 0 : operatorState.state;
58887
- const hasEnteredInitialBreakRef = useRef(false);
59024
+ const hasEnteredInitialBreakRef = useRef(isOnBreak);
58888
59025
  useEffect(() => {
59026
+ if (isOnBreak) {
59027
+ hasEnteredInitialBreakRef.current = true;
59028
+ return;
59029
+ }
58889
59030
  if (breaks.length === 0 || callState !== "idle" || hasEnteredInitialBreakRef.current || isOnBreak) {
58890
59031
  return;
58891
59032
  }
@@ -58898,10 +59039,8 @@ function AuthenticatedMobileDialer({
58898
59039
  );
58899
59040
  if (reuniaoBreak) {
58900
59041
  console.log("[MobileDialer] Operador conectado (idle), entrando em pausa inicial:", reuniaoBreak.name);
58901
- Promise.resolve(goOnBreak(reuniaoBreak.id)).then(() => {
58902
- console.log("[MobileDialer] Pausa inicial aplicada com sucesso:", reuniaoBreak.name);
58903
- setCurrentBreakName(reuniaoBreak.name);
58904
- setPauseStartTime(Date.now());
59042
+ Promise.resolve().then(() => goOnBreak(reuniaoBreak.id)).then(() => {
59043
+ console.log("[MobileDialer] Pausa inicial solicitada:", reuniaoBreak.name);
58905
59044
  }).catch((err) => {
58906
59045
  console.error("[MobileDialer] Falha ao entrar em pausa:", err);
58907
59046
  hasEnteredInitialBreakRef.current = false;
@@ -58909,10 +59048,8 @@ function AuthenticatedMobileDialer({
58909
59048
  } else {
58910
59049
  const firstBreak = breaks[0];
58911
59050
  console.log('[MobileDialer] Pausa "Reunião" não encontrada, usando:', firstBreak.name);
58912
- Promise.resolve(goOnBreak(firstBreak.id)).then(() => {
58913
- console.log("[MobileDialer] Pausa inicial aplicada com sucesso:", firstBreak.name);
58914
- setCurrentBreakName(firstBreak.name);
58915
- setPauseStartTime(Date.now());
59051
+ Promise.resolve().then(() => goOnBreak(firstBreak.id)).then(() => {
59052
+ console.log("[MobileDialer] Pausa inicial solicitada:", firstBreak.name);
58916
59053
  }).catch((err) => {
58917
59054
  console.error("[MobileDialer] Falha ao entrar em pausa:", err);
58918
59055
  hasEnteredInitialBreakRef.current = false;
@@ -58994,7 +59131,8 @@ function AuthenticatedMobileDialer({
58994
59131
  const { notice: dialerNotice, dismiss: dismissDialerNotice, recheckDevices } = useDialerNotices();
58995
59132
  const [noticeHeight, setNoticeHeight] = useState(0);
58996
59133
  const isError = callStateStr === "startupError" || callStateStr === "offline" || (dialerNotice == null ? void 0 : dialerNotice.source) === "device" || (dialerNotice == null ? void 0 : dialerNotice.source) === "session";
58997
- const isIdle = !callStateStr || callStateStr === "starting" || callStateStr === "idle" || callStateStr === "manualCallSetup" || callStateStr === "onBreak";
59134
+ const isReconnecting = !callStateStr || callStateStr === "starting";
59135
+ const isIdle = callStateStr === "idle" || callStateStr === "onBreak";
58998
59136
  const isConnecting = callState === "manualCallSetup" || callState === "manualCallRinging" || callState === "callRinging";
58999
59137
  const isInCall = callState === "callInProgress";
59000
59138
  const isAfterCall = callState === "afterCall";
@@ -59027,22 +59165,32 @@ function AuthenticatedMobileDialer({
59027
59165
  return () => clearInterval(interval);
59028
59166
  }, [isInCall]);
59029
59167
  useEffect(() => {
59030
- if (!isOnBreak || !pauseStartTime) {
59168
+ const previous = previousBreakRef.current;
59169
+ if (!hasConfirmedPauseState) {
59170
+ previousBreakRef.current = { ...previous, confirmed: false };
59031
59171
  return;
59032
59172
  }
59033
- const interval = setInterval(() => {
59034
- const elapsed = Math.floor((Date.now() - pauseStartTime) / 1e3);
59035
- setPauseDurationSeconds(elapsed);
59036
- }, 1e3);
59037
- return () => clearInterval(interval);
59038
- }, [isOnBreak, pauseStartTime]);
59039
- useEffect(() => {
59040
- if (!isOnBreak && currentBreakName) {
59041
- setCurrentBreakName(null);
59173
+ previousBreakRef.current = { isOnBreak, id: activeBreakId, confirmed: true };
59174
+ if (!isOnBreak) {
59042
59175
  setPauseStartTime(null);
59043
59176
  setPauseDurationSeconds(0);
59177
+ } else if (previous.confirmed && !previous.isOnBreak || previous.isOnBreak && previous.id !== activeBreakId) {
59178
+ setPauseStartTime(Date.now());
59179
+ setPauseDurationSeconds(0);
59044
59180
  }
59045
- }, [isOnBreak, currentBreakName]);
59181
+ }, [isOnBreak, activeBreakId, hasConfirmedPauseState]);
59182
+ useEffect(() => {
59183
+ if (!isOnBreak || pauseStartTime === null) {
59184
+ return;
59185
+ }
59186
+ const update2 = () => {
59187
+ const elapsed = Math.max(0, Math.floor((Date.now() - pauseStartTime) / 1e3));
59188
+ setPauseDurationSeconds(elapsed);
59189
+ };
59190
+ update2();
59191
+ const interval = setInterval(update2, 1e3);
59192
+ return () => clearInterval(interval);
59193
+ }, [isOnBreak, pauseStartTime]);
59046
59194
  useEffect(() => {
59047
59195
  const onClickToCall = (event) => {
59048
59196
  const task = event.detail;
@@ -59168,6 +59316,7 @@ function AuthenticatedMobileDialer({
59168
59316
  setDialValue((prev) => prev.slice(0, -1));
59169
59317
  }, [haptic]);
59170
59318
  const handleCall = useCallback(async () => {
59319
+ if (!isIdle) return;
59171
59320
  console.log("[MobileDialer] handleCall triggered, dialValue:", dialValue);
59172
59321
  addLog("event", "Call", "📞 Iniciando chamada manual", { dialValue });
59173
59322
  const sanitized = sanitizeInput(dialValue);
@@ -59200,7 +59349,7 @@ function AuthenticatedMobileDialer({
59200
59349
  setCallError("Erro ao iniciar chamada.");
59201
59350
  haptic.notificationError();
59202
59351
  }
59203
- }, [dialValue, makeManualCall, sanitizeInput, haptic]);
59352
+ }, [dialValue, makeManualCall, sanitizeInput, haptic, isIdle]);
59204
59353
  const handleHangup = useCallback(async () => {
59205
59354
  if (!hangup) return;
59206
59355
  addLog("event", "Call", "📴 Encerrando chamada");
@@ -59372,28 +59521,21 @@ function AuthenticatedMobileDialer({
59372
59521
  async (breakId) => {
59373
59522
  if (!goOnBreak) return;
59374
59523
  try {
59375
- const selectedBreak = breaks.find((b) => b.id === breakId);
59376
59524
  await goOnBreak(breakId);
59377
59525
  haptic.notificationSuccess();
59378
- setCurrentBreakName((selectedBreak == null ? void 0 : selectedBreak.name) ?? "Em pausa");
59379
- setPauseStartTime(Date.now());
59380
- setPauseDurationSeconds(0);
59381
59526
  setIsPauseModalOpen(false);
59382
59527
  } catch (error2) {
59383
59528
  console.error("Erro ao entrar em pausa", error2);
59384
59529
  haptic.notificationError();
59385
59530
  }
59386
59531
  },
59387
- [goOnBreak, breaks, haptic]
59532
+ [goOnBreak, haptic]
59388
59533
  );
59389
59534
  const handleGoOffBreak = useCallback(async () => {
59390
59535
  if (!goOffBreak) return;
59391
59536
  try {
59392
59537
  await goOffBreak();
59393
59538
  haptic.notificationSuccess();
59394
- setCurrentBreakName(null);
59395
- setPauseStartTime(null);
59396
- setPauseDurationSeconds(0);
59397
59539
  setIsPauseModalOpen(false);
59398
59540
  } catch (error2) {
59399
59541
  console.error("Erro ao sair de pausa", error2);
@@ -59439,6 +59581,7 @@ function AuthenticatedMobileDialer({
59439
59581
  return activeCallNumber ? formatPhoneForDisplay(activeCallNumber) : "(--) ----- ----";
59440
59582
  }, [callInfo, formattedDial, activeCallNumber]);
59441
59583
  const callStatusLabel = useMemo(() => {
59584
+ if (isReconnecting) return "Conectando...";
59442
59585
  if (isConnecting) return "Ligando...";
59443
59586
  if (isInCall) return "Em chamada";
59444
59587
  if (isAfterCall) return "Encerrando...";
@@ -59446,19 +59589,22 @@ function AuthenticatedMobileDialer({
59446
59589
  if (isError) return (dialerNotice == null ? void 0 : dialerNotice.title) ?? "Erro de conexão";
59447
59590
  if (isOnBreak) return "Em pausa";
59448
59591
  return "Disponível";
59449
- }, [callStateStr, dialerNotice, isAfterCall, isConnecting, isError, isInCall, isOnBreak]);
59592
+ }, [callStateStr, dialerNotice, isAfterCall, isConnecting, isError, isInCall, isOnBreak, isReconnecting]);
59450
59593
  const statusBadgeClass = useMemo(() => {
59594
+ if (isReconnecting) return "connecting";
59451
59595
  if (isError) return "error";
59452
59596
  if (isOnBreak) return "paused";
59453
59597
  return "available";
59454
- }, [isError, isOnBreak]);
59455
- const activeBreakName = (operatorState == null ? void 0 : operatorState.state) === "onBreak" ? (_b = operatorState.break) == null ? void 0 : _b.name : void 0;
59598
+ }, [isError, isOnBreak, isReconnecting]);
59599
+ const activeBreakName = activeBreak == null ? void 0 : activeBreak.name;
59456
59600
  const statusBadgeText = useMemo(() => {
59601
+ if (isReconnecting) return "Conectando";
59457
59602
  if (isError) return (dialerNotice == null ? void 0 : dialerNotice.source) === "device" ? dialerNotice.title : "Erro";
59458
- if (isOnBreak) return currentBreakName || activeBreakName || "Em pausa";
59603
+ if (isOnBreak) return activeBreakName || "Em pausa";
59459
59604
  return "Disponível";
59460
- }, [dialerNotice, isError, isOnBreak, currentBreakName, activeBreakName]);
59605
+ }, [dialerNotice, isError, isOnBreak, activeBreakName, isReconnecting]);
59461
59606
  const pauseDurationLabel = useMemo(() => {
59607
+ if (pauseStartTime === null) return "--:--";
59462
59608
  const hours = Math.floor(pauseDurationSeconds / 3600);
59463
59609
  const minutes = Math.floor(pauseDurationSeconds % 3600 / 60).toString().padStart(2, "0");
59464
59610
  const seconds = (pauseDurationSeconds % 60).toString().padStart(2, "0");
@@ -59466,7 +59612,7 @@ function AuthenticatedMobileDialer({
59466
59612
  return `${hours}:${minutes}:${seconds}`;
59467
59613
  }
59468
59614
  return `${minutes}:${seconds}`;
59469
- }, [pauseDurationSeconds]);
59615
+ }, [pauseDurationSeconds, pauseStartTime]);
59470
59616
  const callDurationLabel = useMemo(() => {
59471
59617
  const m = Math.floor(callDurationSeconds / 60).toString().padStart(2, "0");
59472
59618
  const s = (callDurationSeconds % 60).toString().padStart(2, "0");
@@ -59507,7 +59653,7 @@ function AuthenticatedMobileDialer({
59507
59653
  isOnBreak ? /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "resume-btn", id: "resumeBtn", onClick: handleGoOffBreak, children: [
59508
59654
  /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-play" }),
59509
59655
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Voltar" })
59510
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "pause-btn", id: "pauseBtn", onClick: () => setIsPauseModalOpen(true), children: [
59656
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "pause-btn", id: "pauseBtn", disabled: isReconnecting || callStateStr === "offline" || callStateStr === "startupError", onClick: () => setIsPauseModalOpen(true), children: [
59511
59657
  /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-pause" }),
59512
59658
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Pausa" })
59513
59659
  ] }),
@@ -59595,15 +59741,15 @@ function AuthenticatedMobileDialer({
59595
59741
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "client-card-header", children: [
59596
59742
  /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-user-circle client-avatar" }),
59597
59743
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "client-main-info", children: [
59598
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-name", id: "clientName", children: findValue(contactData.finalData, ["DECISOR", "NOME", "CLIENTE", "PROSPECT"]) ?? ((_c = callInfo.info.campaignContact) == null ? void 0 : _c.label) ?? "--" }),
59599
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-company", id: "clientCompany", children: findValue(contactData.finalData, ["EMPRESA", "RAZAO SOCIAL", "RAZÃO SOCIAL", "RAZAO_SOCIAL"]) ?? ((_d = callInfo.info.campaign) == null ? void 0 : _d.name) ?? "--" })
59744
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-name", id: "clientName", children: findValue(contactData.finalData, ["DECISOR", "NOME", "CLIENTE", "PROSPECT"]) ?? ((_b = callInfo.info.campaignContact) == null ? void 0 : _b.label) ?? "--" }),
59745
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-company", id: "clientCompany", children: findValue(contactData.finalData, ["EMPRESA", "RAZAO SOCIAL", "RAZÃO SOCIAL", "RAZAO_SOCIAL"]) ?? ((_c = callInfo.info.campaign) == null ? void 0 : _c.name) ?? "--" })
59600
59746
  ] })
59601
59747
  ] }),
59602
59748
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "client-card-divider" }),
59603
59749
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "client-info-list", children: [
59604
59750
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "client-info-row", children: [
59605
59751
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-info-label", children: "Socio" }),
59606
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-info-value", children: findValue(contactData.finalData, ["SOCIO", "PROSPECT", "DECISOR", "NOME", "CLIENTE"]) ?? ((_e = callInfo.info.campaignContact) == null ? void 0 : _e.label) ?? "--" })
59752
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-info-value", children: findValue(contactData.finalData, ["SOCIO", "PROSPECT", "DECISOR", "NOME", "CLIENTE"]) ?? ((_d = callInfo.info.campaignContact) == null ? void 0 : _d.label) ?? "--" })
59607
59753
  ] }),
59608
59754
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "client-info-row", children: [
59609
59755
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "client-info-label", children: "Empresa" }),
@@ -59800,7 +59946,7 @@ function AuthenticatedMobileDialer({
59800
59946
  {
59801
59947
  isOpen: isClientInfoModalOpen,
59802
59948
  campaignCallInfo,
59803
- campaignContactPhoneId: ((_f = currentCall == null ? void 0 : currentCall.data) == null ? void 0 : _f.campaignContactPhoneId) ?? null,
59949
+ campaignContactPhoneId: ((_e = currentCall == null ? void 0 : currentCall.data) == null ? void 0 : _e.campaignContactPhoneId) ?? null,
59804
59950
  callDuration: callDurationSeconds,
59805
59951
  isCallActive: isInCall || isConnecting,
59806
59952
  onClose: () => setIsClientInfoModalOpen(false)
@@ -60362,6 +60508,27 @@ function AuthenticatedMiniDialer({
60362
60508
  )
60363
60509
  ] });
60364
60510
  }
60511
+ async function endUserSdkSession(apiUrl, token, timeoutMs = 2e4) {
60512
+ const controller = new AbortController();
60513
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
60514
+ try {
60515
+ const response = await fetch(apiUrl, {
60516
+ method: "DELETE",
60517
+ headers: { Authorization: `Bearer ${token}`, "x-session-token": token },
60518
+ credentials: "same-origin",
60519
+ signal: controller.signal
60520
+ });
60521
+ const data = await response.json().catch(() => null);
60522
+ if (!response.ok || (data == null ? void 0 : data.success) === false) {
60523
+ throw new Error((data == null ? void 0 : data.error) || `O Callix não confirmou o encerramento da sessão (${response.status}).`);
60524
+ }
60525
+ } catch (error2) {
60526
+ if (controller.signal.aborted) throw new Error("O Callix não respondeu ao pedido de encerramento da sessão.");
60527
+ throw error2;
60528
+ } finally {
60529
+ clearTimeout(timer);
60530
+ }
60531
+ }
60365
60532
  const PROBE_TIMEOUT_MS = 600;
60366
60533
  const TAKEOVER_GRACE_MS = 3e3;
60367
60534
  const TAKEOVER_JITTER_MS = 600;
@@ -60814,7 +60981,7 @@ function ClickToCallAgent({ enabled = true, pollIntervalMs, sessionToken, onTask
60814
60981
  }, [enabled, pollIntervalMs, sessionToken]);
60815
60982
  return null;
60816
60983
  }
60817
- const WIDGET_VERSION = "2.8.3";
60984
+ const WIDGET_VERSION = "2.8.5";
60818
60985
  let currentSipWebSocket = null;
60819
60986
  function sendSipUnregisterIfPossible(ws) {
60820
60987
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -61172,6 +61339,7 @@ function DialerWrapper({ options, containerEl }) {
61172
61339
  const [tabDecided, setTabDecided] = useState(false);
61173
61340
  const [tabNote, setTabNote] = useState(null);
61174
61341
  const [engineWaiting, setEngineWaiting] = useState(false);
61342
+ const [engineError, setEngineError] = useState(null);
61175
61343
  const busRef = useRef(null);
61176
61344
  const takeoverTimerRef = useRef(null);
61177
61345
  const isAuthenticatedRef = useRef(false);
@@ -61179,6 +61347,8 @@ function DialerWrapper({ options, containerEl }) {
61179
61347
  const isLoadingRef = useRef(false);
61180
61348
  isLoadingRef.current = isLoading;
61181
61349
  const logoffPendingRef = useRef(false);
61350
+ const logoutInFlightRef = useRef(null);
61351
+ const logoutRetryRef = useRef(false);
61182
61352
  const handleLoginRef = useRef(async () => void 0);
61183
61353
  const handleLogoutRef = useRef(async () => void 0);
61184
61354
  const cancelTakeover = useCallback(() => {
@@ -61223,6 +61393,7 @@ function DialerWrapper({ options, containerEl }) {
61223
61393
  log: (message) => console.log("[Callix Dialer/motor]", message)
61224
61394
  });
61225
61395
  client2.onSessionLost((lost) => {
61396
+ if (logoffPendingRef.current) return;
61226
61397
  const notice = describeSessionDrop(lost.originator);
61227
61398
  setError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
61228
61399
  setIsAuthenticated(false);
@@ -61321,6 +61492,7 @@ function DialerWrapper({ options, containerEl }) {
61321
61492
  [options.domain, options.apiUrl]
61322
61493
  );
61323
61494
  const enterEngine = useCallback(async (user, domain, token) => {
61495
+ var _a2, _b, _c, _d;
61324
61496
  if (token) {
61325
61497
  const skip = await fetchSkipQualification(domain, token);
61326
61498
  if (skip !== null) {
@@ -61331,6 +61503,16 @@ function DialerWrapper({ options, containerEl }) {
61331
61503
  }
61332
61504
  }
61333
61505
  }
61506
+ const engineState = (_a2 = engineRef.current) == null ? void 0 : _a2.getState();
61507
+ if (!engineState || engineState.wsState !== "open" || !engineState.tabId || !engineState.view || engineState.rejected) {
61508
+ throw new Error("A conexão com o motor caiu. Aguarde a reconexão antes de entrar.");
61509
+ }
61510
+ if (engineState.view.engine !== "running" && engineState.view.engine !== "starting" || engineState.sessionLost || engineState.view.sessionLost) {
61511
+ throw new Error("A sessão do motor foi encerrada durante a entrada. Entre novamente.");
61512
+ }
61513
+ if (engineState.view.logoffError || engineState.view.loggingOff || ((_b = engineState.view.snapshot) == null ? void 0 : _b.leg.logoffError) || ((_c = engineState.view.snapshot) == null ? void 0 : _c.leg.loggingOff)) {
61514
+ throw new Error(engineState.view.logoffError || ((_d = engineState.view.snapshot) == null ? void 0 : _d.leg.logoffError) || "A sessão anterior ainda está sendo encerrada.");
61515
+ }
61334
61516
  try {
61335
61517
  localStorage.setItem(LAST_USERNAME_KEY, user);
61336
61518
  if (token) {
@@ -61371,12 +61553,15 @@ function DialerWrapper({ options, containerEl }) {
61371
61553
  if (!engineMode || !username || isRestoringSession) return;
61372
61554
  const user = username;
61373
61555
  const client2 = ensureEngine(user);
61374
- setEngineWaiting(!client2.getState().view);
61556
+ const initial = client2.getState();
61557
+ setEngineWaiting(initial.wsState !== "open" || !initial.tabId || !initial.view);
61375
61558
  const fallback = setTimeout(() => setEngineWaiting(false), 15e3);
61376
61559
  let joining = false;
61377
61560
  let rejectedNote = false;
61378
61561
  const sync = () => {
61379
- const { view, sessionLost, rejected } = client2.getState();
61562
+ var _a2;
61563
+ const { view, sessionLost, rejected, wsState, tabId, lastError } = client2.getState();
61564
+ setEngineError((view == null ? void 0 : view.logoffError) ?? ((_a2 = view == null ? void 0 : view.snapshot) == null ? void 0 : _a2.leg.logoffError) ?? lastError ?? null);
61380
61565
  if (!rejected && rejectedNote) {
61381
61566
  rejectedNote = false;
61382
61567
  setTabNote(null);
@@ -61394,11 +61579,10 @@ function DialerWrapper({ options, containerEl }) {
61394
61579
  setTabNote(rejected.message);
61395
61580
  return;
61396
61581
  }
61397
- if (!view) return;
61582
+ if (wsState !== "open" || !tabId || !view) return;
61398
61583
  setEngineWaiting(false);
61399
- if (view.engine === "stopped" || view.engine === "crashed") logoffPendingRef.current = false;
61400
61584
  const hasSession = (view.engine === "running" || view.engine === "starting") && !sessionLost;
61401
- if (hasSession && !logoffPendingRef.current && !isAuthenticatedRef.current && !isLoadingRef.current && !joining) {
61585
+ if (hasSession && !view.loggingOff && !view.logoffError && !logoffPendingRef.current && !isAuthenticatedRef.current && !isLoadingRef.current && !joining) {
61402
61586
  joining = true;
61403
61587
  console.log("[Callix Dialer] o motor já segura a sessão deste agente; entrando por aqui também");
61404
61588
  void joinEngine(user).finally(() => {
@@ -61567,6 +61751,11 @@ function DialerWrapper({ options, containerEl }) {
61567
61751
  }, [engineMode, tabRole, username]);
61568
61752
  const handleLogin = useCallback(async (user) => {
61569
61753
  var _a2, _b, _c, _d;
61754
+ if (logoutInFlightRef.current) return;
61755
+ if (logoutRetryRef.current) {
61756
+ await handleLogoutRef.current({ broadcast: false });
61757
+ if (logoutRetryRef.current) return;
61758
+ }
61570
61759
  setIsLoading(true);
61571
61760
  setError(null);
61572
61761
  setLoginStatus("Preparando login...");
@@ -61585,6 +61774,9 @@ function DialerWrapper({ options, containerEl }) {
61585
61774
  console.log("[Callix Dialer] o motor recusou a credencial desta aba; fazendo login novo que substitui a sessão de lá");
61586
61775
  }
61587
61776
  const domain = engineDomain();
61777
+ if ((view == null ? void 0 : view.loggingOff) || (view == null ? void 0 : view.logoffError) && (view.engine === "running" || view.engine === "starting")) {
61778
+ throw new Error(view.logoffError || "A sessão anterior ainda está sendo encerrada. Aguarde antes de entrar novamente.");
61779
+ }
61588
61780
  const engineHasSession = view !== null && (view.engine === "running" || view.engine === "starting") && !view.sessionLost;
61589
61781
  let token = null;
61590
61782
  if (engineHasSession) {
@@ -61800,54 +61992,116 @@ function DialerWrapper({ options, containerEl }) {
61800
61992
  useEffect(() => {
61801
61993
  handleLoginRef.current = handleLogin;
61802
61994
  }, [handleLogin]);
61803
- const handleLogout = useCallback(async (opts) => {
61804
- var _a2, _b;
61995
+ const handleLogout = useCallback((opts) => {
61996
+ if (logoutInFlightRef.current) return logoutInFlightRef.current;
61805
61997
  const broadcast = (opts == null ? void 0 : opts.broadcast) !== false;
61806
61998
  const keepDecision = (opts == null ? void 0 : opts.keepDecision) === true;
61807
61999
  const currentUsername = username;
62000
+ const currentToken = sessionToken ?? (currentUsername ? readStoredToken(currentUsername) : null);
62001
+ const ownsSdkSession = tabRoleRef.current === "leader" || isAuthenticatedRef.current;
61808
62002
  cancelTakeover();
61809
- if (options.engineUrl) {
61810
- if (isAuthenticatedRef.current) {
61811
- logoffPendingRef.current = true;
61812
- (_a2 = engineRef.current) == null ? void 0 : _a2.command({ name: "logoff" });
61813
- }
61814
- } else {
61815
- if (broadcast) (_b = busRef.current) == null ? void 0 : _b.post({ type: "logout" });
61816
- if (cleanupRef.current) {
61817
- console.log("[Callix Dialer] 📴 Logout: Chamando goOffline()...");
61818
- cleanupRef.current.goOffline();
61819
- await new Promise((resolve) => setTimeout(resolve, 500));
61820
- console.log("[Callix Dialer] 🛑 Logout: Chamando stop()...");
61821
- cleanupRef.current.stop();
61822
- await new Promise((resolve) => setTimeout(resolve, 500));
61823
- }
61824
- if (currentUsername) releaseLeader(currentUsername, tabIdRef.current);
61825
- }
61826
- setIsAuthenticated(false);
61827
- setSessionToken(null);
61828
- setCallixDomain(null);
62003
+ setIsLoading(true);
62004
+ isLoadingRef.current = true;
62005
+ setLoginStatus("Encerrando sessão...");
61829
62006
  setError(null);
61830
- if (keepDecision) {
61831
- setTabRole("member");
61832
- if (currentUsername) saveTabRole(currentUsername, "member");
61833
- return;
61834
- }
61835
- setTabRole(null);
61836
- clearTabRole();
61837
- setUsername(presetUsername ?? null);
61838
- if (typeof window !== "undefined" && currentUsername) {
62007
+ const run = async () => {
62008
+ var _a2, _b;
61839
62009
  try {
61840
- const STORAGE_KEYS = getStorageKeys(currentUsername);
61841
- localStorage.removeItem(LAST_USERNAME_KEY);
61842
- localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
61843
- localStorage.removeItem(STORAGE_KEYS.DOMAIN);
61844
- localStorage.removeItem(STORAGE_KEYS.TIMESTAMP);
61845
- console.log("[Callix Dialer] Sessão removida do localStorage");
62010
+ if (options.engineUrl) {
62011
+ const client2 = engineRef.current;
62012
+ const state = client2 == null ? void 0 : client2.getState();
62013
+ if (!client2) throw new Error("Não há conexão com o motor para confirmar a saída.");
62014
+ if (!(state == null ? void 0 : state.rejected) && !(state == null ? void 0 : state.sessionLost)) {
62015
+ logoffPendingRef.current = true;
62016
+ const finishStoppedSession = async () => {
62017
+ if (!currentToken) throw new Error("O motor parou sem confirmar a saída e esta aba não tem o token da sessão.");
62018
+ const stored = currentUsername ? readStoredToken(currentUsername) : null;
62019
+ if (stored && stored !== currentToken) throw new EngineAccessError("session-changed", "Outra aba substituiu a sessão durante o encerramento.");
62020
+ await endUserSdkSession(options.apiUrl || "/api/user-sdk-session", currentToken);
62021
+ };
62022
+ const isStopped = () => {
62023
+ const view = client2.getState().view;
62024
+ return (view == null ? void 0 : view.engine) === "stopped" || (view == null ? void 0 : view.engine) === "crashed";
62025
+ };
62026
+ if (isStopped()) {
62027
+ await finishStoppedSession();
62028
+ } else {
62029
+ try {
62030
+ await client2.logoff();
62031
+ } catch (err) {
62032
+ if (!isStopped()) throw err;
62033
+ await finishStoppedSession();
62034
+ }
62035
+ }
62036
+ }
62037
+ } else {
62038
+ if (broadcast) (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "logout" });
62039
+ if (cleanupRef.current) {
62040
+ cleanupRef.current.goOffline();
62041
+ await new Promise((resolve) => setTimeout(resolve, 500));
62042
+ (_b = cleanupRef.current) == null ? void 0 : _b.stop();
62043
+ await new Promise((resolve) => setTimeout(resolve, 500));
62044
+ }
62045
+ if (ownsSdkSession && !keepDecision) {
62046
+ if (!currentToken) throw new Error("Não há token para encerrar a sessão no Callix.");
62047
+ const stored = currentUsername ? readStoredToken(currentUsername) : null;
62048
+ if (stored && stored !== currentToken) throw new EngineAccessError("session-changed", "Outra aba substituiu a sessão durante o encerramento.");
62049
+ await endUserSdkSession(options.apiUrl || "/api/user-sdk-session", currentToken);
62050
+ }
62051
+ if (currentUsername) releaseLeader(currentUsername, tabIdRef.current);
62052
+ }
62053
+ logoutRetryRef.current = false;
62054
+ logoffPendingRef.current = false;
62055
+ setIsAuthenticated(false);
62056
+ setSessionToken(null);
62057
+ currentSessionRef.current.token = null;
62058
+ setCallixDomain(null);
62059
+ if (keepDecision) {
62060
+ setTabRole("member");
62061
+ if (currentUsername) saveTabRole(currentUsername, "member");
62062
+ return;
62063
+ }
62064
+ setTabRole(null);
62065
+ clearTabRole();
62066
+ setUsername(presetUsername ?? null);
62067
+ if (typeof window !== "undefined" && currentUsername && (options.engineUrl || ownsSdkSession)) {
62068
+ try {
62069
+ const STORAGE_KEYS = getStorageKeys(currentUsername);
62070
+ if (localStorage.getItem(STORAGE_KEYS.SESSION_TOKEN) === currentToken) {
62071
+ if (localStorage.getItem(LAST_USERNAME_KEY) === currentUsername) localStorage.removeItem(LAST_USERNAME_KEY);
62072
+ localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
62073
+ localStorage.removeItem(STORAGE_KEYS.DOMAIN);
62074
+ localStorage.removeItem(STORAGE_KEYS.TIMESTAMP);
62075
+ }
62076
+ } catch (err) {
62077
+ console.warn("[Callix Dialer] Não foi possível limpar os dados locais da sessão:", err);
62078
+ }
62079
+ }
61846
62080
  } catch (err) {
61847
- console.warn("[Callix Dialer] Não foi possível limpar sessão:", err);
62081
+ console.error("[Callix Dialer] Não foi possível confirmar a saída:", err);
62082
+ if (err instanceof EngineAccessError && err.code === "session-changed") {
62083
+ logoutRetryRef.current = false;
62084
+ logoffPendingRef.current = false;
62085
+ setSessionToken(null);
62086
+ setIsAuthenticated(false);
62087
+ setError("A sessão foi substituída por outro login durante a saída. Entre novamente para usar a sessão atual.");
62088
+ return;
62089
+ }
62090
+ logoutRetryRef.current = true;
62091
+ setIsAuthenticated(false);
62092
+ setError(`Não foi possível confirmar a saída: ${err instanceof Error ? err.message : "falha ao encerrar a sessão"}. Ao entrar novamente, o discador tentará concluir o encerramento primeiro.`);
62093
+ } finally {
62094
+ setIsLoading(false);
62095
+ isLoadingRef.current = false;
62096
+ setLoginStatus(null);
61848
62097
  }
61849
- }
61850
- }, [username, presetUsername, options.engineUrl, cancelTakeover]);
62098
+ };
62099
+ const completion = run().finally(() => {
62100
+ logoutInFlightRef.current = null;
62101
+ });
62102
+ logoutInFlightRef.current = completion;
62103
+ return completion;
62104
+ }, [username, sessionToken, presetUsername, options.apiUrl, options.engineUrl, cancelTakeover]);
61851
62105
  useEffect(() => {
61852
62106
  handleLogoutRef.current = handleLogout;
61853
62107
  }, [handleLogout]);
@@ -61875,6 +62129,7 @@ function DialerWrapper({ options, containerEl }) {
61875
62129
  const handleSdkSessionDropped = useCallback(
61876
62130
  (originator, notice) => {
61877
62131
  var _a2;
62132
+ if (logoutInFlightRef.current) return;
61878
62133
  const user = username;
61879
62134
  if (user) releaseLeader(user, tabIdRef.current);
61880
62135
  (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "dropped", originator });
@@ -61934,7 +62189,7 @@ function DialerWrapper({ options, containerEl }) {
61934
62189
  onLogin: handleLogin,
61935
62190
  onLogout: handleLogout,
61936
62191
  isLoadingSession: isLoading || isRestoringSession || deciding,
61937
- sessionError: error2,
62192
+ sessionError: error2 || engineError,
61938
62193
  sessionStatus: loginStatus,
61939
62194
  loginConfirmation,
61940
62195
  onCancelLoginConfirmation: handleCancelLoginConfirmation,
@@ -61957,7 +62212,7 @@ function DialerWrapper({ options, containerEl }) {
61957
62212
  onLogin: handleLogin,
61958
62213
  onLogout: handleLogout,
61959
62214
  isLoadingSession: isLoading,
61960
- sessionError: error2,
62215
+ sessionError: error2 || engineError,
61961
62216
  sessionStatus: loginStatus,
61962
62217
  variant: uiVariant,
61963
62218
  portalTarget,