feat: Implement order flow management with backend logic, configuration UI, and order timeline display.

This commit is contained in:
2025-12-30 12:30:26 -03:00
parent 5c0e9f0d2e
commit e97bcfbd6a
7 changed files with 2010 additions and 1 deletions

View File

@@ -55,6 +55,7 @@ import type * as logsLogin from "../logsLogin.js";
import type * as menu from "../menu.js";
import type * as monitoramento from "../monitoramento.js";
import type * as objetos from "../objetos.js";
import type * as pedidoFlow from "../pedidoFlow.js";
import type * as pedidos from "../pedidos.js";
import type * as permissoesAcoes from "../permissoesAcoes.js";
import type * as planejamentos from "../planejamentos.js";
@@ -155,6 +156,7 @@ declare const fullApi: ApiFromModules<{
menu: typeof menu;
monitoramento: typeof monitoramento;
objetos: typeof objetos;
pedidoFlow: typeof pedidoFlow;
pedidos: typeof pedidos;
permissoesAcoes: typeof permissoesAcoes;
planejamentos: typeof planejamentos;

View 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
};
}
});

View File

@@ -112,5 +112,46 @@ export const pedidosTables = {
})
.index('by_requestId', ['requestId'])
.index('by_pedidoId', ['pedidoId'])
.index('by_criadoPor', ['criadoPor'])
.index('by_criadoPor', ['criadoPor']),
// ========== FLUXO DE PEDIDOS ==========
// Configuração das etapas do fluxo de pedidos (dinâmico)
pedidoFluxoEtapa: defineTable({
nome: v.string(), // Nome da etapa (ex: "Rascunho", "Aguardando Aceite")
codigo: v.string(), // Código único (ex: "rascunho", "aguardando_aceite")
descricao: v.optional(v.string()),
setorId: v.optional(v.id('setores')), // Setor responsável por esta etapa
tempoEstimadoDias: v.optional(v.number()), // Tempo estimado em dias
incluirNoTimeline: v.boolean(), // Se false, não aparece no timeline (ex: rascunho)
ordem: v.number(), // Ordem de exibição
criadoEm: v.number(),
atualizadoEm: v.number()
})
.index('by_codigo', ['codigo'])
.index('by_ordem', ['ordem']),
// Transições possíveis entre etapas
pedidoFluxoTransicao: defineTable({
etapaOrigemId: v.id('pedidoFluxoEtapa'),
etapaDestinoId: v.id('pedidoFluxoEtapa'),
isPadrao: v.boolean(), // Se é a transição padrão quando há múltiplas opções
criadoEm: v.number()
})
.index('by_etapaOrigemId', ['etapaOrigemId'])
.index('by_etapaOrigemId_isPadrao', ['etapaOrigemId', 'isPadrao']),
// Histórico de etapas do pedido
pedidoEtapasHistorico: defineTable({
pedidoId: v.id('pedidos'),
etapaId: v.id('pedidoFluxoEtapa'),
inicioData: v.number(),
fimData: v.optional(v.number()),
funcionarioId: v.optional(v.id('funcionarios')),
atual: v.boolean()
})
.index('by_pedidoId', ['pedidoId'])
.index('by_pedidoId_atual', ['pedidoId', 'atual'])
.index('by_etapaId', ['etapaId'])
.index('by_funcionarioId', ['funcionarioId'])
};