leonardo 2 ヶ月 前
コミット
3c74209671

+ 7 - 2
src/api/conversations.js

@@ -1,7 +1,12 @@
 import { apiFetch } from "./client.js";
 
-export async function listConversations() {
-  return apiFetch("/api/conversations");
+export async function listConversations({ limit = 20, offset = 0 } = {}) {
+  const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }).toString();
+  return apiFetch(`/api/conversations?${params}`);
+}
+
+export async function exportConversation(id) {
+  return apiFetch(`/api/conversations/${id}/export`);
 }
 
 export async function createConversation(title = "Nova conversa") {

+ 17 - 10
src/components/ChatWindow.vue

@@ -12,7 +12,7 @@
       </div>
 
       <div v-else class="grid gap-4">
-        <div v-for="(m, idx) in messages" :key="idx" class="group flex flex-col" :class="m.role === 'user' ? 'items-end' : 'items-start'">
+        <div v-for="(m, idx) in messages" v-show="!m.streaming || m.content" :key="idx" class="group flex flex-col" :class="m.role === 'user' ? 'items-end' : 'items-start'">
           <div class="mb-0.5 flex items-center gap-1.5" :class="m.role === 'user' ? 'flex-row-reverse' : ''">
             <div
               class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-bold text-white"
@@ -29,7 +29,6 @@
           </div>
 
           <div class="relative max-w-[78%]">
-            <!-- Botão copiar mensagem -->
             <button
               type="button"
               class="copy-btn absolute -top-1 z-10 flex items-center gap-1 rounded-md border border-gray-200 bg-white px-1.5 py-0.5 text-[10px] font-medium text-gray-500 opacity-0 shadow-sm transition-opacity group-hover:opacity-100 hover:text-gray-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
@@ -55,16 +54,23 @@
                   : 'prose-chat border-gray-200 bg-white/50 text-foreground dark:border-gray-700 dark:bg-gray-800/60'
               "
             >
-              <!-- Usuário: texto puro -->
+              
               <span v-if="m.role === 'user'" class="whitespace-pre-wrap">{{ m.content }}</span>
-              <!-- Assistente: Markdown renderizado -->
-              <!-- eslint-disable-next-line vue/no-v-html -->
-              <div v-else class="prose-content" v-html="renderMarkdown(m.content)" />
+              
+              <template v-else>
+                
+                <div class="prose-content" v-html="renderMarkdown(m.content)" />
+                <span
+                  v-if="m.streaming"
+                  class="ml-0.5 inline-block h-[1em] w-0.5 animate-pulse rounded-sm bg-current align-middle opacity-60"
+                  aria-hidden="true"
+                />
+              </template>
             </div>
           </div>
         </div>
 
-        <div v-if="loading" class="flex flex-col items-start gap-0.5">
+        <div v-if="loading && !isStreaming" class="flex flex-col items-start gap-0.5">
           <div class="mb-0.5 flex items-center gap-1.5">
             <div class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-gray-400 text-[9px] font-bold text-white dark:bg-gray-600">O</div>
             <span class="text-xs font-medium text-gray-400 dark:text-gray-500">Oráculo</span>
@@ -118,7 +124,7 @@
 </template>
 
 <script setup>
-import { ref, nextTick, onMounted, onUnmounted, watch } from "vue";
+import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
 import { marked, Renderer } from "marked";
 import DOMPurify from "dompurify";
 import hljs from "highlight.js";
@@ -141,7 +147,9 @@ const chatEl = ref(null);
 const copiado = ref(null);
 let copiadoTimeout = null;
 
-// Configurar marked com highlight.js
+const isStreaming = computed(() => props.messages.some((m) => m.streaming && m.content));
+
+
 const renderer = new Renderer();
 
 renderer.code = ({ text, lang }) => {
@@ -175,7 +183,6 @@ function renderMarkdown(content) {
   });
 }
 
