callix-dialer-widget 2.4.0 → 2.5.1

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.
@@ -48425,6 +48425,125 @@ if (!globalTwind.__twindInitialized) {
48425
48425
  setup(config);
48426
48426
  globalTwind.__twindInitialized = true;
48427
48427
  }
48428
+ const AUDIO_ISSUES = {
48429
+ unsupported: { level: "error", title: "Navegador sem acesso ao microfone", detail: "Este navegador não permite usar o microfone pela página.", action: "Use Chrome, Edge ou Firefox atualizados." },
48430
+ "insecure-context": { level: "error", title: "Página sem HTTPS", detail: "O navegador só libera o microfone em páginas HTTPS (ou localhost).", action: "Abra o discador pelo endereço com https://." },
48431
+ "no-microphone": { level: "error", title: "Sem microfone", detail: "Nenhum microfone foi encontrado neste computador, por isso o discador não consegue conectar nem ligar.", action: "Conecte um fone ou headset e confira se o microfone está habilitado nas configurações de som do Windows; o discador reconhece sozinho quando aparecer." },
48432
+ "mic-permission-denied": { level: "error", title: "Microfone bloqueado pelo navegador", detail: "O navegador não permitiu que o discador use o microfone.", action: "Clique no cadeado ao lado do endereço, permita o microfone e recarregue a página." },
48433
+ "mic-unavailable": { level: "error", title: "Microfone em uso ou com falha", detail: "Outro programa pode estar usando o microfone, ou o dispositivo falhou ao iniciar.", action: "Feche outros aplicativos de áudio (Teams, Meet, gravadores) e tente de novo." },
48434
+ "no-speaker": { level: "warning", title: "Sem saída de som", detail: "Nenhum alto-falante ou fone foi encontrado: você não vai ouvir o cliente.", action: "Conecte um fone e confira o dispositivo de saída nas configurações de som." },
48435
+ "enumerate-failed": { level: "warning", title: "Não foi possível verificar os dispositivos de áudio", detail: "O navegador não listou os dispositivos de áudio.", action: "Confira as permissões da página e recarregue." }
48436
+ };
48437
+ function audioIssue(kind, detailSuffix) {
48438
+ const base = AUDIO_ISSUES[kind];
48439
+ return { ...base, detail: detailSuffix ? `${base.detail} (${detailSuffix})` : base.detail, kind, source: "device", code: `device:${kind}` };
48440
+ }
48441
+ const NO_DEVICE_ERRORS = /* @__PURE__ */ new Set(["NotFoundError", "DevicesNotFoundError", "OverconstrainedError"]);
48442
+ const DENIED_ERRORS = /* @__PURE__ */ new Set(["NotAllowedError", "PermissionDeniedError", "PermissionDismissedError", "SecurityError"]);
48443
+ const UNAVAILABLE_ERRORS = /* @__PURE__ */ new Set(["NotReadableError", "TrackStartError", "AbortError", "SourceUnavailableError"]);
48444
+ function classifyGetUserMediaError(error2) {
48445
+ const name = (error2 == null ? void 0 : error2.name) ?? "";
48446
+ if (NO_DEVICE_ERRORS.has(name)) return "no-microphone";
48447
+ if (DENIED_ERRORS.has(name)) return "mic-permission-denied";
48448
+ if (UNAVAILABLE_ERRORS.has(name)) return "mic-unavailable";
48449
+ return "mic-unavailable";
48450
+ }
48451
+ async function checkAudioDevices(options = {}) {
48452
+ const mediaDevices = options.mediaDevices === void 0 ? typeof navigator !== "undefined" ? navigator.mediaDevices : null : options.mediaDevices;
48453
+ const secure = options.isSecureContext ?? (typeof window !== "undefined" ? window.isSecureContext !== false : true);
48454
+ if (!mediaDevices || typeof mediaDevices.getUserMedia !== "function") return audioIssue(secure ? "unsupported" : "insecure-context");
48455
+ try {
48456
+ const stream = await mediaDevices.getUserMedia({ audio: true });
48457
+ for (const track of stream.getTracks()) track.stop();
48458
+ } catch (error2) {
48459
+ const kind = classifyGetUserMediaError(error2);
48460
+ const name = error2 == null ? void 0 : error2.name;
48461
+ return audioIssue(kind, kind === "mic-unavailable" && name ? name : void 0);
48462
+ }
48463
+ if (typeof mediaDevices.enumerateDevices !== "function") return null;
48464
+ let devices;
48465
+ try {
48466
+ devices = await mediaDevices.enumerateDevices();
48467
+ } catch {
48468
+ return audioIssue("enumerate-failed");
48469
+ }
48470
+ if (!devices.some((device) => device.kind === "audioinput")) return audioIssue("no-microphone");
48471
+ const canListOutputs = options.canListOutputs ?? (typeof HTMLMediaElement !== "undefined" && "setSinkId" in HTMLMediaElement.prototype);
48472
+ if (canListOutputs && !devices.some((device) => device.kind === "audiooutput")) return audioIssue("no-speaker");
48473
+ return null;
48474
+ }
48475
+ const STARTUP_NOTICES = {
48476
+ missingAudioPermissions: { level: "error", title: "Discador sem acesso ao microfone", detail: "O Callix não conseguiu usar o microfone para iniciar o atendimento.", action: "Conecte ou habilite um microfone, permita o uso no navegador e recarregue a página." },
48477
+ maxUsersReached: { level: "error", title: "Limite de usuários do Callix atingido", detail: "O serviço já está com o número máximo de agentes conectados.", action: "Aguarde um colega sair ou fale com o supervisor." },
48478
+ userIsAlreadyConnectedOnAnotherDevice: { level: "error", title: "Usuário já conectado em outro lugar", detail: "Este login está ativo em outro computador, aba ou no Callix web.", action: "Saia de lá (ou feche a outra aba) e entre de novo aqui." },
48479
+ unknownError: { level: "error", title: "O discador não conseguiu iniciar", detail: "O Callix não respondeu como esperado ao iniciar o atendimento.", action: "Recarregue a página; se continuar, confira a internet e avise o suporte." }
48480
+ };
48481
+ function describeStartupError(error2) {
48482
+ const kind = error2 && error2 in STARTUP_NOTICES ? error2 : "unknownError";
48483
+ return { ...STARTUP_NOTICES[kind], source: "startup", code: `startup:${kind}` };
48484
+ }
48485
+ const SESSION_DROP_NOTICES = {
48486
+ exclusiveLogin: { level: "error", title: "Você entrou em outro lugar", detail: "Este login foi aberto em outro computador, aba ou no Callix web, e a sessão daqui foi encerrada.", action: "Feche o outro acesso e entre de novo aqui." },
48487
+ supervisor: { level: "error", title: "Sessão encerrada pelo supervisor", detail: "Um supervisor desconectou você do atendimento.", action: "Fale com o supervisor antes de entrar de novo." },
48488
+ userBlocked: { level: "error", title: "Usuário bloqueado", detail: "Seu usuário foi bloqueado no Callix.", action: "Fale com o supervisor ou o administrador." },
48489
+ breakTimeLimit: { level: "warning", title: "Tempo de pausa excedido", detail: "A pausa passou do limite e o Callix encerrou a sessão.", action: "Entre de novo para voltar ao atendimento." },
48490
+ manualCallTimeLimit: { level: "warning", title: "Tempo de chamada manual excedido", detail: "A chamada manual passou do limite configurado e a sessão foi encerrada.", action: "Entre de novo para continuar." },
48491
+ idle: { level: "warning", title: "Sessão encerrada por inatividade", detail: "Ficou muito tempo sem atividade e o Callix desconectou.", action: "Entre de novo para continuar." },
48492
+ user: { level: "warning", title: "Você saiu do atendimento", detail: "A sessão foi encerrada a seu pedido.", action: "Entre de novo quando quiser voltar." }
48493
+ };
48494
+ function describeSessionDrop(originator) {
48495
+ const kind = originator && originator in SESSION_DROP_NOTICES ? originator : null;
48496
+ if (!kind) return { level: "error", source: "session", code: "session:unknown", title: "Sessão encerrada", detail: `A sessão no Callix foi encerrada${originator ? ` (${originator})` : ""}.`, action: "Entre de novo para continuar." };
48497
+ return { ...SESSION_DROP_NOTICES[kind], source: "session", code: `session:${kind}` };
48498
+ }
48499
+ function describeOffline() {
48500
+ return { level: "error", source: "offline", code: "offline", title: "Discador desconectado do Callix", detail: "O atendimento está offline: sem internet, sem microfone ou o Callix derrubou a conexão.", action: "Confira a internet e o microfone; o discador tenta reconectar sozinho. Se não voltar, recarregue a página." };
48501
+ }
48502
+ const REJECT_REASONS = {
48503
+ 1: { title: "Número inválido", detail: "O Callix recusou o número discado.", action: "Confira o DDD e o número." },
48504
+ 2: { title: "Usuário não encontrado", detail: "O Callix não reconheceu o usuário para esta chamada.", action: "Saia e entre de novo no discador." },
48505
+ 3: { title: "Equipe bloqueada para ligar", detail: "Sua equipe está sem permissão de chamada manual neste momento.", action: "Fale com o supervisor." },
48506
+ 4: { title: "Número na black list", detail: "Este número está na lista de bloqueio e não pode ser discado." },
48507
+ 5: { title: "Sem rota para este número", detail: "A telefonia não tem rota de saída para este destino.", action: "Confira o número ou fale com o suporte." },
48508
+ 6: { title: "Chamada recusada pelo Callix", detail: "O Callix não autorizou esta chamada." },
48509
+ 7: { title: "Erro interno do Callix", detail: "O Callix falhou ao iniciar a chamada.", action: "Tente de novo em instantes." },
48510
+ 8: { title: "Ligação internacional não autorizada", detail: "Chamadas internacionais estão desligadas para o seu usuário.", action: "Fale com o supervisor." },
48511
+ 9: { title: "Saldo de telefonia insuficiente", detail: "A conta de telefonia do Callix está sem saldo.", action: "Avise o administrador." },
48512
+ 10: { title: "Caixa postal ou silêncio", detail: "A chamada caiu em caixa postal ou ninguém falou do outro lado." }
48513
+ };
48514
+ const CAUSES = {
48515
+ Busy: { title: "Número ocupado", detail: "O destino está em outra ligação.", action: "Tente de novo em alguns minutos." },
48516
+ Unavailable: { title: "Número indisponível", detail: "O destino não atendeu ou está fora de área." },
48517
+ "Not Found": { title: "Número não existe", detail: "A operadora respondeu que este número não existe.", action: "Confira o DDD e o número." },
48518
+ "Address Incomplete": { title: "Número incompleto", detail: "Faltam dígitos no número discado.", action: "Confira o DDD e o número." },
48519
+ "Blocked By Carrier": { title: "Bloqueado pela operadora", detail: "A operadora bloqueou a chamada para este destino." },
48520
+ Rejected: { title: "Chamada recusada", detail: "O destino ou a operadora recusou a chamada." },
48521
+ "SIP Failure Code": { title: "Falha na telefonia", detail: "A operadora devolveu um erro ao completar a chamada.", action: "Tente de novo; se repetir, avise o suporte." },
48522
+ "Bad Media Description": { title: "Falha de áudio na chamada", detail: "A negociação de áudio com a operadora falhou.", action: "Recarregue a página e tente de novo." },
48523
+ "Silence or Machine Detected": { title: "Caixa postal ou silêncio", detail: "A chamada caiu em caixa postal ou ninguém falou do outro lado." }
48524
+ };
48525
+ function describeCallFailure(failure) {
48526
+ const reject = (failure == null ? void 0 : failure.rejectReason) != null ? REJECT_REASONS[failure.rejectReason] : void 0;
48527
+ const cause = (failure == null ? void 0 : failure.cause) ? CAUSES[failure.cause] : void 0;
48528
+ const chosen = reject ?? cause;
48529
+ if (chosen) return { level: "warning", source: "call", code: `call:${(failure == null ? void 0 : failure.rejectReason) != null ? `reject-${failure.rejectReason}` : failure == null ? void 0 : failure.cause}`, ...chosen };
48530
+ const raw = (failure == null ? void 0 : failure.message) || (failure == null ? void 0 : failure.cause);
48531
+ return { level: "warning", source: "call", code: "call:unknown", title: "Chamada não concluída", detail: raw ? `O Callix informou: ${raw}.` : "O Callix não informou o motivo.", action: "Tente de novo; se repetir, avise o suporte." };
48532
+ }
48533
+ function pickNotice(inputs) {
48534
+ if (inputs.sessionDrop) return describeSessionDrop(inputs.sessionDrop);
48535
+ if (inputs.operatorState === "startupError") {
48536
+ if (inputs.startupError === "missingAudioPermissions" && inputs.deviceIssue) {
48537
+ const action = inputs.deviceIssue.action ?? "";
48538
+ return { ...inputs.deviceIssue, action: /recarregue a página/i.test(action) ? action : `${action} Depois, recarregue a página para o discador iniciar.`.trim() };
48539
+ }
48540
+ return describeStartupError(inputs.startupError);
48541
+ }
48542
+ if (inputs.deviceIssue) return inputs.deviceIssue;
48543
+ if (inputs.operatorState === "manualCallFailed" || inputs.callFailure) return describeCallFailure(inputs.callFailure);
48544
+ if (inputs.operatorState === "offline") return describeOffline();
48545
+ return null;
48546
+ }
48428
48547
  const DEFAULT_API_BASE_URL = "https://callix-zeta.vercel.app";
