leonardo 1 месяц назад
Родитель
Сommit
e844c6ddc8

+ 21 - 11
src/api/client.js

@@ -66,26 +66,36 @@ export async function apiFetch(path, { method = "GET", body } = {}) {
   const headers = { "content-type": "application/json" };
   if (_accessToken) headers["Authorization"] = `Bearer ${_accessToken}`;
 
-  let res = await fetch(`${apiBaseUrl}${path}`, {
-    method,
-    headers,
-    body: body ? JSON.stringify(body) : undefined
-  });
+  let res;
+  try {
+    res = await fetch(`${apiBaseUrl}${path}`, {
+      method,
+      headers,
+      body: body ? JSON.stringify(body) : undefined
+    });
+  } catch {
+    throw new ApiError("network", "network_error");
+  }
 
   if (res.status === 401 && _refreshFn) {
+    let newToken;
     try {
-      const newToken = await refreshAccessToken();
-      if (newToken) {
-        headers["Authorization"] = `Bearer ${newToken}`;
+      newToken = await refreshAccessToken();
+    } catch {
+      _authErrorFn?.();
+      throw new Error("session_expired");
+    }
+    if (newToken) {
+      headers["Authorization"] = `Bearer ${newToken}`;
+      try {
         res = await fetch(`${apiBaseUrl}${path}`, {
           method,
           headers,
           body: body ? JSON.stringify(body) : undefined
         });
+      } catch {
+        throw new ApiError("network", "network_error");
       }
-    } catch {
-      _authErrorFn?.();
-      throw new Error("session_expired");
     }
   }
 

+ 5 - 0
src/components/ChatWindow.vue

@@ -163,6 +163,11 @@ const chatEl = ref(null);
 const copiado = ref(null);
 let copiadoTimeout = null;
 
+watch(() => props.messages, () => {
+  clearTimeout(copiadoTimeout);
+  copiado.value = null;
+});
+
 const isStreaming = computed(() => props.messages.some((m) => m.streaming && m.content));
 const streamingStatusMsg = computed(() => {
   const m = [...props.messages].reverse().find((m) => m.streaming);

+ 7 - 1
src/components/ModalUsuario.vue

@@ -40,7 +40,7 @@
 
           <div v-if="!usuario" class="grid gap-1.5">
             <label for="mu-senha" class="text-sm font-medium text-gray-700 dark:text-gray-300">Senha</label>
-            <input id="mu-senha" v-model="form.Senha" type="password" placeholder="Mínimo 6 caracteres" required :disabled="salvando" autocomplete="new-password" />
+            <input id="mu-senha" v-model="form.Senha" type="password" placeholder="Mínimo 6 caracteres" required minlength="6" :disabled="salvando" autocomplete="new-password" />
           </div>
 
           <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
@@ -123,6 +123,12 @@ watch(
 
 async function onSubmit() {
   erroForm.value = "";
+
+  if (!props.usuario && form.value.Senha.length < 6) {
+    erroForm.value = "A senha deve ter no mínimo 6 caracteres.";
+    return;
+  }
+
   salvando.value = true;
   try {
     if (props.usuario) {

+ 4 - 1
src/composables/useAuth.js

@@ -2,6 +2,7 @@ import { computed, readonly, ref } from "vue";
 import { loginRequest, logoutRequest, refreshTokenRequest } from "../api/auth.js";
 import { setAccessToken, setRefreshCallback, setAuthErrorCallback } from "../api/client.js";
 import { resetChatSingleton } from "./useChat.js";
+import { resetConversasSingleton } from "./useConversas.js";
 
 const LOCAL_STORAGE_KEY = "auth.session";
 const SESSION_STORAGE_KEY = "auth.session.temp";
@@ -99,12 +100,13 @@ function setupAuth() {
   setAuthErrorCallback(() => {
     clearTimeout(_proactiveRefreshTimer);
     resetChatSingleton();
+    resetConversasSingleton();
     session.value = null;
     persistSession(null);
     setAccessToken(null);
     setRefreshCallback(null);
     setAuthErrorCallback(null);
-    window.location.href = "/login";
+    window.location.href = "/login?redirect=" + encodeURIComponent(window.location.pathname + window.location.search);
   });
 }
 
@@ -131,6 +133,7 @@ async function logout() {
     await logoutRequest({ refreshToken: rt });
   } finally {
     resetChatSingleton();
+    resetConversasSingleton();
     session.value = null;
     persistSession(null);
     setAccessToken(null);

+ 8 - 0
src/composables/useConversas.js

@@ -5,6 +5,14 @@ let singleton;
 
 const POLL_INTERVAL_MS = 3000;
 
+export function resetConversasSingleton() {
+  if (singleton) {
+    singleton.fecharConversa();
+    singleton.conversas.value = [];
+  }
+  singleton = undefined;
+}
+
 export function useConversas() {
   if (singleton) return singleton;
 

+ 0 - 3
src/layout/LayoutSistema.vue

@@ -1,7 +1,6 @@
 <script setup>
 import HeaderSistema from "./HeaderSistema.vue";
 import SidebarSistema from "./SidebarSistema.vue";
-import ModalDocumentos from "./ModalDocumentos.vue";
 import { useSidebar } from "../composables/useSidebar.js";
 
 const { layoutStyle } = useSidebar();
@@ -26,8 +25,6 @@ const { layoutStyle } = useSidebar();
 
       <SidebarSistema />
 
-      <ModalDocumentos />
-
       <main
         class="col-start-2 row-start-2 relative z-10 flex min-h-0 min-w-0 flex-col overflow-hidden p-0 max-[980px]:col-start-1 max-[980px]:row-start-2"
       >

+ 5 - 2
src/views/atendimentos/AtendimentoView.vue

@@ -98,11 +98,14 @@ async function carregar(id) {
   erro.value = "";
   atendimento.value = null;
   try {
-    atendimento.value = await obterAtendimento(id);
+    const resultado = await obterAtendimento(id);
+    if (String(route.params.id) !== String(id)) return;
+    atendimento.value = resultado;
   } catch (err) {
+    if (String(route.params.id) !== String(id)) return;
     erro.value = err?.message ?? "Falha ao carregar o atendimento";
   } finally {
-    carregando.value = false;
+    if (String(route.params.id) === String(id)) carregando.value = false;
   }
 }
 

+ 29 - 9
src/views/atendimentos/AvaliacoesView.vue

@@ -63,6 +63,14 @@ const progresso = computed(() => {
   return { total: totalAt, feitos, pct: Math.round((feitos / totalAt) * 100) };
 });
 
+
+const resolvidoParaApi = computed(() =>
+  ["sim", "parcial", "nao", "indefinido"].includes(filtroResolvido.value) ? filtroResolvido.value : undefined
+);
+const statusCancelamentoParaApi = computed(() =>
+  filtroResolvido.value === "cancelou" || filtroResolvido.value === "ameacou" ? filtroResolvido.value : undefined
+);
+
 function formatData(value) {
   if (!value) return "";
   const d = new Date(value);
@@ -75,16 +83,22 @@ function pct(parte, todo) {
   return `${Math.round((parte / todo) * 100)}%`;
 }
 
+let statsRequestId = 0;
+
 async function carregarStats() {
+  const requestId = ++statsRequestId;
   try {
-    stats.value = await estatisticasAvaliacoes({
+    const resultado = await estatisticasAvaliacoes({
       setor: filtroSetor.value || undefined,
-      resolvido: filtroResolvido.value || undefined,
-      sentimento: filtroSentimento.value === "cancelados" ? undefined : filtroSentimento.value || undefined,
-      statusCancelamento: filtroSentimento.value === "cancelados" ? "cancelou" : undefined,
+      resolvido: resolvidoParaApi.value,
+      sentimento: filtroSentimento.value || undefined,
+      statusCancelamento: statusCancelamentoParaApi.value,
       busca: filtroBusca.value.trim() || undefined
     });
+    if (requestId !== statsRequestId) return;
+    stats.value = resultado;
   } catch {
+    if (requestId !== statsRequestId) return;
     stats.value = null;
   }
 }
@@ -98,7 +112,10 @@ async function carregarSetores() {
   }
 }
 
+let listaRequestId = 0;
+
 async function carregarLista() {
+  const requestId = ++listaRequestId;
   carregando.value = true;
   erro.value = "";
   try {
@@ -106,18 +123,20 @@ async function carregarLista() {
       page: page.value,
       pageSize,
       setor: filtroSetor.value || undefined,
-      resolvido: filtroResolvido.value || undefined,
-      sentimento: filtroSentimento.value === "cancelados" ? undefined : filtroSentimento.value || undefined,
-      statusCancelamento: filtroSentimento.value === "cancelados" ? "cancelou" : undefined,
+      resolvido: resolvidoParaApi.value,
+      sentimento: filtroSentimento.value || undefined,
+      statusCancelamento: statusCancelamentoParaApi.value,
       busca: filtroBusca.value.trim() || undefined,
       ordenacao: filtroOrdenacao.value
     });
+    if (requestId !== listaRequestId) return;
     avaliacoes.value = r.results ?? [];
     total.value = r.total ?? 0;
   } catch (err) {
+    if (requestId !== listaRequestId) return;
     erro.value = err?.message ?? "Falha ao carregar avaliações";
   } finally {
-    carregando.value = false;
+    if (requestId === listaRequestId) carregando.value = false;
   }
 }
 
@@ -357,6 +376,8 @@ onBeforeUnmount(() => {
               <option value="parcial">Parcial</option>
               <option value="nao">Não resolvido</option>
               <option value="indefinido">Indefinido</option>
+              <option value="cancelou">Cancelou</option>
+              <option value="ameacou">Quer cancelar</option>
             </select>
             <select
               v-model="filtroSentimento"
@@ -368,7 +389,6 @@ onBeforeUnmount(() => {
               <option value="neutro">Neutro</option>
               <option value="negativo">Negativo</option>
               <option value="indefinido">Indefinido</option>
-              <option value="cancelados">Cancelados</option>
             </select>
             <BaseButton
               variant="secondary"