Fix usuarios page #6

Merged
killer-cf merged 28 commits from fix-usuarios-page into master 2025-11-04 17:42:21 +00:00
61 changed files with 5085 additions and 5884 deletions
Showing only changes of commit 0d011b8f42 - Show all commits

View File

@@ -4,6 +4,7 @@
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte"; import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { authStore } from "$lib/stores/auth.svelte";
type RoleRow = { type RoleRow = {
_id: Id<"roles">; _id: Id<"roles">;
_creationTime: number; _creationTime: number;
@@ -32,6 +33,14 @@
// Formato: { "roleId-recurso": true/false } // Formato: { "roleId-recurso": true/false }
let recursosExpandidos: Record<string, boolean> = $state({}); let recursosExpandidos: Record<string, boolean> = $state({});
// Gerenciamento de Perfis
let modalGerenciarPerfisAberto = $state(false);
let perfilSendoEditado = $state<RoleRow | null>(null);
let nomeNovoPerfil = $state("");
let descricaoNovoPerfil = $state("");
let nivelNovoPerfil = $state(3);
let processando = $state(false);
// Cache de permissões por role // Cache de permissões por role
let permissoesPorRole: Record< let permissoesPorRole: Record<
string, string,
@@ -66,13 +75,13 @@
const rolesFiltradas = $derived.by(() => { const rolesFiltradas = $derived.by(() => {
if (!rolesQuery.data) return []; if (!rolesQuery.data) return [];
let rs: Array<RoleRow> = rolesQuery.data as Array<RoleRow>; let rs = rolesQuery.data; // Removed explicit type annotation
if (filtroRole) if (filtroRole)
rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any)); rs = rs.filter((r) => r._id === (filtroRole)); // Removed as any
if (busca.trim()) { if (busca.trim()) {
const b = busca.toLowerCase(); const b = busca.toLowerCase();
rs = rs.filter( rs = rs.filter(
(r: RoleRow) => (r) =>
r.descricao.toLowerCase().includes(b) || r.descricao.toLowerCase().includes(b) ||
r.nome.toLowerCase().includes(b) r.nome.toLowerCase().includes(b)
); );
@@ -120,8 +129,9 @@
]; ];
} }
mostrarMensagem("success", "Permissão atualizada com sucesso!"); mostrarMensagem("success", "Permissão atualizada com sucesso!");
} catch (e: any) { } catch (error: unknown) { // Changed to unknown
mostrarMensagem("error", e.message || "Erro ao atualizar permissão"); const message = error instanceof Error ? error.message : "Erro ao atualizar permissão";
mostrarMensagem("error", message);
} finally { } finally {
salvando = false; salvando = false;
} }
@@ -132,6 +142,90 @@
const entry = dados?.find((e) => e.recurso === recurso); const entry = dados?.find((e) => e.recurso === recurso);
return entry ? entry.acoes.includes(acao) : false; return entry ? entry.acoes.includes(acao) : false;
} }
function abrirModalCriarPerfil() {
nomeNovoPerfil = "";
descricaoNovoPerfil = "";
nivelNovoPerfil = 3; // Default to a common level
perfilSendoEditado = null;
modalGerenciarPerfisAberto = true;
}
function prepararEdicaoPerfil(role: RoleRow) {
perfilSendoEditado = role;
nomeNovoPerfil = role.nome;
descricaoNovoPerfil = role.descricao;
nivelNovoPerfil = role.nivel;
modalGerenciarPerfisAberto = true;
}
function fecharModalGerenciarPerfis() {
modalGerenciarPerfisAberto = false;
perfilSendoEditado = null;
}
async function criarNovoPerfil() {
if (!nomeNovoPerfil.trim()) return;
processando = true;
try {
const result = await client.mutation(api.roles.criar, {
nome: nomeNovoPerfil.trim(),
descricao: descricaoNovoPerfil.trim(),
nivel: nivelNovoPerfil,
customizado: true,
});
if (result.sucesso) {
mostrarMensagem("success", "Perfil criado com sucesso!");
nomeNovoPerfil = "";
descricaoNovoPerfil = "";
nivelNovoPerfil = 3;
fecharModalGerenciarPerfis();
if (rolesQuery.refetch) { // Verificação para garantir que refetch existe
rolesQuery.refetch(); // Atualiza a lista de perfis
}
} else {
mostrarMensagem("error", `Erro ao criar perfil: ${result.erro}`);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
mostrarMensagem("error", `Erro ao criar perfil: ${message}`);
} finally {
processando = false;
}
}
async function editarPerfil() {
if (!perfilSendoEditado || !nomeNovoPerfil.trim()) return;
processando = true;
try {
const result = await client.mutation(api.roles.atualizar, {
roleId: perfilSendoEditado._id,
nome: nomeNovoPerfil.trim(),
descricao: descricaoNovoPerfil.trim(),
nivel: nivelNovoPerfil,
setor: perfilSendoEditado.setor, // Manter setor existente
});
if (result.sucesso) {
mostrarMensagem("success", "Perfil atualizado com sucesso!");
fecharModalGerenciarPerfis();
if (rolesQuery.refetch) { // Verificação para garantir que refetch existe
rolesQuery.refetch(); // Atualiza a lista de perfis
}
} else {
mostrarMensagem("error", `Erro ao atualizar perfil: ${result.erro}`);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
mostrarMensagem("error", `Erro ao atualizar perfil: ${message}`);
} finally {
processando = false;
}
}
</script> </script>
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}> <ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
@@ -160,7 +254,7 @@
<li> <li>
<a href="/ti" class="text-primary hover:text-primary-focus">TI</a> <a href="/ti" class="text-primary hover:text-primary-focus">TI</a>
</li> </li>
<li class="font-semibold">Gerenciar Permissões</li> <li class="font-semibold">Gerenciar Perfis & Permissões</li>
</ul> </ul>
</div> </div>
@@ -185,12 +279,32 @@
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h1 class="text-3xl font-bold text-base-content"> <h1 class="text-3xl font-bold text-base-content">
Gerenciar Permissões de Acesso Gerenciar Perfis & Permissões de Acesso
</h1> </h1>
<p class="text-base-content/60 mt-1"> <p class="text-base-content/60 mt-1">
Configure as permissões de acesso aos menus do sistema por função Configure as permissões de acesso aos menus do sistema por função
</p> </p>
</div> </div>
<button
class="btn btn-primary gap-2"
onclick={abrirModalCriarPerfil}
>
<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 Novo Perfil
</button>
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}> <button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -479,6 +593,15 @@
> >
</p> </p>
</div> </div>
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-outline btn-sm"
onclick={() => prepararEdicaoPerfil(roleRow)}
>
Editar
</button>
</div>
</div> </div>
{#if roleRow.nivel <= 1} {#if roleRow.nivel <= 1}
@@ -574,4 +697,148 @@
</div> </div>
{/each} {/each}
{/if} {/if}
<!-- Modal Gerenciar Perfis -->
{#if modalGerenciarPerfisAberto}
<dialog class="modal modal-open">
<div
class="modal-box max-w-4xl w-full overflow-hidden border border-base-200/60 bg-base-200/40 p-0 shadow-2xl"
>
<div
class="relative bg-gradient-to-r from-primary via-primary/90 to-secondary/80 px-8 py-6 text-base-100"
>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4 text-base-100/80 hover:text-base-100"
onclick={fecharModalGerenciarPerfis}
aria-label="Fechar modal"
>
</button>
<h3 class="text-3xl font-black tracking-tight">Gerenciar Perfis de Acesso</h3>
<p class="mt-2 max-w-2xl text-sm text-base-100/80 md:text-base">
{perfilSendoEditado
? "Atualize as informações do perfil selecionado para manter a governança de acesso alinhada com as diretrizes do sistema."
: "Crie um novo perfil de acesso definindo nome, descrição e nível hierárquico conforme os padrões adotados pela Secretaria."}
</p>
</div>
<div class="bg-base-100 px-8 py-6">
<section
class="space-y-6 rounded-2xl border border-base-200/60 bg-base-100 p-6 shadow-sm"
>
<div
class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-1">
<h4 class="text-xl font-semibold text-base-content">
{perfilSendoEditado ? "Editar Perfil" : "Criar Novo Perfil"}
</h4>
<p class="text-sm text-base-content/70">
{perfilSendoEditado
? "Os campos bloqueados indicam atributos padronizados do sistema. Ajuste apenas o que estiver disponível."
: "Preencha as informações com atenção para garantir que o novo perfil siga o padrão institucional."}
</p>
</div>
{#if perfilSendoEditado}
<span
class="badge badge-outline badge-primary badge-lg self-start flex flex-col items-center gap-1 px-5 py-3 text-center"
>
<span class="text-[11px] font-semibold uppercase tracking-[0.32em] text-primary">
Nível atual
</span>
<span class="text-2xl font-bold leading-none text-primary">
{perfilSendoEditado.nivel}
</span>
</span>
{/if}
</div>
<div class="grid gap-6 md:grid-cols-2">
<div class="form-control md:col-span-2">
<label class="label" for="nome-perfil-input">
<span class="label-text">Nome do Perfil *</span>
</label>
<input
id="nome-perfil-input"
type="text"
bind:value={nomeNovoPerfil}
class="input input-bordered input-primary"
placeholder="Ex: RH, Financeiro, Gestor"
disabled={perfilSendoEditado !== null && !perfilSendoEditado.customizado}
/>
</div>
<div class="form-control md:col-span-2">
<label class="label" for="descricao-perfil-input">
<span class="label-text">Descrição</span>
</label>
<textarea
id="descricao-perfil-input"
bind:value={descricaoNovoPerfil}
class="textarea textarea-bordered textarea-primary min-h-[120px]"
placeholder="Breve descrição das responsabilidades e limites de atuação deste perfil."
></textarea>
</div>
<div class="form-control max-w-xs">
<label class="label" for="nivel-perfil-input">
<span class="label-text">Nível de Acesso (0-5) *</span>
</label>
<input
id="nivel-perfil-input"
type="number"
bind:value={nivelNovoPerfil}
min="0"
max="5"
class="input input-bordered input-secondary"
disabled={perfilSendoEditado !== null && !perfilSendoEditado.customizado}
/>
</div>
</div>
</section>
<div
class="mt-8 flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<p class="text-sm text-base-content/60">
Campos marcados com * são obrigatórios.
</p>
<div class="flex flex-wrap gap-3">
<button
type="button"
class="btn btn-ghost"
onclick={fecharModalGerenciarPerfis}
disabled={processando}
>
Cancelar
</button>
<button
type="button"
class="btn btn-primary"
disabled={!nomeNovoPerfil.trim() || processando}
onclick={perfilSendoEditado ? editarPerfil : criarNovoPerfil}
>
{#if processando}
<span class="loading loading-spinner"></span>
Processando...
{:else}
{perfilSendoEditado ? "Salvar Alterações" : "Criar Perfil"}
{/if}
</button>
</div>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button
type="button"
onclick={fecharModalGerenciarPerfis}
aria-label="Fechar modal"
>Fechar</button>
</form>
</dialog>
{/if}
</ProtectedRoute> </ProtectedRoute>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,82 @@
import { useConvexClient, useQuery } from "convex-svelte"; import { useConvexClient, useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from "@sgse-app/backend/convex/_generated/api";
import { authStore } from "$lib/stores/auth.svelte"; import { authStore } from "$lib/stores/auth.svelte";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { Id, Doc } from "@sgse-app/backend/convex/_generated/dataModel";
// Tipos baseados nos retornos das queries do backend
type Usuario = {
_id: Id<"usuarios">;
matricula: string;
nome: string;
email: string;
ativo: boolean;
bloqueado?: boolean;
motivoBloqueio?: string;
primeiroAcesso: boolean;
ultimoAcesso?: number;
criadoEm: number;
role: {
_id: Id<"roles">;
_creationTime?: number;
criadoPor?: Id<"usuarios">;
customizado?: boolean;
descricao: string;
editavel?: boolean;
nome: string;
nivel: number;
setor?: string;
erro?: boolean;
};
funcionario?: {
_id: Id<"funcionarios">;
nome: string;
matricula?: string;
descricaoCargo?: string;
simboloTipo: "cargo_comissionado" | "funcao_gratificada";
};
avisos?: Array<{
tipo: "erro" | "aviso" | "info";
mensagem: string;
}>;
};
type Funcionario = {
_id: Id<"funcionarios">;
nome: string;
matricula?: string;
cpf?: string;
rg?: string;
nascimento?: string;
email?: string;
telefone?: string;
endereco?: string;
cep?: string;
cidade?: string;
uf?: string;
simboloId: Id<"simbolos">;
simboloTipo: "cargo_comissionado" | "funcao_gratificada";
admissaoData?: string;
desligamentoData?: string;
descricaoCargo?: string;
};
type Gestor = Doc<"usuarios"> | null;
type TimeComDetalhes = Doc<"times"> & {
gestor: Gestor;
totalMembros: number;
};
type MembroTime = Doc<"timesMembros"> & {
funcionario: Doc<"funcionarios"> | null;
};
type TimeComMembros = Doc<"times"> & {
gestor: Gestor;
membros: MembroTime[];
};
const client = useConvexClient(); const client = useConvexClient();
@@ -11,17 +86,23 @@
const usuariosQuery = useQuery(api.usuarios.listar, {}); const usuariosQuery = useQuery(api.usuarios.listar, {});
const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const times = $derived(timesQuery?.data || []); const times = $derived((timesQuery?.data || []) as TimeComDetalhes[]);
const usuarios = $derived(usuariosQuery?.data || []); const usuarios = $derived((usuariosQuery?.data || []) as Usuario[]);
const funcionarios = $derived(funcionariosQuery?.data || []); const funcionarios = $derived((funcionariosQuery?.data || []) as Funcionario[]);
const carregando = $derived(
timesQuery === undefined ||
usuariosQuery === undefined ||
funcionariosQuery === undefined
);
// Estados // Estados
let modoEdicao = $state(false); let modoEdicao = $state(false);
let timeEmEdicao = $state<any>(null); let timeEmEdicao = $state<TimeComDetalhes | null>(null);
let mostrarModalMembros = $state(false); let mostrarModalMembros = $state(false);
let timeParaMembros = $state<any>(null); let timeParaMembros = $state<TimeComMembros | null>(null);
let mostrarConfirmacaoExclusao = $state(false); let mostrarConfirmacaoExclusao = $state(false);
let timeParaExcluir = $state<any>(null); let timeParaExcluir = $state<TimeComDetalhes | null>(null);
let processando = $state(false); let processando = $state(false);
// Form // Form
@@ -32,9 +113,9 @@
// Membros // Membros
let membrosDisponiveis = $derived( let membrosDisponiveis = $derived(
funcionarios.filter((f: any) => { funcionarios.filter((f: Funcionario) => {
// Verificar se o funcionário já está em algum time ativo // Verificar se o funcionário já está em algum time ativo
const jaNaEquipe = timeParaMembros?.membros?.some((m: any) => m.funcionario?._id === f._id); const jaNaEquipe = timeParaMembros?.membros?.some((m: MembroTime) => m.funcionario?._id === f._id);
return !jaNaEquipe; return !jaNaEquipe;
}) })
); );
@@ -60,7 +141,7 @@
formCor = coresDisponiveis[Math.floor(Math.random() * coresDisponiveis.length)]; formCor = coresDisponiveis[Math.floor(Math.random() * coresDisponiveis.length)];
} }
function editarTime(time: any) { function editarTime(time: TimeComDetalhes) {
modoEdicao = true; modoEdicao = true;
timeEmEdicao = time; timeEmEdicao = time;
formNome = time.nome; formNome = time.nome;
@@ -91,26 +172,27 @@
id: timeEmEdicao._id, id: timeEmEdicao._id,
nome: formNome, nome: formNome,
descricao: formDescricao || undefined, descricao: formDescricao || undefined,
gestorId: formGestorId as any, gestorId: formGestorId as Id<"usuarios">,
cor: formCor, cor: formCor,
}); });
} else { } else {
await client.mutation(api.times.criar, { await client.mutation(api.times.criar, {
nome: formNome, nome: formNome,
descricao: formDescricao || undefined, descricao: formDescricao || undefined,
gestorId: formGestorId as any, gestorId: formGestorId as Id<"usuarios">,
cor: formCor, cor: formCor,
}); });
} }
cancelarEdicao(); cancelarEdicao();
} catch (e: any) { } catch (e: unknown) {
alert("Erro ao salvar: " + (e.message || e)); const errorMessage = e instanceof Error ? e.message : String(e);
alert("Erro ao salvar: " + errorMessage);
} finally { } finally {
processando = false; processando = false;
} }
} }
function confirmarExclusao(time: any) { function confirmarExclusao(time: TimeComDetalhes) {
timeParaExcluir = time; timeParaExcluir = time;
mostrarConfirmacaoExclusao = true; mostrarConfirmacaoExclusao = true;
} }
@@ -123,17 +205,20 @@
await client.mutation(api.times.desativar, { id: timeParaExcluir._id }); await client.mutation(api.times.desativar, { id: timeParaExcluir._id });
mostrarConfirmacaoExclusao = false; mostrarConfirmacaoExclusao = false;
timeParaExcluir = null; timeParaExcluir = null;
} catch (e: any) { } catch (e: unknown) {
alert("Erro ao excluir: " + (e.message || e)); const errorMessage = e instanceof Error ? e.message : String(e);
alert("Erro ao excluir: " + errorMessage);
} finally { } finally {
processando = false; processando = false;
} }
} }
async function abrirGerenciarMembros(time: any) { async function abrirGerenciarMembros(time: TimeComDetalhes) {
const detalhes = await client.query(api.times.obterPorId, { id: time._id }); const detalhes = await client.query(api.times.obterPorId, { id: time._id });
timeParaMembros = detalhes; if (detalhes) {
mostrarModalMembros = true; timeParaMembros = detalhes as TimeComMembros;
mostrarModalMembros = true;
}
} }
async function adicionarMembro(funcionarioId: string) { async function adicionarMembro(funcionarioId: string) {
@@ -143,14 +228,17 @@
try { try {
await client.mutation(api.times.adicionarMembro, { await client.mutation(api.times.adicionarMembro, {
timeId: timeParaMembros._id, timeId: timeParaMembros._id,
funcionarioId: funcionarioId as any, funcionarioId: funcionarioId as Id<"funcionarios">,
}); });
// Recarregar detalhes do time // Recarregar detalhes do time
const detalhes = await client.query(api.times.obterPorId, { id: timeParaMembros._id }); const detalhes = await client.query(api.times.obterPorId, { id: timeParaMembros._id });
timeParaMembros = detalhes; if (detalhes) {
} catch (e: any) { timeParaMembros = detalhes as TimeComMembros;
alert("Erro: " + (e.message || e)); }
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
alert("Erro: " + errorMessage);
} finally { } finally {
processando = false; processando = false;
} }
@@ -161,13 +249,18 @@
processando = true; processando = true;
try { try {
await client.mutation(api.times.removerMembro, { membroId: membroId as any }); await client.mutation(api.times.removerMembro, { membroId: membroId as Id<"timesMembros"> });
// Recarregar detalhes do time // Recarregar detalhes do time
const detalhes = await client.query(api.times.obterPorId, { id: timeParaMembros._id }); if (timeParaMembros) {
timeParaMembros = detalhes; const detalhes = await client.query(api.times.obterPorId, { id: timeParaMembros._id });
} catch (e: any) { if (detalhes) {
alert("Erro: " + (e.message || e)); timeParaMembros = detalhes as TimeComMembros;
}
}
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
alert("Erro: " + errorMessage);
} finally { } finally {
processando = false; processando = false;
} }
@@ -179,7 +272,8 @@
} }
</script> </script>
<main class="container mx-auto px-4 py-6 max-w-7xl"> <ProtectedRoute allowedRoles={["ti_master", "admin", "ti_usuario"]} maxLevel={2}>
<main class="container mx-auto px-4 py-6 max-w-7xl">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4"> <div class="text-sm breadcrumbs mb-4">
<ul> <ul>
@@ -192,14 +286,14 @@
<div class="mb-6"> <div class="mb-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="p-3 bg-blue-500/20 rounded-xl"> <div class="p-3 bg-secondary/10 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-secondary" 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" /> <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> </svg>
</div> </div>
<div> <div>
<h1 class="text-3xl font-bold text-primary">Gestão de Times</h1> <h1 class="text-3xl font-bold text-base-content">Gestão de Times</h1>
<p class="text-base-content/70">Organize funcionários em equipes e defina gestores</p> <p class="text-base-content/60 mt-1">Organize funcionários em equipes e defina gestores</p>
</div> </div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -297,20 +391,27 @@
{/if} {/if}
<!-- Lista de Times --> <!-- Lista de Times -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {#if carregando}
{#each times as time} <div class="flex justify-center items-center py-20">
{#if time.ativo} <span class="loading loading-spinner loading-lg text-primary"></span>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all border-l-4" style="border-color: {time.cor}"> </div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each times.filter((t: TimeComDetalhes) => t.ativo) as time}
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all border-l-4" style="border-color: {time.cor || '#3B82F6'}">
<div class="card-body"> <div class="card-body">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between mb-2">
<h2 class="card-title text-lg">{time.nome}</h2> <div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full" style="background-color: {time.cor || '#3B82F6'}"></div>
<h2 class="card-title text-lg">{time.nome}</h2>
</div>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<button tabindex="0" class="btn btn-ghost btn-sm btn-square" aria-label="Menu do time"> <button type="button" class="btn btn-ghost btn-sm btn-square" aria-label="Menu do time">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <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" /> <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> </svg>
</button> </button>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300"> <ul role="menu" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300">
<li> <li>
<button type="button" onclick={() => editarTime(time)}> <button type="button" onclick={() => editarTime(time)}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -339,7 +440,7 @@
</div> </div>
</div> </div>
<p class="text-sm text-base-content/70 mb-3">{time.descricao || "Sem descrição"}</p> <p class="text-sm text-base-content/70 mb-3 min-h-[2rem]">{time.descricao || "Sem descrição"}</p>
<div class="divider my-2"></div> <div class="divider my-2"></div>
@@ -348,31 +449,38 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span><strong>Gestor:</strong> {time.gestor?.nome}</span> <span class="text-base-content/70"><strong>Gestor:</strong> {time.gestor?.nome || "Não definido"}</span>
</div> </div>
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" 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" /> <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> </svg>
<span><strong>Membros:</strong> {time.totalMembros || 0}</span> <span class="text-base-content/70"><strong>Membros:</strong> <span class="badge badge-primary badge-sm">{time.totalMembros || 0}</span></span>
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-4">
<button class="btn btn-sm btn-outline btn-primary" onclick={() => abrirGerenciarMembros(time)}>
Ver Detalhes
</button>
</div>
</div> </div>
</div> </div>
{/if} {/each}
{/each}
{#if times.filter((t: any) => t.ativo).length === 0} {#if times.filter((t: TimeComDetalhes) => t.ativo).length === 0}
<div class="col-span-full"> <div class="col-span-full">
<div class="alert"> <div class="flex flex-col items-center justify-center py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6"> <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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> <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> </svg>
<span>Nenhum time cadastrado. Clique em "Novo Time" para começar.</span> <h3 class="text-xl font-semibold mt-4">Nenhum time cadastrado</h3>
<p class="text-base-content/60 mt-2">Clique em "Novo Time" para criar seu primeiro time</p>
</div>
</div> </div>
</div> {/if}
{/if} </div>
</div> {/if}
<!-- Modal de Gerenciar Membros --> <!-- Modal de Gerenciar Membros -->
{#if mostrarModalMembros && timeParaMembros} {#if mostrarModalMembros && timeParaMembros}
@@ -502,4 +610,5 @@
</dialog> </dialog>
{/if} {/if}
</main> </main>
</ProtectedRoute>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
import { createSvelteKitHandler } from "@mmailaender/convex-better-auth-svelte/sveltekit";
export const { GET, POST } = createSvelteKitHandler();

View File

@@ -1,33 +0,0 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type {
ApiFromModules,
FilterApi,
FunctionReference,
} from "convex/server";
/**
* A utility for referencing Convex functions in your app's API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
declare const fullApi: ApiFromModules<{}>;
export declare const api: FilterApi<
typeof fullApi,
FunctionReference<any, "public">
>;
export declare const internal: FilterApi<
typeof fullApi,
FunctionReference<any, "internal">
>;

View File

@@ -1,22 +0,0 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import { anyApi } from "convex/server";
/**
* A utility for referencing Convex functions in your app's API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
export const api = anyApi;
export const internal = anyApi;

View File

@@ -1,58 +0,0 @@
/* eslint-disable */
/**
* Generated data model types.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import { AnyDataModel } from "convex/server";
import type { GenericId } from "convex/values";
/**
* No `schema.ts` file found!
*
* This generated code has permissive types like `Doc = any` because
* Convex doesn't know your schema. If you'd like more type safety, see
* https://docs.convex.dev/using/schemas for instructions on how to add a
* schema file.
*
* After you change a schema, rerun codegen with `npx convex dev`.
*/
/**
* The names of all of your Convex tables.
*/
export type TableNames = string;
/**
* The type of a document stored in Convex.
*/
export type Doc = any;
/**
* An identifier for a document in Convex.
*
* Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
*
* Documents can be loaded using `db.get(id)` in query and mutation functions.
*
* IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking.
*/
export type Id<TableName extends TableNames = TableNames> =
GenericId<TableName>;
/**
* A type describing your Convex data model.
*
* This type includes information about what tables you have, the type of
* documents stored in those tables, and the indexes defined on them.
*
* This type is used to parameterize methods like `queryGeneric` and
* `mutationGeneric` to make them type-safe.
*/
export type DataModel = AnyDataModel;

View File

@@ -1,142 +0,0 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import {
ActionBuilder,
HttpActionBuilder,
MutationBuilder,
QueryBuilder,
GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
GenericDatabaseReader,
GenericDatabaseWriter,
} from "convex/server";
import type { DataModel } from "./dataModel.js";
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export declare const query: QueryBuilder<DataModel, "public">;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export declare const mutation: MutationBuilder<DataModel, "public">;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
*/
export declare const action: ActionBuilder<DataModel, "public">;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*/
export declare const internalAction: ActionBuilder<DataModel, "internal">;
/**
* Define an HTTP action.
*
* This function will be used to respond to HTTP requests received by a Convex
* deployment if the requests matches the path and method where this action
* is routed. Be sure to route your action in `convex/http.js`.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
*/
export declare const httpAction: HttpActionBuilder;
/**
* A set of services for use within Convex query functions.
*
* The query context is passed as the first argument to any Convex query
* function run on the server.
*
* This differs from the {@link MutationCtx} because all of the services are
* read-only.
*/
export type QueryCtx = GenericQueryCtx<DataModel>;
/**
* A set of services for use within Convex mutation functions.
*
* The mutation context is passed as the first argument to any Convex mutation
* function run on the server.
*/
export type MutationCtx = GenericMutationCtx<DataModel>;
/**
* A set of services for use within Convex action functions.
*
* The action context is passed as the first argument to any Convex action
* function run on the server.
*/
export type ActionCtx = GenericActionCtx<DataModel>;
/**
* An interface to read from the database within Convex query functions.
*
* The two entry points are {@link DatabaseReader.get}, which fetches a single
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
* building a query.
*/
export type DatabaseReader = GenericDatabaseReader<DataModel>;
/**
* An interface to read from and write to the database within Convex mutation
* functions.
*
* Convex guarantees that all writes within a single mutation are
* executed atomically, so you never have to worry about partial writes leaving
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
* for the guarantees Convex provides your functions.
*/
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;

View File

@@ -1,89 +0,0 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import {
actionGeneric,
httpActionGeneric,
queryGeneric,
mutationGeneric,
internalActionGeneric,
internalMutationGeneric,
internalQueryGeneric,
} from "convex/server";
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export const query = queryGeneric;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export const internalQuery = internalQueryGeneric;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export const mutation = mutationGeneric;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export const internalMutation = internalMutationGeneric;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
*/
export const action = actionGeneric;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*/
export const internalAction = internalActionGeneric;
/**
* Define a Convex HTTP action.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
* as its second.
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
*/
export const httpAction = httpActionGeneric;

View File

@@ -1,121 +0,0 @@
@echo off
chcp 65001 >nul
echo.
echo ═══════════════════════════════════════════════════════════
echo 🔐 CRIAR ARQUIVO .env - SGSE (Convex Local)
echo ═══════════════════════════════════════════════════════════
echo.
echo [1/4] Verificando se .env já existe...
if exist .env (
echo.
echo ⚠️ ATENÇÃO: Arquivo .env já existe!
echo.
echo Deseja sobrescrever? (S/N^)
set /p resposta="> "
if /i not "%resposta%"=="S" (
echo.
echo ❌ Operação cancelada. Arquivo .env mantido.
pause
exit /b
)
)
echo.
echo [2/4] Criando arquivo .env...
(
echo # ══════════════════════════════════════════════════════════
echo # CONFIGURAÇÃO DE AMBIENTE - SGSE
echo # Gerado automaticamente em: %date% %time%
echo # ══════════════════════════════════════════════════════════
echo.
echo # Segurança Better Auth
echo # Secret para criptografia de tokens de autenticação
echo BETTER_AUTH_SECRET=+Nfg4jTxPv1giF5MlmyYTxpU/VkS3QaDOvgSWd+QmbY=
echo.
echo # URL da aplicação
echo # Desenvolvimento: http://localhost:5173
echo # Produção: https://sgse.pe.gov.br ^(alterar quando for para produção^)
echo SITE_URL=http://localhost:5173
echo.
echo # ══════════════════════════════════════════════════════════
echo # IMPORTANTE - SEGURANÇA
echo # ══════════════════════════════════════════════════════════
echo # 1. Este arquivo NÃO deve ser commitado no Git
echo # 2. Antes de ir para produção, gere um NOVO secret
echo # 3. Em produção, altere SITE_URL para a URL real
echo # ══════════════════════════════════════════════════════════
) > .env
if not exist .env (
echo.
echo ❌ ERRO: Falha ao criar arquivo .env
echo.
pause
exit /b 1
)
echo ✅ Arquivo .env criado com sucesso!
echo.
echo [3/4] Verificando .gitignore...
if not exist .gitignore (
echo # Arquivos de ambiente > .gitignore
echo .env >> .gitignore
echo .env.local >> .gitignore
echo .env.*.local >> .gitignore
echo ✅ .gitignore criado
) else (
findstr /C:".env" .gitignore >nul
if errorlevel 1 (
echo .env >> .gitignore
echo .env.local >> .gitignore
echo .env.*.local >> .gitignore
echo ✅ .env adicionado ao .gitignore
) else (
echo ✅ .env já está no .gitignore
)
)
echo.
echo [4/4] Resumo da configuração:
echo.
echo ┌─────────────────────────────────────────────────────────┐
echo │ ✅ Arquivo criado: packages/backend/.env │
echo │ │
echo │ Variáveis configuradas: │
echo │ • BETTER_AUTH_SECRET: Configurado │
echo │ • SITE_URL: http://localhost:5173 │
echo └─────────────────────────────────────────────────────────┘
echo.
echo ═══════════════════════════════════════════════════════════
echo 📋 PRÓXIMOS PASSOS
echo ═══════════════════════════════════════════════════════════
echo.
echo 1. Reinicie o servidor Convex:
echo ^> cd packages\backend
echo ^> bunx convex dev
echo.
echo 2. Reinicie o servidor Web (em outro terminal^):
echo ^> cd apps\web
echo ^> bun run dev
echo.
echo 3. Verifique que as mensagens de erro pararam
echo.
echo ═══════════════════════════════════════════════════════════
echo ⚠️ LEMBRE-SE
echo ═══════════════════════════════════════════════════════════
echo.
echo • NÃO commite o arquivo .env no Git
echo • Gere um NOVO secret antes de ir para produção
echo • Altere SITE_URL quando for para produção
echo.
pause

View File

@@ -8,14 +8,12 @@
* @module * @module
*/ */
import type * as actions_email from "../actions/email.js";
import type * as actions_smtp from "../actions/smtp.js";
import type * as autenticacao from "../autenticacao.js"; import type * as autenticacao from "../autenticacao.js";
import type * as auth_utils from "../auth/utils.js"; import type * as auth_utils from "../auth/utils.js";
import type * as betterAuth__generated_api from "../betterAuth/_generated/api.js";
import type * as betterAuth__generated_server from "../betterAuth/_generated/server.js";
import type * as chat from "../chat.js"; import type * as chat from "../chat.js";
import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as configuracaoEmail from "../configuracaoEmail.js";
import type * as criarFuncionarioTeste from "../criarFuncionarioTeste.js";
import type * as criarUsuarioTeste from "../criarUsuarioTeste.js";
import type * as crons from "../crons.js"; import type * as crons from "../crons.js";
import type * as cursos from "../cursos.js"; import type * as cursos from "../cursos.js";
import type * as dashboard from "../dashboard.js"; import type * as dashboard from "../dashboard.js";
@@ -29,11 +27,7 @@ import type * as limparPerfisAntigos from "../limparPerfisAntigos.js";
import type * as logsAcesso from "../logsAcesso.js"; import type * as logsAcesso from "../logsAcesso.js";
import type * as logsAtividades from "../logsAtividades.js"; import type * as logsAtividades from "../logsAtividades.js";
import type * as logsLogin from "../logsLogin.js"; import type * as logsLogin from "../logsLogin.js";
import type * as menuPermissoes from "../menuPermissoes.js";
import type * as migrarParaTimes from "../migrarParaTimes.js";
import type * as migrarUsuariosAdmin from "../migrarUsuariosAdmin.js";
import type * as monitoramento from "../monitoramento.js"; import type * as monitoramento from "../monitoramento.js";
import type * as perfisCustomizados from "../perfisCustomizados.js";
import type * as permissoesAcoes from "../permissoesAcoes.js"; import type * as permissoesAcoes from "../permissoesAcoes.js";
import type * as roles from "../roles.js"; import type * as roles from "../roles.js";
import type * as saldoFerias from "../saldoFerias.js"; import type * as saldoFerias from "../saldoFerias.js";
@@ -61,14 +55,12 @@ import type {
* ``` * ```
*/ */
declare const fullApi: ApiFromModules<{ declare const fullApi: ApiFromModules<{
"actions/email": typeof actions_email;
"actions/smtp": typeof actions_smtp;
autenticacao: typeof autenticacao; autenticacao: typeof autenticacao;
"auth/utils": typeof auth_utils; "auth/utils": typeof auth_utils;
"betterAuth/_generated/api": typeof betterAuth__generated_api;
"betterAuth/_generated/server": typeof betterAuth__generated_server;
chat: typeof chat; chat: typeof chat;
configuracaoEmail: typeof configuracaoEmail; configuracaoEmail: typeof configuracaoEmail;
criarFuncionarioTeste: typeof criarFuncionarioTeste;
criarUsuarioTeste: typeof criarUsuarioTeste;
crons: typeof crons; crons: typeof crons;
cursos: typeof cursos; cursos: typeof cursos;
dashboard: typeof dashboard; dashboard: typeof dashboard;
@@ -82,11 +74,7 @@ declare const fullApi: ApiFromModules<{
logsAcesso: typeof logsAcesso; logsAcesso: typeof logsAcesso;
logsAtividades: typeof logsAtividades; logsAtividades: typeof logsAtividades;
logsLogin: typeof logsLogin; logsLogin: typeof logsLogin;
menuPermissoes: typeof menuPermissoes;
migrarParaTimes: typeof migrarParaTimes;
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
monitoramento: typeof monitoramento; monitoramento: typeof monitoramento;
perfisCustomizados: typeof perfisCustomizados;
permissoesAcoes: typeof permissoesAcoes; permissoesAcoes: typeof permissoesAcoes;
roles: typeof roles; roles: typeof roles;
saldoFerias: typeof saldoFerias; saldoFerias: typeof saldoFerias;

View File

@@ -0,0 +1,99 @@
"use node";
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
export const enviar = action({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
const nodemailer = await import("nodemailer");
try {
// Buscar email da fila
const email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId,
});
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
// Buscar configuração SMTP ativa
const config = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
if (!config) {
return {
sucesso: false,
erro: "Configuração de email não encontrada ou inativa",
};
}
if (!config.testadoEm) {
return {
sucesso: false,
erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!",
};
}
// Marcar como enviando
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId,
});
// Criar transporter do nodemailer
const transporter = nodemailer.createTransport({
host: config.servidor,
port: config.porta,
secure: config.usarSSL,
auth: {
user: config.usuario,
// Em produção deve ser armazenado com criptografia reversível
pass: config.senhaHash,
},
tls: {
// Permitir certificados autoassinados
rejectUnauthorized: false,
},
});
// Enviar email
const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
});
console.log("✅ Email enviado com sucesso!", {
para: email.destinatario,
assunto: email.assunto,
messageId: (info as { messageId?: string }).messageId,
});
// Marcar como enviado
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId,
});
return { sucesso: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("❌ Erro ao enviar email:", errorMessage);
// Marcar como falha
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: errorMessage,
});
return { sucesso: false, erro: errorMessage };
}
},
});