48429
48548
  function resolveApiBaseUrl() {
48430
48549
  if (typeof window === "undefined") {
@@ -52115,6 +52234,10 @@ function AuthenticatedDialer({
52115
52234
  const visibleInputRef = useRef(null);
52116
52235
  const [callError, setCallError] = useState(null);
52117
52236
  const [availabilityError, setAvailabilityError] = useState(null);
52237
+ clientSdkReactExports.useCallOperatorEventListener("userSessionDropped", (data) => {
52238
+ const notice = describeSessionDrop(data == null ? void 0 : data.originator);
52239
+ setAvailabilityError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
52240
+ });
52118
52241
  const [afterCallError, setAfterCallError] = useState(null);
52119
52242
  const [isSubmittingAfterCall, setIsSubmittingAfterCall] = useState(false);
52120
52243
  const [afterCallResult, setAfterCallResult] = useState("success");
@@ -52306,7 +52429,11 @@ function AuthenticatedDialer({
52306
52429
  const inputs = devices.filter((device) => device.kind === "audioinput");
52307
52430
  const outputs = devices.filter((device) => device.kind === "audiooutput");
52308
52431
  setAudioDevices({ inputs, outputs });
52309
- setAudioDeviceError(null);
52432
+ const issue = await checkAudioDevices();
52433
+ if (cancelled) {
52434
+ return;
52435
+ }
52436
+ setAudioDeviceError(issue ? `${issue.title}: ${issue.detail}${issue.action ? ` ${issue.action}` : ""}` : null);
52310
52437
  setSelectedInputId((prev) => {
52311
52438
  var _a4;
52312
52439
  return prev ?? ((_a4 = inputs[0]) == null ? void 0 : _a4.deviceId) ?? null;
@@ -52521,9 +52648,8 @@ function AuthenticatedDialer({
52521
52648
  return;
52522
52649
  }
52523
52650
  const handleFailed = (event) => {
52524
- var _a3, _b2;
52525
- const cause = ((_a3 = event == null ? void 0 : event.failure) == null ? void 0 : _a3.message) || ((_b2 = event == null ? void 0 : event.failure) == null ? void 0 : _b2.cause);
52526
- setCallError(cause ? `Chamada nao concluida: ${cause}` : "Chamada nao concluida.");
52651
+ const notice = describeCallFailure(event == null ? void 0 : event.failure);
52652
+ setCallError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
52527
52653
  };
52528
52654
  currentCall.on("failed", handleFailed);
52529
52655
  return () => {
@@ -54430,6 +54556,179 @@ function filterHistoryItems(items, query) {
54430
54556
  }
54431
54557
  return items.filter((item) => itemMatchesHistoryQuery(item, parsed));
54432
54558
  }
54559
+ const EXPORT_STATUS = { pending: 1, inProgress: 2, completed: 3 };
54560
+ const EXPORT_KIND_LABELS = { manual: "Chamadas manuais", campaign: "Chamadas de campanha" };
54561
+ const EXPORT_CONTENT_LABELS = { audio: "Áudios (zip)", data: "Dados (planilha)" };
54562
+ const dayInBrasilia = (date) => new Intl.DateTimeFormat("en-CA", { timeZone: "America/Sao_Paulo", year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
54563
+ function defaultExportRange(now = /* @__PURE__ */ new Date()) {
54564
+ return { from: dayInBrasilia(new Date(now.getTime() - 30 * 24 * 36e5)), to: dayInBrasilia(now) };
54565
+ }
54566
+ function formatFileSize(bytes) {
54567
+ if (!bytes || bytes <= 0) return "";
54568
+ if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
54569
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
54570
+ }
54571
+ function describeExport(summary) {
54572
+ const what = `${summary.kind ? EXPORT_KIND_LABELS[summary.kind] : "Exportação"} · ${summary.content ? EXPORT_CONTENT_LABELS[summary.content].toLowerCase() : ""}`.replace(/ · $/, "");
54573
+ if (summary.status === EXPORT_STATUS.completed) return `${what}: pronta${summary.fileSize ? ` (${formatFileSize(summary.fileSize)})` : ""}`;
54574
+ if (summary.status === EXPORT_STATUS.inProgress) return `${what}: processando${summary.progress ? ` ${summary.progress}%` : ""}`;
54575
+ if (summary.status === EXPORT_STATUS.pending) return `${what}: na fila do Callix`;
54576
+ return `${what}: ${summary.statusLabel}`;
54577
+ }
54578
+ async function readError(response, fallback) {
54579
+ const body = await response.json().catch(() => null);
54580
+ return typeof (body == null ? void 0 : body.error) === "string" && body.error ? body.error : fallback;
54581
+ }
54582
+ async function createCallExport(apiBaseUrl, headers, request) {
54583
+ const response = await fetch(`${apiBaseUrl}/api/call-exports`, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, credentials: "omit", body: JSON.stringify(request) });
54584
+ if (!response.ok) throw new Error(await readError(response, `Não foi possível pedir a exportação (${response.status})`));
54585
+ const body = await response.json();
54586
+ return body.export;
54587
+ }
54588
+ async function listCallExports(apiBaseUrl, headers) {
54589
+ const response = await fetch(`${apiBaseUrl}/api/call-exports`, { method: "GET", headers, credentials: "omit" });
54590
+ if (!response.ok) throw new Error(await readError(response, `Não foi possível listar as exportações (${response.status})`));
54591
+ const body = await response.json();
54592
+ return body.items ?? [];
54593
+ }
54594
+ async function fetchCallExportDownload(apiBaseUrl, headers, id) {
54595
+ const response = await fetch(`${apiBaseUrl}/api/call-exports/${encodeURIComponent(id)}/download`, { method: "GET", headers, credentials: "omit" });
54596
+ const body = await response.json().catch(() => null);
54597
+ if (response.ok && (body == null ? void 0 : body.url) && body.export) return { state: "ready", url: body.url, filename: body.filename ?? null, fileSize: body.fileSize ?? null, export: body.export };
54598
+ if (response.status === 409 && (body == null ? void 0 : body.export)) {
54599
+ return body.export.status <= EXPORT_STATUS.completed ? { state: "pending", export: body.export } : { state: "failed", message: body.error ?? `Exportação ${body.export.statusLabel}`, export: body.export };
54600
+ }
54601
+ return { state: "failed", message: (body == null ? void 0 : body.error) ?? `Não foi possível consultar a exportação (${response.status})`, export: body == null ? void 0 : body.export };
54602
+ }
54603
+ const RECENT_LIMIT = 3;
54604
+ function CallExportPanel({ apiBaseUrl, getAuthHeaders: getAuthHeaders2, onClose, pollIntervalMs = 5e3 }) {
54605
+ const [range] = useState(() => defaultExportRange());
54606
+ const [kind, setKind] = useState("manual");
54607
+ const [content, setContent] = useState("audio");
54608
+ const [from, setFrom] = useState(range.from);
54609
+ const [to, setTo] = useState(range.to);
54610
+ const [submitting, setSubmitting] = useState(false);
54611
+ const [error2, setError] = useState(null);
54612
+ const [job, setJob] = useState(null);
54613
+ const [download, setDownload] = useState(null);
54614
+ const [recent, setRecent] = useState([]);
54615
+ const [opening, setOpening] = useState(null);
54616
+ const mounted = useRef(true);
54617
+ useEffect(() => {
54618
+ mounted.current = true;
54619
+ return () => {
54620
+ mounted.current = false;
54621
+ };
54622
+ }, []);
54623
+ useEffect(() => {
54624
+ let active = true;
54625
+ listCallExports(apiBaseUrl, getAuthHeaders2()).then((items) => {
54626
+ if (active) setRecent(items.slice(0, RECENT_LIMIT));
54627
+ }).catch(() => void 0);
54628
+ return () => {
54629
+ active = false;
54630
+ };
54631
+ }, [apiBaseUrl, getAuthHeaders2]);
54632
+ useEffect(() => {
54633
+ if (!job || (download == null ? void 0 : download.state) === "ready" || (download == null ? void 0 : download.state) === "failed") return;
54634
+ let timer = null;
54635
+ let active = true;
54636
+ const check = async () => {
54637
+ const result2 = await fetchCallExportDownload(apiBaseUrl, getAuthHeaders2(), job.id);
54638
+ if (!active) return;
54639
+ setDownload(result2);
54640
+ if (result2.state === "pending") {
54641
+ setJob(result2.export);
54642
+ timer = setTimeout(check, pollIntervalMs);
54643
+ } else if (result2.state === "ready") {
54644
+ setJob(result2.export);
54645
+ setRecent((items) => [result2.export, ...items.filter((i2) => i2.id !== result2.export.id)].slice(0, RECENT_LIMIT));
54646
+ }
54647
+ };
54648
+ void check();
54649
+ return () => {
54650
+ active = false;
54651
+ if (timer) clearTimeout(timer);
54652
+ };
54653
+ }, [job == null ? void 0 : job.id, apiBaseUrl, getAuthHeaders2, pollIntervalMs]);
54654
+ const submit = useCallback(async () => {
54655
+ setError(null);
54656
+ if (!from || !to) {
54657
+ setError("Informe o período.");
54658
+ return;
54659
+ }
54660
+ if (to < from) {
54661
+ setError("O fim do período vem antes do início.");
54662
+ return;
54663
+ }
54664
+ setSubmitting(true);
54665
+ setDownload(null);
54666
+ try {
54667
+ const created = await createCallExport(apiBaseUrl, getAuthHeaders2(), { kind, content, from, to });
54668
+ if (!mounted.current) return;
54669
+ setJob(created);
54670
+ setRecent((items) => [created, ...items.filter((i2) => i2.id !== created.id)].slice(0, RECENT_LIMIT));
54671
+ } catch (err) {
54672
+ if (mounted.current) setError(err instanceof Error ? err.message : "Não foi possível pedir a exportação.");
54673
+ } finally {
54674
+ if (mounted.current) setSubmitting(false);
54675
+ }
54676
+ }, [apiBaseUrl, getAuthHeaders2, kind, content, from, to]);
54677
+ const openRecent = useCallback(
54678
+ async (item) => {
54679
+ setOpening(item.id);
54680
+ setError(null);
54681
+ try {
54682
+ const result2 = await fetchCallExportDownload(apiBaseUrl, getAuthHeaders2(), item.id);
54683
+ if (!mounted.current) return;
54684
+ if (result2.state === "ready") window.open(result2.url, "_blank", "noopener,noreferrer");
54685
+ else setError(result2.state === "pending" ? describeExport(result2.export) : result2.message);
54686
+ } finally {
54687
+ if (mounted.current) setOpening(null);
54688
+ }
54689
+ },
54690
+ [apiBaseUrl, getAuthHeaders2]
54691
+ );
54692
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "export-panel", role: "region", "aria-label": "Exportar histórico", children: [
54693
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "export-panel-header", children: [
54694
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "export-panel-title", children: "Exportar histórico" }),
54695
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "export-panel-close", onClick: onClose, "aria-label": "Fechar exportação", children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-xmark", "aria-hidden": "true" }) })
54696
+ ] }),
54697
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "export-panel-form", children: [
54698
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "export-panel-field", children: [
54699
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Tipo" }),
54700
+ /* @__PURE__ */ jsxRuntimeExports.jsx("select", { value: kind, onChange: (event) => setKind(event.target.value), "aria-label": "Tipo de chamada", children: Object.keys(EXPORT_KIND_LABELS).map((value) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value, children: EXPORT_KIND_LABELS[value] }, value)) })
54701
+ ] }),
54702
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "export-panel-field", children: [
54703
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Conteúdo" }),
54704
+ /* @__PURE__ */ jsxRuntimeExports.jsx("select", { value: content, onChange: (event) => setContent(event.target.value), "aria-label": "Conteúdo", children: Object.keys(EXPORT_CONTENT_LABELS).map((value) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value, children: EXPORT_CONTENT_LABELS[value] }, value)) })
54705
+ ] }),
54706
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "export-panel-field", children: [
54707
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "De" }),
54708
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "date", value: from, max: to, onChange: (event) => setFrom(event.target.value), "aria-label": "Início do período" })
54709
+ ] }),
54710
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "export-panel-field", children: [
54711
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Até" }),
54712
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "date", value: to, min: from, onChange: (event) => setTo(event.target.value), "aria-label": "Fim do período" })
54713
+ ] }),
54714
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "retry-btn export-panel-submit", onClick: () => void submit(), disabled: submitting, children: submitting ? "Pedindo..." : "Exportar" })
54715
+ ] }),
54716
+ error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "export-panel-error", role: "alert", children: error2 }),
54717
+ job && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "export-panel-job", role: "status", children: [
54718
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: (download == null ? void 0 : download.state) === "failed" ? download.message : describeExport(job) }),
54719
+ (download == null ? void 0 : download.state) === "ready" && /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { className: "export-panel-link", href: download.url, target: "_blank", rel: "noopener noreferrer", children: [
54720
+ /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-download", "aria-hidden": "true" }),
54721
+ " Baixar ",
54722
+ download.filename ?? "arquivo"
54723
+ ] })
54724
+ ] }),
54725
+ recent.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "export-panel-recent", "aria-label": "Exportações recentes", children: recent.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [
54726
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: describeExport(item) }),
54727
+ item.status === EXPORT_STATUS.completed && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "export-panel-recent-btn", onClick: () => void openRecent(item), disabled: opening === item.id, children: opening === item.id ? "..." : "Baixar" })
54728
+ ] }, item.id)) }),
54729
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "export-panel-hint", children: "O Callix prepara o arquivo em alguns minutos; o link de download vale por cerca de uma hora." })
54730
+ ] });
54731
+ }
54433
54732
  const DEFAULT_QUALIFICATION = "chamada sem qualificação";