-// Delegação de eventos para botões de copiar dentro do HTML renderizado
 function handleCodeCopy(e) {
   const btn = e.target.closest(".code-copy-btn");
   if (!btn) return;

+ 5 - 5
src/components/ModalUsuario.vue

@@ -1,7 +1,7 @@
 <template>
   <Teleport to="body">
-    <div class="fixed inset-0 z-[9999] flex items-center justify-center p-4">
-      <div class="absolute inset-0 bg-black/40 dark:bg-black/60" @click="!salvando && $emit('close')" />
+    <div class="fixed inset-0 z-[9999] flex items-center justify-center p-4" @click="!salvando && $emit('close')">
+      <div class="absolute inset-0 bg-black/40 dark:bg-black/60" />
       <div
         class="relative w-full max-w-md overflow-hidden rounded-2xl border border-gray-200 bg-background text-foreground shadow-xl dark:border-gray-700"
         @click.stop
@@ -47,9 +47,9 @@
             <div class="grid gap-1.5">
               <label for="mu-nivel" class="text-sm font-medium text-gray-700 dark:text-gray-300">Nível</label>
               <select id="mu-nivel" v-model="form.Nivel" :disabled="salvando">
-                <option value="1">1 — Básico</option>
-                <option value="2">2 — Intermediário</option>
-                <option value="3">3 — Administrador</option>
+                <option value="1">Usuário</option>
+                <option value="2">Gestor</option>
+                <option value="3">Administrador</option>
               </select>
             </div>
             <div class="grid gap-1.5">

+ 58 - 10
src/composables/useChat.js

@@ -5,7 +5,8 @@ import {
   createConversation,
   getConversationMessages,
   updateConversationTitle as updateConversationTitleAPI,
-  deleteConversation as deleteConversationAPI
+  deleteConversation as deleteConversationAPI,
+  exportConversation as exportConversationAPI
 } from "../api/conversations.js";
 import { useToast } from "./useToast.js";
 
@@ -33,6 +34,8 @@ export function useChat() {
     }
   ];
 
+  const CONV_PAGE_SIZE = 20;
+
   const conversations = ref([]);
   const activeConversationId = ref(null);
   const messages = ref([...defaultMessages]);
