- Added "svelte-dnd-action" dependency to facilitate drag-and-drop functionality. - Introduced new "Fluxos de Trabalho" section in the dashboard for managing workflow templates and instances. - Updated permission handling for sectors and flow templates in the backend. - Enhanced schema definitions to support flow templates, instances, and associated documents. - Improved UI components to include new workflow management features across various dashboard pages.
1103 lines
28 KiB
TypeScript
1103 lines
28 KiB
TypeScript
import { query, mutation } from './_generated/server';
|
|
import { v } from 'convex/values';
|
|
import { getCurrentUserFunction } from './auth';
|
|
import type { Id, Doc } from './_generated/dataModel';
|
|
import { flowTemplateStatus, flowInstanceStatus, flowInstanceStepStatus } from './schema';
|
|
|
|
// ============================================
|
|
// FLOW TEMPLATES - CRUD
|
|
// ============================================
|
|
|
|
/**
|
|
* Listar todos os templates de fluxo
|
|
*/
|
|
export const listTemplates = query({
|
|
args: {
|
|
status: v.optional(flowTemplateStatus)
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('flowTemplates'),
|
|
_creationTime: v.number(),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
status: flowTemplateStatus,
|
|
createdBy: v.id('usuarios'),
|
|
createdAt: v.number(),
|
|
createdByName: v.optional(v.string()),
|
|
stepsCount: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
let templates;
|
|
|
|
if (args.status) {
|
|
templates = await ctx.db
|
|
.query('flowTemplates')
|
|
.withIndex('by_status', (q) => q.eq('status', args.status!))
|
|
.order('desc')
|
|
.collect();
|
|
} else {
|
|
templates = await ctx.db.query('flowTemplates').order('desc').collect();
|
|
}
|
|
|
|
const result: Array<{
|
|
_id: Id<'flowTemplates'>;
|
|
_creationTime: number;
|
|
name: string;
|
|
description: string | undefined;
|
|
status: Doc<'flowTemplates'>['status'];
|
|
createdBy: Id<'usuarios'>;
|
|
createdAt: number;
|
|
createdByName: string | undefined;
|
|
stepsCount: number;
|
|
}> = [];
|
|
|
|
for (const template of templates) {
|
|
const creator = await ctx.db.get(template.createdBy);
|
|
const steps = await ctx.db
|
|
.query('flowSteps')
|
|
.withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', template._id))
|
|
.collect();
|
|
|
|
result.push({
|
|
_id: template._id,
|
|
_creationTime: template._creationTime,
|
|
name: template.name,
|
|
description: template.description,
|
|
status: template.status,
|
|
createdBy: template.createdBy,
|
|
createdAt: template.createdAt,
|
|
createdByName: creator?.nome,
|
|
stepsCount: steps.length
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Obter um template de fluxo pelo ID
|
|
*/
|
|
export const getTemplate = query({
|
|
args: { id: v.id('flowTemplates') },
|
|
returns: v.union(
|
|
v.object({
|
|
_id: v.id('flowTemplates'),
|
|
_creationTime: v.number(),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
status: flowTemplateStatus,
|
|
createdBy: v.id('usuarios'),
|
|
createdAt: v.number(),
|
|
createdByName: v.optional(v.string())
|
|
}),
|
|
v.null()
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const template = await ctx.db.get(args.id);
|
|
if (!template) return null;
|
|
|
|
const creator = await ctx.db.get(template.createdBy);
|
|
return {
|
|
...template,
|
|
createdByName: creator?.nome
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Criar um novo template de fluxo
|
|
*/
|
|
export const createTemplate = mutation({
|
|
args: {
|
|
name: v.string(),
|
|
description: v.optional(v.string())
|
|
},
|
|
returns: v.id('flowTemplates'),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const templateId = await ctx.db.insert('flowTemplates', {
|
|
name: args.name,
|
|
description: args.description,
|
|
status: 'draft',
|
|
createdBy: usuario._id,
|
|
createdAt: Date.now()
|
|
});
|
|
|
|
return templateId;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Atualizar um template de fluxo
|
|
*/
|
|
export const updateTemplate = mutation({
|
|
args: {
|
|
id: v.id('flowTemplates'),
|
|
name: v.optional(v.string()),
|
|
description: v.optional(v.string()),
|
|
status: v.optional(flowTemplateStatus)
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const template = await ctx.db.get(args.id);
|
|
if (!template) {
|
|
throw new Error('Template não encontrado');
|
|
}
|
|
|
|
const updates: Partial<Doc<'flowTemplates'>> = {};
|
|
if (args.name !== undefined) updates.name = args.name;
|
|
if (args.description !== undefined) updates.description = args.description;
|
|
if (args.status !== undefined) updates.status = args.status;
|
|
|
|
await ctx.db.patch(args.id, updates);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Excluir um template de fluxo
|
|
*/
|
|
export const deleteTemplate = mutation({
|
|
args: { id: v.id('flowTemplates') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const template = await ctx.db.get(args.id);
|
|
if (!template) {
|
|
throw new Error('Template não encontrado');
|
|
}
|
|
|
|
// Verificar se há instâncias vinculadas
|
|
const instancias = await ctx.db
|
|
.query('flowInstances')
|
|
.withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.id))
|
|
.first();
|
|
if (instancias) {
|
|
throw new Error('Não é possível excluir um template com instâncias vinculadas');
|
|
}
|
|
|
|
// Excluir todos os passos do template
|
|
const steps = await ctx.db
|
|
.query('flowSteps')
|
|
.withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.id))
|
|
.collect();
|
|
for (const step of steps) {
|
|
await ctx.db.delete(step._id);
|
|
}
|
|
|
|
await ctx.db.delete(args.id);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// FLOW STEPS - CRUD
|
|
// ============================================
|
|
|
|
/**
|
|
* Listar passos de um template
|
|
*/
|
|
export const listStepsByTemplate = query({
|
|
args: { flowTemplateId: v.id('flowTemplates') },
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('flowSteps'),
|
|
_creationTime: v.number(),
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
position: v.number(),
|
|
expectedDuration: v.number(),
|
|
setorId: v.id('setores'),
|
|
setorNome: v.optional(v.string()),
|
|
defaultAssigneeId: v.optional(v.id('usuarios')),
|
|
defaultAssigneeName: v.optional(v.string()),
|
|
requiredDocuments: v.optional(v.array(v.string()))
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const steps = await ctx.db
|
|
.query('flowSteps')
|
|
.withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.flowTemplateId))
|
|
.collect();
|
|
|
|
// Ordenar por position
|
|
steps.sort((a, b) => a.position - b.position);
|
|
|
|
const result: Array<{
|
|
_id: Id<'flowSteps'>;
|
|
_creationTime: number;
|
|
flowTemplateId: Id<'flowTemplates'>;
|
|
name: string;
|
|
description: string | undefined;
|
|
position: number;
|
|
expectedDuration: number;
|
|
setorId: Id<'setores'>;
|
|
setorNome: string | undefined;
|
|
defaultAssigneeId: Id<'usuarios'> | undefined;
|
|
defaultAssigneeName: string | undefined;
|
|
requiredDocuments: string[] | undefined;
|
|
}> = [];
|
|
|
|
for (const step of steps) {
|
|
const setor = await ctx.db.get(step.setorId);
|
|
const assignee = step.defaultAssigneeId ? await ctx.db.get(step.defaultAssigneeId) : null;
|
|
|
|
result.push({
|
|
_id: step._id,
|
|
_creationTime: step._creationTime,
|
|
flowTemplateId: step.flowTemplateId,
|
|
name: step.name,
|
|
description: step.description,
|
|
position: step.position,
|
|
expectedDuration: step.expectedDuration,
|
|
setorId: step.setorId,
|
|
setorNome: setor?.nome,
|
|
defaultAssigneeId: step.defaultAssigneeId,
|
|
defaultAssigneeName: assignee?.nome,
|
|
requiredDocuments: step.requiredDocuments
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Criar um novo passo
|
|
*/
|
|
export const createStep = mutation({
|
|
args: {
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
expectedDuration: v.number(),
|
|
setorId: v.id('setores'),
|
|
defaultAssigneeId: v.optional(v.id('usuarios')),
|
|
requiredDocuments: v.optional(v.array(v.string()))
|
|
},
|
|
returns: v.id('flowSteps'),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// Verificar se o template existe
|
|
const template = await ctx.db.get(args.flowTemplateId);
|
|
if (!template) {
|
|
throw new Error('Template não encontrado');
|
|
}
|
|
|
|
// Verificar se o setor existe
|
|
const setor = await ctx.db.get(args.setorId);
|
|
if (!setor) {
|
|
throw new Error('Setor não encontrado');
|
|
}
|
|
|
|
// Obter a próxima posição
|
|
const existingSteps = await ctx.db
|
|
.query('flowSteps')
|
|
.withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.flowTemplateId))
|
|
.collect();
|
|
const maxPosition = existingSteps.reduce((max, step) => Math.max(max, step.position), 0);
|
|
|
|
const stepId = await ctx.db.insert('flowSteps', {
|
|
flowTemplateId: args.flowTemplateId,
|
|
name: args.name,
|
|
description: args.description,
|
|
position: maxPosition + 1,
|
|
expectedDuration: args.expectedDuration,
|
|
setorId: args.setorId,
|
|
defaultAssigneeId: args.defaultAssigneeId,
|
|
requiredDocuments: args.requiredDocuments
|
|
});
|
|
|
|
return stepId;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Atualizar um passo
|
|
*/
|
|
export const updateStep = mutation({
|
|
args: {
|
|
id: v.id('flowSteps'),
|
|
name: v.optional(v.string()),
|
|
description: v.optional(v.string()),
|
|
expectedDuration: v.optional(v.number()),
|
|
setorId: v.optional(v.id('setores')),
|
|
defaultAssigneeId: v.optional(v.id('usuarios')),
|
|
requiredDocuments: v.optional(v.array(v.string()))
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.id);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
const updates: Partial<Doc<'flowSteps'>> = {};
|
|
if (args.name !== undefined) updates.name = args.name;
|
|
if (args.description !== undefined) updates.description = args.description;
|
|
if (args.expectedDuration !== undefined) updates.expectedDuration = args.expectedDuration;
|
|
if (args.setorId !== undefined) updates.setorId = args.setorId;
|
|
if (args.defaultAssigneeId !== undefined) updates.defaultAssigneeId = args.defaultAssigneeId;
|
|
if (args.requiredDocuments !== undefined) updates.requiredDocuments = args.requiredDocuments;
|
|
|
|
await ctx.db.patch(args.id, updates);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Reordenar passos
|
|
*/
|
|
export const reorderSteps = mutation({
|
|
args: {
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
stepIds: v.array(v.id('flowSteps'))
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// Atualizar posições
|
|
for (let i = 0; i < args.stepIds.length; i++) {
|
|
await ctx.db.patch(args.stepIds[i], { position: i + 1 });
|
|
}
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Excluir um passo
|
|
*/
|
|
export const deleteStep = mutation({
|
|
args: { id: v.id('flowSteps') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.id);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
await ctx.db.delete(args.id);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// FLOW INSTANCES
|
|
// ============================================
|
|
|
|
/**
|
|
* Listar instâncias de fluxo
|
|
*/
|
|
export const listInstances = query({
|
|
args: {
|
|
status: v.optional(flowInstanceStatus),
|
|
targetType: v.optional(v.string()),
|
|
managerId: v.optional(v.id('usuarios'))
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('flowInstances'),
|
|
_creationTime: v.number(),
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
templateName: v.optional(v.string()),
|
|
targetType: v.string(),
|
|
targetId: v.string(),
|
|
managerId: v.id('usuarios'),
|
|
managerName: v.optional(v.string()),
|
|
status: flowInstanceStatus,
|
|
startedAt: v.number(),
|
|
finishedAt: v.optional(v.number()),
|
|
currentStepId: v.optional(v.id('flowInstanceSteps')),
|
|
currentStepName: v.optional(v.string()),
|
|
progress: v.object({
|
|
completed: v.number(),
|
|
total: v.number()
|
|
})
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
let instances;
|
|
|
|
if (args.status) {
|
|
instances = await ctx.db
|
|
.query('flowInstances')
|
|
.withIndex('by_status', (q) => q.eq('status', args.status!))
|
|
.order('desc')
|
|
.collect();
|
|
} else if (args.managerId) {
|
|
instances = await ctx.db
|
|
.query('flowInstances')
|
|
.withIndex('by_managerId', (q) => q.eq('managerId', args.managerId!))
|
|
.order('desc')
|
|
.collect();
|
|
} else {
|
|
instances = await ctx.db.query('flowInstances').order('desc').collect();
|
|
}
|
|
|
|
// Filtrar por targetType se especificado
|
|
const filteredInstances = args.targetType
|
|
? instances.filter((i) => i.targetType === args.targetType)
|
|
: instances;
|
|
|
|
const result: Array<{
|
|
_id: Id<'flowInstances'>;
|
|
_creationTime: number;
|
|
flowTemplateId: Id<'flowTemplates'>;
|
|
templateName: string | undefined;
|
|
targetType: string;
|
|
targetId: string;
|
|
managerId: Id<'usuarios'>;
|
|
managerName: string | undefined;
|
|
status: Doc<'flowInstances'>['status'];
|
|
startedAt: number;
|
|
finishedAt: number | undefined;
|
|
currentStepId: Id<'flowInstanceSteps'> | undefined;
|
|
currentStepName: string | undefined;
|
|
progress: { completed: number; total: number };
|
|
}> = [];
|
|
|
|
for (const instance of filteredInstances) {
|
|
const template = await ctx.db.get(instance.flowTemplateId);
|
|
const manager = await ctx.db.get(instance.managerId);
|
|
|
|
// Obter passos da instância
|
|
const instanceSteps = await ctx.db
|
|
.query('flowInstanceSteps')
|
|
.withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', instance._id))
|
|
.collect();
|
|
|
|
const completedSteps = instanceSteps.filter((s) => s.status === 'completed').length;
|
|
|
|
// Obter nome do passo atual
|
|
let currentStepName: string | undefined;
|
|
if (instance.currentStepId) {
|
|
const currentStep = await ctx.db.get(instance.currentStepId);
|
|
if (currentStep) {
|
|
const flowStep = await ctx.db.get(currentStep.flowStepId);
|
|
currentStepName = flowStep?.name;
|
|
}
|
|
}
|
|
|
|
result.push({
|
|
_id: instance._id,
|
|
_creationTime: instance._creationTime,
|
|
flowTemplateId: instance.flowTemplateId,
|
|
templateName: template?.name,
|
|
targetType: instance.targetType,
|
|
targetId: instance.targetId,
|
|
managerId: instance.managerId,
|
|
managerName: manager?.nome,
|
|
status: instance.status,
|
|
startedAt: instance.startedAt,
|
|
finishedAt: instance.finishedAt,
|
|
currentStepId: instance.currentStepId,
|
|
currentStepName,
|
|
progress: {
|
|
completed: completedSteps,
|
|
total: instanceSteps.length
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Obter uma instância de fluxo com seus passos
|
|
*/
|
|
export const getInstanceWithSteps = query({
|
|
args: { id: v.id('flowInstances') },
|
|
returns: v.union(
|
|
v.object({
|
|
instance: v.object({
|
|
_id: v.id('flowInstances'),
|
|
_creationTime: v.number(),
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
templateName: v.optional(v.string()),
|
|
targetType: v.string(),
|
|
targetId: v.string(),
|
|
managerId: v.id('usuarios'),
|
|
managerName: v.optional(v.string()),
|
|
status: flowInstanceStatus,
|
|
startedAt: v.number(),
|
|
finishedAt: v.optional(v.number()),
|
|
currentStepId: v.optional(v.id('flowInstanceSteps'))
|
|
}),
|
|
steps: v.array(
|
|
v.object({
|
|
_id: v.id('flowInstanceSteps'),
|
|
_creationTime: v.number(),
|
|
flowInstanceId: v.id('flowInstances'),
|
|
flowStepId: v.id('flowSteps'),
|
|
stepName: v.string(),
|
|
stepDescription: v.optional(v.string()),
|
|
setorId: v.id('setores'),
|
|
setorNome: v.optional(v.string()),
|
|
assignedToId: v.optional(v.id('usuarios')),
|
|
assignedToName: v.optional(v.string()),
|
|
status: flowInstanceStepStatus,
|
|
startedAt: v.optional(v.number()),
|
|
finishedAt: v.optional(v.number()),
|
|
notes: v.optional(v.string()),
|
|
dueDate: v.optional(v.number()),
|
|
position: v.number(),
|
|
documents: v.array(
|
|
v.object({
|
|
_id: v.id('flowInstanceDocuments'),
|
|
name: v.string(),
|
|
uploadedAt: v.number(),
|
|
uploadedByName: v.optional(v.string())
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}),
|
|
v.null()
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const instance = await ctx.db.get(args.id);
|
|
if (!instance) return null;
|
|
|
|
const template = await ctx.db.get(instance.flowTemplateId);
|
|
const manager = await ctx.db.get(instance.managerId);
|
|
|
|
// Obter passos da instância
|
|
const instanceSteps = await ctx.db
|
|
.query('flowInstanceSteps')
|
|
.withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', args.id))
|
|
.collect();
|
|
|
|
// Mapear passos com informações adicionais
|
|
const stepsWithDetails: Array<{
|
|
_id: Id<'flowInstanceSteps'>;
|
|
_creationTime: number;
|
|
flowInstanceId: Id<'flowInstances'>;
|
|
flowStepId: Id<'flowSteps'>;
|
|
stepName: string;
|
|
stepDescription: string | undefined;
|
|
setorId: Id<'setores'>;
|
|
setorNome: string | undefined;
|
|
assignedToId: Id<'usuarios'> | undefined;
|
|
assignedToName: string | undefined;
|
|
status: Doc<'flowInstanceSteps'>['status'];
|
|
startedAt: number | undefined;
|
|
finishedAt: number | undefined;
|
|
notes: string | undefined;
|
|
dueDate: number | undefined;
|
|
position: number;
|
|
documents: Array<{
|
|
_id: Id<'flowInstanceDocuments'>;
|
|
name: string;
|
|
uploadedAt: number;
|
|
uploadedByName: string | undefined;
|
|
}>;
|
|
}> = [];
|
|
|
|
for (const step of instanceSteps) {
|
|
const flowStep = await ctx.db.get(step.flowStepId);
|
|
const setor = await ctx.db.get(step.setorId);
|
|
const assignee = step.assignedToId ? await ctx.db.get(step.assignedToId) : null;
|
|
|
|
// Obter documentos do passo
|
|
const documents = await ctx.db
|
|
.query('flowInstanceDocuments')
|
|
.withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', step._id))
|
|
.collect();
|
|
|
|
const docsWithUploader: Array<{
|
|
_id: Id<'flowInstanceDocuments'>;
|
|
name: string;
|
|
uploadedAt: number;
|
|
uploadedByName: string | undefined;
|
|
}> = [];
|
|
for (const doc of documents) {
|
|
const uploader = await ctx.db.get(doc.uploadedById);
|
|
docsWithUploader.push({
|
|
_id: doc._id,
|
|
name: doc.name,
|
|
uploadedAt: doc.uploadedAt,
|
|
uploadedByName: uploader?.nome
|
|
});
|
|
}
|
|
|
|
stepsWithDetails.push({
|
|
_id: step._id,
|
|
_creationTime: step._creationTime,
|
|
flowInstanceId: step.flowInstanceId,
|
|
flowStepId: step.flowStepId,
|
|
stepName: flowStep?.name ?? 'Passo desconhecido',
|
|
stepDescription: flowStep?.description,
|
|
setorId: step.setorId,
|
|
setorNome: setor?.nome,
|
|
assignedToId: step.assignedToId,
|
|
assignedToName: assignee?.nome,
|
|
status: step.status,
|
|
startedAt: step.startedAt,
|
|
finishedAt: step.finishedAt,
|
|
notes: step.notes,
|
|
dueDate: step.dueDate,
|
|
position: flowStep?.position ?? 0,
|
|
documents: docsWithUploader
|
|
});
|
|
}
|
|
|
|
// Ordenar por position
|
|
stepsWithDetails.sort((a, b) => a.position - b.position);
|
|
|
|
return {
|
|
instance: {
|
|
...instance,
|
|
templateName: template?.name,
|
|
managerName: manager?.nome
|
|
},
|
|
steps: stepsWithDetails
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Instanciar um fluxo
|
|
*/
|
|
export const instantiateFlow = mutation({
|
|
args: {
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
targetType: v.string(),
|
|
targetId: v.string(),
|
|
managerId: v.id('usuarios')
|
|
},
|
|
returns: v.id('flowInstances'),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
// Verificar se o template existe e está publicado
|
|
const template = await ctx.db.get(args.flowTemplateId);
|
|
if (!template) {
|
|
throw new Error('Template não encontrado');
|
|
}
|
|
if (template.status !== 'published') {
|
|
throw new Error('Apenas templates publicados podem ser instanciados');
|
|
}
|
|
|
|
// Obter passos do template
|
|
const templateSteps = await ctx.db
|
|
.query('flowSteps')
|
|
.withIndex('by_flowTemplateId', (q) => q.eq('flowTemplateId', args.flowTemplateId))
|
|
.collect();
|
|
|
|
if (templateSteps.length === 0) {
|
|
throw new Error('O template não possui passos definidos');
|
|
}
|
|
|
|
// Ordenar por position
|
|
templateSteps.sort((a, b) => a.position - b.position);
|
|
|
|
const now = Date.now();
|
|
|
|
// Criar a instância
|
|
const instanceId = await ctx.db.insert('flowInstances', {
|
|
flowTemplateId: args.flowTemplateId,
|
|
targetType: args.targetType,
|
|
targetId: args.targetId,
|
|
managerId: args.managerId,
|
|
status: 'active',
|
|
startedAt: now
|
|
});
|
|
|
|
// Criar os passos da instância
|
|
let firstStepId: Id<'flowInstanceSteps'> | undefined;
|
|
let cumulativeDays = 0;
|
|
|
|
for (let i = 0; i < templateSteps.length; i++) {
|
|
const step = templateSteps[i];
|
|
const dueDate = now + cumulativeDays * 24 * 60 * 60 * 1000 + step.expectedDuration * 24 * 60 * 60 * 1000;
|
|
cumulativeDays += step.expectedDuration;
|
|
|
|
const instanceStepId = await ctx.db.insert('flowInstanceSteps', {
|
|
flowInstanceId: instanceId,
|
|
flowStepId: step._id,
|
|
setorId: step.setorId,
|
|
assignedToId: step.defaultAssigneeId,
|
|
status: i === 0 ? 'pending' : 'pending',
|
|
dueDate
|
|
});
|
|
|
|
if (i === 0) {
|
|
firstStepId = instanceStepId;
|
|
}
|
|
}
|
|
|
|
// Atualizar o currentStepId da instância
|
|
if (firstStepId) {
|
|
await ctx.db.patch(instanceId, { currentStepId: firstStepId });
|
|
}
|
|
|
|
return instanceId;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Completar um passo da instância
|
|
*/
|
|
export const completeStep = mutation({
|
|
args: {
|
|
instanceStepId: v.id('flowInstanceSteps'),
|
|
notes: v.optional(v.string())
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.instanceStepId);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
if (step.status === 'completed') {
|
|
throw new Error('Este passo já foi concluído');
|
|
}
|
|
|
|
const instance = await ctx.db.get(step.flowInstanceId);
|
|
if (!instance) {
|
|
throw new Error('Instância não encontrada');
|
|
}
|
|
|
|
if (instance.status !== 'active') {
|
|
throw new Error('Não é possível completar passos de uma instância inativa');
|
|
}
|
|
|
|
const now = Date.now();
|
|
|
|
// Marcar passo como completado
|
|
await ctx.db.patch(args.instanceStepId, {
|
|
status: 'completed',
|
|
finishedAt: now,
|
|
notes: args.notes
|
|
});
|
|
|
|
// Obter todos os passos da instância
|
|
const allSteps = await ctx.db
|
|
.query('flowInstanceSteps')
|
|
.withIndex('by_flowInstanceId', (q) => q.eq('flowInstanceId', step.flowInstanceId))
|
|
.collect();
|
|
|
|
// Encontrar o próximo passo pendente
|
|
const flowSteps: Array<{ stepId: Id<'flowInstanceSteps'>; position: number }> = [];
|
|
for (const s of allSteps) {
|
|
const flowStep = await ctx.db.get(s.flowStepId);
|
|
if (flowStep) {
|
|
flowSteps.push({ stepId: s._id, position: flowStep.position });
|
|
}
|
|
}
|
|
flowSteps.sort((a, b) => a.position - b.position);
|
|
|
|
const currentPosition = flowSteps.findIndex((s) => s.stepId === args.instanceStepId);
|
|
const nextStep = flowSteps[currentPosition + 1];
|
|
|
|
if (nextStep) {
|
|
// Atualizar currentStepId para o próximo passo
|
|
await ctx.db.patch(step.flowInstanceId, { currentStepId: nextStep.stepId });
|
|
} else {
|
|
// Todos os passos concluídos, marcar instância como completada
|
|
await ctx.db.patch(step.flowInstanceId, {
|
|
status: 'completed',
|
|
finishedAt: now,
|
|
currentStepId: undefined
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Atualizar status de um passo
|
|
*/
|
|
export const updateStepStatus = mutation({
|
|
args: {
|
|
instanceStepId: v.id('flowInstanceSteps'),
|
|
status: flowInstanceStepStatus
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.instanceStepId);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
const updates: Partial<Doc<'flowInstanceSteps'>> = { status: args.status };
|
|
|
|
if (args.status === 'in_progress' && !step.startedAt) {
|
|
updates.startedAt = Date.now();
|
|
}
|
|
|
|
await ctx.db.patch(args.instanceStepId, updates);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Reatribuir responsável de um passo
|
|
*/
|
|
export const reassignStep = mutation({
|
|
args: {
|
|
instanceStepId: v.id('flowInstanceSteps'),
|
|
assignedToId: v.id('usuarios')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.instanceStepId);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
// Verificar se o usuário existe
|
|
const assignee = await ctx.db.get(args.assignedToId);
|
|
if (!assignee) {
|
|
throw new Error('Usuário não encontrado');
|
|
}
|
|
|
|
await ctx.db.patch(args.instanceStepId, { assignedToId: args.assignedToId });
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Atualizar notas de um passo
|
|
*/
|
|
export const updateStepNotes = mutation({
|
|
args: {
|
|
instanceStepId: v.id('flowInstanceSteps'),
|
|
notes: v.string()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.instanceStepId);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
await ctx.db.patch(args.instanceStepId, { notes: args.notes });
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Cancelar uma instância de fluxo
|
|
*/
|
|
export const cancelInstance = mutation({
|
|
args: { id: v.id('flowInstances') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const instance = await ctx.db.get(args.id);
|
|
if (!instance) {
|
|
throw new Error('Instância não encontrada');
|
|
}
|
|
|
|
if (instance.status !== 'active') {
|
|
throw new Error('Apenas instâncias ativas podem ser canceladas');
|
|
}
|
|
|
|
await ctx.db.patch(args.id, {
|
|
status: 'cancelled',
|
|
finishedAt: Date.now()
|
|
});
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// FLOW DOCUMENTS
|
|
// ============================================
|
|
|
|
/**
|
|
* Listar documentos de um passo
|
|
*/
|
|
export const listDocumentsByStep = query({
|
|
args: { flowInstanceStepId: v.id('flowInstanceSteps') },
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('flowInstanceDocuments'),
|
|
_creationTime: v.number(),
|
|
flowInstanceStepId: v.id('flowInstanceSteps'),
|
|
uploadedById: v.id('usuarios'),
|
|
uploadedByName: v.optional(v.string()),
|
|
storageId: v.id('_storage'),
|
|
name: v.string(),
|
|
uploadedAt: v.number(),
|
|
url: v.optional(v.string())
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const documents = await ctx.db
|
|
.query('flowInstanceDocuments')
|
|
.withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId))
|
|
.collect();
|
|
|
|
const result: Array<{
|
|
_id: Id<'flowInstanceDocuments'>;
|
|
_creationTime: number;
|
|
flowInstanceStepId: Id<'flowInstanceSteps'>;
|
|
uploadedById: Id<'usuarios'>;
|
|
uploadedByName: string | undefined;
|
|
storageId: Id<'_storage'>;
|
|
name: string;
|
|
uploadedAt: number;
|
|
url: string | undefined;
|
|
}> = [];
|
|
|
|
for (const doc of documents) {
|
|
const uploader = await ctx.db.get(doc.uploadedById);
|
|
const url = await ctx.storage.getUrl(doc.storageId);
|
|
|
|
result.push({
|
|
...doc,
|
|
uploadedByName: uploader?.nome,
|
|
url: url ?? undefined
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Registrar upload de documento
|
|
*/
|
|
export const registerDocument = mutation({
|
|
args: {
|
|
flowInstanceStepId: v.id('flowInstanceSteps'),
|
|
storageId: v.id('_storage'),
|
|
name: v.string()
|
|
},
|
|
returns: v.id('flowInstanceDocuments'),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const step = await ctx.db.get(args.flowInstanceStepId);
|
|
if (!step) {
|
|
throw new Error('Passo não encontrado');
|
|
}
|
|
|
|
const documentId = await ctx.db.insert('flowInstanceDocuments', {
|
|
flowInstanceStepId: args.flowInstanceStepId,
|
|
uploadedById: usuario._id,
|
|
storageId: args.storageId,
|
|
name: args.name,
|
|
uploadedAt: Date.now()
|
|
});
|
|
|
|
return documentId;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Excluir documento
|
|
*/
|
|
export const deleteDocument = mutation({
|
|
args: { id: v.id('flowInstanceDocuments') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
const document = await ctx.db.get(args.id);
|
|
if (!document) {
|
|
throw new Error('Documento não encontrado');
|
|
}
|
|
|
|
// Excluir o arquivo do storage
|
|
await ctx.storage.delete(document.storageId);
|
|
|
|
// Excluir o registro do documento
|
|
await ctx.db.delete(args.id);
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Gerar URL de upload
|
|
*/
|
|
export const generateUploadUrl = mutation({
|
|
args: {},
|
|
returns: v.string(),
|
|
handler: async (ctx) => {
|
|
const usuario = await getCurrentUserFunction(ctx);
|
|
if (!usuario) {
|
|
throw new Error('Usuário não autenticado');
|
|
}
|
|
|
|
return await ctx.storage.generateUploadUrl();
|
|
}
|
|
});
|
|
|