feat: Implement order flow management with backend logic, configuration UI, and order timeline display.
This commit is contained in:
851
packages/backend/convex/pedidoFlow.ts
Normal file
851
packages/backend/convex/pedidoFlow.ts
Normal file
@@ -0,0 +1,851 @@
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
const usuario = await ctx.db
|
||||
.query('usuarios')
|
||||
.filter((q) => q.eq(q.field('email'), identity.email))
|
||||
.first();
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não encontrado');
|
||||
}
|
||||
return usuario;
|
||||
}
|
||||
|
||||
async function getEtapaAtualDoPedido(ctx: QueryCtx | MutationCtx, pedidoId: Id<'pedidos'>) {
|
||||
const etapaAtual = await ctx.db
|
||||
.query('pedidoEtapasHistorico')
|
||||
.withIndex('by_pedidoId_atual', (q) => q.eq('pedidoId', pedidoId).eq('atual', true))
|
||||
.first();
|
||||
return etapaAtual;
|
||||
}
|
||||
|
||||
// ========== ETAPAS - QUERIES ==========
|
||||
|
||||
export const listEtapas = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoFluxoEtapa'),
|
||||
_creationTime: v.number(),
|
||||
nome: v.string(),
|
||||
codigo: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
setorId: v.optional(v.id('setores')),
|
||||
setorNome: v.optional(v.string()),
|
||||
tempoEstimadoDias: v.optional(v.number()),
|
||||
incluirNoTimeline: v.boolean(),
|
||||
ordem: v.number(),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const etapas = await ctx.db.query('pedidoFluxoEtapa').withIndex('by_ordem').collect();
|
||||
|
||||
const result = await Promise.all(
|
||||
etapas.map(async (etapa) => {
|
||||
let setorNome: string | undefined;
|
||||
if (etapa.setorId) {
|
||||
const setor = await ctx.db.get(etapa.setorId);
|
||||
setorNome = setor?.nome;
|
||||
}
|
||||
return {
|
||||
...etapa,
|
||||
setorNome
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
export const getEtapa = query({
|
||||
args: { id: v.id('pedidoFluxoEtapa') },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('pedidoFluxoEtapa'),
|
||||
_creationTime: v.number(),
|
||||
nome: v.string(),
|
||||
codigo: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
setorId: v.optional(v.id('setores')),
|
||||
tempoEstimadoDias: v.optional(v.number()),
|
||||
incluirNoTimeline: v.boolean(),
|
||||
ordem: v.number(),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
}
|
||||
});
|
||||
|
||||
export const getEtapaByCodigo = query({
|
||||
args: { codigo: v.string() },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('pedidoFluxoEtapa'),
|
||||
_creationTime: v.number(),
|
||||
nome: v.string(),
|
||||
codigo: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
setorId: v.optional(v.id('setores')),
|
||||
tempoEstimadoDias: v.optional(v.number()),
|
||||
incluirNoTimeline: v.boolean(),
|
||||
ordem: v.number(),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query('pedidoFluxoEtapa')
|
||||
.withIndex('by_codigo', (q) => q.eq('codigo', args.codigo))
|
||||
.first();
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ETAPAS - MUTATIONS ==========
|
||||
|
||||
export const createEtapa = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
codigo: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
setorId: v.optional(v.id('setores')),
|
||||
tempoEstimadoDias: v.optional(v.number()),
|
||||
incluirNoTimeline: v.boolean()
|
||||
},
|
||||
returns: v.id('pedidoFluxoEtapa'),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
// Verificar se já existe etapa com este código
|
||||
const existente = await ctx.db
|
||||
.query('pedidoFluxoEtapa')
|
||||
.withIndex('by_codigo', (q) => q.eq('codigo', args.codigo))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
throw new Error(`Já existe uma etapa com o código "${args.codigo}"`);
|
||||
}
|
||||
|
||||
// Obter a maior ordem atual
|
||||
const todasEtapas = await ctx.db.query('pedidoFluxoEtapa').collect();
|
||||
const maiorOrdem = todasEtapas.reduce((max, e) => Math.max(max, e.ordem), 0);
|
||||
|
||||
const now = Date.now();
|
||||
return await ctx.db.insert('pedidoFluxoEtapa', {
|
||||
nome: args.nome,
|
||||
codigo: args.codigo,
|
||||
descricao: args.descricao,
|
||||
setorId: args.setorId,
|
||||
tempoEstimadoDias: args.tempoEstimadoDias,
|
||||
incluirNoTimeline: args.incluirNoTimeline,
|
||||
ordem: maiorOrdem + 1,
|
||||
criadoEm: now,
|
||||
atualizadoEm: now
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const updateEtapa = mutation({
|
||||
args: {
|
||||
id: v.id('pedidoFluxoEtapa'),
|
||||
nome: v.optional(v.string()),
|
||||
codigo: v.optional(v.string()),
|
||||
descricao: v.optional(v.string()),
|
||||
setorId: v.optional(v.id('setores')),
|
||||
tempoEstimadoDias: v.optional(v.number()),
|
||||
incluirNoTimeline: v.optional(v.boolean()),
|
||||
ordem: v.optional(v.number())
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
const etapa = await ctx.db.get(args.id);
|
||||
if (!etapa) {
|
||||
throw new Error('Etapa não encontrada');
|
||||
}
|
||||
|
||||
const codigo = args.codigo;
|
||||
|
||||
// Se estiver mudando o código, verificar duplicidade
|
||||
if (codigo && codigo !== etapa.codigo) {
|
||||
const existente = await ctx.db
|
||||
.query('pedidoFluxoEtapa')
|
||||
.withIndex('by_codigo', (q) => q.eq('codigo', codigo))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
throw new Error(`Já existe uma etapa com o código "${args.codigo}"`);
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
...(args.nome !== undefined && { nome: args.nome }),
|
||||
...(args.codigo !== undefined && { codigo: args.codigo }),
|
||||
...(args.descricao !== undefined && { descricao: args.descricao }),
|
||||
...(args.setorId !== undefined && { setorId: args.setorId }),
|
||||
...(args.tempoEstimadoDias !== undefined && { tempoEstimadoDias: args.tempoEstimadoDias }),
|
||||
...(args.incluirNoTimeline !== undefined && { incluirNoTimeline: args.incluirNoTimeline }),
|
||||
...(args.ordem !== undefined && { ordem: args.ordem }),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteEtapa = mutation({
|
||||
args: { id: v.id('pedidoFluxoEtapa') },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
const etapa = await ctx.db.get(args.id);
|
||||
if (!etapa) {
|
||||
throw new Error('Etapa não encontrada');
|
||||
}
|
||||
|
||||
// Verificar se há transições usando esta etapa
|
||||
const transicoesOrigem = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', args.id))
|
||||
.collect();
|
||||
|
||||
if (transicoesOrigem.length > 0) {
|
||||
throw new Error('Não é possível excluir: esta etapa possui transições de saída');
|
||||
}
|
||||
|
||||
// Verificar se há transições de destino
|
||||
const transicoesDestino = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.filter((q) => q.eq(q.field('etapaDestinoId'), args.id))
|
||||
.collect();
|
||||
|
||||
if (transicoesDestino.length > 0) {
|
||||
throw new Error('Não é possível excluir: esta etapa é destino de outras transições');
|
||||
}
|
||||
|
||||
// Verificar se há histórico usando esta etapa
|
||||
const historico = await ctx.db
|
||||
.query('pedidoEtapasHistorico')
|
||||
.withIndex('by_etapaId', (q) => q.eq('etapaId', args.id))
|
||||
.first();
|
||||
|
||||
if (historico) {
|
||||
throw new Error('Não é possível excluir: esta etapa já foi utilizada em pedidos');
|
||||
}
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const reordenarEtapas = mutation({
|
||||
args: {
|
||||
ordens: v.array(
|
||||
v.object({
|
||||
id: v.id('pedidoFluxoEtapa'),
|
||||
ordem: v.number()
|
||||
})
|
||||
)
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
for (const item of args.ordens) {
|
||||
await ctx.db.patch(item.id, { ordem: item.ordem, atualizadoEm: Date.now() });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== TRANSIÇÕES - QUERIES ==========
|
||||
|
||||
export const listTransicoes = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoFluxoTransicao'),
|
||||
_creationTime: v.number(),
|
||||
etapaOrigemId: v.id('pedidoFluxoEtapa'),
|
||||
etapaOrigemNome: v.string(),
|
||||
etapaDestinoId: v.id('pedidoFluxoEtapa'),
|
||||
etapaDestinoNome: v.string(),
|
||||
isPadrao: v.boolean(),
|
||||
criadoEm: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const transicoes = await ctx.db.query('pedidoFluxoTransicao').collect();
|
||||
|
||||
const result = await Promise.all(
|
||||
transicoes.map(async (t) => {
|
||||
const origem = await ctx.db.get(t.etapaOrigemId);
|
||||
const destino = await ctx.db.get(t.etapaDestinoId);
|
||||
return {
|
||||
...t,
|
||||
etapaOrigemNome: origem?.nome ?? 'Desconhecido',
|
||||
etapaDestinoNome: destino?.nome ?? 'Desconhecido'
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
export const getProximasEtapas = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoFluxoEtapa'),
|
||||
nome: v.string(),
|
||||
codigo: v.string(),
|
||||
isPadrao: v.boolean()
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const etapaAtualHistorico = await getEtapaAtualDoPedido(ctx, args.pedidoId);
|
||||
|
||||
if (!etapaAtualHistorico) {
|
||||
// Pedido não tem etapa, retornar a primeira etapa do fluxo
|
||||
const primeiraEtapa = await ctx.db.query('pedidoFluxoEtapa').withIndex('by_ordem').first();
|
||||
|
||||
if (!primeiraEtapa) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
_id: primeiraEtapa._id,
|
||||
nome: primeiraEtapa.nome,
|
||||
codigo: primeiraEtapa.codigo,
|
||||
isPadrao: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Buscar transições a partir da etapa atual
|
||||
const transicoes = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', etapaAtualHistorico.etapaId))
|
||||
.collect();
|
||||
|
||||
const result = await Promise.all(
|
||||
transicoes.map(async (t) => {
|
||||
const etapa = await ctx.db.get(t.etapaDestinoId);
|
||||
if (!etapa) return null;
|
||||
return {
|
||||
_id: etapa._id,
|
||||
nome: etapa.nome,
|
||||
codigo: etapa.codigo,
|
||||
isPadrao: t.isPadrao
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return result.filter((r): r is NonNullable<typeof r> => r !== null);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== TRANSIÇÕES - MUTATIONS ==========
|
||||
|
||||
export const createTransicao = mutation({
|
||||
args: {
|
||||
etapaOrigemId: v.id('pedidoFluxoEtapa'),
|
||||
etapaDestinoId: v.id('pedidoFluxoEtapa')
|
||||
},
|
||||
returns: v.id('pedidoFluxoTransicao'),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
if (args.etapaOrigemId === args.etapaDestinoId) {
|
||||
throw new Error('A etapa de origem e destino não podem ser iguais');
|
||||
}
|
||||
|
||||
// Verificar se etapas existem
|
||||
const origem = await ctx.db.get(args.etapaOrigemId);
|
||||
const destino = await ctx.db.get(args.etapaDestinoId);
|
||||
|
||||
if (!origem) throw new Error('Etapa de origem não encontrada');
|
||||
if (!destino) throw new Error('Etapa de destino não encontrada');
|
||||
|
||||
// Verificar se já existe esta transição
|
||||
const existente = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', args.etapaOrigemId))
|
||||
.filter((q) => q.eq(q.field('etapaDestinoId'), args.etapaDestinoId))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
throw new Error('Esta transição já existe');
|
||||
}
|
||||
|
||||
// Verificar se já existe alguma transição de saída para esta origem
|
||||
const transicoesExistentes = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', args.etapaOrigemId))
|
||||
.collect();
|
||||
|
||||
// Se for a primeira transição, ela é a padrão
|
||||
const isPadrao = transicoesExistentes.length === 0;
|
||||
|
||||
return await ctx.db.insert('pedidoFluxoTransicao', {
|
||||
etapaOrigemId: args.etapaOrigemId,
|
||||
etapaDestinoId: args.etapaDestinoId,
|
||||
isPadrao,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteTransicao = mutation({
|
||||
args: { id: v.id('pedidoFluxoTransicao') },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
const transicao = await ctx.db.get(args.id);
|
||||
if (!transicao) {
|
||||
throw new Error('Transição não encontrada');
|
||||
}
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
|
||||
// Se era a transição padrão, definir outra como padrão
|
||||
if (transicao.isPadrao) {
|
||||
const outraTransicao = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', transicao.etapaOrigemId))
|
||||
.first();
|
||||
|
||||
if (outraTransicao) {
|
||||
await ctx.db.patch(outraTransicao._id, { isPadrao: true });
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const setTransicaoPadrao = mutation({
|
||||
args: { id: v.id('pedidoFluxoTransicao') },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
const transicao = await ctx.db.get(args.id);
|
||||
if (!transicao) {
|
||||
throw new Error('Transição não encontrada');
|
||||
}
|
||||
|
||||
// Remover isPadrao de todas as transições da mesma origem
|
||||
const todasTransicoes = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', transicao.etapaOrigemId))
|
||||
.collect();
|
||||
|
||||
for (const t of todasTransicoes) {
|
||||
if (t._id !== args.id && t.isPadrao) {
|
||||
await ctx.db.patch(t._id, { isPadrao: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Definir esta como padrão
|
||||
await ctx.db.patch(args.id, { isPadrao: true });
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== HISTÓRICO E TIMELINE ==========
|
||||
|
||||
export const getEtapasHistorico = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoEtapasHistorico'),
|
||||
_creationTime: v.number(),
|
||||
pedidoId: v.id('pedidos'),
|
||||
etapaId: v.id('pedidoFluxoEtapa'),
|
||||
etapaNome: v.string(),
|
||||
etapaCodigo: v.string(),
|
||||
inicioData: v.number(),
|
||||
fimData: v.optional(v.number()),
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
funcionarioNome: v.optional(v.string()),
|
||||
atual: v.boolean()
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const historico = await ctx.db
|
||||
.query('pedidoEtapasHistorico')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
// Ordenar por inicioData
|
||||
historico.sort((a, b) => a.inicioData - b.inicioData);
|
||||
|
||||
const result = await Promise.all(
|
||||
historico.map(async (h) => {
|
||||
const etapa = await ctx.db.get(h.etapaId);
|
||||
let funcionarioNome: string | undefined;
|
||||
if (h.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(h.funcionarioId);
|
||||
funcionarioNome = funcionario?.nome;
|
||||
}
|
||||
return {
|
||||
...h,
|
||||
etapaNome: etapa?.nome ?? 'Desconhecido',
|
||||
etapaCodigo: etapa?.codigo ?? 'desconhecido',
|
||||
funcionarioNome
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
export const getPedidoTimeline = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.object({
|
||||
passado: v.array(
|
||||
v.object({
|
||||
etapaId: v.id('pedidoFluxoEtapa'),
|
||||
etapaNome: v.string(),
|
||||
etapaCodigo: v.string(),
|
||||
inicioData: v.number(),
|
||||
fimData: v.optional(v.number()),
|
||||
funcionarioNome: v.optional(v.string()),
|
||||
atual: v.boolean(),
|
||||
incluirNoTimeline: v.boolean()
|
||||
})
|
||||
),
|
||||
futuro: v.array(
|
||||
v.object({
|
||||
etapaId: v.id('pedidoFluxoEtapa'),
|
||||
etapaNome: v.string(),
|
||||
etapaCodigo: v.string(),
|
||||
dataPrevisao: v.number(),
|
||||
incluirNoTimeline: v.boolean()
|
||||
})
|
||||
)
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar histórico passado
|
||||
const historico = await ctx.db
|
||||
.query('pedidoEtapasHistorico')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
historico.sort((a, b) => a.inicioData - b.inicioData);
|
||||
|
||||
const passado = await Promise.all(
|
||||
historico.map(async (h) => {
|
||||
const etapa = await ctx.db.get(h.etapaId);
|
||||
let funcionarioNome: string | undefined;
|
||||
if (h.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(h.funcionarioId);
|
||||
funcionarioNome = funcionario?.nome;
|
||||
}
|
||||
return {
|
||||
etapaId: h.etapaId,
|
||||
etapaNome: etapa?.nome ?? 'Desconhecido',
|
||||
etapaCodigo: etapa?.codigo ?? 'desconhecido',
|
||||
inicioData: h.inicioData,
|
||||
fimData: h.fimData,
|
||||
funcionarioNome,
|
||||
atual: h.atual,
|
||||
incluirNoTimeline: etapa?.incluirNoTimeline ?? true
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Calcular previsão futura usando transições padrão
|
||||
const futuro: {
|
||||
etapaId: Id<'pedidoFluxoEtapa'>;
|
||||
etapaNome: string;
|
||||
etapaCodigo: string;
|
||||
dataPrevisao: number;
|
||||
incluirNoTimeline: boolean;
|
||||
}[] = [];
|
||||
|
||||
const etapaAtualHistorico = historico.find((h) => h.atual);
|
||||
if (etapaAtualHistorico) {
|
||||
let etapaAtualId = etapaAtualHistorico.etapaId;
|
||||
let dataPrevisao = Date.now();
|
||||
|
||||
// Calcular tempo decorrido na etapa atual
|
||||
const etapaAtual = await ctx.db.get(etapaAtualId);
|
||||
if (etapaAtual?.tempoEstimadoDias) {
|
||||
dataPrevisao += etapaAtual.tempoEstimadoDias * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
// Seguir transições padrão até não ter mais
|
||||
const visitadas = new Set<string>();
|
||||
let iteracoes = 0;
|
||||
const MAX_ITERACOES = 20; // Evitar loop infinito
|
||||
|
||||
while (iteracoes < MAX_ITERACOES) {
|
||||
iteracoes++;
|
||||
|
||||
if (visitadas.has(etapaAtualId)) break;
|
||||
visitadas.add(etapaAtualId);
|
||||
|
||||
// Buscar transição padrão
|
||||
const transicaoPadrao = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId_isPadrao', (q) =>
|
||||
q.eq('etapaOrigemId', etapaAtualId).eq('isPadrao', true)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!transicaoPadrao) break;
|
||||
|
||||
const proximaEtapa = await ctx.db.get(transicaoPadrao.etapaDestinoId);
|
||||
if (!proximaEtapa) break;
|
||||
|
||||
futuro.push({
|
||||
etapaId: proximaEtapa._id,
|
||||
etapaNome: proximaEtapa.nome,
|
||||
etapaCodigo: proximaEtapa.codigo,
|
||||
dataPrevisao,
|
||||
incluirNoTimeline: proximaEtapa.incluirNoTimeline
|
||||
});
|
||||
|
||||
// Atualizar para próxima iteração
|
||||
if (proximaEtapa.tempoEstimadoDias) {
|
||||
dataPrevisao += proximaEtapa.tempoEstimadoDias * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
etapaAtualId = proximaEtapa._id;
|
||||
}
|
||||
}
|
||||
|
||||
return { passado, futuro };
|
||||
}
|
||||
});
|
||||
|
||||
// ========== MUDANÇA DE ETAPA ==========
|
||||
|
||||
export const mudarEtapa = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
novaEtapaId: v.id('pedidoFluxoEtapa'),
|
||||
funcionarioId: v.optional(v.id('funcionarios'))
|
||||
},
|
||||
returns: v.id('pedidoEtapasHistorico'),
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getUsuarioAutenticado(ctx);
|
||||
|
||||
const pedido = await ctx.db.get(args.pedidoId);
|
||||
if (!pedido) {
|
||||
throw new Error('Pedido não encontrado');
|
||||
}
|
||||
|
||||
const novaEtapa = await ctx.db.get(args.novaEtapaId);
|
||||
if (!novaEtapa) {
|
||||
throw new Error('Etapa não encontrada');
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Buscar etapa atual
|
||||
const etapaAtualHistorico = await getEtapaAtualDoPedido(ctx, args.pedidoId);
|
||||
|
||||
if (etapaAtualHistorico) {
|
||||
// Validar se a transição é permitida
|
||||
const transicaoPermitida = await ctx.db
|
||||
.query('pedidoFluxoTransicao')
|
||||
.withIndex('by_etapaOrigemId', (q) => q.eq('etapaOrigemId', etapaAtualHistorico.etapaId))
|
||||
.filter((q) => q.eq(q.field('etapaDestinoId'), args.novaEtapaId))
|
||||
.first();
|
||||
|
||||
if (!transicaoPermitida) {
|
||||
throw new Error('Transição não permitida para esta etapa');
|
||||
}
|
||||
|
||||
// Finalizar etapa atual
|
||||
await ctx.db.patch(etapaAtualHistorico._id, {
|
||||
atual: false,
|
||||
fimData: now
|
||||
});
|
||||
}
|
||||
|
||||
// Criar novo registro de etapa
|
||||
const novoHistoricoId = await ctx.db.insert('pedidoEtapasHistorico', {
|
||||
pedidoId: args.pedidoId,
|
||||
etapaId: args.novaEtapaId,
|
||||
inicioData: now,
|
||||
funcionarioId: args.funcionarioId,
|
||||
atual: true
|
||||
});
|
||||
|
||||
// Registrar no histórico de pedidos
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: usuario._id,
|
||||
acao: 'alteracao_etapa',
|
||||
detalhes: JSON.stringify({
|
||||
novaEtapa: novaEtapa.codigo,
|
||||
novaEtapaNome: novaEtapa.nome,
|
||||
funcionarioId: args.funcionarioId
|
||||
}),
|
||||
data: now
|
||||
});
|
||||
|
||||
return novoHistoricoId;
|
||||
}
|
||||
});
|
||||
|
||||
export const iniciarFluxoPedido = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos')
|
||||
},
|
||||
returns: v.id('pedidoEtapasHistorico'),
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getUsuarioAutenticado(ctx);
|
||||
|
||||
const pedido = await ctx.db.get(args.pedidoId);
|
||||
if (!pedido) {
|
||||
throw new Error('Pedido não encontrado');
|
||||
}
|
||||
|
||||
// Verificar se já tem histórico
|
||||
const historicoExistente = await ctx.db
|
||||
.query('pedidoEtapasHistorico')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.first();
|
||||
|
||||
if (historicoExistente) {
|
||||
throw new Error('Este pedido já possui histórico de etapas');
|
||||
}
|
||||
|
||||
// Buscar primeira etapa do fluxo
|
||||
const primeiraEtapa = await ctx.db.query('pedidoFluxoEtapa').withIndex('by_ordem').first();
|
||||
|
||||
if (!primeiraEtapa) {
|
||||
throw new Error('Nenhuma etapa configurada no fluxo');
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Criar primeiro registro
|
||||
const historicoId = await ctx.db.insert('pedidoEtapasHistorico', {
|
||||
pedidoId: args.pedidoId,
|
||||
etapaId: primeiraEtapa._id,
|
||||
inicioData: now,
|
||||
atual: true
|
||||
});
|
||||
|
||||
// Registrar no histórico
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: usuario._id,
|
||||
acao: 'inicio_fluxo',
|
||||
detalhes: JSON.stringify({
|
||||
etapa: primeiraEtapa.codigo,
|
||||
etapaNome: primeiraEtapa.nome
|
||||
}),
|
||||
data: now
|
||||
});
|
||||
|
||||
return historicoId;
|
||||
}
|
||||
});
|
||||
|
||||
export const atribuirFuncionario = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await getUsuarioAutenticado(ctx);
|
||||
|
||||
const etapaAtual = await getEtapaAtualDoPedido(ctx, args.pedidoId);
|
||||
if (!etapaAtual) {
|
||||
throw new Error('Pedido não possui etapa atual');
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error('Funcionário não encontrado');
|
||||
}
|
||||
|
||||
await ctx.db.patch(etapaAtual._id, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== QUERY PARA OBTER ETAPA ATUAL ==========
|
||||
|
||||
export const getEtapaAtual = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('pedidoEtapasHistorico'),
|
||||
etapaId: v.id('pedidoFluxoEtapa'),
|
||||
etapaNome: v.string(),
|
||||
etapaCodigo: v.string(),
|
||||
setorId: v.optional(v.id('setores')),
|
||||
setorNome: v.optional(v.string()),
|
||||
inicioData: v.number(),
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
funcionarioNome: v.optional(v.string())
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const etapaAtualHistorico = await getEtapaAtualDoPedido(ctx, args.pedidoId);
|
||||
if (!etapaAtualHistorico) return null;
|
||||
|
||||
const etapa = await ctx.db.get(etapaAtualHistorico.etapaId);
|
||||
if (!etapa) return null;
|
||||
|
||||
let setorNome: string | undefined;
|
||||
if (etapa.setorId) {
|
||||
const setor = await ctx.db.get(etapa.setorId);
|
||||
setorNome = setor?.nome;
|
||||
}
|
||||
|
||||
let funcionarioNome: string | undefined;
|
||||
if (etapaAtualHistorico.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(etapaAtualHistorico.funcionarioId);
|
||||
funcionarioNome = funcionario?.nome;
|
||||
}
|
||||
|
||||
return {
|
||||
_id: etapaAtualHistorico._id,
|
||||
etapaId: etapa._id,
|
||||
etapaNome: etapa.nome,
|
||||
etapaCodigo: etapa.codigo,
|
||||
setorId: etapa.setorId,
|
||||
setorNome,
|
||||
inicioData: etapaAtualHistorico.inicioData,
|
||||
funcionarioId: etapaAtualHistorico.funcionarioId,
|
||||
funcionarioNome
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user