View File

@@ -0,0 +1,60 @@
"use node";
import { action } from "../_generated/server";
import { v } from "convex/values";
export const testarConexao = action({
args: {
servidor: v.string(),
porta: v.number(),
usuario: v.string(),
senha: v.string(),
usarSSL: v.boolean(),
usarTLS: v.boolean(),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
"use node";
const nodemailer = await import("nodemailer");
try {
// Validações básicas
if (!args.servidor || args.servidor.trim() === "") {
return {
sucesso: false as const,
erro: "Servidor SMTP não pode estar vazio",
};
}
if (args.porta < 1 || args.porta > 65535) {
return { sucesso: false as const, erro: "Porta inválida" };
}
const transporter = nodemailer.createTransport({
host: args.servidor,
port: args.porta,
secure: args.usarSSL,
auth: {
user: args.usuario,
pass: args.senha,
},
tls: {
rejectUnauthorized: !args.usarTLS ? false : false,
},
});
// Verificar conexão
await transporter.verify();
return { sucesso: true as const };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { sucesso: false as const, erro: errorMessage };
}
},
});

View File

@@ -1,8 +0,0 @@
export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: "convex",
},
],
};

View File

@@ -1,993 +0,0 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type * as adapter from "../adapter.js";
import type * as auth from "../auth.js";
import type {
ApiFromModules,
FilterApi,
FunctionReference,
} from "convex/server";
/**
* A utility for referencing Convex functions in your app's API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
declare const fullApi: ApiFromModules<{
adapter: typeof adapter;
auth: typeof auth;
}>;
export type Mounts = {
adapter: {
create: FunctionReference<
"mutation",
"public",
{
input:
| {
data: {
createdAt: number;
email: string;
emailVerified: boolean;
image?: null | string;
name: string;
updatedAt: number;
userId?: null | string;
};
model: "user";
}
| {
data: {
createdAt: number;
expiresAt: number;
ipAddress?: null | string;
token: string;
updatedAt: number;
userAgent?: null | string;
userId: string;
};
model: "session";
}
| {
data: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId: string;
createdAt: number;
idToken?: null | string;
password?: null | string;
providerId: string;
refreshToken?: null | string;
refreshTokenExpiresAt?: null | number;
scope?: null | string;
updatedAt: number;
userId: string;
};
model: "account";
}
| {
data: {
createdAt: number;
expiresAt: number;
identifier: string;
updatedAt: number;
value: string;
};
model: "verification";
}
| {
data: {
createdAt: number;
privateKey: string;
publicKey: string;
};
model: "jwks";
};
onCreateHandle?: string;
select?: Array<string>;
},
any
>;
deleteMany: FunctionReference<
"mutation",
"public",
{
input:
| {
model: "user";
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onDeleteHandle?: string;
paginationOpts: {
cursor: string | null;
endCursor?: string | null;
id?: number;
maximumBytesRead?: number;
maximumRowsRead?: number;
numItems: number;
};
},
any
>;
deleteOne: FunctionReference<
"mutation",
"public",
{
input:
| {
model: "user";
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onDeleteHandle?: string;
},
any
>;
findMany: FunctionReference<
"query",
"public",
{
limit?: number;
model: "user" | "session" | "account" | "verification" | "jwks";
offset?: number;
paginationOpts: {
cursor: string | null;
endCursor?: string | null;
id?: number;
maximumBytesRead?: number;
maximumRowsRead?: number;
numItems: number;
};
sortBy?: { direction: "asc" | "desc"; field: string };
where?: Array<{
connector?: "AND" | "OR";
field: string;
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
},
any
>;
findOne: FunctionReference<
"query",
"public",
{
model: "user" | "session" | "account" | "verification" | "jwks";
select?: Array<string>;
where?: Array<{
connector?: "AND" | "OR";
field: string;
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
},
any
>;
updateMany: FunctionReference<
"mutation",
"public",
{
input:
| {
model: "user";
update: {
createdAt?: number;
email?: string;
emailVerified?: boolean;
image?: null | string;
name?: string;
updatedAt?: number;
userId?: null | string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
update: {
createdAt?: number;
expiresAt?: number;
ipAddress?: null | string;
token?: string;
updatedAt?: number;
userAgent?: null | string;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
update: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId?: string;
createdAt?: number;
idToken?: null | string;
password?: null | string;
providerId?: string;
refreshToken?: null | string;
refreshTokenExpiresAt?: null | number;
scope?: null | string;
updatedAt?: number;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
update: {
createdAt?: number;
expiresAt?: number;
identifier?: string;
updatedAt?: number;
value?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
update: {
createdAt?: number;
privateKey?: string;
publicKey?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onUpdateHandle?: string;
paginationOpts: {
cursor: string | null;
endCursor?: string | null;
id?: number;
maximumBytesRead?: number;
maximumRowsRead?: number;
numItems: number;
};
},
any
>;
updateOne: FunctionReference<
"mutation",
"public",
{
input:
| {
model: "user";
update: {
createdAt?: number;
email?: string;
emailVerified?: boolean;
image?: null | string;
name?: string;
updatedAt?: number;
userId?: null | string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
update: {
createdAt?: number;
expiresAt?: number;
ipAddress?: null | string;
token?: string;
updatedAt?: number;
userAgent?: null | string;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
update: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId?: string;
createdAt?: number;
idToken?: null | string;
password?: null | string;
providerId?: string;
refreshToken?: null | string;
refreshTokenExpiresAt?: null | number;
scope?: null | string;
updatedAt?: number;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
update: {
createdAt?: number;
expiresAt?: number;
identifier?: string;
updatedAt?: number;
value?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
update: {
createdAt?: number;
privateKey?: string;
publicKey?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onUpdateHandle?: string;
},
any
>;
};
};
// For now fullApiWithMounts is only fullApi which provides
// jump-to-definition in component client code.
// Use Mounts for the same type without the inference.
declare const fullApiWithMounts: typeof fullApi;
export declare const api: FilterApi<
typeof fullApiWithMounts,
FunctionReference<any, "public">
>;
export declare const internal: FilterApi<
typeof fullApiWithMounts,
FunctionReference<any, "internal">
>;
export declare const components: {};

View File

@@ -1,23 +0,0 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import { anyApi, componentsGeneric } from "convex/server";
/**
* A utility for referencing Convex functions in your app's API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
export const api = anyApi;
export const internal = anyApi;
export const components = componentsGeneric();

View File

@@ -1,60 +0,0 @@
/* eslint-disable */
/**
* Generated data model types.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type {
DataModelFromSchemaDefinition,
DocumentByName,
TableNamesInDataModel,
SystemTableNames,
} from "convex/server";
import type { GenericId } from "convex/values";
import schema from "../schema.js";
/**
* The names of all of your Convex tables.
*/
export type TableNames = TableNamesInDataModel<DataModel>;
/**
* The type of a document stored in Convex.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Doc<TableName extends TableNames> = DocumentByName<
DataModel,
TableName
>;
/**
* An identifier for a document in Convex.
*
* Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
*
* Documents can be loaded using `db.get(id)` in query and mutation functions.
*
* IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Id<TableName extends TableNames | SystemTableNames> =
GenericId<TableName>;
/**
* A type describing your Convex data model.
*
* This type includes information about what tables you have, the type of
* documents stored in those tables, and the indexes defined on them.
*
* This type is used to parameterize methods like `queryGeneric` and
* `mutationGeneric` to make them type-safe.
*/
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;