54434
54733
  const SEARCH_DEBOUNCE_MS = 350;
54435
54734
  const DETAILS_TITLE = "Ver detalhes da chamada";
@@ -54903,6 +55202,7 @@ function CallHistoryList({
54903
55202
  }) {
54904
55203
  const [openPlayerId, setOpenPlayerId] = useState(null);
54905
55204
  const [selectedId, setSelectedId] = useState(null);
55205
+ const [exportOpen, setExportOpen] = useState(false);
54906
55206
  const recordings = useRecordings(apiBaseUrl, getAuthHeaders2);
54907
55207
  const { retain } = recordings;
54908
55208
  const [draft, setDraft] = useState(query ?? "");
@@ -54988,8 +55288,12 @@ function CallHistoryList({
54988
55288
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
54989
55289
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "history-header", children: [
54990
55290
  /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "header-title", children: "Recentes" }),
54991
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "history-refresh-btn", onClick: onRefresh, disabled: isLoading, "aria-label": "Atualizar histórico", children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: `fas fa-arrows-rotate ${isLoading ? "fa-spin" : ""}`, "aria-hidden": "true" }) })
55291
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "history-header-actions", children: [
55292
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: `history-refresh-btn history-export-btn ${exportOpen ? "active" : ""}`, onClick: () => setExportOpen((open) => !open), "aria-label": "Exportar histórico", "aria-pressed": exportOpen, title: "Exportar áudios ou dados do período", children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-file-export", "aria-hidden": "true" }) }),
55293
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "history-refresh-btn", onClick: onRefresh, disabled: isLoading, "aria-label": "Atualizar histórico", children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: `fas fa-arrows-rotate ${isLoading ? "fa-spin" : ""}`, "aria-hidden": "true" }) })
55294
+ ] })
54992
55295
  ] }),
