1404 lines
46 KiB
Svelte
1404 lines
46 KiB
Svelte
<script lang="ts">
|
|
import { useQuery, useConvexClient } from "convex-svelte";
|
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
|
import UserStatusBadge from "$lib/components/ti/UserStatusBadge.svelte";
|
|
import { format } from "date-fns";
|
|
import { ptBR } from "date-fns/locale";
|
|
|
|
type AvisoUsuario = {
|
|
tipo: "erro" | "aviso" | "info";
|
|
mensagem: string;
|
|
};
|
|
|
|
type RoleUsuario = {
|
|
_id: Id<"roles">;
|
|
nome: string;
|
|
nivel: number;
|
|
descricao: string;
|
|
setor?: string;
|
|
erro?: boolean;
|
|
};
|
|
|
|
type Usuario = {
|
|
_id: Id<"usuarios">;
|
|
matricula: string;
|
|
nome: string;
|
|
email: string;
|
|
ativo: boolean;
|
|
bloqueado?: boolean;
|
|
motivoBloqueio?: string;
|
|
primeiroAcesso: boolean;
|
|
ultimoAcesso?: number;
|
|
criadoEm: number;
|
|
role: RoleUsuario;
|
|
funcionario?: {
|
|
_id: Id<"funcionarios">;
|
|
nome: string;
|
|
matricula?: string;
|
|
descricaoCargo?: string;
|
|
simboloTipo: "cargo_comissionado" | "funcao_gratificada";
|
|
};
|
|
avisos?: AvisoUsuario[];
|
|
};
|
|
|
|
type Funcionario = {
|
|
_id: Id<"funcionarios">;
|
|
nome: string;
|
|
matricula?: string;
|
|
cpf?: string;
|
|
descricaoCargo?: string;
|
|
};
|
|
|
|
type Mensagem = {
|
|
tipo: "success" | "error" | "info";
|
|
texto: string;
|
|
};
|
|
|
|
const client = useConvexClient();
|
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
|
|
|
// Usar useQuery reativo - seguindo padrão usado em ti/times/+page.svelte
|
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
|
|
|
// Extrair dados e determinar estado de carregamento
|
|
const usuarios = $derived.by(() => {
|
|
// Se usuariosQuery é undefined ou null, está carregando
|
|
if (usuariosQuery === undefined || usuariosQuery === null) {
|
|
return [];
|
|
}
|
|
|
|
// Se usuariosQuery é um objeto vazio {}, está carregando
|
|
if (
|
|
typeof usuariosQuery === "object" &&
|
|
Object.keys(usuariosQuery).length === 0
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
// Se tem propriedade data, usar os dados
|
|
if ("data" in usuariosQuery && usuariosQuery.data !== undefined) {
|
|
return Array.isArray(usuariosQuery.data) ? usuariosQuery.data : [];
|
|
}
|
|
|
|
// Se usuariosQuery é diretamente um array (caso não tenha .data)
|
|
if (Array.isArray(usuariosQuery)) {
|
|
return usuariosQuery;
|
|
}
|
|
|
|
// Caso padrão: ainda carregando ou sem dados
|
|
return [];
|
|
});
|
|
|
|
const carregandoUsuarios = $derived.by(() => {
|
|
// Se é undefined/null, está carregando
|
|
if (usuariosQuery === undefined || usuariosQuery === null) {
|
|
return true;
|
|
}
|
|
|
|
// Se é um objeto vazio {}, está carregando
|
|
if (
|
|
typeof usuariosQuery === "object" &&
|
|
Object.keys(usuariosQuery).length === 0
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// Se não tem propriedade data, está carregando
|
|
if (!("data" in usuariosQuery)) {
|
|
return true;
|
|
}
|
|
|
|
// Se data é undefined, está carregando
|
|
if (usuariosQuery.data === undefined) {
|
|
return true;
|
|
}
|
|
|
|
// Caso contrário, dados estão prontos
|
|
return false;
|
|
});
|
|
|
|
let erroUsuarios = $state<string | null>(null);
|
|
|
|
// Monitorar erros e estado da query
|
|
$effect(() => {
|
|
try {
|
|
// Detectar se há erro na query
|
|
if (usuariosQuery && typeof usuariosQuery === "object") {
|
|
// Verificar se há propriedade de erro
|
|
if ("error" in usuariosQuery) {
|
|
const error = (usuariosQuery as { error?: unknown }).error;
|
|
if (error !== undefined && error !== null) {
|
|
erroUsuarios =
|
|
error instanceof Error ? error.message : String(error);
|
|
console.error("❌ [ERROR] Erro na query de usuários:", error);
|
|
} else {
|
|
// Se error existe mas é undefined/null, limpar erro
|
|
erroUsuarios = null;
|
|
}
|
|
} else if ("data" in usuariosQuery) {
|
|
const queryData = usuariosQuery as { data?: Usuario[] };
|
|
if (queryData.data !== undefined) {
|
|
// Se tem dados, limpar erro
|
|
erroUsuarios = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debug para identificar problemas (apenas em desenvolvimento)
|
|
if (
|
|
typeof window !== "undefined" &&
|
|
window.location.hostname === "localhost"
|
|
) {
|
|
console.log("🔍 [DEBUG] usuariosQuery:", usuariosQuery);
|
|
console.log("🔍 [DEBUG] typeof usuariosQuery:", typeof usuariosQuery);
|
|
if (usuariosQuery && typeof usuariosQuery === "object") {
|
|
console.log(
|
|
"🔍 [DEBUG] Object.keys(usuariosQuery):",
|
|
Object.keys(usuariosQuery),
|
|
);
|
|
console.log(
|
|
"🔍 [DEBUG] usuariosQuery?.data:",
|
|
(usuariosQuery as { data?: unknown }).data,
|
|
);
|
|
console.log(
|
|
"🔍 [DEBUG] 'data' in usuariosQuery:",
|
|
"data" in usuariosQuery,
|
|
);
|
|
console.log(
|
|
"🔍 [DEBUG] 'error' in usuariosQuery:",
|
|
"error" in usuariosQuery,
|
|
);
|
|
}
|
|
console.log("🔍 [DEBUG] usuarios (extraídos):", usuarios);
|
|
console.log("🔍 [DEBUG] quantidade:", usuarios.length);
|
|
console.log("🔍 [DEBUG] carregandoUsuarios:", carregandoUsuarios);
|
|
console.log("🔍 [DEBUG] erroUsuarios:", erroUsuarios);
|
|
}
|
|
} catch (error) {
|
|
// Tratamento de erro no próprio effect
|
|
console.error("❌ [ERROR] Erro ao processar estado da query:", error);
|
|
if (error instanceof Error) {
|
|
erroUsuarios = `Erro inesperado: ${error.message}`;
|
|
}
|
|
}
|
|
});
|
|
|
|
let funcionarios = $state<Funcionario[]>([]);
|
|
let carregandoFuncionarios = $state(false);
|
|
|
|
let usuarioSelecionado = $state<Usuario | null>(null);
|
|
let modalAssociarAberto = $state(false);
|
|
let funcionarioSelecionadoId = $state<Id<"funcionarios"> | "">("");
|
|
let buscaFuncionario = $state("");
|
|
|
|
let modalExcluirAberto = $state(false);
|
|
let processando = $state(false);
|
|
let mensagem = $state<Mensagem | null>(null);
|
|
|
|
// Filtros
|
|
let filtroNome = $state("");
|
|
let filtroMatricula = $state("");
|
|
let filtroSetor = $state("");
|
|
let filtroStatus = $state<"todos" | "ativo" | "inativo" | "bloqueado">(
|
|
"todos",
|
|
);
|
|
let filtroDataCriacaoInicio = $state("");
|
|
let filtroDataCriacaoFim = $state("");
|
|
let filtroUltimoAcessoInicio = $state("");
|
|
let filtroUltimoAcessoFim = $state("");
|
|
|
|
// Função para recarregar usuários (mantida para compatibilidade com mutações)
|
|
async function carregarUsuarios() {
|
|
// Como estamos usando useQuery reativo, os dados serão atualizados automaticamente
|
|
// Esta função pode ser chamada após mutações, mas não precisa fazer nada
|
|
// O useQuery já atualizará automaticamente
|
|
}
|
|
|
|
async function carregarFuncionarios() {
|
|
if (carregandoFuncionarios) return;
|
|
carregandoFuncionarios = true;
|
|
|
|
try {
|
|
const resposta = await client.query(api.funcionarios.getAll, {});
|
|
// Suportar ambos formatos (array direto ou objeto com .data), dependendo do hook/cliente
|
|
const lista = Array.isArray(resposta)
|
|
? resposta
|
|
: resposta &&
|
|
typeof resposta === "object" &&
|
|
Array.isArray((resposta as { data?: unknown }).data)
|
|
? (resposta as { data: unknown[] }).data
|
|
: [];
|
|
|
|
funcionarios = lista as Funcionario[];
|
|
} catch (error: unknown) {
|
|
console.error("Erro ao carregar funcionários:", error);
|
|
funcionarios = [];
|
|
} finally {
|
|
carregandoFuncionarios = false;
|
|
}
|
|
}
|
|
|
|
function mostrarMensagem(tipo: Mensagem["tipo"], texto: string) {
|
|
mensagem = { tipo, texto };
|
|
setTimeout(() => {
|
|
mensagem = null;
|
|
}, 4000);
|
|
}
|
|
|
|
const funcionariosFiltrados = $derived.by(() => {
|
|
if (!buscaFuncionario) {
|
|
return [...funcionarios].sort((a, b) => a.nome.localeCompare(b.nome));
|
|
}
|
|
|
|
const buscaNormalizada = buscaFuncionario.toLowerCase();
|
|
return funcionarios
|
|
.filter((f) => {
|
|
return (
|
|
f.nome.toLowerCase().includes(buscaNormalizada) ||
|
|
f.cpf?.toLowerCase().includes(buscaNormalizada) ||
|
|
f.matricula?.toLowerCase().includes(buscaNormalizada)
|
|
);
|
|
})
|
|
.sort((a, b) => a.nome.localeCompare(b.nome));
|
|
});
|
|
|
|
// Verificar se há usuários com problemas
|
|
const usuariosComProblemas = $derived.by(() => {
|
|
return usuarios.filter(
|
|
(u) => u.role?.erro === true || (u.avisos && u.avisos.length > 0),
|
|
);
|
|
});
|
|
|
|
// Lógica de filtros reativos
|
|
const usuariosFiltrados = $derived.by(() => {
|
|
let resultado = usuarios;
|
|
|
|
// Filtro por nome
|
|
if (filtroNome.trim()) {
|
|
const buscaNome = filtroNome.toLowerCase();
|
|
resultado = resultado.filter((u) =>
|
|
u.nome.toLowerCase().includes(buscaNome),
|
|
);
|
|
}
|
|
|
|
// Filtro por matrícula
|
|
if (filtroMatricula.trim()) {
|
|
const buscaMatricula = filtroMatricula.toLowerCase();
|
|
resultado = resultado.filter((u) =>
|
|
u.matricula.toLowerCase().includes(buscaMatricula),
|
|
);
|
|
}
|
|
|
|
// Filtro por setor
|
|
if (filtroSetor) {
|
|
resultado = resultado.filter((u) => u.role.setor === filtroSetor);
|
|
}
|
|
|
|
// Filtro por status
|
|
if (filtroStatus !== "todos") {
|
|
if (filtroStatus === "ativo") {
|
|
resultado = resultado.filter((u) => u.ativo && !u.bloqueado);
|
|
} else if (filtroStatus === "inativo") {
|
|
resultado = resultado.filter((u) => !u.ativo);
|
|
} else if (filtroStatus === "bloqueado") {
|
|
resultado = resultado.filter((u) => u.bloqueado);
|
|
}
|
|
}
|
|
|
|
// Filtro por data de criação
|
|
if (filtroDataCriacaoInicio) {
|
|
const dataInicio = new Date(filtroDataCriacaoInicio);
|
|
dataInicio.setHours(0, 0, 0, 0);
|
|
resultado = resultado.filter((u) => u.criadoEm >= dataInicio.getTime());
|
|
}
|
|
if (filtroDataCriacaoFim) {
|
|
const dataFim = new Date(filtroDataCriacaoFim);
|
|
dataFim.setHours(23, 59, 59, 999);
|
|
resultado = resultado.filter((u) => u.criadoEm <= dataFim.getTime());
|
|
}
|
|
|
|
// Filtro por último acesso
|
|
if (filtroUltimoAcessoInicio) {
|
|
const dataInicio = new Date(filtroUltimoAcessoInicio);
|
|
dataInicio.setHours(0, 0, 0, 0);
|
|
resultado = resultado.filter((u) => {
|
|
if (!u.ultimoAcesso) return false;
|
|
return u.ultimoAcesso >= dataInicio.getTime();
|
|
});
|
|
}
|
|
if (filtroUltimoAcessoFim) {
|
|
const dataFim = new Date(filtroUltimoAcessoFim);
|
|
dataFim.setHours(23, 59, 59, 999);
|
|
resultado = resultado.filter((u) => {
|
|
if (!u.ultimoAcesso) return false;
|
|
return u.ultimoAcesso <= dataFim.getTime();
|
|
});
|
|
}
|
|
|
|
return resultado;
|
|
});
|
|
|
|
function formatarData(timestamp: number | undefined): string {
|
|
if (!timestamp) return "Nunca";
|
|
try {
|
|
return format(new Date(timestamp), "dd/MM/yyyy HH:mm", { locale: ptBR });
|
|
} catch {
|
|
return "Data inválida";
|
|
}
|
|
}
|
|
|
|
function limparFiltros() {
|
|
filtroNome = "";
|
|
filtroMatricula = "";
|
|
filtroSetor = "";
|
|
filtroStatus = "todos";
|
|
filtroDataCriacaoInicio = "";
|
|
filtroDataCriacaoFim = "";
|
|
filtroUltimoAcessoInicio = "";
|
|
filtroUltimoAcessoFim = "";
|
|
}
|
|
|
|
function obterSetoresDisponiveis(): string[] {
|
|
const setoresSet = new Set<string>();
|
|
usuarios.forEach((u) => {
|
|
if (u.role.setor) {
|
|
setoresSet.add(u.role.setor);
|
|
}
|
|
});
|
|
return Array.from(setoresSet).sort();
|
|
}
|
|
|
|
async function ativarDesativarUsuario(usuario: Usuario) {
|
|
const confirmar = window.confirm(
|
|
`Deseja realmente ${usuario.ativo ? "desativar" : "ativar"} o usuário ${usuario.nome}?`,
|
|
);
|
|
if (!confirmar) return;
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.usuarios.alterarStatus, {
|
|
usuarioId: usuario._id,
|
|
ativo: !usuario.ativo,
|
|
});
|
|
|
|
mostrarMensagem(
|
|
"success",
|
|
`Usuário ${usuario.ativo ? "desativado" : "ativado"} com sucesso!`,
|
|
);
|
|
await carregarUsuarios();
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Erro ao alterar status do usuário.";
|
|
mostrarMensagem("error", message);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function abrirModalAssociar(usuario: Usuario) {
|
|
usuarioSelecionado = usuario;
|
|
funcionarioSelecionadoId = usuario.funcionario?._id ?? "";
|
|
buscaFuncionario = "";
|
|
modalAssociarAberto = true;
|
|
|
|
if (funcionarios.length === 0 && !carregandoFuncionarios) {
|
|
void carregarFuncionarios();
|
|
}
|
|
}
|
|
|
|
function fecharModalAssociar() {
|
|
modalAssociarAberto = false;
|
|
usuarioSelecionado = null;
|
|
funcionarioSelecionadoId = "";
|
|
buscaFuncionario = "";
|
|
}
|
|
|
|
async function associarFuncionario() {
|
|
if (
|
|
!usuarioSelecionado ||
|
|
!funcionarioSelecionadoId ||
|
|
funcionarioSelecionadoId === ""
|
|
) {
|
|
return;
|
|
}
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.usuarios.associarFuncionario, {
|
|
usuarioId: usuarioSelecionado._id,
|
|
funcionarioId: funcionarioSelecionadoId,
|
|
});
|
|
|
|
mostrarMensagem("success", "Funcionário associado com sucesso!");
|
|
fecharModalAssociar();
|
|
await carregarUsuarios();
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Não foi possível associar o funcionário.";
|
|
mostrarMensagem("error", message);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function desassociarFuncionario() {
|
|
if (!usuarioSelecionado) {
|
|
return;
|
|
}
|
|
|
|
const confirmar = window.confirm(
|
|
"Deseja realmente desassociar o funcionário deste usuário?",
|
|
);
|
|
if (!confirmar) return;
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.usuarios.desassociarFuncionario, {
|
|
usuarioId: usuarioSelecionado._id,
|
|
});
|
|
|
|
mostrarMensagem("success", "Funcionário desassociado com sucesso!");
|
|
fecharModalAssociar();
|
|
await carregarUsuarios();
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Não foi possível desassociar o funcionário.";
|
|
mostrarMensagem("error", message);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function bloquearUsuario(usuario: Usuario) {
|
|
const motivo = window.prompt(
|
|
"Digite o motivo do bloqueio:",
|
|
usuario.motivoBloqueio || "",
|
|
);
|
|
|
|
if (!motivo || motivo.trim() === "") {
|
|
return;
|
|
}
|
|
|
|
if (!currentUser?.data) {
|
|
mostrarMensagem("error", "Usuário não autenticado");
|
|
return;
|
|
}
|
|
|
|
processando = true;
|
|
try {
|
|
const resultado = await client.mutation(api.usuarios.bloquearUsuario, {
|
|
usuarioId: usuario._id,
|
|
motivo: motivo.trim(),
|
|
bloqueadoPorId: currentUser.data._id as Id<"usuarios">,
|
|
});
|
|
|
|
if (resultado.sucesso) {
|
|
mostrarMensagem("success", "Usuário bloqueado com sucesso!");
|
|
await carregarUsuarios();
|
|
} else {
|
|
mostrarMensagem("error", resultado.erro);
|
|
}
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Erro ao bloquear usuário.";
|
|
mostrarMensagem("error", message);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function desbloquearUsuario(usuario: Usuario) {
|
|
if (!currentUser?.data) {
|
|
mostrarMensagem("error", "Usuário não autenticado");
|
|
return;
|
|
}
|
|
|
|
const confirmar = window.confirm(
|
|
`Deseja realmente desbloquear o usuário ${usuario.nome}?`,
|
|
);
|
|
if (!confirmar) return;
|
|
|
|
processando = true;
|
|
try {
|
|
const resultado = await client.mutation(api.usuarios.desbloquearUsuario, {
|
|
usuarioId: usuario._id,
|
|
desbloqueadoPorId: currentUser.data._id as Id<"usuarios">,
|
|
});
|
|
|
|
if (resultado.sucesso) {
|
|
mostrarMensagem("success", "Usuário desbloqueado com sucesso!");
|
|
await carregarUsuarios();
|
|
} else {
|
|
mostrarMensagem("error", resultado.erro);
|
|
}
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Erro ao desbloquear usuário.";
|
|
mostrarMensagem("error", message);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function abrirModalExcluir(usuario: Usuario) {
|
|
usuarioSelecionado = usuario;
|
|
modalExcluirAberto = true;
|
|
}
|
|
|
|
function fecharModalExcluir() {
|
|
modalExcluirAberto = false;
|
|
usuarioSelecionado = null;
|
|
}
|
|
|
|
async function excluirUsuario() {
|
|
if (!usuarioSelecionado) {
|
|
return;
|
|
}
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.usuarios.excluir, {
|
|
usuarioId: usuarioSelecionado._id,
|
|
});
|
|
|
|
mostrarMensagem("success", "Usuário excluído com sucesso!");
|
|
fecharModalExcluir();
|
|
await carregarUsuarios();
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Erro ao excluir usuário.";
|
|
mostrarMensagem("error", message);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<ProtectedRoute
|
|
allowedRoles={["ti_master", "admin", "ti_usuario"]}
|
|
maxLevel={3}
|
|
>
|
|
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div class="flex items-center gap-4">
|
|
<div class="p-3 bg-primary/10 rounded-xl">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8 text-primary"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-base-content">
|
|
Gestão de Usuários
|
|
</h1>
|
|
<p class="text-base-content/60 mt-1">
|
|
Administre os usuários do sistema
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<a href="/ti/usuarios/criar" class="btn btn-primary">
|
|
<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 4v16m8-8H4"
|
|
/>
|
|
</svg>
|
|
Criar Usuário
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alerta de Usuários com Problemas -->
|
|
{#if !carregandoUsuarios && usuariosComProblemas.length > 0}
|
|
<div class="alert alert-warning shadow-lg 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"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
<div>
|
|
<h3 class="font-bold">Atenção: Usuários com Problemas Detectados</h3>
|
|
<div class="text-sm mt-2">
|
|
<p>
|
|
{usuariosComProblemas.length} usuário(s) possui(em) problemas que requerem
|
|
atenção:
|
|
</p>
|
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
|
{#each usuariosComProblemas.slice(0, 3) as usuario}
|
|
<li>
|
|
<strong>{usuario.nome}</strong> ({usuario.matricula})
|
|
{#if usuario.avisos && usuario.avisos.length > 0}
|
|
- {usuario.avisos[0].mensagem}
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
{#if usuariosComProblemas.length > 3}
|
|
<li class="text-base-content/60">
|
|
... e mais {usuariosComProblemas.length - 3} usuário(s)
|
|
</li>
|
|
{/if}
|
|
</ul>
|
|
<p class="mt-2 font-semibold">
|
|
Por favor, corrija os perfis desses usuários para garantir acesso
|
|
adequado ao sistema.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Mensagens -->
|
|
{#if mensagem}
|
|
<div
|
|
class="alert shadow-lg"
|
|
class:alert-success={mensagem.tipo === "success"}
|
|
class:alert-error={mensagem.tipo === "error"}
|
|
class:alert-info={mensagem.tipo === "info"}
|
|
>
|
|
{#if mensagem.tipo === "success"}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="stroke-current shrink-0 h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<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"
|
|
/>
|
|
</svg>
|
|
{:else if mensagem.tipo === "error"}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="stroke-current shrink-0 h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<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"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="stroke-current shrink-0 h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
<span class="font-semibold">{mensagem.texto}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Filtros -->
|
|
{#if !carregandoUsuarios && usuarios.length > 0}
|
|
<div class="card bg-base-100 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="card-title">Filtros de Busca</h2>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline"
|
|
onclick={limparFiltros}
|
|
>
|
|
<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>
|
|
Limpar Filtros
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<!-- Nome -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-nome">
|
|
<span class="label-text font-medium">Nome</span>
|
|
</label>
|
|
<input
|
|
id="filtro-nome"
|
|
type="text"
|
|
bind:value={filtroNome}
|
|
placeholder="Buscar por nome..."
|
|
class="input input-bordered input-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Matrícula -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-matricula">
|
|
<span class="label-text font-medium">Matrícula</span>
|
|
</label>
|
|
<input
|
|
id="filtro-matricula"
|
|
type="text"
|
|
bind:value={filtroMatricula}
|
|
placeholder="Buscar por matrícula..."
|
|
class="input input-bordered input-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Setor -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-setor">
|
|
<span class="label-text font-medium">Setor</span>
|
|
</label>
|
|
<select
|
|
id="filtro-setor"
|
|
bind:value={filtroSetor}
|
|
class="select select-bordered select-sm"
|
|
>
|
|
<option value="">Todos os setores</option>
|
|
{#each obterSetoresDisponiveis() as setor}
|
|
<option value={setor}>{setor}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-status">
|
|
<span class="label-text font-medium">Status</span>
|
|
</label>
|
|
<select
|
|
id="filtro-status"
|
|
bind:value={filtroStatus}
|
|
class="select select-bordered select-sm"
|
|
>
|
|
<option value="todos">Todos</option>
|
|
<option value="ativo">Ativo</option>
|
|
<option value="inativo">Inativo</option>
|
|
<option value="bloqueado">Bloqueado</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Data de Criação Início -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-data-criacao-inicio">
|
|
<span class="label-text font-medium"
|
|
>Data de Criação (Início)</span
|
|
>
|
|
</label>
|
|
<input
|
|
id="filtro-data-criacao-inicio"
|
|
type="date"
|
|
bind:value={filtroDataCriacaoInicio}
|
|
class="input input-bordered input-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Data de Criação Fim -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-data-criacao-fim">
|
|
<span class="label-text font-medium">Data de Criação (Fim)</span
|
|
>
|
|
</label>
|
|
<input
|
|
id="filtro-data-criacao-fim"
|
|
type="date"
|
|
bind:value={filtroDataCriacaoFim}
|
|
class="input input-bordered input-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Último Acesso Início -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-ultimo-acesso-inicio">
|
|
<span class="label-text font-medium"
|
|
>Último Acesso (Início)</span
|
|
>
|
|
</label>
|
|
<input
|
|
id="filtro-ultimo-acesso-inicio"
|
|
type="date"
|
|
bind:value={filtroUltimoAcessoInicio}
|
|
class="input input-bordered input-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Último Acesso Fim -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtro-ultimo-acesso-fim">
|
|
<span class="label-text font-medium">Último Acesso (Fim)</span>
|
|
</label>
|
|
<input
|
|
id="filtro-ultimo-acesso-fim"
|
|
type="date"
|
|
bind:value={filtroUltimoAcessoFim}
|
|
class="input input-bordered input-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 text-sm text-base-content/60">
|
|
Mostrando {usuariosFiltrados.length} de {usuarios.length} usuário(s)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Lista de Usuários -->
|
|
{#if carregandoUsuarios}
|
|
<div class="flex flex-col justify-center items-center py-20">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
<p class="mt-4 text-base-content/60">Carregando usuários...</p>
|
|
</div>
|
|
{:else if erroUsuarios}
|
|
<div class="alert alert-error shadow-lg">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="stroke-current shrink-0 h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<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"
|
|
/>
|
|
</svg>
|
|
<div>
|
|
<h3 class="font-bold">Erro ao carregar usuários</h3>
|
|
<div class="text-sm mt-1">{erroUsuarios}</div>
|
|
<div class="text-xs mt-2">
|
|
Por favor, recarregue a página ou entre em contato com o suporte
|
|
técnico se o problema persistir.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if usuarios.length === 0}
|
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-16 w-16 text-base-content/30"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
/>
|
|
</svg>
|
|
<h3 class="text-xl font-semibold mt-4">Nenhum usuário encontrado</h3>
|
|
<p class="text-base-content/60 mt-2">
|
|
Cadastre um usuário para começar a gestão de acessos.
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl mb-4">Usuários ({usuarios.length})</h2>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th>Matrícula</th>
|
|
<th>Nome</th>
|
|
<th>Email</th>
|
|
<th>Role/Perfil</th>
|
|
<th>Setor</th>
|
|
<th>Funcionário Vinculado</th>
|
|
<th>Status</th>
|
|
<th>Primeiro Acesso</th>
|
|
<th>Último Acesso</th>
|
|
<th>Data de Criação</th>
|
|
<th>Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each usuariosFiltrados as usuario}
|
|
<tr>
|
|
<td class="font-mono font-semibold">{usuario.matricula}</td>
|
|
<td class="font-semibold">{usuario.nome}</td>
|
|
<td>{usuario.email}</td>
|
|
<td>
|
|
<div class="space-y-1">
|
|
{#if usuario.role.erro}
|
|
<div class="badge badge-error gap-1">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-3 w-3"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
{usuario.role.descricao}
|
|
</div>
|
|
{#if usuario.avisos && usuario.avisos.length > 0}
|
|
<div
|
|
class="tooltip tooltip-error"
|
|
data-tip={usuario.avisos[0].mensagem}
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-error btn-circle"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
mostrarMensagem(
|
|
"error",
|
|
usuario.avisos![0].mensagem,
|
|
);
|
|
}}
|
|
>
|
|
!
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<div class="badge badge-outline">
|
|
{usuario.role.nome}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</td>
|
|
<td>{usuario.role.setor || "-"}</td>
|
|
<td>
|
|
{#if usuario.funcionario}
|
|
<div class="space-y-1">
|
|
<div class="badge badge-success gap-2">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-3 w-3"
|
|
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>
|
|
Associado
|
|
</div>
|
|
<div class="text-sm font-medium">
|
|
{usuario.funcionario.nome}
|
|
</div>
|
|
{#if usuario.funcionario.matricula}
|
|
<div class="text-xs text-base-content/60">
|
|
Mat: {usuario.funcionario.matricula}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="badge badge-warning gap-2">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-3 w-3"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
Não associado
|
|
</div>
|
|
{/if}
|
|
</td>
|
|
<td>
|
|
<UserStatusBadge
|
|
ativo={usuario.ativo}
|
|
bloqueado={usuario.bloqueado}
|
|
/>
|
|
</td>
|
|
<td>
|
|
{#if usuario.primeiroAcesso}
|
|
<div class="badge badge-warning">Sim</div>
|
|
{:else}
|
|
<div class="badge badge-success">Não</div>
|
|
{/if}
|
|
</td>
|
|
<td>
|
|
<span class="text-sm"
|
|
>{formatarData(usuario.ultimoAcesso)}</span
|
|
>
|
|
</td>
|
|
<td>
|
|
<span class="text-sm"
|
|
>{formatarData(usuario.criadoEm)}</span
|
|
>
|
|
</td>
|
|
<td>
|
|
<div class="dropdown dropdown-end">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-ghost"
|
|
aria-label="Menu de ações"
|
|
>
|
|
<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
|
|
role="menu"
|
|
class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-48 z-[1]"
|
|
>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
onclick={() => ativarDesativarUsuario(usuario)}
|
|
disabled={processando}
|
|
>
|
|
{usuario.ativo ? "Desativar" : "Ativar"}
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
onclick={() => abrirModalAssociar(usuario)}
|
|
disabled={processando}
|
|
>
|
|
{usuario.funcionario
|
|
? "Alterar Funcionário"
|
|
: "Associar Funcionário"}
|
|
</button>
|
|
</li>
|
|
<li>
|
|
{#if usuario.bloqueado}
|
|
<button
|
|
type="button"
|
|
onclick={() => desbloquearUsuario(usuario)}
|
|
disabled={processando}
|
|
class="text-success"
|
|
>
|
|
Desbloquear
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
onclick={() => bloquearUsuario(usuario)}
|
|
disabled={processando}
|
|
class="text-error"
|
|
>
|
|
Bloquear
|
|
</button>
|
|
{/if}
|
|
</li>
|
|
<div class="divider my-0"></div>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
onclick={() => abrirModalExcluir(usuario)}
|
|
disabled={processando}
|
|
class="text-error"
|
|
>
|
|
Excluir
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Modal Associar Funcionário -->
|
|
{#if modalAssociarAberto && usuarioSelecionado}
|
|
<div class="modal modal-open">
|
|
<div class="modal-box max-w-2xl">
|
|
<h3 class="font-bold text-lg mb-4">Associar Funcionário ao Usuário</h3>
|
|
|
|
<div class="mb-6">
|
|
<p class="text-base-content/80 mb-2">
|
|
<strong>Usuário:</strong>
|
|
{usuarioSelecionado.nome} ({usuarioSelecionado.matricula})
|
|
</p>
|
|
|
|
{#if usuarioSelecionado.funcionario}
|
|
<div class="alert alert-info">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
class="stroke-current shrink-0 w-6 h-6"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
></path>
|
|
</svg>
|
|
<span>
|
|
Este usuário já possui um funcionário associado. Você pode
|
|
alterá-lo ou desassociá-lo.
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="form-control mb-4">
|
|
<label for="busca-funcionario" class="label">
|
|
<span class="label-text">Buscar Funcionário</span>
|
|
</label>
|
|
<input
|
|
id="busca-funcionario"
|
|
type="text"
|
|
bind:value={buscaFuncionario}
|
|
placeholder="Digite nome, CPF ou matrícula..."
|
|
class="input input-bordered"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control mb-6">
|
|
<div class="label">
|
|
<span class="label-text">Selecione o Funcionário *</span>
|
|
</div>
|
|
|
|
<div class="border rounded-lg max-h-96 overflow-y-auto">
|
|
{#if carregandoFuncionarios}
|
|
<div
|
|
class="p-4 flex items-center justify-center gap-3 text-base-content/60"
|
|
>
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Carregando funcionários disponíveis...
|
|
</div>
|
|
{:else if funcionariosFiltrados.length === 0}
|
|
<div class="p-4 text-center text-base-content/60">
|
|
{buscaFuncionario
|
|
? "Nenhum funcionário encontrado com esse critério"
|
|
: "Nenhum funcionário disponível para associação."}
|
|
</div>
|
|
{:else}
|
|
{#each funcionariosFiltrados as func}
|
|
<label
|
|
class="flex items-center gap-3 p-3 hover:bg-base-200 cursor-pointer border-b last:border-b-0"
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="funcionario"
|
|
value={func._id}
|
|
bind:group={funcionarioSelecionadoId}
|
|
class="radio radio-primary"
|
|
/>
|
|
<div class="flex-1">
|
|
<div class="font-semibold">{func.nome}</div>
|
|
<div class="text-sm text-base-content/70">
|
|
CPF: {func.cpf || "N/A"}
|
|
{#if func.matricula}
|
|
| Matrícula: {func.matricula}
|
|
{/if}
|
|
</div>
|
|
{#if func.descricaoCargo}
|
|
<div class="text-xs text-base-content/60">
|
|
{func.descricaoCargo}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</label>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost"
|
|
onclick={fecharModalAssociar}
|
|
disabled={processando}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
|
|
{#if usuarioSelecionado.funcionario}
|
|
<button
|
|
type="button"
|
|
class="btn btn-error"
|
|
onclick={desassociarFuncionario}
|
|
disabled={processando}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
Desassociar
|
|
</button>
|
|
{/if}
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick={associarFuncionario}
|
|
disabled={processando ||
|
|
!funcionarioSelecionadoId ||
|
|
funcionarioSelecionadoId === ""}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
{usuarioSelecionado.funcionario ? "Alterar" : "Associar"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop">
|
|
<button
|
|
type="button"
|
|
onclick={fecharModalAssociar}
|
|
onkeydown={(e) => e.key === "Escape" && fecharModalAssociar()}
|
|
aria-label="Fechar modal"
|
|
class="sr-only">Fechar</button
|
|
>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Modal Excluir Usuário -->
|
|
{#if modalExcluirAberto && usuarioSelecionado}
|
|
<div class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg mb-4 text-error">Excluir Usuário</h3>
|
|
|
|
<div class="mb-4">
|
|
<p class="text-base-content/80 mb-2">
|
|
<strong>Usuário:</strong>
|
|
{usuarioSelecionado.nome} ({usuarioSelecionado.matricula})
|
|
</p>
|
|
<div class="alert alert-error">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
class="stroke-current shrink-0 w-6 h-6"
|
|
>
|
|
<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"
|
|
></path>
|
|
</svg>
|
|
<span>
|
|
Esta ação não pode ser desfeita. O usuário será permanentemente
|
|
excluído do sistema.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost"
|
|
onclick={fecharModalExcluir}
|
|
disabled={processando}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-error"
|
|
onclick={excluirUsuario}
|
|
disabled={processando}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop">
|
|
<button
|
|
type="button"
|
|
onclick={fecharModalExcluir}
|
|
onkeydown={(e) => e.key === "Escape" && fecharModalExcluir()}
|
|
aria-label="Fechar modal"
|
|
class="sr-only">Fechar</button
|
|
>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</ProtectedRoute>
|