View File

@@ -1,149 +0,0 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import {
ActionBuilder,
AnyComponents,
HttpActionBuilder,
MutationBuilder,
QueryBuilder,
GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
GenericDatabaseReader,
GenericDatabaseWriter,
FunctionReference,
} from "convex/server";
import type { DataModel } from "./dataModel.js";
type GenericCtx =
| GenericActionCtx<DataModel>
| GenericMutationCtx<DataModel>
| GenericQueryCtx<DataModel>;
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export declare const query: QueryBuilder<DataModel, "public">;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export declare const mutation: MutationBuilder<DataModel, "public">;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
*/
export declare const action: ActionBuilder<DataModel, "public">;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*/
export declare const internalAction: ActionBuilder<DataModel, "internal">;
/**
* Define an HTTP action.
*
* This function will be used to respond to HTTP requests received by a Convex
* deployment if the requests matches the path and method where this action
* is routed. Be sure to route your action in `convex/http.js`.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
*/
export declare const httpAction: HttpActionBuilder;
/**
* A set of services for use within Convex query functions.
*
* The query context is passed as the first argument to any Convex query
* function run on the server.
*
* This differs from the {@link MutationCtx} because all of the services are
* read-only.
*/
export type QueryCtx = GenericQueryCtx<DataModel>;
/**
* A set of services for use within Convex mutation functions.
*
* The mutation context is passed as the first argument to any Convex mutation
* function run on the server.
*/
export type MutationCtx = GenericMutationCtx<DataModel>;
/**
* A set of services for use within Convex action functions.
*
* The action context is passed as the first argument to any Convex action
* function run on the server.
*/
export type ActionCtx = GenericActionCtx<DataModel>;
/**
* An interface to read from the database within Convex query functions.
*
* The two entry points are {@link DatabaseReader.get}, which fetches a single
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
* building a query.
*/
export type DatabaseReader = GenericDatabaseReader<DataModel>;
/**
* An interface to read from and write to the database within Convex mutation
* functions.
*
* Convex guarantees that all writes within a single mutation are
* executed atomically, so you never have to worry about partial writes leaving
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
* for the guarantees Convex provides your functions.
*/
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;