55296
+ exportOpen && /* @__PURE__ */ jsxRuntimeExports.jsx(CallExportPanel, { apiBaseUrl, getAuthHeaders: getAuthHeaders2, onClose: () => setExportOpen(false) }),
54993
55297
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "history-search-bar", children: [
54994
55298
  /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "history-search", children: [
54995
55299
  /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-magnifying-glass history-search-icon", "aria-hidden": "true" }),
@@ -56500,6 +56804,122 @@ function DebugPanel({ onClose }) {
56500
56804
  showCopySuccess && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: styles.copySuccess, children: "✅ Logs copiados para a área de transferência!" })
56501
56805
  ] });
56502
56806
  }
56807
+ function DialerNoticeBanner({ notice, onDismiss, onRecheck, onHeightChange }) {
56808
+ const ref = useRef(null);
56809
+ const visible = notice !== null;
56810
+ useLayoutEffect(() => {
56811
+ const el = ref.current;
56812
+ if (!onHeightChange) return;
56813
+ if (!el) {
56814
+ onHeightChange(0);
56815
+ return;
56816
+ }
56817
+ const report = () => onHeightChange(el.offsetHeight);
56818
+ report();
56819
+ if (typeof ResizeObserver === "undefined") return;
56820
+ const observer = new ResizeObserver(report);
56821
+ observer.observe(el);
56822
+ return () => {
56823
+ observer.disconnect();
56824
+ onHeightChange(0);
56825
+ };
56826
+ }, [visible, onHeightChange]);
56827
+ if (!notice) return null;
56828
+ const dismissible = notice.source === "call" || notice.source === "session";
56829
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref, className: `dialer-notice ${notice.level}`, role: notice.level === "error" ? "alert" : "status", "data-code": notice.code, children: [
56830
+ /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: `fas ${notice.level === "error" ? "fa-circle-exclamation" : "fa-triangle-exclamation"} dialer-notice-icon`, "aria-hidden": "true" }),
56831
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "dialer-notice-body", children: [
56832
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "dialer-notice-title", children: notice.title }),
56833
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "dialer-notice-detail", children: notice.detail }),
56834
+ notice.action && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "dialer-notice-action", children: notice.action }),
56835
+ notice.source === "device" && onRecheck && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "dialer-notice-btn", onClick: onRecheck, children: "Verificar de novo" })
56836
+ ] }),
56837
+ dismissible && onDismiss && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "dialer-notice-close", onClick: onDismiss, "aria-label": "Fechar aviso", children: /* @__PURE__ */ jsxRuntimeExports.jsx("i", { className: "fas fa-xmark", "aria-hidden": "true" }) })
56838
+ ] });
56839
+ }
56840
+ function useDialerNotices(options = {}) {
56841
+ const operatorState = clientSdkReactExports.useCallOperatorState();
56842
+ const currentCall = clientSdkReactExports.useCallOperatorCurrentCall();
56843
+ const [deviceIssue, setDeviceIssue] = useState(null);
56844
+ const [sessionDrop, setSessionDrop] = useState(null);
56845
+ const [callFailure, setCallFailure] = useState(null);
56846
+ const [dismissedCode, setDismissedCode] = useState(null);
56847
+ const failureTimer = useRef(null);
56848
+ const ttl = options.callFailureTtlMs ?? 2e4;
56849
+ const recheckDevices = useCallback(async () => {
56850
+ if (options.checkDevices === false) return;
56851
+ const issue = await checkAudioDevices();
56852
+ setDeviceIssue(issue);
56853
+ }, [options.checkDevices]);
56854
+ useEffect(() => {
56855
+ var _a2;
56856
+ if (options.checkDevices === false) return;
56857
+ let cancelled = false;
56858
+ const run = async () => {
56859
+ const issue = await checkAudioDevices();
56860
+ if (!cancelled) setDeviceIssue(issue);
56861
+ };
56862
+ void run();
56863
+ const mediaDevices = typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
56864
+ const handler = () => void run();
56865
+ (_a2 = mediaDevices == null ? void 0 : mediaDevices.addEventListener) == null ? void 0 : _a2.call(mediaDevices, "devicechange", handler);
56866
+ return () => {
56867
+ var _a3;
56868
+ cancelled = true;
56869
+ (_a3 = mediaDevices == null ? void 0 : mediaDevices.removeEventListener) == null ? void 0 : _a3.call(mediaDevices, "devicechange", handler);
56870
+ };
56871
+ }, [options.checkDevices]);
56872
+ clientSdkReactExports.useCallOperatorEventListener("userSessionDropped", (data) => {
56873
+ setSessionDrop((data == null ? void 0 : data.originator) ?? "unknown");
56874
+ });
56875
+ useEffect(() => {
56876
+ if (!(currentCall == null ? void 0 : currentCall.on)) return;
56877
+ const handleFailed = (event) => {
56878
+ var _a2;
56879
+ const failure = (event == null ? void 0 : event.failure) ?? ((_a2 = currentCall.toCallData) == null ? void 0 : _a2.call(currentCall).failure) ?? null;
56880
+ setCallFailure(failure ?? { cause: null });
56881
+ setDismissedCode(null);
56882
+ if (failureTimer.current) clearTimeout(failureTimer.current);
56883
+ failureTimer.current = setTimeout(() => setCallFailure(null), ttl);
56884
+ };
56885
+ currentCall.on("failed", handleFailed);
56886
+ return () => {
56887
+ var _a2;
56888
+ (_a2 = currentCall.off) == null ? void 0 : _a2.call(currentCall, "failed", handleFailed);
56889
+ };
56890
+ }, [currentCall, ttl]);
56891
+ useEffect(() => {
56892
+ var _a2, _b;
56893
+ if ((operatorState == null ? void 0 : operatorState.state) !== "manualCallFailed") return;
56894
+ const failure = ((_b = (_a2 = operatorState.call) == null ? void 0 : _a2.toCallData) == null ? void 0 : _b.call(_a2).failure) ?? null;
56895
+ setCallFailure((current) => current ?? failure ?? { cause: null });
56896
+ setDismissedCode(null);
56897
+ }, [operatorState]);
56898
+ useEffect(() => {
56899
+ if ((operatorState == null ? void 0 : operatorState.state) === "callInProgress" || (operatorState == null ? void 0 : operatorState.state) === "manualCallRinging" || (operatorState == null ? void 0 : operatorState.state) === "callRinging") {
56900
+ setCallFailure(null);
56901
+ }
56902
+ if ((operatorState == null ? void 0 : operatorState.state) === "idle" || (operatorState == null ? void 0 : operatorState.state) === "onBreak") setSessionDrop(null);
56903
+ }, [operatorState == null ? void 0 : operatorState.state]);
56904
+ useEffect(() => () => {
56905
+ if (failureTimer.current) clearTimeout(failureTimer.current);
56906
+ }, []);
56907
+ const notice = pickNotice({
56908
+ deviceIssue,
56909
+ operatorState: (operatorState == null ? void 0 : operatorState.state) ?? null,
56910
+ startupError: (operatorState == null ? void 0 : operatorState.state) === "startupError" ? operatorState.error ?? null : null,
56911
+ sessionDrop,
56912
+ callFailure
56913
+ });
56914
+ const visible = notice && notice.code !== dismissedCode ? notice : null;
56915
+ const dismiss = useCallback(() => {
56916
+ if (!notice) return;
56917
+ if (notice.source === "call") setCallFailure(null);
56918
+ else if (notice.source === "session") setSessionDrop(null);
56919
+ else setDismissedCode(notice.code);
56920
+ }, [notice]);
56921
+ return { notice: visible, deviceIssue, dismiss, recheckDevices };
56922
+ }
56503
56923
  const VIEWPORT_MARGIN$1 = 16;