@@ -40,6 +43,8 @@ export function useChat() {
   const error = ref("");
   const conversationsLoading = ref(false);
   const conversationsError = ref("");
+  const conversationsHasMore = ref(false);
+  const conversationsOffset = ref(0);
 
   function normalizeApiMessages(items) {
     return (items ?? []).map((m) => ({
@@ -54,20 +59,29 @@ export function useChat() {
     }));
   }
 
+  function normalizeConversation(c) {
+    return {
+      id: String(c.Id),
+      title: c.Title || "Nova conversa",
+      createdAt: new Date(c.CreatedAt).getTime(),
+      updatedAt: new Date(c.UpdatedAt).getTime(),
+      messageCount: 0
+    };
+  }
+
   async function loadConversationsList() {
     conversationsLoading.value = true;
     conversationsError.value = "";
+    conversationsOffset.value = 0;
     try {
-      const data = await listConversations();
-      conversations.value = (data.items ?? []).map((c) => ({
-        id: String(c.Id),
-        title: c.Title || "Nova conversa",
-        createdAt: new Date(c.CreatedAt).getTime(),
-        updatedAt: new Date(c.UpdatedAt).getTime(),
-        messageCount: 0
-      }));
+      const data = await listConversations({ limit: CONV_PAGE_SIZE, offset: 0 });
+      const items = data.items ?? [];
+      conversations.value = items.map(normalizeConversation);
+      conversationsHasMore.value = items.length >= CONV_PAGE_SIZE;
+      conversationsOffset.value = items.length;
     } catch (err) {
       conversations.value = [];
+      conversationsHasMore.value = false;
       if (err?.message !== "session_expired") {
         conversationsError.value = err?.message || "Erro ao carregar conversas.";
       }
@@ -76,6 +90,22 @@ export function useChat() {
     }
   }
 
+  async function loadMoreConversations() {
+    if (!conversationsHasMore.value || conversationsLoading.value) return;
+    conversationsLoading.value = true;
+    try {
+      const data = await listConversations({ limit: CONV_PAGE_SIZE, offset: conversationsOffset.value });
+      const items = data.items ?? [];
+      conversations.value = [...conversations.value, ...items.map(normalizeConversation)];
+      conversationsHasMore.value = items.length >= CONV_PAGE_SIZE;
+      conversationsOffset.value += items.length;
+    } catch {
+      // keep existing conversations on error
+    } finally {
+      conversationsLoading.value = false;
+    }
+  }
+
   async function setActiveConversation(id) {
     const strId = String(id ?? "").trim();
     if (!strId) return;
@@ -129,6 +159,21 @@ export function useChat() {
     }
   }
 
+  async function exportConversation(id) {
+    const numId = Number(String(id ?? "").trim());
+    if (!numId) return;
+    const data = await exportConversationAPI(numId);
+    const conv = conversations.value.find((c) => c.id === String(id));
+    const filename = `conversa-${conv?.title?.slice(0, 40).replace(/[^a-zA-Z0-9À-ÿ\s]/g, "").trim().replace(/\s+/g, "-") || numId}.md`;
+    const blob = new Blob([data.markdown ?? ""], { type: "text/markdown" });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement("a");
+    a.href = url;
+    a.download = filename;
+    a.click();
+    URL.revokeObjectURL(url);
+  }
+
   function searchConversations(query) {
     const q = String(query ?? "").trim().toLowerCase();
     const sorted = conversations.value
@@ -231,6 +276,7 @@ export function useChat() {
     error,
     conversationsLoading,
     conversationsError,
+    conversationsHasMore,
     send,
     clearError,
     cancelCurrentStream,
@@ -239,7 +285,9 @@ export function useChat() {
     renameConversation,
     deleteConversation,
     searchConversations,
-    loadConversationsList
+    loadConversationsList,
+    loadMoreConversations,
+    exportConversation
   };
 
   return singleton;

+ 13 - 1
src/layout/LayoutSistema.vue

@@ -26,10 +26,12 @@ const {
   activeConversationId,
   conversationsLoading,
   conversationsError,
+  conversationsHasMore,
   setActiveConversation,
   deleteConversation,
   renameConversation,
   newConversation,
+  loadMoreConversations,
 } = useChat();
 const { user, logout } = useAuth();
 
@@ -605,6 +607,16 @@ watch(
             >
               Nenhuma conversa ainda
             </div>
+
+            <button
+              v-if="conversationsHasMore"
+              type="button"
+              class="mt-1 w-full rounded-xl px-3 py-2 text-left text-xs text-gray-400 transition-colors hover:bg-gray-100/80 hover:text-gray-700 dark:text-white/30 dark:hover:bg-white/10 dark:hover:text-white/70"
+              :disabled="conversationsLoading"
+              @click="loadMoreConversations"
+            >
+              {{ conversationsLoading ? "Carregando..." : "Carregar mais" }}
+            </button>
           </div>
 
           <!-- Seções inferiores: Biblioteca + Sistema -->
@@ -705,7 +717,7 @@ watch(
               class="modal-backdrop absolute inset-0 bg-black/40 dark:bg-black/60"
               @click="fecharModal"
             />
-            <div class="absolute inset-0 flex items-center justify-center p-4">
+            <div class="absolute inset-0 flex items-center justify-center p-4" @click="fecharModal">
               <div
                 class="modal-panel w-full max-w-[980px] overflow-hidden rounded-2xl border border-gray-200 bg-background text-foreground shadow-xl dark:border-gray-700"
                 @click.stop

+ 36 - 1
src/views/pagina-inicial/PaginaInicialView.vue

@@ -5,9 +5,30 @@ import LayoutSistema from "../../layout/LayoutSistema.vue";
 import { useChat } from "../../composables/useChat.js";
 import BaseButton from "../../components/base/BaseButton.vue";
 
-const { messages: chatMessages, loading: chatLoading, error: chatError, send: sendMessage, clearError, cancelCurrentStream } = useChat();
+const {
+  messages: chatMessages,
+  loading: chatLoading,
+  error: chatError,
+  send: sendMessage,
+  clearError,
+  cancelCurrentStream,
+  activeConversationId,
+  exportConversation
+} = useChat();
 onUnmounted(() => cancelCurrentStream());
 
+const exportando = ref(false);
+
+async function onExport() {
+  if (!activeConversationId.value || exportando.value) return;
+  exportando.value = true;
+  try {
+    await exportConversation(activeConversationId.value);
+  } finally {
+    exportando.value = false;
+  }
+}
+
 const landingDraft = ref("");
 const isLanding = computed(() => {
   const ms = chatMessages.value ?? [];
@@ -66,6 +87,20 @@ async function onLandingSend() {
 
     <div v-else class="relative h-full w-full">
       <div class="relative flex h-full w-full flex-col p-4 md:p-6">
+        <div class="mb-2 flex items-center justify-end">
+          <button
+            v-if="activeConversationId"
+            type="button"
+            class="inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white/60 px-2.5 py-1.5 text-xs font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-700 disabled:opacity-50 dark:border-white/10 dark:bg-white/5 dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white/80"
+            :disabled="exportando || chatLoading"
+            @click="onExport"
+          >
+            <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="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
+            </svg>
+            {{ exportando ? "Exportando..." : "Exportar .md" }}
+          </button>
+        </div>
         <ChatWindow class="flex-1 min-h-1" :messages="chatMessages" :loading="chatLoading" :error="chatError" @send="sendMessage" @clearError="clearError" />
       </div>
     </div>

+ 162 - 63
src/views/usuarios/UsuariosView.vue

@@ -15,6 +15,33 @@ const totalUsuarios = computed(() => users.value?.length ?? 0);
 const modalAberto = ref(false);
 const usuarioEmEdicao = ref(null);
 
+const busca = ref("");
+const filtroStatus = ref("todos");
+
+const filtros = [
+  { v: "todos", l: "Todos" },
+  { v: "ativo", l: "Ativos" },
+  { v: "inativo", l: "Inativos" },
+];
+
+const usuariosFiltrados = computed(() => {
+  let lista = users.value ?? [];
+  if (filtroStatus.value === "ativo")   lista = lista.filter((u) => String(u.Status) === "1");
+  if (filtroStatus.value === "inativo") lista = lista.filter((u) => String(u.Status) === "0");
+  if (busca.value.trim()) {
+    const q = busca.value.toLowerCase();
+    lista = lista.filter(
+      (u) =>
+        (u.Nome ?? "").toLowerCase().includes(q) ||
+        (u.Login ?? "").toLowerCase().includes(q) ||
+        (u.Email ?? "").toLowerCase().includes(q),
+    );
+  }
+  return lista;
+});
+
+const nivelLabel = { "1": "Usuário", "2": "Gestor", "3": "Administrador" };
+
 function formatarStatus(status) {
   return String(status) === "0" ? "Inativo" : "Ativo";
 }
@@ -61,6 +88,7 @@ onMounted(async () => {
     <section
       class="rounded-2xl border border-gray-200 bg-gray-100/50 p-6 shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50"
     >
+      
       <div class="flex items-start justify-between gap-4">
         <div>
           <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Usuários</h2>
@@ -83,6 +111,40 @@ onMounted(async () => {
         </div>
       </div>
 
+      
+      <div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
+        <div class="relative flex-1">
+          <svg
+            class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400 dark:text-gray-500"
+            viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+          >
+            <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
+          </svg>
+          <input
+            v-model="busca"
+            placeholder="Buscar por nome, login ou e-mail..."
+            class="w-full rounded-xl border border-gray-200 bg-background py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:placeholder:text-gray-500"
+          />
+        </div>
+        <div class="flex shrink-0 items-center gap-1.5">
+          <button
+            v-for="f in filtros"
+            :key="f.v"
+            type="button"
+            class="rounded-full px-3 py-1 text-xs font-medium transition-colors"
+            :class="
+              filtroStatus === f.v
+                ? 'bg-primary text-white'
+                : 'border border-gray-200 text-gray-500 hover:border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:hover:border-gray-600'
+            "
+            @click="filtroStatus = f.v"
+          >
+            {{ f.l }}
+          </button>
+        </div>
+      </div>
+
+     
       <div v-if="loading" class="mt-6 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
         <svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
           <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v3m0 12v3m9-9h-3M6 12H3m15.364-6.364-2.121 2.121M8.757 15.243l-2.121 2.121M18.364 18.364l-2.121-2.121M8.757 8.757 6.636 6.636" />
@@ -97,6 +159,7 @@ onMounted(async () => {
         {{ error }}
       </div>
 
+  
       <div
         v-else-if="!users.length"
         class="mt-6 flex flex-col items-center gap-3 py-8 text-center"
@@ -110,72 +173,108 @@ onMounted(async () => {
         <div class="text-sm font-medium text-gray-500 dark:text-gray-400">Nenhum usuário encontrado.</div>
       </div>
 
-      <div v-else class="mt-6 grid gap-3">
-        <article
-          v-for="usuario in users"
-          :key="usuario.Id"
-          class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20"
-        >
-          <div class="flex flex-col gap-3 md:flex-row md:items-start md:gap-4">
-            <div
-              class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-bold text-white"
-              :class="avatarCor(usuario.Nome || usuario.Login)"
+      
+      <div v-else class="mt-4 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700">
+        <table class="w-full text-sm">
+          <thead class="bg-gray-50 dark:bg-white/5">
+            <tr>
+              <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Usuário</th>
+              <th class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400 sm:table-cell">Login</th>
+              <th class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400 md:table-cell">E-mail</th>
+              <th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Status</th>
+              <th class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400 lg:table-cell">Nível</th>
+              <th class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400 lg:table-cell">Setor</th>
+              <th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Ações</th>
+            </tr>
+          </thead>
+          <tbody class="divide-y divide-gray-100 dark:divide-gray-700/60">
+            <tr
+              v-for="usuario in usuariosFiltrados"
+              :key="usuario.Id"
+              class="transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.03]"
             >
-              {{ avatarIniciais(usuario.Nome || usuario.Login) }}
-            </div>
-
-            <div class="min-w-0 flex-1">
-              <div class="text-base font-semibold text-gray-900 dark:text-gray-100">
-                {{ usuario.Nome || usuario.Login || "Usuário sem nome" }}
-              </div>
-              <div class="mt-1.5 flex flex-wrap gap-x-4 gap-y-1">
-                <div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
-                  <svg class="h-3.5 w-3.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
-                    <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
-                  </svg>
-                  <span>{{ usuario.Login || "-" }}</span>
+             
+              <td class="px-4 py-3">
+                <div class="flex items-center gap-3">
+                  <div
+                    class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white"
+                    :class="avatarCor(usuario.Nome || usuario.Login)"
+                  >
+                    {{ avatarIniciais(usuario.Nome || usuario.Login) }}
+                  </div>
+                  <span class="font-medium text-gray-900 dark:text-gray-100">
+                    {{ usuario.Nome || usuario.Login || "Usuário sem nome" }}
+                  </span>
                 </div>
-                <div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
-                  <svg class="h-3.5 w-3.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
-                    <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
-                  </svg>
-                  <span>{{ usuario.Email || "-" }}</span>
+              </td>
+
+              
+              <td class="hidden px-4 py-3 text-gray-600 dark:text-gray-400 sm:table-cell">
+                {{ usuario.Login || "-" }}
+              </td>
+
+              
+              <td class="hidden px-4 py-3 text-gray-600 dark:text-gray-400 md:table-cell">
+                {{ usuario.Email || "-" }}
+              </td>
+
+              
+              <td class="px-4 py-3">
+                <BaseBadge :variant="String(usuario.Status) === '0' ? 'danger' : 'success'">
+                  {{ formatarStatus(usuario.Status) }}
+                </BaseBadge>
+              </td>
+
+             
+              <td class="hidden px-4 py-3 lg:table-cell">
+                <BaseBadge variant="info">
+                  {{ nivelLabel[String(usuario.Nivel)] ?? `Nível ${usuario.Nivel ?? "-"}` }}
+                </BaseBadge>
+              </td>
+
+              
+              <td class="hidden px-4 py-3 lg:table-cell">
+                <BaseBadge variant="violet">{{ usuario.Setor ?? "-" }}</BaseBadge>
+              </td>
+
+              
+              <td class="px-4 py-3">
+                <div class="flex items-center justify-end gap-1.5">
+                  <BaseButton
+                    variant="ghost"
+                    size="sm"
+                    class="!px-2"
+                    title="Editar usuário"
+                    aria-label="Editar usuário"
+                    @click="abrirEdicao(usuario)"
+                  >
+                    <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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
+                    </svg>
+                  </BaseButton>
+                  <BaseButton
+                    :variant="String(usuario.Status) === '1' ? 'danger' : 'secondary'"
+                    size="sm"
+                    :title="String(usuario.Status) === '1' ? 'Desativar usuário' : 'Ativar usuário'"
+                    :aria-label="String(usuario.Status) === '1' ? 'Desativar usuário' : 'Ativar usuário'"
+                    @click="alternarStatus(usuario)"
+                  >
+                    {{ String(usuario.Status) === "1" ? "Desativar" : "Ativar" }}
+                  </BaseButton>
                 </div>
-              </div>
-            </div>
-
-            <div class="flex flex-wrap items-center gap-2">
-              <BaseBadge :variant="String(usuario.Status) === '0' ? 'danger' : 'success'">
-                {{ formatarStatus(usuario.Status) }}
-              </BaseBadge>
-              <BaseBadge variant="info">Nível {{ usuario.Nivel ?? "-" }}</BaseBadge>
-              <BaseBadge variant="violet">{{ usuario.Setor ?? "-" }}</BaseBadge>
-
-              <BaseButton
-                variant="ghost"
-                size="sm"
-                class="ml-auto !px-2"
-                title="Editar usuário"
-                aria-label="Editar usuário"
-                @click="abrirEdicao(usuario)"
-              >
-                <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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
-                </svg>
-              </BaseButton>
-
-              <BaseButton
-                :variant="String(usuario.Status) === '1' ? 'danger' : 'secondary'"
-                size="sm"
-                :title="String(usuario.Status) === '1' ? 'Desativar usuário' : 'Ativar usuário'"
-                :aria-label="String(usuario.Status) === '1' ? 'Desativar usuário' : 'Ativar usuário'"
-                @click="alternarStatus(usuario)"
-              >
-                {{ String(usuario.Status) === "1" ? "Desativar" : "Ativar" }}
-              </BaseButton>
-            </div>
-          </div>
-        </article>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+
+        
+        <div
+          v-if="!usuariosFiltrados.length"
+          class="py-10 text-center text-sm text-gray-500 dark:text-gray-400"
+        >
+          Nenhum resultado para
+          <span class="font-medium">{{ busca || (filtroStatus !== "todos" ? filtroStatus : "") }}</span>.
+        </div>
       </div>
     </section>