feat: implement advanced access control system with user blocking, rate limiting, and enhanced login security; update UI components for improved user experience and documentation
This commit is contained in:
308
apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte
Normal file
308
apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte
Normal file
@@ -0,0 +1,308 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import UserStatusBadge from "$lib/components/ti/UserStatusBadge.svelte";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
const client = useConvexClient();
|
||||
const usuarios = useQuery(api.usuarios.listar, {});
|
||||
|
||||
let filtroNome = $state("");
|
||||
let filtroStatus = $state<"todos" | "ativo" | "bloqueado" | "inativo">("todos");
|
||||
let usuarioSelecionado = $state<any>(null);
|
||||
let modalAberto = $state(false);
|
||||
let modalAcao = $state<"bloquear" | "desbloquear" | "reset">("bloquear");
|
||||
let motivo = $state("");
|
||||
let processando = $state(false);
|
||||
|
||||
// Usuários filtrados
|
||||
const usuariosFiltrados = $derived.by(() => {
|
||||
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
|
||||
|
||||
return usuarios.data.filter(u => {
|
||||
const matchNome = !filtroNome ||
|
||||
u.nome.toLowerCase().includes(filtroNome.toLowerCase()) ||
|
||||
u.matricula.includes(filtroNome) ||
|
||||
u.email?.toLowerCase().includes(filtroNome.toLowerCase());
|
||||
|
||||
const matchStatus = filtroStatus === "todos" ||
|
||||
(filtroStatus === "ativo" && u.ativo && !u.bloqueado) ||
|
||||
(filtroStatus === "bloqueado" && u.bloqueado) ||
|
||||
(filtroStatus === "inativo" && !u.ativo);
|
||||
|
||||
return matchNome && matchStatus;
|
||||
});
|
||||
});
|
||||
|
||||
const stats = $derived.by(() => {
|
||||
if (!usuarios?.data || !Array.isArray(usuarios.data)) return null;
|
||||
return {
|
||||
total: usuarios.data.length,
|
||||
ativos: usuarios.data.filter(u => u.ativo && !u.bloqueado).length,
|
||||
bloqueados: usuarios.data.filter(u => u.bloqueado).length,
|
||||
inativos: usuarios.data.filter(u => !u.ativo).length
|
||||
};
|
||||
});
|
||||
|
||||
function abrirModal(usuario: any, acao: typeof modalAcao) {
|
||||
usuarioSelecionado = usuario;
|
||||
modalAcao = acao;
|
||||
motivo = "";
|
||||
modalAberto = true;
|
||||
}
|
||||
|
||||
function fecharModal() {
|
||||
modalAberto = false;
|
||||
usuarioSelecionado = null;
|
||||
motivo = "";
|
||||
}
|
||||
|
||||
async function executarAcao() {
|
||||
if (!usuarioSelecionado) return;
|
||||
|
||||
if (!authStore.usuario) {
|
||||
alert("Usuário não autenticado");
|
||||
return;
|
||||
}
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
if (modalAcao === "bloquear") {
|
||||
await client.mutation(api.usuarios.bloquearUsuario, {
|
||||
usuarioId: usuarioSelecionado._id as Id<"usuarios">,
|
||||
motivo,
|
||||
bloqueadoPorId: authStore.usuario._id as Id<"usuarios">
|
||||
});
|
||||
} else if (modalAcao === "desbloquear") {
|
||||
await client.mutation(api.usuarios.desbloquearUsuario, {
|
||||
usuarioId: usuarioSelecionado._id as Id<"usuarios">,
|
||||
desbloqueadoPorId: authStore.usuario._id as Id<"usuarios">
|
||||
});
|
||||
} else if (modalAcao === "reset") {
|
||||
await client.mutation(api.usuarios.resetarSenhaUsuario, {
|
||||
usuarioId: usuarioSelecionado._id as Id<"usuarios">,
|
||||
resetadoPorId: authStore.usuario._id as Id<"usuarios">
|
||||
});
|
||||
}
|
||||
|
||||
fecharModal();
|
||||
} catch (error) {
|
||||
console.error("Erro ao executar ação:", error);
|
||||
alert("Erro ao executar ação. Veja o console.");
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Gestão de Usuários</h1>
|
||||
<p class="text-base-content/60 mt-1">Gerenciar usuários do sistema</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Stats -->
|
||||
{#if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat bg-base-100 shadow rounded-lg">
|
||||
<div class="stat-title">Total</div>
|
||||
<div class="stat-value text-primary">{stats.total}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-100 shadow rounded-lg">
|
||||
<div class="stat-title">Ativos</div>
|
||||
<div class="stat-value text-success">{stats.ativos}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-100 shadow rounded-lg">
|
||||
<div class="stat-title">Bloqueados</div>
|
||||
<div class="stat-value text-error">{stats.bloqueados}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-100 shadow rounded-lg">
|
||||
<div class="stat-title">Inativos</div>
|
||||
<div class="stat-value text-warning">{stats.inativos}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Buscar por nome, matrícula ou email</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={filtroNome}
|
||||
placeholder="Digite para buscar..."
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Filtrar por status</span>
|
||||
</label>
|
||||
<select bind:value={filtroStatus} class="select select-bordered">
|
||||
<option value="todos">Todos</option>
|
||||
<option value="ativo">Ativos</option>
|
||||
<option value="bloqueado">Bloqueados</option>
|
||||
<option value="inativo">Inativos</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
Usuários ({usuariosFiltrados.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>Status</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each usuariosFiltrados as usuario}
|
||||
<tr>
|
||||
<td class="font-mono">{usuario.matricula}</td>
|
||||
<td>{usuario.nome}</td>
|
||||
<td>{usuario.email || "-"}</td>
|
||||
<td>
|
||||
<UserStatusBadge ativo={usuario.ativo} bloqueado={usuario.bloqueado} />
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
{#if usuario.bloqueado}
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={() => abrirModal(usuario, "desbloquear")}
|
||||
>
|
||||
<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="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Desbloquear
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-sm btn-error"
|
||||
onclick={() => abrirModal(usuario, "bloquear")}
|
||||
>
|
||||
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
Bloquear
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-warning"
|
||||
onclick={() => abrirModal(usuario, "reset")}
|
||||
>
|
||||
<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="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
Reset Senha
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-8 text-base-content/60">
|
||||
Nenhum usuário encontrado
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{#if modalAberto}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
{modalAcao === "bloquear" ? "Bloquear Usuário" :
|
||||
modalAcao === "desbloquear" ? "Desbloquear Usuário" :
|
||||
"Resetar Senha"}
|
||||
</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-base-content/80">
|
||||
<strong>Usuário:</strong> {usuarioSelecionado?.nome} ({usuarioSelecionado?.matricula})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if modalAcao === "bloquear"}
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Motivo do bloqueio *</span>
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={motivo}
|
||||
class="textarea textarea-bordered"
|
||||
placeholder="Digite o motivo..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if modalAcao === "reset"}
|
||||
<div class="alert alert-info mb-4">
|
||||
<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>Uma senha temporária será gerada automaticamente.</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharModal}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={executarAcao}
|
||||
disabled={processando || (modalAcao === "bloquear" && !motivo.trim())}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={fecharModal}></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
559
apps/web/src/routes/(dashboard)/ti/usuarios/criar/+page.svelte
Normal file
559
apps/web/src/routes/(dashboard)/ti/usuarios/criar/+page.svelte
Normal file
@@ -0,0 +1,559 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { goto } from "$app/navigation";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
const roles = useQuery(api.roles.listar, {});
|
||||
const funcionarios = useQuery(api.funcionarios.getAll, {});
|
||||
|
||||
// Debug - Remover após teste
|
||||
$effect(() => {
|
||||
console.log("=== DEBUG PERFIS ===");
|
||||
console.log("roles:", roles);
|
||||
console.log("roles?.data:", roles?.data);
|
||||
console.log("É array?", Array.isArray(roles?.data));
|
||||
if (roles?.data) {
|
||||
console.log("Quantidade de perfis:", roles.data.length);
|
||||
console.log("Perfis:", roles.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Estados do formulário
|
||||
let matricula = $state("");
|
||||
let nome = $state("");
|
||||
let email = $state("");
|
||||
let roleId = $state("");
|
||||
let funcionarioId = $state("");
|
||||
let senhaInicial = $state("");
|
||||
let confirmarSenha = $state("");
|
||||
let processando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validações
|
||||
const matriculaStr = String(matricula).trim();
|
||||
if (!matriculaStr || !nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||
return;
|
||||
}
|
||||
|
||||
if (senhaInicial !== confirmarSenha) {
|
||||
mostrarMensagem("error", "As senhas não conferem");
|
||||
return;
|
||||
}
|
||||
|
||||
if (senhaInicial.length < 8) {
|
||||
mostrarMensagem("error", "A senha deve ter no mínimo 8 caracteres");
|
||||
return;
|
||||
}
|
||||
|
||||
processando = true;
|
||||
|
||||
try {
|
||||
const resultado = await client.mutation(api.usuarios.criar, {
|
||||
matricula: matriculaStr,
|
||||
nome: nome.trim(),
|
||||
email: email.trim(),
|
||||
roleId: roleId as Id<"roles">,
|
||||
funcionarioId: funcionarioId ? (funcionarioId as Id<"funcionarios">) : undefined,
|
||||
senhaInicial: senhaInicial,
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
if (senhaGerada) {
|
||||
mostrarMensagem(
|
||||
"success",
|
||||
`Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!`
|
||||
);
|
||||
setTimeout(() => {
|
||||
goto("/ti/usuarios");
|
||||
}, 5000);
|
||||
} else {
|
||||
mostrarMensagem("success", "Usuário criado com sucesso!");
|
||||
setTimeout(() => {
|
||||
goto("/ti/usuarios");
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
mostrarMensagem("error", resultado.erro);
|
||||
}
|
||||
} catch (error: any) {
|
||||
mostrarMensagem("error", error.message || "Erro ao criar usuário");
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
let senhaGerada = $state("");
|
||||
let mostrarSenha = $state(false);
|
||||
|
||||
// Auto-completar ao selecionar funcionário
|
||||
$effect(() => {
|
||||
if (funcionarioId && funcionarios?.data) {
|
||||
const funcSelecionado = funcionarios.data.find((f: any) => f._id === funcionarioId);
|
||||
if (funcSelecionado) {
|
||||
email = funcSelecionado.email || email;
|
||||
nome = funcSelecionado.nome || nome;
|
||||
matricula = funcSelecionado.matricula || matricula;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function gerarSenhaAleatoria() {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
||||
let senha = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
senhaInicial = senha;
|
||||
confirmarSenha = senha;
|
||||
senhaGerada = senha;
|
||||
mostrarSenha = true;
|
||||
}
|
||||
|
||||
function copiarSenha() {
|
||||
if (senhaGerada) {
|
||||
navigator.clipboard.writeText(senhaGerada);
|
||||
mostrarMensagem("success", "Senha copiada para área de transferência!");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
|
||||
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<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-10 w-10 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Criar Novo Usuário</h1>
|
||||
<p class="text-base-content/60 mt-1">Cadastre um novo usuário no sistema</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2">
|
||||
<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 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
Voltar para Usuários
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="text-sm breadcrumbs mb-6">
|
||||
<ul>
|
||||
<li><a href="/ti/painel-administrativo">Dashboard TI</a></li>
|
||||
<li><a href="/ti/usuarios">Usuários</a></li>
|
||||
<li>Criar Usuário</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Mensagens -->
|
||||
{#if mensagem}
|
||||
<div
|
||||
class="alert mb-6"
|
||||
class:alert-success={mensagem.tipo === "success"}
|
||||
class:alert-error={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"
|
||||
>
|
||||
{#if mensagem.tipo === "success"}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{mensagem.texto}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formulário -->
|
||||
<div class="card bg-base-100 shadow-2xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2 bg-primary/10 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="card-title text-2xl">Informações do Usuário</h2>
|
||||
</div>
|
||||
|
||||
<form onsubmit={handleSubmit}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Funcionário (primeiro) -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="funcionario">
|
||||
<span class="label-text font-semibold">Vincular Funcionário (Opcional)</span>
|
||||
</label>
|
||||
<select
|
||||
id="funcionario"
|
||||
class="select select-bordered"
|
||||
bind:value={funcionarioId}
|
||||
disabled={processando || !funcionarios?.data}
|
||||
>
|
||||
<option value="">Selecione um funcionário para auto-completar dados</option>
|
||||
{#if funcionarios?.data}
|
||||
{#each funcionarios.data as func}
|
||||
<option value={func._id}>{func.nome} - Mat: {func.matricula}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Ao selecionar, os campos serão preenchidos automaticamente</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matrícula -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="matricula">
|
||||
<span class="label-text font-semibold">Matrícula *</span>
|
||||
</label>
|
||||
<input
|
||||
id="matricula"
|
||||
type="number"
|
||||
placeholder="Ex: 12345"
|
||||
class="input input-bordered"
|
||||
bind:value={matricula}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="nome">
|
||||
<span class="label-text font-semibold">Nome Completo *</span>
|
||||
</label>
|
||||
<input
|
||||
id="nome"
|
||||
type="text"
|
||||
placeholder="Ex: João da Silva"
|
||||
class="input input-bordered"
|
||||
bind:value={nome}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="email">
|
||||
<span class="label-text font-semibold">E-mail *</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="usuario@sgse.pe.gov.br"
|
||||
class="input input-bordered"
|
||||
bind:value={email}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Perfil/Role -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="role">
|
||||
<span class="label-text font-semibold">Perfil de Acesso *</span>
|
||||
</label>
|
||||
<select
|
||||
id="role"
|
||||
class="select select-bordered"
|
||||
bind:value={roleId}
|
||||
required
|
||||
disabled={processando || !roles?.data}
|
||||
>
|
||||
<option value="">Selecione um perfil</option>
|
||||
{#if roles?.data && Array.isArray(roles.data)}
|
||||
{#each roles.data as role}
|
||||
<option value={role._id}>
|
||||
{role.descricao} ({role.nome})
|
||||
</option>
|
||||
{/each}
|
||||
{:else}
|
||||
<option disabled>Carregando perfis...</option>
|
||||
{/if}
|
||||
</select>
|
||||
{#if !roles?.data || !Array.isArray(roles.data)}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">Carregando perfis disponíveis...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="divider md:col-span-2 mt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
Senha Inicial
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Senha -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="senha">
|
||||
<span class="label-text font-semibold">Senha Inicial *</span>
|
||||
</label>
|
||||
<input
|
||||
id="senha"
|
||||
type="password"
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
class="input input-bordered"
|
||||
bind:value={senhaInicial}
|
||||
required
|
||||
minlength="8"
|
||||
disabled={processando}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Mínimo 8 caracteres</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar Senha -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="confirmar-senha">
|
||||
<span class="label-text font-semibold">Confirmar Senha *</span>
|
||||
</label>
|
||||
<input
|
||||
id="confirmar-senha"
|
||||
type="password"
|
||||
placeholder="Digite novamente"
|
||||
class="input input-bordered"
|
||||
bind:value={confirmarSenha}
|
||||
required
|
||||
minlength="8"
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Botão Gerar Senha e Visualização -->
|
||||
<div class="md:col-span-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-info"
|
||||
onclick={gerarSenhaAleatoria}
|
||||
disabled={processando}
|
||||
>
|
||||
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Gerar Senha Forte Aleatória
|
||||
</button>
|
||||
|
||||
{#if mostrarSenha && senhaGerada}
|
||||
<div class="alert alert-warning mt-4">
|
||||
<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 class="flex-1">
|
||||
<h3 class="font-bold">Senha Gerada:</h3>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<code class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all">
|
||||
{senhaGerada}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={copiarSenha}
|
||||
title="Copiar senha"
|
||||
>
|
||||
<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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Copiar
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mt-2">
|
||||
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará repassá-la
|
||||
manualmente ao usuário até que o SMTP seja configurado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-6">
|
||||
<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>
|
||||
<div>
|
||||
<h3 class="font-bold">Informações Importantes</h3>
|
||||
<ul class="text-sm list-disc list-inside mt-2 space-y-1">
|
||||
<li>O usuário deverá alterar a senha no primeiro acesso</li>
|
||||
<li>As credenciais devem ser repassadas manualmente (por enquanto)</li>
|
||||
<li>
|
||||
Configure o SMTP em <a href="/ti/configuracoes-email" class="link"
|
||||
>Configurações de Email</a
|
||||
> para envio automático
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-8 pt-6 border-t border-base-300">
|
||||
<a href="/ti/usuarios" class="btn btn-ghost gap-2" class:btn-disabled={processando}>
|
||||
<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="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2" disabled={processando}>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Criando Usuário...
|
||||
{:else}
|
||||
<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="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Criar Usuário
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
|
||||
Reference in New Issue
Block a user