56504
56924
  const BASE_WIDTH = 520;
56505
56925
  const FALLBACK_HEIGHT = 600;
@@ -57562,7 +57982,9 @@ function AuthenticatedMobileDialer({
57562
57982
  };
57563
57983
  }, [campaignFormKey]);
57564
57984
  const callStateStr = callState;
57565
- const isError = callStateStr === "startup.error" || callStateStr === "offline";
57985
+ const { notice: dialerNotice, dismiss: dismissDialerNotice, recheckDevices } = useDialerNotices();
57986
+ const [noticeHeight, setNoticeHeight] = useState(0);
57987
+ const isError = callStateStr === "startupError" || callStateStr === "offline" || (dialerNotice == null ? void 0 : dialerNotice.source) === "device" || (dialerNotice == null ? void 0 : dialerNotice.source) === "session";
57566
57988
  const isIdle = !callStateStr || callStateStr === "starting" || callStateStr === "idle" || callStateStr === "manualCallSetup" || callStateStr === "onBreak";
57567
57989
  const isConnecting = callState === "manualCallSetup" || callState === "manualCallRinging" || callState === "callRinging";
57568
57990
  const isInCall = callState === "callInProgress";
@@ -58008,20 +58430,21 @@ function AuthenticatedMobileDialer({
58008
58430
  if (isConnecting) return "Ligando...";
58009
58431
  if (isInCall) return "Em chamada";
58010
58432
  if (isAfterCall) return "Encerrando...";
58011
- if (isError) return "Erro de conexão";
58433
+ if (callStateStr === "manualCallFailed") return "Chamada não concluída";
58434
+ if (isError) return (dialerNotice == null ? void 0 : dialerNotice.title) ?? "Erro de conexão";
58012
58435
  if (isOnBreak) return "Em pausa";
58013
58436
  return "Disponível";
58014
- }, [isAfterCall, isConnecting, isError, isInCall, isOnBreak]);
58437
+ }, [callStateStr, dialerNotice, isAfterCall, isConnecting, isError, isInCall, isOnBreak]);
58015
58438
  const statusBadgeClass = useMemo(() => {
58016
58439
  if (isError) return "error";
58017
58440
  if (isOnBreak) return "paused";
58018
58441
  return "available";
58019
58442
  }, [isError, isOnBreak]);
