import { query, mutation } from './_generated/server'; import { v } from 'convex/values'; import { getCurrentUserFunction } from './auth'; import type { Id, Doc } from './_generated/dataModel'; import { flowTemplateStatus, flowInstanceStatus, flowInstanceStepStatus } from './schema'; // ============================================ // 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 // ============================================ /** * Listar instâncias de fluxo */ export const listInstances = query({ args: { status: v.optional(flowInstanceStatus), targetType: v.optional(v.string()), 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()), targetType: v.string(), targetId: v.string(), 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) => { 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 { instances = await ctx.db.query('flowInstances').order('desc').collect(); } // Filtrar por targetType se especificado const filteredInstances = args.targetType ? instances.filter((i) => i.targetType === args.targetType) : instances; const result: Array<{ _id: Id<'flowInstances'>; _creationTime: number; flowTemplateId: Id<'flowTemplates'>; templateName: string | undefined; targetType: string; targetId: string; 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 filteredInstances) { 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, targetType: instance.targetType, targetId: instance.targetId, 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()), targetType: v.string(), targetId: v.string(), 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')) }), 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()), dueDate: v.optional(v.number()), position: 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 instance = await ctx.db.get(args.id); if (!instance) return null; 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; 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, dueDate: step.dueDate, position: flowStep?.position ?? 0, documents: docsWithUploader }); } // Ordenar por position stepsWithDetails.sort((a, b) => a.position - b.position); return { instance: { ...instance, templateName: template?.name, managerName: manager?.nome }, steps: stepsWithDetails }; } }); /** * Instanciar um fluxo */ export const instantiateFlow = mutation({ args: { flowTemplateId: v.id('flowTemplates'), targetType: v.string(), targetId: v.string(), 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, targetType: args.targetType, targetId: args.targetId, 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 }); } 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]; if (nextStep) { // Atualizar currentStepId para o próximo passo await ctx.db.patch(step.flowInstanceId, { currentStepId: nextStep.stepId }); } else { // Todos os passos concluídos, marcar instância como completada await ctx.db.patch(step.flowInstanceId, { status: 'completed', finishedAt: now, currentStepId: undefined }); } 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(); } await ctx.db.patch(args.instanceStepId, updates); 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'); } 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 }); 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; } }); // ============================================ // 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(); } });