Files
sgse-app/apps/web/src/routes/(dashboard)/ti/usuarios/+page.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>