diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index ebe51d3..0000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = false -insert_final_newline = false \ No newline at end of file diff --git a/apps/web/src/lib/components/chat/ChatWidget.svelte b/apps/web/src/lib/components/chat/ChatWidget.svelte index 82edee7..9180f2b 100644 --- a/apps/web/src/lib/components/chat/ChatWidget.svelte +++ b/apps/web/src/lib/components/chat/ChatWidget.svelte @@ -1,817 +1,773 @@ {#if (!isOpen || isMinimized) && position} - {@const winWidth = - windowDimensions.width || - (typeof window !== "undefined" ? window.innerWidth : 0)} - {@const winHeight = - windowDimensions.height || - (typeof window !== "undefined" ? window.innerHeight : 0)} - {@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`} - {@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`} - + +
+
+
+
+
+ {/if} {#if isOpen && !isMinimized && position} - {@const winWidth = - windowDimensions.width || - (typeof window !== "undefined" ? window.innerWidth : 0)} - {@const winHeight = - windowDimensions.height || - (typeof window !== "undefined" ? window.innerHeight : 0)} - {@const bottomPos = `${Math.max(0, winHeight - position.y - windowSize.height)}px`} - {@const rightPos = `${Math.max(0, winWidth - position.x - windowSize.width)}px`} -
- -
- -
-
- -

- -
- {#if avatarUrlDoUsuario()} - {currentUser?.data?.nome - {:else} - - - - - - - {/if} -
- Mensagens -

+ onmousedown={handleMouseDown} + role="button" + tabindex="0" + aria-label="Arrastar janela do chat" + > + +
+
+ +

+ +
+ {#if avatarUrlDoUsuario()} + {currentUser?.data?.nome + {:else} + + + + + + + {/if} +
+ Mensagens +

- -
- - + +
+ + - - + + - - -
-
+ + +
+
- -
- {#if !activeConversation} - - {:else} - - {/if} + +
+ {#if !activeConversation} + + {:else} + + {/if} - - -
handleResizeStart(e, "n")} - onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "n")} - style="border-radius: 24px 24px 0 0;" - >
- -
handleResizeStart(e, "s")} - onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "s")} - style="border-radius: 0 0 24px 24px;" - >
- -
handleResizeStart(e, "w")} - onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "w")} - style="border-radius: 24px 0 0 24px;" - >
- -
handleResizeStart(e, "e")} - onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "e")} - style="border-radius: 0 24px 24px 0;" - >
- -
handleResizeStart(e, "nw")} - onkeydown={(e) => - e.key === "Enter" && handleResizeStart(e as any, "nw")} - style="border-radius: 24px 0 0 0;" - >
-
handleResizeStart(e, "ne")} - onkeydown={(e) => - e.key === "Enter" && handleResizeStart(e as any, "ne")} - style="border-radius: 0 24px 0 0;" - >
-
handleResizeStart(e, "sw")} - onkeydown={(e) => - e.key === "Enter" && handleResizeStart(e as any, "sw")} - style="border-radius: 0 0 0 24px;" - >
-
handleResizeStart(e, "se")} - onkeydown={(e) => - e.key === "Enter" && handleResizeStart(e as any, "se")} - style="border-radius: 0 0 24px 0;" - >
-
-
+ + +
handleResizeStart(e, 'n')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')} + style="border-radius: 24px 24px 0 0;" + >
+ +
handleResizeStart(e, 's')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')} + style="border-radius: 0 0 24px 24px;" + >
+ +
handleResizeStart(e, 'w')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')} + style="border-radius: 24px 0 0 24px;" + >
+ +
handleResizeStart(e, 'e')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')} + style="border-radius: 0 24px 24px 0;" + >
+ +
handleResizeStart(e, 'nw')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')} + style="border-radius: 24px 0 0 0;" + >
+
handleResizeStart(e, 'ne')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')} + style="border-radius: 0 24px 0 0;" + >
+
handleResizeStart(e, 'sw')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')} + style="border-radius: 0 0 0 24px;" + >
+
handleResizeStart(e, 'se')} + onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')} + style="border-radius: 0 0 24px 0;" + >
+ + {/if} {#if showGlobalNotificationPopup && globalNotificationMessage} - {@const notificationMsg = globalNotificationMessage} -
{ - const conversaIdToOpen = notificationMsg?.conversaId; - showGlobalNotificationPopup = false; - globalNotificationMessage = null; - if (globalNotificationTimeout) { - clearTimeout(globalNotificationTimeout); - } - // Abrir chat e conversa ao clicar - if (conversaIdToOpen) { - abrirChat(); - abrirConversa(conversaIdToOpen as Id<"conversas">); - } - }} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - const conversaIdToOpen = notificationMsg?.conversaId; - showGlobalNotificationPopup = false; - globalNotificationMessage = null; - if (globalNotificationTimeout) { - clearTimeout(globalNotificationTimeout); - } - if (conversaIdToOpen) { - abrirChat(); - abrirConversa(conversaIdToOpen as Id<"conversas">); - } - } - }} - > -
-
- - - -
-
-

- Nova mensagem de {notificationMsg.remetente} -

-

- {notificationMsg.conteudo} -

-

Clique para abrir

-
- -
-
+ {@const notificationMsg = globalNotificationMessage} +
{ + const conversaIdToOpen = notificationMsg?.conversaId; + showGlobalNotificationPopup = false; + globalNotificationMessage = null; + if (globalNotificationTimeout) { + clearTimeout(globalNotificationTimeout); + } + // Abrir chat e conversa ao clicar + if (conversaIdToOpen) { + abrirChat(); + abrirConversa(conversaIdToOpen as Id<'conversas'>); + } + }} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const conversaIdToOpen = notificationMsg?.conversaId; + showGlobalNotificationPopup = false; + globalNotificationMessage = null; + if (globalNotificationTimeout) { + clearTimeout(globalNotificationTimeout); + } + if (conversaIdToOpen) { + abrirChat(); + abrirConversa(conversaIdToOpen as Id<'conversas'>); + } + } + }} + > +
+
+ + + +
+
+

+ Nova mensagem de {notificationMsg.remetente} +

+

+ {notificationMsg.conteudo} +

+

Clique para abrir

+
+ +
+
{/if} diff --git a/packages/backend/convex/atestadosLicencas.ts b/packages/backend/convex/atestadosLicencas.ts index 385aef7..d26b5a6 100644 --- a/packages/backend/convex/atestadosLicencas.ts +++ b/packages/backend/convex/atestadosLicencas.ts @@ -1,8 +1,9 @@ -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; -import { Id, Doc } from "./_generated/dataModel"; -import type { QueryCtx, MutationCtx } from "./_generated/server"; -import { registrarAtividade } from "./logsAtividades"; +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { Id } from './_generated/dataModel'; +import type { QueryCtx, MutationCtx } from './_generated/server'; +import { registrarAtividade } from './logsAtividades'; +import { getCurrentUserFunction } from './auth'; // ========== HELPERS ========== @@ -10,40 +11,19 @@ import { registrarAtividade } from "./logsAtividades"; * Helper function para obter usuário autenticado */ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) { - const identity = await ctx.auth.getUserIdentity(); - let usuarioAtual = null; - - if (identity && identity.email) { - usuarioAtual = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", identity.email!)) - .first(); - } - - 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); - } - } - - return usuarioAtual; + const usuario = await getCurrentUserFunction(ctx); + return usuario ?? null; } /** * Helper para calcular dias entre duas datas */ function calcularDias(dataInicio: string, dataFim: string): number { - const inicio = new Date(dataInicio); - const fim = new Date(dataFim); - const diffTime = Math.abs(fim.getTime() - inicio.getTime()); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; - return diffDays; + const inicio = new Date(dataInicio); + const fim = new Date(dataFim); + const diffTime = Math.abs(fim.getTime() - inicio.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; + return diffDays; } // ========== QUERIES ========== @@ -52,676 +32,656 @@ function calcularDias(dataInicio: string, dataFim: string): number { * Listar todos os atestados e licenças com detalhes do funcionário */ export const listarTodos = query({ - args: {}, - handler: async (ctx) => { - try { - const [atestados, licencas] = await Promise.all([ - ctx.db.query("atestados").collect(), - ctx.db.query("licencas").collect(), - ]); + args: {}, + handler: async (ctx) => { + try { + const [atestados, licencas] = await Promise.all([ + ctx.db.query('atestados').collect(), + ctx.db.query('licencas').collect() + ]); - const atestadosComDetalhes = await Promise.all( - atestados.map(async (a) => { - try { - const funcionario = await ctx.db.get(a.funcionarioId); - const criadoPor = await ctx.db.get(a.criadoPor); - return { - ...a, - funcionario, - criadoPorNome: criadoPor?.nome || "Sistema", - dias: calcularDias(a.dataInicio, a.dataFim), - status: new Date(a.dataFim) >= new Date() ? "ativo" : "finalizado", - }; - } catch (error) { - console.error("Erro ao buscar detalhes do atestado:", error); - return { - ...a, - funcionario: null, - criadoPorNome: "Sistema", - dias: calcularDias(a.dataInicio, a.dataFim), - status: new Date(a.dataFim) >= new Date() ? "ativo" : "finalizado", - }; - } - }) - ); + const atestadosComDetalhes = await Promise.all( + atestados.map(async (a) => { + try { + const funcionario = await ctx.db.get(a.funcionarioId); + const criadoPor = await ctx.db.get(a.criadoPor); + return { + ...a, + funcionario, + criadoPorNome: criadoPor?.nome || 'Sistema', + dias: calcularDias(a.dataInicio, a.dataFim), + status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado' + }; + } catch (error) { + console.error('Erro ao buscar detalhes do atestado:', error); + return { + ...a, + funcionario: null, + criadoPorNome: 'Sistema', + dias: calcularDias(a.dataInicio, a.dataFim), + status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado' + }; + } + }) + ); - const licencasComDetalhes = await Promise.all( - licencas.map(async (l) => { - try { - const funcionario = await ctx.db.get(l.funcionarioId); - const criadoPor = await ctx.db.get(l.criadoPor); - const licencaOriginal = l.licencaOriginalId - ? await ctx.db.get(l.licencaOriginalId) - : null; - return { - ...l, - funcionario, - criadoPorNome: criadoPor?.nome || "Sistema", - licencaOriginal, - dias: calcularDias(l.dataInicio, l.dataFim), - status: new Date(l.dataFim) >= new Date() ? "ativo" : "finalizado", - }; - } catch (error) { - console.error("Erro ao buscar detalhes da licença:", error); - return { - ...l, - funcionario: null, - criadoPorNome: "Sistema", - licencaOriginal: null, - dias: calcularDias(l.dataInicio, l.dataFim), - status: new Date(l.dataFim) >= new Date() ? "ativo" : "finalizado", - }; - } - }) - ); + const licencasComDetalhes = await Promise.all( + licencas.map(async (l) => { + try { + const funcionario = await ctx.db.get(l.funcionarioId); + const criadoPor = await ctx.db.get(l.criadoPor); + const licencaOriginal = l.licencaOriginalId + ? await ctx.db.get(l.licencaOriginalId) + : null; + return { + ...l, + funcionario, + criadoPorNome: criadoPor?.nome || 'Sistema', + licencaOriginal, + dias: calcularDias(l.dataInicio, l.dataFim), + status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado' + }; + } catch (error) { + console.error('Erro ao buscar detalhes da licença:', error); + return { + ...l, + funcionario: null, + criadoPorNome: 'Sistema', + licencaOriginal: null, + dias: calcularDias(l.dataInicio, l.dataFim), + status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado' + }; + } + }) + ); - return { - atestados: atestadosComDetalhes.sort( - (a, b) => b._creationTime - a._creationTime - ), - licencas: licencasComDetalhes.sort( - (a, b) => b._creationTime - a._creationTime - ), - }; - } catch (error) { - console.error("Erro em listarTodos:", error); - return { - atestados: [], - licencas: [], - }; - } - }, + return { + atestados: atestadosComDetalhes.sort((a, b) => b._creationTime - a._creationTime), + licencas: licencasComDetalhes.sort((a, b) => b._creationTime - a._creationTime) + }; + } catch (error) { + console.error('Erro em listarTodos:', error); + return { + atestados: [], + licencas: [] + }; + } + } }); /** * Listar por funcionário específico */ export const listarPorFuncionario = query({ - args: { funcionarioId: v.id("funcionarios") }, - handler: async (ctx, args) => { - const [atestados, licencas] = await Promise.all([ - ctx.db - .query("atestados") - .withIndex("by_funcionario", (q) => - q.eq("funcionarioId", args.funcionarioId) - ) - .collect(), - ctx.db - .query("licencas") - .withIndex("by_funcionario", (q) => - q.eq("funcionarioId", args.funcionarioId) - ) - .collect(), - ]); + args: { funcionarioId: v.id('funcionarios') }, + handler: async (ctx, args) => { + const [atestados, licencas] = await Promise.all([ + ctx.db + .query('atestados') + .withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId)) + .collect(), + ctx.db + .query('licencas') + .withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId)) + .collect() + ]); - return { - atestados: atestados.sort((a, b) => b._creationTime - a._creationTime), - licencas: licencas.sort((a, b) => b._creationTime - a._creationTime), - }; - }, + return { + atestados: atestados.sort((a, b) => b._creationTime - a._creationTime), + licencas: licencas.sort((a, b) => b._creationTime - a._creationTime) + }; + } }); /** * Listar por período */ export const listarPorPeriodo = query({ - args: { - dataInicio: v.string(), - dataFim: v.string(), - }, - handler: async (ctx, args) => { - const dataInicioObj = new Date(args.dataInicio); - const dataFimObj = new Date(args.dataFim); + args: { + dataInicio: v.string(), + dataFim: v.string() + }, + handler: async (ctx, args) => { + const dataInicioObj = new Date(args.dataInicio); + const dataFimObj = new Date(args.dataFim); - const atestados = await ctx.db.query("atestados").collect(); - const licencas = await ctx.db.query("licencas").collect(); + const atestados = await ctx.db.query('atestados').collect(); + const licencas = await ctx.db.query('licencas').collect(); - const atestadosFiltrados = atestados.filter((a) => { - const inicio = new Date(a.dataInicio); - const fim = new Date(a.dataFim); - return ( - (inicio >= dataInicioObj && inicio <= dataFimObj) || - (fim >= dataInicioObj && fim <= dataFimObj) || - (inicio <= dataInicioObj && fim >= dataFimObj) - ); - }); + const atestadosFiltrados = atestados.filter((a) => { + const inicio = new Date(a.dataInicio); + const fim = new Date(a.dataFim); + return ( + (inicio >= dataInicioObj && inicio <= dataFimObj) || + (fim >= dataInicioObj && fim <= dataFimObj) || + (inicio <= dataInicioObj && fim >= dataFimObj) + ); + }); - const licencasFiltradas = licencas.filter((l) => { - const inicio = new Date(l.dataInicio); - const fim = new Date(l.dataFim); - return ( - (inicio >= dataInicioObj && inicio <= dataFimObj) || - (fim >= dataInicioObj && fim <= dataFimObj) || - (inicio <= dataInicioObj && fim >= dataFimObj) - ); - }); + const licencasFiltradas = licencas.filter((l) => { + const inicio = new Date(l.dataInicio); + const fim = new Date(l.dataFim); + return ( + (inicio >= dataInicioObj && inicio <= dataFimObj) || + (fim >= dataInicioObj && fim <= dataFimObj) || + (inicio <= dataInicioObj && fim >= dataFimObj) + ); + }); - return { - atestados: atestadosFiltrados, - licencas: licencasFiltradas, - }; - }, + return { + atestados: atestadosFiltrados, + licencas: licencasFiltradas + }; + } }); /** * Obter dados para gráficos */ export const obterDadosGraficos = query({ - args: { - periodo: v.optional(v.number()), // dias (padrão: 30) - }, - handler: async (ctx, args) => { - try { - const dias = args.periodo || 30; - const dataLimite = Date.now() - dias * 24 * 60 * 60 * 1000; + args: { + periodo: v.optional(v.number()) // dias (padrão: 30) + }, + handler: async (ctx, args) => { + try { + const dias = args.periodo || 30; + const dataLimite = Date.now() - dias * 24 * 60 * 60 * 1000; - const [atestados, licencas] = await Promise.all([ - ctx.db.query("atestados").collect(), - ctx.db.query("licencas").collect(), - ]); + const [atestados, licencas] = await Promise.all([ + ctx.db.query('atestados').collect(), + ctx.db.query('licencas').collect() + ]); - // Filtrar por período - const atestadosFiltrados = atestados.filter( - (a) => new Date(a.criadoEm) >= new Date(dataLimite) - ); - const licencasFiltradas = licencas.filter( - (l) => new Date(l.criadoEm) >= new Date(dataLimite) - ); + // Filtrar por período + const atestadosFiltrados = atestados.filter( + (a) => new Date(a.criadoEm) >= new Date(dataLimite) + ); + const licencasFiltradas = licencas.filter( + (l) => new Date(l.criadoEm) >= new Date(dataLimite) + ); - // 1. Total de dias por tipo (para gráfico de barras) - const totalDiasPorTipo: Record = { - atestado_medico: 0, - declaracao_comparecimento: 0, - maternidade: 0, - paternidade: 0, - ferias: 0, - }; + // 1. Total de dias por tipo (para gráfico de barras) + const totalDiasPorTipo: Record = { + atestado_medico: 0, + declaracao_comparecimento: 0, + maternidade: 0, + paternidade: 0, + ferias: 0 + }; - atestadosFiltrados.forEach((a) => { - const dias = calcularDias(a.dataInicio, a.dataFim); - if (a.tipo === "atestado_medico") { - totalDiasPorTipo.atestado_medico += dias; - } else { - totalDiasPorTipo.declaracao_comparecimento += dias; - } - }); + atestadosFiltrados.forEach((a) => { + const dias = calcularDias(a.dataInicio, a.dataFim); + if (a.tipo === 'atestado_medico') { + totalDiasPorTipo.atestado_medico += dias; + } else { + totalDiasPorTipo.declaracao_comparecimento += dias; + } + }); - licencasFiltradas.forEach((l) => { - const dias = calcularDias(l.dataInicio, l.dataFim); - if (l.tipo === "maternidade") { - totalDiasPorTipo.maternidade += dias; - } else { - totalDiasPorTipo.paternidade += dias; - } - }); + licencasFiltradas.forEach((l) => { + const dias = calcularDias(l.dataInicio, l.dataFim); + if (l.tipo === 'maternidade') { + totalDiasPorTipo.maternidade += dias; + } else { + totalDiasPorTipo.paternidade += dias; + } + }); - // Buscar férias do período - try { - const solicitacoesFerias = await ctx.db - .query("solicitacoesFerias") - .filter((q) => - q.or( - q.eq(q.field("status"), "aprovado"), - q.eq(q.field("status"), "data_ajustada_aprovada") - ) - ) - .collect(); + // Buscar férias do período + try { + const solicitacoesFerias = await ctx.db + .query('solicitacoesFerias') + .filter((q) => + q.or( + q.eq(q.field('status'), 'aprovado'), + q.eq(q.field('status'), 'data_ajustada_aprovada') + ) + ) + .collect(); - solicitacoesFerias.forEach((s) => { - if (s.periodos && Array.isArray(s.periodos)) { - s.periodos.forEach((p: { dataInicio: string; dataFim: string }) => { - const dias = calcularDias(p.dataInicio, p.dataFim); - totalDiasPorTipo.ferias += dias; - }); - } - }); - } catch (error) { - console.error("Erro ao buscar férias para gráfico:", error); - } + solicitacoesFerias.forEach((s) => { + if (s.periodos && Array.isArray(s.periodos)) { + s.periodos.forEach((p: { dataInicio: string; dataFim: string }) => { + const dias = calcularDias(p.dataInicio, p.dataFim); + totalDiasPorTipo.ferias += dias; + }); + } + }); + } catch (error) { + console.error('Erro ao buscar férias para gráfico:', error); + } - // 2. Tendências mensais (últimos 6 meses) - const meses: Record< - string, - { - atestado_medico: number; - declaracao_comparecimento: number; - maternidade: number; - paternidade: number; - ferias: number; - } - > = {}; + // 2. Tendências mensais (últimos 6 meses) + const meses: Record< + string, + { + atestado_medico: number; + declaracao_comparecimento: number; + maternidade: number; + paternidade: number; + ferias: number; + } + > = {}; - const hoje = new Date(); - for (let i = 5; i >= 0; i--) { - const mesData = new Date(hoje.getFullYear(), hoje.getMonth() - i, 1); - const mesKey = mesData.toLocaleDateString("pt-BR", { - month: "short", - year: "numeric", - }); - meses[mesKey] = { - atestado_medico: 0, - declaracao_comparecimento: 0, - maternidade: 0, - paternidade: 0, - ferias: 0, - }; - } + const hoje = new Date(); + for (let i = 5; i >= 0; i--) { + const mesData = new Date(hoje.getFullYear(), hoje.getMonth() - i, 1); + const mesKey = mesData.toLocaleDateString('pt-BR', { + month: 'short', + year: 'numeric' + }); + meses[mesKey] = { + atestado_medico: 0, + declaracao_comparecimento: 0, + maternidade: 0, + paternidade: 0, + ferias: 0 + }; + } - // Processar atestados para tendências mensais (usar todos, não apenas filtrados) - atestados.forEach((item) => { - try { - const mesData = new Date(item.criadoEm); - const mesKey = mesData.toLocaleDateString("pt-BR", { - month: "short", - year: "numeric", - }); + // Processar atestados para tendências mensais (usar todos, não apenas filtrados) + atestados.forEach((item) => { + try { + const mesData = new Date(item.criadoEm); + const mesKey = mesData.toLocaleDateString('pt-BR', { + month: 'short', + year: 'numeric' + }); - if (meses[mesKey]) { - const dias = calcularDias(item.dataInicio, item.dataFim); - if (item.tipo === "atestado_medico") { - meses[mesKey].atestado_medico += dias; - } else if (item.tipo === "declaracao_comparecimento") { - meses[mesKey].declaracao_comparecimento += dias; - } - } - } catch (error) { - console.error("Erro ao processar atestado para tendências:", error); - } - }); + if (meses[mesKey]) { + const dias = calcularDias(item.dataInicio, item.dataFim); + if (item.tipo === 'atestado_medico') { + meses[mesKey].atestado_medico += dias; + } else if (item.tipo === 'declaracao_comparecimento') { + meses[mesKey].declaracao_comparecimento += dias; + } + } + } catch (error) { + console.error('Erro ao processar atestado para tendências:', error); + } + }); - // Processar licenças para tendências mensais (usar todas, não apenas filtradas) - licencas.forEach((item) => { - try { - const mesData = new Date(item.criadoEm); - const mesKey = mesData.toLocaleDateString("pt-BR", { - month: "short", - year: "numeric", - }); + // Processar licenças para tendências mensais (usar todas, não apenas filtradas) + licencas.forEach((item) => { + try { + const mesData = new Date(item.criadoEm); + const mesKey = mesData.toLocaleDateString('pt-BR', { + month: 'short', + year: 'numeric' + }); - if (meses[mesKey]) { - const dias = calcularDias(item.dataInicio, item.dataFim); - if (item.tipo === "maternidade") { - meses[mesKey].maternidade += dias; - } else if (item.tipo === "paternidade") { - meses[mesKey].paternidade += dias; - } - } - } catch (error) { - console.error("Erro ao processar licença para tendências:", error); - } - }); + if (meses[mesKey]) { + const dias = calcularDias(item.dataInicio, item.dataFim); + if (item.tipo === 'maternidade') { + meses[mesKey].maternidade += dias; + } else if (item.tipo === 'paternidade') { + meses[mesKey].paternidade += dias; + } + } + } catch (error) { + console.error('Erro ao processar licença para tendências:', error); + } + }); - // 3. Funcionários atualmente afastados - const hojeStr = new Date().toISOString().split("T")[0]; - const funcionariosAfastados: Array<{ - funcionarioId: Id<"funcionarios">; - funcionarioNome: string; - tipo: string; - dataInicio: string; - dataFim: string; - }> = []; + // 3. Funcionários atualmente afastados + const hojeStr = new Date().toISOString().split('T')[0]; + const funcionariosAfastados: Array<{ + funcionarioId: Id<'funcionarios'>; + funcionarioNome: string; + tipo: string; + dataInicio: string; + dataFim: string; + }> = []; - // Processar atestados (verificar funcionários atualmente afastados) - atestadosFiltrados.forEach((item) => { - try { - const inicio = new Date(item.dataInicio); - const fim = new Date(item.dataFim); - const hoje = new Date(hojeStr); + // Processar atestados (verificar funcionários atualmente afastados) + atestadosFiltrados.forEach((item) => { + try { + const inicio = new Date(item.dataInicio); + const fim = new Date(item.dataFim); + const hoje = new Date(hojeStr); - if (hoje >= inicio && hoje <= fim) { - funcionariosAfastados.push({ - funcionarioId: item.funcionarioId, - funcionarioNome: "Carregando...", - tipo: item.tipo, - dataInicio: item.dataInicio, - dataFim: item.dataFim, - }); - } - } catch (error) { - console.error("Erro ao processar atestado:", error); - } - }); + if (hoje >= inicio && hoje <= fim) { + funcionariosAfastados.push({ + funcionarioId: item.funcionarioId, + funcionarioNome: 'Carregando...', + tipo: item.tipo, + dataInicio: item.dataInicio, + dataFim: item.dataFim + }); + } + } catch (error) { + console.error('Erro ao processar atestado:', error); + } + }); - // Processar licenças (verificar funcionários atualmente afastados) - licencasFiltradas.forEach((item) => { - try { - const inicio = new Date(item.dataInicio); - const fim = new Date(item.dataFim); - const hoje = new Date(hojeStr); + // Processar licenças (verificar funcionários atualmente afastados) + licencasFiltradas.forEach((item) => { + try { + const inicio = new Date(item.dataInicio); + const fim = new Date(item.dataFim); + const hoje = new Date(hojeStr); - if (hoje >= inicio && hoje <= fim) { - funcionariosAfastados.push({ - funcionarioId: item.funcionarioId, - funcionarioNome: "Carregando...", - tipo: item.tipo, - dataInicio: item.dataInicio, - dataFim: item.dataFim, - }); - } - } catch (error) { - console.error("Erro ao processar licença:", error); - } - }); + if (hoje >= inicio && hoje <= fim) { + funcionariosAfastados.push({ + funcionarioId: item.funcionarioId, + funcionarioNome: 'Carregando...', + tipo: item.tipo, + dataInicio: item.dataInicio, + dataFim: item.dataFim + }); + } + } catch (error) { + console.error('Erro ao processar licença:', error); + } + }); - // Buscar nomes dos funcionários - const funcionariosAfastadosComNomes = await Promise.all( - funcionariosAfastados.map(async (item) => { - try { - const funcionario = await ctx.db.get(item.funcionarioId); - return { - ...item, - funcionarioNome: funcionario?.nome || "Desconhecido", - }; - } catch (error) { - console.error("Erro ao buscar funcionário:", error); - return { - ...item, - funcionarioNome: "Desconhecido", - }; - } - }) - ); + // Buscar nomes dos funcionários + const funcionariosAfastadosComNomes = await Promise.all( + funcionariosAfastados.map(async (item) => { + try { + const funcionario = await ctx.db.get(item.funcionarioId); + return { + ...item, + funcionarioNome: funcionario?.nome || 'Desconhecido' + }; + } catch (error) { + console.error('Erro ao buscar funcionário:', error); + return { + ...item, + funcionarioNome: 'Desconhecido' + }; + } + }) + ); - return { - totalDiasPorTipo: [ - { tipo: "Atestado Médico", dias: totalDiasPorTipo.atestado_medico }, - { - tipo: "Declaração", - dias: totalDiasPorTipo.declaracao_comparecimento, - }, - { tipo: "Licença Maternidade", dias: totalDiasPorTipo.maternidade }, - { tipo: "Licença Paternidade", dias: totalDiasPorTipo.paternidade }, - { tipo: "Férias", dias: totalDiasPorTipo.ferias }, - ], - tendenciasMensais: Object.entries(meses).map(([mes, dados]) => ({ - mes, - ...dados, - })), - funcionariosAfastados: funcionariosAfastadosComNomes, - }; - } catch (error) { - console.error("Erro em obterDadosGraficos:", error); - // Retornar dados vazios em caso de erro para não quebrar a página - return { - totalDiasPorTipo: [ - { tipo: "Atestado Médico", dias: 0 }, - { tipo: "Declaração", dias: 0 }, - { tipo: "Licença Maternidade", dias: 0 }, - { tipo: "Licença Paternidade", dias: 0 }, - { tipo: "Férias", dias: 0 }, - ], - tendenciasMensais: [], - funcionariosAfastados: [], - }; - } - }, + return { + totalDiasPorTipo: [ + { tipo: 'Atestado Médico', dias: totalDiasPorTipo.atestado_medico }, + { + tipo: 'Declaração', + dias: totalDiasPorTipo.declaracao_comparecimento + }, + { tipo: 'Licença Maternidade', dias: totalDiasPorTipo.maternidade }, + { tipo: 'Licença Paternidade', dias: totalDiasPorTipo.paternidade }, + { tipo: 'Férias', dias: totalDiasPorTipo.ferias } + ], + tendenciasMensais: Object.entries(meses).map(([mes, dados]) => ({ + mes, + ...dados + })), + funcionariosAfastados: funcionariosAfastadosComNomes + }; + } catch (error) { + console.error('Erro em obterDadosGraficos:', error); + // Retornar dados vazios em caso de erro para não quebrar a página + return { + totalDiasPorTipo: [ + { tipo: 'Atestado Médico', dias: 0 }, + { tipo: 'Declaração', dias: 0 }, + { tipo: 'Licença Maternidade', dias: 0 }, + { tipo: 'Licença Paternidade', dias: 0 }, + { tipo: 'Férias', dias: 0 } + ], + tendenciasMensais: [], + funcionariosAfastados: [] + }; + } + } }); /** * Obter estatísticas para dashboard */ export const obterEstatisticas = query({ - args: {}, - handler: async (ctx) => { - const hoje = new Date(); - hoje.setHours(0, 0, 0, 0); - const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1); - const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0); + args: {}, + handler: async (ctx) => { + const hoje = new Date(); + hoje.setHours(0, 0, 0, 0); + const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1); + const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0); - const [atestados, licencas] = await Promise.all([ - ctx.db.query("atestados").collect(), - ctx.db.query("licencas").collect(), - ]); + const [atestados, licencas] = await Promise.all([ + ctx.db.query('atestados').collect(), + ctx.db.query('licencas').collect() + ]); - // Atestados ativos - const atestadosAtivos = atestados.filter( - (a) => new Date(a.dataFim) >= hoje - ); + // Atestados ativos + const atestadosAtivos = atestados.filter((a) => new Date(a.dataFim) >= hoje); - // Licenças ativas - const licencasAtivas = licencas.filter( - (l) => new Date(l.dataFim) >= hoje - ); + // Licenças ativas + const licencasAtivas = licencas.filter((l) => new Date(l.dataFim) >= hoje); - // Funcionários afastados hoje - const funcionariosAfastadosHoje = new Set(); - [...atestados, ...licencas].forEach((item) => { - const inicio = new Date(item.dataInicio); - const fim = new Date(item.dataFim); - if (hoje >= inicio && hoje <= fim) { - funcionariosAfastadosHoje.add(item.funcionarioId); - } - }); + // Funcionários afastados hoje + const funcionariosAfastadosHoje = new Set(); + [...atestados, ...licencas].forEach((item) => { + const inicio = new Date(item.dataInicio); + const fim = new Date(item.dataFim); + if (hoje >= inicio && hoje <= fim) { + funcionariosAfastadosHoje.add(item.funcionarioId); + } + }); - // Total de dias no mês - let totalDiasMes = 0; - [...atestados, ...licencas].forEach((item) => { - const inicio = new Date(item.dataInicio); - const fim = new Date(item.dataFim); - if ( - (inicio >= inicioMes && inicio <= fimMes) || - (fim >= inicioMes && fim <= fimMes) || - (inicio <= inicioMes && fim >= fimMes) - ) { - const dias = calcularDias(item.dataInicio, item.dataFim); - totalDiasMes += dias; - } - }); + // Total de dias no mês + let totalDiasMes = 0; + [...atestados, ...licencas].forEach((item) => { + const inicio = new Date(item.dataInicio); + const fim = new Date(item.dataFim); + if ( + (inicio >= inicioMes && inicio <= fimMes) || + (fim >= inicioMes && fim <= fimMes) || + (inicio <= inicioMes && fim >= fimMes) + ) { + const dias = calcularDias(item.dataInicio, item.dataFim); + totalDiasMes += dias; + } + }); - return { - totalAtestadosAtivos: atestadosAtivos.length, - totalLicencasAtivas: licencasAtivas.length, - funcionariosAfastadosHoje: funcionariosAfastadosHoje.size, - totalDiasAfastamentoMes: totalDiasMes, - }; - }, + return { + totalAtestadosAtivos: atestadosAtivos.length, + totalLicencasAtivas: licencasAtivas.length, + funcionariosAfastadosHoje: funcionariosAfastadosHoje.size, + totalDiasAfastamentoMes: totalDiasMes + }; + } }); /** * Obter eventos formatados para calendário */ export const obterEventosCalendario = query({ - args: { - dataInicio: v.optional(v.string()), - dataFim: v.optional(v.string()), - tipoFiltro: v.optional( - v.union( - v.literal("todos"), - v.literal("atestado_medico"), - v.literal("declaracao_comparecimento"), - v.literal("maternidade"), - v.literal("paternidade"), - v.literal("ferias") - ) - ), - }, - handler: async (ctx, args) => { - const eventos: Array<{ - id: string; - title: string; - start: string; - end: string; - color: string; - tipo: string; - funcionarioNome: string; - funcionarioId: string; - }> = []; + args: { + dataInicio: v.optional(v.string()), + dataFim: v.optional(v.string()), + tipoFiltro: v.optional( + v.union( + v.literal('todos'), + v.literal('atestado_medico'), + v.literal('declaracao_comparecimento'), + v.literal('maternidade'), + v.literal('paternidade'), + v.literal('ferias') + ) + ) + }, + handler: async (ctx, args) => { + const eventos: Array<{ + id: string; + title: string; + start: string; + end: string; + color: string; + tipo: string; + funcionarioNome: string; + funcionarioId: string; + }> = []; - try { - // Buscar atestados - if ( - !args.tipoFiltro || - args.tipoFiltro === "todos" || - args.tipoFiltro === "atestado_medico" || - args.tipoFiltro === "declaracao_comparecimento" - ) { - try { - const atestados = await ctx.db.query("atestados").collect(); - for (const atestado of atestados) { - try { - if ( - args.tipoFiltro && - args.tipoFiltro !== "todos" && - atestado.tipo !== args.tipoFiltro - ) { - continue; - } + try { + // Buscar atestados + if ( + !args.tipoFiltro || + args.tipoFiltro === 'todos' || + args.tipoFiltro === 'atestado_medico' || + args.tipoFiltro === 'declaracao_comparecimento' + ) { + try { + const atestados = await ctx.db.query('atestados').collect(); + for (const atestado of atestados) { + try { + if ( + args.tipoFiltro && + args.tipoFiltro !== 'todos' && + atestado.tipo !== args.tipoFiltro + ) { + continue; + } - const funcionario = await ctx.db.get(atestado.funcionarioId); - if (!funcionario) continue; + const funcionario = await ctx.db.get(atestado.funcionarioId); + if (!funcionario) continue; - if (!atestado.dataInicio || !atestado.dataFim) continue; + if (!atestado.dataInicio || !atestado.dataFim) continue; - const cor = - atestado.tipo === "atestado_medico" - ? "#ef4444" - : "#f97316"; // vermelho ou laranja + const cor = atestado.tipo === 'atestado_medico' ? '#ef4444' : '#f97316'; // vermelho ou laranja - eventos.push({ - id: `atestado-${atestado._id}`, - title: `${funcionario.nome} - ${ - atestado.tipo === "atestado_medico" - ? "Atestado Médico" - : "Declaração" - }`, - start: atestado.dataInicio, - end: atestado.dataFim, - color: cor, - tipo: atestado.tipo, - funcionarioNome: funcionario.nome, - funcionarioId: funcionario._id, - }); - } catch (error) { - console.error(`Erro ao processar atestado ${atestado._id}:`, error); - continue; - } - } - } catch (error) { - console.error("Erro ao buscar atestados:", error); - } - } + eventos.push({ + id: `atestado-${atestado._id}`, + title: `${funcionario.nome} - ${ + atestado.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração' + }`, + start: atestado.dataInicio, + end: atestado.dataFim, + color: cor, + tipo: atestado.tipo, + funcionarioNome: funcionario.nome, + funcionarioId: funcionario._id + }); + } catch (error) { + console.error(`Erro ao processar atestado ${atestado._id}:`, error); + continue; + } + } + } catch (error) { + console.error('Erro ao buscar atestados:', error); + } + } - // Buscar licenças - if ( - !args.tipoFiltro || - args.tipoFiltro === "todos" || - args.tipoFiltro === "maternidade" || - args.tipoFiltro === "paternidade" - ) { - try { - const licencas = await ctx.db.query("licencas").collect(); - for (const licenca of licencas) { - try { - if ( - args.tipoFiltro && - args.tipoFiltro !== "todos" && - licenca.tipo !== args.tipoFiltro - ) { - continue; - } + // Buscar licenças + if ( + !args.tipoFiltro || + args.tipoFiltro === 'todos' || + args.tipoFiltro === 'maternidade' || + args.tipoFiltro === 'paternidade' + ) { + try { + const licencas = await ctx.db.query('licencas').collect(); + for (const licenca of licencas) { + try { + if ( + args.tipoFiltro && + args.tipoFiltro !== 'todos' && + licenca.tipo !== args.tipoFiltro + ) { + continue; + } - const funcionario = await ctx.db.get(licenca.funcionarioId); - if (!funcionario) continue; + const funcionario = await ctx.db.get(licenca.funcionarioId); + if (!funcionario) continue; - if (!licenca.dataInicio || !licenca.dataFim) continue; + if (!licenca.dataInicio || !licenca.dataFim) continue; - const cor = - licenca.tipo === "maternidade" - ? "#ec4899" - : "#3b82f6"; // rosa ou azul + const cor = licenca.tipo === 'maternidade' ? '#ec4899' : '#3b82f6'; // rosa ou azul - eventos.push({ - id: `licenca-${licenca._id}`, - title: `${funcionario.nome} - Licença ${ - licenca.tipo === "maternidade" ? "Maternidade" : "Paternidade" - }`, - start: licenca.dataInicio, - end: licenca.dataFim, - color: cor, - tipo: licenca.tipo, - funcionarioNome: funcionario.nome, - funcionarioId: funcionario._id, - }); - } catch (error) { - console.error(`Erro ao processar licença ${licenca._id}:`, error); - continue; - } - } - } catch (error) { - console.error("Erro ao buscar licenças:", error); - } - } - } catch (error) { - console.error("Erro geral em obterEventosCalendario:", error); - return eventos; // Retorna eventos já coletados mesmo se houver erro - } + eventos.push({ + id: `licenca-${licenca._id}`, + title: `${funcionario.nome} - Licença ${ + licenca.tipo === 'maternidade' ? 'Maternidade' : 'Paternidade' + }`, + start: licenca.dataInicio, + end: licenca.dataFim, + color: cor, + tipo: licenca.tipo, + funcionarioNome: funcionario.nome, + funcionarioId: funcionario._id + }); + } catch (error) { + console.error(`Erro ao processar licença ${licenca._id}:`, error); + continue; + } + } + } catch (error) { + console.error('Erro ao buscar licenças:', error); + } + } + } catch (error) { + console.error('Erro geral em obterEventosCalendario:', error); + return eventos; // Retorna eventos já coletados mesmo se houver erro + } - // Integrar com férias (se não estiver filtrando por tipo específico) - if (!args.tipoFiltro || args.tipoFiltro === "todos" || args.tipoFiltro === "ferias") { - try { - // Buscar solicitações de férias aprovadas - const solicitacoesFerias = await ctx.db - .query("solicitacoesFerias") - .filter((q) => - q.or( - q.eq(q.field("status"), "aprovado"), - q.eq(q.field("status"), "data_ajustada_aprovada") - ) - ) - .collect(); + // Integrar com férias (se não estiver filtrando por tipo específico) + if (!args.tipoFiltro || args.tipoFiltro === 'todos' || args.tipoFiltro === 'ferias') { + try { + // Buscar solicitações de férias aprovadas + const solicitacoesFerias = await ctx.db + .query('solicitacoesFerias') + .filter((q) => + q.or( + q.eq(q.field('status'), 'aprovado'), + q.eq(q.field('status'), 'data_ajustada_aprovada') + ) + ) + .collect(); - for (const solicitacao of solicitacoesFerias) { - try { - const funcionario = await ctx.db.get(solicitacao.funcionarioId); - if (!funcionario) continue; + for (const solicitacao of solicitacoesFerias) { + try { + const funcionario = await ctx.db.get(solicitacao.funcionarioId); + if (!funcionario) continue; - // Verificar se periodos existe e é um array - if (!solicitacao.periodos || !Array.isArray(solicitacao.periodos)) { - continue; - } + // Verificar se periodos existe e é um array + if (!solicitacao.periodos || !Array.isArray(solicitacao.periodos)) { + continue; + } - for (const periodo of solicitacao.periodos) { - if (!periodo.dataInicio || !periodo.dataFim) continue; + for (const periodo of solicitacao.periodos) { + if (!periodo.dataInicio || !periodo.dataFim) continue; - eventos.push({ - id: `ferias-${solicitacao._id}-${periodo.dataInicio}`, - title: `${funcionario.nome} - Férias`, - start: periodo.dataInicio, - end: periodo.dataFim, - color: "#10b981", // verde - tipo: "ferias", - funcionarioNome: funcionario.nome, - funcionarioId: funcionario._id, - }); - } - } catch (error) { - console.error(`Erro ao processar solicitação de férias ${solicitacao._id}:`, error); - continue; - } - } - } catch (error) { - console.error("Erro ao buscar solicitações de férias:", error); - // Continua mesmo se houver erro ao buscar férias - } - } + eventos.push({ + id: `ferias-${solicitacao._id}-${periodo.dataInicio}`, + title: `${funcionario.nome} - Férias`, + start: periodo.dataInicio, + end: periodo.dataFim, + color: '#10b981', // verde + tipo: 'ferias', + funcionarioNome: funcionario.nome, + funcionarioId: funcionario._id + }); + } + } catch (error) { + console.error(`Erro ao processar solicitação de férias ${solicitacao._id}:`, error); + continue; + } + } + } catch (error) { + console.error('Erro ao buscar solicitações de férias:', error); + // Continua mesmo se houver erro ao buscar férias + } + } - // Filtrar por período se fornecido - if (args.dataInicio && args.dataFim) { - const inicio = new Date(args.dataInicio); - const fim = new Date(args.dataFim); - return eventos.filter((e) => { - const eventStart = new Date(e.start); - const eventEnd = new Date(e.end); - return ( - (eventStart >= inicio && eventStart <= fim) || - (eventEnd >= inicio && eventEnd <= fim) || - (eventStart <= inicio && eventEnd >= fim) - ); - }); - } + // Filtrar por período se fornecido + if (args.dataInicio && args.dataFim) { + const inicio = new Date(args.dataInicio); + const fim = new Date(args.dataFim); + return eventos.filter((e) => { + const eventStart = new Date(e.start); + const eventEnd = new Date(e.end); + return ( + (eventStart >= inicio && eventStart <= fim) || + (eventEnd >= inicio && eventEnd <= fim) || + (eventStart <= inicio && eventEnd >= fim) + ); + }); + } - return eventos; - }, + return eventos; + } }); // ========== MUTATIONS ========== @@ -730,335 +690,335 @@ export const obterEventosCalendario = query({ * Gerar URL para upload de documentos */ export const generateUploadUrl = mutation({ - args: {}, - returns: v.string(), - handler: async (ctx) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: {}, + returns: v.string(), + handler: async (ctx) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - return await ctx.storage.generateUploadUrl(); - }, + return await ctx.storage.generateUploadUrl(); + } }); /** * Obter URL de um documento armazenado */ export const obterUrlDocumento = query({ - args: { - storageId: v.id("_storage"), - }, - returns: v.union(v.string(), v.null()), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + storageId: v.id('_storage') + }, + returns: v.union(v.string(), v.null()), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - return await ctx.storage.getUrl(args.storageId); - }, + return await ctx.storage.getUrl(args.storageId); + } }); /** * Criar atestado médico */ export const criarAtestadoMedico = mutation({ - args: { - funcionarioId: v.id("funcionarios"), - dataInicio: v.string(), - dataFim: v.string(), - cid: v.string(), - observacoes: v.optional(v.string()), - documentoId: v.optional(v.id("_storage")), - }, - returns: v.id("atestados"), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + funcionarioId: v.id('funcionarios'), + dataInicio: v.string(), + dataFim: v.string(), + cid: v.string(), + observacoes: v.optional(v.string()), + documentoId: v.optional(v.id('_storage')) + }, + returns: v.id('atestados'), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - // Validar datas - if (new Date(args.dataFim) < new Date(args.dataInicio)) { - throw new Error("Data fim deve ser maior ou igual à data início"); - } + // Validar datas + if (new Date(args.dataFim) < new Date(args.dataInicio)) { + throw new Error('Data fim deve ser maior ou igual à data início'); + } - const atestadoId = await ctx.db.insert("atestados", { - funcionarioId: args.funcionarioId, - tipo: "atestado_medico", - dataInicio: args.dataInicio, - dataFim: args.dataFim, - cid: args.cid, - observacoes: args.observacoes, - documentoId: args.documentoId, - criadoPor: usuario._id, - criadoEm: Date.now(), - }); + const atestadoId = await ctx.db.insert('atestados', { + funcionarioId: args.funcionarioId, + tipo: 'atestado_medico', + dataInicio: args.dataInicio, + dataFim: args.dataFim, + cid: args.cid, + observacoes: args.observacoes, + documentoId: args.documentoId, + criadoPor: usuario._id, + criadoEm: Date.now() + }); - await registrarAtividade( - ctx, - usuario._id, - "criar", - "atestados", - `Atestado médico criado para funcionário ${args.funcionarioId}`, - atestadoId - ); + await registrarAtividade( + ctx, + usuario._id, + 'criar', + 'atestados', + `Atestado médico criado para funcionário ${args.funcionarioId}`, + atestadoId + ); - return atestadoId; - }, + return atestadoId; + } }); /** * Criar declaração de comparecimento */ export const criarDeclaracaoComparecimento = mutation({ - args: { - funcionarioId: v.id("funcionarios"), - dataInicio: v.string(), - dataFim: v.string(), - observacoes: v.optional(v.string()), - documentoId: v.optional(v.id("_storage")), - }, - returns: v.id("atestados"), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + funcionarioId: v.id('funcionarios'), + dataInicio: v.string(), + dataFim: v.string(), + observacoes: v.optional(v.string()), + documentoId: v.optional(v.id('_storage')) + }, + returns: v.id('atestados'), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - // Validar datas - if (new Date(args.dataFim) < new Date(args.dataInicio)) { - throw new Error("Data fim deve ser maior ou igual à data início"); - } + // Validar datas + if (new Date(args.dataFim) < new Date(args.dataInicio)) { + throw new Error('Data fim deve ser maior ou igual à data início'); + } - const atestadoId = await ctx.db.insert("atestados", { - funcionarioId: args.funcionarioId, - tipo: "declaracao_comparecimento", - dataInicio: args.dataInicio, - dataFim: args.dataFim, - observacoes: args.observacoes, - documentoId: args.documentoId, - criadoPor: usuario._id, - criadoEm: Date.now(), - }); + const atestadoId = await ctx.db.insert('atestados', { + funcionarioId: args.funcionarioId, + tipo: 'declaracao_comparecimento', + dataInicio: args.dataInicio, + dataFim: args.dataFim, + observacoes: args.observacoes, + documentoId: args.documentoId, + criadoPor: usuario._id, + criadoEm: Date.now() + }); - await registrarAtividade( - ctx, - usuario._id, - "criar", - "atestados", - `Declaração de comparecimento criada para funcionário ${args.funcionarioId}`, - atestadoId - ); + await registrarAtividade( + ctx, + usuario._id, + 'criar', + 'atestados', + `Declaração de comparecimento criada para funcionário ${args.funcionarioId}`, + atestadoId + ); - return atestadoId; - }, + return atestadoId; + } }); /** * Criar licença maternidade */ export const criarLicencaMaternidade = mutation({ - args: { - funcionarioId: v.id("funcionarios"), - dataInicio: v.string(), - dataFim: v.string(), - observacoes: v.optional(v.string()), - documentoId: v.optional(v.id("_storage")), - licencaOriginalId: v.optional(v.id("licencas")), - }, - returns: v.id("licencas"), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + funcionarioId: v.id('funcionarios'), + dataInicio: v.string(), + dataFim: v.string(), + observacoes: v.optional(v.string()), + documentoId: v.optional(v.id('_storage')), + licencaOriginalId: v.optional(v.id('licencas')) + }, + returns: v.id('licencas'), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - // Validar datas - if (new Date(args.dataFim) < new Date(args.dataInicio)) { - throw new Error("Data fim deve ser maior ou igual à data início"); - } + // Validar datas + if (new Date(args.dataFim) < new Date(args.dataInicio)) { + throw new Error('Data fim deve ser maior ou igual à data início'); + } - const ehProrrogacao = !!args.licencaOriginalId; - if (ehProrrogacao && !args.licencaOriginalId) { - throw new Error("Licença original é obrigatória para prorrogação"); - } + const ehProrrogacao = !!args.licencaOriginalId; + if (ehProrrogacao && !args.licencaOriginalId) { + throw new Error('Licença original é obrigatória para prorrogação'); + } - const licencaId = await ctx.db.insert("licencas", { - funcionarioId: args.funcionarioId, - tipo: "maternidade", - dataInicio: args.dataInicio, - dataFim: args.dataFim, - observacoes: args.observacoes, - documentoId: args.documentoId, - licencaOriginalId: args.licencaOriginalId, - ehProrrogacao, - criadoPor: usuario._id, - criadoEm: Date.now(), - }); + const licencaId = await ctx.db.insert('licencas', { + funcionarioId: args.funcionarioId, + tipo: 'maternidade', + dataInicio: args.dataInicio, + dataFim: args.dataFim, + observacoes: args.observacoes, + documentoId: args.documentoId, + licencaOriginalId: args.licencaOriginalId, + ehProrrogacao, + criadoPor: usuario._id, + criadoEm: Date.now() + }); - await registrarAtividade( - ctx, - usuario._id, - "criar", - "licencas", - `Licença maternidade criada para funcionário ${args.funcionarioId}${ehProrrogacao ? " (prorrogação)" : ""}`, - licencaId - ); + await registrarAtividade( + ctx, + usuario._id, + 'criar', + 'licencas', + `Licença maternidade criada para funcionário ${args.funcionarioId}${ehProrrogacao ? ' (prorrogação)' : ''}`, + licencaId + ); - return licencaId; - }, + return licencaId; + } }); /** * Criar licença paternidade */ export const criarLicencaPaternidade = mutation({ - args: { - funcionarioId: v.id("funcionarios"), - dataInicio: v.string(), - dataFim: v.string(), - observacoes: v.optional(v.string()), - documentoId: v.optional(v.id("_storage")), - }, - returns: v.id("licencas"), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + funcionarioId: v.id('funcionarios'), + dataInicio: v.string(), + dataFim: v.string(), + observacoes: v.optional(v.string()), + documentoId: v.optional(v.id('_storage')) + }, + returns: v.id('licencas'), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - // Validar datas - if (new Date(args.dataFim) < new Date(args.dataInicio)) { - throw new Error("Data fim deve ser maior ou igual à data início"); - } + // Validar datas + if (new Date(args.dataFim) < new Date(args.dataInicio)) { + throw new Error('Data fim deve ser maior ou igual à data início'); + } - const licencaId = await ctx.db.insert("licencas", { - funcionarioId: args.funcionarioId, - tipo: "paternidade", - dataInicio: args.dataInicio, - dataFim: args.dataFim, - observacoes: args.observacoes, - documentoId: args.documentoId, - ehProrrogacao: false, - criadoPor: usuario._id, - criadoEm: Date.now(), - }); + const licencaId = await ctx.db.insert('licencas', { + funcionarioId: args.funcionarioId, + tipo: 'paternidade', + dataInicio: args.dataInicio, + dataFim: args.dataFim, + observacoes: args.observacoes, + documentoId: args.documentoId, + ehProrrogacao: false, + criadoPor: usuario._id, + criadoEm: Date.now() + }); - await registrarAtividade( - ctx, - usuario._id, - "criar", - "licencas", - `Licença paternidade criada para funcionário ${args.funcionarioId}`, - licencaId - ); + await registrarAtividade( + ctx, + usuario._id, + 'criar', + 'licencas', + `Licença paternidade criada para funcionário ${args.funcionarioId}`, + licencaId + ); - return licencaId; - }, + return licencaId; + } }); /** * Prorrogar licença maternidade */ export const prorrogarLicencaMaternidade = mutation({ - args: { - licencaOriginalId: v.id("licencas"), - dataInicio: v.string(), - dataFim: v.string(), - observacoes: v.optional(v.string()), - documentoId: v.optional(v.id("_storage")), - }, - returns: v.id("licencas"), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + licencaOriginalId: v.id('licencas'), + dataInicio: v.string(), + dataFim: v.string(), + observacoes: v.optional(v.string()), + documentoId: v.optional(v.id('_storage')) + }, + returns: v.id('licencas'), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - const licencaOriginal = await ctx.db.get(args.licencaOriginalId); - if (!licencaOriginal) { - throw new Error("Licença original não encontrada"); - } + const licencaOriginal = await ctx.db.get(args.licencaOriginalId); + if (!licencaOriginal) { + throw new Error('Licença original não encontrada'); + } - if (licencaOriginal.tipo !== "maternidade") { - throw new Error("Apenas licenças de maternidade podem ser prorrogadas"); - } + if (licencaOriginal.tipo !== 'maternidade') { + throw new Error('Apenas licenças de maternidade podem ser prorrogadas'); + } - // Validar datas - if (new Date(args.dataFim) < new Date(args.dataInicio)) { - throw new Error("Data fim deve ser maior ou igual à data início"); - } + // Validar datas + if (new Date(args.dataFim) < new Date(args.dataInicio)) { + throw new Error('Data fim deve ser maior ou igual à data início'); + } - const prorrogacaoId = await ctx.db.insert("licencas", { - funcionarioId: licencaOriginal.funcionarioId, - tipo: "maternidade", - dataInicio: args.dataInicio, - dataFim: args.dataFim, - observacoes: args.observacoes, - documentoId: args.documentoId, - licencaOriginalId: args.licencaOriginalId, - ehProrrogacao: true, - criadoPor: usuario._id, - criadoEm: Date.now(), - }); + const prorrogacaoId = await ctx.db.insert('licencas', { + funcionarioId: licencaOriginal.funcionarioId, + tipo: 'maternidade', + dataInicio: args.dataInicio, + dataFim: args.dataFim, + observacoes: args.observacoes, + documentoId: args.documentoId, + licencaOriginalId: args.licencaOriginalId, + ehProrrogacao: true, + criadoPor: usuario._id, + criadoEm: Date.now() + }); - await registrarAtividade( - ctx, - usuario._id, - "criar", - "licencas", - `Prorrogação de licença maternidade criada para funcionário ${licencaOriginal.funcionarioId}`, - prorrogacaoId - ); + await registrarAtividade( + ctx, + usuario._id, + 'criar', + 'licencas', + `Prorrogação de licença maternidade criada para funcionário ${licencaOriginal.funcionarioId}`, + prorrogacaoId + ); - return prorrogacaoId; - }, + return prorrogacaoId; + } }); /** * Excluir atestado */ export const excluirAtestado = mutation({ - args: { - id: v.id("atestados"), - }, - returns: v.null(), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + id: v.id('atestados') + }, + returns: v.null(), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - const atestado = await ctx.db.get(args.id); - if (!atestado) throw new Error("Atestado não encontrado"); + const atestado = await ctx.db.get(args.id); + if (!atestado) throw new Error('Atestado não encontrado'); - await ctx.db.delete(args.id); + await ctx.db.delete(args.id); - await registrarAtividade( - ctx, - usuario._id, - "excluir", - "atestados", - `Atestado excluído: ${args.id}`, - args.id - ); + await registrarAtividade( + ctx, + usuario._id, + 'excluir', + 'atestados', + `Atestado excluído: ${args.id}`, + args.id + ); - return null; - }, + return null; + } }); /** * Excluir licença */ export const excluirLicenca = mutation({ - args: { - id: v.id("licencas"), - }, - returns: v.null(), - handler: async (ctx, args) => { - const usuario = await getUsuarioAutenticado(ctx); - if (!usuario) throw new Error("Não autenticado"); + args: { + id: v.id('licencas') + }, + returns: v.null(), + handler: async (ctx, args) => { + const usuario = await getUsuarioAutenticado(ctx); + if (!usuario) throw new Error('Não autenticado'); - const licenca = await ctx.db.get(args.id); - if (!licenca) throw new Error("Licença não encontrada"); + const licenca = await ctx.db.get(args.id); + if (!licenca) throw new Error('Licença não encontrada'); - await ctx.db.delete(args.id); + await ctx.db.delete(args.id); - await registrarAtividade( - ctx, - usuario._id, - "excluir", - "licencas", - `Licença excluída: ${args.id}`, - args.id - ); + await registrarAtividade( + ctx, + usuario._id, + 'excluir', + 'licencas', + `Licença excluída: ${args.id}`, + args.id + ); - return null; - }, + return null; + } }); diff --git a/packages/backend/convex/chat.ts b/packages/backend/convex/chat.ts index b0f1a92..7be20da 100644 --- a/packages/backend/convex/chat.ts +++ b/packages/backend/convex/chat.ts @@ -1,8 +1,9 @@ -import { v } from "convex/values"; -import { mutation, query, internalMutation } from "./_generated/server"; -import { Doc, Id } from "./_generated/dataModel"; -import type { QueryCtx, MutationCtx } from "./_generated/server"; -import { internal, api } from "./_generated/api"; +import { v } from 'convex/values'; +import { mutation, query, internalMutation } from './_generated/server'; +import { Doc, Id } from './_generated/dataModel'; +import type { QueryCtx, MutationCtx } from './_generated/server'; +import { internal, api } from './_generated/api'; +import { getCurrentUserFunction } from './auth'; // ========== HELPERS ========== @@ -10,81 +11,54 @@ import { internal, api } from "./_generated/api"; * Normaliza texto para busca (remove acentos, converte para lowercase) */ function normalizarTextoParaBusca(texto: string): string { - return texto - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") // Remove diacríticos - .trim(); + return texto + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') // Remove diacríticos + .trim(); } /** * Helper function para obter usuário autenticado - * + * * FASE 1 IMPLEMENTADA: Usa Custom Auth Provider configurado no convex.config.ts - * + * * O provider tenta: * 1. Buscar sessão customizada por token (sistema atual) ✅ FUNCIONANDO * 2. Validar via Better Auth (quando configurado) ⏳ PRÓXIMA FASE - * + * * ⚠️ CORREÇÃO DE SEGURANÇA: Busca sessão por token específico (não mais recente) */ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) { - // Tentar autenticação via Custom Auth Provider (convex.config.ts) - // Isso funciona tanto com tokens customizados quanto com Better Auth - const identity = await ctx.auth.getUserIdentity(); - let usuarioAtual = null; - - if (identity && identity.email) { - // Buscar usuário por email (vindo do provider) - usuarioAtual = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", identity.email!)) - .first(); - - if (usuarioAtual) { - // Log para debug (apenas em desenvolvimento) - if (process.env.NODE_ENV === "development") { - console.log("✅ [getUsuarioAutenticado] Usuário identificado via Custom Auth Provider:", { - id: usuarioAtual._id, - nome: usuarioAtual.nome, - email: usuarioAtual.email, - subject: identity.subject - }); - } - return usuarioAtual; - } - } - - // Se não encontrou, logar aviso - if (!usuarioAtual) { - console.warn("⚠️ [getUsuarioAutenticado] Usuário não autenticado - token inválido ou expirado"); - } - - return usuarioAtual; + const usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) { + console.warn('⚠️ [getUsuarioAutenticado] Usuário não autenticado - token inválido ou expirado'); + } + return usuarioAtual || null; } /** * Helper function para verificar se usuário é administrador de uma sala de reunião */ async function verificarPermissaoAdmin( - ctx: QueryCtx | MutationCtx, - conversaId: Id<"conversas">, - usuarioId: Id<"usuarios"> + ctx: QueryCtx | MutationCtx, + conversaId: Id<'conversas'>, + usuarioId: Id<'usuarios'> ): Promise { - const conversa = await ctx.db.get(conversaId); - if (!conversa) return false; - - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") return false; - - // Verificar se tem array de administradores - if (!conversa.administradores || conversa.administradores.length === 0) { - // Se não tem administradores definidos, o criador é admin por padrão - return conversa.criadoPor === usuarioId; - } - - // Verificar se está na lista de administradores - return conversa.administradores.includes(usuarioId); + const conversa = await ctx.db.get(conversaId); + if (!conversa) return false; + + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') return false; + + // Verificar se tem array de administradores + if (!conversa.administradores || conversa.administradores.length === 0) { + // Se não tem administradores definidos, o criador é admin por padrão + return conversa.criadoPor === usuarioId; + } + + // Verificar se está na lista de administradores + return conversa.administradores.includes(usuarioId); } // ========== MUTATIONS ========== @@ -93,508 +67,493 @@ async function verificarPermissaoAdmin( * Cria uma nova conversa (individual ou grupo) */ export const criarConversa = mutation({ - args: { - tipo: v.union(v.literal("individual"), v.literal("grupo"), v.literal("sala_reuniao")), - participantes: v.array(v.id("usuarios")), - nome: v.optional(v.string()), - avatar: v.optional(v.string()), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + tipo: v.union(v.literal('individual'), v.literal('grupo'), v.literal('sala_reuniao')), + participantes: v.array(v.id('usuarios')), + nome: v.optional(v.string()), + avatar: v.optional(v.string()) + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - // Validar participantes - if (!args.participantes.includes(usuarioAtual._id)) { - args.participantes.push(usuarioAtual._id); - } + // Validar participantes + if (!args.participantes.includes(usuarioAtual._id)) { + args.participantes.push(usuarioAtual._id); + } - // Se for conversa individual, verificar se já existe - if (args.tipo === "individual" && args.participantes.length === 2) { - const conversaExistente = await ctx.db - .query("conversas") - .filter((q) => q.eq(q.field("tipo"), "individual")) - .collect(); + // Se for conversa individual, verificar se já existe + if (args.tipo === 'individual' && args.participantes.length === 2) { + const conversaExistente = await ctx.db + .query('conversas') + .filter((q) => q.eq(q.field('tipo'), 'individual')) + .collect(); - for (const conversa of conversaExistente) { - if ( - conversa.participantes.length === 2 && - conversa.participantes.every((p) => args.participantes.includes(p)) - ) { - return conversa._id; - } - } - } + for (const conversa of conversaExistente) { + if ( + conversa.participantes.length === 2 && + conversa.participantes.every((p) => args.participantes.includes(p)) + ) { + return conversa._id; + } + } + } - // Preparar dados da conversa - const dadosConversa: any = { - tipo: args.tipo, - nome: args.nome, - avatar: args.avatar, - participantes: args.participantes, - criadoPor: usuarioAtual._id, - criadoEm: Date.now(), - }; + // Preparar dados da conversa + const dadosConversa: Omit, '_id' | '_creationTime'> = { + tipo: args.tipo, + nome: args.nome, + avatar: args.avatar, + participantes: args.participantes, + criadoPor: usuarioAtual._id, + criadoEm: Date.now() + }; - // Se for sala de reunião, adicionar administradores (criador sempre é admin) - if (args.tipo === "sala_reuniao") { - dadosConversa.administradores = [usuarioAtual._id]; - } + // Se for sala de reunião, adicionar administradores (criador sempre é admin) + if (args.tipo === 'sala_reuniao') { + dadosConversa.administradores = [usuarioAtual._id]; + } - // Criar nova conversa - const conversaId = await ctx.db.insert("conversas", dadosConversa); + // Criar nova conversa + const conversaId = await ctx.db.insert('conversas', dadosConversa); - // Criar notificações para outros participantes - if (args.tipo === "grupo" || args.tipo === "sala_reuniao") { - const tipoNotificacao = args.tipo === "sala_reuniao" ? "adicionado_grupo" : "adicionado_grupo"; - const tipoTexto = args.tipo === "sala_reuniao" ? "sala de reunião" : "grupo"; - - for (const participanteId of args.participantes) { - if (participanteId !== usuarioAtual._id) { - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: tipoNotificacao, - conversaId, - remetenteId: usuarioAtual._id, - titulo: args.tipo === "sala_reuniao" ? "Adicionado a sala de reunião" : "Adicionado a grupo", - descricao: `Você foi adicionado à ${tipoTexto} "${ - args.nome || "Sem nome" - }" por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); - } - } - } + // Criar notificações para outros participantes + if (args.tipo === 'grupo' || args.tipo === 'sala_reuniao') { + const tipoNotificacao = + args.tipo === 'sala_reuniao' ? 'adicionado_grupo' : 'adicionado_grupo'; + const tipoTexto = args.tipo === 'sala_reuniao' ? 'sala de reunião' : 'grupo'; - return conversaId; - }, + for (const participanteId of args.participantes) { + if (participanteId !== usuarioAtual._id) { + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: tipoNotificacao, + conversaId, + remetenteId: usuarioAtual._id, + titulo: + args.tipo === 'sala_reuniao' ? 'Adicionado a sala de reunião' : 'Adicionado a grupo', + descricao: `Você foi adicionado à ${tipoTexto} "${ + args.nome || 'Sem nome' + }" por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); + } + } + } + + return conversaId; + } }); /** * Cria uma nova sala de reunião (wrapper específico para facilitar uso) */ export const criarSalaReuniao = mutation({ - args: { - nome: v.string(), - participantes: v.array(v.id("usuarios")), - avatar: v.optional(v.string()), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + nome: v.string(), + participantes: v.array(v.id('usuarios')), + avatar: v.optional(v.string()) + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - // Validar nome - if (!args.nome || args.nome.trim().length === 0) { - throw new Error("O nome da sala de reunião é obrigatório"); - } + // Validar nome + if (!args.nome || args.nome.trim().length === 0) { + throw new Error('O nome da sala de reunião é obrigatório'); + } - // Validar participantes - const participantesUnicos = [...new Set(args.participantes)]; - if (!participantesUnicos.includes(usuarioAtual._id)) { - participantesUnicos.push(usuarioAtual._id); - } + // Validar participantes + const participantesUnicos = [...new Set(args.participantes)]; + if (!participantesUnicos.includes(usuarioAtual._id)) { + participantesUnicos.push(usuarioAtual._id); + } - // Preparar dados da conversa - const dadosConversa: any = { - tipo: "sala_reuniao" as const, - nome: args.nome.trim(), - avatar: args.avatar, - participantes: participantesUnicos, - criadoPor: usuarioAtual._id, - criadoEm: Date.now(), - administradores: [usuarioAtual._id], // Criador sempre é admin - }; + // Preparar dados da conversa + const dadosConversa: Omit, '_id' | '_creationTime'> = { + tipo: 'sala_reuniao' as const, + nome: args.nome.trim(), + avatar: args.avatar, + participantes: participantesUnicos, + criadoPor: usuarioAtual._id, + criadoEm: Date.now(), + administradores: [usuarioAtual._id] // Criador sempre é admin + }; - // Criar nova conversa - const conversaId = await ctx.db.insert("conversas", dadosConversa); + // Criar nova conversa + const conversaId = await ctx.db.insert('conversas', dadosConversa); - // Criar notificações para outros participantes - const tipoNotificacao = "adicionado_grupo"; - const tipoTexto = "sala de reunião"; - - for (const participanteId of participantesUnicos) { - if (participanteId !== usuarioAtual._id) { - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: tipoNotificacao, - conversaId, - remetenteId: usuarioAtual._id, - titulo: "Adicionado a sala de reunião", - descricao: `Você foi adicionado à ${tipoTexto} "${ - args.nome || "Sem nome" - }" por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); - } - } + // Criar notificações para outros participantes + const tipoNotificacao = 'adicionado_grupo'; + const tipoTexto = 'sala de reunião'; - return conversaId; - }, + for (const participanteId of participantesUnicos) { + if (participanteId !== usuarioAtual._id) { + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: tipoNotificacao, + conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Adicionado a sala de reunião', + descricao: `Você foi adicionado à ${tipoTexto} "${ + args.nome || 'Sem nome' + }" por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); + } + } + + return conversaId; + } }); /** * Cria ou busca uma conversa individual com outro usuário */ export const criarOuBuscarConversaIndividual = mutation({ - args: { - outroUsuarioId: v.id("usuarios"), - }, - returns: v.id("conversas"), - handler: async (ctx, args) => { - // TENTAR BETTER AUTH PRIMEIRO - const identity = await ctx.auth.getUserIdentity(); + args: { + outroUsuarioId: v.id('usuarios') + }, + returns: v.id('conversas'), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - let usuarioAtual = null; + if (!usuarioAtual) throw new Error('Usuário não autenticado'); - 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(); - } + // Buscar conversa individual existente entre os dois usuários + const conversasExistentes = await ctx.db + .query('conversas') + .filter((q) => q.eq(q.field('tipo'), 'individual')) + .collect(); - // 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(); + for (const conversa of conversasExistentes) { + if ( + conversa.participantes.length === 2 && + conversa.participantes.includes(usuarioAtual._id) && + conversa.participantes.includes(args.outroUsuarioId) + ) { + return conversa._id; + } + } - if (sessaoAtiva) { - usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); - } - } + // Se não existe, criar nova conversa individual + const conversaId = await ctx.db.insert('conversas', { + tipo: 'individual', + participantes: [usuarioAtual._id, args.outroUsuarioId], + criadoPor: usuarioAtual._id, + criadoEm: Date.now() + }); - if (!usuarioAtual) throw new Error("Usuário não autenticado"); - - // Buscar conversa individual existente entre os dois usuários - const conversasExistentes = await ctx.db - .query("conversas") - .filter((q) => q.eq(q.field("tipo"), "individual")) - .collect(); - - for (const conversa of conversasExistentes) { - if ( - conversa.participantes.length === 2 && - conversa.participantes.includes(usuarioAtual._id) && - conversa.participantes.includes(args.outroUsuarioId) - ) { - return conversa._id; - } - } - - // Se não existe, criar nova conversa individual - const conversaId = await ctx.db.insert("conversas", { - tipo: "individual", - participantes: [usuarioAtual._id, args.outroUsuarioId], - criadoPor: usuarioAtual._id, - criadoEm: Date.now(), - }); - - return conversaId; - }, + return conversaId; + } }); /** * Envia uma mensagem em uma conversa */ export const enviarMensagem = mutation({ - args: { - conversaId: v.id("conversas"), - conteudo: v.string(), - tipo: v.union( - v.literal("texto"), - v.literal("arquivo"), - v.literal("imagem") - ), - arquivoId: v.optional(v.id("_storage")), - arquivoNome: v.optional(v.string()), - arquivoTamanho: v.optional(v.number()), - arquivoTipo: v.optional(v.string()), - mencoes: v.optional(v.array(v.id("usuarios"))), - respostaPara: v.optional(v.id("mensagens")), // ID da mensagem que está respondendo - permitirNotificacaoParaSiMesmo: v.optional(v.boolean()), // ✅ NOVO: Permite criar notificação para si mesmo - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - console.error("❌ [enviarMensagem] Usuário não autenticado - Better Auth não conseguiu identificar"); - throw new Error("Não autenticado"); - } + args: { + conversaId: v.id('conversas'), + conteudo: v.string(), + tipo: v.union(v.literal('texto'), v.literal('arquivo'), v.literal('imagem')), + arquivoId: v.optional(v.id('_storage')), + arquivoNome: v.optional(v.string()), + arquivoTamanho: v.optional(v.number()), + arquivoTipo: v.optional(v.string()), + mencoes: v.optional(v.array(v.id('usuarios'))), + respostaPara: v.optional(v.id('mensagens')), // ID da mensagem que está respondendo + permitirNotificacaoParaSiMesmo: v.optional(v.boolean()) // ✅ NOVO: Permite criar notificação para si mesmo + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + console.error( + '❌ [enviarMensagem] Usuário não autenticado - Better Auth não conseguiu identificar' + ); + throw new Error('Não autenticado'); + } - // Log para debug (apenas em desenvolvimento) - if (process.env.NODE_ENV === "development") { - console.log("✅ [enviarMensagem] Usuário identificado:", { - id: usuarioAtual._id, - nome: usuarioAtual.nome, - email: usuarioAtual.email - }); - } + // Log para debug (apenas em desenvolvimento) + if (process.env.NODE_ENV === 'development') { + console.log('✅ [enviarMensagem] Usuário identificado:', { + id: usuarioAtual._id, + nome: usuarioAtual.nome, + email: usuarioAtual.email + }); + } - // Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) throw new Error("Conversa não encontrada"); - if (!conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não pertence a esta conversa"); - } + // Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) throw new Error('Conversa não encontrada'); + if (!conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não pertence a esta conversa'); + } - // Normalizar conteúdo para busca (remover acentos, lowercase) - const conteudoBusca = normalizarTextoParaBusca(args.conteudo); + // Normalizar conteúdo para busca (remover acentos, lowercase) + const conteudoBusca = normalizarTextoParaBusca(args.conteudo); - // Verificar se é resposta a outra mensagem - if (args.respostaPara) { - const mensagemOriginal = await ctx.db.get(args.respostaPara); - if (!mensagemOriginal || mensagemOriginal.conversaId !== args.conversaId) { - throw new Error("Mensagem original não encontrada ou não pertence à mesma conversa"); - } - // SEGURANÇA: Verificar se o remetente da mensagem original é participante da conversa - if (!conversa.participantes.includes(mensagemOriginal.remetenteId)) { - throw new Error("Mensagem original inválida"); - } - if (mensagemOriginal.deletada) { - throw new Error("Não é possível responder a uma mensagem deletada"); - } - } + // Verificar se é resposta a outra mensagem + if (args.respostaPara) { + const mensagemOriginal = await ctx.db.get(args.respostaPara); + if (!mensagemOriginal || mensagemOriginal.conversaId !== args.conversaId) { + throw new Error('Mensagem original não encontrada ou não pertence à mesma conversa'); + } + // SEGURANÇA: Verificar se o remetente da mensagem original é participante da conversa + if (!conversa.participantes.includes(mensagemOriginal.remetenteId)) { + throw new Error('Mensagem original inválida'); + } + if (mensagemOriginal.deletada) { + throw new Error('Não é possível responder a uma mensagem deletada'); + } + } - // Criar mensagem - const mensagemId = await ctx.db.insert("mensagens", { - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - tipo: args.tipo, - conteudo: args.conteudo, - conteudoBusca, - arquivoId: args.arquivoId, - arquivoNome: args.arquivoNome, - arquivoTamanho: args.arquivoTamanho, - arquivoTipo: args.arquivoTipo, - mencoes: args.mencoes, - respostaPara: args.respostaPara, - enviadaEm: Date.now(), - lidaPor: [], // Inicializar como array vazio - }); + // Criar mensagem + const mensagemId = await ctx.db.insert('mensagens', { + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + tipo: args.tipo, + conteudo: args.conteudo, + conteudoBusca, + arquivoId: args.arquivoId, + arquivoNome: args.arquivoNome, + arquivoTamanho: args.arquivoTamanho, + arquivoTipo: args.arquivoTipo, + mencoes: args.mencoes, + respostaPara: args.respostaPara, + enviadaEm: Date.now(), + lidaPor: [] // Inicializar como array vazio + }); - // Detectar URLs no conteúdo e extrair preview (apenas para mensagens de texto, assíncrono) - if (args.tipo === "texto") { - const urlRegex = /(https?:\/\/[^\s]+)/g; - const urls = args.conteudo.match(urlRegex); - if (urls && urls.length > 0) { - // Pegar primeira URL encontrada - const primeiraUrl = urls[0]; - // Agendar processamento de preview via action wrapper - ctx.scheduler.runAfter(1000, api.actions.linkPreview.processarPreviewLink, { - mensagemId, - url: primeiraUrl, - }).catch((error) => { - console.error("Erro ao agendar processamento de preview de link:", error); - }); - } - } + // Detectar URLs no conteúdo e extrair preview (apenas para mensagens de texto, assíncrono) + if (args.tipo === 'texto') { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const urls = args.conteudo.match(urlRegex); + if (urls && urls.length > 0) { + // Pegar primeira URL encontrada + const primeiraUrl = urls[0]; + // Agendar processamento de preview via action wrapper + ctx.scheduler + .runAfter(1000, api.actions.linkPreview.processarPreviewLink, { + mensagemId, + url: primeiraUrl + }) + .catch((error) => { + console.error('Erro ao agendar processamento de preview de link:', error); + }); + } + } - // Atualizar última mensagem da conversa - await ctx.db.patch(args.conversaId, { - ultimaMensagem: args.conteudo.substring(0, 100), - ultimaMensagemTimestamp: Date.now(), - ultimaMensagemRemetenteId: usuarioAtual._id, // Guardar ID do remetente da última mensagem - }); + // Atualizar última mensagem da conversa + await ctx.db.patch(args.conversaId, { + ultimaMensagem: args.conteudo.substring(0, 100), + ultimaMensagemTimestamp: Date.now(), + ultimaMensagemRemetenteId: usuarioAtual._id // Guardar ID do remetente da última mensagem + }); - // Criar notificações para participantes (com tratamento de erro) - try { - for (const participanteId of conversa.participantes) { - // ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa - const ehOMesmoUsuario = participanteId === usuarioAtual._id; - const deveCriarNotificacao = - !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo; + // Criar notificações para participantes (com tratamento de erro) + try { + for (const participanteId of conversa.participantes) { + // ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa + const ehOMesmoUsuario = participanteId === usuarioAtual._id; + const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo; - if (deveCriarNotificacao) { - const tipoNotificacao = args.mencoes?.includes(participanteId) - ? "mencao" - : "nova_mensagem"; + if (deveCriarNotificacao) { + const tipoNotificacao = args.mencoes?.includes(participanteId) + ? 'mencao' + : 'nova_mensagem'; - const titulo = - tipoNotificacao === "mencao" - ? `${usuarioAtual.nome} mencionou você` - : `Nova mensagem de ${usuarioAtual.nome}`; - const descricao = args.conteudo.substring(0, 100); + const titulo = + tipoNotificacao === 'mencao' + ? `${usuarioAtual.nome} mencionou você` + : `Nova mensagem de ${usuarioAtual.nome}`; + const descricao = args.conteudo.substring(0, 100); - // Criar notificação no banco - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: tipoNotificacao, - conversaId: args.conversaId, - mensagemId, - remetenteId: usuarioAtual._id, - titulo, - descricao, - lida: false, - criadaEm: Date.now(), - }); + // Criar notificação no banco + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: tipoNotificacao, + conversaId: args.conversaId, + mensagemId, + remetenteId: usuarioAtual._id, + titulo, + descricao, + lida: false, + criadaEm: Date.now() + }); - // Enviar push notification (assíncrono, não bloqueia) - ctx.scheduler.runAfter(0, internal.pushNotifications.enviarPushNotification, { - usuarioId: participanteId, - titulo, - corpo: descricao, - data: { - conversaId: args.conversaId, - mensagemId, - tipo: tipoNotificacao, - }, - }).catch((error) => { - console.error(`Erro ao agendar push para usuário ${participanteId}:`, error); - }); + // Enviar push notification (assíncrono, não bloqueia) + ctx.scheduler + .runAfter(0, internal.pushNotifications.enviarPushNotification, { + usuarioId: participanteId, + titulo, + corpo: descricao, + data: { + conversaId: args.conversaId, + mensagemId, + tipo: tipoNotificacao + } + }) + .catch((error) => { + console.error(`Erro ao agendar push para usuário ${participanteId}:`, error); + }); - // Se usuário offline, enviar email (assíncrono) - const usuarioOnline = await ctx.runQuery(internal.pushNotifications.verificarUsuarioOnline, { - usuarioId: participanteId, - }); + // Se usuário offline, enviar email (assíncrono) + const usuarioOnline = await ctx.runQuery( + internal.pushNotifications.verificarUsuarioOnline, + { + usuarioId: participanteId + } + ); - if (!usuarioOnline) { - // Verificar preferências de email para esta conversa - const preferencias = await ctx.db - .query("preferenciasNotificacaoConversa") - .withIndex("by_usuario_conversa", (q) => - q.eq("usuarioId", participanteId).eq("conversaId", args.conversaId) - ) - .first(); + if (!usuarioOnline) { + // Verificar preferências de email para esta conversa + const preferencias = await ctx.db + .query('preferenciasNotificacaoConversa') + .withIndex('by_usuario_conversa', (q) => + q.eq('usuarioId', participanteId).eq('conversaId', args.conversaId) + ) + .first(); - const deveEnviarEmail = !preferencias || preferencias.emailAtivado !== false; + const deveEnviarEmail = !preferencias || preferencias.emailAtivado !== false; - if (deveEnviarEmail) { - // Buscar email do usuário - const usuarioParticipante = await ctx.db.get(participanteId); - if (usuarioParticipante?.email) { - // Obter URL do sistema (padrão: localhost para dev) - let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173"; - - // Garantir que a URL sempre tenha protocolo - if (!urlSistema.match(/^https?:\/\//i)) { - urlSistema = `http://${urlSistema}`; - } - - ctx.scheduler.runAfter(1000, api.email.enviarEmailComTemplate, { - destinatario: usuarioParticipante.email, - destinatarioId: participanteId, - templateCodigo: tipoNotificacao === "mencao" ? "chat_mencao" : "chat_mensagem", - variaveis: { - remetente: usuarioAtual.nome, - mensagem: descricao, - conversaId: args.conversaId.toString(), - urlSistema, - }, - enviadoPor: usuarioAtual._id, - }).catch((error) => { - console.error(`Erro ao agendar email para usuário ${participanteId}:`, error); - }); - } - } - } - } - } - } catch (error) { - // Log do erro mas não falhar o envio da mensagem - console.error("Erro ao criar notificações:", error); - // A mensagem já foi criada, então retornamos o ID normalmente - } + if (deveEnviarEmail) { + // Buscar email do usuário + const usuarioParticipante = await ctx.db.get(participanteId); + if (usuarioParticipante?.email) { + // Obter URL do sistema (padrão: localhost para dev) + let urlSistema = process.env.FRONTEND_URL || 'http://localhost:5173'; - return mensagemId; - }, + // Garantir que a URL sempre tenha protocolo + if (!urlSistema.match(/^https?:\/\//i)) { + urlSistema = `http://${urlSistema}`; + } + + ctx.scheduler + .runAfter(1000, api.email.enviarEmailComTemplate, { + destinatario: usuarioParticipante.email, + destinatarioId: participanteId, + templateCodigo: tipoNotificacao === 'mencao' ? 'chat_mencao' : 'chat_mensagem', + variaveis: { + remetente: usuarioAtual.nome, + mensagem: descricao, + conversaId: args.conversaId.toString(), + urlSistema + }, + enviadoPor: usuarioAtual._id + }) + .catch((error) => { + console.error(`Erro ao agendar email para usuário ${participanteId}:`, error); + }); + } + } + } + } + } + } catch (error) { + // Log do erro mas não falhar o envio da mensagem + console.error('Erro ao criar notificações:', error); + // A mensagem já foi criada, então retornamos o ID normalmente + } + + return mensagemId; + } }); /** * Agenda uma mensagem para envio futuro */ export const agendarMensagem = mutation({ - args: { - conversaId: v.id("conversas"), - conteudo: v.string(), - agendadaPara: v.number(), // timestamp - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + conversaId: v.id('conversas'), + conteudo: v.string(), + agendadaPara: v.number() // timestamp + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - // Validar data futura - if (args.agendadaPara <= Date.now()) { - throw new Error("Data de agendamento deve ser futura"); - } + // Validar data futura + if (args.agendadaPara <= Date.now()) { + throw new Error('Data de agendamento deve ser futura'); + } - // Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) throw new Error("Conversa não encontrada"); - if (!conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não pertence a esta conversa"); - } + // Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) throw new Error('Conversa não encontrada'); + if (!conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não pertence a esta conversa'); + } - // Normalizar conteúdo para busca - const conteudoBusca = normalizarTextoParaBusca(args.conteudo); + // Normalizar conteúdo para busca + const conteudoBusca = normalizarTextoParaBusca(args.conteudo); - // Criar mensagem agendada - const mensagemId = await ctx.db.insert("mensagens", { - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - tipo: "texto", - conteudo: args.conteudo, - conteudoBusca, - agendadaPara: args.agendadaPara, - enviadaEm: args.agendadaPara, // Será atualizado quando a mensagem for enviada - lidaPor: [], // Inicializar como array vazio - }); + // Criar mensagem agendada + const mensagemId = await ctx.db.insert('mensagens', { + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + tipo: 'texto', + conteudo: args.conteudo, + conteudoBusca, + agendadaPara: args.agendadaPara, + enviadaEm: args.agendadaPara, // Será atualizado quando a mensagem for enviada + lidaPor: [] // Inicializar como array vazio + }); - return mensagemId; - }, + return mensagemId; + } }); /** * Cancela uma mensagem agendada */ export const cancelarMensagemAgendada = mutation({ - args: { - mensagemId: v.id("mensagens"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args): Promise<{ sucesso: boolean; erro?: string }> => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Usuário não autenticado" }; - } + args: { + mensagemId: v.id('mensagens') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args): Promise<{ sucesso: boolean; erro?: string }> => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Usuário não autenticado' }; + } - const mensagem = await ctx.db.get(args.mensagemId); - if (!mensagem) { - return { sucesso: false, erro: "Mensagem não encontrada" }; - } + const mensagem = await ctx.db.get(args.mensagemId); + if (!mensagem) { + return { sucesso: false, erro: 'Mensagem não encontrada' }; + } - // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante - const conversa = await ctx.db.get(mensagem.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return { sucesso: false, erro: "Você não tem acesso a esta mensagem" }; - } + // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante + const conversa = await ctx.db.get(mensagem.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return { sucesso: false, erro: 'Você não tem acesso a esta mensagem' }; + } - // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa - if (!conversa.participantes.includes(mensagem.remetenteId)) { - return { sucesso: false, erro: "Mensagem inválida" }; - } + // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa + if (!conversa.participantes.includes(mensagem.remetenteId)) { + return { sucesso: false, erro: 'Mensagem inválida' }; + } - if (mensagem.remetenteId !== usuarioAtual._id) { - return { - sucesso: false, - erro: "Você só pode cancelar suas próprias mensagens", - }; - } + if (mensagem.remetenteId !== usuarioAtual._id) { + return { + sucesso: false, + erro: 'Você só pode cancelar suas próprias mensagens' + }; + } - if (!mensagem.agendadaPara) { - return { sucesso: false, erro: "Esta mensagem não está agendada" }; - } + if (!mensagem.agendadaPara) { + return { sucesso: false, erro: 'Esta mensagem não está agendada' }; + } - if (mensagem.agendadaPara <= Date.now()) { - return { sucesso: false, erro: "A data de agendamento já passou" }; - } + if (mensagem.agendadaPara <= Date.now()) { + return { sucesso: false, erro: 'A data de agendamento já passou' }; + } - await ctx.db.delete(args.mensagemId); - return { sucesso: true }; - }, + await ctx.db.delete(args.mensagemId); + return { sucesso: true }; + } }); /** @@ -602,52 +561,49 @@ export const cancelarMensagemAgendada = mutation({ * SEGURANÇA: Usuário só pode reagir a mensagens de conversas onde é participante */ export const reagirMensagem = mutation({ - args: { - mensagemId: v.id("mensagens"), - emoji: v.string(), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + mensagemId: v.id('mensagens'), + emoji: v.string() + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - const mensagem = await ctx.db.get(args.mensagemId); - if (!mensagem) throw new Error("Mensagem não encontrada"); + const mensagem = await ctx.db.get(args.mensagemId); + if (!mensagem) throw new Error('Mensagem não encontrada'); - // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante - const conversa = await ctx.db.get(mensagem.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não pode reagir a mensagens de conversas onde não participa"); - } + // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante + const conversa = await ctx.db.get(mensagem.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não pode reagir a mensagens de conversas onde não participa'); + } - // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa - if (!conversa.participantes.includes(mensagem.remetenteId)) { - throw new Error("Mensagem inválida"); - } + // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa + if (!conversa.participantes.includes(mensagem.remetenteId)) { + throw new Error('Mensagem inválida'); + } - const reacoes = mensagem.reagiuPor || []; - const reacaoExistente = reacoes.find( - (r) => r.usuarioId === usuarioAtual._id && r.emoji === args.emoji - ); + const reacoes = mensagem.reagiuPor || []; + const reacaoExistente = reacoes.find( + (r) => r.usuarioId === usuarioAtual._id && r.emoji === args.emoji + ); - if (reacaoExistente) { - // Remover reação - await ctx.db.patch(args.mensagemId, { - reagiuPor: reacoes.filter( - (r) => !(r.usuarioId === usuarioAtual._id && r.emoji === args.emoji) - ), - }); - } else { - // Adicionar reação - await ctx.db.patch(args.mensagemId, { - reagiuPor: [ - ...reacoes, - { usuarioId: usuarioAtual._id, emoji: args.emoji }, - ], - }); - } + if (reacaoExistente) { + // Remover reação + await ctx.db.patch(args.mensagemId, { + reagiuPor: reacoes.filter( + (r) => !(r.usuarioId === usuarioAtual._id && r.emoji === args.emoji) + ) + }); + } else { + // Adicionar reação + await ctx.db.patch(args.mensagemId, { + reagiuPor: [...reacoes, { usuarioId: usuarioAtual._id, emoji: args.emoji }] + }); + } - return true; - }, + return true; + } }); /** @@ -655,115 +611,113 @@ export const reagirMensagem = mutation({ * SEGURANÇA: Usuário só pode marcar como lida mensagens de conversas onde é participante */ export const marcarComoLida = mutation({ - args: { - conversaId: v.id("conversas"), - mensagemId: v.id("mensagens"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + conversaId: v.id('conversas'), + mensagemId: v.id('mensagens') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - // SEGURANÇA: Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não pertence a esta conversa"); - } + // SEGURANÇA: Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não pertence a esta conversa'); + } - // SEGURANÇA: Verificar se a mensagem pertence à conversa e se o remetente é participante - const mensagem = await ctx.db.get(args.mensagemId); - if (!mensagem || mensagem.conversaId !== args.conversaId) { - throw new Error("Mensagem não encontrada nesta conversa"); - } - if (!conversa.participantes.includes(mensagem.remetenteId)) { - throw new Error("Mensagem inválida"); - } + // SEGURANÇA: Verificar se a mensagem pertence à conversa e se o remetente é participante + const mensagem = await ctx.db.get(args.mensagemId); + if (!mensagem || mensagem.conversaId !== args.conversaId) { + throw new Error('Mensagem não encontrada nesta conversa'); + } + if (!conversa.participantes.includes(mensagem.remetenteId)) { + throw new Error('Mensagem inválida'); + } - // Buscar registro de leitura existente - const leituraExistente = await ctx.db - .query("leituras") - .withIndex("by_conversa_usuario", (q) => - q.eq("conversaId", args.conversaId).eq("usuarioId", usuarioAtual._id) - ) - .first(); + // Buscar registro de leitura existente + const leituraExistente = await ctx.db + .query('leituras') + .withIndex('by_conversa_usuario', (q) => + q.eq('conversaId', args.conversaId).eq('usuarioId', usuarioAtual._id) + ) + .first(); - if (leituraExistente) { - await ctx.db.patch(leituraExistente._id, { - ultimaMensagemLida: args.mensagemId, - lidaEm: Date.now(), - }); - } else { - await ctx.db.insert("leituras", { - conversaId: args.conversaId, - usuarioId: usuarioAtual._id, - ultimaMensagemLida: args.mensagemId, - lidaEm: Date.now(), - }); - } + if (leituraExistente) { + await ctx.db.patch(leituraExistente._id, { + ultimaMensagemLida: args.mensagemId, + lidaEm: Date.now() + }); + } else { + await ctx.db.insert('leituras', { + conversaId: args.conversaId, + usuarioId: usuarioAtual._id, + ultimaMensagemLida: args.mensagemId, + lidaEm: Date.now() + }); + } - // Atualizar status de leitura nas mensagens - // Buscar todas as mensagens até a mensagem atual (incluindo ela) na conversa - const todasMensagens = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) - .filter((q) => - q.and( - q.lte(q.field("enviadaEm"), mensagem.enviadaEm), - q.neq(q.field("remetenteId"), usuarioAtual._id) // Apenas mensagens de outros usuários - ) - ) - .collect(); + // Atualizar status de leitura nas mensagens + // Buscar todas as mensagens até a mensagem atual (incluindo ela) na conversa + const todasMensagens = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId)) + .filter((q) => + q.and( + q.lte(q.field('enviadaEm'), mensagem.enviadaEm), + q.neq(q.field('remetenteId'), usuarioAtual._id) // Apenas mensagens de outros usuários + ) + ) + .collect(); - // Atualizar cada mensagem para incluir o usuário atual no array lidaPor (se ainda não estiver) - for (const msg of todasMensagens) { - const lidaPor = msg.lidaPor || []; - if (!lidaPor.includes(usuarioAtual._id)) { - await ctx.db.patch(msg._id, { - lidaPor: [...lidaPor, usuarioAtual._id], - }); - } - } + // Atualizar cada mensagem para incluir o usuário atual no array lidaPor (se ainda não estiver) + for (const msg of todasMensagens) { + const lidaPor = msg.lidaPor || []; + if (!lidaPor.includes(usuarioAtual._id)) { + await ctx.db.patch(msg._id, { + lidaPor: [...lidaPor, usuarioAtual._id] + }); + } + } - // Marcar notificações desta conversa como lidas - const notificacoes = await ctx.db - .query("notificacoes") - .withIndex("by_usuario_lida", (q) => - q.eq("usuarioId", usuarioAtual._id).eq("lida", false) - ) - .filter((q) => q.eq(q.field("conversaId"), args.conversaId)) - .collect(); + // Marcar notificações desta conversa como lidas + const notificacoes = await ctx.db + .query('notificacoes') + .withIndex('by_usuario_lida', (q) => q.eq('usuarioId', usuarioAtual._id).eq('lida', false)) + .filter((q) => q.eq(q.field('conversaId'), args.conversaId)) + .collect(); - for (const notificacao of notificacoes) { - await ctx.db.patch(notificacao._id, { lida: true }); - } + for (const notificacao of notificacoes) { + await ctx.db.patch(notificacao._id, { lida: true }); + } - return true; - }, + return true; + } }); /** * Atualiza o status de presença do usuário */ export const atualizarStatusPresenca = mutation({ - args: { - status: v.union( - v.literal("online"), - v.literal("offline"), - v.literal("ausente"), - v.literal("externo"), - v.literal("em_reuniao") - ), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + status: v.union( + v.literal('online'), + v.literal('offline'), + v.literal('ausente'), + v.literal('externo'), + v.literal('em_reuniao') + ) + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - await ctx.db.patch(usuarioAtual._id, { - statusPresenca: args.status, - ultimaAtividade: Date.now(), - }); + await ctx.db.patch(usuarioAtual._id, { + statusPresenca: args.status, + ultimaAtividade: Date.now() + }); - return true; - }, + return true; + } }); /** @@ -771,63 +725,63 @@ export const atualizarStatusPresenca = mutation({ * SEGURANÇA: Usuário só pode indicar digitação em conversas onde é participante */ export const indicarDigitacao = mutation({ - args: { - conversaId: v.id("conversas"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + conversaId: v.id('conversas') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - // SEGURANÇA: Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não pertence a esta conversa"); - } + // SEGURANÇA: Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não pertence a esta conversa'); + } - // Buscar indicador existente - const indicadorExistente = await ctx.db - .query("digitando") - .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioAtual._id)) - .filter((q) => q.eq(q.field("conversaId"), args.conversaId)) - .first(); + // Buscar indicador existente + const indicadorExistente = await ctx.db + .query('digitando') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuarioAtual._id)) + .filter((q) => q.eq(q.field('conversaId'), args.conversaId)) + .first(); - if (indicadorExistente) { - await ctx.db.patch(indicadorExistente._id, { - iniciouEm: Date.now(), - }); - } else { - await ctx.db.insert("digitando", { - conversaId: args.conversaId, - usuarioId: usuarioAtual._id, - iniciouEm: Date.now(), - }); - } + if (indicadorExistente) { + await ctx.db.patch(indicadorExistente._id, { + iniciouEm: Date.now() + }); + } else { + await ctx.db.insert('digitando', { + conversaId: args.conversaId, + usuarioId: usuarioAtual._id, + iniciouEm: Date.now() + }); + } - return true; - }, + return true; + } }); /** * Gera URL para upload de arquivo no chat */ export const uploadArquivoChat = mutation({ - args: { - conversaId: v.id("conversas"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + conversaId: v.id('conversas') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - // Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) throw new Error("Conversa não encontrada"); + // Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) throw new Error('Conversa não encontrada'); - if (!conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não pertence a esta conversa"); - } + if (!conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não pertence a esta conversa'); + } - return await ctx.storage.generateUploadUrl(); - }, + return await ctx.storage.generateUploadUrl(); + } }); /** @@ -835,56 +789,54 @@ export const uploadArquivoChat = mutation({ * SEGURANÇA: Usuário só pode marcar como lida suas próprias notificações */ export const marcarNotificacaoLida = mutation({ - args: { - notificacaoId: v.id("notificacoes"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + notificacaoId: v.id('notificacoes') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - const notificacao = await ctx.db.get(args.notificacaoId); - if (!notificacao) throw new Error("Notificação não encontrada"); + const notificacao = await ctx.db.get(args.notificacaoId); + if (!notificacao) throw new Error('Notificação não encontrada'); - // SEGURANÇA: Verificar se a notificação pertence ao usuário atual - if (notificacao.usuarioId !== usuarioAtual._id) { - throw new Error("Você não tem permissão para marcar esta notificação como lida"); - } + // SEGURANÇA: Verificar se a notificação pertence ao usuário atual + if (notificacao.usuarioId !== usuarioAtual._id) { + throw new Error('Você não tem permissão para marcar esta notificação como lida'); + } - // SEGURANÇA: Se a notificação tem conversaId, verificar se usuário ainda é participante - if (notificacao.conversaId) { - const conversa = await ctx.db.get(notificacao.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não tem acesso a esta notificação"); - } - } + // SEGURANÇA: Se a notificação tem conversaId, verificar se usuário ainda é participante + if (notificacao.conversaId) { + const conversa = await ctx.db.get(notificacao.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não tem acesso a esta notificação'); + } + } - await ctx.db.patch(args.notificacaoId, { lida: true }); - return true; - }, + await ctx.db.patch(args.notificacaoId, { lida: true }); + return true; + } }); /** * Marca todas as notificações como lidas */ export const marcarTodasNotificacoesLidas = mutation({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - const notificacoes = await ctx.db - .query("notificacoes") - .withIndex("by_usuario_lida", (q) => - q.eq("usuarioId", usuarioAtual._id).eq("lida", false) - ) - .collect(); + const notificacoes = await ctx.db + .query('notificacoes') + .withIndex('by_usuario_lida', (q) => q.eq('usuarioId', usuarioAtual._id).eq('lida', false)) + .collect(); - for (const notificacao of notificacoes) { - await ctx.db.patch(notificacao._id, { lida: true }); - } + for (const notificacao of notificacoes) { + await ctx.db.patch(notificacao._id, { lida: true }); + } - return true; - }, + return true; + } }); /** @@ -892,22 +844,22 @@ export const marcarTodasNotificacoesLidas = mutation({ * SEGURANÇA: Usuário só pode deletar suas próprias notificações */ export const limparTodasNotificacoes = mutation({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - const notificacoes = await ctx.db - .query("notificacoes") - .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioAtual._id)) - .collect(); + const notificacoes = await ctx.db + .query('notificacoes') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuarioAtual._id)) + .collect(); - for (const notificacao of notificacoes) { - await ctx.db.delete(notificacao._id); - } + for (const notificacao of notificacoes) { + await ctx.db.delete(notificacao._id); + } - return { excluidas: notificacoes.length }; - }, + return { excluidas: notificacoes.length }; + } }); /** @@ -915,24 +867,22 @@ export const limparTodasNotificacoes = mutation({ * SEGURANÇA: Usuário só pode deletar suas próprias notificações */ export const limparNotificacoesNaoLidas = mutation({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - const notificacoes = await ctx.db - .query("notificacoes") - .withIndex("by_usuario_lida", (q) => - q.eq("usuarioId", usuarioAtual._id).eq("lida", false) - ) - .collect(); + const notificacoes = await ctx.db + .query('notificacoes') + .withIndex('by_usuario_lida', (q) => q.eq('usuarioId', usuarioAtual._id).eq('lida', false)) + .collect(); - for (const notificacao of notificacoes) { - await ctx.db.delete(notificacao._id); - } + for (const notificacao of notificacoes) { + await ctx.db.delete(notificacao._id); + } - return { excluidas: notificacoes.length }; - }, + return { excluidas: notificacoes.length }; + } }); /** @@ -942,534 +892,559 @@ export const limparNotificacoesNaoLidas = mutation({ * Editar mensagem enviada */ export const editarMensagem = mutation({ - args: { - mensagemId: v.id("mensagens"), - novoConteudo: v.string(), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + mensagemId: v.id('mensagens'), + novoConteudo: v.string() + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const mensagem = await ctx.db.get(args.mensagemId); - if (!mensagem) { - return { sucesso: false, erro: "Mensagem não encontrada" }; - } + const mensagem = await ctx.db.get(args.mensagemId); + if (!mensagem) { + return { sucesso: false, erro: 'Mensagem não encontrada' }; + } - // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante - const conversa = await ctx.db.get(mensagem.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return { sucesso: false, erro: "Você não tem acesso a esta mensagem" }; - } + // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante + const conversa = await ctx.db.get(mensagem.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return { sucesso: false, erro: 'Você não tem acesso a esta mensagem' }; + } - // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa - if (!conversa.participantes.includes(mensagem.remetenteId)) { - return { sucesso: false, erro: "Mensagem inválida" }; - } + // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa + if (!conversa.participantes.includes(mensagem.remetenteId)) { + return { sucesso: false, erro: 'Mensagem inválida' }; + } - // Verificar se usuário é o remetente - if (mensagem.remetenteId !== usuarioAtual._id) { - return { sucesso: false, erro: "Você só pode editar suas próprias mensagens" }; - } + // Verificar se usuário é o remetente + if (mensagem.remetenteId !== usuarioAtual._id) { + return { sucesso: false, erro: 'Você só pode editar suas próprias mensagens' }; + } - // Verificar se mensagem não foi deletada - if (mensagem.deletada) { - return { sucesso: false, erro: "Não é possível editar uma mensagem deletada" }; - } + // Verificar se mensagem não foi deletada + if (mensagem.deletada) { + return { sucesso: false, erro: 'Não é possível editar uma mensagem deletada' }; + } - // Verificar se não é mensagem agendada - if (mensagem.agendadaPara) { - return { sucesso: false, erro: "Não é possível editar mensagens agendadas" }; - } + // Verificar se não é mensagem agendada + if (mensagem.agendadaPara) { + return { sucesso: false, erro: 'Não é possível editar mensagens agendadas' }; + } - // Validar novo conteúdo - if (!args.novoConteudo || args.novoConteudo.trim().length === 0) { - return { sucesso: false, erro: "O conteúdo da mensagem não pode estar vazio" }; - } + // Validar novo conteúdo + if (!args.novoConteudo || args.novoConteudo.trim().length === 0) { + return { sucesso: false, erro: 'O conteúdo da mensagem não pode estar vazio' }; + } - // Normalizar conteúdo para busca - const conteudoBusca = normalizarTextoParaBusca(args.novoConteudo); + // Normalizar conteúdo para busca + const conteudoBusca = normalizarTextoParaBusca(args.novoConteudo); - // Atualizar mensagem - await ctx.db.patch(args.mensagemId, { - conteudo: args.novoConteudo.trim(), - conteudoBusca, - editadaEm: Date.now(), - }); + // Atualizar mensagem + await ctx.db.patch(args.mensagemId, { + conteudo: args.novoConteudo.trim(), + conteudoBusca, + editadaEm: Date.now() + }); - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Mutation interna para atualizar link preview */ export const atualizarLinkPreview = internalMutation({ - args: { - mensagemId: v.id("mensagens"), - linkPreview: v.object({ - url: v.string(), - titulo: v.optional(v.string()), - descricao: v.optional(v.string()), - imagem: v.optional(v.string()), - site: v.optional(v.string()), - }), - }, - returns: v.null(), - handler: async (ctx, args) => { - await ctx.db.patch(args.mensagemId, { - linkPreview: args.linkPreview, - }); - return null; - }, + args: { + mensagemId: v.id('mensagens'), + linkPreview: v.object({ + url: v.string(), + titulo: v.optional(v.string()), + descricao: v.optional(v.string()), + imagem: v.optional(v.string()), + site: v.optional(v.string()) + }) + }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.patch(args.mensagemId, { + linkPreview: args.linkPreview + }); + return null; + } }); export const deletarMensagem = mutation({ - args: { - mensagemId: v.id("mensagens"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) throw new Error("Não autenticado"); + args: { + mensagemId: v.id('mensagens') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error('Não autenticado'); - const mensagem = await ctx.db.get(args.mensagemId); - if (!mensagem) throw new Error("Mensagem não encontrada"); + const mensagem = await ctx.db.get(args.mensagemId); + if (!mensagem) throw new Error('Mensagem não encontrada'); - // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante - const conversa = await ctx.db.get(mensagem.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - throw new Error("Você não tem acesso a esta mensagem"); - } + // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante + const conversa = await ctx.db.get(mensagem.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + throw new Error('Você não tem acesso a esta mensagem'); + } - // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa - if (!conversa.participantes.includes(mensagem.remetenteId)) { - throw new Error("Mensagem inválida"); - } + // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa + if (!conversa.participantes.includes(mensagem.remetenteId)) { + throw new Error('Mensagem inválida'); + } - // Verificar se é admin de sala de reunião ou se é o próprio remetente - const isAdmin = await verificarPermissaoAdmin(ctx, mensagem.conversaId, usuarioAtual._id); - - if (mensagem.remetenteId !== usuarioAtual._id && !isAdmin) { - throw new Error("Você só pode deletar suas próprias mensagens"); - } + // Verificar se é admin de sala de reunião ou se é o próprio remetente + const isAdmin = await verificarPermissaoAdmin(ctx, mensagem.conversaId, usuarioAtual._id); - await ctx.db.patch(args.mensagemId, { - deletada: true, - conteudo: "Mensagem deletada", - }); + if (mensagem.remetenteId !== usuarioAtual._id && !isAdmin) { + throw new Error('Você só pode deletar suas próprias mensagens'); + } - return true; - }, + await ctx.db.patch(args.mensagemId, { + deletada: true, + conteudo: 'Mensagem deletada' + }); + + return true; + } }); /** * Deleta uma mensagem como administrador (com notificação ao remetente) */ export const deletarMensagemComoAdmin = mutation({ - args: { - mensagemId: v.id("mensagens"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + mensagemId: v.id('mensagens') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const mensagem = await ctx.db.get(args.mensagemId); - if (!mensagem) { - return { sucesso: false, erro: "Mensagem não encontrada" }; - } + const mensagem = await ctx.db.get(args.mensagemId); + if (!mensagem) { + return { sucesso: false, erro: 'Mensagem não encontrada' }; + } - // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante - const conversa = await ctx.db.get(mensagem.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return { sucesso: false, erro: "Você não tem acesso a esta mensagem" }; - } + // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante + const conversa = await ctx.db.get(mensagem.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return { sucesso: false, erro: 'Você não tem acesso a esta mensagem' }; + } - // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa - if (!conversa.participantes.includes(mensagem.remetenteId)) { - return { sucesso: false, erro: "Mensagem inválida" }; - } + // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa + if (!conversa.participantes.includes(mensagem.remetenteId)) { + return { sucesso: false, erro: 'Mensagem inválida' }; + } - // Verificar se usuário é administrador da sala - const isAdmin = await verificarPermissaoAdmin(ctx, mensagem.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem deletar mensagens de outros usuários" }; - } + // Verificar se usuário é administrador da sala + const isAdmin = await verificarPermissaoAdmin(ctx, mensagem.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { + sucesso: false, + erro: 'Apenas administradores podem deletar mensagens de outros usuários' + }; + } - // Não permitir deletar mensagem já deletada - if (mensagem.deletada) { - return { sucesso: false, erro: "Mensagem já foi deletada" }; - } + // Não permitir deletar mensagem já deletada + if (mensagem.deletada) { + return { sucesso: false, erro: 'Mensagem já foi deletada' }; + } - // Deletar mensagem - await ctx.db.patch(args.mensagemId, { - deletada: true, - conteudo: "Mensagem deletada por administrador", - }); + // Deletar mensagem + await ctx.db.patch(args.mensagemId, { + deletada: true, + conteudo: 'Mensagem deletada por administrador' + }); - // Criar notificação para o remetente original (se não for o próprio admin) - if (mensagem.remetenteId !== usuarioAtual._id) { - const remetente = await ctx.db.get(mensagem.remetenteId); - if (remetente) { - await ctx.db.insert("notificacoes", { - usuarioId: mensagem.remetenteId, - tipo: "nova_mensagem", - conversaId: mensagem.conversaId, - mensagemId: args.mensagemId, - remetenteId: usuarioAtual._id, - titulo: "Mensagem deletada", - descricao: `Sua mensagem foi deletada por um administrador da sala "${conversa.nome || "Sem nome"}"`, - lida: false, - criadaEm: Date.now(), - }); - } - } + // Criar notificação para o remetente original (se não for o próprio admin) + if (mensagem.remetenteId !== usuarioAtual._id) { + const remetente = await ctx.db.get(mensagem.remetenteId); + if (remetente) { + await ctx.db.insert('notificacoes', { + usuarioId: mensagem.remetenteId, + tipo: 'nova_mensagem', + conversaId: mensagem.conversaId, + mensagemId: args.mensagemId, + remetenteId: usuarioAtual._id, + titulo: 'Mensagem deletada', + descricao: `Sua mensagem foi deletada por um administrador da sala "${conversa.nome || 'Sem nome'}"`, + lida: false, + criadaEm: Date.now() + }); + } + } - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Adiciona um participante à sala de reunião (apenas administradores) */ export const adicionarParticipanteSala = mutation({ - args: { - conversaId: v.id("conversas"), - participanteId: v.id("usuarios"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas'), + participanteId: v.id('usuarios') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Sala de reunião não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Sala de reunião não encontrada' }; + } - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" }; - } + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') { + return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' }; + } - // Verificar se usuário é administrador - const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem adicionar participantes" }; - } + // Verificar se usuário é administrador + const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { sucesso: false, erro: 'Apenas administradores podem adicionar participantes' }; + } - // Verificar se participante já está na sala - if (conversa.participantes.includes(args.participanteId)) { - return { sucesso: false, erro: "Usuário já é participante desta sala" }; - } + // Verificar se participante já está na sala + if (conversa.participantes.includes(args.participanteId)) { + return { sucesso: false, erro: 'Usuário já é participante desta sala' }; + } - // Verificar se participante existe - const participante = await ctx.db.get(args.participanteId); - if (!participante) { - return { sucesso: false, erro: "Usuário não encontrado" }; - } + // Verificar se participante existe + const participante = await ctx.db.get(args.participanteId); + if (!participante) { + return { sucesso: false, erro: 'Usuário não encontrado' }; + } - // Adicionar participante - const novosParticipantes = [...conversa.participantes, args.participanteId]; - await ctx.db.patch(args.conversaId, { - participantes: novosParticipantes, - }); + // Adicionar participante + const novosParticipantes = [...conversa.participantes, args.participanteId]; + await ctx.db.patch(args.conversaId, { + participantes: novosParticipantes + }); - // Criar notificação para o novo participante - await ctx.db.insert("notificacoes", { - usuarioId: args.participanteId, - tipo: "adicionado_grupo", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: "Adicionado a sala de reunião", - descricao: `Você foi adicionado à sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); + // Criar notificação para o novo participante + await ctx.db.insert('notificacoes', { + usuarioId: args.participanteId, + tipo: 'adicionado_grupo', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Adicionado a sala de reunião', + descricao: `Você foi adicionado à sala de reunião "${conversa.nome || 'Sem nome'}" por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Remove um participante da sala de reunião (apenas administradores, não pode remover outros admins) */ export const removerParticipanteSala = mutation({ - args: { - conversaId: v.id("conversas"), - participanteId: v.id("usuarios"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas'), + participanteId: v.id('usuarios') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Sala de reunião não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Sala de reunião não encontrada' }; + } - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" }; - } + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') { + return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' }; + } - // Verificar se usuário é administrador - const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem remover participantes" }; - } + // Verificar se usuário é administrador + const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { sucesso: false, erro: 'Apenas administradores podem remover participantes' }; + } - // Verificar se participante está na sala - if (!conversa.participantes.includes(args.participanteId)) { - return { sucesso: false, erro: "Usuário não é participante desta sala" }; - } + // Verificar se participante está na sala + if (!conversa.participantes.includes(args.participanteId)) { + return { sucesso: false, erro: 'Usuário não é participante desta sala' }; + } - // Verificar se está tentando remover outro administrador - const isParticipanteAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId); - if (isParticipanteAdmin) { - return { sucesso: false, erro: "Não é possível remover outros administradores" }; - } + // Verificar se está tentando remover outro administrador + const isParticipanteAdmin = await verificarPermissaoAdmin( + ctx, + args.conversaId, + args.participanteId + ); + if (isParticipanteAdmin) { + return { sucesso: false, erro: 'Não é possível remover outros administradores' }; + } - // Remover participante - const novosParticipantes = conversa.participantes.filter((p) => p !== args.participanteId); - await ctx.db.patch(args.conversaId, { - participantes: novosParticipantes, - }); + // Remover participante + const novosParticipantes = conversa.participantes.filter((p) => p !== args.participanteId); + await ctx.db.patch(args.conversaId, { + participantes: novosParticipantes + }); - // Criar notificação para o participante removido - const participanteRemovido = await ctx.db.get(args.participanteId); - if (participanteRemovido) { - await ctx.db.insert("notificacoes", { - usuarioId: args.participanteId, - tipo: "nova_mensagem", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: "Removido da sala de reunião", - descricao: `Você foi removido da sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); - } + // Criar notificação para o participante removido + const participanteRemovido = await ctx.db.get(args.participanteId); + if (participanteRemovido) { + await ctx.db.insert('notificacoes', { + usuarioId: args.participanteId, + tipo: 'nova_mensagem', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Removido da sala de reunião', + descricao: `Você foi removido da sala de reunião "${conversa.nome || 'Sem nome'}" por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); + } - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Promove um participante a administrador (apenas administradores) */ export const promoverAdministrador = mutation({ - args: { - conversaId: v.id("conversas"), - participanteId: v.id("usuarios"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas'), + participanteId: v.id('usuarios') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Sala de reunião não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Sala de reunião não encontrada' }; + } - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" }; - } + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') { + return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' }; + } - // Verificar se usuário é administrador - const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem promover outros administradores" }; - } + // Verificar se usuário é administrador + const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { + sucesso: false, + erro: 'Apenas administradores podem promover outros administradores' + }; + } - // Verificar se participante está na sala - if (!conversa.participantes.includes(args.participanteId)) { - return { sucesso: false, erro: "Usuário não é participante desta sala" }; - } + // Verificar se participante está na sala + if (!conversa.participantes.includes(args.participanteId)) { + return { sucesso: false, erro: 'Usuário não é participante desta sala' }; + } - // Verificar se já é administrador - const jaEhAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId); - if (jaEhAdmin) { - return { sucesso: false, erro: "Usuário já é administrador desta sala" }; - } + // Verificar se já é administrador + const jaEhAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId); + if (jaEhAdmin) { + return { sucesso: false, erro: 'Usuário já é administrador desta sala' }; + } - // Obter lista atual de administradores ou criar nova - const administradoresAtuais = conversa.administradores || []; - - // Se não está na lista, adicionar - if (!administradoresAtuais.includes(args.participanteId)) { - const novosAdministradores = [...administradoresAtuais, args.participanteId]; - await ctx.db.patch(args.conversaId, { - administradores: novosAdministradores, - }); + // Obter lista atual de administradores ou criar nova + const administradoresAtuais = conversa.administradores || []; - // Criar notificação para o novo administrador - const novoAdmin = await ctx.db.get(args.participanteId); - if (novoAdmin) { - await ctx.db.insert("notificacoes", { - usuarioId: args.participanteId, - tipo: "nova_mensagem", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: "Promovido a administrador", - descricao: `Você foi promovido a administrador da sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); - } - } + // Se não está na lista, adicionar + if (!administradoresAtuais.includes(args.participanteId)) { + const novosAdministradores = [...administradoresAtuais, args.participanteId]; + await ctx.db.patch(args.conversaId, { + administradores: novosAdministradores + }); - return { sucesso: true }; - }, + // Criar notificação para o novo administrador + const novoAdmin = await ctx.db.get(args.participanteId); + if (novoAdmin) { + await ctx.db.insert('notificacoes', { + usuarioId: args.participanteId, + tipo: 'nova_mensagem', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Promovido a administrador', + descricao: `Você foi promovido a administrador da sala de reunião "${conversa.nome || 'Sem nome'}" por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); + } + } + + return { sucesso: true }; + } }); /** * Rebaixa um administrador a participante (apenas administradores, não pode rebaixar a si mesmo) */ export const rebaixarAdministrador = mutation({ - args: { - conversaId: v.id("conversas"), - participanteId: v.id("usuarios"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas'), + participanteId: v.id('usuarios') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Sala de reunião não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Sala de reunião não encontrada' }; + } - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" }; - } + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') { + return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' }; + } - // Verificar se usuário é administrador - const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem rebaixar outros administradores" }; - } + // Verificar se usuário é administrador + const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { + sucesso: false, + erro: 'Apenas administradores podem rebaixar outros administradores' + }; + } - // Não permitir rebaixar a si mesmo - if (args.participanteId === usuarioAtual._id) { - return { sucesso: false, erro: "Você não pode rebaixar a si mesmo" }; - } + // Não permitir rebaixar a si mesmo + if (args.participanteId === usuarioAtual._id) { + return { sucesso: false, erro: 'Você não pode rebaixar a si mesmo' }; + } - // Verificar se é administrador - const isParticipanteAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId); - if (!isParticipanteAdmin) { - return { sucesso: false, erro: "Usuário não é administrador desta sala" }; - } + // Verificar se é administrador + const isParticipanteAdmin = await verificarPermissaoAdmin( + ctx, + args.conversaId, + args.participanteId + ); + if (!isParticipanteAdmin) { + return { sucesso: false, erro: 'Usuário não é administrador desta sala' }; + } - // Não permitir rebaixar o criador da sala - if (conversa.criadoPor === args.participanteId) { - return { sucesso: false, erro: "Não é possível rebaixar o criador da sala" }; - } + // Não permitir rebaixar o criador da sala + if (conversa.criadoPor === args.participanteId) { + return { sucesso: false, erro: 'Não é possível rebaixar o criador da sala' }; + } - // Remover da lista de administradores - const administradoresAtuais = conversa.administradores || []; - const novosAdministradores = administradoresAtuais.filter((adminId) => adminId !== args.participanteId); - - await ctx.db.patch(args.conversaId, { - administradores: novosAdministradores.length > 0 ? novosAdministradores : undefined, - }); + // Remover da lista de administradores + const administradoresAtuais = conversa.administradores || []; + const novosAdministradores = administradoresAtuais.filter( + (adminId) => adminId !== args.participanteId + ); - // Criar notificação para o administrador rebaixado - const adminRebaixado = await ctx.db.get(args.participanteId); - if (adminRebaixado) { - await ctx.db.insert("notificacoes", { - usuarioId: args.participanteId, - tipo: "nova_mensagem", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: "Rebaixado de administrador", - descricao: `Você foi rebaixado de administrador da sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); - } + await ctx.db.patch(args.conversaId, { + administradores: novosAdministradores.length > 0 ? novosAdministradores : undefined + }); - return { sucesso: true }; - }, + // Criar notificação para o administrador rebaixado + const adminRebaixado = await ctx.db.get(args.participanteId); + if (adminRebaixado) { + await ctx.db.insert('notificacoes', { + usuarioId: args.participanteId, + tipo: 'nova_mensagem', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Rebaixado de administrador', + descricao: `Você foi rebaixado de administrador da sala de reunião "${conversa.nome || 'Sem nome'}" por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); + } + + return { sucesso: true }; + } }); /** * Permite que um usuário saia de um grupo ou sala de reunião */ export const sairGrupoOuSala = mutation({ - args: { - conversaId: v.id("conversas"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Conversa não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Conversa não encontrada' }; + } - // Verificar se é grupo ou sala de reunião - if (conversa.tipo !== "grupo" && conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para grupos e salas de reunião" }; - } + // Verificar se é grupo ou sala de reunião + if (conversa.tipo !== 'grupo' && conversa.tipo !== 'sala_reuniao') { + return { + sucesso: false, + erro: 'Esta funcionalidade é apenas para grupos e salas de reunião' + }; + } - // Verificar se usuário é participante - if (!conversa.participantes.includes(usuarioAtual._id)) { - return { sucesso: false, erro: "Você não é participante desta conversa" }; - } + // Verificar se usuário é participante + if (!conversa.participantes.includes(usuarioAtual._id)) { + return { sucesso: false, erro: 'Você não é participante desta conversa' }; + } - // Remover usuário dos participantes - const novosParticipantes = conversa.participantes.filter((p) => p !== usuarioAtual._id); - - // Se for sala de reunião e o usuário for administrador, removê-lo também dos administradores - let novosAdministradores = conversa.administradores; - if (conversa.tipo === "sala_reuniao" && conversa.administradores) { - novosAdministradores = conversa.administradores.filter((adminId) => adminId !== usuarioAtual._id); - } + // Remover usuário dos participantes + const novosParticipantes = conversa.participantes.filter((p) => p !== usuarioAtual._id); - await ctx.db.patch(args.conversaId, { - participantes: novosParticipantes, - administradores: novosAdministradores && novosAdministradores.length > 0 ? novosAdministradores : undefined, - }); + // Se for sala de reunião e o usuário for administrador, removê-lo também dos administradores + let novosAdministradores = conversa.administradores; + if (conversa.tipo === 'sala_reuniao' && conversa.administradores) { + novosAdministradores = conversa.administradores.filter( + (adminId) => adminId !== usuarioAtual._id + ); + } - // Criar notificação para outros participantes informando que o usuário saiu - const tipoTexto = conversa.tipo === "sala_reuniao" ? "sala de reunião" : "grupo"; - for (const participanteId of novosParticipantes) { - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: "nova_mensagem", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: "Participante saiu", - descricao: `${usuarioAtual.nome} saiu da ${tipoTexto} "${conversa.nome || "Sem nome"}"`, - lida: false, - criadaEm: Date.now(), - }); - } + await ctx.db.patch(args.conversaId, { + participantes: novosParticipantes, + administradores: + novosAdministradores && novosAdministradores.length > 0 ? novosAdministradores : undefined + }); - return { sucesso: true }; - }, + // Criar notificação para outros participantes informando que o usuário saiu + const tipoTexto = conversa.tipo === 'sala_reuniao' ? 'sala de reunião' : 'grupo'; + for (const participanteId of novosParticipantes) { + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: 'nova_mensagem', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Participante saiu', + descricao: `${usuarioAtual.nome} saiu da ${tipoTexto} "${conversa.nome || 'Sem nome'}"`, + lida: false, + criadaEm: Date.now() + }); + } + + return { sucesso: true }; + } }); /** @@ -1477,124 +1452,126 @@ export const sairGrupoOuSala = mutation({ * Remove todos os participantes e marca a sala como encerrada */ export const encerrarReuniao = mutation({ - args: { - conversaId: v.id("conversas"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas') + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Sala de reunião não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Sala de reunião não encontrada' }; + } - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" }; - } + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') { + return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' }; + } - // Verificar se usuário é administrador - const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem encerrar a reunião" }; - } + // Verificar se usuário é administrador + const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { sucesso: false, erro: 'Apenas administradores podem encerrar a reunião' }; + } - // Criar notificação para todos os participantes informando que a reunião foi encerrada - for (const participanteId of conversa.participantes) { - if (participanteId !== usuarioAtual._id) { - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: "nova_mensagem", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: "Reunião encerrada", - descricao: `A sala de reunião "${conversa.nome || "Sem nome"}" foi encerrada por ${usuarioAtual.nome}`, - lida: false, - criadaEm: Date.now(), - }); - } - } + // Criar notificação para todos os participantes informando que a reunião foi encerrada + for (const participanteId of conversa.participantes) { + if (participanteId !== usuarioAtual._id) { + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: 'nova_mensagem', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: 'Reunião encerrada', + descricao: `A sala de reunião "${conversa.nome || 'Sem nome'}" foi encerrada por ${usuarioAtual.nome}`, + lida: false, + criadaEm: Date.now() + }); + } + } - // Remover todos os participantes (exceto o criador, se necessário manter histórico) - // Por enquanto, vamos apenas limpar a lista de participantes - await ctx.db.patch(args.conversaId, { - participantes: [], - administradores: undefined, - }); + // Remover todos os participantes (exceto o criador, se necessário manter histórico) + // Por enquanto, vamos apenas limpar a lista de participantes + await ctx.db.patch(args.conversaId, { + participantes: [], + administradores: undefined + }); - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Envia uma notificação para todos os participantes de uma sala de reunião (apenas administradores) */ export const enviarNotificacaoReuniao = mutation({ - args: { - conversaId: v.id("conversas"), - titulo: v.string(), - mensagem: v.string(), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return { sucesso: false, erro: "Não autenticado" }; - } + args: { + conversaId: v.id('conversas'), + titulo: v.string(), + mensagem: v.string() + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, erro: 'Não autenticado' }; + } - const conversa = await ctx.db.get(args.conversaId); - if (!conversa) { - return { sucesso: false, erro: "Sala de reunião não encontrada" }; - } + const conversa = await ctx.db.get(args.conversaId); + if (!conversa) { + return { sucesso: false, erro: 'Sala de reunião não encontrada' }; + } - // Verificar se é sala de reunião - if (conversa.tipo !== "sala_reuniao") { - return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" }; - } + // Verificar se é sala de reunião + if (conversa.tipo !== 'sala_reuniao') { + return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' }; + } - // Verificar se usuário é administrador - const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - if (!isAdmin) { - return { sucesso: false, erro: "Apenas administradores podem enviar notificações" }; - } + // Verificar se usuário é administrador + const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + if (!isAdmin) { + return { sucesso: false, erro: 'Apenas administradores podem enviar notificações' }; + } - // Criar notificação para todos os participantes - for (const participanteId of conversa.participantes) { - const tituloNotificacao = args.titulo || "Notificação da sala de reunião"; - const descricaoNotificacao = args.mensagem.substring(0, 100); // Limitar descrição para push - - // Criar notificação no banco - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: "nova_mensagem", - conversaId: args.conversaId, - remetenteId: usuarioAtual._id, - titulo: tituloNotificacao, - descricao: args.mensagem, - lida: false, - criadaEm: Date.now(), - }); + // Criar notificação para todos os participantes + for (const participanteId of conversa.participantes) { + const tituloNotificacao = args.titulo || 'Notificação da sala de reunião'; + const descricaoNotificacao = args.mensagem.substring(0, 100); // Limitar descrição para push - // Enviar push notification (assíncrono, não bloqueia) - ctx.scheduler.runAfter(0, internal.pushNotifications.enviarPushNotification, { - usuarioId: participanteId, - titulo: tituloNotificacao, - corpo: descricaoNotificacao, - data: { - conversaId: args.conversaId, - tipo: "notificacao_reuniao", - }, - }).catch((error) => { - console.error(`Erro ao agendar push para usuário ${participanteId}:`, error); - }); - } + // Criar notificação no banco + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: 'nova_mensagem', + conversaId: args.conversaId, + remetenteId: usuarioAtual._id, + titulo: tituloNotificacao, + descricao: args.mensagem, + lida: false, + criadaEm: Date.now() + }); - return { sucesso: true }; - }, + // Enviar push notification (assíncrono, não bloqueia) + ctx.scheduler + .runAfter(0, internal.pushNotifications.enviarPushNotification, { + usuarioId: participanteId, + titulo: tituloNotificacao, + corpo: descricaoNotificacao, + data: { + conversaId: args.conversaId, + tipo: 'notificacao_reuniao' + } + }) + .catch((error) => { + console.error(`Erro ao agendar push para usuário ${participanteId}:`, error); + }); + } + + return { sucesso: true }; + } }); // ========== QUERIES ========== @@ -1603,16 +1580,16 @@ export const enviarNotificacaoReuniao = mutation({ * Verifica se o usuário atual é administrador de uma sala de reunião */ export const verificarSeEhAdmin = query({ - args: { - conversaId: v.id("conversas"), - }, - returns: v.boolean(), - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return false; + args: { + conversaId: v.id('conversas') + }, + returns: v.boolean(), + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return false; - return await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); - }, + return await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id); + } }); /** @@ -1620,129 +1597,120 @@ export const verificarSeEhAdmin = query({ * SEGURANÇA: Usuário só vê conversas onde é participante */ export const listarConversas = query({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - // Buscar todas as conversas do usuário (SEGURANÇA: filtrar por participante) - const todasConversas = await ctx.db.query("conversas").collect(); - const conversasDoUsuario = todasConversas.filter((c) => - c.participantes.includes(usuarioAtual._id) - ); + // Buscar todas as conversas do usuário (SEGURANÇA: filtrar por participante) + const todasConversas = await ctx.db.query('conversas').collect(); + const conversasDoUsuario = todasConversas.filter((c) => + c.participantes.includes(usuarioAtual._id) + ); - // Ordenar por última mensagem - conversasDoUsuario.sort((a, b) => { - const timestampA = a.ultimaMensagemTimestamp || a.criadoEm; - const timestampB = b.ultimaMensagemTimestamp || b.criadoEm; - return timestampB - timestampA; - }); + // Ordenar por última mensagem + conversasDoUsuario.sort((a, b) => { + const timestampA = a.ultimaMensagemTimestamp || a.criadoEm; + const timestampB = b.ultimaMensagemTimestamp || b.criadoEm; + return timestampB - timestampA; + }); - // Enriquecer com informações dos participantes - const conversasEnriquecidas = await Promise.all( - conversasDoUsuario.map(async (conversa) => { - // Buscar participantes - const participantes = await Promise.all( - conversa.participantes.map((id) => ctx.db.get(id)) - ); + // Enriquecer com informações dos participantes + const conversasEnriquecidas = await Promise.all( + conversasDoUsuario.map(async (conversa) => { + // Buscar participantes + const participantes = await Promise.all(conversa.participantes.map((id) => ctx.db.get(id))); - // Para conversas individuais, pegar o outro usuário - let outroUsuario = null; - if (conversa.tipo === "individual") { - const outroUsuarioRaw = participantes.find( - (p) => p?._id !== usuarioAtual._id - ); - if (outroUsuarioRaw) { - // 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot) - const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id); + // Para conversas individuais, pegar o outro usuário + let outroUsuario = null; + if (conversa.tipo === 'individual') { + const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id); + if (outroUsuarioRaw) { + // 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot) + const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id); - if (usuarioAtualizado) { - // Adicionar URL da foto de perfil - let fotoPerfilUrl = null; - if (usuarioAtualizado.fotoPerfil) { - fotoPerfilUrl = await ctx.storage.getUrl( - usuarioAtualizado.fotoPerfil - ); - } - outroUsuario = { - ...usuarioAtualizado, - fotoPerfilUrl, - }; - } - } - } + if (usuarioAtualizado) { + // Adicionar URL da foto de perfil + let fotoPerfilUrl = null; + if (usuarioAtualizado.fotoPerfil) { + fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil); + } + outroUsuario = { + ...usuarioAtualizado, + fotoPerfilUrl + }; + } + } + } - // Contar mensagens não lidas (apenas mensagens NÃO agendadas) - const leitura = await ctx.db - .query("leituras") - .withIndex("by_conversa_usuario", (q) => - q.eq("conversaId", conversa._id).eq("usuarioId", usuarioAtual._id) - ) - .first(); + // Contar mensagens não lidas (apenas mensagens NÃO agendadas) + const leitura = await ctx.db + .query('leituras') + .withIndex('by_conversa_usuario', (q) => + q.eq('conversaId', conversa._id).eq('usuarioId', usuarioAtual._id) + ) + .first(); - // CORRIGIDO: Buscar apenas mensagens NÃO agendadas (agendadaPara === undefined) - // SEGURANÇA: Filtrar apenas mensagens de participantes da conversa - const todasMensagens = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", conversa._id)) - .collect(); + // CORRIGIDO: Buscar apenas mensagens NÃO agendadas (agendadaPara === undefined) + // SEGURANÇA: Filtrar apenas mensagens de participantes da conversa + const todasMensagens = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', conversa._id)) + .collect(); - // Filtrar mensagens agendadas e garantir que remetente é participante - const mensagens = todasMensagens.filter((m) => { - if (m.agendadaPara) return false; - // Garantir que o remetente é participante da conversa - return conversa.participantes.includes(m.remetenteId); - }); + // Filtrar mensagens agendadas e garantir que remetente é participante + const mensagens = todasMensagens.filter((m) => { + if (m.agendadaPara) return false; + // Garantir que o remetente é participante da conversa + return conversa.participantes.includes(m.remetenteId); + }); - let naoLidas = 0; - if (leitura) { - naoLidas = mensagens.filter( - (m) => - m.enviadaEm > (leitura.lidaEm || 0) && - m.remetenteId !== usuarioAtual._id - ).length; - } else { - naoLidas = mensagens.filter( - (m) => m.remetenteId !== usuarioAtual._id - ).length; - } + let naoLidas = 0; + if (leitura) { + naoLidas = mensagens.filter( + (m) => m.enviadaEm > (leitura.lidaEm || 0) && m.remetenteId !== usuarioAtual._id + ).length; + } else { + naoLidas = mensagens.filter((m) => m.remetenteId !== usuarioAtual._id).length; + } - // Verificar se usuário é administrador (apenas para salas de reunião) - const isAdmin = conversa.tipo === "sala_reuniao" - ? await verificarPermissaoAdmin(ctx, conversa._id, usuarioAtual._id) - : false; + // Verificar se usuário é administrador (apenas para salas de reunião) + const isAdmin = + conversa.tipo === 'sala_reuniao' + ? await verificarPermissaoAdmin(ctx, conversa._id, usuarioAtual._id) + : false; - // Enriquecer participantes com fotoPerfilUrl (para grupos e salas) - const participantesInfo = await Promise.all( - participantes - .filter((p) => p !== null) - .map(async (participante) => { - if (!participante) return null; - - let fotoPerfilUrl = null; - if (participante.fotoPerfil) { - fotoPerfilUrl = await ctx.storage.getUrl(participante.fotoPerfil); - } - - return { - ...participante, - fotoPerfilUrl, - }; - }) - ); + // Enriquecer participantes com fotoPerfilUrl (para grupos e salas) + const participantesInfo = await Promise.all( + participantes + .filter((p) => p !== null) + .map(async (participante) => { + if (!participante) return null; - return { - ...conversa, - outroUsuario, - participantesInfo: participantesInfo.filter((p) => p !== null), - naoLidas, - isAdmin, // Adicionar flag de admin - }; - }) - ); + let fotoPerfilUrl = null; + if (participante.fotoPerfil) { + fotoPerfilUrl = await ctx.storage.getUrl(participante.fotoPerfil); + } - return conversasEnriquecidas; - }, + return { + ...participante, + fotoPerfilUrl + }; + }) + ); + + return { + ...conversa, + outroUsuario, + participantesInfo: participantesInfo.filter((p) => p !== null), + naoLidas, + isAdmin // Adicionar flag de admin + }; + }) + ); + + return conversasEnriquecidas; + } }); /** @@ -1750,86 +1718,88 @@ export const listarConversas = query({ * SEGURANÇA: Usuário só vê mensagens de conversas onde é participante */ export const obterMensagens = query({ - args: { - conversaId: v.id("conversas"), - limit: v.optional(v.number()), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: { + conversaId: v.id('conversas'), + limit: v.optional(v.number()) + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - // Verificar se usuário pertence à conversa (SEGURANÇA CRÍTICA) - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return []; - } + // Verificar se usuário pertence à conversa (SEGURANÇA CRÍTICA) + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return []; + } - // Buscar mensagens (excluir agendadas) - const mensagens = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) - .order("desc") - .take(args.limit || 50); + // Buscar mensagens (excluir agendadas) + const mensagens = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId)) + .order('desc') + .take(args.limit || 50); - // Filtrar mensagens agendadas e garantir que são da conversa correta - // SEGURANÇA: Apenas mensagens de participantes da conversa são retornadas - const mensagensFiltradas = mensagens.filter((m) => { - // Excluir agendadas - if (m.agendadaPara) return false; - - // Garantir que a mensagem pertence à conversa correta (segurança adicional) - if (m.conversaId !== args.conversaId) return false; - - // SEGURANÇA CRÍTICA: Garantir que o remetente é participante da conversa - // Isso garante que usuários só veem mensagens de conversas onde participam - return conversa.participantes.includes(m.remetenteId); - }); + // Filtrar mensagens agendadas e garantir que são da conversa correta + // SEGURANÇA: Apenas mensagens de participantes da conversa são retornadas + const mensagensFiltradas = mensagens.filter((m) => { + // Excluir agendadas + if (m.agendadaPara) return false; - // Enriquecer com informações do remetente e mensagem respondida - const mensagensEnriquecidas = await Promise.all( - mensagensFiltradas.map(async (mensagem) => { - const remetente = await ctx.db.get(mensagem.remetenteId); - - // SEGURANÇA: Não retornar informações de remetente se não for participante - if (!remetente || !conversa.participantes.includes(remetente._id)) { - return null; - } - - let arquivoUrl = null; - if (mensagem.arquivoId) { - arquivoUrl = await ctx.storage.getUrl(mensagem.arquivoId); - } + // Garantir que a mensagem pertence à conversa correta (segurança adicional) + if (m.conversaId !== args.conversaId) return false; - // Buscar mensagem original se for resposta - let mensagemOriginal = null; - if (mensagem.respostaPara) { - const original = await ctx.db.get(mensagem.respostaPara); - if (original && conversa.participantes.includes(original.remetenteId)) { - const remetenteOriginal = await ctx.db.get(original.remetenteId); - mensagemOriginal = { - _id: original._id, - conteudo: original.conteudo.substring(0, 100), // Limitar tamanho - remetente: remetenteOriginal ? { - _id: remetenteOriginal._id, - nome: remetenteOriginal.nome, - } : null, - deletada: original.deletada || false, - }; - } - } + // SEGURANÇA CRÍTICA: Garantir que o remetente é participante da conversa + // Isso garante que usuários só veem mensagens de conversas onde participam + return conversa.participantes.includes(m.remetenteId); + }); - return { - ...mensagem, - remetente, - arquivoUrl, - mensagemOriginal, - }; - }) - ); + // Enriquecer com informações do remetente e mensagem respondida + const mensagensEnriquecidas = await Promise.all( + mensagensFiltradas.map(async (mensagem) => { + const remetente = await ctx.db.get(mensagem.remetenteId); - // Filtrar nulls (caso alguma mensagem tenha sido rejeitada por segurança) - return mensagensEnriquecidas.filter((m) => m !== null).reverse(); - }, + // SEGURANÇA: Não retornar informações de remetente se não for participante + if (!remetente || !conversa.participantes.includes(remetente._id)) { + return null; + } + + let arquivoUrl = null; + if (mensagem.arquivoId) { + arquivoUrl = await ctx.storage.getUrl(mensagem.arquivoId); + } + + // Buscar mensagem original se for resposta + let mensagemOriginal = null; + if (mensagem.respostaPara) { + const original = await ctx.db.get(mensagem.respostaPara); + if (original && conversa.participantes.includes(original.remetenteId)) { + const remetenteOriginal = await ctx.db.get(original.remetenteId); + mensagemOriginal = { + _id: original._id, + conteudo: original.conteudo.substring(0, 100), // Limitar tamanho + remetente: remetenteOriginal + ? { + _id: remetenteOriginal._id, + nome: remetenteOriginal.nome + } + : null, + deletada: original.deletada || false + }; + } + } + + return { + ...mensagem, + remetente, + arquivoUrl, + mensagemOriginal + }; + }) + ); + + // Filtrar nulls (caso alguma mensagem tenha sido rejeitada por segurança) + return mensagensEnriquecidas.filter((m) => m !== null).reverse(); + } }); /** @@ -1837,38 +1807,36 @@ export const obterMensagens = query({ * SEGURANÇA: Usuário só vê suas próprias mensagens agendadas de conversas onde é participante */ export const obterMensagensAgendadas = query({ - args: { - conversaId: v.id("conversas"), - }, - handler: async (ctx, args): Promise[]> => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: { + conversaId: v.id('conversas') + }, + handler: async (ctx, args): Promise[]> => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - // SEGURANÇA: Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return []; - } + // SEGURANÇA: Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return []; + } - // Buscar mensagens agendadas - const todasMensagens = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) - .collect(); + // Buscar mensagens agendadas + const todasMensagens = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId)) + .collect(); - // Filtrar apenas as agendadas do usuário atual (SEGURANÇA: só suas próprias mensagens) - const minhasMensagensAgendadas = todasMensagens.filter( - (m) => - m.remetenteId === usuarioAtual._id && - m.agendadaPara !== undefined && - m.agendadaPara > Date.now() && - m.conversaId === args.conversaId // Garantir que pertence à conversa correta - ); + // Filtrar apenas as agendadas do usuário atual (SEGURANÇA: só suas próprias mensagens) + const minhasMensagensAgendadas = todasMensagens.filter( + (m) => + m.remetenteId === usuarioAtual._id && + m.agendadaPara !== undefined && + m.agendadaPara > Date.now() && + m.conversaId === args.conversaId // Garantir que pertence à conversa correta + ); - return minhasMensagensAgendadas.sort( - (a, b) => (a.agendadaPara ?? 0) - (b.agendadaPara ?? 0) - ); - }, + return minhasMensagensAgendadas.sort((a, b) => (a.agendadaPara ?? 0) - (b.agendadaPara ?? 0)); + } }); /** @@ -1876,77 +1844,75 @@ export const obterMensagensAgendadas = query({ * SEGURANÇA: Usuário só vê suas próprias mensagens agendadas de conversas onde ainda é participante */ export const listarAgendamentosChat = query({ - args: {}, - handler: async ( - ctx - ): Promise< - Array< - Doc<"mensagens"> & { - conversaInfo: Doc<"conversas"> | null; - destinatarioInfo: Doc<"usuarios"> | null; - } - > - > => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) { - return []; - } + args: {}, + handler: async ( + ctx + ): Promise< + Array< + Doc<'mensagens'> & { + conversaInfo: Doc<'conversas'> | null; + destinatarioInfo: Doc<'usuarios'> | null; + } + > + > => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return []; + } - // Buscar todas as mensagens agendadas do usuário - const todasMensagens = await ctx.db - .query("mensagens") - .withIndex("by_remetente", (q) => q.eq("remetenteId", usuarioAtual._id)) - .collect(); + // Buscar todas as mensagens agendadas do usuário + const todasMensagens = await ctx.db + .query('mensagens') + .withIndex('by_remetente', (q) => q.eq('remetenteId', usuarioAtual._id)) + .collect(); - // Filtrar apenas as que têm agendamento (passadas ou futuras) - const mensagensAgendadas = todasMensagens.filter( - (m) => m.agendadaPara !== undefined - ); + // Filtrar apenas as que têm agendamento (passadas ou futuras) + const mensagensAgendadas = todasMensagens.filter((m) => m.agendadaPara !== undefined); - // Enriquecer com informações da conversa e destinatário - const mensagensEnriquecidas = await Promise.all( - mensagensAgendadas.map(async (mensagem) => { - const conversaInfo = await ctx.db.get(mensagem.conversaId); + // Enriquecer com informações da conversa e destinatário + const mensagensEnriquecidas = await Promise.all( + mensagensAgendadas.map(async (mensagem) => { + const conversaInfo = await ctx.db.get(mensagem.conversaId); - // SEGURANÇA: Verificar se usuário ainda é participante da conversa - if (!conversaInfo || !conversaInfo.participantes.includes(usuarioAtual._id)) { - return null; // Usuário não é mais participante, não mostrar mensagem - } + // SEGURANÇA: Verificar se usuário ainda é participante da conversa + if (!conversaInfo || !conversaInfo.participantes.includes(usuarioAtual._id)) { + return null; // Usuário não é mais participante, não mostrar mensagem + } - // SEGURANÇA: Verificar se o remetente (que deve ser o usuário atual) é participante - if (!conversaInfo.participantes.includes(mensagem.remetenteId)) { - return null; // Remetente não é participante, mensagem inválida - } + // SEGURANÇA: Verificar se o remetente (que deve ser o usuário atual) é participante + if (!conversaInfo.participantes.includes(mensagem.remetenteId)) { + return null; // Remetente não é participante, mensagem inválida + } - let destinatarioInfo: Doc<"usuarios"> | null = null; + let destinatarioInfo: Doc<'usuarios'> | null = null; - // Se for conversa individual, encontrar o outro participante - if (conversaInfo.tipo === "individual") { - const outroParticipanteId = conversaInfo.participantes.find( - (p) => p !== usuarioAtual._id - ); - if (outroParticipanteId) { - destinatarioInfo = await ctx.db.get(outroParticipanteId); - } - } + // Se for conversa individual, encontrar o outro participante + if (conversaInfo.tipo === 'individual') { + const outroParticipanteId = conversaInfo.participantes.find( + (p) => p !== usuarioAtual._id + ); + if (outroParticipanteId) { + destinatarioInfo = await ctx.db.get(outroParticipanteId); + } + } - return { - ...mensagem, - conversaInfo, - destinatarioInfo, - }; - }) - ); + return { + ...mensagem, + conversaInfo, + destinatarioInfo + }; + }) + ); - // Filtrar nulls e ordenar por data de agendamento (mais próximos primeiro) - return mensagensEnriquecidas - .filter((m): m is NonNullable => m !== null) - .sort((a, b) => { - const dataA = a.agendadaPara ?? 0; - const dataB = b.agendadaPara ?? 0; - return dataA - dataB; - }); - }, + // Filtrar nulls e ordenar por data de agendamento (mais próximos primeiro) + return mensagensEnriquecidas + .filter((m): m is NonNullable => m !== null) + .sort((a, b) => { + const dataA = a.agendadaPara ?? 0; + const dataB = b.agendadaPara ?? 0; + return dataA - dataB; + }); + } }); /** @@ -1954,154 +1920,153 @@ export const listarAgendamentosChat = query({ * SEGURANÇA: Usuário só vê notificações de conversas onde ainda é participante */ export const obterNotificacoes = query({ - args: { - apenasPendentes: v.optional(v.boolean()), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: { + apenasPendentes: v.optional(v.boolean()) + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - let query = ctx.db - .query("notificacoes") - .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioAtual._id)); + let query = ctx.db + .query('notificacoes') + .withIndex('by_usuario', (q) => q.eq('usuarioId', usuarioAtual._id)); - if (args.apenasPendentes) { - query = ctx.db - .query("notificacoes") - .withIndex("by_usuario_lida", (q) => - q.eq("usuarioId", usuarioAtual._id).eq("lida", false) - ); - } + if (args.apenasPendentes) { + query = ctx.db + .query('notificacoes') + .withIndex('by_usuario_lida', (q) => q.eq('usuarioId', usuarioAtual._id).eq('lida', false)); + } - const notificacoes = await query.order("desc").take(50); + const notificacoes = await query.order('desc').take(50); - // Enriquecer com informações do remetente e validar acesso - const notificacoesEnriquecidas = await Promise.all( - notificacoes.map(async (notificacao) => { - // SEGURANÇA: Se a notificação tem conversaId, verificar se usuário ainda é participante - if (notificacao.conversaId) { - const conversa = await ctx.db.get(notificacao.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return null; // Usuário não é mais participante, não mostrar notificação - } + // Enriquecer com informações do remetente e validar acesso + const notificacoesEnriquecidas = await Promise.all( + notificacoes.map(async (notificacao) => { + // SEGURANÇA: Se a notificação tem conversaId, verificar se usuário ainda é participante + if (notificacao.conversaId) { + const conversa = await ctx.db.get(notificacao.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return null; // Usuário não é mais participante, não mostrar notificação + } - // SEGURANÇA: Se tem remetenteId, verificar se é participante da conversa - if (notificacao.remetenteId && !conversa.participantes.includes(notificacao.remetenteId)) { - return null; // Remetente não é participante, notificação inválida - } - } + // SEGURANÇA: Se tem remetenteId, verificar se é participante da conversa + if ( + notificacao.remetenteId && + !conversa.participantes.includes(notificacao.remetenteId) + ) { + return null; // Remetente não é participante, notificação inválida + } + } - let remetente = null; - if (notificacao.remetenteId) { - remetente = await ctx.db.get(notificacao.remetenteId); - } - return { - ...notificacao, - remetente, - }; - }) - ); + let remetente = null; + if (notificacao.remetenteId) { + remetente = await ctx.db.get(notificacao.remetenteId); + } + return { + ...notificacao, + remetente + }; + }) + ); - // Filtrar nulls antes de retornar - return notificacoesEnriquecidas.filter((n): n is NonNullable => n !== null); - }, + // Filtrar nulls antes de retornar + return notificacoesEnriquecidas.filter((n): n is NonNullable => n !== null); + } }); /** * Conta o número de notificações não lidas */ export const contarNotificacoesNaoLidas = query({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return 0; + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return 0; - const notificacoes = await ctx.db - .query("notificacoes") - .withIndex("by_usuario_lida", (q) => - q.eq("usuarioId", usuarioAtual._id).eq("lida", false) - ) - .collect(); + const notificacoes = await ctx.db + .query('notificacoes') + .withIndex('by_usuario_lida', (q) => q.eq('usuarioId', usuarioAtual._id).eq('lida', false)) + .collect(); - return notificacoes.length; - }, + return notificacoes.length; + } }); /** * Obtém usuários online */ export const obterUsuariosOnline = query({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - const usuarios = await ctx.db - .query("usuarios") - .withIndex("by_status_presenca", (q) => q.eq("statusPresenca", "online")) - .collect(); + const usuarios = await ctx.db + .query('usuarios') + .withIndex('by_status_presenca', (q) => q.eq('statusPresenca', 'online')) + .collect(); - return usuarios.map((u) => ({ - _id: u._id, - nome: u.nome, - email: u.email, - avatar: u.avatar, - fotoPerfil: u.fotoPerfil, - statusPresenca: u.statusPresenca, - statusMensagem: u.statusMensagem, - setor: u.setor, - })); - }, + return usuarios.map((u) => ({ + _id: u._id, + nome: u.nome, + email: u.email, + avatar: u.avatar, + fotoPerfil: u.fotoPerfil, + statusPresenca: u.statusPresenca, + statusMensagem: u.statusMensagem, + setor: u.setor + })); + } }); /** * Lista todos os usuários (para criar nova conversa) */ export const listarTodosUsuarios = query({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - const usuarios = await ctx.db - .query("usuarios") - .withIndex("by_ativo", (q) => q.eq("ativo", true)) - .collect(); + const usuarios = await ctx.db + .query('usuarios') + .withIndex('by_ativo', (q) => q.eq('ativo', true)) + .collect(); - // Excluir o usuário atual e buscar matrículas - const usuariosComMatricula = await Promise.all( - usuarios - .filter((u) => u._id !== usuarioAtual._id) - .map(async (u) => { - let matricula: string | undefined = undefined; - if (u.funcionarioId) { - const funcionario = await ctx.db.get(u.funcionarioId); - matricula = funcionario?.matricula; - } - - // Buscar URL da foto de perfil se existir - let fotoPerfilUrl: string | null = null; - if (u.fotoPerfil) { - fotoPerfilUrl = await ctx.storage.getUrl(u.fotoPerfil); - } - - return { - _id: u._id, - nome: u.nome, - email: u.email, - matricula, - avatar: u.avatar, - fotoPerfil: u.fotoPerfil, - fotoPerfilUrl, - statusPresenca: u.statusPresenca, - statusMensagem: u.statusMensagem, - setor: u.setor, - }; - }) - ); + // Excluir o usuário atual e buscar matrículas + const usuariosComMatricula = await Promise.all( + usuarios + .filter((u) => u._id !== usuarioAtual._id) + .map(async (u) => { + let matricula: string | undefined = undefined; + if (u.funcionarioId) { + const funcionario = await ctx.db.get(u.funcionarioId); + matricula = funcionario?.matricula; + } - return usuariosComMatricula; - }, + // Buscar URL da foto de perfil se existir + let fotoPerfilUrl: string | null = null; + if (u.fotoPerfil) { + fotoPerfilUrl = await ctx.storage.getUrl(u.fotoPerfil); + } + + return { + _id: u._id, + nome: u.nome, + email: u.email, + matricula, + avatar: u.avatar, + fotoPerfil: u.fotoPerfil, + fotoPerfilUrl, + statusPresenca: u.statusPresenca, + statusMensagem: u.statusMensagem, + setor: u.setor + }; + }) + ); + + return usuariosComMatricula; + } }); /** @@ -2109,166 +2074,167 @@ export const listarTodosUsuarios = query({ * SEGURANÇA: Usuário só vê mensagens de conversas onde é participante e onde o remetente também é participante */ export const buscarMensagens = query({ - args: { - query: v.string(), - conversaId: v.optional(v.id("conversas")), - remetenteId: v.optional(v.id("usuarios")), - tipo: v.optional(v.union(v.literal("texto"), v.literal("arquivo"), v.literal("imagem"))), - dataInicio: v.optional(v.number()), - dataFim: v.optional(v.number()), - limite: v.optional(v.number()), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: { + query: v.string(), + conversaId: v.optional(v.id('conversas')), + remetenteId: v.optional(v.id('usuarios')), + tipo: v.optional(v.union(v.literal('texto'), v.literal('arquivo'), v.literal('imagem'))), + dataInicio: v.optional(v.number()), + dataFim: v.optional(v.number()), + limite: v.optional(v.number()) + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - // Normalizar query para busca - const queryNormalizada = normalizarTextoParaBusca(args.query); + // Normalizar query para busca + const queryNormalizada = normalizarTextoParaBusca(args.query); - // Buscar em todas as conversas do usuário - const todasConversas = await ctx.db.query("conversas").collect(); - const conversasDoUsuario = todasConversas.filter((c) => - c.participantes.includes(usuarioAtual._id) - ); + // Buscar em todas as conversas do usuário + const todasConversas = await ctx.db.query('conversas').collect(); + const conversasDoUsuario = todasConversas.filter((c) => + c.participantes.includes(usuarioAtual._id) + ); - // SEGURANÇA: Se filtrar por remetente, verificar se ele é participante de alguma conversa do usuário - if (args.remetenteId) { - const remetenteEParticipante = conversasDoUsuario.some(c => - c.participantes.includes(args.remetenteId!) - ); - if (!remetenteEParticipante) { - return []; // Remetente não é participante de nenhuma conversa do usuário - } - } + // SEGURANÇA: Se filtrar por remetente, verificar se ele é participante de alguma conversa do usuário + if (args.remetenteId) { + const remetenteEParticipante = conversasDoUsuario.some((c) => + c.participantes.includes(args.remetenteId!) + ); + if (!remetenteEParticipante) { + return []; // Remetente não é participante de nenhuma conversa do usuário + } + } - let mensagens: Doc<"mensagens">[] = []; + let mensagens: Doc<'mensagens'>[] = []; - if (args.conversaId !== undefined) { - // Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return []; - } + if (args.conversaId !== undefined) { + // Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return []; + } - // Buscar em conversa específica - const mensagensConversa = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId!)) - .collect(); - - // SEGURANÇA: Filtrar apenas mensagens cujo remetente é participante desta conversa específica - mensagens = mensagensConversa.filter(m => - conversa.participantes.includes(m.remetenteId) - ); - } else { - // Buscar em todas as conversas do usuário - for (const conversa of conversasDoUsuario) { - const mensagensConversa = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", conversa._id)) - .collect(); - - // SEGURANÇA: Filtrar apenas mensagens cujo remetente é participante desta conversa específica - const mensagensValidas = mensagensConversa.filter(m => - conversa.participantes.includes(m.remetenteId) - ); - mensagens.push(...mensagensValidas); - } - } + // Buscar em conversa específica + const mensagensConversa = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId!)) + .collect(); - // Aplicar filtros - let mensagensFiltradas = mensagens.filter((m) => { - // Excluir deletadas e agendadas - if (m.deletada || m.agendadaPara) { - return false; - } + // SEGURANÇA: Filtrar apenas mensagens cujo remetente é participante desta conversa específica + mensagens = mensagensConversa.filter((m) => conversa.participantes.includes(m.remetenteId)); + } else { + // Buscar em todas as conversas do usuário + for (const conversa of conversasDoUsuario) { + const mensagensConversa = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', conversa._id)) + .collect(); - // SEGURANÇA CRÍTICA: Garantir que a mensagem pertence a uma conversa do usuário - // e que o remetente é participante dessa conversa específica - const conversaDaMensagem = conversasDoUsuario.find(c => c._id === m.conversaId); - if (!conversaDaMensagem) { - return false; - } - - // SEGURANÇA CRÍTICA: Garantir que o remetente é participante da conversa específica da mensagem - if (!conversaDaMensagem.participantes.includes(m.remetenteId)) { - return false; - } + // SEGURANÇA: Filtrar apenas mensagens cujo remetente é participante desta conversa específica + const mensagensValidas = mensagensConversa.filter((m) => + conversa.participantes.includes(m.remetenteId) + ); + mensagens.push(...mensagensValidas); + } + } - // Filtrar por query (busca no conteúdo normalizado) - if (queryNormalizada && queryNormalizada.length > 0) { - const conteudoBusca = m.conteudoBusca || normalizarTextoParaBusca(m.conteudo); - if (!conteudoBusca.includes(queryNormalizada)) { - return false; - } - } + // Aplicar filtros + let mensagensFiltradas = mensagens.filter((m) => { + // Excluir deletadas e agendadas + if (m.deletada || m.agendadaPara) { + return false; + } - // Filtrar por remetente (já verificado acima, mas garantir novamente) - if (args.remetenteId) { - if (m.remetenteId !== args.remetenteId) { - return false; - } - // Verificar novamente se o remetente é participante da conversa específica desta mensagem - if (!conversaDaMensagem.participantes.includes(args.remetenteId)) { - return false; - } - } + // SEGURANÇA CRÍTICA: Garantir que a mensagem pertence a uma conversa do usuário + // e que o remetente é participante dessa conversa específica + const conversaDaMensagem = conversasDoUsuario.find((c) => c._id === m.conversaId); + if (!conversaDaMensagem) { + return false; + } - // Filtrar por tipo - if (args.tipo && m.tipo !== args.tipo) { - return false; - } + // SEGURANÇA CRÍTICA: Garantir que o remetente é participante da conversa específica da mensagem + if (!conversaDaMensagem.participantes.includes(m.remetenteId)) { + return false; + } - // Filtrar por data - if (args.dataInicio && m.enviadaEm < args.dataInicio) { - return false; - } - if (args.dataFim && m.enviadaEm > args.dataFim) { - return false; - } + // Filtrar por query (busca no conteúdo normalizado) + if (queryNormalizada && queryNormalizada.length > 0) { + const conteudoBusca = m.conteudoBusca || normalizarTextoParaBusca(m.conteudo); + if (!conteudoBusca.includes(queryNormalizada)) { + return false; + } + } - return true; - }); + // Filtrar por remetente (já verificado acima, mas garantir novamente) + if (args.remetenteId) { + if (m.remetenteId !== args.remetenteId) { + return false; + } + // Verificar novamente se o remetente é participante da conversa específica desta mensagem + if (!conversaDaMensagem.participantes.includes(args.remetenteId)) { + return false; + } + } - // Ordenar por data (mais recentes primeiro) - mensagensFiltradas.sort((a, b) => b.enviadaEm - a.enviadaEm); + // Filtrar por tipo + if (args.tipo && m.tipo !== args.tipo) { + return false; + } - // Limitar resultados - if (args.limite) { - mensagensFiltradas = mensagensFiltradas.slice(0, args.limite); - } + // Filtrar por data + if (args.dataInicio && m.enviadaEm < args.dataInicio) { + return false; + } + if (args.dataFim && m.enviadaEm > args.dataFim) { + return false; + } - // Enriquecer com informações (apenas para mensagens válidas) - const mensagensEnriquecidas = await Promise.all( - mensagensFiltradas.map(async (mensagem) => { - const conversaDaMensagem = conversasDoUsuario.find(c => c._id === mensagem.conversaId); - - // SEGURANÇA: Validar novamente antes de enriquecer - if (!conversaDaMensagem || !conversaDaMensagem.participantes.includes(mensagem.remetenteId)) { - return null; - } + return true; + }); - const remetente = await ctx.db.get(mensagem.remetenteId); - - // SEGURANÇA: Só retornar se remetente for participante - if (!remetente || !conversaDaMensagem.participantes.includes(remetente._id)) { - return null; - } + // Ordenar por data (mais recentes primeiro) + mensagensFiltradas.sort((a, b) => b.enviadaEm - a.enviadaEm); - return { - ...mensagem, - remetente, - conversa: conversaDaMensagem, - }; - }) - ); + // Limitar resultados + if (args.limite) { + mensagensFiltradas = mensagensFiltradas.slice(0, args.limite); + } - // Filtrar nulls antes de retornar - return mensagensEnriquecidas - .filter((m): m is NonNullable => m !== null) - .sort((a, b) => b.enviadaEm - a.enviadaEm) - .slice(0, 50); - }, + // Enriquecer com informações (apenas para mensagens válidas) + const mensagensEnriquecidas = await Promise.all( + mensagensFiltradas.map(async (mensagem) => { + const conversaDaMensagem = conversasDoUsuario.find((c) => c._id === mensagem.conversaId); + + // SEGURANÇA: Validar novamente antes de enriquecer + if ( + !conversaDaMensagem || + !conversaDaMensagem.participantes.includes(mensagem.remetenteId) + ) { + return null; + } + + const remetente = await ctx.db.get(mensagem.remetenteId); + + // SEGURANÇA: Só retornar se remetente for participante + if (!remetente || !conversaDaMensagem.participantes.includes(remetente._id)) { + return null; + } + + return { + ...mensagem, + remetente, + conversa: conversaDaMensagem + }; + }) + ); + + // Filtrar nulls antes de retornar + return mensagensEnriquecidas + .filter((m): m is NonNullable => m !== null) + .sort((a, b) => b.enviadaEm - a.enviadaEm) + .slice(0, 50); + } }); /** @@ -2276,49 +2242,47 @@ export const buscarMensagens = query({ * SEGURANÇA: Usuário só vê digitação de conversas onde é participante */ export const obterDigitando = query({ - args: { - conversaId: v.id("conversas"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return []; + args: { + conversaId: v.id('conversas') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return []; - // SEGURANÇA: Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return []; - } + // SEGURANÇA: Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return []; + } - // Buscar indicadores de digitação (últimos 10 segundos) - const dezSegundosAtras = Date.now() - 10000; - const digitando = await ctx.db - .query("digitando") - .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) - .filter((q) => q.gte(q.field("iniciouEm"), dezSegundosAtras)) - .collect(); + // Buscar indicadores de digitação (últimos 10 segundos) + const dezSegundosAtras = Date.now() - 10000; + const digitando = await ctx.db + .query('digitando') + .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId)) + .filter((q) => q.gte(q.field('iniciouEm'), dezSegundosAtras)) + .collect(); - // Filtrar usuário atual e garantir que são participantes da conversa - const digitandoFiltrado = digitando.filter( - (d) => { - if (d.usuarioId === usuarioAtual._id) return false; - // Garantir que o usuário digitando é participante da conversa - return conversa.participantes.includes(d.usuarioId); - } - ); + // Filtrar usuário atual e garantir que são participantes da conversa + const digitandoFiltrado = digitando.filter((d) => { + if (d.usuarioId === usuarioAtual._id) return false; + // Garantir que o usuário digitando é participante da conversa + return conversa.participantes.includes(d.usuarioId); + }); - const usuarios = await Promise.all( - digitandoFiltrado.map(async (d) => { - const usuario = await ctx.db.get(d.usuarioId); - // SEGURANÇA: Só retornar se for participante - if (!usuario || !conversa.participantes.includes(usuario._id)) { - return null; - } - return usuario; - }) - ); + const usuarios = await Promise.all( + digitandoFiltrado.map(async (d) => { + const usuario = await ctx.db.get(d.usuarioId); + // SEGURANÇA: Só retornar se for participante + if (!usuario || !conversa.participantes.includes(usuario._id)) { + return null; + } + return usuario; + }) + ); - return usuarios.filter((u) => u !== null); - }, + return usuarios.filter((u) => u !== null); + } }); /** @@ -2326,47 +2290,43 @@ export const obterDigitando = query({ * SEGURANÇA: Usuário só conta mensagens de conversas onde é participante */ export const contarNaoLidas = query({ - args: { - conversaId: v.id("conversas"), - }, - handler: async (ctx, args) => { - const usuarioAtual = await getUsuarioAutenticado(ctx); - if (!usuarioAtual) return 0; + args: { + conversaId: v.id('conversas') + }, + handler: async (ctx, args) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) return 0; - // SEGURANÇA: Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { - return 0; - } + // SEGURANÇA: Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) { + return 0; + } - const leitura = await ctx.db - .query("leituras") - .withIndex("by_conversa_usuario", (q) => - q.eq("conversaId", args.conversaId).eq("usuarioId", usuarioAtual._id) - ) - .first(); + const leitura = await ctx.db + .query('leituras') + .withIndex('by_conversa_usuario', (q) => + q.eq('conversaId', args.conversaId).eq('usuarioId', usuarioAtual._id) + ) + .first(); - const todasMensagens = await ctx.db - .query("mensagens") - .withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId)) - .filter((q) => q.eq(q.field("agendadaPara"), undefined)) - .collect(); + const todasMensagens = await ctx.db + .query('mensagens') + .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId)) + .filter((q) => q.eq(q.field('agendadaPara'), undefined)) + .collect(); - // SEGURANÇA: Filtrar apenas mensagens de participantes da conversa - const mensagens = todasMensagens.filter((m) => - conversa.participantes.includes(m.remetenteId) - ); + // SEGURANÇA: Filtrar apenas mensagens de participantes da conversa + const mensagens = todasMensagens.filter((m) => conversa.participantes.includes(m.remetenteId)); - if (leitura) { - return mensagens.filter( - (m) => - m.enviadaEm > (leitura.lidaEm || 0) && - m.remetenteId !== usuarioAtual._id - ).length; - } + if (leitura) { + return mensagens.filter( + (m) => m.enviadaEm > (leitura.lidaEm || 0) && m.remetenteId !== usuarioAtual._id + ).length; + } - return mensagens.filter((m) => m.remetenteId !== usuarioAtual._id).length; - }, + return mensagens.filter((m) => m.remetenteId !== usuarioAtual._id).length; + } }); // ========== INTERNAL MUTATIONS (para crons) ========== @@ -2375,104 +2335,108 @@ export const contarNaoLidas = query({ * Envia mensagens agendadas (chamado pelo cron) */ export const enviarMensagensAgendadas = internalMutation({ - args: {}, - handler: async (ctx) => { - const agora = Date.now(); + args: {}, + handler: async (ctx) => { + const agora = Date.now(); - // Buscar mensagens que deveriam ser enviadas - // Como o índice by_agendamento indexa por agendadaPara, podemos usar range query - // Buscar mensagens com agendadaPara entre 0 e agora (mensagens agendadas que já devem ser enviadas) - // Valores undefined não aparecem no índice, então só buscamos mensagens realmente agendadas - const mensagensAgendadas = await ctx.db - .query("mensagens") - .withIndex("by_agendamento", (q) => q.gte("agendadaPara", 0).lte("agendadaPara", agora)) - .collect(); + // Buscar mensagens que deveriam ser enviadas + // Como o índice by_agendamento indexa por agendadaPara, podemos usar range query + // Buscar mensagens com agendadaPara entre 0 e agora (mensagens agendadas que já devem ser enviadas) + // Valores undefined não aparecem no índice, então só buscamos mensagens realmente agendadas + const mensagensAgendadas = await ctx.db + .query('mensagens') + .withIndex('by_agendamento', (q) => q.gte('agendadaPara', 0).lte('agendadaPara', agora)) + .collect(); - for (const mensagem of mensagensAgendadas) { - // Normalizar conteúdo para busca (se ainda não foi feito) - const conteudoBusca = mensagem.conteudoBusca || normalizarTextoParaBusca(mensagem.conteudo); - - // Atualizar mensagem para "enviada" - await ctx.db.patch(mensagem._id, { - agendadaPara: undefined, - enviadaEm: agora, - conteudoBusca: conteudoBusca, // Garantir que tem conteúdo de busca - }); + for (const mensagem of mensagensAgendadas) { + // Normalizar conteúdo para busca (se ainda não foi feito) + const conteudoBusca = mensagem.conteudoBusca || normalizarTextoParaBusca(mensagem.conteudo); - // Atualizar última mensagem da conversa - const conversa = await ctx.db.get(mensagem.conversaId); - if (conversa) { - await ctx.db.patch(mensagem.conversaId, { - ultimaMensagem: mensagem.conteudo.substring(0, 100), - ultimaMensagemTimestamp: agora, - ultimaMensagemRemetenteId: mensagem.remetenteId, // Guardar ID do remetente - }); + // Atualizar mensagem para "enviada" + await ctx.db.patch(mensagem._id, { + agendadaPara: undefined, + enviadaEm: agora, + conteudoBusca: conteudoBusca // Garantir que tem conteúdo de busca + }); - // Criar notificações para outros participantes - const remetente = await ctx.db.get(mensagem.remetenteId); - if (remetente) { - // Determinar tipo de notificação (se há menções) - const tipoNotificacao = mensagem.mencoes && mensagem.mencoes.length > 0 ? "mencao" : "nova_mensagem"; - const titulo = tipoNotificacao === "mencao" - ? `${remetente.nome} mencionou você` - : `Nova mensagem de ${remetente.nome}`; - const descricao = mensagem.conteudo.substring(0, 100); + // Atualizar última mensagem da conversa + const conversa = await ctx.db.get(mensagem.conversaId); + if (conversa) { + await ctx.db.patch(mensagem.conversaId, { + ultimaMensagem: mensagem.conteudo.substring(0, 100), + ultimaMensagemTimestamp: agora, + ultimaMensagemRemetenteId: mensagem.remetenteId // Guardar ID do remetente + }); - for (const participanteId of conversa.participantes) { - if (participanteId !== mensagem.remetenteId) { - // Criar notificação no banco - await ctx.db.insert("notificacoes", { - usuarioId: participanteId, - tipo: tipoNotificacao, - conversaId: mensagem.conversaId, - mensagemId: mensagem._id, - remetenteId: mensagem.remetenteId, - titulo, - descricao, - lida: false, - criadaEm: agora, - }); + // Criar notificações para outros participantes + const remetente = await ctx.db.get(mensagem.remetenteId); + if (remetente) { + // Determinar tipo de notificação (se há menções) + const tipoNotificacao = + mensagem.mencoes && mensagem.mencoes.length > 0 ? 'mencao' : 'nova_mensagem'; + const titulo = + tipoNotificacao === 'mencao' + ? `${remetente.nome} mencionou você` + : `Nova mensagem de ${remetente.nome}`; + const descricao = mensagem.conteudo.substring(0, 100); - // Enviar push notification (assíncrono, não bloqueia) - ctx.scheduler.runAfter(0, internal.pushNotifications.enviarPushNotification, { - usuarioId: participanteId, - titulo, - corpo: descricao, - data: { - conversaId: mensagem.conversaId, - mensagemId: mensagem._id, - tipo: tipoNotificacao, - }, - }).catch((error) => { - console.error(`Erro ao agendar push para usuário ${participanteId}:`, error); - }); - } - } - } - } - } + for (const participanteId of conversa.participantes) { + if (participanteId !== mensagem.remetenteId) { + // Criar notificação no banco + await ctx.db.insert('notificacoes', { + usuarioId: participanteId, + tipo: tipoNotificacao, + conversaId: mensagem.conversaId, + mensagemId: mensagem._id, + remetenteId: mensagem.remetenteId, + titulo, + descricao, + lida: false, + criadaEm: agora + }); - return mensagensAgendadas.length; - }, + // Enviar push notification (assíncrono, não bloqueia) + ctx.scheduler + .runAfter(0, internal.pushNotifications.enviarPushNotification, { + usuarioId: participanteId, + titulo, + corpo: descricao, + data: { + conversaId: mensagem.conversaId, + mensagemId: mensagem._id, + tipo: tipoNotificacao + } + }) + .catch((error) => { + console.error(`Erro ao agendar push para usuário ${participanteId}:`, error); + }); + } + } + } + } + } + + return mensagensAgendadas.length; + } }); /** * Limpa indicadores de digitação antigos (chamado pelo cron) */ export const limparIndicadoresDigitacao = internalMutation({ - args: {}, - handler: async (ctx) => { - const dezSegundosAtras = Date.now() - 10000; + args: {}, + handler: async (ctx) => { + const dezSegundosAtras = Date.now() - 10000; - const indicadoresAntigos = await ctx.db - .query("digitando") - .filter((q) => q.lt(q.field("iniciouEm"), dezSegundosAtras)) - .collect(); + const indicadoresAntigos = await ctx.db + .query('digitando') + .filter((q) => q.lt(q.field('iniciouEm'), dezSegundosAtras)) + .collect(); - for (const indicador of indicadoresAntigos) { - await ctx.db.delete(indicador._id); - } + for (const indicador of indicadoresAntigos) { + await ctx.db.delete(indicador._id); + } - return indicadoresAntigos.length; - }, + return indicadoresAntigos.length; + } }); diff --git a/packages/backend/convex/permissoesAcoes.ts b/packages/backend/convex/permissoesAcoes.ts index e17a9f4..762507b 100644 --- a/packages/backend/convex/permissoesAcoes.ts +++ b/packages/backend/convex/permissoesAcoes.ts @@ -1,211 +1,184 @@ -import { query, mutation, internalQuery } from "./_generated/server"; -import { v } from "convex/values"; -import type { Doc } from "./_generated/dataModel"; +import { query, mutation, internalQuery } from './_generated/server'; +import { v } from 'convex/values'; +import type { Doc } from './_generated/dataModel'; +import { getCurrentUserFunction } from './auth'; // Catálogo base de recursos e ações // Ajuste/expanda conforme os módulos disponíveis no sistema export const CATALOGO_RECURSOS = [ - { - recurso: "funcionarios", - acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"], - }, - { - recurso: "simbolos", - acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"], - }, + { + recurso: 'funcionarios', + acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir'] + }, + { + recurso: 'simbolos', + acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir'] + } ] as const; export const listarRecursosEAcoes = query({ - args: {}, - returns: v.array( - v.object({ - recurso: v.string(), - acoes: v.array(v.string()), - }) - ), - handler: async () => { - return CATALOGO_RECURSOS.map((r) => ({ - recurso: r.recurso, - acoes: [...r.acoes], - })); - }, + args: {}, + returns: v.array( + v.object({ + recurso: v.string(), + acoes: v.array(v.string()) + }) + ), + handler: async () => { + return CATALOGO_RECURSOS.map((r) => ({ + recurso: r.recurso, + acoes: [...r.acoes] + })); + } }); export const listarPermissoesAcoesPorRole = query({ - args: { roleId: v.id("roles") }, - returns: v.array( - v.object({ - recurso: v.string(), - acoes: v.array(v.string()), - }) - ), - handler: async (ctx, args) => { - // Buscar vínculos permissao<-role - const rolePerms = await ctx.db - .query("rolePermissoes") - .withIndex("by_role", (q) => q.eq("roleId", args.roleId)) - .collect(); + args: { roleId: v.id('roles') }, + returns: v.array( + v.object({ + recurso: v.string(), + acoes: v.array(v.string()) + }) + ), + handler: async (ctx, args) => { + // Buscar vínculos permissao<-role + const rolePerms = await ctx.db + .query('rolePermissoes') + .withIndex('by_role', (q) => q.eq('roleId', args.roleId)) + .collect(); - // Carregar documentos de permissões - const actionsByResource: Record> = {}; - for (const rp of rolePerms) { - const perm = await ctx.db.get(rp.permissaoId); - if (!perm) continue; - const set = (actionsByResource[perm.recurso] ||= new Set()); - set.add(perm.acao); - } + // Carregar documentos de permissões + const actionsByResource: Record> = {}; + for (const rp of rolePerms) { + const perm = await ctx.db.get(rp.permissaoId); + if (!perm) continue; + const set = (actionsByResource[perm.recurso] ||= new Set()); + set.add(perm.acao); + } - // Normalizar para todos os recursos do catálogo - const result: Array<{ recurso: string; acoes: Array }> = []; - for (const item of CATALOGO_RECURSOS) { - const granted = Array.from( - actionsByResource[item.recurso] ?? new Set() - ); - result.push({ recurso: item.recurso, acoes: granted }); - } - return result; - }, + // Normalizar para todos os recursos do catálogo + const result: Array<{ recurso: string; acoes: Array }> = []; + for (const item of CATALOGO_RECURSOS) { + const granted = Array.from(actionsByResource[item.recurso] ?? new Set()); + result.push({ recurso: item.recurso, acoes: granted }); + } + return result; + } }); export const atualizarPermissaoAcao = mutation({ - args: { - roleId: v.id("roles"), - recurso: v.string(), - acao: v.string(), - conceder: v.boolean(), - }, - returns: v.null(), - handler: async (ctx, args) => { - // Garantir documento de permissão (recurso+acao) - let permissao = await ctx.db - .query("permissoes") - .withIndex("by_recurso_e_acao", (q) => - q.eq("recurso", args.recurso).eq("acao", args.acao) - ) - .first(); + args: { + roleId: v.id('roles'), + recurso: v.string(), + acao: v.string(), + conceder: v.boolean() + }, + returns: v.null(), + handler: async (ctx, args) => { + // Garantir documento de permissão (recurso+acao) + let permissao = await ctx.db + .query('permissoes') + .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao)) + .first(); - if (!permissao) { - const nome = `${args.recurso}.${args.acao}`; - const descricao = `Permite ${args.acao} em ${args.recurso}`; - const id = await ctx.db.insert("permissoes", { - nome, - descricao, - recurso: args.recurso, - acao: args.acao, - }); - permissao = await ctx.db.get(id); - } + if (!permissao) { + const nome = `${args.recurso}.${args.acao}`; + const descricao = `Permite ${args.acao} em ${args.recurso}`; + const id = await ctx.db.insert('permissoes', { + nome, + descricao, + recurso: args.recurso, + acao: args.acao + }); + permissao = await ctx.db.get(id); + } - if (!permissao) return null; + if (!permissao) return null; - // Verificar vínculo atual - const existente = await ctx.db - .query("rolePermissoes") - .withIndex("by_role", (q) => q.eq("roleId", args.roleId)) - .collect(); + // Verificar vínculo atual + const existente = await ctx.db + .query('rolePermissoes') + .withIndex('by_role', (q) => q.eq('roleId', args.roleId)) + .collect(); - const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id); + const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id); - if (args.conceder) { - if (!vinculo) { - await ctx.db.insert("rolePermissoes", { - roleId: args.roleId, - permissaoId: permissao._id, - }); - } - } else { - if (vinculo) { - await ctx.db.delete(vinculo._id); - } - } - return null; - }, + if (args.conceder) { + if (!vinculo) { + await ctx.db.insert('rolePermissoes', { + roleId: args.roleId, + permissaoId: permissao._id + }); + } + } else { + if (vinculo) { + await ctx.db.delete(vinculo._id); + } + } + return null; + } }); export const verificarAcao = query({ - args: { - usuarioId: v.id("usuarios"), - recurso: v.string(), - acao: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario) throw new Error("acesso_negado"); + args: { + usuarioId: v.id('usuarios'), + recurso: v.string(), + acao: v.string() + }, + returns: v.null(), + handler: async (ctx, args) => { + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario) throw new Error('acesso_negado'); - const role = await ctx.db.get(usuario.roleId); - if (!role) throw new Error("acesso_negado"); + const role = await ctx.db.get(usuario.roleId); + if (!role) throw new Error('acesso_negado'); - // Níveis administrativos têm acesso total - if (role.nivel <= 1) return null; + // Níveis administrativos têm acesso total + if (role.nivel <= 1) return null; - // Encontrar permissão - const permissao = await ctx.db - .query("permissoes") - .withIndex("by_recurso_e_acao", (q) => - q.eq("recurso", args.recurso).eq("acao", args.acao) - ) - .first(); - if (!permissao) throw new Error("acesso_negado"); + // Encontrar permissão + const permissao = await ctx.db + .query('permissoes') + .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao)) + .first(); + if (!permissao) throw new Error('acesso_negado'); - const hasLink = await ctx.db - .query("rolePermissoes") - .withIndex("by_role", (q) => q.eq("roleId", usuario.roleId)) - .collect(); - const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id); - if (!permitido) throw new Error("acesso_negado"); - return null; - }, + const hasLink = await ctx.db + .query('rolePermissoes') + .withIndex('by_role', (q) => q.eq('roleId', usuario.roleId)) + .collect(); + const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id); + if (!permitido) throw new Error('acesso_negado'); + return null; + } }); export const assertPermissaoAcaoAtual = internalQuery({ - args: { - recurso: v.string(), - acao: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - const identity = await ctx.auth.getUserIdentity(); - let usuarioAtual: Doc<"usuarios"> | null = null; + args: { + recurso: v.string(), + acao: v.string() + }, + returns: v.null(), + handler: async (ctx, args) => { + const usuarioAtual: Doc<'usuarios'> | null = (await getCurrentUserFunction(ctx)) ?? null; + if (!usuarioAtual) throw new Error('acesso_negado'); - if (identity && identity.email) { - usuarioAtual = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", identity.email!)) - .first(); - } + const role = await ctx.db.get(usuarioAtual.roleId); + if (!role) throw new Error('acesso_negado'); + if (role.nivel <= 1) return null; - 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); - } - } + const permissao = await ctx.db + .query('permissoes') + .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao)) + .first(); + if (!permissao) throw new Error('acesso_negado'); - if (!usuarioAtual) throw new Error("acesso_negado"); - - const role = await ctx.db.get(usuarioAtual.roleId); - if (!role) throw new Error("acesso_negado"); - if (role.nivel <= 1) return null; - - const permissao = await ctx.db - .query("permissoes") - .withIndex("by_recurso_e_acao", (q) => - q.eq("recurso", args.recurso).eq("acao", args.acao) - ) - .first(); - if (!permissao) throw new Error("acesso_negado"); - - const links = await ctx.db - .query("rolePermissoes") - .withIndex("by_role", (q) => q.eq("roleId", role._id)) - .collect(); - const ok = links.some((rp) => rp.permissaoId === permissao!._id); - if (!ok) throw new Error("acesso_negado"); - return null; - }, + const links = await ctx.db + .query('rolePermissoes') + .withIndex('by_role', (q) => q.eq('roleId', role._id)) + .collect(); + const ok = links.some((rp) => rp.permissaoId === permissao!._id); + if (!ok) throw new Error('acesso_negado'); + return null; + } }); diff --git a/packages/backend/convex/preferenciasNotificacao.ts b/packages/backend/convex/preferenciasNotificacao.ts index ede1089..686f9db 100644 --- a/packages/backend/convex/preferenciasNotificacao.ts +++ b/packages/backend/convex/preferenciasNotificacao.ts @@ -1,136 +1,113 @@ -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; -import { Id } from "./_generated/dataModel"; +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { getCurrentUserFunction } from './auth'; /** * Obter preferências de notificação para uma conversa */ export const obterPreferenciasConversa = query({ - args: { - conversaId: v.id("conversas"), - }, - returns: v.union( - v.object({ - pushAtivado: v.boolean(), - emailAtivado: v.boolean(), - somAtivado: v.boolean(), - silenciado: v.boolean(), - apenasMencoes: v.boolean(), - }), - v.null() - ), - handler: async (ctx, args) => { - const identity = await ctx.auth.getUserIdentity(); - if (!identity?.email) { - return null; - } + args: { + conversaId: v.id('conversas') + }, + returns: v.union( + v.object({ + pushAtivado: v.boolean(), + emailAtivado: v.boolean(), + somAtivado: v.boolean(), + silenciado: v.boolean(), + apenasMencoes: v.boolean() + }), + v.null() + ), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) return null; - const usuario = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", identity.email!)) - .first(); + const preferencias = await ctx.db + .query('preferenciasNotificacaoConversa') + .withIndex('by_usuario_conversa', (q) => + q.eq('usuarioId', usuario._id).eq('conversaId', args.conversaId) + ) + .first(); - if (!usuario) { - return null; - } + if (!preferencias) { + // Retornar valores padrão + return { + pushAtivado: true, + emailAtivado: true, + somAtivado: true, + silenciado: false, + apenasMencoes: false + }; + } - const preferencias = await ctx.db - .query("preferenciasNotificacaoConversa") - .withIndex("by_usuario_conversa", (q) => - q.eq("usuarioId", usuario._id).eq("conversaId", args.conversaId) - ) - .first(); - - if (!preferencias) { - // Retornar valores padrão - return { - pushAtivado: true, - emailAtivado: true, - somAtivado: true, - silenciado: false, - apenasMencoes: false, - }; - } - - return { - pushAtivado: preferencias.pushAtivado, - emailAtivado: preferencias.emailAtivado, - somAtivado: preferencias.somAtivado, - silenciado: preferencias.silenciado, - apenasMencoes: preferencias.apenasMencoes, - }; - }, + return { + pushAtivado: preferencias.pushAtivado, + emailAtivado: preferencias.emailAtivado, + somAtivado: preferencias.somAtivado, + silenciado: preferencias.silenciado, + apenasMencoes: preferencias.apenasMencoes + }; + } }); /** * Atualizar preferências de notificação para uma conversa */ export const atualizarPreferenciasConversa = mutation({ - args: { - conversaId: v.id("conversas"), - pushAtivado: v.optional(v.boolean()), - emailAtivado: v.optional(v.boolean()), - somAtivado: v.optional(v.boolean()), - silenciado: v.optional(v.boolean()), - apenasMencoes: v.optional(v.boolean()), - }, - returns: v.object({ sucesso: v.boolean() }), - handler: async (ctx, args) => { - const identity = await ctx.auth.getUserIdentity(); - if (!identity?.email) { - return { sucesso: false }; - } + args: { + conversaId: v.id('conversas'), + pushAtivado: v.optional(v.boolean()), + emailAtivado: v.optional(v.boolean()), + somAtivado: v.optional(v.boolean()), + silenciado: v.optional(v.boolean()), + apenasMencoes: v.optional(v.boolean()) + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) return { sucesso: false }; - const usuario = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", identity.email!)) - .first(); + // Verificar se usuário pertence à conversa + const conversa = await ctx.db.get(args.conversaId); + if (!conversa || !conversa.participantes.includes(usuario._id)) { + return { sucesso: false }; + } - if (!usuario) { - return { sucesso: false }; - } + const preferenciasExistentes = await ctx.db + .query('preferenciasNotificacaoConversa') + .withIndex('by_usuario_conversa', (q) => + q.eq('usuarioId', usuario._id).eq('conversaId', args.conversaId) + ) + .first(); - // Verificar se usuário pertence à conversa - const conversa = await ctx.db.get(args.conversaId); - if (!conversa || !conversa.participantes.includes(usuario._id)) { - return { sucesso: false }; - } + const agora = Date.now(); - const preferenciasExistentes = await ctx.db - .query("preferenciasNotificacaoConversa") - .withIndex("by_usuario_conversa", (q) => - q.eq("usuarioId", usuario._id).eq("conversaId", args.conversaId) - ) - .first(); + if (preferenciasExistentes) { + // Atualizar preferências existentes + await ctx.db.patch(preferenciasExistentes._id, { + pushAtivado: args.pushAtivado ?? preferenciasExistentes.pushAtivado, + emailAtivado: args.emailAtivado ?? preferenciasExistentes.emailAtivado, + somAtivado: args.somAtivado ?? preferenciasExistentes.somAtivado, + silenciado: args.silenciado ?? preferenciasExistentes.silenciado, + apenasMencoes: args.apenasMencoes ?? preferenciasExistentes.apenasMencoes, + atualizadoEm: agora + }); + } else { + // Criar novas preferências com valores padrão + await ctx.db.insert('preferenciasNotificacaoConversa', { + usuarioId: usuario._id, + conversaId: args.conversaId, + pushAtivado: args.pushAtivado ?? true, + emailAtivado: args.emailAtivado ?? true, + somAtivado: args.somAtivado ?? true, + silenciado: args.silenciado ?? false, + apenasMencoes: args.apenasMencoes ?? false, + criadoEm: agora, + atualizadoEm: agora + }); + } - const agora = Date.now(); - - if (preferenciasExistentes) { - // Atualizar preferências existentes - await ctx.db.patch(preferenciasExistentes._id, { - pushAtivado: args.pushAtivado ?? preferenciasExistentes.pushAtivado, - emailAtivado: args.emailAtivado ?? preferenciasExistentes.emailAtivado, - somAtivado: args.somAtivado ?? preferenciasExistentes.somAtivado, - silenciado: args.silenciado ?? preferenciasExistentes.silenciado, - apenasMencoes: args.apenasMencoes ?? preferenciasExistentes.apenasMencoes, - atualizadoEm: agora, - }); - } else { - // Criar novas preferências com valores padrão - await ctx.db.insert("preferenciasNotificacaoConversa", { - usuarioId: usuario._id, - conversaId: args.conversaId, - pushAtivado: args.pushAtivado ?? true, - emailAtivado: args.emailAtivado ?? true, - somAtivado: args.somAtivado ?? true, - silenciado: args.silenciado ?? false, - apenasMencoes: args.apenasMencoes ?? false, - criadoEm: agora, - atualizadoEm: agora, - }); - } - - return { sucesso: true }; - }, + return { sucesso: true }; + } }); - diff --git a/packages/backend/convex/pushNotifications.ts b/packages/backend/convex/pushNotifications.ts index f719ab2..1809b3a 100644 --- a/packages/backend/convex/pushNotifications.ts +++ b/packages/backend/convex/pushNotifications.ts @@ -1,120 +1,111 @@ -import { v } from "convex/values"; -import { mutation, query, internalMutation, internalQuery } from "./_generated/server"; -import { Id } from "./_generated/dataModel"; -import { internal, api } from "./_generated/api"; +import { v } from 'convex/values'; +import { mutation, internalMutation, internalQuery } from './_generated/server'; +import { internal, api } from './_generated/api'; +import { getCurrentUserFunction } from './auth'; /** * Registrar subscription de push notification */ export const registrarPushSubscription = mutation({ - args: { - endpoint: v.string(), - keys: v.object({ - p256dh: v.string(), - auth: v.string(), - }), - userAgent: v.optional(v.string()), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - // Obter usuário autenticado - const identity = await ctx.auth.getUserIdentity(); - if (!identity?.email) { - return { sucesso: false, erro: "Usuário não autenticado" }; - } + args: { + endpoint: v.string(), + keys: v.object({ + p256dh: v.string(), + auth: v.string() + }), + userAgent: v.optional(v.string()) + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + // Obter usuário autenticado + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + return { sucesso: false, erro: 'Usuário não autenticado' }; + } - const usuario = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", identity.email!)) - .first(); + // Verificar se já existe subscription com este endpoint + const existente = await ctx.db + .query('pushSubscriptions') + .withIndex('by_endpoint', (q) => q.eq('endpoint', args.endpoint)) + .first(); - if (!usuario) { - return { sucesso: false, erro: "Usuário não encontrado" }; - } + if (existente) { + // Atualizar subscription existente + await ctx.db.patch(existente._id, { + usuarioId: usuario._id, + keys: args.keys, + userAgent: args.userAgent, + ultimaAtividade: Date.now(), + ativo: true + }); + } else { + // Criar nova subscription + await ctx.db.insert('pushSubscriptions', { + usuarioId: usuario._id, + endpoint: args.endpoint, + keys: args.keys, + userAgent: args.userAgent, + criadoEm: Date.now(), + ultimaAtividade: Date.now(), + ativo: true + }); + } - // Verificar se já existe subscription com este endpoint - const existente = await ctx.db - .query("pushSubscriptions") - .withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint)) - .first(); - - if (existente) { - // Atualizar subscription existente - await ctx.db.patch(existente._id, { - usuarioId: usuario._id, - keys: args.keys, - userAgent: args.userAgent, - ultimaAtividade: Date.now(), - ativo: true, - }); - } else { - // Criar nova subscription - await ctx.db.insert("pushSubscriptions", { - usuarioId: usuario._id, - endpoint: args.endpoint, - keys: args.keys, - userAgent: args.userAgent, - criadoEm: Date.now(), - ultimaAtividade: Date.now(), - ativo: true, - }); - } - - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Remover subscription de push notification */ export const removerPushSubscription = mutation({ - args: { - endpoint: v.string(), - }, - returns: v.object({ sucesso: v.boolean() }), - handler: async (ctx, args) => { - const subscription = await ctx.db - .query("pushSubscriptions") - .withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint)) - .first(); + args: { + endpoint: v.string() + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + const subscription = await ctx.db + .query('pushSubscriptions') + .withIndex('by_endpoint', (q) => q.eq('endpoint', args.endpoint)) + .first(); - if (subscription) { - await ctx.db.patch(subscription._id, { ativo: false }); - } + if (subscription) { + await ctx.db.patch(subscription._id, { ativo: false }); + } - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Obter subscriptions ativas de um usuário */ export const obterPushSubscriptions = internalQuery({ - args: { - usuarioId: v.id("usuarios"), - }, - returns: v.array( - v.object({ - _id: v.id("pushSubscriptions"), - endpoint: v.string(), - keys: v.object({ - p256dh: v.string(), - auth: v.string(), - }), - }) - ), - handler: async (ctx, args) => { - const subscriptions = await ctx.db - .query("pushSubscriptions") - .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId).eq("ativo", true)) - .collect(); + args: { + usuarioId: v.id('usuarios') + }, + returns: v.array( + v.object({ + _id: v.id('pushSubscriptions'), + endpoint: v.string(), + keys: v.object({ + p256dh: v.string(), + auth: v.string() + }) + }) + ), + handler: async (ctx, args) => { + const subscriptions = await ctx.db + .query('pushSubscriptions') + .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId).eq('ativo', true)) + .collect(); - return subscriptions.map((sub) => ({ - _id: sub._id, - endpoint: sub.endpoint, - keys: sub.keys, - })); - }, + return subscriptions.map((sub) => ({ + _id: sub._id, + endpoint: sub.endpoint, + keys: sub.keys + })); + } }); /** @@ -122,157 +113,156 @@ export const obterPushSubscriptions = internalQuery({ * Esta função será chamada quando uma nova mensagem chegar */ export const enviarPushNotification = internalMutation({ - args: { - usuarioId: v.id("usuarios"), - titulo: v.string(), - corpo: v.string(), - data: v.optional( - v.object({ - conversaId: v.optional(v.id("conversas")), - mensagemId: v.optional(v.id("mensagens")), - tipo: v.optional(v.string()), - }) - ), - }, - returns: v.object({ enviados: v.number(), falhas: v.number() }), - handler: async (ctx, args) => { - // Buscar subscriptions ativas do usuário - const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, { - usuarioId: args.usuarioId, - }); + args: { + usuarioId: v.id('usuarios'), + titulo: v.string(), + corpo: v.string(), + data: v.optional( + v.object({ + conversaId: v.optional(v.id('conversas')), + mensagemId: v.optional(v.id('mensagens')), + tipo: v.optional(v.string()) + }) + ) + }, + returns: v.object({ enviados: v.number(), falhas: v.number() }), + handler: async (ctx, args) => { + // Buscar subscriptions ativas do usuário + const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, { + usuarioId: args.usuarioId + }); - if (subscriptions.length === 0) { - return { enviados: 0, falhas: 0 }; - } + if (subscriptions.length === 0) { + return { enviados: 0, falhas: 0 }; + } - // Verificar preferências do usuário - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario || usuario.notificacoesAtivadas === false) { - return { enviados: 0, falhas: 0 }; - } + // Verificar preferências do usuário + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario || usuario.notificacoesAtivadas === false) { + return { enviados: 0, falhas: 0 }; + } - // Se há conversaId, verificar preferências específicas da conversa - const conversaId = args.data?.conversaId; - if (conversaId) { - const preferencias = await ctx.db - .query("preferenciasNotificacaoConversa") - .withIndex("by_usuario_conversa", (q) => - q.eq("usuarioId", args.usuarioId).eq("conversaId", conversaId) - ) - .first(); + // Se há conversaId, verificar preferências específicas da conversa + const conversaId = args.data?.conversaId; + if (conversaId) { + const preferencias = await ctx.db + .query('preferenciasNotificacaoConversa') + .withIndex('by_usuario_conversa', (q) => + q.eq('usuarioId', args.usuarioId).eq('conversaId', conversaId) + ) + .first(); - if (preferencias) { - // Se silenciado ou push desativado, não enviar - if (preferencias.silenciado || !preferencias.pushAtivado) { - return { enviados: 0, falhas: 0 }; - } + if (preferencias) { + // Se silenciado ou push desativado, não enviar + if (preferencias.silenciado || !preferencias.pushAtivado) { + return { enviados: 0, falhas: 0 }; + } - // Se apenas menções e não é menção, não enviar - if (preferencias.apenasMencoes && args.data?.tipo !== "mencao") { - return { enviados: 0, falhas: 0 }; - } - } - } + // Se apenas menções e não é menção, não enviar + if (preferencias.apenasMencoes && args.data?.tipo !== 'mencao') { + return { enviados: 0, falhas: 0 }; + } + } + } - // Agendar envio de push via action (que roda em Node.js) - let enviados = 0; - let falhas = 0; + // Agendar envio de push via action (que roda em Node.js) + let enviados = 0; + let falhas = 0; - // Converter IDs para strings ao passar para a action - // A action espera strings, mas recebemos Ids do Convex - const dataParaAction = args.data - ? { - conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined, - mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined, - tipo: args.data.tipo, - } - : undefined; + // Converter IDs para strings ao passar para a action + // A action espera strings, mas recebemos Ids do Convex + const dataParaAction = args.data + ? { + conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined, + mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined, + tipo: args.data.tipo + } + : undefined; - for (const subscription of subscriptions) { - try { - await ctx.scheduler.runAfter(0, api.actions.pushNotifications.enviarPush, { - subscriptionId: subscription._id, - titulo: args.titulo, - corpo: args.corpo, - data: dataParaAction, - }); - enviados++; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`Erro ao agendar push para subscription ${subscription._id}:`, errorMessage); - falhas++; - } - } + for (const subscription of subscriptions) { + try { + await ctx.scheduler.runAfter(0, api.actions.pushNotifications.enviarPush, { + subscriptionId: subscription._id, + titulo: args.titulo, + corpo: args.corpo, + data: dataParaAction + }); + enviados++; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Erro ao agendar push para subscription ${subscription._id}:`, errorMessage); + falhas++; + } + } - return { enviados, falhas }; - }, + return { enviados, falhas }; + } }); /** * Obter subscription por ID (para actions) */ export const getSubscriptionById = internalQuery({ - args: { - subscriptionId: v.id("pushSubscriptions"), - }, - returns: v.union( - v.object({ - _id: v.id("pushSubscriptions"), - endpoint: v.string(), - keys: v.object({ - p256dh: v.string(), - auth: v.string(), - }), - ativo: v.boolean(), - }), - v.null() - ), - handler: async (ctx, args) => { - const subscription = await ctx.db.get(args.subscriptionId); - if (!subscription) { - return null; - } + args: { + subscriptionId: v.id('pushSubscriptions') + }, + returns: v.union( + v.object({ + _id: v.id('pushSubscriptions'), + endpoint: v.string(), + keys: v.object({ + p256dh: v.string(), + auth: v.string() + }), + ativo: v.boolean() + }), + v.null() + ), + handler: async (ctx, args) => { + const subscription = await ctx.db.get(args.subscriptionId); + if (!subscription) { + return null; + } - return { - _id: subscription._id, - endpoint: subscription.endpoint, - keys: subscription.keys, - ativo: subscription.ativo, - }; - }, + return { + _id: subscription._id, + endpoint: subscription.endpoint, + keys: subscription.keys, + ativo: subscription.ativo + }; + } }); /** * Marcar subscription como inativa */ export const marcarSubscriptionInativa = internalMutation({ - args: { - subscriptionId: v.id("pushSubscriptions"), - }, - returns: v.null(), - handler: async (ctx, args) => { - await ctx.db.patch(args.subscriptionId, { ativo: false }); - return null; - }, + args: { + subscriptionId: v.id('pushSubscriptions') + }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.patch(args.subscriptionId, { ativo: false }); + return null; + } }); /** * Verificar se usuário está online (última atividade recente) */ export const verificarUsuarioOnline = internalQuery({ - args: { - usuarioId: v.id("usuarios"), - }, - returns: v.boolean(), - handler: async (ctx, args) => { - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario || !usuario.ultimaAtividade) { - return false; - } + args: { + usuarioId: v.id('usuarios') + }, + returns: v.boolean(), + handler: async (ctx, args) => { + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario || !usuario.ultimaAtividade) { + return false; + } - // Considerar online se última atividade foi há menos de 5 minutos - const cincoMinutosAtras = Date.now() - 5 * 60 * 1000; - return usuario.ultimaAtividade >= cincoMinutosAtras; - }, + // Considerar online se última atividade foi há menos de 5 minutos + const cincoMinutosAtras = Date.now() - 5 * 60 * 1000; + return usuario.ultimaAtividade >= cincoMinutosAtras; + } }); - diff --git a/packages/backend/convex/usuarios.ts b/packages/backend/convex/usuarios.ts index 063a9f5..b045cdb 100644 --- a/packages/backend/convex/usuarios.ts +++ b/packages/backend/convex/usuarios.ts @@ -1,354 +1,349 @@ -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; -import { hashPassword } from "./auth/utils"; -import { registrarAtividade } from "./logsAtividades"; -import { Id, Doc } from "./_generated/dataModel"; -import type { QueryCtx, MutationCtx } from "./_generated/server"; -import { createAuthUser, getCurrentUserFunction } from "./auth"; +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { registrarAtividade } from './logsAtividades'; +import { Id, Doc } from './_generated/dataModel'; +import type { QueryCtx } from './_generated/server'; +import { createAuthUser, getCurrentUserFunction } from './auth'; /** * Helper para obter a matrícula do usuário (do funcionário se houver) */ async function obterMatriculaUsuario( - ctx: QueryCtx, - usuario: Doc<"usuarios"> + ctx: QueryCtx, + usuario: Doc<'usuarios'> ): Promise { - if (usuario.funcionarioId) { - const funcionario = await ctx.db.get(usuario.funcionarioId); - return funcionario?.matricula; - } - return undefined; + if (usuario.funcionarioId) { + const funcionario = await ctx.db.get(usuario.funcionarioId); + return funcionario?.matricula; + } + return undefined; } /** * Associar funcionário a um usuário */ export const associarFuncionario = mutation({ - args: { - usuarioId: v.id("usuarios"), - funcionarioId: v.id("funcionarios"), - }, - returns: v.object({ sucesso: v.boolean() }), - handler: async (ctx, args) => { - // Verificar se o funcionário existe - const funcionario = await ctx.db.get(args.funcionarioId); - if (!funcionario) { - throw new Error("Funcionário não encontrado"); - } + args: { + usuarioId: v.id('usuarios'), + funcionarioId: v.id('funcionarios') + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + // Verificar se o funcionário existe + const funcionario = await ctx.db.get(args.funcionarioId); + if (!funcionario) { + throw new Error('Funcionário não encontrado'); + } - // Verificar se o funcionário já está associado a outro usuário - const usuarioExistente = await ctx.db - .query("usuarios") - .withIndex("by_funcionarioId", (q) => - q.eq("funcionarioId", args.funcionarioId) - ) - .first(); + // Verificar se o funcionário já está associado a outro usuário + const usuarioExistente = await ctx.db + .query('usuarios') + .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId)) + .first(); - if (usuarioExistente && usuarioExistente._id !== args.usuarioId) { - const matricula = await obterMatriculaUsuario(ctx, usuarioExistente); - throw new Error( - `Este funcionário já está associado ao usuário: ${ - usuarioExistente.nome - }${matricula ? ` (${matricula})` : ""}` - ); - } + if (usuarioExistente && usuarioExistente._id !== args.usuarioId) { + const matricula = await obterMatriculaUsuario(ctx, usuarioExistente); + throw new Error( + `Este funcionário já está associado ao usuário: ${ + usuarioExistente.nome + }${matricula ? ` (${matricula})` : ''}` + ); + } - // Associar funcionário ao usuário - await ctx.db.patch(args.usuarioId, { - funcionarioId: args.funcionarioId, - }); + // Associar funcionário ao usuário + await ctx.db.patch(args.usuarioId, { + funcionarioId: args.funcionarioId + }); - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Desassociar funcionário de um usuário */ export const desassociarFuncionario = mutation({ - args: { - usuarioId: v.id("usuarios"), - }, - returns: v.object({ sucesso: v.boolean() }), - handler: async (ctx, args) => { - await ctx.db.patch(args.usuarioId, { - funcionarioId: undefined, - }); + args: { + usuarioId: v.id('usuarios') + }, + returns: v.object({ sucesso: v.boolean() }), + handler: async (ctx, args) => { + await ctx.db.patch(args.usuarioId, { + funcionarioId: undefined + }); - return { sucesso: true }; - }, + return { sucesso: true }; + } }); /** * Criar novo usuário (apenas TI) */ export const criar = mutation({ - args: { - 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 email já existe - const emailExistente = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", args.email)) - .first(); + args: { + 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 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" }; - } + if (emailExistente) { + return { sucesso: false as const, erro: 'E-mail já cadastrado' }; + } - const senhaTemporaria = args.senhaInicial; + const senhaTemporaria = args.senhaInicial; - const authUserId = await createAuthUser(ctx, { - nome: args.nome, - email: args.email, - password: senhaTemporaria, - }); + const authUserId = await createAuthUser(ctx, { + nome: args.nome, + email: args.email, + password: senhaTemporaria + }); - // Criar usuário - const usuarioId = await ctx.db.insert("usuarios", { - authId: authUserId, - nome: args.nome, - email: args.email, - funcionarioId: args.funcionarioId, - roleId: args.roleId, - ativo: true, - primeiroAcesso: true, - criadoEm: Date.now(), - atualizadoEm: Date.now(), - }); + // Criar usuário + const usuarioId = await ctx.db.insert('usuarios', { + authId: authUserId, + 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 }; - }, + 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()), - }, - handler: async (ctx, args) => { - let usuarios = await ctx.db.query("usuarios").collect(); + args: { + setor: v.optional(v.string()), + matricula: v.optional(v.string()), + ativo: v.optional(v.boolean()) + }, + handler: async (ctx, args) => { + let usuarios = await ctx.db.query('usuarios').collect(); - // Filtrar por matrícula (buscar no funcionário) - if (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 matrícula (buscar no funcionário) + if (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 - if (args.ativo !== undefined) { - usuarios = usuarios.filter((u) => u.ativo === args.ativo); - } + // Filtrar por ativo + if (args.ativo !== undefined) { + usuarios = usuarios.filter((u) => u.ativo === args.ativo); + } - // Buscar roles e funcionários - const resultado = []; - const usuariosSemRole: Array<{ - nome: string; - matricula: string; - roleId: Id<"roles">; - }> = []; + // Buscar roles e funcionários + const resultado = []; + const usuariosSemRole: Array<{ + nome: string; + matricula: string; + roleId: Id<'roles'>; + }> = []; - for (const usuario of usuarios) { - try { - const role = await ctx.db.get(usuario.roleId); + for (const usuario of usuarios) { + try { + const role = await ctx.db.get(usuario.roleId); - // Se a role não existe, criar uma role de erro mas ainda incluir o usuário - if (!role) { - const matricula = await obterMatriculaUsuario(ctx, usuario); - usuariosSemRole.push({ - nome: usuario.nome, - matricula: matricula || "N/A", - roleId: usuario.roleId, - }); + // Se a role não existe, criar uma role de erro mas ainda incluir o usuário + if (!role) { + const matricula = await obterMatriculaUsuario(ctx, usuario); + usuariosSemRole.push({ + nome: usuario.nome, + matricula: matricula || 'N/A', + roleId: usuario.roleId + }); - // Filtrar por setor - se filtro está ativo e role não existe, pular - if (args.setor) { - continue; - } + // Filtrar por setor - se filtro está ativo e role não existe, pular + if (args.setor) { + continue; + } - // Incluir usuário com role de erro - let funcionario = undefined; - 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 - ); - } - } + // Incluir usuário com role de erro + let funcionario = undefined; + 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 + ); + } + } - const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); + const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); - // Criar role de erro (sem _creationTime pois a role não existe) - resultado.push({ - _id: usuario._id, - matricula: matriculaUsuario, - 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; - } + // Criar role de erro (sem _creationTime pois a role não existe) + resultado.push({ + _id: usuario._id, + matricula: matriculaUsuario, + 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; + } - // Filtrar por setor - if (args.setor && role.setor !== args.setor) { - continue; - } + // Filtrar por setor + if (args.setor && role.setor !== args.setor) { + continue; + } - // Buscar funcionário associado - let funcionario = undefined; - 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 - ); - } - } + // Buscar funcionário associado + let funcionario = undefined; + 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 + ); + } + } - // Construir objeto role - incluir _creationTime se existir (campo automático do Convex) - const roleObj = { - _id: role._id, - descricao: role.descricao, - nome: role.nome, - nivel: role.nivel, - ...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }), - ...(role.customizado !== undefined && { - customizado: role.customizado, - }), - ...(role.editavel !== undefined && { editavel: role.editavel }), - ...(role.setor !== undefined && { setor: role.setor }), - }; + // Construir objeto role - incluir _creationTime se existir (campo automático do Convex) + const roleObj = { + _id: role._id, + descricao: role.descricao, + nome: role.nome, + nivel: role.nivel, + ...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }), + ...(role.customizado !== undefined && { + customizado: role.customizado + }), + ...(role.editavel !== undefined && { editavel: role.editavel }), + ...(role.setor !== undefined && { setor: role.setor }) + }; - const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); + const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); - resultado.push({ - _id: usuario._id, - matricula: matriculaUsuario, - 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 - } - } + resultado.push({ + _id: usuario._id, + matricula: matriculaUsuario, + 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 !== "N/A" ? ` (${u.matricula})` : "" - } - RoleID: ${u.roleId}` - ) - ); - } + // 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 !== 'N/A' ? ` (${u.matricula})` : ''} - RoleID: ${u.roleId}` + ) + ); + } - return resultado; - }, + 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(), - }); + 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(); + // 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 }); - } - } + for (const sessao of sessoes) { + await ctx.db.patch(sessao._id, { ativo: false }); + } + } - return null; - }, + return null; + } }); /** @@ -387,358 +382,309 @@ export const alterarStatus = mutation({ * 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(); + 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); - } + for (const sessao of sessoes) { + await ctx.db.delete(sessao._id); + } - // Excluir usuário - await ctx.db.delete(args.usuarioId); + // Excluir usuário + await ctx.db.delete(args.usuarioId); - return null; - }, + 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; - }, + 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(), - }); + 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(); + // 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 }); - } + for (const sessao of sessoes) { + await ctx.db.patch(sessao._id, { ativo: false }); + } - return null; - }, + 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"); - } + 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(), - }); + // Atualizar usuário + await ctx.db.patch(args.usuarioId, { + roleId: args.novaRoleId, + atualizadoEm: Date.now() + }); - return null; - }, + 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(); + 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) => { + const usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) throw new Error('Usuário não encontrado'); - let usuarioAtual = null; + // 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'); + } - 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(); - } + // Atualizar apenas os campos fornecidos + const updates: Partial> & { atualizadoEm: number } = { + atualizadoEm: Date.now() + }; - if (!usuarioAtual) throw new Error("Usuário não encontrado"); + 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; - // 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"); - } + await ctx.db.patch(usuarioAtual._id, updates); - // Atualizar apenas os campos fornecidos - const updates: Partial> & { atualizadoEm: number } = { - 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; - }, + return null; + } }); /** * Obter perfil do usuário atual */ export const obterPerfil = query({ - args: {}, - returns: v.union( - v.object({ - _id: v.id("usuarios"), - nome: v.string(), - email: v.string(), - matricula: v.optional(v.string()), - funcionarioId: v.optional(v.id("funcionarios")), - avatar: v.optional(v.string()), - fotoPerfil: v.optional(v.id("_storage")), - fotoPerfilUrl: v.union(v.string(), v.null()), - 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.boolean(), - somNotificacao: v.boolean(), - }), - v.null() - ), - handler: async (ctx) => { - const identity = await ctx.auth.getUserIdentity(); - console.log("Identity:", identity ? "encontrado" : "null"); + args: {}, + returns: v.union( + v.object({ + _id: v.id('usuarios'), + nome: v.string(), + email: v.string(), + matricula: v.optional(v.string()), + funcionarioId: v.optional(v.id('funcionarios')), + avatar: v.optional(v.string()), + fotoPerfil: v.optional(v.id('_storage')), + fotoPerfilUrl: v.union(v.string(), v.null()), + 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.boolean(), + somNotificacao: v.boolean() + }), + v.null() + ), + handler: async (ctx) => { + const usuarioAutenticado = await getCurrentUserFunction(ctx); + console.log('Usuario autenticado:', usuarioAutenticado); + if (!usuarioAutenticado) { + return null; + } - let usuarioAtual = null; + const usuarioAtual = usuarioAutenticado; - 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:', usuarioAtual.nome); - if (!usuarioAtual) { - return null; - } + // Buscar fotoPerfil URL se existir + let fotoPerfilUrl = null; + if (usuarioAtual.fotoPerfil) { + fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil); + } - console.log("✅ Usuário encontrado:", usuarioAtual.nome); + const matricula = await obterMatriculaUsuario(ctx, usuarioAtual); - // Buscar fotoPerfil URL se existir - let fotoPerfilUrl = null; - if (usuarioAtual.fotoPerfil) { - fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil); - } - - const matricula = await obterMatriculaUsuario(ctx, usuarioAtual); - - return { - _id: usuarioAtual._id, - nome: usuarioAtual.nome, - email: usuarioAtual.email, - matricula: matricula || undefined, - funcionarioId: usuarioAtual.funcionarioId, - avatar: usuarioAtual.avatar, - fotoPerfil: usuarioAtual.fotoPerfil, - fotoPerfilUrl, - setor: usuarioAtual.setor, - statusMensagem: usuarioAtual.statusMensagem, - statusPresenca: usuarioAtual.statusPresenca, - notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true, - somNotificacao: usuarioAtual.somNotificacao ?? true, - }; - }, + return { + _id: usuarioAtual._id, + nome: usuarioAtual.nome, + email: usuarioAtual.email, + matricula: matricula || undefined, + funcionarioId: usuarioAtual.funcionarioId, + 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.optional(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) => { - // Obter usuário autenticado usando função helper compartilhada - const usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - return []; - } + args: {}, + returns: v.array( + v.object({ + _id: v.id('usuarios'), + nome: v.string(), + email: v.string(), + matricula: v.optional(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) => { + // Obter usuário autenticado usando função helper compartilhada + const usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) { + return []; + } - // Buscar todos os usuários ativos - const usuarios = await ctx.db - .query("usuarios") - .filter((q) => q.eq(q.field("ativo"), true)) - .collect(); + // Buscar todos os usuários ativos + const usuarios = await ctx.db + .query('usuarios') + .filter((q) => q.eq(q.field('ativo'), true)) + .collect(); - // Filtrar o usuário atual da lista apenas se conseguimos identificá-lo com certeza - // Se não conseguimos identificar (usuarioAtual é null), retornar todos - // O frontend fará um filtro adicional usando obterPerfil como camada de segurança - const usuariosFiltrados = usuarioAtual - ? usuarios.filter((u) => u._id !== usuarioAtual._id) - : usuarios; + // Filtrar o usuário atual da lista apenas se conseguimos identificá-lo com certeza + // Se não conseguimos identificar (usuarioAtual é null), retornar todos + // O frontend fará um filtro adicional usando obterPerfil como camada de segurança + const usuariosFiltrados = usuarioAtual + ? usuarios.filter((u) => u._id !== usuarioAtual._id) + : usuarios; - // Buscar foto de perfil URL para cada usuário - const usuariosComFoto = await Promise.all( - usuariosFiltrados.map(async (usuario) => { - let fotoPerfilUrl = null; - if (usuario.fotoPerfil) { - fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); - } + // Buscar foto de perfil URL para cada usuário + const usuariosComFoto = await Promise.all( + usuariosFiltrados.map(async (usuario) => { + let fotoPerfilUrl = null; + if (usuario.fotoPerfil) { + fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); + } - const matricula = await obterMatriculaUsuario(ctx, usuario); + const matricula = await obterMatriculaUsuario(ctx, usuario); - return { - _id: usuario._id, - nome: usuario.nome, - email: usuario.email, - matricula: matricula || undefined, - avatar: usuario.avatar, - fotoPerfil: usuario.fotoPerfil, - fotoPerfilUrl, - statusPresenca: usuario.statusPresenca || "offline", - statusMensagem: usuario.statusMensagem, - ultimaAtividade: usuario.ultimaAtividade, - }; - }) - ); + return { + _id: usuario._id, + nome: usuario.nome, + email: usuario.email, + matricula: matricula || undefined, + avatar: usuario.avatar, + fotoPerfil: usuario.fotoPerfil, + fotoPerfilUrl, + statusPresenca: usuario.statusPresenca || 'offline', + statusMensagem: usuario.statusMensagem, + ultimaAtividade: usuario.ultimaAtividade + }; + }) + ); - return usuariosComFoto; - }, + return usuariosComFoto; + } }); /** * Gera URL para upload de foto de perfil */ export const uploadFotoPerfil = mutation({ - args: {}, - returns: v.string(), - handler: async (ctx) => { - // TENTAR BETTER AUTH PRIMEIRO - const identity = await ctx.auth.getUserIdentity(); + args: {}, + returns: v.string(), + handler: async (ctx) => { + const usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) throw new Error('Usuário não autenticado'); - 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(); - }, + return await ctx.storage.generateUploadUrl(); + } }); // ==================== GESTÃO AVANÇADA DE USUÁRIOS (TI_MASTER) ==================== @@ -747,128 +693,128 @@ export const uploadFotoPerfil = mutation({ * Bloquear usuário (apenas TI_MASTER) */ export const bloquearUsuario = mutation({ - args: { - usuarioId: v.id("usuarios"), - motivo: v.string(), - bloqueadoPorId: 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 usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - return { sucesso: false as const, erro: "Usuário não autenticado" }; - } + args: { + usuarioId: v.id('usuarios'), + motivo: v.string(), + bloqueadoPorId: 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 usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) { + return { sucesso: false as const, erro: 'Usuário não autenticado' }; + } - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario) { - return { sucesso: false as const, erro: "Usuário não encontrado" }; - } + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario) { + return { sucesso: false as const, erro: 'Usuário não encontrado' }; + } - // Atualizar usuário como bloqueado - await ctx.db.patch(args.usuarioId, { - bloqueado: true, - motivoBloqueio: args.motivo, - dataBloqueio: Date.now(), - atualizadoEm: Date.now(), - }); + // Atualizar usuário como bloqueado + await ctx.db.patch(args.usuarioId, { + bloqueado: true, + motivoBloqueio: args.motivo, + dataBloqueio: Date.now(), + atualizadoEm: Date.now() + }); - // Registrar no histórico de bloqueios - await ctx.db.insert("bloqueiosUsuarios", { - usuarioId: args.usuarioId, - motivo: args.motivo, - bloqueadoPor: args.bloqueadoPorId, - dataInicio: Date.now(), - ativo: true, - }); + // Registrar no histórico de bloqueios + await ctx.db.insert('bloqueiosUsuarios', { + usuarioId: args.usuarioId, + motivo: args.motivo, + bloqueadoPor: args.bloqueadoPorId, + dataInicio: Date.now(), + ativo: true + }); - // Desativar todas as sessões ativas do usuário - const sessoes = await ctx.db - .query("sessoes") - .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) - .filter((q) => q.eq(q.field("ativo"), true)) - .collect(); + // Desativar todas as sessões ativas do usuário + const sessoes = await ctx.db + .query('sessoes') + .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId)) + .filter((q) => q.eq(q.field('ativo'), true)) + .collect(); - for (const sessao of sessoes) { - await ctx.db.patch(sessao._id, { ativo: false }); - } + for (const sessao of sessoes) { + await ctx.db.patch(sessao._id, { ativo: false }); + } - // Log de atividade - await registrarAtividade( - ctx, - args.bloqueadoPorId, - "bloquear", - "usuarios", - JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }), - args.usuarioId - ); + // Log de atividade + await registrarAtividade( + ctx, + args.bloqueadoPorId, + 'bloquear', + 'usuarios', + JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }), + args.usuarioId + ); - return { sucesso: true as const }; - }, + return { sucesso: true as const }; + } }); /** * Desbloquear usuário (apenas TI_MASTER) */ export const desbloquearUsuario = mutation({ - args: { - usuarioId: v.id("usuarios"), - desbloqueadoPorId: 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 usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - return { sucesso: false as const, erro: "Usuário não autenticado" }; - } + args: { + usuarioId: v.id('usuarios'), + desbloqueadoPorId: 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 usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) { + return { sucesso: false as const, erro: 'Usuário não autenticado' }; + } - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario) { - return { sucesso: false as const, erro: "Usuário não encontrado" }; - } + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario) { + return { sucesso: false as const, erro: 'Usuário não encontrado' }; + } - // Atualizar usuário como desbloqueado - await ctx.db.patch(args.usuarioId, { - bloqueado: false, - motivoBloqueio: undefined, - dataBloqueio: undefined, - tentativasLogin: 0, - ultimaTentativaLogin: undefined, - atualizadoEm: Date.now(), - }); + // Atualizar usuário como desbloqueado + await ctx.db.patch(args.usuarioId, { + bloqueado: false, + motivoBloqueio: undefined, + dataBloqueio: undefined, + tentativasLogin: 0, + ultimaTentativaLogin: undefined, + atualizadoEm: Date.now() + }); - // Fechar bloqueios ativos - const bloqueiosAtivos = await ctx.db - .query("bloqueiosUsuarios") - .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) - .filter((q) => q.eq(q.field("ativo"), true)) - .collect(); + // Fechar bloqueios ativos + const bloqueiosAtivos = await ctx.db + .query('bloqueiosUsuarios') + .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId)) + .filter((q) => q.eq(q.field('ativo'), true)) + .collect(); - for (const bloqueio of bloqueiosAtivos) { - await ctx.db.patch(bloqueio._id, { - ativo: false, - dataFim: Date.now(), - desbloqueadoPor: args.desbloqueadoPorId, - }); - } + for (const bloqueio of bloqueiosAtivos) { + await ctx.db.patch(bloqueio._id, { + ativo: false, + dataFim: Date.now(), + desbloqueadoPor: args.desbloqueadoPorId + }); + } - // Log de atividade - await registrarAtividade( - ctx, - args.desbloqueadoPorId, - "desbloquear", - "usuarios", - JSON.stringify({ usuarioId: args.usuarioId }), - args.usuarioId - ); + // Log de atividade + await registrarAtividade( + ctx, + args.desbloqueadoPorId, + 'desbloquear', + 'usuarios', + JSON.stringify({ usuarioId: args.usuarioId }), + args.usuarioId + ); - return { sucesso: true as const }; - }, + return { sucesso: true as const }; + } }); /** @@ -919,165 +865,164 @@ export const desbloquearUsuario = mutation({ // Helper para gerar senha temporária function gerarSenhaTemporaria(): string { - const chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%"; - let senha = ""; - for (let i = 0; i < 12; i++) { - senha += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return senha; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%'; + let senha = ''; + for (let i = 0; i < 12; i++) { + senha += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return senha; } /** * Editar dados de usuário (apenas TI_MASTER) */ export const editarUsuario = mutation({ - args: { - usuarioId: v.id("usuarios"), - nome: v.optional(v.string()), - email: v.optional(v.string()), - roleId: v.optional(v.id("roles")), - setor: 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 usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - return { sucesso: false as const, erro: "Usuário não autenticado" }; - } + args: { + usuarioId: v.id('usuarios'), + nome: v.optional(v.string()), + email: v.optional(v.string()), + roleId: v.optional(v.id('roles')), + setor: 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 usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) { + return { sucesso: false as const, erro: 'Usuário não autenticado' }; + } - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario) { - return { sucesso: false as const, erro: "Usuário não encontrado" }; - } + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario) { + return { sucesso: false as const, erro: 'Usuário não encontrado' }; + } - // Verificar se email já existe (se estiver mudando) - if (args.email && args.email !== usuario.email) { - const emailExistente = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", args.email!)) - .first(); + // Verificar se email já existe (se estiver mudando) + if (args.email && args.email !== usuario.email) { + 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" }; - } - } + if (emailExistente) { + return { sucesso: false as const, erro: 'E-mail já cadastrado' }; + } + } - // Atualizar campos fornecidos - const updates: Partial> & { atualizadoEm: number } = { - atualizadoEm: Date.now(), - }; + // Atualizar campos fornecidos + const updates: Partial> & { atualizadoEm: number } = { + atualizadoEm: Date.now() + }; - if (args.nome !== undefined) updates.nome = args.nome; - if (args.email !== undefined) updates.email = args.email; - if (args.roleId !== undefined) updates.roleId = args.roleId; - if (args.setor !== undefined) updates.setor = args.setor; + if (args.nome !== undefined) updates.nome = args.nome; + if (args.email !== undefined) updates.email = args.email; + if (args.roleId !== undefined) updates.roleId = args.roleId; + if (args.setor !== undefined) updates.setor = args.setor; - await ctx.db.patch(args.usuarioId, updates); + await ctx.db.patch(args.usuarioId, updates); - // Log de atividade - await registrarAtividade( - ctx, - args.editadoPorId, - "editar", - "usuarios", - JSON.stringify(updates), - args.usuarioId - ); + // Log de atividade + await registrarAtividade( + ctx, + args.editadoPorId, + 'editar', + 'usuarios', + JSON.stringify(updates), + args.usuarioId + ); - return { sucesso: true as const }; - }, + return { sucesso: true as const }; + } }); /** * Criar/Promover usuário Admin Master (TI_MASTER - nível 0) */ export const criarAdminMaster = mutation({ - args: { - nome: v.string(), - email: v.string(), - senha: v.optional(v.string()), - }, - returns: v.union( - v.object({ - sucesso: v.literal(true), - usuarioId: v.id("usuarios"), - senhaTemporaria: v.string(), - }), - v.object({ sucesso: v.literal(false), erro: v.string() }) - ), - handler: async (ctx, args) => { - // Garantir que a role TI_MASTER exista (nível 0) - let roleTIMaster = await ctx.db - .query("roles") - .withIndex("by_nome", (q) => q.eq("nome", "ti_master")) - .first(); + args: { + nome: v.string(), + email: v.string(), + senha: v.optional(v.string()) + }, + returns: v.union( + v.object({ + sucesso: v.literal(true), + usuarioId: v.id('usuarios'), + senhaTemporaria: v.string() + }), + v.object({ sucesso: v.literal(false), erro: v.string() }) + ), + handler: async (ctx, args) => { + // Garantir que a role TI_MASTER exista (nível 0) + let roleTIMaster = await ctx.db + .query('roles') + .withIndex('by_nome', (q) => q.eq('nome', 'ti_master')) + .first(); - if (!roleTIMaster) { - const roleId = await ctx.db.insert("roles", { - nome: "ti_master", - descricao: "TI Master", - nivel: 0, - setor: "ti", - customizado: false, - editavel: false, - }); - roleTIMaster = await ctx.db.get(roleId); - } + if (!roleTIMaster) { + const roleId = await ctx.db.insert('roles', { + nome: 'ti_master', + descricao: 'TI Master', + nivel: 0, + setor: 'ti', + customizado: false, + editavel: false + }); + roleTIMaster = await ctx.db.get(roleId); + } - if (!roleTIMaster) { - return { - sucesso: false as const, - erro: "Falha ao garantir role TI Master", - }; - } + if (!roleTIMaster) { + return { + sucesso: false as const, + erro: 'Falha ao garantir role TI Master' + }; + } - const senhaTemporaria = args.senha || gerarSenhaTemporaria(); + const senhaTemporaria = args.senha || gerarSenhaTemporaria(); - const authUserId = await createAuthUser(ctx, { - nome: args.nome, - email: args.email, - password: senhaTemporaria, - }); + const authUserId = await createAuthUser(ctx, { + nome: args.nome, + email: args.email, + password: senhaTemporaria + }); - // Verificar se email já existe - const existentePorEmail = await ctx.db - .query("usuarios") - .withIndex("by_email", (q) => q.eq("email", args.email)) - .first(); - if (existentePorEmail) { - // Promove usuário existente por email - await ctx.db.patch(existentePorEmail._id, { - nome: args.nome, - roleId: roleTIMaster._id, - ativo: true, - primeiroAcesso: true, - atualizadoEm: Date.now(), - authId: authUserId, - }); - return { - sucesso: true as const, - usuarioId: existentePorEmail._id, - senhaTemporaria, - }; - } + // Verificar se email já existe + const existentePorEmail = await ctx.db + .query('usuarios') + .withIndex('by_email', (q) => q.eq('email', args.email)) + .first(); + if (existentePorEmail) { + // Promove usuário existente por email + await ctx.db.patch(existentePorEmail._id, { + nome: args.nome, + roleId: roleTIMaster._id, + ativo: true, + primeiroAcesso: true, + atualizadoEm: Date.now(), + authId: authUserId + }); + return { + sucesso: true as const, + usuarioId: existentePorEmail._id, + senhaTemporaria + }; + } - // Criar novo usuário TI Master - const usuarioId = await ctx.db.insert("usuarios", { - authId: authUserId, - nome: args.nome, - email: args.email, - roleId: roleTIMaster._id, - ativo: true, - primeiroAcesso: true, - criadoEm: Date.now(), - atualizadoEm: Date.now(), - }); + // Criar novo usuário TI Master + const usuarioId = await ctx.db.insert('usuarios', { + authId: authUserId, + nome: args.nome, + email: args.email, + roleId: roleTIMaster._id, + ativo: true, + primeiroAcesso: true, + criadoEm: Date.now(), + atualizadoEm: Date.now() + }); - return { sucesso: true as const, usuarioId, senhaTemporaria }; - }, + return { sucesso: true as const, usuarioId, senhaTemporaria }; + } });