Feat ausencia #7

Merged
deyvisonwanderley merged 8 commits from feat-ausencia into master 2025-11-05 13:49:12 +00:00
14 changed files with 4375 additions and 498 deletions
Showing only changes of commit c86397f150 - Show all commits

View File

@@ -5,7 +5,7 @@ root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = false trim_trailing_whitespace = false

View File

@@ -1 +1 @@
nodejs 25.0.0 nodejs 22.21.1

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,6 @@
}); });
// Estados do formulário // Estados do formulário
let matricula = $state("");
let nome = $state(""); let nome = $state("");
let email = $state(""); let email = $state("");
let roleId = $state(""); let roleId = $state("");
@@ -30,7 +29,9 @@
let senhaInicial = $state(""); let senhaInicial = $state("");
let confirmarSenha = $state(""); let confirmarSenha = $state("");
let processando = $state(false); let processando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null); let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
null,
);
function mostrarMensagem(tipo: "success" | "error", texto: string) { function mostrarMensagem(tipo: "success" | "error", texto: string) {
mensagem = { tipo, texto }; mensagem = { tipo, texto };
@@ -43,8 +44,7 @@
e.preventDefault(); e.preventDefault();
// Validações // Validações
const matriculaStr = String(matricula).trim(); if (!nome.trim() || !email.trim() || !roleId || !senhaInicial) {
if (!matriculaStr || !nome.trim() || !email.trim() || !roleId || !senhaInicial) {
mostrarMensagem("error", "Preencha todos os campos obrigatórios"); mostrarMensagem("error", "Preencha todos os campos obrigatórios");
return; return;
} }
@@ -63,11 +63,12 @@
try { try {
const resultado = await client.mutation(api.usuarios.criar, { const resultado = await client.mutation(api.usuarios.criar, {
matricula: matriculaStr,
nome: nome.trim(), nome: nome.trim(),
email: email.trim(), email: email.trim(),
roleId: roleId as Id<"roles">, roleId: roleId as Id<"roles">,
funcionarioId: funcionarioId ? (funcionarioId as Id<"funcionarios">) : undefined, funcionarioId: funcionarioId
? (funcionarioId as Id<"funcionarios">)
: undefined,
senhaInicial: senhaInicial, senhaInicial: senhaInicial,
}); });
@@ -75,7 +76,7 @@
if (senhaGerada) { if (senhaGerada) {
mostrarMensagem( mostrarMensagem(
"success", "success",
`Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!` `Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!`,
); );
setTimeout(() => { setTimeout(() => {
goto("/ti/usuarios"); goto("/ti/usuarios");
@@ -102,17 +103,19 @@
// Auto-completar ao selecionar funcionário // Auto-completar ao selecionar funcionário
$effect(() => { $effect(() => {
if (funcionarioId && funcionarios?.data) { if (funcionarioId && funcionarios?.data) {
const funcSelecionado = funcionarios.data.find((f: any) => f._id === funcionarioId); const funcSelecionado = funcionarios.data.find(
(f: any) => f._id === funcionarioId,
);
if (funcSelecionado) { if (funcSelecionado) {
email = funcSelecionado.email || email; email = funcSelecionado.email || email;
nome = funcSelecionado.nome || nome; nome = funcSelecionado.nome || nome;
matricula = funcSelecionado.matricula || matricula;
} }
} }
}); });
function gerarSenhaAleatoria() { function gerarSenhaAleatoria() {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!"; const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
let senha = ""; let senha = "";
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
senha += chars.charAt(Math.floor(Math.random() * chars.length)); senha += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -154,8 +157,12 @@
</svg> </svg>
</div> </div>
<div> <div>
<h1 class="text-3xl font-bold text-base-content">Criar Novo Usuário</h1> <h1 class="text-3xl font-bold text-base-content">
<p class="text-base-content/60 mt-1">Cadastre um novo usuário no sistema</p> Criar Novo Usuário
</h1>
<p class="text-base-content/60 mt-1">
Cadastre um novo usuário no sistema
</p>
</div> </div>
</div> </div>
<a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2"> <a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2">
@@ -248,7 +255,9 @@
<!-- Funcionário (primeiro) --> <!-- Funcionário (primeiro) -->
<div class="form-control md:col-span-2"> <div class="form-control md:col-span-2">
<label class="label" for="funcionario"> <label class="label" for="funcionario">
<span class="label-text font-semibold">Vincular Funcionário (Opcional)</span> <span class="label-text font-semibold"
>Vincular Funcionário (Opcional)</span
>
</label> </label>
<select <select
id="funcionario" id="funcionario"
@@ -256,34 +265,24 @@
bind:value={funcionarioId} bind:value={funcionarioId}
disabled={processando || !funcionarios?.data} disabled={processando || !funcionarios?.data}
> >
<option value="">Selecione um funcionário para auto-completar dados</option> <option value=""
>Selecione um funcionário para auto-completar dados</option
>
{#if funcionarios?.data} {#if funcionarios?.data}
{#each funcionarios.data as func} {#each funcionarios.data as func}
<option value={func._id}>{func.nome} - Mat: {func.matricula}</option> <option value={func._id}
>{func.nome} - Mat: {func.matricula}</option
>
{/each} {/each}
{/if} {/if}
</select> </select>
<div class="label"> <div class="label">
<span class="label-text-alt">Ao selecionar, os campos serão preenchidos automaticamente</span> <span class="label-text-alt"
>Ao selecionar, os campos serão preenchidos automaticamente</span
>
</div> </div>
</div> </div>
<!-- Matrícula -->
<div class="form-control">
<label class="label" for="matricula">
<span class="label-text font-semibold">Matrícula *</span>
</label>
<input
id="matricula"
type="number"
placeholder="Ex: 12345"
class="input input-bordered"
bind:value={matricula}
required
disabled={processando}
/>
</div>
<!-- Nome --> <!-- Nome -->
<div class="form-control"> <div class="form-control">
<label class="label" for="nome"> <label class="label" for="nome">
@@ -301,7 +300,7 @@
</div> </div>
<!-- Email --> <!-- Email -->
<div class="form-control md:col-span-2"> <div class="form-control">
<label class="label" for="email"> <label class="label" for="email">
<span class="label-text font-semibold">E-mail *</span> <span class="label-text font-semibold">E-mail *</span>
</label> </label>
@@ -341,7 +340,9 @@
</select> </select>
{#if !roles?.data || !Array.isArray(roles.data)} {#if !roles?.data || !Array.isArray(roles.data)}
<div class="label"> <div class="label">
<span class="label-text-alt text-warning">Carregando perfis disponíveis...</span> <span class="label-text-alt text-warning"
>Carregando perfis disponíveis...</span
>
</div> </div>
{/if} {/if}
</div> </div>
@@ -446,7 +447,9 @@
<div class="flex-1"> <div class="flex-1">
<h3 class="font-bold">Senha Gerada:</h3> <h3 class="font-bold">Senha Gerada:</h3>
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">
<code class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all"> <code
class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all"
>
{senhaGerada} {senhaGerada}
</code> </code>
<button <button
@@ -473,8 +476,8 @@
</button> </button>
</div> </div>
<p class="text-sm mt-2"> <p class="text-sm mt-2">
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará repassá-la ⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará
manualmente ao usuário até que o SMTP seja configurado. repassá-la manualmente ao usuário até que o SMTP seja configurado.
</p> </p>
</div> </div>
</div> </div>
@@ -500,18 +503,27 @@
<h3 class="font-bold">Informações Importantes</h3> <h3 class="font-bold">Informações Importantes</h3>
<ul class="text-sm list-disc list-inside mt-2 space-y-1"> <ul class="text-sm list-disc list-inside mt-2 space-y-1">
<li>O usuário deverá alterar a senha no primeiro acesso</li> <li>O usuário deverá alterar a senha no primeiro acesso</li>
<li>As credenciais devem ser repassadas manualmente (por enquanto)</li>
<li> <li>
Configure o SMTP em <a href="/ti/configuracoes-email" class="link" As credenciais devem ser repassadas manualmente (por enquanto)
>Configurações de Email</a </li>
<li>
Configure o SMTP em <a
href="/ti/configuracoes-email"
class="link">Configurações de Email</a
> para envio automático > para envio automático
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-8 pt-6 border-t border-base-300"> <div
<a href="/ti/usuarios" class="btn btn-ghost gap-2" class:btn-disabled={processando}> class="card-actions justify-end mt-8 pt-6 border-t border-base-300"
>
<a
href="/ti/usuarios"
class="btn btn-ghost gap-2"
class:btn-disabled={processando}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5" class="h-5 w-5"
@@ -528,7 +540,11 @@
</svg> </svg>
Cancelar Cancelar
</a> </a>
<button type="submit" class="btn btn-primary gap-2" disabled={processando}> <button
type="submit"
class="btn btn-primary gap-2"
disabled={processando}
>
{#if processando} {#if processando}
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
Criando Usuário... Criando Usuário...
@@ -556,4 +572,3 @@
</div> </div>
</div> </div>
</ProtectedRoute> </ProtectedRoute>

View File

@@ -294,12 +294,19 @@ export const login = mutation({
timestamp: agora, timestamp: agora,
}); });
// Obter matrícula do funcionário se houver
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
return { return {
sucesso: true as const, sucesso: true as const,
token, token,
usuario: { usuario: {
_id: usuario._id, _id: usuario._id,
matricula: usuario.matricula, matricula: matricula || "",
nome: usuario.nome, nome: usuario.nome,
email: usuario.email, email: usuario.email,
funcionarioId: usuario.funcionarioId, funcionarioId: usuario.funcionarioId,
@@ -568,12 +575,19 @@ export const loginComIP = internalMutation({
timestamp: agora, timestamp: agora,
}); });
// Obter matrícula do funcionário se houver
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
return { return {
sucesso: true as const, sucesso: true as const,
token, token,
usuario: { usuario: {
_id: usuario._id, _id: usuario._id,
matricula: usuario.matricula, matricula: matricula || "",
nome: usuario.nome, nome: usuario.nome,
email: usuario.email, email: usuario.email,
funcionarioId: usuario.funcionarioId, funcionarioId: usuario.funcionarioId,
@@ -688,11 +702,18 @@ export const verificarSessao = query({
return { valido: false as const, motivo: "Role não encontrada" }; return { valido: false as const, motivo: "Role não encontrada" };
} }
// Obter matrícula do funcionário se houver
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
return { return {
valido: true as const, valido: true as const,
usuario: { usuario: {
_id: usuario._id, _id: usuario._id,
matricula: usuario.matricula, matricula: matricula || "",
nome: usuario.nome, nome: usuario.nome,
email: usuario.email, email: usuario.email,
funcionarioId: usuario.funcionarioId, funcionarioId: usuario.funcionarioId,

View File

@@ -94,7 +94,9 @@ export const criarConversa = mutation({
conversaId, conversaId,
remetenteId: usuarioAtual._id, remetenteId: usuarioAtual._id,
titulo: "Adicionado a grupo", titulo: "Adicionado a grupo",
descricao: `Você foi adicionado ao grupo "${args.nome || "Sem nome"}" por ${usuarioAtual.nome}`, descricao: `Você foi adicionado ao grupo "${
args.nome || "Sem nome"
}" por ${usuarioAtual.nome}`,
lida: false, lida: false,
criadaEm: Date.now(), criadaEm: Date.now(),
}); });
@@ -226,7 +228,8 @@ export const enviarMensagem = mutation({
for (const participanteId of conversa.participantes) { for (const participanteId of conversa.participantes) {
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa // ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
const ehOMesmoUsuario = participanteId === usuarioAtual._id; const ehOMesmoUsuario = participanteId === usuarioAtual._id;
const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo; const deveCriarNotificacao =
!ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
if (deveCriarNotificacao) { if (deveCriarNotificacao) {
const tipoNotificacao = args.mencoes?.includes(participanteId) const tipoNotificacao = args.mencoes?.includes(participanteId)
@@ -318,7 +321,10 @@ export const cancelarMensagemAgendada = mutation({
} }
if (mensagem.remetenteId !== usuarioAtual._id) { if (mensagem.remetenteId !== usuarioAtual._id) {
return { sucesso: false, erro: "Você só pode cancelar suas próprias mensagens" }; return {
sucesso: false,
erro: "Você só pode cancelar suas próprias mensagens",
};
} }
if (!mensagem.agendadaPara) { if (!mensagem.agendadaPara) {
@@ -611,7 +617,9 @@ export const listarConversas = query({
// Para conversas individuais, pegar o outro usuário // Para conversas individuais, pegar o outro usuário
let outroUsuario = null; let outroUsuario = null;
if (conversa.tipo === "individual") { if (conversa.tipo === "individual") {
const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id); const outroUsuarioRaw = participantes.find(
(p) => p?._id !== usuarioAtual._id
);
if (outroUsuarioRaw) { if (outroUsuarioRaw) {
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot) // 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id); const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
@@ -620,7 +628,9 @@ export const listarConversas = query({
// Adicionar URL da foto de perfil // Adicionar URL da foto de perfil
let fotoPerfilUrl = null; let fotoPerfilUrl = null;
if (usuarioAtualizado.fotoPerfil) { if (usuarioAtualizado.fotoPerfil) {
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil); fotoPerfilUrl = await ctx.storage.getUrl(
usuarioAtualizado.fotoPerfil
);
} }
outroUsuario = { outroUsuario = {
...usuarioAtualizado, ...usuarioAtualizado,
@@ -756,7 +766,16 @@ export const obterMensagensAgendadas = query({
*/ */
export const listarAgendamentosChat = query({ export const listarAgendamentosChat = query({
args: {}, args: {},
handler: async (ctx): Promise<Array<Doc<"mensagens"> & { conversaInfo: Doc<"conversas"> | null; destinatarioInfo: Doc<"usuarios"> | null }>> => { handler: async (
ctx
): Promise<
Array<
Doc<"mensagens"> & {
conversaInfo: Doc<"conversas"> | null;
destinatarioInfo: Doc<"usuarios"> | null;
}
>
> => {
const usuarioAtual = await getUsuarioAutenticado(ctx); const usuarioAtual = await getUsuarioAutenticado(ctx);
if (!usuarioAtual) { if (!usuarioAtual) {
return []; return [];
@@ -912,20 +931,31 @@ export const listarTodosUsuarios = query({
.withIndex("by_ativo", (q) => q.eq("ativo", true)) .withIndex("by_ativo", (q) => q.eq("ativo", true))
.collect(); .collect();
// Excluir o usuário atual // Excluir o usuário atual e buscar matrículas
return usuarios const usuariosComMatricula = await Promise.all(
usuarios
.filter((u) => u._id !== usuarioAtual._id) .filter((u) => u._id !== usuarioAtual._id)
.map((u) => ({ .map(async (u) => {
let matricula: string | undefined = undefined;
if (u.funcionarioId) {
const funcionario = await ctx.db.get(u.funcionarioId);
matricula = funcionario?.matricula;
}
return {
_id: u._id, _id: u._id,
nome: u.nome, nome: u.nome,
email: u.email, email: u.email,
matricula: u.matricula, matricula,
avatar: u.avatar, avatar: u.avatar,
fotoPerfil: u.fotoPerfil, fotoPerfil: u.fotoPerfil,
statusPresenca: u.statusPresenca, statusPresenca: u.statusPresenca,
statusMensagem: u.statusMensagem, statusMensagem: u.statusMensagem,
setor: u.setor, setor: u.setor,
})); };
})
);
return usuariosComMatricula;
}, },
}); });

View File

@@ -88,9 +88,14 @@ export const listar = query({
if (log.usuarioId) { if (log.usuarioId) {
const user = await ctx.db.get(log.usuarioId); const user = await ctx.db.get(log.usuarioId);
if (user) { if (user) {
let matricula: string | undefined = undefined;
if (user.funcionarioId) {
const funcionario = await ctx.db.get(user.funcionarioId);
matricula = funcionario?.matricula;
}
usuario = { usuario = {
_id: user._id, _id: user._id,
matricula: user.matricula, matricula: matricula || "",
nome: user.nome, nome: user.nome,
}; };
} }

View File

@@ -78,10 +78,15 @@ export const listarAtividades = query({
const atividadesComUsuarios = await Promise.all( const atividadesComUsuarios = await Promise.all(
atividades.map(async (atividade) => { atividades.map(async (atividade) => {
const usuario = await ctx.db.get(atividade.usuarioId); const usuario = await ctx.db.get(atividade.usuarioId);
let matricula = "N/A";
if (usuario?.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula || "N/A";
}
return { return {
...atividade, ...atividade,
usuarioNome: usuario?.nome || "Usuário Desconhecido", usuarioNome: usuario?.nome || "Usuário Desconhecido",
usuarioMatricula: usuario?.matricula || "N/A", usuarioMatricula: matricula,
}; };
}) })
); );
@@ -157,10 +162,15 @@ export const obterHistoricoRecurso = query({
const atividadesComUsuarios = await Promise.all( const atividadesComUsuarios = await Promise.all(
atividades.map(async (atividade) => { atividades.map(async (atividade) => {
const usuario = await ctx.db.get(atividade.usuarioId); const usuario = await ctx.db.get(atividade.usuarioId);
let matricula = "N/A";
if (usuario?.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula || "N/A";
}
return { return {
...atividade, ...atividade,
usuarioNome: usuario?.nome || "Usuário Desconhecido", usuarioNome: usuario?.nome || "Usuário Desconhecido",
usuarioMatricula: usuario?.matricula || "N/A", usuarioMatricula: matricula,
}; };
}) })
); );

View File

@@ -30,18 +30,19 @@ export default defineSchema({
simboloId: v.id("simbolos"), simboloId: v.id("simbolos"),
simboloTipo: simboloTipo, simboloTipo: simboloTipo,
gestorId: v.optional(v.id("usuarios")), gestorId: v.optional(v.id("usuarios")),
statusFerias: v.optional(v.union( statusFerias: v.optional(
v.literal("ativo"), v.union(v.literal("ativo"), v.literal("em_ferias"))
v.literal("em_ferias") ),
)),
// Regime de trabalho (para cálculo correto de férias) // Regime de trabalho (para cálculo correto de férias)
regimeTrabalho: v.optional(v.union( regimeTrabalho: v.optional(
v.union(
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
v.literal("estatutario_federal"), // Servidor Público Federal v.literal("estatutario_federal"), // Servidor Público Federal
v.literal("estatutario_municipal") // Servidor Público Municipal v.literal("estatutario_municipal") // Servidor Público Municipal
)), )
),
// Dados Pessoais Adicionais (opcionais) // Dados Pessoais Adicionais (opcionais)
nomePai: v.optional(v.string()), nomePai: v.optional(v.string()),
@@ -191,10 +192,7 @@ export default defineSchema({
licencas: defineTable({ licencas: defineTable({
funcionarioId: v.id("funcionarios"), funcionarioId: v.id("funcionarios"),
tipo: v.union( tipo: v.union(v.literal("maternidade"), v.literal("paternidade")),
v.literal("maternidade"),
v.literal("paternidade")
),
dataInicio: v.string(), dataInicio: v.string(),
dataFim: v.string(), dataFim: v.string(),
documentoId: v.optional(v.id("_storage")), documentoId: v.optional(v.id("_storage")),
@@ -237,11 +235,15 @@ export default defineSchema({
data: v.number(), data: v.number(),
usuarioId: v.id("usuarios"), usuarioId: v.id("usuarios"),
acao: v.string(), acao: v.string(),
periodosAnteriores: v.optional(v.array(v.object({ periodosAnteriores: v.optional(
v.array(
v.object({
dataInicio: v.string(), dataInicio: v.string(),
dataFim: v.string(), dataFim: v.string(),
diasCorridos: v.number(), diasCorridos: v.number(),
}))), })
)
),
}) })
) )
), ),
@@ -379,7 +381,6 @@ export default defineSchema({
// Sistema de Autenticação e Controle de Acesso // Sistema de Autenticação e Controle de Acesso
usuarios: defineTable({ usuarios: defineTable({
matricula: v.string(),
senhaHash: v.string(), // Senha criptografada com bcrypt senhaHash: v.string(), // Senha criptografada com bcrypt
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
@@ -416,7 +417,6 @@ export default defineSchema({
notificacoesAtivadas: v.optional(v.boolean()), notificacoesAtivadas: v.optional(v.boolean()),
somNotificacao: v.optional(v.boolean()), somNotificacao: v.optional(v.boolean()),
}) })
.index("by_matricula", ["matricula"])
.index("by_email", ["email"]) .index("by_email", ["email"])
.index("by_role", ["roleId"]) .index("by_role", ["roleId"])
.index("by_ativo", ["ativo"]) .index("by_ativo", ["ativo"])
@@ -699,8 +699,7 @@ export default defineSchema({
mensagensPorMinuto: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()), errosCount: v.optional(v.number()),
}) }).index("by_timestamp", ["timestamp"]),
.index("by_timestamp", ["timestamp"]),
alertConfigurations: defineTable({ alertConfigurations: defineTable({
metricName: v.string(), metricName: v.string(),
@@ -717,8 +716,7 @@ export default defineSchema({
notifyByChat: v.boolean(), notifyByChat: v.boolean(),
createdBy: v.id("usuarios"), createdBy: v.id("usuarios"),
lastModified: v.number(), lastModified: v.number(),
}) }).index("by_enabled", ["enabled"]),
.index("by_enabled", ["enabled"]),
alertHistory: defineTable({ alertHistory: defineTable({
configId: v.id("alertConfigurations"), configId: v.id("alertConfigurations"),

View File

@@ -316,7 +316,6 @@ export const seedDatabase = internalMutation({
const senhaInicial = await hashPassword("Mudar@123"); const senhaInicial = await hashPassword("Mudar@123");
await ctx.db.insert("usuarios", { await ctx.db.insert("usuarios", {
matricula: funcionario.matricula,
senhaHash: senhaInicial, senhaHash: senhaInicial,
nome: funcionario.nome, nome: funcionario.nome,
email: funcionario.email, email: funcionario.email,

View File

@@ -4,6 +4,21 @@ import { hashPassword, generateToken } from "./auth/utils";
import { registrarAtividade } from "./logsAtividades"; import { registrarAtividade } from "./logsAtividades";
import { Id, Doc } from "./_generated/dataModel"; import { Id, Doc } from "./_generated/dataModel";
import { api } from "./_generated/api"; import { api } from "./_generated/api";
import type { QueryCtx } from "./_generated/server";
/**
* Helper para obter a matrícula do usuário (do funcionário se houver)
*/
async function obterMatriculaUsuario(
ctx: QueryCtx,
usuario: Doc<"usuarios">
): Promise<string | undefined> {
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
return funcionario?.matricula;
}
return undefined;
}
/** /**
* Associar funcionário a um usuário * Associar funcionário a um usuário
@@ -30,8 +45,11 @@ export const associarFuncionario = mutation({
.first(); .first();
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) { if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
const matricula = await obterMatriculaUsuario(ctx, usuarioExistente);
throw new Error( throw new Error(
`Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})` `Este funcionário já está associado ao usuário: ${
usuarioExistente.nome
}${matricula ? ` (${matricula})` : ""}`
); );
} }
@@ -66,7 +84,6 @@ export const desassociarFuncionario = mutation({
*/ */
export const criar = mutation({ export const criar = mutation({
args: { args: {
matricula: v.string(),
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
roleId: v.id("roles"), roleId: v.id("roles"),
@@ -78,16 +95,6 @@ export const criar = mutation({
v.object({ sucesso: v.literal(false), erro: v.string() }) v.object({ sucesso: v.literal(false), erro: v.string() })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Verificar se matrícula já existe
const existente = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (existente) {
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
}
// Verificar se email já existe // Verificar se email já existe
const emailExistente = await ctx.db const emailExistente = await ctx.db
.query("usuarios") .query("usuarios")
@@ -103,7 +110,6 @@ export const criar = mutation({
// Criar usuário // Criar usuário
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash, senhaHash,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
@@ -194,9 +200,17 @@ export const listar = query({
handler: async (ctx, args) => { handler: async (ctx, args) => {
let usuarios = await ctx.db.query("usuarios").collect(); let usuarios = await ctx.db.query("usuarios").collect();
// Filtrar por matrícula // Filtrar por matrícula (buscar no funcionário)
if (args.matricula) { if (args.matricula) {
usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!)); const usuariosComMatricula = await Promise.all(
usuarios.map(async (u) => {
const matricula = await obterMatriculaUsuario(ctx, u);
return { usuario: u, matricula };
})
);
usuarios = usuariosComMatricula
.filter(({ matricula }) => matricula?.includes(args.matricula!))
.map(({ usuario }) => usuario);
} }
// Filtrar por ativo // Filtrar por ativo
@@ -206,7 +220,11 @@ 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"> }> = []; const usuariosSemRole: Array<{
nome: string;
matricula: string;
roleId: Id<"roles">;
}> = [];
for (const usuario of usuarios) { for (const usuario of usuarios) {
try { try {
@@ -214,9 +232,10 @@ export const listar = query({
// Se a role não existe, criar uma role de erro mas ainda incluir o usuário // Se a role não existe, criar uma role de erro mas ainda incluir o usuário
if (!role) { if (!role) {
const matricula = await obterMatriculaUsuario(ctx, usuario);
usuariosSemRole.push({ usuariosSemRole.push({
nome: usuario.nome, nome: usuario.nome,
matricula: usuario.matricula, matricula: matricula || "N/A",
roleId: usuario.roleId, roleId: usuario.roleId,
}); });
@@ -240,14 +259,19 @@ export const listar = query({
}; };
} }
} catch (error) { } catch (error) {
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error); console.error(
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
error
);
} }
} }
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
// Criar role de erro (sem _creationTime pois a role não existe) // Criar role de erro (sem _creationTime pois a role não existe)
resultado.push({ resultado.push({
_id: usuario._id, _id: usuario._id,
matricula: usuario.matricula, matricula: matriculaUsuario,
nome: usuario.nome, nome: usuario.nome,
email: usuario.email, email: usuario.email,
ativo: usuario.ativo, ativo: usuario.ativo,
@@ -294,7 +318,10 @@ export const listar = query({
}; };
} }
} catch (error) { } catch (error) {
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error); console.error(
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
error
);
} }
} }
@@ -305,14 +332,18 @@ export const listar = query({
nome: role.nome, nome: role.nome,
nivel: role.nivel, nivel: role.nivel,
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }), ...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
...(role.customizado !== undefined && { customizado: role.customizado }), ...(role.customizado !== undefined && {
customizado: role.customizado,
}),
...(role.editavel !== undefined && { editavel: role.editavel }), ...(role.editavel !== undefined && { editavel: role.editavel }),
...(role.setor !== undefined && { setor: role.setor }), ...(role.setor !== undefined && { setor: role.setor }),
}; };
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
resultado.push({ resultado.push({
_id: usuario._id, _id: usuario._id,
matricula: usuario.matricula, matricula: matriculaUsuario,
nome: usuario.nome, nome: usuario.nome,
email: usuario.email, email: usuario.email,
ativo: usuario.ativo, ativo: usuario.ativo,
@@ -334,7 +365,12 @@ export const listar = query({
if (usuariosSemRole.length > 0) { if (usuariosSemRole.length > 0) {
console.warn( console.warn(
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`, `⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
usuariosSemRole.map((u) => `${u.nome} (${u.matricula}) - RoleID: ${u.roleId}`) usuariosSemRole.map(
(u) =>
`${u.nome}${
u.matricula !== "N/A" ? ` (${u.matricula})` : ""
} - RoleID: ${u.roleId}`
)
); );
} }
@@ -559,7 +595,9 @@ export const atualizarPerfil = mutation({
} }
// Atualizar apenas os campos fornecidos // Atualizar apenas os campos fornecidos
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = { atualizadoEm: Date.now() }; const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = {
atualizadoEm: Date.now(),
};
if (args.avatar !== undefined) updates.avatar = args.avatar; if (args.avatar !== undefined) updates.avatar = args.avatar;
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
@@ -591,7 +629,7 @@ export const obterPerfil = query({
_id: v.id("usuarios"), _id: v.id("usuarios"),
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
matricula: v.string(), matricula: v.optional(v.string()),
funcionarioId: v.optional(v.id("funcionarios")), funcionarioId: v.optional(v.id("funcionarios")),
avatar: v.optional(v.string()), avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")), fotoPerfil: v.optional(v.id("_storage")),
@@ -675,11 +713,13 @@ export const obterPerfil = query({
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil); fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
} }
const matricula = await obterMatriculaUsuario(ctx, usuarioAtual);
return { return {
_id: usuarioAtual._id, _id: usuarioAtual._id,
nome: usuarioAtual.nome, nome: usuarioAtual.nome,
email: usuarioAtual.email, email: usuarioAtual.email,
matricula: usuarioAtual.matricula, matricula: matricula || undefined,
funcionarioId: usuarioAtual.funcionarioId, funcionarioId: usuarioAtual.funcionarioId,
avatar: usuarioAtual.avatar, avatar: usuarioAtual.avatar,
fotoPerfil: usuarioAtual.fotoPerfil, fotoPerfil: usuarioAtual.fotoPerfil,
@@ -735,11 +775,13 @@ export const listarParaChat = query({
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
} }
const matricula = await obterMatriculaUsuario(ctx, usuario);
return { return {
_id: usuario._id, _id: usuario._id,
nome: usuario.nome, nome: usuario.nome,
email: usuario.email, email: usuario.email,
matricula: usuario.matricula || undefined, matricula: matricula || undefined,
avatar: usuario.avatar, avatar: usuario.avatar,
fotoPerfil: usuario.fotoPerfil, fotoPerfil: usuario.fotoPerfil,
fotoPerfilUrl, fotoPerfilUrl,
@@ -1035,7 +1077,6 @@ export const editarUsuario = mutation({
*/ */
export const criarAdminMaster = mutation({ export const criarAdminMaster = mutation({
args: { args: {
matricula: v.string(),
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
senha: v.optional(v.string()), senha: v.optional(v.string()),
@@ -1074,32 +1115,9 @@ export const criarAdminMaster = mutation({
}; };
} }
// Se já existir usuário por matrícula, promove/atualiza
const existentePorMatricula = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
const senhaTemporaria = args.senha || gerarSenhaTemporaria(); const senhaTemporaria = args.senha || gerarSenhaTemporaria();
const senhaHash = await hashPassword(senhaTemporaria); const senhaHash = await hashPassword(senhaTemporaria);
if (existentePorMatricula) {
await ctx.db.patch(existentePorMatricula._id, {
nome: args.nome,
email: args.email,
senhaHash,
roleId: roleTIMaster._id,
ativo: true,
primeiroAcesso: true,
atualizadoEm: Date.now(),
});
return {
sucesso: true as const,
usuarioId: existentePorMatricula._id,
senhaTemporaria,
};
}
// Verificar se email já existe // Verificar se email já existe
const existentePorEmail = await ctx.db const existentePorEmail = await ctx.db
.query("usuarios") .query("usuarios")
@@ -1108,7 +1126,6 @@ export const criarAdminMaster = mutation({
if (existentePorEmail) { if (existentePorEmail) {
// Promove usuário existente por email // Promove usuário existente por email
await ctx.db.patch(existentePorEmail._id, { await ctx.db.patch(existentePorEmail._id, {
matricula: args.matricula,
nome: args.nome, nome: args.nome,
senhaHash, senhaHash,
roleId: roleTIMaster._id, roleId: roleTIMaster._id,
@@ -1125,7 +1142,6 @@ export const criarAdminMaster = mutation({
// Criar novo usuário TI Master // Criar novo usuário TI Master
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash, senhaHash,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
@@ -1194,7 +1210,6 @@ export const excluirUsuarioLogico = mutation({
*/ */
export const criarUsuarioCompleto = mutation({ export const criarUsuarioCompleto = mutation({
args: { args: {
matricula: v.string(),
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
roleId: v.id("roles"), roleId: v.id("roles"),
@@ -1212,16 +1227,6 @@ export const criarUsuarioCompleto = mutation({
v.object({ sucesso: v.literal(false), erro: v.string() }) v.object({ sucesso: v.literal(false), erro: v.string() })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Verificar se matrícula já existe
const existente = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (existente) {
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
}
// Verificar se email já existe // Verificar se email já existe
const emailExistente = await ctx.db const emailExistente = await ctx.db
.query("usuarios") .query("usuarios")
@@ -1238,7 +1243,6 @@ export const criarUsuarioCompleto = mutation({
// Criar usuário // Criar usuário
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash, senhaHash,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
@@ -1256,7 +1260,7 @@ export const criarUsuarioCompleto = mutation({
args.criadoPorId, args.criadoPorId,
"criar", "criar",
"usuarios", "usuarios",
JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }), JSON.stringify({ usuarioId, nome: args.nome }),
usuarioId usuarioId
); );
@@ -1272,7 +1276,6 @@ export const criarUsuarioCompleto = mutation({
*/ */
export const criarAdminPadrao = mutation({ export const criarAdminPadrao = mutation({
args: { args: {
matricula: v.optional(v.string()),
nome: v.optional(v.string()), nome: v.optional(v.string()),
email: v.optional(v.string()), email: v.optional(v.string()),
senha: v.optional(v.string()), senha: v.optional(v.string()),
@@ -1282,7 +1285,6 @@ export const criarAdminPadrao = mutation({
usuarioId: v.optional(v.id("usuarios")), usuarioId: v.optional(v.id("usuarios")),
}), }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const matricula = args.matricula ?? "0000";
const nome = args.nome ?? "Administrador Geral"; const nome = args.nome ?? "Administrador Geral";
const email = args.email ?? "admin@sgse.pe.gov.br"; const email = args.email ?? "admin@sgse.pe.gov.br";
const senha = args.senha ?? "Admin@123"; const senha = args.senha ?? "Admin@123";
@@ -1306,12 +1308,7 @@ export const criarAdminPadrao = mutation({
if (!roleAdmin) return { sucesso: false }; if (!roleAdmin) return { sucesso: false };
// Verificar se já existe por matrícula ou email // Verificar se já existe por email
const existentePorMatricula = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
.first();
const existentePorEmail = await ctx.db const existentePorEmail = await ctx.db
.query("usuarios") .query("usuarios")
.withIndex("by_email", (q) => q.eq("email", email)) .withIndex("by_email", (q) => q.eq("email", email))
@@ -1319,10 +1316,8 @@ export const criarAdminPadrao = mutation({
const senhaHash = await hashPassword(senha); const senhaHash = await hashPassword(senha);
if (existentePorMatricula || existentePorEmail) { if (existentePorEmail) {
const alvo = existentePorMatricula ?? existentePorEmail!; await ctx.db.patch(existentePorEmail._id, {
await ctx.db.patch(alvo._id, {
matricula,
nome, nome,
email, email,
senhaHash, senhaHash,
@@ -1331,11 +1326,10 @@ export const criarAdminPadrao = mutation({
primeiroAcesso: false, primeiroAcesso: false,
atualizadoEm: Date.now(), atualizadoEm: Date.now(),
}); });
return { sucesso: true, usuarioId: alvo._id }; return { sucesso: true, usuarioId: existentePorEmail._id };
} }
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert("usuarios", {
matricula,
senhaHash, senhaHash,
nome, nome,
email, email,

View File

@@ -3,7 +3,7 @@ import { v } from "convex/values";
import { Id, Doc } from "./_generated/dataModel"; import { Id, Doc } from "./_generated/dataModel";
/** /**
* Verificar duplicatas de matrícula * Verificar duplicatas de matrícula (agora busca do funcionário associado)
*/ */
export const verificarDuplicatas = query({ export const verificarDuplicatas = query({
args: {}, args: {},
@@ -23,18 +23,27 @@ export const verificarDuplicatas = query({
handler: async (ctx) => { handler: async (ctx) => {
const usuarios = await ctx.db.query("usuarios").collect(); const usuarios = await ctx.db.query("usuarios").collect();
// Agrupar por matrícula // Agrupar por matrícula do funcionário associado
const gruposPorMatricula = usuarios.reduce((acc, usuario) => { const gruposPorMatricula: Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>> = {};
if (!acc[usuario.matricula]) {
acc[usuario.matricula] = []; for (const usuario of usuarios) {
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
} }
acc[usuario.matricula].push({
if (matricula) {
if (!gruposPorMatricula[matricula]) {
gruposPorMatricula[matricula] = [];
}
gruposPorMatricula[matricula].push({
_id: usuario._id, _id: usuario._id,
nome: usuario.nome, nome: usuario.nome,
email: usuario.email || "", email: usuario.email || "",
}); });
return acc; }
}, {} as Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>>); }
// Filtrar apenas duplicatas // Filtrar apenas duplicatas
const duplicatas = Object.entries(gruposPorMatricula) const duplicatas = Object.entries(gruposPorMatricula)
@@ -50,7 +59,7 @@ export const verificarDuplicatas = query({
}); });
/** /**
* Remover duplicatas mantendo apenas o mais recente * Remover duplicatas mantendo apenas o mais recente (agora busca do funcionário associado)
*/ */
export const removerDuplicatas = internalMutation({ export const removerDuplicatas = internalMutation({
args: {}, args: {},
@@ -61,14 +70,23 @@ export const removerDuplicatas = internalMutation({
handler: async (ctx) => { handler: async (ctx) => {
const usuarios = await ctx.db.query("usuarios").collect(); const usuarios = await ctx.db.query("usuarios").collect();
// Agrupar por matrícula // Agrupar por matrícula do funcionário associado
const gruposPorMatricula = usuarios.reduce((acc, usuario) => { const gruposPorMatricula: Record<string, Doc<"usuarios">[]> = {};
if (!acc[usuario.matricula]) {
acc[usuario.matricula] = []; for (const usuario of usuarios) {
let matricula: string | undefined = undefined;
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula;
}
if (matricula) {
if (!gruposPorMatricula[matricula]) {
gruposPorMatricula[matricula] = [];
}
gruposPorMatricula[matricula].push(usuario);
}
} }
acc[usuario.matricula].push(usuario);
return acc;
}, {} as Record<string, Doc<"usuarios">[]>);
let removidos = 0; let removidos = 0;
const matriculasDuplicadas: string[] = []; const matriculasDuplicadas: string[] = [];