Files
sgse-app/packages/backend/convex/flows.ts
killer-cf f8d9c17f63 feat: add Svelte DnD action and enhance flow management features
- Added "svelte-dnd-action" dependency to facilitate drag-and-drop functionality.
- Introduced new "Fluxos de Trabalho" section in the dashboard for managing workflow templates and instances.
- Updated permission handling for sectors and flow templates in the backend.
- Enhanced schema definitions to support flow templates, instances, and associated documents.
- Improved UI components to include new workflow management features across various dashboard pages.
2025-11-25 00:21:35 -03:00

1103 lines
28 KiB
TypeScript

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<Doc<'flowTemplates'>> = {};
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<Doc<'flowSteps'>> = {};
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<Doc<'flowInstanceSteps'>> = { 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();
}
});