2175 lines
58 KiB
TypeScript
2175 lines
58 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 { flowInstanceStatus, flowInstanceStepStatus, flowTemplateStatus } from './tables/flows';
|
|
|
|
// ============================================
|
|
// 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;
|
|
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;
|
|
}>;
|
|
}> = [];
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
const notesUpdater = step.notesUpdatedBy ? await ctx.db.get(step.notesUpdatedBy) : null;
|
|
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: notesUpdater?.nome,
|
|
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;
|
|
}
|
|
});
|