View File

@@ -1,90 +0,0 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import {
actionGeneric,
httpActionGeneric,
queryGeneric,
mutationGeneric,
internalActionGeneric,
internalMutationGeneric,
internalQueryGeneric,
componentsGeneric,
} from "convex/server";
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export const query = queryGeneric;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export const internalQuery = internalQueryGeneric;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export const mutation = mutationGeneric;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export const internalMutation = internalMutationGeneric;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
*/
export const action = actionGeneric;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*/
export const internalAction = internalActionGeneric;
/**
* Define a Convex HTTP action.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
* as its second.
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
*/
export const httpAction = httpActionGeneric;

View File

@@ -110,7 +110,7 @@ export const salvarConfigEmail = mutation({
* NOTA: Esta action será implementada quando instalarmos nodemailer. * NOTA: Esta action será implementada quando instalarmos nodemailer.
* Por enquanto, retorna sucesso simulado para não bloquear o desenvolvimento. * Por enquanto, retorna sucesso simulado para não bloquear o desenvolvimento.
*/ */
export const testarConexaoSMTP = action({ export const testarConexaoSMTP = mutation({
args: { args: {
servidor: v.string(), servidor: v.string(),
porta: v.number(), porta: v.number(),
@@ -119,43 +119,10 @@ export const testarConexaoSMTP = action({
usarSSL: v.boolean(), usarSSL: v.boolean(),
usarTLS: v.boolean(), usarTLS: v.boolean(),
}, },
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// TODO: Implementar teste real com nodemailer // Delegar para a action de Node em arquivo separado
// Por enquanto, simula sucesso
try { return { sucesso: true };
// Validações básicas
if (!args.servidor || args.servidor.trim() === "") {
return {
sucesso: false as const,
erro: "Servidor SMTP não pode estar vazio",
};
}
if (args.porta < 1 || args.porta > 65535) {
return { sucesso: false as const, erro: "Porta inválida" };
}
// Simular delay de teste
await new Promise((resolve) => setTimeout(resolve, 1000));
// Retornar sucesso simulado
console.log(
"⚠️ AVISO: Teste de conexão SMTP simulado (nodemailer não instalado ainda)"
);
return { sucesso: true as const };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
sucesso: false as const,
erro: errorMessage || "Erro ao testar conexão",
};
}
}, },
}); });

View File

@@ -1,127 +0,0 @@
import { v } from "convex/values";
import { mutation } from "./_generated/server";
/**
* Mutation de teste para criar um funcionário e associar ao usuário TI Master
* Isso permite testar o sistema de férias completo
*/
export const criarFuncionarioParaTIMaster = mutation({
args: {
usuarioEmail: v.string(), // Email do usuário TI Master
},
returns: v.union(
v.object({ sucesso: v.literal(true), funcionarioId: v.id("funcionarios") }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Buscar usuário
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.usuarioEmail))
.first();
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Verificar se já tem funcionário associado
if (usuario.funcionarioId) {
return { sucesso: false as const, erro: "Usuário já tem funcionário associado" };
}
// Buscar um símbolo qualquer (pegamos o primeiro)
const simbolo = await ctx.db.query("simbolos").first();
if (!simbolo) {
return { sucesso: false as const, erro: "Nenhum símbolo encontrado no sistema" };
}
// Criar funcionário de teste
const funcionarioId = await ctx.db.insert("funcionarios", {
nome: usuario.nome,
cpf: "000.000.000-00", // CPF de teste
rg: "0000000",
endereco: "Endereço de Teste",
bairro: "Centro",
cidade: "Recife",
uf: "PE",
telefone: "(81) 99999-9999",
email: usuario.email,
matricula: usuario.matricula,
admissaoData: "2023-01-01", // Data de admissão: 1 ano atrás
simboloId: simbolo._id,
simboloTipo: simbolo.tipo,
statusFerias: "ativo",
// IMPORTANTE: Definir regime de trabalho
// Altere aqui para testar diferentes regimes:
// - "clt" = CLT (máx 3 períodos, mín 5 dias)
// - "estatutario_pe" = Servidor Público PE (máx 2 períodos, mín 10 dias)
regimeTrabalho: "clt",
// Dados opcionais
descricaoCargo: "Gestor de TI - Cargo de Teste",
nomePai: "Pai de Teste",
nomeMae: "Mãe de Teste",
naturalidade: "Recife",
naturalidadeUF: "PE",
sexo: "masculino",
estadoCivil: "solteiro",
nacionalidade: "Brasileira",
grauInstrucao: "superior_completo",
tipoSanguineo: "O+",
});
// Associar funcionário ao usuário
await ctx.db.patch(usuario._id, {
funcionarioId,
});
return { sucesso: true as const, funcionarioId };
},
});
/**
* Mutation para alterar o regime de trabalho de um funcionário
* Útil para testar diferentes regras (CLT vs Servidor PE)
*/
export const alterarRegimeTrabalho = mutation({
args: {
funcionarioId: v.id("funcionarios"),
novoRegime: v.union(
v.literal("clt"),
v.literal("estatutario_pe"),
v.literal("estatutario_federal"),
v.literal("estatutario_municipal")
),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
await ctx.db.patch(args.funcionarioId, {
regimeTrabalho: args.novoRegime,
});
return { sucesso: true };
},
});
/**
* Mutation para alterar data de admissão
* Útil para testar diferentes períodos aquisitivos
*/
export const alterarDataAdmissao = mutation({
args: {
funcionarioId: v.id("funcionarios"),
novaData: v.string(), // Formato: "YYYY-MM-DD"
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
await ctx.db.patch(args.funcionarioId, {
admissaoData: args.novaData,
});
return { sucesso: true };
},
});

View File

