1314 lines
39 KiB
Svelte
1314 lines
39 KiB
Svelte
<script lang="ts">
|
|
import { resolve } from '$app/paths';
|
|
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';
|
|
import {
|
|
Users,
|
|
Plus,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
XCircle,
|
|
Info,
|
|
X,
|
|
Check,
|
|
UserPlus,
|
|
Search,
|
|
MoreVertical,
|
|
Copy,
|
|
CheckCircle2
|
|
} from 'lucide-svelte';
|
|
|
|
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 modalResetarSenhaAberto = $state(false);
|
|
let novaSenhaGerada = $state<string | null>(null);
|
|
let senhaCopiada = $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;
|
|
}
|
|
|
|
function abrirModalResetarSenha(usuario: Usuario) {
|
|
usuarioSelecionado = usuario;
|
|
novaSenhaGerada = null;
|
|
modalResetarSenhaAberto = true;
|
|
}
|
|
|
|
function fecharModalResetarSenha() {
|
|
modalResetarSenhaAberto = false;
|
|
novaSenhaGerada = null;
|
|
senhaCopiada = false;
|
|
usuarioSelecionado = null;
|
|
}
|
|
|
|
async function copiarSenha() {
|
|
if (!novaSenhaGerada) return;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(novaSenhaGerada);
|
|
senhaCopiada = true;
|
|
setTimeout(() => {
|
|
senhaCopiada = false;
|
|
}, 2000);
|
|
} catch (error) {
|
|
console.error('Erro ao copiar senha:', error);
|
|
}
|
|
}
|
|
|
|
async function resetarSenha() {
|
|
if (!usuarioSelecionado || !currentUser?.data) {
|
|
console.error('Erro: usuarioSelecionado ou currentUser não definido');
|
|
mostrarMensagem('error', 'Erro: dados do usuário não encontrados');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
processando = true;
|
|
console.log('Iniciando reset de senha para:', usuarioSelecionado._id);
|
|
|
|
const resultado = await client.mutation(api.usuarios.resetarSenhaUsuario, {
|
|
usuarioId: usuarioSelecionado._id,
|
|
resetadoPorId: currentUser.data._id
|
|
});
|
|
|
|
console.log('Resultado do reset:', resultado);
|
|
|
|
if (resultado && resultado.sucesso) {
|
|
novaSenhaGerada = resultado.senhaTemporaria;
|
|
mostrarMensagem('success', 'Senha resetada com sucesso! Email enviado ao usuário.');
|
|
} else {
|
|
const erroMsg = resultado?.erro || 'Erro ao resetar senha';
|
|
console.error('Erro no reset:', erroMsg);
|
|
mostrarMensagem('error', erroMsg);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao resetar senha:', error);
|
|
const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido ao resetar senha';
|
|
mostrarMensagem('error', errorMessage);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
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={1}>
|
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
|
<!-- Header -->
|
|
<div class="mb-6 flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div class="bg-primary/10 rounded-xl p-3">
|
|
<Users class="text-primary h-8 w-8" strokeWidth={2} />
|
|
</div>
|
|
<div>
|
|
<h1 class="text-base-content text-3xl font-bold">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={resolve('/ti/usuarios/criar')} class="btn btn-primary">
|
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
|
Criar Usuário
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alerta de Usuários com Problemas -->
|
|
{#if !carregandoUsuarios && usuariosComProblemas.length > 0}
|
|
<div class="alert alert-warning mb-6 shadow-lg">
|
|
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div>
|
|
<h3 class="font-bold">Atenção: Usuários com Problemas Detectados</h3>
|
|
<div class="mt-2 text-sm">
|
|
<p>
|
|
{usuariosComProblemas.length} usuário(s) possui(em) problemas que requerem atenção:
|
|
</p>
|
|
<ul class="mt-2 list-inside list-disc 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'}
|
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{:else if mensagem.tipo === 'error'}
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{:else}
|
|
<Info class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{/if}
|
|
<span class="font-semibold">{mensagem.texto}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Filtros -->
|
|
{#if !carregandoUsuarios && usuarios.length > 0}
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="mb-4 flex items-center justify-between">
|
|
<h2 class="card-title">Filtros de Busca</h2>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick={limparFiltros}>
|
|
<X class="h-4 w-4" strokeWidth={2} />
|
|
Limpar Filtros
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
<!-- 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="text-base-content/60 mt-4 text-sm">
|
|
Mostrando {usuariosFiltrados.length} de {usuarios.length} usuário(s)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Lista de Usuários -->
|
|
{#if carregandoUsuarios}
|
|
<div class="flex flex-col items-center justify-center py-20">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
<p class="text-base-content/60 mt-4">Carregando usuários...</p>
|
|
</div>
|
|
{:else if erroUsuarios}
|
|
<div class="alert alert-error shadow-lg">
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div>
|
|
<h3 class="font-bold">Erro ao carregar usuários</h3>
|
|
<div class="mt-1 text-sm">{erroUsuarios}</div>
|
|
<div class="mt-2 text-xs">
|
|
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">
|
|
<Users class="text-base-content/30 h-16 w-16" strokeWidth={2} />
|
|
<h3 class="mt-4 text-xl font-semibold">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 mb-4 text-2xl">Usuários ({usuarios.length})</h2>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="table-zebra table">
|
|
<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">
|
|
<AlertTriangle class="h-3 w-3" strokeWidth={2} />
|
|
{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">
|
|
<Check class="h-3 w-3" strokeWidth={2} />
|
|
Associado
|
|
</div>
|
|
<div class="text-sm font-medium">
|
|
{usuario.funcionario.nome}
|
|
</div>
|
|
{#if usuario.funcionario.matricula}
|
|
<div class="text-base-content/60 text-xs">
|
|
Mat: {usuario.funcionario.matricula}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="badge badge-warning gap-2">
|
|
<AlertTriangle class="h-3 w-3" strokeWidth={2} />
|
|
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"
|
|
>
|
|
<MoreVertical class="h-5 w-5" strokeWidth={2} />
|
|
</button>
|
|
<ul
|
|
role="menu"
|
|
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-48 p-2 shadow"
|
|
>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
onclick={() => abrirModalResetarSenha(usuario)}
|
|
disabled={processando}
|
|
class="text-warning"
|
|
>
|
|
Resetar Senha
|
|
</button>
|
|
</li>
|
|
<div class="divider my-0"></div>
|
|
<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="mb-4 text-lg font-bold">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">
|
|
<Info class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<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="max-h-96 overflow-y-auto rounded-lg border">
|
|
{#if carregandoFuncionarios}
|
|
<div class="text-base-content/60 flex items-center justify-center gap-3 p-4">
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Carregando funcionários disponíveis...
|
|
</div>
|
|
{:else if funcionariosFiltrados.length === 0}
|
|
<div class="text-base-content/60 p-4 text-center">
|
|
{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="hover:bg-base-200 flex cursor-pointer items-center gap-3 border-b p-3 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-base-content/70 text-sm">
|
|
CPF: {func.cpf || 'N/A'}
|
|
{#if func.matricula}
|
|
| Matrícula: {func.matricula}
|
|
{/if}
|
|
</div>
|
|
{#if func.descricaoCargo}
|
|
<div class="text-base-content/60 text-xs">
|
|
{func.descricaoCargo}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</label>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" 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="text-error mb-4 text-lg font-bold">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">
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<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" 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}
|
|
|
|
<!-- Modal Resetar Senha -->
|
|
{#if modalResetarSenhaAberto && usuarioSelecionado}
|
|
<div class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="mb-4 text-lg font-bold">Resetar Senha do Usuário</h3>
|
|
|
|
<div class="mb-4">
|
|
<p class="text-base-content/80 mb-2">
|
|
<strong>Usuário:</strong>
|
|
{usuarioSelecionado.nome} ({usuarioSelecionado.matricula})
|
|
</p>
|
|
<p class="text-base-content/70 mb-4 text-sm">
|
|
Uma nova senha temporária será gerada e enviada por email ao usuário.
|
|
</p>
|
|
|
|
{#if novaSenhaGerada}
|
|
<div class="alert alert-success mb-4">
|
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div class="flex-1 w-full">
|
|
<p class="font-semibold text-base">Senha resetada com sucesso!</p>
|
|
<p class="mt-3 text-sm font-medium">
|
|
<strong>Nova senha temporária:</strong>
|
|
</p>
|
|
<div class="bg-base-200 border-base-300 mt-2 flex items-center justify-between gap-2 rounded-lg border p-4">
|
|
<code class="text-xl font-mono font-bold select-all">{novaSenhaGerada}</code>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-ghost shrink-0"
|
|
onclick={copiarSenha}
|
|
aria-label="Copiar senha"
|
|
title="Copiar senha"
|
|
>
|
|
{#if senhaCopiada}
|
|
<CheckCircle2 class="h-5 w-5 text-success" strokeWidth={2} />
|
|
{:else}
|
|
<Copy class="h-5 w-5" strokeWidth={2} />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
<p class="mt-3 text-xs text-base-content/70">
|
|
✓ Esta senha foi enviada por email para <strong>{usuarioSelecionado.email}</strong>
|
|
</p>
|
|
<p class="mt-1 text-xs text-base-content/60">
|
|
O usuário precisará alterar a senha no próximo login.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="alert alert-warning">
|
|
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<span>
|
|
O usuário precisará alterar a senha no próximo login. Todas as sessões ativas serão encerradas.
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
{#if novaSenhaGerada}
|
|
<button type="button" class="btn btn-primary" onclick={fecharModalResetarSenha}>
|
|
Fechar
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="btn"
|
|
onclick={fecharModalResetarSenha}
|
|
disabled={processando}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-warning"
|
|
onclick={resetarSenha}
|
|
disabled={processando}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
Resetar Senha
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop">
|
|
<button
|
|
type="button"
|
|
onclick={fecharModalResetarSenha}
|
|
onkeydown={(e) => e.key === 'Escape' && fecharModalResetarSenha()}
|
|
aria-label="Fechar modal"
|
|
class="sr-only"
|
|
>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</ProtectedRoute>
|