import { query, mutation } from './_generated/server'; import { internal } from './_generated/api'; import { v } from 'convex/values'; import { getCurrentUserFunction } from './auth'; import type { Id, Doc } from './_generated/dataModel'; import type { MutationCtx, QueryCtx } from './_generated/server'; import { flowTemplateStatus, flowInstanceStatus, flowInstanceStepStatus } from './schema'; // ============================================ // HELPER FUNCTIONS // ============================================ /** * Criar notificações para todos os funcionários de um setor */ async function criarNotificacaoParaSetor( ctx: MutationCtx, setorId: Id<'setores'>, titulo: string, descricao: string ): Promise { // Buscar funcionários do setor const funcionarioSetores = await ctx.db .query('funcionarioSetores') .withIndex('by_setorId', (q) => q.eq('setorId', setorId)) .collect(); // Para cada funcionário, buscar usuário correspondente e criar notificação for (const relacao of funcionarioSetores) { const funcionario = await ctx.db.get(relacao.funcionarioId); if (!funcionario) continue; // Buscar usuário por email const usuarios = await ctx.db.query('usuarios').collect(); const usuario = usuarios.find((u: Doc<'usuarios'>) => u.email === funcionario.email); if (usuario) { await ctx.db.insert('notificacoes', { usuarioId: usuario._id, tipo: 'etapa_fluxo_concluida', titulo, descricao, lida: false, criadaEm: Date.now() }); } } } /** * Obter a etapa anterior de um passo */ async function obterEtapaAnterior( ctx: MutationCtx | QueryCtx, instanceStepId: Id<'flowInstanceSteps'> ): Promise | null> { const step = await ctx.db.get(instanceStepId); if (!step) { return null; } // Buscar todos os passos da instância const allSteps = await ctx.db .query('flowInstanceSteps') .withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', step.flowInstanceId)) .collect(); // Obter posições de cada passo const stepsWithPosition: Array<{ step: Doc<'flowInstanceSteps'>; position: number }> = []; for (const s of allSteps) { const flowStep = await ctx.db.get(s.flowStepId); if (flowStep) { stepsWithPosition.push({ step: s, position: flowStep.position }); } } // Ordenar por posição stepsWithPosition.sort((a, b) => a.position - b.position); // Encontrar posição do passo atual const currentIndex = stepsWithPosition.findIndex((s) => s.step._id === instanceStepId); // Se for o primeiro passo, não há etapa anterior if (currentIndex <= 0) { return null; } // Retornar etapa anterior return stepsWithPosition[currentIndex - 1].step; } /** * Verificar se usuário pertence a algum setor do fluxo */ async function usuarioPertenceAAlgumSetorDoFluxo( ctx: QueryCtx | MutationCtx, usuarioId: Id<'usuarios'>, flowInstanceId: Id<'flowInstances'> ): Promise { // Buscar usuário const usuario = await ctx.db.get(usuarioId); if (!usuario) { return false; } // Buscar funcionário por email const funcionarios = await ctx.db.query('funcionarios').collect(); const funcionario = funcionarios.find((f) => f.email === usuario.email); if (!funcionario) { return false; } // Buscar setores do funcionário const funcionarioSetores = await ctx.db .query('funcionarioSetores') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .collect(); const setoresDoFuncionario = new Set>(); for (const relacao of funcionarioSetores) { setoresDoFuncionario.add(relacao.setorId); } // Buscar todos os passos do fluxo const instanceSteps = await ctx.db .query('flowInstanceSteps') .withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', flowInstanceId)) .collect(); // Extrair setores do fluxo const setoresDoFluxo = new Set>(); for (const step of instanceSteps) { setoresDoFluxo.add(step.setorId); } // Verificar interseção for (const setorId of setoresDoFuncionario) { if (setoresDoFluxo.has(setorId)) { return true; } } return false; } /** * Verificar se usuário tem permissão para ver todas as instâncias de fluxo */ async function verificarPermissaoVerTodasFluxos(ctx: QueryCtx | MutationCtx): Promise { try { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'fluxos_instancias', acao: 'listar' }); return true; } catch { return false; } } // ============================================ // FLOW TEMPLATES - CRUD // ============================================ /** * Listar todos os templates de fluxo */ export const listTemplates = query({ args: { status: v.optional(flowTemplateStatus) }, returns: v.array( v.object({ _id: v.id('flowTemplates'), _creationTime: v.number(), name: v.string(), description: v.optional(v.string()), status: flowTemplateStatus, createdBy: v.id('usuarios'), createdAt: v.number(), createdByName: v.optional(v.string()), stepsCount: v.number() }) ), handler: async (ctx, args) => { let templates; if (args.status) { templates = await ctx.db .query('flowTemplates') .withIndex('by_status', (q) => q.eq('status', args.status!)) .order('desc') .collect(); } else { templates = await ctx.db.query('flowTemplates').order('desc').collect(); } const result: Array<{ _id: Id<'flowTemplates'>; _creationTime: number; name: string; description: string | undefined; status: Doc<'flowTemplates'>['status']; createdBy: Id<'usuarios'>; createdAt: number; createdByName: string | undefined; stepsCount: number; }> = []; for (const template of templates) { const creator = await ctx.db.get(template.createdBy); const steps = await ctx.db .query('flowSteps') .withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', template._id)) .collect(); result.push({ _id: template._id, _creationTime: template._creationTime, name: template.name, description: template.description, status: template.status, createdBy: template.createdBy, createdAt: template.createdAt, createdByName: creator?.nome, stepsCount: steps.length }); } return result; } }); /** * Obter um template de fluxo pelo ID */ export const getTemplate = query({ args: { id: v.id('flowTemplates') }, returns: v.union( v.object({ _id: v.id('flowTemplates'), _creationTime: v.number(), name: v.string(), description: v.optional(v.string()), status: flowTemplateStatus, createdBy: v.id('usuarios'), createdAt: v.number(), createdByName: v.optional(v.string()) }), v.null() ), handler: async (ctx, args) => { const template = await ctx.db.get(args.id); if (!template) return null; const creator = await ctx.db.get(template.createdBy); return { ...template, createdByName: creator?.nome }; } }); /** * Criar um novo template de fluxo */ export const createTemplate = mutation({ args: { name: v.string(), description: v.optional(v.string()) }, returns: v.id('flowTemplates'), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const templateId = await ctx.db.insert('flowTemplates', { name: args.name, description: args.description, status: 'draft', createdBy: usuario._id, createdAt: Date.now() }); return templateId; } }); /** * Atualizar um template de fluxo */ export const updateTemplate = mutation({ args: { id: v.id('flowTemplates'), name: v.optional(v.string()), description: v.optional(v.string()), status: v.optional(flowTemplateStatus) }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const template = await ctx.db.get(args.id); if (!template) { throw new Error('Template não encontrado'); } const updates: Partial> = {}; if (args.name !== undefined) updates.name = args.name; if (args.description !== undefined) updates.description = args.description; if (args.status !== undefined) updates.status = args.status; await ctx.db.patch(args.id, updates); return null; } }); /** * Excluir um template de fluxo */ export const deleteTemplate = mutation({ args: { id: v.id('flowTemplates') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const template = await ctx.db.get(args.id); if (!template) { throw new Error('Template não encontrado'); } // Verificar se há instâncias vinculadas const instancias = await ctx.db .query('flowInstances') .withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.id)) .first(); if (instancias) { throw new Error('Não é possível excluir um template com instâncias vinculadas'); } // Excluir todos os passos do template const steps = await ctx.db .query('flowSteps') .withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.id)) .collect(); for (const step of steps) { await ctx.db.delete(step._id); } await ctx.db.delete(args.id); return null; } }); // ============================================ // FLOW STEPS - CRUD // ============================================ /** * Listar passos de um template */ export const listStepsByTemplate = query({ args: { flowTemplateId: v.id('flowTemplates') }, returns: v.array( v.object({ _id: v.id('flowSteps'), _creationTime: v.number(), flowTemplateId: v.id('flowTemplates'), name: v.string(), description: v.optional(v.string()), position: v.number(), expectedDuration: v.number(), setorId: v.id('setores'), setorNome: v.optional(v.string()), defaultAssigneeId: v.optional(v.id('usuarios')), defaultAssigneeName: v.optional(v.string()), requiredDocuments: v.optional(v.array(v.string())) }) ), handler: async (ctx, args) => { const steps = await ctx.db .query('flowSteps') .withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.flowTemplateId)) .collect(); // Ordenar por position steps.sort((a, b) => a.position - b.position); const result: Array<{ _id: Id<'flowSteps'>; _creationTime: number; flowTemplateId: Id<'flowTemplates'>; name: string; description: string | undefined; position: number; expectedDuration: number; setorId: Id<'setores'>; setorNome: string | undefined; defaultAssigneeId: Id<'usuarios'> | undefined; defaultAssigneeName: string | undefined; requiredDocuments: string[] | undefined; }> = []; for (const step of steps) { const setor = await ctx.db.get(step.setorId); const assignee = step.defaultAssigneeId ? await ctx.db.get(step.defaultAssigneeId) : null; result.push({ _id: step._id, _creationTime: step._creationTime, flowTemplateId: step.flowTemplateId, name: step.name, description: step.description, position: step.position, expectedDuration: step.expectedDuration, setorId: step.setorId, setorNome: setor?.nome, defaultAssigneeId: step.defaultAssigneeId, defaultAssigneeName: assignee?.nome, requiredDocuments: step.requiredDocuments }); } return result; } }); /** * Criar um novo passo */ export const createStep = mutation({ args: { flowTemplateId: v.id('flowTemplates'), name: v.string(), description: v.optional(v.string()), expectedDuration: v.number(), setorId: v.id('setores'), defaultAssigneeId: v.optional(v.id('usuarios')), requiredDocuments: v.optional(v.array(v.string())) }, returns: v.id('flowSteps'), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Verificar se o template existe const template = await ctx.db.get(args.flowTemplateId); if (!template) { throw new Error('Template não encontrado'); } // Verificar se o setor existe const setor = await ctx.db.get(args.setorId); if (!setor) { throw new Error('Setor não encontrado'); } // Obter a próxima posição const existingSteps = await ctx.db .query('flowSteps') .withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.flowTemplateId)) .collect(); const maxPosition = existingSteps.reduce((max, step) => Math.max(max, step.position), 0); const stepId = await ctx.db.insert('flowSteps', { flowTemplateId: args.flowTemplateId, name: args.name, description: args.description, position: maxPosition + 1, expectedDuration: args.expectedDuration, setorId: args.setorId, defaultAssigneeId: args.defaultAssigneeId, requiredDocuments: args.requiredDocuments }); return stepId; } }); /** * Atualizar um passo */ export const updateStep = mutation({ args: { id: v.id('flowSteps'), name: v.optional(v.string()), description: v.optional(v.string()), expectedDuration: v.optional(v.number()), setorId: v.optional(v.id('setores')), defaultAssigneeId: v.optional(v.id('usuarios')), requiredDocuments: v.optional(v.array(v.string())) }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.id); if (!step) { throw new Error('Passo não encontrado'); } const updates: Partial> = {}; if (args.name !== undefined) updates.name = args.name; if (args.description !== undefined) updates.description = args.description; if (args.expectedDuration !== undefined) updates.expectedDuration = args.expectedDuration; if (args.setorId !== undefined) updates.setorId = args.setorId; if (args.defaultAssigneeId !== undefined) updates.defaultAssigneeId = args.defaultAssigneeId; if (args.requiredDocuments !== undefined) updates.requiredDocuments = args.requiredDocuments; await ctx.db.patch(args.id, updates); return null; } }); /** * Reordenar passos */ export const reorderSteps = mutation({ args: { flowTemplateId: v.id('flowTemplates'), stepIds: v.array(v.id('flowSteps')) }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Atualizar posições for (let i = 0; i < args.stepIds.length; i++) { await ctx.db.patch(args.stepIds[i], { position: i + 1 }); } return null; } }); /** * Excluir um passo */ export const deleteStep = mutation({ args: { id: v.id('flowSteps') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.id); if (!step) { throw new Error('Passo não encontrado'); } await ctx.db.delete(args.id); return null; } }); // ============================================ // FLOW INSTANCES // ============================================ /** * Verificar permissões do usuário para um fluxo */ export const verificarPermissoesFluxo = query({ args: { flowInstanceId: v.id('flowInstances') }, returns: v.object({ podeAtribuir: v.boolean(), podeEditar: v.boolean(), podeVer: v.boolean(), eCriador: v.boolean(), eGestor: v.boolean() }), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return { podeAtribuir: false, podeEditar: false, podeVer: false, eCriador: false, eGestor: false }; } const instance = await ctx.db.get(args.flowInstanceId); if (!instance) { return { podeAtribuir: false, podeEditar: false, podeVer: false, eCriador: false, eGestor: false }; } // Verificar se é gestor const eGestor = instance.managerId === usuario._id; // Verificar se é criador do template const template = await ctx.db.get(instance.flowTemplateId); const eCriador = template?.createdBy === usuario._id; // Verificar permissão de ver todas const temPermissaoVerTodas = await verificarPermissaoVerTodasFluxos(ctx); // Verificar se pertence a setor do fluxo const pertenceAoSetor = await usuarioPertenceAAlgumSetorDoFluxo( ctx, usuario._id, args.flowInstanceId ); // Pode ver se: tem permissão, é gestor, é criador, ou pertence ao setor const podeVer = temPermissaoVerTodas || eGestor || eCriador || pertenceAoSetor; // Pode editar se: é criador ou tem permissão de ver todas const podeEditar = eCriador || temPermissaoVerTodas; // Pode atribuir se: é criador, é gestor, ou tem permissão de ver todas const podeAtribuir = eCriador || eGestor || temPermissaoVerTodas; return { podeAtribuir, podeEditar, podeVer, eCriador, eGestor }; } }); /** * Listar instâncias de fluxo */ export const listInstances = query({ args: { status: v.optional(flowInstanceStatus), contratoId: v.optional(v.id('contratos')), managerId: v.optional(v.id('usuarios')) }, returns: v.array( v.object({ _id: v.id('flowInstances'), _creationTime: v.number(), flowTemplateId: v.id('flowTemplates'), templateName: v.optional(v.string()), contratoId: v.optional(v.id('contratos')), managerId: v.id('usuarios'), managerName: v.optional(v.string()), status: flowInstanceStatus, startedAt: v.number(), finishedAt: v.optional(v.number()), currentStepId: v.optional(v.id('flowInstanceSteps')), currentStepName: v.optional(v.string()), progress: v.object({ completed: v.number(), total: v.number() }) }) ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return []; } // Verificar se usuário tem permissão para ver todas const temPermissaoVerTodas = await verificarPermissaoVerTodasFluxos(ctx); let instances; if (args.status) { instances = await ctx.db .query('flowInstances') .withIndex('by_status', (q) => q.eq('status', args.status!)) .order('desc') .collect(); } else if (args.managerId) { instances = await ctx.db .query('flowInstances') .withIndex('by_managerId', (q) => q.eq('managerId', args.managerId!)) .order('desc') .collect(); } else if (args.contratoId) { instances = await ctx.db .query('flowInstances') .withIndex('by_contratoId', (q) => q.eq('contratoId', args.contratoId!)) .order('desc') .collect(); } else { instances = await ctx.db.query('flowInstances').order('desc').collect(); } const result: Array<{ _id: Id<'flowInstances'>; _creationTime: number; flowTemplateId: Id<'flowTemplates'>; templateName: string | undefined; contratoId: Id<'contratos'> | undefined; managerId: Id<'usuarios'>; managerName: string | undefined; status: Doc<'flowInstances'>['status']; startedAt: number; finishedAt: number | undefined; currentStepId: Id<'flowInstanceSteps'> | undefined; currentStepName: string | undefined; progress: { completed: number; total: number }; }> = []; for (const instance of instances) { // Se não tem permissão de ver todas, verificar se usuário pertence a algum setor do fluxo if (!temPermissaoVerTodas) { const pertenceAoSetor = await usuarioPertenceAAlgumSetorDoFluxo( ctx, usuario._id, instance._id ); if (!pertenceAoSetor) { // Também verificar se é o manager if (instance.managerId !== usuario._id) { continue; // Pular esta instância } } } const template = await ctx.db.get(instance.flowTemplateId); const manager = await ctx.db.get(instance.managerId); // Obter passos da instância const instanceSteps = await ctx.db .query('flowInstanceSteps') .withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', instance._id)) .collect(); const completedSteps = instanceSteps.filter((s) => s.status === 'completed').length; // Obter nome do passo atual let currentStepName: string | undefined; if (instance.currentStepId) { const currentStep = await ctx.db.get(instance.currentStepId); if (currentStep) { const flowStep = await ctx.db.get(currentStep.flowStepId); currentStepName = flowStep?.name; } } result.push({ _id: instance._id, _creationTime: instance._creationTime, flowTemplateId: instance.flowTemplateId, templateName: template?.name, contratoId: instance.contratoId, managerId: instance.managerId, managerName: manager?.nome, status: instance.status, startedAt: instance.startedAt, finishedAt: instance.finishedAt, currentStepId: instance.currentStepId, currentStepName, progress: { completed: completedSteps, total: instanceSteps.length } }); } return result; } }); /** * Obter uma instância de fluxo com seus passos */ export const getInstanceWithSteps = query({ args: { id: v.id('flowInstances') }, returns: v.union( v.object({ instance: v.object({ _id: v.id('flowInstances'), _creationTime: v.number(), flowTemplateId: v.id('flowTemplates'), templateName: v.optional(v.string()), contratoId: v.optional(v.id('contratos')), managerId: v.id('usuarios'), managerName: v.optional(v.string()), status: flowInstanceStatus, startedAt: v.number(), finishedAt: v.optional(v.number()), currentStepId: v.optional(v.id('flowInstanceSteps')), prazoTotalDias: v.number() }), steps: v.array( v.object({ _id: v.id('flowInstanceSteps'), _creationTime: v.number(), flowInstanceId: v.id('flowInstances'), flowStepId: v.id('flowSteps'), stepName: v.string(), stepDescription: v.optional(v.string()), setorId: v.id('setores'), setorNome: v.optional(v.string()), assignedToId: v.optional(v.id('usuarios')), assignedToName: v.optional(v.string()), status: flowInstanceStepStatus, startedAt: v.optional(v.number()), finishedAt: v.optional(v.number()), notes: v.optional(v.string()), notesUpdatedBy: v.optional(v.id('usuarios')), notesUpdatedByName: v.optional(v.string()), notesUpdatedAt: v.optional(v.number()), dueDate: v.optional(v.number()), position: v.number(), expectedDuration: v.number(), documents: v.array( v.object({ _id: v.id('flowInstanceDocuments'), name: v.string(), uploadedAt: v.number(), uploadedByName: v.optional(v.string()) }) ) }) ) }), v.null() ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return null; } const instance = await ctx.db.get(args.id); if (!instance) return null; // Verificar permissão de visualização const temPermissaoVerTodas = await verificarPermissaoVerTodasFluxos(ctx); if (!temPermissaoVerTodas) { // Verificar se usuário pertence a algum setor do fluxo ou é o manager const pertenceAoSetor = await usuarioPertenceAAlgumSetorDoFluxo( ctx, usuario._id, instance._id ); if (!pertenceAoSetor && instance.managerId !== usuario._id) { return null; // Usuário não tem acesso } } const template = await ctx.db.get(instance.flowTemplateId); const manager = await ctx.db.get(instance.managerId); // Obter passos da instância const instanceSteps = await ctx.db .query('flowInstanceSteps') .withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', args.id)) .collect(); // Mapear passos com informações adicionais const stepsWithDetails: Array<{ _id: Id<'flowInstanceSteps'>; _creationTime: number; flowInstanceId: Id<'flowInstances'>; flowStepId: Id<'flowSteps'>; stepName: string; stepDescription: string | undefined; setorId: Id<'setores'>; setorNome: string | undefined; assignedToId: Id<'usuarios'> | undefined; assignedToName: string | undefined; status: Doc<'flowInstanceSteps'>['status']; startedAt: number | undefined; finishedAt: number | undefined; notes: string | undefined; dueDate: number | undefined; position: number; expectedDuration: number; documents: Array<{ _id: Id<'flowInstanceDocuments'>; name: string; uploadedAt: number; uploadedByName: string | undefined; }>; }> = []; for (const step of instanceSteps) { const flowStep = await ctx.db.get(step.flowStepId); const setor = await ctx.db.get(step.setorId); const assignee = step.assignedToId ? await ctx.db.get(step.assignedToId) : null; // Obter documentos do passo const documents = await ctx.db .query('flowInstanceDocuments') .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', step._id)) .collect(); const docsWithUploader: Array<{ _id: Id<'flowInstanceDocuments'>; name: string; uploadedAt: number; uploadedByName: string | undefined; }> = []; for (const doc of documents) { const uploader = await ctx.db.get(doc.uploadedById); docsWithUploader.push({ _id: doc._id, name: doc.name, uploadedAt: doc.uploadedAt, uploadedByName: uploader?.nome }); } stepsWithDetails.push({ _id: step._id, _creationTime: step._creationTime, flowInstanceId: step.flowInstanceId, flowStepId: step.flowStepId, stepName: flowStep?.name ?? 'Passo desconhecido', stepDescription: flowStep?.description, setorId: step.setorId, setorNome: setor?.nome, assignedToId: step.assignedToId, assignedToName: assignee?.nome, status: step.status, startedAt: step.startedAt, finishedAt: step.finishedAt, notes: step.notes, notesUpdatedBy: step.notesUpdatedBy, notesUpdatedByName: step.notesUpdatedBy ? (await ctx.db.get(step.notesUpdatedBy))?.nome : undefined, notesUpdatedAt: step.notesUpdatedAt, dueDate: step.dueDate, position: flowStep?.position ?? 0, expectedDuration: flowStep?.expectedDuration ?? 0, documents: docsWithUploader }); } // Ordenar por position stepsWithDetails.sort((a, b) => a.position - b.position); // Calcular prazo total do fluxo (soma de expectedDuration de todas as etapas) const prazoTotalDias = stepsWithDetails.reduce((sum, step) => sum + step.expectedDuration, 0); return { instance: { _id: instance._id, _creationTime: instance._creationTime, flowTemplateId: instance.flowTemplateId, templateName: template?.name, contratoId: instance.contratoId, managerId: instance.managerId, managerName: manager?.nome, status: instance.status, startedAt: instance.startedAt, finishedAt: instance.finishedAt, currentStepId: instance.currentStepId, prazoTotalDias }, steps: stepsWithDetails as Array<{ _id: Id<'flowInstanceSteps'>; _creationTime: number; flowInstanceId: Id<'flowInstances'>; flowStepId: Id<'flowSteps'>; stepName: string; stepDescription: string | undefined; setorId: Id<'setores'>; setorNome: string | undefined; assignedToId: Id<'usuarios'> | undefined; assignedToName: string | undefined; status: Doc<'flowInstanceSteps'>['status']; startedAt: number | undefined; finishedAt: number | undefined; notes: string | undefined; notesUpdatedBy: Id<'usuarios'> | undefined; notesUpdatedByName: string | undefined; notesUpdatedAt: number | undefined; dueDate: number | undefined; position: number; expectedDuration: number; documents: Array<{ _id: Id<'flowInstanceDocuments'>; name: string; uploadedAt: number; uploadedByName: string | undefined; }>; }> }; } }); /** * Instanciar um fluxo */ export const instantiateFlow = mutation({ args: { flowTemplateId: v.id('flowTemplates'), contratoId: v.optional(v.id('contratos')), managerId: v.id('usuarios') }, returns: v.id('flowInstances'), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Verificar se o template existe e está publicado const template = await ctx.db.get(args.flowTemplateId); if (!template) { throw new Error('Template não encontrado'); } if (template.status !== 'published') { throw new Error('Apenas templates publicados podem ser instanciados'); } // Obter passos do template const templateSteps = await ctx.db .query('flowSteps') .withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.flowTemplateId)) .collect(); if (templateSteps.length === 0) { throw new Error('O template não possui passos definidos'); } // Ordenar por position templateSteps.sort((a, b) => a.position - b.position); const now = Date.now(); // Criar a instância const instanceId = await ctx.db.insert('flowInstances', { flowTemplateId: args.flowTemplateId, contratoId: args.contratoId, managerId: args.managerId, status: 'active', startedAt: now }); // Criar os passos da instância let firstStepId: Id<'flowInstanceSteps'> | undefined; let cumulativeDays = 0; for (let i = 0; i < templateSteps.length; i++) { const step = templateSteps[i]; const dueDate = now + cumulativeDays * 24 * 60 * 60 * 1000 + step.expectedDuration * 24 * 60 * 60 * 1000; cumulativeDays += step.expectedDuration; const instanceStepId = await ctx.db.insert('flowInstanceSteps', { flowInstanceId: instanceId, flowStepId: step._id, setorId: step.setorId, assignedToId: step.defaultAssigneeId, status: i === 0 ? 'pending' : 'pending', dueDate }); if (i === 0) { firstStepId = instanceStepId; } } // Atualizar o currentStepId da instância if (firstStepId) { await ctx.db.patch(instanceId, { currentStepId: firstStepId }); } // Notificar gestor sobre criação do fluxo if (args.managerId) { await ctx.db.insert('notificacoes', { usuarioId: args.managerId, tipo: 'etapa_fluxo_concluida', titulo: 'Novo Fluxo Criado', descricao: `Um novo fluxo "${template?.name ?? 'Fluxo'}" foi criado e você foi designado como gestor.`, lida: false, criadaEm: Date.now() }); } return instanceId; } }); /** * Completar um passo da instância */ export const completeStep = mutation({ args: { instanceStepId: v.id('flowInstanceSteps'), notes: v.optional(v.string()) }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.instanceStepId); if (!step) { throw new Error('Passo não encontrado'); } if (step.status === 'completed') { throw new Error('Este passo já foi concluído'); } const instance = await ctx.db.get(step.flowInstanceId); if (!instance) { throw new Error('Instância não encontrada'); } if (instance.status !== 'active') { throw new Error('Não é possível completar passos de uma instância inativa'); } const now = Date.now(); // Marcar passo como completado await ctx.db.patch(args.instanceStepId, { status: 'completed', finishedAt: now, notes: args.notes }); // Obter todos os passos da instância const allSteps = await ctx.db .query('flowInstanceSteps') .withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', step.flowInstanceId)) .collect(); // Encontrar o próximo passo pendente const flowSteps: Array<{ stepId: Id<'flowInstanceSteps'>; position: number }> = []; for (const s of allSteps) { const flowStep = await ctx.db.get(s.flowStepId); if (flowStep) { flowSteps.push({ stepId: s._id, position: flowStep.position }); } } flowSteps.sort((a, b) => a.position - b.position); const currentPosition = flowSteps.findIndex((s) => s.stepId === args.instanceStepId); const nextStep = flowSteps[currentPosition + 1]; // Obter informações do template e do passo para as notificações const template = await ctx.db.get(instance.flowTemplateId); const flowStep = await ctx.db.get(step.flowStepId); const setorAtual = await ctx.db.get(step.setorId); // Criar notificações para o setor do passo concluído if (setorAtual && flowStep) { const tituloSetorAtual = 'Etapa de Fluxo Concluída'; const descricaoSetorAtual = `A etapa "${flowStep.name}" do fluxo "${template?.name ?? 'Fluxo'}" foi concluída.`; await criarNotificacaoParaSetor(ctx, step.setorId, tituloSetorAtual, descricaoSetorAtual); } // Notificar gestor sobre conclusão da etapa if (instance.managerId) { await ctx.db.insert('notificacoes', { usuarioId: instance.managerId, tipo: 'etapa_fluxo_concluida', titulo: 'Etapa de Fluxo Concluída', descricao: `A etapa "${flowStep?.name ?? 'Etapa'}" do fluxo "${template?.name ?? 'Fluxo'}" foi concluída.`, lida: false, criadaEm: Date.now() }); } if (nextStep) { // Atualizar currentStepId para o próximo passo await ctx.db.patch(step.flowInstanceId, { currentStepId: nextStep.stepId }); // Criar notificações para o setor do próximo passo const nextStepData = await ctx.db.get(nextStep.stepId); if (nextStepData) { const nextFlowStep = await ctx.db.get(nextStepData.flowStepId); const nextSetor = await ctx.db.get(nextStepData.setorId); if (nextSetor && nextFlowStep) { const tituloProximoSetor = 'Nova Etapa de Fluxo Disponível'; const descricaoProximoSetor = `A etapa "${nextFlowStep.name}" do fluxo "${template?.name ?? 'Fluxo'}" está pronta para ser iniciada.`; await criarNotificacaoParaSetor(ctx, nextStepData.setorId, tituloProximoSetor, descricaoProximoSetor); } } } else { // Todos os passos concluídos, marcar instância como completada await ctx.db.patch(step.flowInstanceId, { status: 'completed', finishedAt: now, currentStepId: undefined }); // Notificar gestor sobre conclusão do fluxo if (instance.managerId) { await ctx.db.insert('notificacoes', { usuarioId: instance.managerId, tipo: 'etapa_fluxo_concluida', titulo: 'Fluxo Concluído', descricao: `O fluxo "${template?.name ?? 'Fluxo'}" foi concluído com sucesso.`, lida: false, criadaEm: Date.now() }); } } return null; } }); /** * Atualizar status de um passo */ export const updateStepStatus = mutation({ args: { instanceStepId: v.id('flowInstanceSteps'), status: flowInstanceStepStatus }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.instanceStepId); if (!step) { throw new Error('Passo não encontrado'); } const updates: Partial> = { status: args.status }; if (args.status === 'in_progress' && !step.startedAt) { updates.startedAt = Date.now(); // Notificar gestor sobre início da etapa const instance = await ctx.db.get(step.flowInstanceId); if (instance?.managerId) { const flowStep = await ctx.db.get(step.flowStepId); const template = await ctx.db.get(instance.flowTemplateId); await ctx.db.insert('notificacoes', { usuarioId: instance.managerId, tipo: 'etapa_fluxo_concluida', titulo: 'Etapa de Fluxo Iniciada', descricao: `A etapa "${flowStep?.name ?? 'Etapa'}" do fluxo "${template?.name ?? 'Fluxo'}" foi iniciada.`, lida: false, criadaEm: Date.now() }); } } await ctx.db.patch(args.instanceStepId, updates); return null; } }); /** * Alterar gestor do fluxo */ export const alterarGestorFluxo = mutation({ args: { flowInstanceId: v.id('flowInstances'), novoManagerId: v.id('usuarios') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const instance = await ctx.db.get(args.flowInstanceId); if (!instance) { throw new Error('Fluxo não encontrado'); } // Verificar se usuário atual é gestor ou tem permissão const eGestor = instance.managerId === usuario._id; const temPermissao = await verificarPermissaoVerTodasFluxos(ctx); const template = await ctx.db.get(instance.flowTemplateId); const eCriador = template?.createdBy === usuario._id; if (!eGestor && !temPermissao && !eCriador) { throw new Error('Somente o gestor atual, criador do template ou usuário com permissão pode alterar o gestor'); } // Verificar se novo gestor existe const novoGestor = await ctx.db.get(args.novoManagerId); if (!novoGestor) { throw new Error('Usuário não encontrado'); } // Atualizar gestor await ctx.db.patch(args.flowInstanceId, { managerId: args.novoManagerId }); // Criar notificação para novo gestor await ctx.db.insert('notificacoes', { usuarioId: args.novoManagerId, tipo: 'etapa_fluxo_concluida', titulo: 'Você foi designado como gestor de um fluxo', descricao: `Você foi designado como gestor do fluxo "${template?.name ?? 'Fluxo'}"`, lida: false, criadaEm: Date.now() }); return null; } }); /** * Reatribuir responsável de um passo */ export const reassignStep = mutation({ args: { instanceStepId: v.id('flowInstanceSteps'), assignedToId: v.id('usuarios') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.instanceStepId); if (!step) { throw new Error('Passo não encontrado'); } // Verificar se o usuário existe const assignee = await ctx.db.get(args.assignedToId); if (!assignee) { throw new Error('Usuário não encontrado'); } // Verificar se o usuário atual pode atribuir esta etapa const instance = await ctx.db.get(step.flowInstanceId); if (!instance) { throw new Error('Instância não encontrada'); } // Verificar se é criador do template const template = await ctx.db.get(instance.flowTemplateId); const eCriador = template?.createdBy === usuario._id; // Se for criador, permitir atribuição if (!eCriador) { // Se não for criador, verificar regra normal const etapaAnterior = await obterEtapaAnterior(ctx, args.instanceStepId); if (etapaAnterior) { // Se há etapa anterior, verificar se o usuário atual é a pessoa atribuída if (etapaAnterior.assignedToId) { if (etapaAnterior.assignedToId !== usuario._id) { throw new Error('Somente a pessoa da etapa anterior pode atribuir esta etapa'); } } // Se a etapa anterior não tem atribuição, permitir (vai para o setor) } else { // Se não há etapa anterior (primeiro passo), verificar se é o manager // Se não for o manager, verificar permissão if (instance.managerId !== usuario._id) { const temPermissao = await verificarPermissaoVerTodasFluxos(ctx); if (!temPermissao) { throw new Error('Somente o gerente do fluxo ou pessoa da etapa anterior pode atribuir esta etapa'); } } } } // Verificar se o funcionário atribuído pertence ao setor do passo const funcionarioSetores = await ctx.db .query('funcionarioSetores') .withIndex('by_setorId', (q) => q.eq('setorId', step.setorId)) .collect(); // Buscar funcionários vinculados a este setor const funcionariosDoSetor = []; for (const relacao of funcionarioSetores) { const funcionario = await ctx.db.get(relacao.funcionarioId); if (funcionario) { funcionariosDoSetor.push(funcionario); } } // Verificar se o usuário atribuído corresponde a um funcionário do setor const funcionarioDoUsuario = funcionariosDoSetor.find( (f) => f.email === assignee.email ); if (!funcionarioDoUsuario) { throw new Error('O funcionário atribuído não pertence ao setor deste passo'); } await ctx.db.patch(args.instanceStepId, { assignedToId: args.assignedToId }); return null; } }); /** * Atualizar notas de um passo */ export const updateStepNotes = mutation({ args: { instanceStepId: v.id('flowInstanceSteps'), notes: v.string() }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.instanceStepId); if (!step) { throw new Error('Passo não encontrado'); } await ctx.db.patch(args.instanceStepId, { notes: args.notes, notesUpdatedBy: usuario._id, notesUpdatedAt: Date.now() }); return null; } }); /** * Cancelar uma instância de fluxo */ export const cancelInstance = mutation({ args: { id: v.id('flowInstances') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const instance = await ctx.db.get(args.id); if (!instance) { throw new Error('Instância não encontrada'); } if (instance.status !== 'active') { throw new Error('Apenas instâncias ativas podem ser canceladas'); } await ctx.db.patch(args.id, { status: 'cancelled', finishedAt: Date.now() }); return null; } }); // ============================================ // SUB-ETAPAS // ============================================ /** * Listar sub-etapas de um step */ export const listarSubEtapas = query({ args: { flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')) }, returns: v.array( v.object({ _id: v.id('flowSubSteps'), _creationTime: v.number(), flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), name: v.string(), description: v.optional(v.string()), status: v.union( v.literal('pending'), v.literal('in_progress'), v.literal('completed'), v.literal('blocked') ), position: v.number(), createdBy: v.id('usuarios'), createdByName: v.optional(v.string()) }) ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return []; } let subEtapas; if (args.flowStepId) { subEtapas = await ctx.db .query('flowSubSteps') .withIndex('by_flowStepId', (q) => q.eq('flowStepId', args.flowStepId)) .collect(); } else if (args.flowInstanceStepId) { subEtapas = await ctx.db .query('flowSubSteps') .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) .collect(); } else { return []; } // Ordenar por position subEtapas.sort((a, b) => a.position - b.position); // Adicionar nome do criador const subEtapasComCriador = await Promise.all( subEtapas.map(async (subEtapa) => { const criador = await ctx.db.get(subEtapa.createdBy); return { _id: subEtapa._id, _creationTime: subEtapa._creationTime, flowStepId: subEtapa.flowStepId, flowInstanceStepId: subEtapa.flowInstanceStepId, name: subEtapa.name, description: subEtapa.description, status: subEtapa.status, position: subEtapa.position, createdBy: subEtapa.createdBy, createdByName: criador?.nome }; }) ); return subEtapasComCriador; } }); /** * Criar sub-etapa */ export const criarSubEtapa = mutation({ args: { flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), name: v.string(), description: v.optional(v.string()) }, returns: v.id('flowSubSteps'), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } if (!args.flowStepId && !args.flowInstanceStepId) { throw new Error('É necessário fornecer flowStepId ou flowInstanceStepId'); } // Verificar se o step existe if (args.flowStepId) { const flowStep = await ctx.db.get(args.flowStepId); if (!flowStep) { throw new Error('Passo do template não encontrado'); } } if (args.flowInstanceStepId) { const flowInstanceStep = await ctx.db.get(args.flowInstanceStepId); if (!flowInstanceStep) { throw new Error('Passo da instância não encontrado'); } } // Obter posição máxima let maxPosition = 0; if (args.flowStepId) { const existingSubEtapas = await ctx.db .query('flowSubSteps') .withIndex('by_flowStepId', (q) => q.eq('flowStepId', args.flowStepId)) .collect(); if (existingSubEtapas.length > 0) { maxPosition = Math.max(...existingSubEtapas.map((s) => s.position)); } } else if (args.flowInstanceStepId) { const existingSubEtapas = await ctx.db .query('flowSubSteps') .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) .collect(); if (existingSubEtapas.length > 0) { maxPosition = Math.max(...existingSubEtapas.map((s) => s.position)); } } const subEtapaId = await ctx.db.insert('flowSubSteps', { flowStepId: args.flowStepId, flowInstanceStepId: args.flowInstanceStepId, name: args.name, description: args.description, status: 'pending', position: maxPosition + 1, createdBy: usuario._id, createdAt: Date.now() }); return subEtapaId; } }); /** * Atualizar sub-etapa */ export const atualizarSubEtapa = mutation({ args: { subEtapaId: v.id('flowSubSteps'), name: v.optional(v.string()), description: v.optional(v.string()), status: v.optional( v.union( v.literal('pending'), v.literal('in_progress'), v.literal('completed'), v.literal('blocked') ) ) }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const subEtapa = await ctx.db.get(args.subEtapaId); if (!subEtapa) { throw new Error('Sub-etapa não encontrada'); } const updates: Partial> = {}; if (args.name !== undefined) { updates.name = args.name; } if (args.description !== undefined) { updates.description = args.description; } if (args.status !== undefined) { updates.status = args.status; } await ctx.db.patch(args.subEtapaId, updates); return null; } }); /** * Deletar sub-etapa */ export const deletarSubEtapa = mutation({ args: { subEtapaId: v.id('flowSubSteps') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const subEtapa = await ctx.db.get(args.subEtapaId); if (!subEtapa) { throw new Error('Sub-etapa não encontrada'); } await ctx.db.delete(args.subEtapaId); return null; } }); /** * Reordenar sub-etapas */ export const reordenarSubEtapas = mutation({ args: { subEtapaIds: v.array(v.id('flowSubSteps')) }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // Atualizar posições for (let i = 0; i < args.subEtapaIds.length; i++) { await ctx.db.patch(args.subEtapaIds[i], { position: i + 1 }); } return null; } }); // ============================================ // NOTAS // ============================================ /** * Listar notas de um step ou sub-etapa */ export const listarNotas = query({ args: { flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), flowSubStepId: v.optional(v.id('flowSubSteps')) }, returns: v.array( v.object({ _id: v.id('flowStepNotes'), _creationTime: v.number(), flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), flowSubStepId: v.optional(v.id('flowSubSteps')), texto: v.string(), criadoPor: v.id('usuarios'), criadoPorNome: v.optional(v.string()), criadoEm: v.number(), arquivos: v.array( v.object({ storageId: v.id('_storage'), name: v.string() }) ) }) ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return []; } let notas; if (args.flowStepId) { notas = await ctx.db .query('flowStepNotes') .withIndex('by_flowStepId', (q) => q.eq('flowStepId', args.flowStepId)) .collect(); } else if (args.flowInstanceStepId) { notas = await ctx.db .query('flowStepNotes') .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) .collect(); } else if (args.flowSubStepId) { notas = await ctx.db .query('flowStepNotes') .withIndex('by_flowSubStepId', (q) => q.eq('flowSubStepId', args.flowSubStepId)) .collect(); } else { return []; } // Ordenar por data de criação (mais recente primeiro) notas.sort((a, b) => b.criadoEm - a.criadoEm); // Adicionar nome do criador e informações dos arquivos const notasComDetalhes = await Promise.all( notas.map(async (nota) => { const criador = await ctx.db.get(nota.criadoPor); // Obter informações dos arquivos const arquivosComNome = await Promise.all( nota.arquivos.map(async (storageId) => { // Buscar documento que referencia este storageId // Como não temos uma tabela direta, vamos buscar nos flowInstanceDocuments const documentos = await ctx.db .query('flowInstanceDocuments') .collect(); const documento = documentos.find((d) => d.storageId === storageId); return { storageId, name: documento?.name ?? 'Arquivo' }; }) ); return { _id: nota._id, _creationTime: nota._creationTime, flowStepId: nota.flowStepId, flowInstanceStepId: nota.flowInstanceStepId, flowSubStepId: nota.flowSubStepId, texto: nota.texto, criadoPor: nota.criadoPor, criadoPorNome: criador?.nome, criadoEm: nota.criadoEm, arquivos: arquivosComNome }; }) ); return notasComDetalhes; } }); /** * Adicionar nota */ export const adicionarNota = mutation({ args: { flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), flowSubStepId: v.optional(v.id('flowSubSteps')), texto: v.string(), arquivos: v.optional(v.array(v.id('_storage'))) }, returns: v.id('flowStepNotes'), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } if (!args.flowStepId && !args.flowInstanceStepId && !args.flowSubStepId) { throw new Error('É necessário fornecer flowStepId, flowInstanceStepId ou flowSubStepId'); } const notaId = await ctx.db.insert('flowStepNotes', { flowStepId: args.flowStepId, flowInstanceStepId: args.flowInstanceStepId, flowSubStepId: args.flowSubStepId, texto: args.texto, criadoPor: usuario._id, criadoEm: Date.now(), arquivos: args.arquivos ?? [] }); return notaId; } }); /** * Atualizar nota */ export const atualizarNota = mutation({ args: { notaId: v.id('flowStepNotes'), texto: v.string() }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const nota = await ctx.db.get(args.notaId); if (!nota) { throw new Error('Nota não encontrada'); } // Verificar se o usuário é o criador if (nota.criadoPor !== usuario._id) { throw new Error('Somente o criador da nota pode editá-la'); } await ctx.db.patch(args.notaId, { texto: args.texto }); return null; } }); /** * Deletar nota */ export const deletarNota = mutation({ args: { notaId: v.id('flowStepNotes') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const nota = await ctx.db.get(args.notaId); if (!nota) { throw new Error('Nota não encontrada'); } // Verificar se o usuário é o criador if (nota.criadoPor !== usuario._id) { throw new Error('Somente o criador da nota pode deletá-la'); } await ctx.db.delete(args.notaId); return null; } }); /** * Adicionar arquivo a uma nota */ export const adicionarArquivoNota = mutation({ args: { notaId: v.id('flowStepNotes'), storageId: v.id('_storage') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const nota = await ctx.db.get(args.notaId); if (!nota) { throw new Error('Nota não encontrada'); } // Verificar se o usuário é o criador if (nota.criadoPor !== usuario._id) { throw new Error('Somente o criador da nota pode adicionar arquivos'); } // Adicionar arquivo à lista const novosArquivos = [...nota.arquivos, args.storageId]; await ctx.db.patch(args.notaId, { arquivos: novosArquivos }); return null; } }); /** * Remover arquivo de uma nota */ export const removerArquivoNota = mutation({ args: { notaId: v.id('flowStepNotes'), storageId: v.id('_storage') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const nota = await ctx.db.get(args.notaId); if (!nota) { throw new Error('Nota não encontrada'); } // Verificar se o usuário é o criador if (nota.criadoPor !== usuario._id) { throw new Error('Somente o criador da nota pode remover arquivos'); } // Remover arquivo da lista const novosArquivos = nota.arquivos.filter((id) => id !== args.storageId); await ctx.db.patch(args.notaId, { arquivos: novosArquivos }); return null; } }); // ============================================ // FLOW DOCUMENTS // ============================================ /** * Listar documentos de um passo */ export const listDocumentsByStep = query({ args: { flowInstanceStepId: v.id('flowInstanceSteps') }, returns: v.array( v.object({ _id: v.id('flowInstanceDocuments'), _creationTime: v.number(), flowInstanceStepId: v.id('flowInstanceSteps'), uploadedById: v.id('usuarios'), uploadedByName: v.optional(v.string()), storageId: v.id('_storage'), name: v.string(), uploadedAt: v.number(), url: v.optional(v.string()) }) ), handler: async (ctx, args) => { const documents = await ctx.db .query('flowInstanceDocuments') .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) .collect(); const result: Array<{ _id: Id<'flowInstanceDocuments'>; _creationTime: number; flowInstanceStepId: Id<'flowInstanceSteps'>; uploadedById: Id<'usuarios'>; uploadedByName: string | undefined; storageId: Id<'_storage'>; name: string; uploadedAt: number; url: string | undefined; }> = []; for (const doc of documents) { const uploader = await ctx.db.get(doc.uploadedById); const url = await ctx.storage.getUrl(doc.storageId); result.push({ ...doc, uploadedByName: uploader?.nome, url: url ?? undefined }); } return result; } }); /** * Registrar upload de documento */ export const registerDocument = mutation({ args: { flowInstanceStepId: v.id('flowInstanceSteps'), storageId: v.id('_storage'), name: v.string() }, returns: v.id('flowInstanceDocuments'), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const step = await ctx.db.get(args.flowInstanceStepId); if (!step) { throw new Error('Passo não encontrado'); } const documentId = await ctx.db.insert('flowInstanceDocuments', { flowInstanceStepId: args.flowInstanceStepId, uploadedById: usuario._id, storageId: args.storageId, name: args.name, uploadedAt: Date.now() }); return documentId; } }); /** * Excluir documento */ export const deleteDocument = mutation({ args: { id: v.id('flowInstanceDocuments') }, returns: v.null(), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } const document = await ctx.db.get(args.id); if (!document) { throw new Error('Documento não encontrado'); } // Excluir o arquivo do storage await ctx.storage.delete(document.storageId); // Excluir o registro do documento await ctx.db.delete(args.id); return null; } }); /** * Gerar URL de upload */ export const generateUploadUrl = mutation({ args: {}, returns: v.string(), handler: async (ctx) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } return await ctx.storage.generateUploadUrl(); } }); /** * Obter usuários que podem ser atribuídos a um passo (baseado no setor) */ export const getUsuariosBySetorForAssignment = query({ args: { setorId: v.id('setores') }, returns: v.array( v.object({ _id: v.id('usuarios'), _creationTime: v.number(), nome: v.string(), email: v.string() }) ), handler: async (ctx, args) => { // Buscar funcionários do setor const funcionarioSetores = await ctx.db .query('funcionarioSetores') .withIndex('by_setorId', (q) => q.eq('setorId', args.setorId)) .collect(); const usuarios: Array<{ _id: Id<'usuarios'>; _creationTime: number; nome: string; email: string; }> = []; // Para cada funcionário do setor, buscar o usuário correspondente for (const relacao of funcionarioSetores) { const funcionario = await ctx.db.get(relacao.funcionarioId); if (!funcionario) continue; // Buscar usuário por email const usuariosList = await ctx.db.query('usuarios').collect(); const usuario = usuariosList.find((u) => u.email === funcionario.email); if (usuario && !usuarios.find((u) => u._id === usuario._id)) { usuarios.push({ _id: usuario._id, _creationTime: usuario._creationTime, nome: usuario.nome, email: usuario.email }); } } return usuarios; } });