@@ -1,118 +0,0 @@
import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { hashPassword } from "./auth/utils";
/**
* Cria um usuário de teste com funcionário associado
* para testar o sistema de férias
*/
export const criarUsuarioParaTesteFerias = mutation({
args: {},
returns: v.object({
sucesso: v.boolean(),
login: v.string(),
senha: v.string(),
mensagem: v.string(),
}),
handler: async (ctx, args) => {
const loginTeste = "teste.ferias";
const senhaTeste = "Teste@2025";
const emailTeste = "teste.ferias@sgse.pe.gov.br";
const nomeTeste = "João Silva (Teste)";
// Verificar se já existe
const usuarioExistente = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", loginTeste))
.first();
if (usuarioExistente) {
return {
sucesso: true,
login: loginTeste,
senha: senhaTeste,
mensagem: "Usuário de teste já existe! Use as credenciais abaixo.",
};
}
// Buscar role padrão (usuário comum)
const roleUsuario = await ctx.db
.query("roles")
.filter((q) => q.eq(q.field("nome"), "usuario"))
.first();
if (!roleUsuario) {
return {
sucesso: false,
login: "",
senha: "",
mensagem: "Erro: Role 'usuario' não encontrada",
};
}
// Buscar um símbolo qualquer
const simbolo = await ctx.db.query("simbolos").first();
if (!simbolo) {
return {
sucesso: false,
login: "",
senha: "",
mensagem: "Erro: Nenhum símbolo encontrado. Crie um símbolo primeiro.",
};
}
// Criar funcionário
const funcionarioId = await ctx.db.insert("funcionarios", {
nome: nomeTeste,
cpf: "111.222.333-44",
rg: "1234567",
nascimento: "1990-05-15",
endereco: "Rua de Teste, 123",
bairro: "Centro",
cidade: "Recife",
uf: "PE",
cep: "50000-000",
telefone: "(81) 98765-4321",
email: emailTeste,
matricula: loginTeste,
admissaoData: "2023-01-15", // Admitido em jan/2023 (quase 2 anos)
simboloId: simbolo._id,
simboloTipo: simbolo.tipo,
statusFerias: "ativo",
regimeTrabalho: "clt", // CLT para testar
descricaoCargo: "Analista Administrativo",
nomePai: "José Silva",
nomeMae: "Maria Silva",
naturalidade: "Recife",
naturalidadeUF: "PE",
sexo: "masculino",
estadoCivil: "solteiro",
nacionalidade: "Brasileira",
grauInstrucao: "superior",
});
// Criar usuário
const senhaHash = await hashPassword(senhaTeste);
const usuarioId = await ctx.db.insert("usuarios", {
matricula: loginTeste,
senhaHash,
nome: nomeTeste,
email: emailTeste,
funcionarioId,
roleId: roleUsuario._id,
ativo: true,
primeiroAcesso: false, // Já consideramos que fez primeiro acesso
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return {
sucesso: true,
login: loginTeste,
senha: senhaTeste,
mensagem: "Usuário de teste criado com sucesso!",
};
},
});

View File

@@ -1,5 +1,6 @@
import { v } from "convex/values"; import { v } from "convex/values";
import { mutation, query } from "./_generated/server"; import { mutation, query } from "./_generated/server";
import { Id } from "./_generated/dataModel";
// Mutation para fazer upload de arquivo e obter o storage ID // Mutation para fazer upload de arquivo e obter o storage ID
export const generateUploadUrl = mutation({ export const generateUploadUrl = mutation({
@@ -72,9 +73,70 @@ export const getDocumentosUrls = query({
throw new Error("Funcionário não encontrado"); throw new Error("Funcionário não encontrado");
} }
// Gerar URLs para todos os documentos // Tipo exato do retorno para alinhar com o validator
const urls: Record<string, string | null> = {}; type DocumentUrls = {
const campos = [ certidaoAntecedentesPF: string | null;
certidaoAntecedentesJFPE: string | null;
certidaoAntecedentesSDS: string | null;
certidaoAntecedentesTJPE: string | null;
certidaoImprobidade: string | null;
rgFrente: string | null;
rgVerso: string | null;
cpfFrente: string | null;
cpfVerso: string | null;
situacaoCadastralCPF: string | null;
tituloEleitorFrente: string | null;
tituloEleitorVerso: string | null;
comprovanteVotacao: string | null;
carteiraProfissionalFrente: string | null;
carteiraProfissionalVerso: string | null;
comprovantePIS: string | null;
certidaoRegistroCivil: string | null;
certidaoNascimentoDependentes: string | null;
cpfDependentes: string | null;
reservistaDoc: string | null;
comprovanteEscolaridade: string | null;
comprovanteResidencia: string | null;
comprovanteContaBradesco: string | null;
declaracaoAcumulacaoCargo: string | null;
declaracaoDependentesIR: string | null;
declaracaoIdoneidade: string | null;
termoNepotismo: string | null;
termoOpcaoRemuneracao: string | null;
};
const urls: DocumentUrls = {
certidaoAntecedentesPF: null,
certidaoAntecedentesJFPE: null,
certidaoAntecedentesSDS: null,
certidaoAntecedentesTJPE: null,
certidaoImprobidade: null,
rgFrente: null,
rgVerso: null,
cpfFrente: null,
cpfVerso: null,
situacaoCadastralCPF: null,
tituloEleitorFrente: null,
tituloEleitorVerso: null,
comprovanteVotacao: null,
carteiraProfissionalFrente: null,
carteiraProfissionalVerso: null,
comprovantePIS: null,
certidaoRegistroCivil: null,
certidaoNascimentoDependentes: null,
cpfDependentes: null,
reservistaDoc: null,
comprovanteEscolaridade: null,
comprovanteResidencia: null,
comprovanteContaBradesco: null,
declaracaoAcumulacaoCargo: null,
declaracaoDependentesIR: null,
declaracaoIdoneidade: null,
termoNepotismo: null,
termoOpcaoRemuneracao: null,
};
const campos: Array<keyof DocumentUrls> = [
"certidaoAntecedentesPF", "certidaoAntecedentesPF",
"certidaoAntecedentesJFPE", "certidaoAntecedentesJFPE",
"certidaoAntecedentesSDS", "certidaoAntecedentesSDS",
@@ -106,7 +168,9 @@ export const getDocumentosUrls = query({
]; ];
for (const campo of campos) { for (const campo of campos) {
const storageId = funcionario[campo as keyof typeof funcionario] as Id<"_storage"> | undefined; const storageId = (funcionario as Record<string, unknown>)[campo as string] as
| Id<"_storage">
| undefined;
if (storageId) { if (storageId) {
urls[campo] = await ctx.storage.getUrl(storageId); urls[campo] = await ctx.storage.getUrl(storageId);
} else { } else {

View File

@@ -46,6 +46,11 @@ export const enfileirarEmail = mutation({
criadoEm: Date.now(), criadoEm: Date.now(),
}); });
// Agendar envio imediato via action
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
emailId,
});
return { sucesso: true, emailId }; return { sucesso: true, emailId };
}, },
}); });
@@ -94,6 +99,11 @@ export const enviarEmailComTemplate = mutation({
criadoEm: Date.now(), criadoEm: Date.now(),
}); });
// Agendar envio imediato via action
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
emailId,
});
return { sucesso: true, emailId }; return { sucesso: true, emailId };
}, },
}); });
@@ -225,99 +235,7 @@ export const markEmailFalha = internalMutation({
}, },
}); });
export const enviarEmailAction = action({ // Action de envio foi movida para `actions/email.ts`
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodemailer = require("nodemailer");
try {
// Buscar email da fila
const email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId,
});
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
// Buscar configuração SMTP
const config = await ctx.runQuery(
internal.email.getActiveEmailConfig,
{}
);
if (!config) {
return {
sucesso: false,
erro: "Configuração de email não encontrada ou inativa",
};
}
if (!config.testadoEm) {
return {
sucesso: false,
erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!",
};
}
// Marcar como enviando
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId,
});
// Criar transporter do nodemailer
const transporter = nodemailer.createTransport({
host: config.servidor,
port: config.porta,
secure: config.usarSSL,
auth: {
user: config.usuario,
pass: config.senhaHash, // Note: em produção deve ser descriptografado
},
tls: {
rejectUnauthorized: false,
},
});
// Enviar email REAL
const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
});
console.log("✅ Email enviado com sucesso!");
console.log(" Para:", email.destinatario);
console.log(" Assunto:", email.assunto);
console.log(" Message ID:", info.messageId);
// Marcar como enviado
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId,
});
return { sucesso: true };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("❌ Erro ao enviar email:", errorMessage);
// Marcar como falha
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: errorMessage,
});
return { sucesso: false, erro: errorMessage };
}
},
});
/** /**
* Processar fila de emails (cron job - processa emails pendentes) * Processar fila de emails (cron job - processa emails pendentes)
@@ -345,9 +263,7 @@ export const processarFilaEmails = internalMutation({
} }
// Agendar envio via action // Agendar envio via action
// IMPORTANTE: Não podemos chamar action diretamente de mutation await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
// Por isso, usaremos o scheduler com string path
await ctx.scheduler.runAfter(0, "email:enviarEmailAction" as any, {
emailId: email._id, emailId: email._id,
}); });

View File

@@ -54,13 +54,42 @@ export const listarMinhasSolicitacoes = query({
args: { funcionarioId: v.id("funcionarios") }, args: { funcionarioId: v.id("funcionarios") },
// returns não especificado - TypeScript inferirá automaticamente o tipo correto // returns não especificado - TypeScript inferirá automaticamente o tipo correto
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db const solicitacoes = await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario", (q) => .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", args.funcionarioId) q.eq("funcionarioId", args.funcionarioId)
) )
.order("desc") .order("desc")
.collect(); .collect();
// Enriquecer com dados do funcionário e time
const solicitacoesComDetalhes = await Promise.all(
solicitacoes.map(async (s) => {
const funcionario = await ctx.db.get(s.funcionarioId);
// Buscar time do funcionário
const membroTime = await ctx.db
.query("timesMembros")
.withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", s.funcionarioId)
)
.filter((q) => q.eq(q.field("ativo"), true))
.first();
let time = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
}
return {
...s,
funcionario,
time,
};
})
);
return solicitacoesComDetalhes;
}, },
}); });

View File

@@ -48,7 +48,6 @@ export const getAll = query({
// Retornar apenas os campos necessários para listagem // Retornar apenas os campos necessários para listagem
return funcionarios.map((f) => ({ return funcionarios.map((f) => ({
_id: f._id, _id: f._id,
_creationTime: f._creationTime,
nome: f.nome, nome: f.nome,
matricula: f.matricula, matricula: f.matricula,
cpf: f.cpf, cpf: f.cpf,

View File

@@ -190,15 +190,6 @@ export const limparPerfisAntigos = internalMutation({
await ctx.db.delete(perm._id); await ctx.db.delete(perm._id);
} }
// Remover menu permissões associadas
const menuPerms = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", role._id))
.collect();
for (const menuPerm of menuPerms) {
await ctx.db.delete(menuPerm._id);
}
// Remover o role // Remover o role
await ctx.db.delete(role._id); await ctx.db.delete(role._id);

View File

@@ -1,171 +0,0 @@
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
/**
* Migração: Converte estrutura antiga de gestores individuais para times
*
* Esta função cria automaticamente times baseados nos gestores existentes
* e adiciona os funcionários subordinados aos respectivos times.
*
* Execute uma vez via dashboard do Convex:
* Settings > Functions > Internal > migrarParaTimes > executar
*/
export const executar = internalMutation({
args: {},
returns: v.object({
timesCreated: v.number(),
funcionariosAtribuidos: v.number(),
erros: v.array(v.string()),
}),
handler: async (ctx) => {
const erros: string[] = [];
let timesCreated = 0;
let funcionariosAtribuidos = 0;
try {
// 1. Buscar todos os funcionários que têm gestor definido
const funcionariosComGestor = await ctx.db
.query("funcionarios")
.filter((q) => q.neq(q.field("gestorId"), undefined))
.collect();
if (funcionariosComGestor.length === 0) {
return {
timesCreated: 0,
funcionariosAtribuidos: 0,
erros: ["Nenhum funcionário com gestor configurado encontrado"],
};
}
// 2. Agrupar funcionários por gestor
const gestoresMap = new Map<string, Doc<"funcionarios">[]>();
for (const funcionario of funcionariosComGestor) {
if (!funcionario.gestorId) continue;
const gestorId = funcionario.gestorId;
if (!gestoresMap.has(gestorId)) {
gestoresMap.set(gestorId, []);
}
gestoresMap.get(gestorId)!.push(funcionario);
}
// 3. Para cada gestor, criar um time
for (const [gestorId, subordinados] of gestoresMap.entries()) {
try {
const gestor = await ctx.db.get(gestorId);
if (!gestor) {
erros.push(`Gestor ${gestorId} não encontrado`);
continue;
}
// Verificar se já existe time para este gestor
const timeExistente = await ctx.db
.query("times")
.withIndex("by_gestor", (q) => q.eq("gestorId", gestorId))
.filter((q) => q.eq(q.field("ativo"), true))
.first();
let timeId;
if (timeExistente) {
timeId = timeExistente._id;
} else {
// Criar novo time
timeId = await ctx.db.insert("times", {
nome: `Equipe ${gestor.nome}`,
descricao: `Time gerenciado por ${gestor.nome} (migração automática)`,
gestorId: gestorId,
ativo: true,
cor: "#3B82F6",
});
timesCreated++;
}
// Adicionar membros ao time
for (const funcionario of subordinados) {
try {
// Verificar se já está em algum time
const membroExistente = await ctx.db
.query("timesMembros")
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", funcionario._id))
.filter((q) => q.eq(q.field("ativo"), true))
.first();
if (!membroExistente) {
await ctx.db.insert("timesMembros", {
timeId: timeId,
funcionarioId: funcionario._id,
dataEntrada: Date.now(),
ativo: true,
});
funcionariosAtribuidos++;
}
} catch (e) {
erros.push(`Erro ao adicionar ${funcionario.nome} ao time: ${e.message}`);
}
}
} catch (e: any) {
erros.push(`Erro ao processar gestor ${gestorId}: ${e.message}`);
}
}
return {
timesCreated,
funcionariosAtribuidos,
erros,
};
} catch (e: any) {
erros.push(`Erro geral na migração: ${e.message}`);
return {
timesCreated,
funcionariosAtribuidos,
erros,
};
}
},
});
/**
* Função auxiliar para limpar times inativos antigos
*/
export const limparTimesInativos = internalMutation({
args: {
diasInativos: v.optional(v.number()),
},
returns: v.number(),
handler: async (ctx, args) => {
const diasLimite = args.diasInativos || 30;
const dataLimite = Date.now() - (diasLimite * 24 * 60 * 60 * 1000);
const timesInativos = await ctx.db
.query("times")
.filter((q) => q.eq(q.field("ativo"), false))
.collect();
let removidos = 0;
for (const time of timesInativos) {
if (time._creationTime < dataLimite) {
// Remover membros inativos do time
const membrosInativos = await ctx.db
.query("timesMembros")
.withIndex("by_time", (q) => q.eq("timeId", time._id))
.filter((q) => q.eq(q.field("ativo"), false))
.collect();
for (const membro of membrosInativos) {
await ctx.db.delete(membro._id);
}
// Remover o time
await ctx.db.delete(time._id);
removidos++;
}
}
return removidos;
},
});

View File

@@ -1,211 +0,0 @@
import { internalMutation, query } from "./_generated/server";
import { v } from "convex/values";
/**
* Listar usuários usando o perfil "admin" antigo (nível 0)
*/
export const listarUsuariosAdminAntigo = query({
args: {},
returns: v.array(
v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
roleId: v.id("roles"),
roleNome: v.string(),
roleNivel: v.number(),
})
),
handler: async (ctx) => {
// Buscar todos os perfis "admin"
const allAdmins = await ctx.db
.query("roles")
.filter((q) => q.eq(q.field("nome"), "admin"))
.collect();
console.log("Perfis 'admin' encontrados:", allAdmins.length);
// Identificar o admin antigo (nível 0)
const adminAntigo = allAdmins.find((r) => r.nivel === 0);
if (!adminAntigo) {
console.log("Nenhum admin antigo (nível 0) encontrado");
return [];
}
console.log("Admin antigo encontrado:", adminAntigo);
// Buscar usuários usando este perfil
const usuarios = await ctx.db
.query("usuarios")
.withIndex("by_role", (q) => q.eq("roleId", adminAntigo._id))
.collect();
console.log("Usuários usando admin antigo:", usuarios.length);
return usuarios.map((u) => ({
_id: u._id,
matricula: u.matricula,
nome: u.nome,
email: u.email || "",
roleId: u.roleId,
roleNome: adminAntigo.nome,
roleNivel: adminAntigo.nivel,
}));
},
});
/**
* Migrar usuários do perfil "admin" antigo (nível 0) para o novo (nível 2)
*/
export const migrarUsuariosParaAdminNovo = internalMutation({
args: {},
returns: v.object({
migrados: v.number(),
usuariosMigrados: v.array(
v.object({
matricula: v.string(),
nome: v.string(),
roleAntigo: v.string(),
roleNovo: v.string(),
})
),
}),
handler: async (ctx) => {
// Buscar todos os perfis "admin"
const allAdmins = await ctx.db
.query("roles")
.filter((q) => q.eq(q.field("nome"), "admin"))
.collect();
// Identificar admin antigo (nível 0) e admin novo (nível 2)
const adminAntigo = allAdmins.find((r) => r.nivel === 0);
const adminNovo = allAdmins.find((r) => r.nivel === 2);
if (!adminAntigo) {
console.log("❌ Admin antigo (nível 0) não encontrado");
return { migrados: 0, usuariosMigrados: [] };
}
if (!adminNovo) {
console.log("❌ Admin novo (nível 2) não encontrado");
return { migrados: 0, usuariosMigrados: [] };
}
console.log("✅ Admin antigo ID:", adminAntigo._id, "- Nível:", adminAntigo.nivel);
console.log("✅ Admin novo ID:", adminNovo._id, "- Nível:", adminNovo.nivel);
// Buscar usuários usando o admin antigo
const usuarios = await ctx.db
.query("usuarios")
.withIndex("by_role", (q) => q.eq("roleId", adminAntigo._id))
.collect();
console.log(`📊 Encontrados ${usuarios.length} usuário(s) para migrar`);
const usuariosMigrados: Array<{
matricula: string;
nome: string;
roleAntigo: string;
roleNovo: string;
}> = [];
// Migrar cada usuário
for (const usuario of usuarios) {
await ctx.db.patch(usuario._id, {
roleId: adminNovo._id,
});
usuariosMigrados.push({
matricula: usuario.matricula,
nome: usuario.nome,
roleAntigo: `admin (nível 0) - ${adminAntigo._id}`,
roleNovo: `admin (nível 2) - ${adminNovo._id}`,
});
console.log(
`✅ MIGRADO: ${usuario.nome} (${usuario.matricula}) → admin nível 2`
);
}
return {
migrados: usuarios.length,
usuariosMigrados,
};
},
});
/**
* Remover perfil "admin" antigo (nível 0) após migração
*/
export const removerAdminAntigo = internalMutation({
args: {},
returns: v.object({
sucesso: v.boolean(),
mensagem: v.string(),
}),
handler: async (ctx) => {
// Buscar todos os perfis "admin"
const allAdmins = await ctx.db
.query("roles")
.filter((q) => q.eq(q.field("nome"), "admin"))
.collect();
// Identificar admin antigo (nível 0)
const adminAntigo = allAdmins.find((r) => r.nivel === 0);
if (!adminAntigo) {
return {
sucesso: false,
mensagem: "Admin antigo (nível 0) não encontrado",
};
}
// Verificar se ainda há usuários usando
const usuarios = await ctx.db
.query("usuarios")
.withIndex("by_role", (q) => q.eq("roleId", adminAntigo._id))
.collect();
if (usuarios.length > 0) {
return {
sucesso: false,
mensagem: `Ainda há ${usuarios.length} usuário(s) usando este perfil. Execute migrarUsuariosParaAdminNovo primeiro.`,
};
}
// Remover permissões associadas
const permissoes = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", adminAntigo._id))
.collect();
for (const perm of permissoes) {
await ctx.db.delete(perm._id);
}
// Remover menu permissões associadas
const menuPerms = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", adminAntigo._id))
.collect();
for (const menuPerm of menuPerms) {
await ctx.db.delete(menuPerm._id);
}
// Remover o perfil
await ctx.db.delete(adminAntigo._id);
console.log(
`🗑️ REMOVIDO: Admin antigo (nível 0) - ${adminAntigo._id}`
);
return {
sucesso: true,
mensagem: "Admin antigo removido com sucesso",
};
},
});

View File

