|
@@ -15,6 +15,33 @@ const totalUsuarios = computed(() => users.value?.length ?? 0);
|
|
|
const modalAberto = ref(false);
|
|
const modalAberto = ref(false);
|
|
|
const usuarioEmEdicao = ref(null);
|
|
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) {
|
|
function formatarStatus(status) {
|
|
|
return String(status) === "0" ? "Inativo" : "Ativo";
|
|
return String(status) === "0" ? "Inativo" : "Ativo";
|
|
|
}
|
|
}
|
|
@@ -61,6 +88,7 @@ onMounted(async () => {
|
|
|
<section
|
|
<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"
|
|
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 class="flex items-start justify-between gap-4">
|
|
|
<div>
|
|
<div>
|
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Usuários</h2>
|
|
<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>
|
|
</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">
|
|
<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">
|
|
<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" />
|
|
<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 }}
|
|
{{ error }}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+
|
|
|
<div
|
|
<div
|
|
|
v-else-if="!users.length"
|
|
v-else-if="!users.length"
|
|
|
class="mt-6 flex flex-col items-center gap-3 py-8 text-center"
|
|
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 class="text-sm font-medium text-gray-500 dark:text-gray-400">Nenhum usuário encontrado.</div>
|
|
|
</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>
|
|
|
- <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>
|
|
|
|
|
-
|
|
|
|
|
- <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>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|