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()}
-
})
- {:else}
-
-
- {/if}
-
-
-
+ onmousedown={handleMouseDown}
+ role="button"
+ tabindex="0"
+ aria-label="Arrastar janela do chat"
+ >
+
+
+
+
+
+
+
+ {#if avatarUrlDoUsuario()}
+
})
+ {:else}
+
+
+ {/if}
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
- {#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 };
+ }
});