leonardo 1 månad sedan
förälder
incheckning
1ef1786bab

+ 17 - 5
src/api/atendimentos.js

@@ -1,4 +1,4 @@
-import { apiFetch, apiBaseUrl, getAccessToken, notifyAuthError } from "./client.js";
+import { apiFetch, apiBaseUrl, buildAuthHeaders, refreshAccessToken, notifyAuthError } from "./client.js";
 
 export function sincronizarAtendimentos(params = {}) {
   return apiFetch("/api/atendimentos/sync", { method: "POST", body: params });
@@ -68,17 +68,29 @@ export async function baixarRelatorioNaoResolvidos({ setor, sentimento, busca, o
   if (dataInicio) query.set("dataInicio", dataInicio);
   if (dataFim) query.set("dataFim", dataFim);
 
-  const token = getAccessToken();
-  const headers = {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
+  const reqUrl = `${apiBaseUrl}/api/atendimentos/avaliacoes/relatorio?${query.toString()}`;
 
   let res;
   try {
-    res = await fetch(`${apiBaseUrl}/api/atendimentos/avaliacoes/relatorio?${query.toString()}`, { headers });
+    res = await fetch(reqUrl, { headers: buildAuthHeaders() });
   } catch {
     throw new Error("network_error");
   }
 
+  if (res.status === 401) {
+    try {
+      await refreshAccessToken();
+    } catch {
+      notifyAuthError();
+      throw new Error("session_expired");
+    }
+    try {
+      res = await fetch(reqUrl, { headers: buildAuthHeaders() });
+    } catch {
+      throw new Error("network_error");
+    }
+  }
+
   if (res.status === 401) {
     notifyAuthError();
     throw new Error("session_expired");

+ 6 - 6
src/components/DocumentList.vue

@@ -14,7 +14,7 @@ const props = defineProps({
 const emit = defineEmits(["refresh"]);
 const toast = useToast();
 
-const deleting = ref(null);
+const deleting = ref(new Set());
 const confirmingSource = ref(null);
 const filterQuery = ref("");
 
@@ -40,8 +40,8 @@ const totalChunks = computed(() => props.items.length);
 const totalDocs = computed(() => Object.keys(grouped.value).length);
 
 async function onConfirmDelete(source) {
-  if (!source) return;
-  deleting.value = source;
+  if (!source || deleting.value.has(source)) return;
+  deleting.value.add(source);
   try {
     await deleteDocumentsBySource(source);
     confirmingSource.value = null;
@@ -50,7 +50,7 @@ async function onConfirmDelete(source) {
   } catch (e) {
     toast.erro(`Erro ao excluir: ${e?.message || "erro desconhecido"}`);
   } finally {
-    deleting.value = null;
+    deleting.value.delete(source);
   }
 }
 
@@ -207,8 +207,8 @@ function fileType(source) {
             <BaseButton
               variant="danger"
               size="sm"
-              :loading="deleting === source"
-              :disabled="deleting === source"
+              :loading="deleting.has(source)"
+              :disabled="deleting.has(source)"
               @click="onConfirmDelete(source)"
             >
               Confirmar

+ 4 - 5
src/composables/useChat.js

@@ -231,15 +231,15 @@ export function useChat() {
 
     if (_streamAbort) {
       _streamAbort.abort();
-      // o abort não dispara onDone/onError (ver sendChatStream) — bolha vazia do
-      // turno interrompido é removida; bolha com conteúdo parcial é congelada
-      // (streaming:false) em vez de ficar com o cursor piscando para sempre
+     
       messages.value = messages.value
         .filter((m) => !(m.role === "assistant" && m.streaming && !m.content))
         .map((m) => (m.role === "assistant" && m.streaming ? { ...m, streaming: false } : m));
     }
     const abortController = new AbortController();
     _streamAbort = abortController;
+     
+    loading.value = true;
 
     let convId = activeConversationId.value ? Number(activeConversationId.value) : null;
 
@@ -248,6 +248,7 @@ export function useChat() {
         const title = trimmed.slice(0, 60);
         const conv = await createConversation(title);
         if (_streamAbort !== abortController) {
+          toast.erro("Você enviou outra pergunta antes desta ser processada; esta pergunta não foi enviada.");
           return;
         }
         convId = conv.id;
@@ -273,8 +274,6 @@ export function useChat() {
 
     const liveMsg = messages.value[messages.value.length - 1];
 
-    loading.value = true;
-
     try {
       await sendChatStream(trimmed, {
         conversationId: convId,

+ 6 - 1
src/composables/useConversas.js

@@ -34,6 +34,7 @@ export function useConversas() {
   let pollTimer = null;
   let ultimoTimestamp = null;
   let buscandoNovasMensagens = false;
+  let conversasEpoch = 0;
   const midiaEmResolucao = new Set();
 
   function limparMidiaCache() {
@@ -60,15 +61,18 @@ export function useConversas() {
   }
 
   async function carregarConversas() {
+    const epoch = ++conversasEpoch;
     conversasLoading.value = true;
     conversasError.value = "";
     try {
       const { results } = await listarConversas();
+      if (epoch !== conversasEpoch) return;
       conversas.value = results;
     } catch (err) {
+      if (epoch !== conversasEpoch) return;
       conversasError.value = err?.message ?? "Falha ao carregar conversas";
     } finally {
-      conversasLoading.value = false;
+      if (epoch === conversasEpoch) conversasLoading.value = false;
     }
   }
 
@@ -178,6 +182,7 @@ export function useConversas() {
 
   async function excluirConversa(id) {
     await excluirConversaApi(id);
+    conversasEpoch++; 
     conversas.value = conversas.value.filter((c) => c.Id !== id);
     if (conversaAtivaId.value === id) fecharConversa();
   }

+ 35 - 3
src/views/conversas/ConversasHistoricoView.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import { useRouter } from "vue-router";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
 import { useChat } from "../../composables/useChat.js";
@@ -11,6 +11,8 @@ const items = computed(() =>
   (conversations.value ?? []).slice().sort((a, b) => Number(b.updatedAt ?? 0) - Number(a.updatedAt ?? 0))
 );
 
+const confirmandoExclusaoId = ref(null);
+
 function formatarData(ts) {
   try {
     return new Date(Number(ts ?? 0)).toLocaleString("pt-BR", {
@@ -27,8 +29,17 @@ function abrirConversa(id) {
   router.push({ name: "home" });
 }
 
-function excluirConversa(id) {
+function pedirExclusao(id) {
+  confirmandoExclusaoId.value = id;
+}
+
+function cancelarExclusao() {
+  confirmandoExclusaoId.value = null;
+}
+
+function confirmarExclusao(id) {
   deleteConversation(id);
+  confirmandoExclusaoId.value = null;
 }
 </script>
 
@@ -78,11 +89,12 @@ function excluirConversa(id) {
           </button>
 
           <button
+            v-if="confirmandoExclusaoId !== c.id"
             type="button"
             class="rounded-xl border border-gray-200 bg-background/60 px-3 py-2 text-gray-400 transition-colors hover:border-red-300 hover:bg-red-50 hover:text-red-600 dark:border-gray-700 dark:bg-background/10 dark:text-gray-500 dark:hover:border-red-500/40 dark:hover:bg-red-500/10 dark:hover:text-red-400"
             title="Excluir conversa"
             aria-label="Excluir conversa"
-            @click="excluirConversa(c.id)"
+            @click="pedirExclusao(c.id)"
           >
             <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
               <path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18" />
@@ -90,6 +102,26 @@ function excluirConversa(id) {
               <path stroke-linecap="round" stroke-linejoin="round" d="M9 6V4h6v2" />
             </svg>
           </button>
+          <div
+            v-else
+            class="flex items-center gap-1.5 rounded-xl border border-red-300 bg-red-50 px-2 dark:border-red-500/40 dark:bg-red-500/10"
+          >
+            <span class="text-xs text-red-600 dark:text-red-400">Excluir?</span>
+            <button
+              type="button"
+              class="rounded-lg bg-red-600 px-2 py-1 text-xs font-medium text-white"
+              @click="confirmarExclusao(c.id)"
+            >
+              Sim
+            </button>
+            <button
+              type="button"
+              class="rounded-lg border border-gray-300 px-2 py-1 text-xs font-medium text-gray-700 dark:border-gray-600 dark:text-gray-300"
+              @click="cancelarExclusao"
+            >
+              Não
+            </button>
+          </div>
         </div>
       </div>
     </section>