Przeglądaj źródła

fix: zera temperature do avaliador de atendimentos para eliminar ruído entre execuções

leonardo 1 miesiąc temu
rodzic
commit
e56c95fee4

+ 48 - 1
src/api/atendimentos.js

@@ -1,4 +1,4 @@
-import { apiFetch } from "./client.js";
+import { apiFetch, apiBaseUrl, getAccessToken, notifyAuthError } from "./client.js";
 
 export function sincronizarAtendimentos(params = {}) {
   return apiFetch("/api/atendimentos/sync", { method: "POST", body: params });
@@ -57,3 +57,50 @@ export function estatisticasAvaliacoes({ setor, resolvido, sentimento, statusCan
 export function listarSetoresAvaliacoes() {
   return apiFetch("/api/atendimentos/avaliacoes/setores");
 }
+
+// não usa apiFetch: a resposta é um PDF (blob), não JSON/texto
+export async function baixarRelatorioNaoResolvidos({ setor, sentimento, busca, ordenacao, dataInicio, dataFim } = {}) {
+  const query = new URLSearchParams();
+  if (setor) query.set("setor", setor);
+  if (sentimento) query.set("sentimento", sentimento);
+  if (busca) query.set("busca", busca);
+  if (ordenacao === "asc") query.set("ordenacao", ordenacao);
+  if (dataInicio) query.set("dataInicio", dataInicio);
+  if (dataFim) query.set("dataFim", dataFim);
+
+  const token = getAccessToken();
+  const headers = {};
+  if (token) headers["Authorization"] = `Bearer ${token}`;
+
+  let res;
+  try {
+    res = await fetch(`${apiBaseUrl}/api/atendimentos/avaliacoes/relatorio?${query.toString()}`, { headers });
+  } catch {
+    throw new Error("network_error");
+  }
+
+  if (res.status === 401) {
+    notifyAuthError();
+    throw new Error("session_expired");
+  }
+
+  if (!res.ok) {
+    const text = await res.text().catch(() => "");
+    let payload = null;
+    try { payload = JSON.parse(text); } catch {}
+    if (payload?.error === "relatorio_muito_grande") {
+      throw new Error(`Muitos atendimentos para gerar o relatório (limite: ${payload.limite}). ${payload.dica ?? "Estreite o período ou os filtros."}`);
+    }
+    throw new Error(payload?.error || text || `http_error:${res.status}`);
+  }
+
+  const blob = await res.blob();
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = `atendimentos-nao-resolvidos-${new Date().toISOString().slice(0, 10)}.pdf`;
+  document.body.appendChild(a);
+  a.click();
+  a.remove();
+  URL.revokeObjectURL(url);
+}

+ 36 - 1
src/views/atendimentos/NaoResolvidosView.vue

@@ -5,7 +5,8 @@ import LayoutSistema from "../../layout/LayoutSistema.vue";
 import BaseBadge from "../../components/base/BaseBadge.vue";
 import BaseButton from "../../components/base/BaseButton.vue";
 import BaseDropdown from "../../components/base/BaseDropdown.vue";
-import { estatisticasAvaliacoes, listarAvaliacoes, listarSetoresAvaliacoes } from "../../api/atendimentos.js";
+import { baixarRelatorioNaoResolvidos, estatisticasAvaliacoes, listarAvaliacoes, listarSetoresAvaliacoes } from "../../api/atendimentos.js";
+import { useToast } from "../../composables/useToast.js";
 import {
   cancelamentoLabel,
   cancelamentoVariant,
@@ -22,6 +23,7 @@ const opcoesSentimento = Object.entries(sentimentoLabel).map(([value, label]) =>
 
 const route = useRoute();
 const router = useRouter();
+const toast = useToast();
 
 function formatISO(date) {
   return date.toISOString().slice(0, 10);
@@ -35,6 +37,7 @@ function padraoDataInicio() {
 
 const carregando = ref(false);
 const erro = ref("");
+const gerandoRelatorio = ref(false);
 
 const stats = ref(null);
 const statsGeral = ref(null);
@@ -195,6 +198,25 @@ function verConversa(id) {
   router.push({ name: "atendimento", params: { id } });
 }
 
+async function baixarRelatorio() {
+  if (gerandoRelatorio.value) return;
+  gerandoRelatorio.value = true;
+  try {
+    await baixarRelatorioNaoResolvidos({
+      setor: filtroSetor.value || undefined,
+      sentimento: filtroSentimento.value || undefined,
+      busca: filtroBusca.value.trim() || undefined,
+      ordenacao: filtroOrdenacao.value,
+      dataInicio: filtroDataInicio.value || undefined,
+      dataFim: filtroDataFim.value || undefined
+    });
+  } catch (err) {
+    toast.erro(err?.message ?? "Falha ao gerar o relatório");
+  } finally {
+    gerandoRelatorio.value = false;
+  }
+}
+
 onMounted(async () => {
   carregarStats();
   carregarSetores();
@@ -346,6 +368,19 @@ onBeforeUnmount(() => {
                 <path v-else stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
               </svg>
             </BaseButton>
+            <BaseButton
+              variant="secondary"
+              size="sm"
+              :loading="gerandoRelatorio"
+              :disabled="total === 0"
+              title="Baixar PDF com os atendimentos filtrados"
+              @click="baixarRelatorio"
+            >
+              Baixar relatório
+              <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v12m0 0-4-4m4 4 4-4M4 20h16" />
+              </svg>
+            </BaseButton>
             <div class="ml-auto text-xs text-gray-500 dark:text-gray-400">
               {{ total }} não resolvidos
             </div>