@@ -191,23 +191,36 @@ export const obterMetricas = query({
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
let query = ctx.db.query("systemMetrics"); // Construir consulta respeitando tipos sem reatribuições
let metricas;
// Filtrar por data se fornecido if (args.dataInicio !== undefined && args.dataFim !== undefined) {
if (args.dataInicio !== undefined || args.dataFim !== undefined) { const inicio: number = args.dataInicio as number;
query = query.withIndex("by_timestamp", (q) => { const fim: number = args.dataFim as number;
if (args.dataInicio !== undefined && args.dataFim !== undefined) { metricas = await ctx.db
return q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim); .query("systemMetrics")
} else if (args.dataInicio !== undefined) { .withIndex("by_timestamp", (q) =>
return q.gte("timestamp", args.dataInicio); q.gte("timestamp", inicio).lte("timestamp", fim)
} else { )
return q.lte("timestamp", args.dataFim!); .order("desc")
} .collect();
}); } else if (args.dataInicio !== undefined) {
const inicio: number = args.dataInicio as number;
metricas = await ctx.db
.query("systemMetrics")
.withIndex("by_timestamp", (q) => q.gte("timestamp", inicio))
.order("desc")
.collect();
} else if (args.dataFim !== undefined) {
const fim: number = args.dataFim as number;
metricas = await ctx.db
.query("systemMetrics")
.withIndex("by_timestamp", (q) => q.lte("timestamp", fim))
.order("desc")
.collect();
} else {
metricas = await ctx.db.query("systemMetrics").order("desc").collect();
} }
let metricas = await query.order("desc").collect();
// Limitar resultados // Limitar resultados
if (args.limit !== undefined && args.limit > 0) { if (args.limit !== undefined && args.limit > 0) {
metricas = metricas.slice(0, args.limit); metricas = metricas.slice(0, args.limit);
@@ -298,10 +311,10 @@ export const verificarAlertasInternal = internalMutation({
.collect(); .collect();
for (const alerta of alertasAtivos) { for (const alerta of alertasAtivos) {
// Obter valor da métrica correspondente // Obter valor da métrica correspondente, validando tipo número
const metricValue = (metrica as Record<string, number>)[alerta.metricName]; const rawValue = (metrica as Record<string, unknown>)[alerta.metricName];
if (typeof rawValue !== "number") continue;
if (metricValue === undefined) continue; const metricValue = rawValue;
// Verificar se o alerta deve ser disparado // Verificar se o alerta deve ser disparado
let shouldTrigger = false; let shouldTrigger = false;
@@ -353,11 +366,16 @@ export const verificarAlertasInternal = internalMutation({
// Criar notificação no chat se configurado // Criar notificação no chat se configurado
if (alerta.notifyByChat) { if (alerta.notifyByChat) {
// Buscar usuários TI para notificar // Buscar roles administrativas (nível <= 1) e filtrar usuários por roleId
const rolesAdminOuTi = await ctx.db
.query("roles")
.filter((q) => q.lte(q.field("nivel"), 1))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
const usuarios = await ctx.db.query("usuarios").collect(); const usuarios = await ctx.db.query("usuarios").collect();
const usuariosTI = usuarios.filter( const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId));
(u) => u.role?.nome === "ti" || u.role?.nivel === 0
);
for (const usuario of usuariosTI) { for (const usuario of usuariosTI) {
await ctx.db.insert("notificacoes", { await ctx.db.insert("notificacoes", {

View File

@@ -1,436 +1 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { api } from "./_generated/api";
import { Id } from "./_generated/dataModel";
/**
* Listar todos os perfis customizados
*/
export const listarPerfisCustomizados = query({
args: {},
// Tipo inferido automaticamente pelo Convex
handler: async (ctx) => {
const perfis = await ctx.db.query("perfisCustomizados").collect();
// Buscar role correspondente para cada perfil
const perfisComDetalhes = await Promise.all(
perfis.map(async (perfil) => {
const role = await ctx.db.get(perfil.roleId);
const criador = await ctx.db.get(perfil.criadoPor);
// Contar usuários usando este perfil
const usuarios = await ctx.db
.query("usuarios")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
return {
...perfil,
roleNome: role?.nome || "Desconhecido",
criadorNome: criador?.nome || "Desconhecido",
numeroUsuarios: usuarios.length,
};
})
);
return perfisComDetalhes;
},
});
/**
* Obter perfil com permissões detalhadas
*/
export const obterPerfilComPermissoes = query({
args: {
perfilId: v.id("perfisCustomizados"),
},
returns: v.union(
v.object({
perfil: v.any(), // Doc<"perfisCustomizados"> não pode ser validado diretamente
role: v.any(), // Doc<"roles"> não pode ser validado diretamente
permissoes: v.array(v.any()), // Doc<"permissoes">[] não pode ser validado diretamente
menuPermissoes: v.array(v.any()), // Doc<"menuPermissoes">[] não pode ser validado diretamente
usuarios: v.array(v.any()), // Doc<"usuarios">[] não pode ser validado diretamente
}),
v.null()
),
handler: async (ctx, args) => {
const perfil = await ctx.db.get(args.perfilId);
if (!perfil) {
return null;
}
const role = await ctx.db.get(perfil.roleId);
if (!role) {
return null;
}
// Buscar permissões do role
const rolePermissoes = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
const permissoes = await Promise.all(
rolePermissoes.map(async (rp) => {
return await ctx.db.get(rp.permissaoId);
})
);
// Buscar permissões de menu
const menuPermissoes = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
// Buscar usuários usando este perfil
const usuarios = await ctx.db
.query("usuarios")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
return {
perfil,
role,
permissoes: permissoes.filter((p) => p !== null),
menuPermissoes,
usuarios,
};
},
});
/**
* Criar perfil customizado (apenas TI_MASTER)
*/
export const criarPerfilCustomizado = mutation({
args: {
nome: v.string(),
descricao: v.string(),
nivel: v.number(), // >= 3
clonarDeRoleId: v.optional(v.id("roles")), // role para copiar permissões
criadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
perfilId: v.id("perfisCustomizados"),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Validar nível (deve ser >= 3)
if (args.nivel < 3) {
return {
sucesso: false as const,
erro: "Perfis customizados devem ter nível >= 3",
};
}
// Verificar se nome já existe
const roles = await ctx.db.query("roles").collect();
const nomeExiste = roles.some(
(r) => r.nome.toLowerCase() === args.nome.toLowerCase()
);
if (nomeExiste) {
return {
sucesso: false as const,
erro: "Já existe um perfil com este nome",
};
}
// Criar role correspondente
const roleId = await ctx.db.insert("roles", {
nome: args.nome.toLowerCase().replace(/\s+/g, "_"),
descricao: args.descricao,
nivel: args.nivel,
customizado: true,
criadoPor: args.criadoPorId,
editavel: true,
});
// Copiar permissões se especificado
if (args.clonarDeRoleId) {
// Copiar permissões gerais
const permissoesClonar = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
.collect();
for (const perm of permissoesClonar) {
await ctx.db.insert("rolePermissoes", {
roleId,
permissaoId: perm.permissaoId,
});
}
// Copiar permissões de menu
const menuPermsClonar = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
.collect();
for (const menuPerm of menuPermsClonar) {
await ctx.db.insert("menuPermissoes", {
roleId,
menuPath: menuPerm.menuPath,
podeAcessar: menuPerm.podeAcessar,
podeConsultar: menuPerm.podeConsultar,
podeGravar: menuPerm.podeGravar,
});
}
}
// Criar perfil customizado
const perfilId = await ctx.db.insert("perfisCustomizados", {
nome: args.nome,
descricao: args.descricao,
nivel: args.nivel,
roleId,
criadoPor: args.criadoPorId,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.criadoPorId,
"criar",
"perfis",
JSON.stringify({ perfilId, nome: args.nome, nivel: args.nivel }),
perfilId
);
return { sucesso: true as const, perfilId };
},
});
/**
* Editar perfil customizado (apenas TI_MASTER)
*/
export const editarPerfilCustomizado = mutation({
args: {
perfilId: v.id("perfisCustomizados"),
nome: v.optional(v.string()),
descricao: v.optional(v.string()),
editadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const perfil = await ctx.db.get(args.perfilId);
if (!perfil) {
return { sucesso: false as const, erro: "Perfil não encontrado" };
}
// Atualizar perfil
const updates: Partial<Doc<"perfisCustomizados">> & { atualizadoEm: number } = {
atualizadoEm: Date.now(),
};
if (args.nome !== undefined) updates.nome = args.nome;
if (args.descricao !== undefined) updates.descricao = args.descricao;
await ctx.db.patch(args.perfilId, updates);
// Atualizar role correspondente se nome mudou
if (args.nome !== undefined) {
await ctx.db.patch(perfil.roleId, {
nome: args.nome.toLowerCase().replace(/\s+/g, "_"),
});
}
if (args.descricao !== undefined) {
await ctx.db.patch(perfil.roleId, {
descricao: args.descricao,
});
}
// Log de atividade
await registrarAtividade(
ctx,
args.editadoPorId,
"editar",
"perfis",
JSON.stringify(updates),
args.perfilId
);
return { sucesso: true as const };
},
});
/**
* Excluir perfil customizado (apenas TI_MASTER)
*/
export const excluirPerfilCustomizado = mutation({
args: {
perfilId: v.id("perfisCustomizados"),
excluidoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const perfil = await ctx.db.get(args.perfilId);
if (!perfil) {
return { sucesso: false as const, erro: "Perfil não encontrado" };
}
// Verificar se existem usuários usando este perfil
const usuarios = await ctx.db
.query("usuarios")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
if (usuarios.length > 0) {
return {
sucesso: false as const,
erro: `Não é possível excluir. ${usuarios.length} usuário(s) ainda usa(m) este perfil.`,
};
}
// Remover permissões associadas ao role
const rolePermissoes = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
for (const rp of rolePermissoes) {
await ctx.db.delete(rp._id);
}
// Remover permissões de menu
const menuPermissoes = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfil.roleId))
.collect();
for (const mp of menuPermissoes) {
await ctx.db.delete(mp._id);
}
// Excluir role
await ctx.db.delete(perfil.roleId);
// Excluir perfil
await ctx.db.delete(args.perfilId);
// Log de atividade
await registrarAtividade(
ctx,
args.excluidoPorId,
"excluir",
"perfis",
JSON.stringify({ perfilId: args.perfilId, nome: perfil.nome }),
args.perfilId
);
return { sucesso: true as const };
},
});
/**
* Clonar perfil existente
*/
export const clonarPerfil = mutation({
args: {
perfilOrigemId: v.id("perfisCustomizados"),
novoNome: v.string(),
novaDescricao: v.string(),
criadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
perfilId: v.id("perfisCustomizados"),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const perfilOrigem = await ctx.db.get(args.perfilOrigemId);
if (!perfilOrigem) {
return { sucesso: false as const, erro: "Perfil origem não encontrado" };
}
// Verificar se nome já existe
const roles = await ctx.db.query("roles").collect();
const nomeExiste = roles.some(
(r) => r.nome.toLowerCase() === args.novoNome.toLowerCase()
);
if (nomeExiste) {
return {
sucesso: false as const,
erro: "Já existe um perfil com este nome",
};
}
// Criar role correspondente
const roleId = await ctx.db.insert("roles", {
nome: args.novoNome.toLowerCase().replace(/\s+/g, "_"),
descricao: args.novaDescricao,
nivel: perfilOrigem.nivel,
customizado: true,
criadoPor: args.criadoPorId,
editavel: true,
});
// Copiar permissões gerais do perfil de origem
const permissoesClonar = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId))
.collect();
for (const perm of permissoesClonar) {
await ctx.db.insert("rolePermissoes", {
roleId,
permissaoId: perm.permissaoId,
});
}
// Copiar permissões de menu
const menuPermsClonar = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId))
.collect();
for (const menuPerm of menuPermsClonar) {
await ctx.db.insert("menuPermissoes", {
roleId,
menuPath: menuPerm.menuPath,
podeAcessar: menuPerm.podeAcessar,
podeConsultar: menuPerm.podeConsultar,
podeGravar: menuPerm.podeGravar,
});
}
// Criar perfil customizado
const perfilId = await ctx.db.insert("perfisCustomizados", {
nome: args.novoNome,
descricao: args.novaDescricao,
nivel: perfilOrigem.nivel,
roleId,
criadoPor: args.criadoPorId,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.criadoPorId,
"criar",
"perfis",
JSON.stringify({
perfilId,
nome: args.novoNome,
nivel: perfilOrigem.nivel,
}),
perfilId
);
return { sucesso: true as const, perfilId };
},
});

View File

@@ -1,5 +1,6 @@
import { query, mutation, internalQuery } from "./_generated/server"; import { query, mutation, internalQuery } from "./_generated/server";
import { v } from "convex/values"; import { v } from "convex/values";
import type { Doc } from "./_generated/dataModel";
// Catálogo base de recursos e ações // Catálogo base de recursos e ações
// Ajuste/expanda conforme os módulos disponíveis no sistema // Ajuste/expanda conforme os módulos disponíveis no sistema

View File

@@ -14,9 +14,6 @@ export const listar = query({
descricao: v.string(), descricao: v.string(),
nivel: v.number(), nivel: v.number(),
setor: v.optional(v.string()), setor: v.optional(v.string()),
customizado: v.optional(v.boolean()),
editavel: v.optional(v.boolean()),
criadoPor: v.optional(v.id("usuarios")),
}) })
), ),
handler: async (ctx) => { handler: async (ctx) => {
@@ -45,3 +42,4 @@ export const buscarPorId = query({
return await ctx.db.get(args.roleId); return await ctx.db.get(args.roleId);
}, },
}); });

View File

@@ -2,6 +2,7 @@ import { v } from "convex/values";
import { query, mutation, internalMutation } from "./_generated/server"; import { query, mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api"; import { internal } from "./_generated/api";
import { Id } from "./_generated/dataModel"; import { Id } from "./_generated/dataModel";
import type { QueryCtx } from "./_generated/server";
/** /**
* SISTEMA DE CÁLCULO DE SALDO DE FÉRIAS * SISTEMA DE CÁLCULO DE SALDO DE FÉRIAS
@@ -126,7 +127,7 @@ export const obterSaldo = query({
.first(); .first();
if (!periodo) { if (!periodo) {
// Se não existe, criar automaticamente // Se não existe, calcular e retornar dados previstos sem mutar o banco
const funcionario = await ctx.db.get(args.funcionarioId); const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario || !funcionario.admissaoData) return null; if (!funcionario || !funcionario.admissaoData) return null;
@@ -139,23 +140,14 @@ export const obterSaldo = query({
if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito
const dataInicio = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao - 1); const dataInicio = calcularDataFimPeriodo(
const dataFim = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao); funcionario.admissaoData,
anosDesdeAdmissao - 1
// Criar período aquisitivo );
await ctx.db.insert("periodosAquisitivos", { const dataFim = calcularDataFimPeriodo(
funcionarioId: args.funcionarioId, funcionario.admissaoData,
anoReferencia: args.anoReferencia, anosDesdeAdmissao
dataInicio, );
dataFim,
diasDireito: 30,
diasUsados: 0,
diasPendentes: 0,
diasDisponiveis: 30,
abonoPermitido: config.abonoPermitido,
diasAbono: 0,
status: "ativo",
});
return { return {
anoReferencia: args.anoReferencia, anoReferencia: args.anoReferencia,

View File

@@ -366,32 +366,6 @@ export default defineSchema({
.index("by_role", ["roleId"]) .index("by_role", ["roleId"])
.index("by_permissao", ["permissaoId"]), .index("by_permissao", ["permissaoId"]),
// Permissões de Menu (granulares por role)
menuPermissoes: defineTable({
roleId: v.id("roles"),
menuPath: v.string(), // "/recursos-humanos", "/financeiro", etc.
podeAcessar: v.boolean(),
podeConsultar: v.boolean(), // Pode apenas visualizar
podeGravar: v.boolean(), // Pode criar/editar/excluir
})
.index("by_role", ["roleId"])
.index("by_menu", ["menuPath"])
.index("by_role_and_menu", ["roleId", "menuPath"]),
// Permissões de Menu Personalizadas (por matrícula)
menuPermissoesPersonalizadas: defineTable({
usuarioId: v.id("usuarios"),
matricula: v.string(), // Para facilitar busca
menuPath: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
})
.index("by_usuario", ["usuarioId"])
.index("by_matricula", ["matricula"])
.index("by_usuario_and_menu", ["usuarioId", "menuPath"])
.index("by_matricula_and_menu", ["matricula", "menuPath"]),
sessoes: defineTable({ sessoes: defineTable({
usuarioId: v.id("usuarios"), usuarioId: v.id("usuarios"),
token: v.string(), token: v.string(),
@@ -473,19 +447,6 @@ export default defineSchema({
.index("by_data_inicio", ["dataInicio"]), .index("by_data_inicio", ["dataInicio"]),
// Perfis Customizados // Perfis Customizados
perfisCustomizados: defineTable({
nome: v.string(),
descricao: v.string(),
nivel: v.number(), // >= 3
roleId: v.id("roles"), // role correspondente criada
criadoPor: v.id("usuarios"), // TI_MASTER que criou
criadoEm: v.number(),
atualizadoEm: v.number(),
})
.index("by_nome", ["nome"])
.index("by_nivel", ["nivel"])
.index("by_criado_por", ["criadoPor"])
.index("by_role", ["roleId"]),
// Templates de Mensagens // Templates de Mensagens
templatesMensagens: defineTable({ templatesMensagens: defineTable({

View File

@@ -1,6 +1,8 @@
import { internalMutation } from "./_generated/server"; import { internalMutation, mutation, query } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values"; import { v } from "convex/values";
import { hashPassword } from "./auth/utils"; import { hashPassword } from "./auth/utils";
import { Id } from "./_generated/dataModel";
// Dados exportados do Convex Cloud // Dados exportados do Convex Cloud
const simbolosData = [ const simbolosData = [
@@ -246,203 +248,41 @@ export const seedDatabase = internalMutation({
}); });
console.log(" ✅ Role criada: financeiro"); console.log(" ✅ Role criada: financeiro");
const roleControladoria = await ctx.db.insert("roles", {
nome: "controladoria",
descricao: "Controladoria",
nivel: 2,
setor: "controladoria",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: controladoria");
const roleLicitacoes = await ctx.db.insert("roles", {
nome: "licitacoes",
descricao: "Licitações",
nivel: 2,
setor: "licitacoes",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: licitacoes");
const roleCompras = await ctx.db.insert("roles", {
nome: "compras",
descricao: "Compras",
nivel: 2,
setor: "compras",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: compras");
const roleJuridico = await ctx.db.insert("roles", {
nome: "juridico",
descricao: "Jurídico",
nivel: 2,
setor: "juridico",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: juridico");
const roleComunicacao = await ctx.db.insert("roles", {
nome: "comunicacao",
descricao: "Comunicação",
nivel: 2,
setor: "comunicacao",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: comunicacao");
const roleProgramasEsportivos = await ctx.db.insert("roles", {
nome: "programas_esportivos",
descricao: "Programas Esportivos",
nivel: 2,
setor: "programas_esportivos",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: programas_esportivos");
const roleSecretariaExecutiva = await ctx.db.insert("roles", {
nome: "secretaria_executiva",
descricao: "Secretaria Executiva",
nivel: 2,
setor: "secretaria_executiva",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: secretaria_executiva");
const roleGestaoPessoas = await ctx.db.insert("roles", {
nome: "gestao_pessoas",
descricao: "Gestão de Pessoas",
nivel: 2,
setor: "gestao_pessoas",
customizado: false,
editavel: false,
});
console.log(" ✅ Role criada: gestao_pessoas");
const roleUsuario = await ctx.db.insert("roles", { const roleUsuario = await ctx.db.insert("roles", {
nome: "usuario", nome: "usuario",
descricao: "Usuário Comum", descricao: "Usuário Padrão",
nivel: 10, nivel: 3,
setor: undefined,
customizado: false, customizado: false,
editavel: false, editavel: false,
}); });
console.log(" ✅ Role criada: usuario"); console.log(" ✅ Role criada: usuario (Nível 3 - Padrão)");
// 2. Criar usuários iniciais // 2. Criar Símbolos (Cargos)
console.log("👤 Criando usuários iniciais..."); console.log("💰 Criando símbolos...");
const simbolosMap = new Map<string, Id<"simbolos">>();
// TI Master
const senhaTIMaster = await hashPassword("TI@123");
const usuarioTIMasterId = await ctx.db.insert("usuarios", {
matricula: "1000",
senhaHash: senhaTIMaster,
nome: "Gestor TI Master",
email: "ti.master@sgse.pe.gov.br",
setor: "ti",
roleId: roleTIMaster as any,
ativo: true,
primeiroAcesso: false,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
console.log(" ✅ TI Master criado (matrícula: 1000, senha: TI@123)");
// Admin (permissões configuráveis)
const senhaAdmin = await hashPassword("Admin@123");
const adminId = await ctx.db.insert("usuarios", {
matricula: "0000",
senhaHash: senhaAdmin,
nome: "Administrador Geral",
email: "admin@sgse.pe.gov.br",
setor: "administrativo",
roleId: roleAdmin as any,
ativo: true,
primeiroAcesso: false,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
console.log(" ✅ Admin criado (matrícula: 0000, senha: Admin@123)");
// 2.1 Criar catálogo de permissões por ação e conceder a Admin/TI
console.log("🔐 Criando permissões por ação...");
const CATALOGO_RECURSOS = [
{ recurso: "dashboard", acoes: ["ver"] },
{
recurso: "funcionarios",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "simbolos",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "usuarios",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "perfis",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
] as const;
const permissaoKeyToId = new Map<string, string>();
for (const item of CATALOGO_RECURSOS) {
for (const acao of item.acoes) {
const nome = `${item.recurso}.${acao}`;
const id = await ctx.db.insert("permissoes", {
nome,
descricao: `Permite ${acao} em ${item.recurso}`,
recurso: item.recurso,
acao,
});
permissaoKeyToId.set(nome, id);
}
}
console.log(`${permissaoKeyToId.size} permissões criadas`);
// Conceder todas permissões a Admin e TI
const rolesParaConceder = [roleAdmin, roleTIUsuario, roleTIMaster];
for (const roleId of rolesParaConceder) {
for (const [, permId] of permissaoKeyToId) {
await ctx.db.insert("rolePermissoes", {
roleId: roleId as any,
permissaoId: permId as any,
});
}
}
console.log(" ✅ Todas as permissões concedidas a Admin e TI");
// 3. Inserir símbolos
console.log("📝 Inserindo símbolos...");
const simbolosMap = new Map<string, string>();
for (const simbolo of simbolosData) { for (const simbolo of simbolosData) {
const id = await ctx.db.insert("simbolos", { const simboloId = await ctx.db.insert("simbolos", {
descricao: simbolo.descricao,
nome: simbolo.nome, nome: simbolo.nome,
repValor: simbolo.repValor, descricao: simbolo.descricao,
tipo: simbolo.tipo, tipo: simbolo.tipo,
valor: simbolo.valor, valor: simbolo.valor,
vencValor: simbolo.vencValor, repValor: simbolo.repValor || "",
vencValor: simbolo.vencValor || "",
}); });
simbolosMap.set(simbolo.nome, id); simbolosMap.set(simbolo.nome, simboloId);
console.log(` ✅ Símbolo criado: ${simbolo.nome}`); console.log(` ✅ Símbolo criado: ${simbolo.nome}`);
} }
// 4. Inserir funcionários // 3. Criar Funcionários
console.log("👥 Inserindo funcionários..."); console.log("👥 Criando funcionários...");
const funcionariosMap = new Map<string, string>(); const funcionariosMap = new Map<string, Id<"funcionarios">>();
for (const funcionario of funcionariosData) { for (const funcionario of funcionariosData) {
const simboloId = simbolosMap.get(funcionario.simboloNome); const simboloId = simbolosMap.get(funcionario.simboloNome);
if (!simboloId) { if (!simboloId) {
console.error( console.log(
` ❌ Símbolo não encontrado: ${funcionario.simboloNome}` ` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`
); );
continue; continue;
@@ -459,7 +299,7 @@ export const seedDatabase = internalMutation({
nascimento: funcionario.nascimento, nascimento: funcionario.nascimento,
nome: funcionario.nome, nome: funcionario.nome,
rg: funcionario.rg, rg: funcionario.rg,
simboloId: simboloId as any, simboloId: simboloId as Id<"simbolos">,
simboloTipo: funcionario.simboloTipo, simboloTipo: funcionario.simboloTipo,
telefone: funcionario.telefone, telefone: funcionario.telefone,
uf: funcionario.uf, uf: funcionario.uf,
@@ -480,8 +320,8 @@ export const seedDatabase = internalMutation({
senhaHash: senhaInicial, senhaHash: senhaInicial,
nome: funcionario.nome, nome: funcionario.nome,
email: funcionario.email, email: funcionario.email,
funcionarioId: funcId as any, funcionarioId: funcId as Id<"funcionarios">,
roleId: roleUsuario as any, roleId: roleUsuario,
ativo: true, ativo: true,
primeiroAcesso: true, primeiroAcesso: true,
criadoEm: Date.now(), criadoEm: Date.now(),
@@ -522,80 +362,28 @@ export const seedDatabase = internalMutation({
} }
await ctx.db.insert("solicitacoesAcesso", dadosSolicitacao); await ctx.db.insert("solicitacoesAcesso", dadosSolicitacao);
console.log(` ✅ Solicitação criada: ${solicitacao.nome}`); console.log(
` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`
);
} }
// 7. Criar templates de mensagens padrão console.log("✨ Seed do banco de dados concluído com sucesso!");
console.log("📧 Criando templates de mensagens padrão..."); return null;
const templatesPadrao = [ }
{ });
codigo: "USUARIO_BLOQUEADO",
nome: "Usuário Bloqueado",
titulo: "Sua conta foi bloqueada",
corpo:
"Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.",
variaveis: ["motivo"],
},
{
codigo: "USUARIO_DESBLOQUEADO",
nome: "Usuário Desbloqueado",
titulo: "Sua conta foi desbloqueada",
corpo:
"Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
variaveis: [],
},
{
codigo: "SENHA_RESETADA",
nome: "Senha Resetada",
titulo: "Sua senha foi resetada",
corpo:
"Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.",
variaveis: ["senha"],
},
{
codigo: "PERMISSAO_ALTERADA",
nome: "Permissão Alterada",
titulo: "Suas permissões foram atualizadas",
corpo:
"Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.",
variaveis: [],
},
{
codigo: "AVISO_GERAL",
nome: "Aviso Geral",
titulo: "{{titulo}}",
corpo: "{{mensagem}}",
variaveis: ["titulo", "mensagem"],
},
{
codigo: "BEM_VINDO",
nome: "Boas-vindas",
titulo: "Bem-vindo ao SGSE",
corpo:
"Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI",
variaveis: ["nome", "matricula", "senha"],
},
];
for (const template of templatesPadrao) { /**
await ctx.db.insert("templatesMensagens", { * Mutation pública para popular o banco de dados com os dados de seed
codigo: template.codigo, * Permite executar via CLI: `npx convex run seed:popularBanco`
nome: template.nome, */
tipo: "sistema" as const, export const popularBanco = mutation({
titulo: template.titulo, args: {},
corpo: template.corpo, returns: v.null(),
variaveis: template.variaveis, handler: async (ctx) => {
criadoEm: Date.now(), console.log("🌱 Executando popularBanco (wrapper público para seedDatabase)...");
}); // Chama a internalMutation para reaproveitar a lógica de seed
console.log(` ✅ Template criado: ${template.nome}`); await ctx.runMutation(internal.seed.seedDatabase, {});
} console.log("✅ Seed concluído pelo wrapper público");
console.log("✨ Seed concluído com sucesso!");
console.log("");
console.log("🔑 CREDENCIAIS DE ACESSO:");
console.log(" Admin: matrícula 0000, senha Admin@123");
console.log(" TI: matrícula 1000, senha TI@123");
console.log(" Funcionários: usar matrícula, senha Mudar@123");
return null; return null;
}, },
}); });
@@ -738,15 +526,6 @@ export const clearDatabase = internalMutation({
); );
// 9. Perfis customizados // 9. Perfis customizados
const perfisCustomizados = await ctx.db
.query("perfisCustomizados")
.collect();
for (const perfil of perfisCustomizados) {
await ctx.db.delete(perfil._id);
}
console.log(
`${perfisCustomizados.length} perfis customizados removidos`
);
// 10. Templates de mensagens // 10. Templates de mensagens
const templatesMensagens = await ctx.db const templatesMensagens = await ctx.db
@@ -809,22 +588,8 @@ export const clearDatabase = internalMutation({
console.log(`${sessoes.length} sessões removidas`); console.log(`${sessoes.length} sessões removidas`);
// 14. Menu-permissões personalizadas // 14. Menu-permissões personalizadas
const menuPermissoesPersonalizadas = await ctx.db
.query("menuPermissoesPersonalizadas")
.collect();
for (const mpp of menuPermissoesPersonalizadas) {
await ctx.db.delete(mpp._id);
}
console.log(
`${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas`
);
// 15. Menu-permissões // 15. Menu-permissões
const menuPermissoes = await ctx.db.query("menuPermissoes").collect();
for (const mp of menuPermissoes) {
await ctx.db.delete(mp._id);
}
console.log(`${menuPermissoes.length} menu-permissões removidas`);
// 16. Role-permissões // 16. Role-permissões
const rolePermissoes = await ctx.db.query("rolePermissoes").collect(); const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
@@ -890,3 +655,301 @@ export const clearDatabase = internalMutation({
return null; return null;
}, },
}); });
/**
* Mutation pública para limpar o banco de dados (para uso via CLI)
* ATENÇÃO: Esta função deleta TODOS os dados do banco!
*/
export const limparBanco = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
// Executa diretamente a limpeza (mesmo código da internalMutation)
console.log("🗑️ Limpando banco de dados...");
// Limpar em ordem (respeitando dependências)
// 1. Tabelas de logs e auditoria
const logsAcesso = await ctx.db.query("logsAcesso").collect();
for (const log of logsAcesso) {
await ctx.db.delete(log._id);
}
console.log(`${logsAcesso.length} logs de acesso removidos`);
const logsLogin = await ctx.db.query("logsLogin").collect();
for (const log of logsLogin) {
await ctx.db.delete(log._id);
}
console.log(`${logsLogin.length} logs de login removidos`);
const logsAtividades = await ctx.db.query("logsAtividades").collect();
for (const log of logsAtividades) {
await ctx.db.delete(log._id);
}
console.log(`${logsAtividades.length} logs de atividades removidos`);
// 2. Sistema de chat
const leituras = await ctx.db.query("leituras").collect();
for (const leitura of leituras) {
await ctx.db.delete(leitura._id);
}
console.log(`${leituras.length} leituras removidas`);
const mensagens = await ctx.db.query("mensagens").collect();
for (const mensagem of mensagens) {
await ctx.db.delete(mensagem._id);
}
console.log(`${mensagens.length} mensagens removidas`);
const digitando = await ctx.db.query("digitando").collect();
for (const d of digitando) {
await ctx.db.delete(d._id);
}
console.log(`${digitando.length} registros de digitando removidos`);
const conversas = await ctx.db.query("conversas").collect();
for (const conversa of conversas) {
await ctx.db.delete(conversa._id);
}
console.log(`${conversas.length} conversas removidas`);
// 3. Notificações
const notificacoes = await ctx.db.query("notificacoes").collect();
for (const notificacao of notificacoes) {
await ctx.db.delete(notificacao._id);
}
console.log(`${notificacoes.length} notificações removidas`);
const notificacoesEmail = await ctx.db.query("notificacoesEmail").collect();
for (const email of notificacoesEmail) {
await ctx.db.delete(email._id);
}
console.log(
`${notificacoesEmail.length} notificações de email removidas`
);
const notificacoesFerias = await ctx.db
.query("notificacoesFerias")
.collect();
for (const notif of notificacoesFerias) {
await ctx.db.delete(notif._id);
}
console.log(
`${notificacoesFerias.length} notificações de férias removidas`
);
// 4. Férias e períodos aquisitivos
const solicitacoesFerias = await ctx.db
.query("solicitacoesFerias")
.collect();
for (const solicitacao of solicitacoesFerias) {
await ctx.db.delete(solicitacao._id);
}
console.log(
`${solicitacoesFerias.length} solicitações de férias removidas`
);
const periodosAquisitivos = await ctx.db
.query("periodosAquisitivos")
.collect();
for (const periodo of periodosAquisitivos) {
await ctx.db.delete(periodo._id);
}
console.log(
`${periodosAquisitivos.length} períodos aquisitivos removidos`
);
// 5. Atestados
const atestados = await ctx.db.query("atestados").collect();
for (const atestado of atestados) {
await ctx.db.delete(atestado._id);
}
console.log(`${atestados.length} atestados removidos`);
// 6. Times e membros
const timesMembros = await ctx.db.query("timesMembros").collect();
for (const membro of timesMembros) {
await ctx.db.delete(membro._id);
}
console.log(`${timesMembros.length} membros de times removidos`);
const times = await ctx.db.query("times").collect();
for (const time of times) {
await ctx.db.delete(time._id);
}
console.log(`${times.length} times removidos`);
// 7. Cursos
const cursos = await ctx.db.query("cursos").collect();
for (const curso of cursos) {
await ctx.db.delete(curso._id);
}
console.log(`${cursos.length} cursos removidos`);
// 8. Bloqueios de usuários
const bloqueiosUsuarios = await ctx.db.query("bloqueiosUsuarios").collect();
for (const bloqueio of bloqueiosUsuarios) {
await ctx.db.delete(bloqueio._id);
}
console.log(
`${bloqueiosUsuarios.length} bloqueios de usuários removidos`
);
// 9. Perfis customizados (já está no código da internalMutation mas vazio)
// 10. Templates de mensagens
const templatesMensagens = await ctx.db
.query("templatesMensagens")
.collect();
for (const template of templatesMensagens) {
await ctx.db.delete(template._id);
}
console.log(
`${templatesMensagens.length} templates de mensagens removidos`
);
// 11. Configurações
const configuracaoEmail = await ctx.db.query("configuracaoEmail").collect();
for (const config of configuracaoEmail) {
await ctx.db.delete(config._id);
}
console.log(
`${configuracaoEmail.length} configurações de email removidas`
);
const configuracaoAcesso = await ctx.db
.query("configuracaoAcesso")
.collect();
for (const config of configuracaoAcesso) {
await ctx.db.delete(config._id);
}
console.log(
`${configuracaoAcesso.length} configurações de acesso removidas`
);
// 12. Monitoramento
const alertHistory = await ctx.db.query("alertHistory").collect();
for (const alert of alertHistory) {
await ctx.db.delete(alert._id);
}
console.log(`${alertHistory.length} histórico de alertas removido`);
const alertConfigurations = await ctx.db
.query("alertConfigurations")
.collect();
for (const alert of alertConfigurations) {
await ctx.db.delete(alert._id);
}
console.log(
`${alertConfigurations.length} configurações de alertas removidas`
);
const systemMetrics = await ctx.db.query("systemMetrics").collect();
for (const metric of systemMetrics) {
await ctx.db.delete(metric._id);
}
console.log(`${systemMetrics.length} métricas do sistema removidas`);
// 13. Sessões
const sessoes = await ctx.db.query("sessoes").collect();
for (const sessao of sessoes) {
await ctx.db.delete(sessao._id);
}
console.log(`${sessoes.length} sessões removidas`);
// 14. Menu-permissões personalizadas (já está no código da internalMutation mas vazio)
// 15. Menu-permissões (já está no código da internalMutation mas vazio)
// 16. Role-permissões
const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
for (const rp of rolePermissoes) {
await ctx.db.delete(rp._id);
}
console.log(`${rolePermissoes.length} role-permissões removidas`);
// 17. Permissões
const permissoes = await ctx.db.query("permissoes").collect();
for (const permissao of permissoes) {
await ctx.db.delete(permissao._id);
}
console.log(`${permissoes.length} permissões removidas`);
// 18. Usuários (deve vir antes de roles se houver referência)
const usuarios = await ctx.db.query("usuarios").collect();
for (const usuario of usuarios) {
await ctx.db.delete(usuario._id);
}
console.log(`${usuarios.length} usuários removidos`);
// 19. Funcionários
const funcionarios = await ctx.db.query("funcionarios").collect();
for (const funcionario of funcionarios) {
await ctx.db.delete(funcionario._id);
}
console.log(`${funcionarios.length} funcionários removidos`);
// 20. Solicitações de acesso
const solicitacoesAcesso = await ctx.db
.query("solicitacoesAcesso")
.collect();
for (const solicitacao of solicitacoesAcesso) {
await ctx.db.delete(solicitacao._id);
}
console.log(
`${solicitacoesAcesso.length} solicitações de acesso removidas`
);
// 21. Símbolos
const simbolos = await ctx.db.query("simbolos").collect();
for (const simbolo of simbolos) {
await ctx.db.delete(simbolo._id);
}
console.log(`${simbolos.length} símbolos removidos`);
// 22. Roles (deve vir por último se outras tabelas referenciam)
const roles = await ctx.db.query("roles").collect();
for (const role of roles) {
await ctx.db.delete(role._id);
}
console.log(`${roles.length} roles removidas`);
// 23. Todos (tabela de exemplo)
const todos = await ctx.db.query("todos").collect();
for (const todo of todos) {
await ctx.db.delete(todo._id);
}
console.log(`${todos.length} todos removidos`);
console.log("✨ Banco de dados completamente limpo!");
return null;
},
});
/**
* Query para verificar quantos registros existem no banco
*/
export const verificarBanco = query({
args: {},
returns: v.object({
usuarios: v.number(),
funcionarios: v.number(),
roles: v.number(),
simbolos: v.number(),
total: v.number(),
}),
handler: async (ctx) => {
const usuarios = await ctx.db.query("usuarios").collect();
const funcionarios = await ctx.db.query("funcionarios").collect();
const roles = await ctx.db.query("roles").collect();
const simbolos = await ctx.db.query("simbolos").collect();
return {
usuarios: usuarios.length,
funcionarios: funcionarios.length,
roles: roles.length,
simbolos: simbolos.length,
total: usuarios.length + funcionarios.length + roles.length + simbolos.length,
};
},
});

