Files
sgse-app/apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte
2025-11-11 16:41:40 -03:00

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>