import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; import { hashPassword } from "./auth/utils"; /** * Criar novo usuário (apenas TI) */ export const criar = mutation({ args: { matricula: v.string(), nome: v.string(), email: v.string(), roleId: v.id("roles"), funcionarioId: v.optional(v.id("funcionarios")), senhaInicial: v.string(), }, returns: v.union( v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios") }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), 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 const emailExistente = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", args.email)) .first(); if (emailExistente) { return { sucesso: false as const, erro: "E-mail já cadastrado" }; } // Gerar hash da senha inicial const senhaHash = await hashPassword(args.senhaInicial); // Criar usuário const usuarioId = await ctx.db.insert("usuarios", { matricula: args.matricula, senhaHash, nome: args.nome, email: args.email, funcionarioId: args.funcionarioId, roleId: args.roleId, ativo: true, primeiroAcesso: true, criadoEm: Date.now(), atualizadoEm: Date.now(), }); return { sucesso: true as const, usuarioId }; }, }); /** * Listar todos os usuários com filtros */ export const listar = query({ args: { setor: v.optional(v.string()), matricula: v.optional(v.string()), ativo: v.optional(v.boolean()), }, returns: v.array( v.object({ _id: v.id("usuarios"), matricula: v.string(), nome: v.string(), email: v.string(), ativo: v.boolean(), primeiroAcesso: v.boolean(), ultimoAcesso: v.optional(v.number()), criadoEm: v.number(), role: v.object({ _id: v.id("roles"), nome: v.string(), nivel: v.number(), setor: v.optional(v.string()), }), funcionario: v.optional( v.object({ _id: v.id("funcionarios"), nome: v.string(), simboloTipo: v.union( v.literal("cargo_comissionado"), v.literal("funcao_gratificada") ), }) ), }) ), handler: async (ctx, args) => { let usuarios = await ctx.db.query("usuarios").collect(); // Filtrar por matrícula if (args.matricula) { usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!) ); } // Filtrar por ativo if (args.ativo !== undefined) { usuarios = usuarios.filter((u) => u.ativo === args.ativo); } // Buscar roles e funcionários const resultado = []; for (const usuario of usuarios) { const role = await ctx.db.get(usuario.roleId); if (!role) continue; // Filtrar por setor if (args.setor && role.setor !== args.setor) { continue; } let funcionario = undefined; if (usuario.funcionarioId) { const func = await ctx.db.get(usuario.funcionarioId); if (func) { funcionario = { _id: func._id, nome: func.nome, simboloTipo: func.simboloTipo, }; } } resultado.push({ _id: usuario._id, matricula: usuario.matricula, nome: usuario.nome, email: usuario.email, ativo: usuario.ativo, primeiroAcesso: usuario.primeiroAcesso, ultimoAcesso: usuario.ultimoAcesso, criadoEm: usuario.criadoEm, role: { _id: role._id, nome: role.nome, nivel: role.nivel, setor: role.setor, }, funcionario, }); } return resultado; }, }); /** * Ativar/Desativar usuário */ export const alterarStatus = mutation({ args: { usuarioId: v.id("usuarios"), ativo: v.boolean(), }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.usuarioId, { ativo: args.ativo, atualizadoEm: Date.now(), }); // Se desativar, desativar todas as sessões if (!args.ativo) { const sessoes = await ctx.db .query("sessoes") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .collect(); for (const sessao of sessoes) { await ctx.db.patch(sessao._id, { ativo: false }); } } return null; }, }); /** * Resetar senha do usuário */ export const resetarSenha = mutation({ args: { usuarioId: v.id("usuarios"), novaSenha: v.string(), }, returns: v.null(), handler: async (ctx, args) => { const senhaHash = await hashPassword(args.novaSenha); await ctx.db.patch(args.usuarioId, { senhaHash, primeiroAcesso: true, atualizadoEm: Date.now(), }); // Desativar todas as sessões const sessoes = await ctx.db .query("sessoes") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .collect(); for (const sessao of sessoes) { await ctx.db.patch(sessao._id, { ativo: false }); } return null; }, }); /** * Excluir usuário */ export const excluir = mutation({ args: { usuarioId: v.id("usuarios"), }, returns: v.null(), handler: async (ctx, args) => { // Excluir sessões const sessoes = await ctx.db .query("sessoes") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .collect(); for (const sessao of sessoes) { await ctx.db.delete(sessao._id); } // Excluir usuário await ctx.db.delete(args.usuarioId); return null; }, }); /** * Ativar usuário */ export const ativar = mutation({ args: { id: v.id("usuarios"), }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.id, { ativo: true, atualizadoEm: Date.now(), }); return null; }, }); /** * Desativar usuário */ export const desativar = mutation({ args: { id: v.id("usuarios"), }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.id, { ativo: false, atualizadoEm: Date.now(), }); // Desativar todas as sessões const sessoes = await ctx.db .query("sessoes") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.id)) .collect(); for (const sessao of sessoes) { await ctx.db.patch(sessao._id, { ativo: false }); } return null; }, }); /** * Alterar role de um usuário */ export const alterarRole = mutation({ args: { usuarioId: v.id("usuarios"), novaRoleId: v.id("roles"), }, returns: v.null(), handler: async (ctx, args) => { // Verificar se a role existe const role = await ctx.db.get(args.novaRoleId); if (!role) { throw new Error("Role não encontrada"); } // Atualizar usuário await ctx.db.patch(args.usuarioId, { roleId: args.novaRoleId, atualizadoEm: Date.now(), }); return null; }, }); /** * Atualizar perfil do usuário (foto, avatar, setor, status, preferências) */ export const atualizarPerfil = mutation({ args: { avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id("_storage")), setor: v.optional(v.string()), statusMensagem: v.optional(v.string()), statusPresenca: v.optional( v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ) ), notificacoesAtivadas: v.optional(v.boolean()), somNotificacao: v.optional(v.boolean()), }, returns: v.null(), handler: async (ctx, args) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); let usuarioAtual = null; if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } } if (!usuarioAtual) throw new Error("Usuário não encontrado"); // Validar statusMensagem (max 100 chars) if (args.statusMensagem && args.statusMensagem.length > 100) { throw new Error("Mensagem de status deve ter no máximo 100 caracteres"); } // Atualizar apenas os campos fornecidos const updates: any = { atualizadoEm: Date.now() }; if (args.avatar !== undefined) updates.avatar = args.avatar; if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.setor !== undefined) updates.setor = args.setor; if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem; if (args.statusPresenca !== undefined) { updates.statusPresenca = args.statusPresenca; updates.ultimaAtividade = Date.now(); } if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas; if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao; await ctx.db.patch(usuarioAtual._id, updates); return null; }, }); /** * Obter perfil do usuário atual */ export const obterPerfil = query({ args: {}, handler: async (ctx) => { console.log("=== DEBUG obterPerfil ==="); // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); console.log("Identity:", identity ? "encontrado" : "null"); let usuarioAtual = null; if (identity && identity.email) { console.log("Tentando buscar por email:", identity.email); // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); console.log("Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO"); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { console.log("Buscando por sessão ativa..."); const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO"); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); console.log("Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO"); } } if (!usuarioAtual) { console.log("❌ Nenhum usuário encontrado"); // Listar todos os usuários para debug const todosUsuarios = await ctx.db.query("usuarios").collect(); console.log("Total de usuários no banco:", todosUsuarios.length); console.log("Emails cadastrados:", todosUsuarios.map(u => u.email)); return null; } console.log("✅ Usuário encontrado:", usuarioAtual.nome); // Buscar fotoPerfil URL se existir let fotoPerfilUrl = null; if (usuarioAtual.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil); } return { _id: usuarioAtual._id, nome: usuarioAtual.nome, email: usuarioAtual.email, matricula: usuarioAtual.matricula, avatar: usuarioAtual.avatar, fotoPerfil: usuarioAtual.fotoPerfil, fotoPerfilUrl, setor: usuarioAtual.setor, statusMensagem: usuarioAtual.statusMensagem, statusPresenca: usuarioAtual.statusPresenca, notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true, somNotificacao: usuarioAtual.somNotificacao ?? true, }; }, }); /** * Listar todos usuários para o chat (com avatar, foto e status) */ export const listarParaChat = query({ args: {}, returns: v.array( v.object({ _id: v.id("usuarios"), nome: v.string(), email: v.string(), matricula: v.string(), avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id("_storage")), fotoPerfilUrl: v.union(v.string(), v.null()), statusPresenca: v.optional( v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ) ), statusMensagem: v.optional(v.string()), ultimaAtividade: v.optional(v.number()), }) ), handler: async (ctx) => { // Buscar todos os usuários ativos const usuarios = await ctx.db .query("usuarios") .filter((q) => q.eq(q.field("ativo"), true)) .collect(); // Buscar foto de perfil URL para cada usuário const usuariosComFoto = await Promise.all( usuarios.map(async (usuario) => { let fotoPerfilUrl = null; if (usuario.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } return { _id: usuario._id, nome: usuario.nome, email: usuario.email, matricula: usuario.matricula, avatar: usuario.avatar, fotoPerfil: usuario.fotoPerfil, fotoPerfilUrl, statusPresenca: usuario.statusPresenca || "offline", statusMensagem: usuario.statusMensagem, ultimaAtividade: usuario.ultimaAtividade, }; }) ); return usuariosComFoto; }, }); /** * Gera URL para upload de foto de perfil */ export const uploadFotoPerfil = mutation({ args: {}, handler: async (ctx) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); let usuarioAtual = null; if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } } if (!usuarioAtual) throw new Error("Usuário não autenticado"); return await ctx.storage.generateUploadUrl(); }, });