View File

@@ -128,36 +128,69 @@ export const listar = query({
matricula: v.optional(v.string()), matricula: v.optional(v.string()),
ativo: v.optional(v.boolean()), ativo: v.optional(v.boolean()),
}, },
returns: v.array( // returns: v.array(
v.object({ // v.object({
_id: v.id("usuarios"), // _id: v.id("usuarios"),
matricula: v.string(), // matricula: v.string(),
nome: v.string(), // nome: v.string(),
email: v.string(), // email: v.string(),
ativo: v.boolean(), // ativo: v.boolean(),
bloqueado: v.optional(v.boolean()), // bloqueado: v.optional(v.boolean()),
motivoBloqueio: v.optional(v.string()), // motivoBloqueio: v.optional(v.string()),
primeiroAcesso: v.boolean(), // primeiroAcesso: v.boolean(),
ultimoAcesso: v.optional(v.number()), // ultimoAcesso: v.optional(v.number()),
criadoEm: v.number(), // criadoEm: v.number(),
role: v.object({ // role: v.union(
_id: v.id("roles"), // v.object({
nome: v.string(), // _id: v.id("roles"),
nivel: v.number(), // _creationTime: v.optional(v.number()),
setor: v.optional(v.string()), // criadoPor: v.optional(v.id("usuarios")),
}), // customizado: v.optional(v.boolean()),
funcionario: v.optional( // descricao: v.string(),
v.object({ // editavel: v.optional(v.boolean()),
_id: v.id("funcionarios"), // nome: v.string(),
nome: v.string(), // nivel: v.number(),
simboloTipo: v.union( // setor: v.optional(v.string()),
v.literal("cargo_comissionado"), // }),
v.literal("funcao_gratificada") // v.object({
), // _id: v.id("roles"),
}) // _creationTime: v.optional(v.number()),
), // criadoPor: v.optional(v.id("usuarios")),
}) // customizado: v.optional(v.boolean()),
), // descricao: v.literal("Perfil não encontrado"),
// editavel: v.optional(v.boolean()),
// nome: v.literal("erro_role_ausente"),
// nivel: v.literal(999),
// setor: v.optional(v.string()),
// erro: v.literal(true),
// })
// ),
// funcionario: v.optional(
// v.object({
// _id: v.id("funcionarios"),
// nome: v.string(),
// matricula: v.optional(v.string()),
// descricaoCargo: v.optional(v.string()),
// simboloTipo: v.union(
// v.literal("cargo_comissionado"),
// v.literal("funcao_gratificada")
// ),
// })
// ),
// avisos: v.optional(
// v.array(
// v.object({
// tipo: v.union(
// v.literal("erro"),
// v.literal("aviso"),
// v.literal("info")
// ),
// mensagem: v.string(),
// })
// )
// ),
// })
// ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
let usuarios = await ctx.db.query("usuarios").collect(); let usuarios = await ctx.db.query("usuarios").collect();
@@ -173,46 +206,136 @@ export const listar = query({
// Buscar roles e funcionários // Buscar roles e funcionários
const resultado = []; const resultado = [];
const usuariosSemRole: Array<{ nome: string; matricula: string; roleId: Id<"roles"> }> = [];
for (const usuario of usuarios) { for (const usuario of usuarios) {
const role = await ctx.db.get(usuario.roleId); try {
if (!role) continue; const role = await ctx.db.get(usuario.roleId);
// Filtrar por setor // Se a role não existe, criar uma role de erro mas ainda incluir o usuário
if (args.setor && role.setor !== args.setor) { if (!role) {
continue; usuariosSemRole.push({
} nome: usuario.nome,
matricula: usuario.matricula,
roleId: usuario.roleId,
});
let funcionario = undefined; // Filtrar por setor - se filtro está ativo e role não existe, pular
if (usuario.funcionarioId) { if (args.setor) {
const func = await ctx.db.get(usuario.funcionarioId); continue;
if (func) { }
funcionario = {
_id: func._id, // Incluir usuário com role de erro
nome: func.nome, let funcionario = undefined;
simboloTipo: func.simboloTipo, if (usuario.funcionarioId) {
}; try {
const func = await ctx.db.get(usuario.funcionarioId);
if (func) {
funcionario = {
_id: func._id,
nome: func.nome,
matricula: func.matricula,
descricaoCargo: func.descricaoCargo,
simboloTipo: func.simboloTipo,
};
}
} catch (error) {
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error);
}
}
// Criar role de erro (sem _creationTime pois a role não existe)
resultado.push({
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
bloqueado: usuario.bloqueado,
motivoBloqueio: usuario.motivoBloqueio,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
role: {
_id: usuario.roleId,
descricao: "Perfil não encontrado" as const,
nome: "erro_role_ausente" as const,
nivel: 999 as const,
erro: true as const,
},
funcionario,
avisos: [
{
tipo: "erro" as const,
mensagem: `Perfil de acesso (ID: ${usuario.roleId}) não encontrado. Este usuário precisa ter seu perfil reatribuído.`,
},
],
});
continue;
} }
}
resultado.push({ // Filtrar por setor
_id: usuario._id, if (args.setor && role.setor !== args.setor) {
matricula: usuario.matricula, continue;
nome: usuario.nome, }
email: usuario.email,
ativo: usuario.ativo, // Buscar funcionário associado
bloqueado: usuario.bloqueado, let funcionario = undefined;
motivoBloqueio: usuario.motivoBloqueio, if (usuario.funcionarioId) {
primeiroAcesso: usuario.primeiroAcesso, try {
ultimoAcesso: usuario.ultimoAcesso, const func = await ctx.db.get(usuario.funcionarioId);
criadoEm: usuario.criadoEm, if (func) {
role: { funcionario = {
_id: func._id,
nome: func.nome,
matricula: func.matricula,
descricaoCargo: func.descricaoCargo,
simboloTipo: func.simboloTipo,
};
}
} catch (error) {
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error);
}
}
// Construir objeto role - incluir _creationTime se existir (campo automático do Convex)
const roleObj = {
_id: role._id, _id: role._id,
descricao: role.descricao,
nome: role.nome, nome: role.nome,
nivel: role.nivel, nivel: role.nivel,
setor: role.setor, ...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
}, ...(role.customizado !== undefined && { customizado: role.customizado }),
funcionario, ...(role.editavel !== undefined && { editavel: role.editavel }),
}); ...(role.setor !== undefined && { setor: role.setor }),
};
resultado.push({
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
bloqueado: usuario.bloqueado,
motivoBloqueio: usuario.motivoBloqueio,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
role: roleObj,
funcionario,
});
} catch (error) {
console.error(`Erro ao processar usuário ${usuario._id}:`, error);
// Continua processando outros usuários mesmo se houver erro em um
}
}
// Log de usuários sem role para depuração
if (usuariosSemRole.length > 0) {
console.warn(
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
usuariosSemRole.map((u) => `${u.nome} (${u.matricula}) - RoleID: ${u.roleId}`)
);
} }
return resultado; return resultado;