diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts
index 5d8de54..66e272e 100644
--- a/packages/backend/convex/_generated/api.d.ts
+++ b/packages/backend/convex/_generated/api.d.ts
@@ -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;
diff --git a/packages/backend/convex/pedidoFlow.ts b/packages/backend/convex/pedidoFlow.ts
new file mode 100644
index 0000000..83feff1
--- /dev/null
+++ b/packages/backend/convex/pedidoFlow.ts
@@ -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 => 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();
+ 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
+ };
+ }
+});
diff --git a/packages/backend/convex/tables/pedidos.ts b/packages/backend/convex/tables/pedidos.ts
index bc849ec..55c2054 100644
--- a/packages/backend/convex/tables/pedidos.ts
+++ b/packages/backend/convex/tables/pedidos.ts
@@ -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'])
};