فهرست منبع

ajustes documentação

leonardo 3 ماه پیش
والد
کامیت
839461a905

+ 35 - 19
src/api/chat.js

@@ -67,29 +67,45 @@ export async function ingestDocuments(documents) {
   });
 }
 
-export async function ingestFile(file, { source } = {}) {
-  const form = new FormData();
-  form.append("file", file);
-  if (source) form.append("source", source);
+export function ingestFile(file, { source, onProgress } = {}) {
+  return new Promise((resolve, reject) => {
+    const form = new FormData();
+    form.append("file", file);
+    if (source) form.append("source", source);
 
-  const headers = {};
-  const token = getAccessToken();
-  if (token) headers["Authorization"] = `Bearer ${token}`;
+    const xhr = new XMLHttpRequest();
 
-  const res = await fetch(`${apiBaseUrl}/api/ingest/file`, {
-    method: "POST",
-    headers,
-    body: form
-  });
+    xhr.upload.onprogress = (e) => {
+      if (e.lengthComputable) onProgress?.(Math.round((e.loaded / e.total) * 100));
+    };
 
-  if (!res.ok) {
-    const text = await res.text().catch(() => "");
-    throw new Error(text || `http_error:${res.status}`);
-  }
+    xhr.onload = () => {
+      if (xhr.status >= 200 && xhr.status < 300) {
+        try {
+          resolve(JSON.parse(xhr.responseText));
+        } catch {
+          resolve(xhr.responseText);
+        }
+      } else {
+        reject(new Error(xhr.responseText || `http_error:${xhr.status}`));
+      }
+    };
+
+    xhr.onerror = () => reject(new Error("network_error"));
+    xhr.ontimeout = () => reject(new Error("timeout"));
 
-  const contentType = res.headers.get("content-type") ?? "";
-  if (contentType.includes("application/json")) return res.json();
-  return res.text();
+    xhr.open("POST", `${apiBaseUrl}/api/ingest/file`);
+    const token = getAccessToken();
+    if (token) xhr.setRequestHeader("Authorization", `Bearer ${token}`);
+    xhr.send(form);
+  });
+}
+
+export async function ingestUrl(url, { source } = {}) {
+  return apiFetch("/api/ingest/url", {
+    method: "POST",
+    body: { url, ...(source ? { source } : {}) }
+  });
 }
 
 export async function search(query) {

+ 56 - 10
src/components/DocumentList.vue

@@ -12,6 +12,8 @@ const emit = defineEmits(["refresh"]);
 
 const deleting = ref(null);
 const confirmingSource = ref(null);
+const deleteError = ref("");
+const filterQuery = ref("");
 
 const grouped = computed(() => {
   const result = {};
@@ -23,19 +25,40 @@ const grouped = computed(() => {
   return result;
 });
 
+const filteredGrouped = computed(() => {
+  const q = filterQuery.value.trim().toLowerCase();
+  if (!q) return grouped.value;
+  return Object.fromEntries(
+    Object.entries(grouped.value).filter(([source]) => source.toLowerCase().includes(q))
+  );
+});
+
+const totalChunks = computed(() => props.items.length);
+const totalDocs = computed(() => Object.keys(grouped.value).length);
+
 async function onConfirmDelete(source) {
   if (!source) return;
+  deleteError.value = "";
   deleting.value = source;
   try {
     await deleteDocumentsBySource(source);
     confirmingSource.value = null;
     emit("refresh");
   } catch (e) {
-    window.alert(`Erro ao excluir: ${e?.message || "erro desconhecido"}`);
+    deleteError.value = `Erro ao excluir: ${e?.message || "erro desconhecido"}`;
   } finally {
     deleting.value = null;
   }
 }
+
+function formatDate(iso) {
+  if (!iso) return null;
+  try {
+    return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "2-digit", hour: "2-digit", minute: "2-digit" }).format(new Date(iso));
+  } catch {
+    return null;
+  }
+}
 </script>
 
 <template>
@@ -44,7 +67,7 @@ async function onConfirmDelete(source) {
       <div class="text-xs text-gray-500 dark:text-gray-400">
         <span v-if="loading">Carregando...</span>
         <span v-else>
-          {{ Object.keys(grouped).length }} documento{{ Object.keys(grouped).length !== 1 ? 's' : '' }}
+          {{ totalChunks }} chunk{{ totalChunks !== 1 ? 's' : '' }} em {{ totalDocs }} documento{{ totalDocs !== 1 ? 's' : '' }}
         </span>
       </div>
       <button
@@ -59,7 +82,16 @@ async function onConfirmDelete(source) {
       </button>
     </div>
 
-   
+    <!-- Filtro -->
+    <div v-if="totalDocs > 3">
+      <input
+        v-model="filterQuery"
+        type="text"
+        placeholder="Filtrar por nome do documento..."
+        class="w-full rounded-xl border border-gray-200 bg-background px-3 py-2 text-sm text-gray-700 placeholder-gray-400 outline-none focus:border-primary dark:border-gray-700 dark:bg-white/5 dark:text-gray-200 dark:placeholder-gray-500"
+      />
+    </div>
+
     <div
       v-if="error"
       class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-300"
@@ -67,12 +99,17 @@ async function onConfirmDelete(source) {
       {{ error }}
     </div>
 
-    
+    <div
+      v-if="deleteError"
+      class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-300"
+    >
+      {{ deleteError }}
+    </div>
+
     <div v-if="loading" class="grid gap-2">
       <div v-for="i in 3" :key="i" class="h-16 animate-pulse rounded-xl bg-gray-100 dark:bg-white/5" />
     </div>
 
-    
     <div
       v-else-if="items.length === 0 && !error"
       class="flex flex-col items-center gap-3 rounded-xl border border-dashed border-gray-200 py-12 text-center dark:border-gray-700"
@@ -88,10 +125,9 @@ async function onConfirmDelete(source) {
       </div>
     </div>
 
-   
     <div v-else class="grid max-h-[520px] gap-2 overflow-auto">
       <div
-        v-for="(chunks, source) in grouped"
+        v-for="(chunks, source) in filteredGrouped"
         :key="source"
         class="overflow-hidden rounded-xl border border-gray-200 bg-background/60 dark:border-gray-700 dark:bg-background/10"
       >
@@ -100,13 +136,17 @@ async function onConfirmDelete(source) {
             <svg class="h-4 w-4 shrink-0 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
               <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
             </svg>
-            <span class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{{ source || "sem fonte" }}</span>
+            <div class="min-w-0">
+              <span class="block truncate text-sm font-medium text-gray-900 dark:text-gray-100">{{ source || "sem fonte" }}</span>
+              <span v-if="chunks[0]?.ingestedAt" class="text-[10px] text-gray-400 dark:text-gray-500">
+                {{ formatDate(chunks[0].ingestedAt) }}
+              </span>
+            </div>
             <span class="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-white/10 dark:text-gray-400">
               {{ chunks.length }} chunk{{ chunks.length !== 1 ? 's' : '' }}
             </span>
           </div>
 
-       
           <div v-if="confirmingSource === source" class="flex shrink-0 items-center gap-1.5">
             <span class="text-xs text-gray-500 dark:text-gray-400">Excluir tudo?</span>
             <button
@@ -135,7 +175,6 @@ async function onConfirmDelete(source) {
           </button>
         </div>
 
-       
         <div class="border-t border-gray-100 px-3 py-2.5 dark:border-gray-700/50">
           <div class="line-clamp-3 whitespace-pre-wrap text-xs leading-relaxed text-gray-500 dark:text-gray-400">
             {{ chunks[0]?.text }}
@@ -145,6 +184,13 @@ async function onConfirmDelete(source) {
           </div>
         </div>
       </div>
+
+      <div
+        v-if="filterQuery && Object.keys(filteredGrouped).length === 0"
+        class="py-6 text-center text-sm text-gray-400 dark:text-gray-500"
+      >
+        Nenhum documento corresponde ao filtro.
+      </div>
     </div>
   </div>
 </template>

+ 58 - 20
src/components/DocumentUpload.vue

@@ -1,12 +1,15 @@
 <script setup>
 import { ref, computed } from "vue";
+import { ingestFile, ingestDocuments, ingestUrl } from "../api/chat.js";
 
 const emit = defineEmits(["ingested"]);
 
 const tab = ref("arquivo");
 const manualText = ref("");
 const file = ref(null);
+const urlInput = ref("");
 const loading = ref(false);
+const uploadProgress = ref(0);
 const status = ref("");
 const error = ref("");
 const dragOver = ref(false);
@@ -55,17 +58,31 @@ async function onIngest() {
   error.value = "";
   status.value = "";
   loading.value = true;
+  uploadProgress.value = 0;
+
   try {
-    if (tab.value === "texto" && manualText.value.trim()) {
-      emit("ingested", { text: manualText.value.trim(), source: "manual" });
-    } else if (file.value) {
-      emit("ingested", { file: file.value, source: "upload" });
+    let result;
+
+    if (tab.value === "texto") {
+      result = await ingestDocuments([{ text: manualText.value.trim(), source: "manual" }]);
+      manualText.value = "";
+    } else if (tab.value === "arquivo") {
+      result = await ingestFile(file.value, {
+        source: file.value.name,
+        onProgress: (pct) => { uploadProgress.value = pct; }
+      });
+      file.value = null;
+      if (fileInputRef.value) fileInputRef.value.value = "";
+    } else if (tab.value === "url") {
+      result = await ingestUrl(urlInput.value.trim());
+      urlInput.value = "";
     }
-    manualText.value = "";
-    file.value = null;
-    if (fileInputRef.value) fileInputRef.value.value = "";
-    status.value = "Documento enviado para ingestão com sucesso.";
+
+    uploadProgress.value = 0;
+    status.value = `Ingerido com sucesso${result?.upserted != null ? ` (${result.upserted} chunks)` : ""}.`;
+    emit("ingested", result);
   } catch (e) {
+    uploadProgress.value = 0;
     error.value = e?.message || "Erro desconhecido.";
   } finally {
     loading.value = false;
@@ -74,6 +91,7 @@ async function onIngest() {
 
 const canSubmit = computed(() => {
   if (tab.value === "arquivo") return !!file.value;
+  if (tab.value === "url") return urlInput.value.trim().startsWith("https://");
   return !!manualText.value.trim();
 });
 </script>
@@ -82,7 +100,7 @@ const canSubmit = computed(() => {
   <div class="grid gap-4">
     <div class="flex gap-1 rounded-xl bg-gray-100 p-1 dark:bg-white/5">
       <button
-        v-for="t in [{ key: 'arquivo', label: 'Arquivo' }, { key: 'texto', label: 'Texto' }]"
+        v-for="t in [{ key: 'arquivo', label: 'Arquivo' }, { key: 'url', label: 'URL' }, { key: 'texto', label: 'Texto' }]"
         :key="t.key"
         type="button"
         class="flex-1 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors"
@@ -95,9 +113,8 @@ const canSubmit = computed(() => {
       </button>
     </div>
 
- 
+    <!-- Aba: Arquivo -->
     <template v-if="tab === 'arquivo'">
-     
       <div
         v-if="file"
         class="flex items-center gap-3 rounded-xl border border-gray-200 bg-background/60 p-3 dark:border-gray-700 dark:bg-background/10"
@@ -128,7 +145,6 @@ const canSubmit = computed(() => {
         </button>
       </div>
 
-     
       <div
         v-else
         class="relative flex cursor-pointer flex-col items-center gap-3 rounded-xl border-2 border-dashed px-6 py-10 transition-colors"
@@ -149,7 +165,7 @@ const canSubmit = computed(() => {
           <div class="text-sm font-medium text-gray-700 dark:text-gray-200">
             Arraste um arquivo ou <span class="text-primary underline underline-offset-2">clique para selecionar</span>
           </div>
-          <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">PDF, Word, TXT, PNG, JPG, WEBP</div>
+          <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">PDF, Word, TXT, PNG, JPG, WEBP — até 25 MB</div>
         </div>
         <input
           ref="fileInputRef"
@@ -159,9 +175,32 @@ const canSubmit = computed(() => {
           @change="onFileInputChange"
         />
       </div>
+
+      <!-- Barra de progresso -->
+      <div v-if="uploadProgress > 0" class="overflow-hidden rounded-full bg-gray-100 dark:bg-white/10">
+        <div
+          class="h-1.5 rounded-full bg-primary transition-all duration-200"
+          :style="{ width: `${uploadProgress}%` }"
+        />
+      </div>
     </template>
 
-  
+    <!-- Aba: URL -->
+    <template v-else-if="tab === 'url'">
+      <div class="grid gap-2">
+        <input
+          v-model="urlInput"
+          type="url"
+          placeholder="https://exemplo.com/pagina"
+          class="w-full rounded-xl border border-gray-200 bg-background px-3 py-2.5 text-sm text-gray-700 placeholder-gray-400 outline-none focus:border-primary dark:border-gray-700 dark:bg-white/5 dark:text-gray-200 dark:placeholder-gray-500"
+        />
+        <p class="text-xs text-gray-400 dark:text-gray-500">
+          Apenas URLs <code class="font-mono">https://</code> públicas. O conteúdo da página será extraído e indexado.
+        </p>
+      </div>
+    </template>
+
+    <!-- Aba: Texto -->
     <template v-else>
       <div class="rounded-2xl border border-gray-200 bg-white/70 p-4 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-white/[0.04]">
         <textarea
@@ -173,12 +212,11 @@ const canSubmit = computed(() => {
       </div>
     </template>
 
-
     <div class="flex items-center justify-between gap-3">
       <div class="text-xs text-gray-400 dark:text-gray-500">
-        {{ tab === 'arquivo'
-          ? 'O arquivo será processado e indexado na base de conhecimento.'
-          : 'O texto será fragmentado e indexado na base.' }}
+        <span v-if="tab === 'arquivo'">O arquivo será processado e indexado na base de conhecimento.</span>
+        <span v-else-if="tab === 'url'">A página será baixada, o texto extraído e indexado.</span>
+        <span v-else>O texto será fragmentado e indexado na base.</span>
       </div>
       <button
         type="button"
@@ -190,11 +228,11 @@ const canSubmit = computed(() => {
           <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
           <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
         </svg>
-        {{ loading ? "Enviando..." : "Ingerir" }}
+        <span v-if="loading && tab === 'arquivo' && uploadProgress > 0">{{ uploadProgress }}%</span>
+        <span v-else>{{ loading ? "Processando..." : "Ingerir" }}</span>
       </button>
     </div>
 
-    
     <div
       v-if="status"
       class="rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-300"

+ 6 - 2
src/composables/useChat.js

@@ -87,7 +87,9 @@ export function useChat() {
     if (!numId || !title?.trim()) return;
     try {
       await updateConversationTitleAPI(numId, title.trim());
-    } catch {}
+    } catch (err) {
+      console.error("[useChat] falha ao renomear conversa:", err);
+    }
     conversations.value = conversations.value.map((c) =>
       c.id === strId ? { ...c, title: title.trim() } : c
     );
@@ -99,7 +101,9 @@ export function useChat() {
     if (!numId) return;
     try {
       await deleteConversationAPI(numId);
-    } catch {}
+    } catch (err) {
+      console.error("[useChat] falha ao excluir conversa:", err);
+    }
     conversations.value = conversations.value.filter((c) => c.id !== strId);
     if (String(activeConversationId.value) === strId) {
       const next = conversations.value[0];

+ 52 - 58
src/layout/LayoutSistema.vue

@@ -12,7 +12,7 @@ import BotaoAlterarTema from "../components/BotaoAlterarTema.vue";
 import DocumentList from "../components/DocumentList.vue";
 import DocumentUpload from "../components/DocumentUpload.vue";
 import SearchBox from "../components/SearchBox.vue";
-import { ingestDocuments, ingestFile, listDocuments } from "../api/chat.js";
+import { listDocuments } from "../api/chat.js";
 import { useChat } from "../composables/useChat.js";
 import { useSearch } from "../composables/useSearch.js";
 import { useAuth } from "../composables/useAuth.js";
@@ -54,6 +54,7 @@ const menuUsuarioRef = ref(null);
 const rotaAtiva = computed(() => String(route.name ?? ""));
 const usuarioNome = computed(() => user.value?.Nome || user.value?.Login || "Usuario");
 const usuarioSetor = computed(() => user.value?.Setor || user.value?.Nivel || "");
+const isNivel1 = computed(() => String(user.value?.Nivel) === "1");
 const layoutStyle = computed(() => ({
   "--layout-sidebar-width": sidebarRecolhida.value ? "84px" : "280px",
 }));
@@ -210,17 +211,8 @@ async function loadDocs() {
   }
 }
 
-async function onIngested({ text, source, file } = {}) {
-  try {
-    if (file) {
-      await ingestFile(file, { source });
-    } else {
-      await ingestDocuments([{ text, source }]);
-    }
-    await loadDocs();
-  } catch (e) {
-    docs.error = e?.message || "erro";
-  }
+async function onIngested() {
+  await loadDocs();
 }
 
 onMounted(async () => {
@@ -585,10 +577,10 @@ watch(
                   <span class="flex-1 min-w-0 truncate leading-snug">{{ c.title }}</span>
                   <div
                     v-show="hoveredConversaId === c.id"
-                    class="absolute right-0 top-0 h-full flex items-center gap-0.5 pr-2 pl-6"
+                    class="absolute right-0 top-0 h-full flex items-center gap-0.5 pr-2 pl-16"
                     :class="String(activeConversationId) === c.id
-                      ? 'bg-gradient-to-l from-gray-200 dark:from-[#131f30] to-transparent'
-                      : 'bg-gradient-to-l from-gray-100 dark:from-[#101827] to-transparent'"
+                      ? 'bg-gradient-to-l from-gray-200 from-60% dark:from-[#131f30] to-transparent'
+                      : 'bg-gradient-to-l from-gray-100 from-60% dark:from-[#101827] to-transparent'"
                     @click.stop
                   >
                     <button
@@ -627,54 +619,56 @@ watch(
           <!-- Seções inferiores: Biblioteca + Sistema -->
           <div class="mt-auto flex-shrink-0 grid gap-1 pt-2">
             <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
-            <div
-              v-if="!sidebarRecolhida"
-              class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
-            >
-              Biblioteca
-            </div>
+            <template v-if="!isNivel1">
+              <div
+                v-if="!sidebarRecolhida"
+                class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
+              >
+                Biblioteca
+              </div>
 
-            <button
-              type="button"
-              :class="classeItemSidebar(painelAberto === 'update')"
-              :title="sidebarRecolhida ? 'Carregar documento' : ''"
-              @click="alternarPainel('update')"
-            >
-              <svg
-                :class="classeIcon(painelAberto === 'update')"
-                viewBox="0 0 24 24"
-                fill="none"
-                stroke="currentColor"
-                stroke-width="1.8"
+              <button
+                type="button"
+                :class="classeItemSidebar(painelAberto === 'update')"
+                :title="sidebarRecolhida ? 'Carregar documento' : ''"
+                @click="alternarPainel('update')"
               >
-                <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v12" />
-                <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 7.5 12 3l4.5 4.5" />
-                <path stroke-linecap="round" stroke-linejoin="round" d="M4 21h16" />
-              </svg>
-              <span :class="classeRotulo">Carregar documento</span>
-            </button>
+                <svg
+                  :class="classeIcon(painelAberto === 'update')"
+                  viewBox="0 0 24 24"
+                  fill="none"
+                  stroke="currentColor"
+                  stroke-width="1.8"
+                >
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v12" />
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 7.5 12 3l4.5 4.5" />
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M4 21h16" />
+                </svg>
+                <span :class="classeRotulo">Carregar documento</span>
+              </button>
 
-            <button
-              type="button"
-              :class="classeItemSidebar(painelAberto === 'documentos')"
-              :title="sidebarRecolhida ? 'Documentos' : ''"
-              @click="alternarPainel('documentos')"
-            >
-              <svg
-                :class="classeIcon(painelAberto === 'documentos')"
-                viewBox="0 0 24 24"
-                fill="none"
-                stroke="currentColor"
-                stroke-width="1.8"
+              <button
+                type="button"
+                :class="classeItemSidebar(painelAberto === 'documentos')"
+                :title="sidebarRecolhida ? 'Documentos' : ''"
+                @click="alternarPainel('documentos')"
               >
-                <path stroke-linecap="round" stroke-linejoin="round" d="M7 3h7l3 3v15a.75.75 0 0 1-.75.75H7.75A.75.75 0 0 1 7 21V3Z" />
-                <path stroke-linecap="round" stroke-linejoin="round" d="M14 3v4h4" />
-                <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6M9 15.5h6" />
-              </svg>
-              <span :class="classeRotulo">Documentos</span>
-            </button>
+                <svg
+                  :class="classeIcon(painelAberto === 'documentos')"
+                  viewBox="0 0 24 24"
+                  fill="none"
+                  stroke="currentColor"
+                  stroke-width="1.8"
+                >
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M7 3h7l3 3v15a.75.75 0 0 1-.75.75H7.75A.75.75 0 0 1 7 21V3Z" />
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M14 3v4h4" />
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6M9 15.5h6" />
+                </svg>
+                <span :class="classeRotulo">Documentos</span>
+              </button>
 
-            <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
+              <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
+            </template>
             <div
               v-if="!sidebarRecolhida"
               class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"

+ 7 - 0
src/router/index.js

@@ -34,5 +34,12 @@ router.beforeEach((to) => {
     };
   }
 
+  if (to.name === "usuarios") {
+    const { user } = useAuth();
+    if (String(user.value?.Nivel) === "1") {
+      return { name: "configuracoes" };
+    }
+  }
+
   return true;
 });

+ 0 - 9
src/services/toast.js

@@ -1,9 +0,0 @@
-let toast = null;
-
-export function setToastInstance(intance){
-    _toast = instance;
-}
-
-export function showToast(options){
-    _toast?.add(options);
-}

+ 0 - 12
src/stores/preferences.js

@@ -1,12 +0,0 @@
-import { defineStore } from 'pinia'
-
-export const usePreferencesStore = defineStore("PreferencesStore", {
-    state: () => ({
-        darkMode: false
-    }),
-    actions: {
-        setDarkMode(value) {
-            this.darkMode = value;
-        }
-    }
-});

+ 5 - 1
src/views/configuracoes/ConfiguracoesView.vue

@@ -1,9 +1,13 @@
 <script setup>
+import { computed } from "vue";
 import { useRouter } from "vue-router";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
 import BotaoAlterarTema from "../../components/BotaoAlterarTema.vue";
+import { useAuth } from "../../composables/useAuth.js";
 
 const router = useRouter();
+const { user } = useAuth();
+const isNivel1 = computed(() => String(user.value?.Nivel) === "1");
 
 function irParaUsuarios() {
   router.push({ name: "usuarios" });
@@ -46,7 +50,7 @@ function irParaUsuarios() {
           </div>
         </section>
 
-        <section>
+        <section v-if="!isNivel1">
           <div class="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-gray-500 dark:text-gray-400">
             Usuários & Acesso
           </div>