58020
58443
  const statusBadgeText = useMemo(() => {
58021
- if (isError) return "Erro";
58444
+ if (isError) return (dialerNotice == null ? void 0 : dialerNotice.source) === "device" ? dialerNotice.title : "Erro";
58022
58445
  if (isOnBreak) return currentBreakName || "Em pausa";
58023
58446
  return "Disponível";
58024
- }, [isError, isOnBreak, currentBreakName]);
58447
+ }, [dialerNotice, isError, isOnBreak, currentBreakName]);
58025
58448
  const pauseDurationLabel = useMemo(() => {
58026
58449
  const hours = Math.floor(pauseDurationSeconds / 3600);
58027
58450
  const minutes = Math.floor(pauseDurationSeconds % 3600 / 60).toString().padStart(2, "0");
@@ -58057,7 +58480,7 @@ function AuthenticatedMobileDialer({
58057
58480
  addLog("info", "Debug", "🔒 Painel de debug fechado");
58058
58481
  } });
58059
58482
  }
58060
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "phone-container", "data-density": density === "compact" ? "compact" : "comfortable", children: [
58483
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "phone-container", "data-density": density === "compact" ? "compact" : "comfortable", style: { "--notice-h": `${noticeHeight}px` }, children: [
58061
58484
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `screen dialer-screen ${activeScreen === "dialer" ? "active" : ""}`, id: "dialerScreen", children: [
58062
58485
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "dialer-header", children: [
58063
58486
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "status-container", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: `status-badge ${statusBadgeClass}`, id: "statusBadge", children: [
@@ -58075,6 +58498,7 @@ function AuthenticatedMobileDialer({
58075
58498
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Pausa" })
58076
58499
  ] }) })
58077
58500
  ] }),
58501
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialerNoticeBanner, { notice: dialerNotice, onDismiss: dismissDialerNotice, onRecheck: () => void recheckDevices(), onHeightChange: setNoticeHeight }),
58078
58502
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "phone-number-display", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
58079
58503
  PhoneNumberInput,
58080
58504
  {
@@ -59129,7 +59553,7 @@ function ClickToCallAgent({ enabled = true, pollIntervalMs, sessionToken, onTask
59129
59553
  }, [enabled, pollIntervalMs, sessionToken]);
59130
59554
  return null;
59131
59555
  }
59132
- const WIDGET_VERSION = "2.4.0";
59556
+ const WIDGET_VERSION = "2.5.1";
59133
59557
  let currentSipWebSocket = null;
59134
59558
  function sendSipUnregisterIfPossible(ws) {
59135
59559
  if (ws.readyState !== WebSocket.OPEN) return;