feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates
This commit is contained in:
@@ -0,0 +1,977 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient, useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
|
||||
const convex = useConvexClient();
|
||||
|
||||
// Aba ativa
|
||||
let abaAtiva = $state<"solicitacoes" | "usuarios" | "logs">("solicitacoes");
|
||||
|
||||
// ========================================
|
||||
// ABA 1: SOLICITAÇÕES DE ACESSO
|
||||
// ========================================
|
||||
const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {});
|
||||
let filtroStatusSolicitacao = $state<"todas" | "pendente" | "aprovado" | "rejeitado">("todas");
|
||||
let filtroNomeSolicitacao = $state("");
|
||||
let filtroMatriculaSolicitacao = $state("");
|
||||
let modalSolicitacaoAberto = $state(false);
|
||||
let solicitacaoSelecionada = $state<Id<"solicitacoesAcesso"> | null>(null);
|
||||
let acaoModalSolicitacao = $state<"aprovar" | "rejeitar" | null>(null);
|
||||
let observacoesSolicitacao = $state("");
|
||||
|
||||
let filteredSolicitacoes = $derived(() => {
|
||||
if (!solicitacoesQuery.data) return [];
|
||||
|
||||
return solicitacoesQuery.data.filter((s: any) => {
|
||||
const matchStatus = filtroStatusSolicitacao === "todas" || s.status === filtroStatusSolicitacao;
|
||||
const matchNome = s.nome.toLowerCase().includes(filtroNomeSolicitacao.toLowerCase());
|
||||
const matchMatricula = s.matricula.toLowerCase().includes(filtroMatriculaSolicitacao.toLowerCase());
|
||||
return matchStatus && matchNome && matchMatricula;
|
||||
});
|
||||
});
|
||||
|
||||
function abrirModalSolicitacao(solicitacaoId: Id<"solicitacoesAcesso">, acao: "aprovar" | "rejeitar") {
|
||||
solicitacaoSelecionada = solicitacaoId;
|
||||
acaoModalSolicitacao = acao;
|
||||
observacoesSolicitacao = "";
|
||||
modalSolicitacaoAberto = true;
|
||||
}
|
||||
|
||||
function fecharModalSolicitacao() {
|
||||
modalSolicitacaoAberto = false;
|
||||
solicitacaoSelecionada = null;
|
||||
acaoModalSolicitacao = null;
|
||||
observacoesSolicitacao = "";
|
||||
}
|
||||
|
||||
async function confirmarAcaoSolicitacao() {
|
||||
if (!solicitacaoSelecionada || !acaoModalSolicitacao) return;
|
||||
|
||||
try {
|
||||
if (acaoModalSolicitacao === "aprovar") {
|
||||
await convex.mutation(api.solicitacoesAcesso.aprovar, {
|
||||
solicitacaoId: solicitacaoSelecionada,
|
||||
observacoes: observacoesSolicitacao || undefined,
|
||||
});
|
||||
mostrarNotice("success", "Solicitação aprovada com sucesso!");
|
||||
} else {
|
||||
await convex.mutation(api.solicitacoesAcesso.rejeitar, {
|
||||
solicitacaoId: solicitacaoSelecionada,
|
||||
observacoes: observacoesSolicitacao || undefined,
|
||||
});
|
||||
mostrarNotice("success", "Solicitação rejeitada com sucesso!");
|
||||
}
|
||||
fecharModalSolicitacao();
|
||||
} catch (error: any) {
|
||||
mostrarNotice("error", error.message || "Erro ao processar solicitação.");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ABA 2: GERENCIAMENTO DE USUÁRIOS
|
||||
// ========================================
|
||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||
const rolesQuery = useQuery(api.roles.listar, {});
|
||||
let filtroNomeUsuario = $state("");
|
||||
let filtroMatriculaUsuario = $state("");
|
||||
let filtroRoleUsuario = $state<string>("todos");
|
||||
let filtroStatusUsuario = $state<"todos" | "ativo" | "inativo">("todos");
|
||||
let modalUsuarioAberto = $state(false);
|
||||
let usuarioSelecionado = $state<Id<"usuarios"> | null>(null);
|
||||
let acaoModalUsuario = $state<"ativar" | "desativar" | "resetar" | "alterar_role" | null>(null);
|
||||
let novaRoleId = $state<Id<"roles"> | null>(null);
|
||||
|
||||
let filteredUsuarios = $derived(() => {
|
||||
if (!usuariosQuery.data) return [];
|
||||
|
||||
return usuariosQuery.data.filter((u: any) => {
|
||||
const matchNome = u.nome.toLowerCase().includes(filtroNomeUsuario.toLowerCase());
|
||||
const matchMatricula = u.matricula.toLowerCase().includes(filtroMatriculaUsuario.toLowerCase());
|
||||
const matchRole = filtroRoleUsuario === "todos" || u.role.nome === filtroRoleUsuario;
|
||||
const matchStatus = filtroStatusUsuario === "todos" ||
|
||||
(filtroStatusUsuario === "ativo" && u.ativo) ||
|
||||
(filtroStatusUsuario === "inativo" && !u.ativo);
|
||||
return matchNome && matchMatricula && matchRole && matchStatus;
|
||||
});
|
||||
});
|
||||
|
||||
function abrirModalUsuario(usuarioId: Id<"usuarios">, acao: "ativar" | "desativar" | "resetar" | "alterar_role") {
|
||||
usuarioSelecionado = usuarioId;
|
||||
acaoModalUsuario = acao;
|
||||
|
||||
// Se for alterar role, pegar a role atual do usuário
|
||||
if (acao === "alterar_role" && usuariosQuery.data) {
|
||||
const usuario = usuariosQuery.data.find((u: any) => u._id === usuarioId);
|
||||
if (usuario) {
|
||||
novaRoleId = usuario.role._id;
|
||||
}
|
||||
}
|
||||
|
||||
modalUsuarioAberto = true;
|
||||
}
|
||||
|
||||
function fecharModalUsuario() {
|
||||
modalUsuarioAberto = false;
|
||||
usuarioSelecionado = null;
|
||||
acaoModalUsuario = null;
|
||||
novaRoleId = null;
|
||||
}
|
||||
|
||||
async function confirmarAcaoUsuario() {
|
||||
if (!usuarioSelecionado || !acaoModalUsuario) return;
|
||||
|
||||
try {
|
||||
if (acaoModalUsuario === "ativar") {
|
||||
await convex.mutation(api.usuarios.ativar, { id: usuarioSelecionado });
|
||||
mostrarNotice("success", "Usuário ativado com sucesso!");
|
||||
} else if (acaoModalUsuario === "desativar") {
|
||||
await convex.mutation(api.usuarios.desativar, { id: usuarioSelecionado });
|
||||
mostrarNotice("success", "Usuário desativado com sucesso!");
|
||||
} else if (acaoModalUsuario === "resetar") {
|
||||
await convex.mutation(api.usuarios.resetarSenha, {
|
||||
usuarioId: usuarioSelecionado,
|
||||
novaSenha: "Mudar@123"
|
||||
});
|
||||
mostrarNotice("success", "Senha resetada para 'Mudar@123' com sucesso!");
|
||||
} else if (acaoModalUsuario === "alterar_role" && novaRoleId) {
|
||||
await convex.mutation(api.usuarios.alterarRole, {
|
||||
usuarioId: usuarioSelecionado,
|
||||
novaRoleId: novaRoleId
|
||||
});
|
||||
mostrarNotice("success", "Função/Nível alterado com sucesso!");
|
||||
}
|
||||
fecharModalUsuario();
|
||||
} catch (error: any) {
|
||||
mostrarNotice("error", error.message || "Erro ao processar ação.");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ABA 3: HISTÓRICO DE ACESSOS
|
||||
// ========================================
|
||||
const logsQuery = useQuery(api.logsAcesso.listar, { limite: 100 });
|
||||
let filtroTipoLog = $state<string>("todos");
|
||||
let filtroUsuarioLog = $state("");
|
||||
let modalLimparLogsAberto = $state(false);
|
||||
|
||||
let filteredLogs = $derived(() => {
|
||||
if (!logsQuery.data) return [];
|
||||
|
||||
return logsQuery.data.filter((log: any) => {
|
||||
const matchTipo = filtroTipoLog === "todos" || log.tipo === filtroTipoLog;
|
||||
const matchUsuario = !filtroUsuarioLog ||
|
||||
(log.usuario && log.usuario.nome.toLowerCase().includes(filtroUsuarioLog.toLowerCase()));
|
||||
return matchTipo && matchUsuario;
|
||||
});
|
||||
});
|
||||
|
||||
async function limparLogs() {
|
||||
try {
|
||||
await convex.mutation(api.logsAcesso.limparTodos, {});
|
||||
mostrarNotice("success", "Histórico de logs limpo com sucesso!");
|
||||
modalLimparLogsAberto = false;
|
||||
} catch (error: any) {
|
||||
mostrarNotice("error", error.message || "Erro ao limpar logs.");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UTILITÁRIOS
|
||||
// ========================================
|
||||
let notice = $state<{ type: "success" | "error"; message: string } | null>(null);
|
||||
|
||||
function mostrarNotice(type: "success" | "error", message: string) {
|
||||
notice = { type, message };
|
||||
setTimeout(() => {
|
||||
notice = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function formatarData(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusBadgeClass(status: string): string {
|
||||
switch (status) {
|
||||
case "pendente":
|
||||
return "badge-warning";
|
||||
case "aprovado":
|
||||
return "badge-success";
|
||||
case "rejeitado":
|
||||
return "badge-error";
|
||||
default:
|
||||
return "badge-ghost";
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case "pendente":
|
||||
return "Pendente";
|
||||
case "aprovado":
|
||||
return "Aprovado";
|
||||
case "rejeitado":
|
||||
return "Rejeitado";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
function getTipoLogIcon(tipo: string): string {
|
||||
switch (tipo) {
|
||||
case "login":
|
||||
return "🔓";
|
||||
case "logout":
|
||||
return "🔒";
|
||||
case "acesso_negado":
|
||||
return "⛔";
|
||||
case "senha_alterada":
|
||||
return "🔑";
|
||||
case "sessao_expirada":
|
||||
return "⏰";
|
||||
default:
|
||||
return "📝";
|
||||
}
|
||||
}
|
||||
|
||||
function getTipoLogLabel(tipo: string): string {
|
||||
switch (tipo) {
|
||||
case "login":
|
||||
return "Login";
|
||||
case "logout":
|
||||
return "Logout";
|
||||
case "acesso_negado":
|
||||
return "Acesso Negado";
|
||||
case "senha_alterada":
|
||||
return "Senha Alterada";
|
||||
case "sessao_expirada":
|
||||
return "Sessão Expirada";
|
||||
default:
|
||||
return tipo;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute requireAuth={true} allowedRoles={["admin", "ti"]} maxLevel={1}>
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<h1 class="text-4xl font-bold text-primary">Painel Administrativo</h1>
|
||||
</div>
|
||||
<p class="text-base-content/70 text-lg">
|
||||
Controle total de acesso, usuários e auditoria do sistema SGSE
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if notice}
|
||||
<div class="alert {notice.type === 'success' ? 'alert-success' : 'alert-error'} mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{#if notice.type === "success"}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.message}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-boxed bg-base-200 p-2 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
class="tab {abaAtiva === 'solicitacoes' ? 'tab-active' : ''}"
|
||||
onclick={() => (abaAtiva = "solicitacoes")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Solicitações de Acesso
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tab {abaAtiva === 'usuarios' ? 'tab-active' : ''}"
|
||||
onclick={() => (abaAtiva = "usuarios")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Gerenciar Usuários
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tab {abaAtiva === 'logs' ? 'tab-active' : ''}"
|
||||
onclick={() => (abaAtiva = "logs")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Histórico de Acessos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ABA 1: SOLICITAÇÕES -->
|
||||
{#if abaAtiva === "solicitacoes"}
|
||||
<!-- Estatísticas -->
|
||||
{#if solicitacoesQuery.data}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat bg-gradient-to-br from-primary/20 to-primary/10 shadow-lg rounded-xl border border-primary/30">
|
||||
<div class="stat-title">Total</div>
|
||||
<div class="stat-value text-primary">{solicitacoesQuery.data.length}</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-warning/20 to-warning/10 shadow-lg rounded-xl border border-warning/30">
|
||||
<div class="stat-title">Pendentes</div>
|
||||
<div class="stat-value text-warning">
|
||||
{solicitacoesQuery.data.filter((s: any) => s.status === "pendente").length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-success/20 to-success/10 shadow-lg rounded-xl border border-success/30">
|
||||
<div class="stat-title">Aprovadas</div>
|
||||
<div class="stat-value text-success">
|
||||
{solicitacoesQuery.data.filter((s: any) => s.status === "aprovado").length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-error/20 to-error/10 shadow-lg rounded-xl border border-error/30">
|
||||
<div class="stat-title">Rejeitadas</div>
|
||||
<div class="stat-value text-error">
|
||||
{solicitacoesQuery.data.filter((s: any) => s.status === "rejeitado").length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-status-solicitacao">
|
||||
<span class="label-text font-semibold">Status</span>
|
||||
</label>
|
||||
<select
|
||||
id="filtro-status-solicitacao"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={filtroStatusSolicitacao}
|
||||
>
|
||||
<option value="todas">Todas</option>
|
||||
<option value="pendente">Pendente</option>
|
||||
<option value="aprovado">Aprovado</option>
|
||||
<option value="rejeitado">Rejeitado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-nome-solicitacao">
|
||||
<span class="label-text font-semibold">Nome</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtro-nome-solicitacao"
|
||||
type="text"
|
||||
placeholder="Buscar por nome..."
|
||||
class="input input-bordered input-primary w-full"
|
||||
bind:value={filtroNomeSolicitacao}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-matricula-solicitacao">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtro-matricula-solicitacao"
|
||||
type="text"
|
||||
placeholder="Buscar por matrícula..."
|
||||
class="input input-bordered input-primary w-full"
|
||||
bind:value={filtroMatriculaSolicitacao}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela de Solicitações -->
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body p-0">
|
||||
{#if solicitacoesQuery.isLoading}
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if solicitacoesQuery.error}
|
||||
<div class="alert alert-error m-4">
|
||||
<span>Erro ao carregar solicitações: {solicitacoesQuery.error.message}</span>
|
||||
</div>
|
||||
{:else if filteredSolicitacoes().length === 0}
|
||||
<div class="text-center py-12 text-base-content/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<p class="text-lg font-semibold">Nenhuma solicitação encontrada</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Nome</th>
|
||||
<th>Matrícula</th>
|
||||
<th>E-mail</th>
|
||||
<th>Telefone</th>
|
||||
<th>Status</th>
|
||||
<th>Resposta</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredSolicitacoes() as solicitacao (solicitacao._id)}
|
||||
<tr class="hover">
|
||||
<td class="font-mono text-sm">{formatarData(solicitacao.dataSolicitacao)}</td>
|
||||
<td class="font-semibold">{solicitacao.nome}</td>
|
||||
<td><code class="bg-base-200 px-2 py-1 rounded">{solicitacao.matricula}</code></td>
|
||||
<td class="text-sm">{solicitacao.email}</td>
|
||||
<td class="text-sm">{solicitacao.telefone}</td>
|
||||
<td>
|
||||
<span class="badge {getStatusBadgeClass(solicitacao.status)} badge-lg">
|
||||
{getStatusLabel(solicitacao.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-mono text-sm">
|
||||
{solicitacao.dataResposta ? formatarData(solicitacao.dataResposta) : "-"}
|
||||
</td>
|
||||
<td>
|
||||
{#if solicitacao.status === "pendente"}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-success btn-sm"
|
||||
onclick={() => abrirModalSolicitacao(solicitacao._id, "aprovar")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Aprovar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error btn-sm"
|
||||
onclick={() => abrirModalSolicitacao(solicitacao._id, "rejeitar")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Rejeitar
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-base-content/50 text-sm italic">
|
||||
{solicitacao.observacoes || "Sem observações"}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ABA 2: USUÁRIOS -->
|
||||
{#if abaAtiva === "usuarios"}
|
||||
<!-- Estatísticas -->
|
||||
{#if usuariosQuery.data}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div class="stat bg-gradient-to-br from-primary/20 to-primary/10 shadow-lg rounded-xl border border-primary/30">
|
||||
<div class="stat-title">Total de Usuários</div>
|
||||
<div class="stat-value text-primary">{usuariosQuery.data.length}</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-success/20 to-success/10 shadow-lg rounded-xl border border-success/30">
|
||||
<div class="stat-title">Ativos</div>
|
||||
<div class="stat-value text-success">
|
||||
{usuariosQuery.data.filter((u: any) => u.ativo).length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-error/20 to-error/10 shadow-lg rounded-xl border border-error/30">
|
||||
<div class="stat-title">Inativos</div>
|
||||
<div class="stat-value text-error">
|
||||
{usuariosQuery.data.filter((u: any) => !u.ativo).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-nome-usuario">
|
||||
<span class="label-text font-semibold">Nome</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtro-nome-usuario"
|
||||
type="text"
|
||||
placeholder="Buscar por nome..."
|
||||
class="input input-bordered input-primary w-full"
|
||||
bind:value={filtroNomeUsuario}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-matricula-usuario">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtro-matricula-usuario"
|
||||
type="text"
|
||||
placeholder="Buscar por matrícula..."
|
||||
class="input input-bordered input-primary w-full"
|
||||
bind:value={filtroMatriculaUsuario}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-role-usuario">
|
||||
<span class="label-text font-semibold">Função</span>
|
||||
</label>
|
||||
<select
|
||||
id="filtro-role-usuario"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={filtroRoleUsuario}
|
||||
>
|
||||
<option value="todos">Todos</option>
|
||||
{#if rolesQuery.data}
|
||||
{#each rolesQuery.data as role}
|
||||
<option value={role.nome}>{role.nome}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-status-usuario">
|
||||
<span class="label-text font-semibold">Status</span>
|
||||
</label>
|
||||
<select
|
||||
id="filtro-status-usuario"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={filtroStatusUsuario}
|
||||
>
|
||||
<option value="todos">Todos</option>
|
||||
<option value="ativo">Ativo</option>
|
||||
<option value="inativo">Inativo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela de Usuários -->
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body p-0">
|
||||
{#if usuariosQuery.isLoading}
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if usuariosQuery.error}
|
||||
<div class="alert alert-error m-4">
|
||||
<span>Erro ao carregar usuários: {usuariosQuery.error.message}</span>
|
||||
</div>
|
||||
{:else if filteredUsuarios().length === 0}
|
||||
<div class="text-center py-12 text-base-content/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<p class="text-lg font-semibold">Nenhum usuário encontrado</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Nome</th>
|
||||
<th>Matrícula</th>
|
||||
<th>E-mail</th>
|
||||
<th>Função</th>
|
||||
<th>Nível</th>
|
||||
<th>Último Acesso</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredUsuarios() as usuario (usuario._id)}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
{#if usuario.ativo}
|
||||
<span class="badge badge-success badge-lg">🟢 Ativo</span>
|
||||
{:else}
|
||||
<span class="badge badge-error badge-lg">🔴 Inativo</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="font-semibold">{usuario.nome}</td>
|
||||
<td><code class="bg-base-200 px-2 py-1 rounded">{usuario.matricula}</code></td>
|
||||
<td class="text-sm">{usuario.email}</td>
|
||||
<td>
|
||||
<span class="badge badge-primary">{usuario.role.nome}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-outline">{usuario.role.nivel}</span>
|
||||
</td>
|
||||
<td class="font-mono text-sm">
|
||||
{usuario.ultimoAcesso ? formatarData(usuario.ultimoAcesso) : "Nunca"}
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown dropdown-end">
|
||||
<button type="button" tabindex="0" class="btn btn-ghost btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52">
|
||||
{#if usuario.ativo}
|
||||
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "desativar")} class="text-error">Desativar</button></li>
|
||||
{:else}
|
||||
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "ativar")} class="text-success">Ativar</button></li>
|
||||
{/if}
|
||||
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "alterar_role")} class="text-primary">Alterar Função/Nível</button></li>
|
||||
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "resetar")}>Resetar Senha</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ABA 3: LOGS -->
|
||||
{#if abaAtiva === "logs"}
|
||||
<!-- Estatísticas -->
|
||||
{#if logsQuery.data}
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
|
||||
<div class="stat bg-gradient-to-br from-primary/20 to-primary/10 shadow-lg rounded-xl border border-primary/30">
|
||||
<div class="stat-title">Total</div>
|
||||
<div class="stat-value text-primary text-3xl">{logsQuery.data.length}</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-success/20 to-success/10 shadow-lg rounded-xl border border-success/30">
|
||||
<div class="stat-title">Logins</div>
|
||||
<div class="stat-value text-success text-3xl">
|
||||
{logsQuery.data.filter((log: any) => log.tipo === "login").length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-info/20 to-info/10 shadow-lg rounded-xl border border-info/30">
|
||||
<div class="stat-title">Logouts</div>
|
||||
<div class="stat-value text-info text-3xl">
|
||||
{logsQuery.data.filter((log: any) => log.tipo === "logout").length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-error/20 to-error/10 shadow-lg rounded-xl border border-error/30">
|
||||
<div class="stat-title">Negados</div>
|
||||
<div class="stat-value text-error text-3xl">
|
||||
{logsQuery.data.filter((log: any) => log.tipo === "acesso_negado").length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-warning/20 to-warning/10 shadow-lg rounded-xl border border-warning/30">
|
||||
<div class="stat-title">Outros</div>
|
||||
<div class="stat-value text-warning text-3xl">
|
||||
{logsQuery.data.filter((log: any) => !["login", "logout", "acesso_negado"].includes(log.tipo)).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filtros e Ações -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="card-title text-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-error btn-sm"
|
||||
onclick={() => (modalLimparLogsAberto = true)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Limpar Histórico
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-tipo-log">
|
||||
<span class="label-text font-semibold">Tipo de Evento</span>
|
||||
</label>
|
||||
<select
|
||||
id="filtro-tipo-log"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={filtroTipoLog}
|
||||
>
|
||||
<option value="todos">Todos</option>
|
||||
<option value="login">Login</option>
|
||||
<option value="logout">Logout</option>
|
||||
<option value="acesso_negado">Acesso Negado</option>
|
||||
<option value="senha_alterada">Senha Alterada</option>
|
||||
<option value="sessao_expirada">Sessão Expirada</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-usuario-log">
|
||||
<span class="label-text font-semibold">Usuário</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtro-usuario-log"
|
||||
type="text"
|
||||
placeholder="Buscar por usuário..."
|
||||
class="input input-bordered input-primary w-full"
|
||||
bind:value={filtroUsuarioLog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela de Logs -->
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body p-0">
|
||||
{#if logsQuery.isLoading}
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if logsQuery.error}
|
||||
<div class="alert alert-error m-4">
|
||||
<span>Erro ao carregar logs: {logsQuery.error.message}</span>
|
||||
</div>
|
||||
{:else if filteredLogs().length === 0}
|
||||
<div class="text-center py-12 text-base-content/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p class="text-lg font-semibold">Nenhum log encontrado</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>Data/Hora</th>
|
||||
<th>Tipo</th>
|
||||
<th>Usuário</th>
|
||||
<th>IP</th>
|
||||
<th>Detalhes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredLogs() as log (log._id)}
|
||||
<tr class="hover">
|
||||
<td class="font-mono text-sm">{formatarData(log.timestamp)}</td>
|
||||
<td>
|
||||
<span class="badge badge-lg">
|
||||
{getTipoLogIcon(log.tipo)} {getTipoLogLabel(log.tipo)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-semibold">
|
||||
{log.usuario ? log.usuario.nome : "Sistema"}
|
||||
</td>
|
||||
<td class="font-mono text-sm">
|
||||
{log.ipAddress || "-"}
|
||||
</td>
|
||||
<td class="text-sm text-base-content/70">
|
||||
{log.detalhes || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Solicitação -->
|
||||
{#if modalSolicitacaoAberto}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
{acaoModalSolicitacao === "aprovar" ? "Aprovar Solicitação" : "Rejeitar Solicitação"}
|
||||
</h3>
|
||||
<p class="mb-4">
|
||||
{acaoModalSolicitacao === "aprovar"
|
||||
? "Tem certeza que deseja aprovar esta solicitação de acesso?"
|
||||
: "Tem certeza que deseja rejeitar esta solicitação de acesso?"}
|
||||
</p>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label" for="observacoes-solicitacao">
|
||||
<span class="label-text">Observações (opcional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="observacoes-solicitacao"
|
||||
class="textarea textarea-bordered h-24"
|
||||
placeholder="Adicione observações sobre esta decisão..."
|
||||
bind:value={observacoesSolicitacao}
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" onclick={fecharModalSolicitacao}>Cancelar</button>
|
||||
<button
|
||||
class="btn {acaoModalSolicitacao === 'aprovar' ? 'btn-success' : 'btn-error'}"
|
||||
onclick={confirmarAcaoSolicitacao}
|
||||
>
|
||||
{acaoModalSolicitacao === "aprovar" ? "Aprovar" : "Rejeitar"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={fecharModalSolicitacao}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Usuário -->
|
||||
{#if modalUsuarioAberto}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
{acaoModalUsuario === "ativar" ? "Ativar Usuário" :
|
||||
acaoModalUsuario === "desativar" ? "Desativar Usuário" :
|
||||
acaoModalUsuario === "alterar_role" ? "Alterar Função/Nível" : "Resetar Senha"}
|
||||
</h3>
|
||||
|
||||
{#if acaoModalUsuario === "alterar_role"}
|
||||
<p class="mb-4">Selecione a nova função/nível para este usuário:</p>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label" for="select-role">
|
||||
<span class="label-text font-semibold">Função</span>
|
||||
</label>
|
||||
{#if rolesQuery.isLoading}
|
||||
<div class="flex justify-center p-4">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
</div>
|
||||
{:else if rolesQuery.data && rolesQuery.data.length > 0}
|
||||
<select
|
||||
id="select-role"
|
||||
class="select select-bordered select-primary w-full"
|
||||
bind:value={novaRoleId}
|
||||
>
|
||||
{#each rolesQuery.data as role}
|
||||
<option value={role._id}>
|
||||
{role.nome} (Nível {role.nivel}){#if role.setor} - {role.setor}{/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Quanto menor o nível, maior o acesso (0 = Admin)
|
||||
</span>
|
||||
</label>
|
||||
{:else}
|
||||
<div class="alert alert-warning">
|
||||
<span>Nenhuma função disponível. Verifique se as roles foram criadas no banco de dados.</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="mb-4">
|
||||
{acaoModalUsuario === "ativar" ? "Tem certeza que deseja ativar este usuário?" :
|
||||
acaoModalUsuario === "desativar" ? "Tem certeza que deseja desativar este usuário?" :
|
||||
"Tem certeza que deseja resetar a senha deste usuário? A nova senha será 'Mudar@123'."}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" onclick={fecharModalUsuario}>Cancelar</button>
|
||||
<button
|
||||
class="btn {acaoModalUsuario === 'ativar' ? 'btn-success' : acaoModalUsuario === 'alterar_role' ? 'btn-primary' : 'btn-error'}"
|
||||
onclick={confirmarAcaoUsuario}
|
||||
disabled={acaoModalUsuario === 'alterar_role' && !novaRoleId}
|
||||
>
|
||||
Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={fecharModalUsuario}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Limpar Logs -->
|
||||
{#if modalLimparLogsAberto}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4 text-error">⚠️ Limpar Histórico de Logs</h3>
|
||||
<p class="mb-4">
|
||||
<strong>ATENÇÃO:</strong> Esta ação irá remover TODOS os logs de acesso do sistema.
|
||||
Esta ação é <strong>IRREVERSÍVEL</strong>.
|
||||
</p>
|
||||
<p class="mb-4 text-base-content/70">
|
||||
Tem certeza que deseja continuar?
|
||||
</p>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" onclick={() => (modalLimparLogsAberto = false)}>Cancelar</button>
|
||||
<button class="btn btn-error" onclick={limparLogs}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Sim, Limpar Tudo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={() => (modalLimparLogsAberto = false)}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
Reference in New Issue
Block a user