Files
sgse-app/packages/backend/convex/flows.ts
killer-cf 6128c20da0 feat: implement sub-steps management in workflow editor
- Added functionality for creating, updating, and deleting sub-steps within the workflow editor.
- Introduced a modal for adding new sub-steps, including fields for name and description.
- Enhanced the UI to display sub-steps with status indicators and options for updating their status.
- Updated navigation links to reflect changes in the workflow structure, ensuring consistency across the application.
- Refactored related components to accommodate the new sub-steps feature, improving overall workflow management.
2025-11-25 14:14:43 -03:00

2158 lines
57 KiB
TypeScript

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<void> {
// 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<Doc<'flowInstanceSteps'> | 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<boolean> {
// 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<Id<'setores'>>();
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<Id<'setores'>>();
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<boolean> {
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<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
// ============================================
/**
* 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<Doc<'flowInstanceSteps'>> = { 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<Doc<'flowSubSteps'>> = {};
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;
}
});