2987 lines
89 KiB
TypeScript
2987 lines
89 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { api, internal } from './_generated/api';
|
|
import type { Doc, Id } from './_generated/dataModel';
|
|
import type { MutationCtx, QueryCtx } from './_generated/server';
|
|
import { internalMutation, mutation, query } from './_generated/server';
|
|
import { getCurrentUserFunction } from './auth';
|
|
import { iniciarFluxoPedidoInternal } from './pedidoFlow';
|
|
import { getTodayYMD, isWithinRangeYMD, maxYMD } from './utils/datas';
|
|
|
|
// ========== HELPERS ==========
|
|
|
|
const pedidoStatusValidator = v.union(
|
|
v.literal('em_rascunho'),
|
|
v.literal('aguardando_aceite'),
|
|
v.literal('em_analise'),
|
|
v.literal('precisa_ajustes'),
|
|
v.literal('cancelado'),
|
|
v.literal('concluido')
|
|
);
|
|
|
|
type PedidoStatus = Doc<'pedidos'>['status'];
|
|
|
|
type PedidoListFilters = {
|
|
statuses?: PedidoStatus[];
|
|
numeroSei?: string;
|
|
criadoPor?: Id<'usuarios'>;
|
|
aceitoPor?: Id<'funcionarios'>;
|
|
periodoInicio?: number;
|
|
periodoFim?: number;
|
|
};
|
|
|
|
function inRange(ts: number, inicio?: number, fim?: number): boolean {
|
|
if (inicio !== undefined && ts < inicio) return false;
|
|
if (fim !== undefined && ts > fim) return false;
|
|
return true;
|
|
}
|
|
|
|
function matchesPeriodo(p: Doc<'pedidos'>, inicio?: number, fim?: number): boolean {
|
|
if (inicio === undefined && fim === undefined) return true;
|
|
if (inRange(p.criadoEm, inicio, fim)) return true;
|
|
if (p.concluidoEm !== undefined && inRange(p.concluidoEm, inicio, fim)) return true;
|
|
return false;
|
|
}
|
|
|
|
function normalizeNumeroSeiQuery(q: string | undefined): string | undefined {
|
|
const trimmed = q?.trim();
|
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
}
|
|
|
|
function applyPedidoFilters(pedidos: Doc<'pedidos'>[], args: PedidoListFilters): Doc<'pedidos'>[] {
|
|
const numeroSeiQuery = normalizeNumeroSeiQuery(args.numeroSei);
|
|
const statusesSet =
|
|
args.statuses && args.statuses.length > 0 ? new Set<PedidoStatus>(args.statuses) : null;
|
|
|
|
const filtered = pedidos.filter((p) => {
|
|
if (statusesSet && !statusesSet.has(p.status)) return false;
|
|
if (args.criadoPor && p.criadoPor !== args.criadoPor) return false;
|
|
if (args.aceitoPor && p.aceitoPor !== args.aceitoPor) return false;
|
|
|
|
if (!matchesPeriodo(p, args.periodoInicio, args.periodoFim)) return false;
|
|
|
|
if (numeroSeiQuery) {
|
|
const sei = (p.numeroSei ?? '').toLowerCase();
|
|
if (!sei.includes(numeroSeiQuery)) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Ordem mais útil para listagem: mais recentes primeiro
|
|
filtered.sort((a, b) => b.criadoEm - a.criadoEm);
|
|
return filtered;
|
|
}
|
|
|
|
function dedupePedidos(pedidos: Doc<'pedidos'>[]): Doc<'pedidos'>[] {
|
|
const map = new Map<string, Doc<'pedidos'>>();
|
|
for (const p of pedidos) map.set(String(p._id), p);
|
|
return [...map.values()];
|
|
}
|
|
|
|
async function fetchPedidosBase(ctx: QueryCtx, args: PedidoListFilters): Promise<Doc<'pedidos'>[]> {
|
|
// 1) Se há período, buscamos por índices de criadoEm e concluidoEm e unimos (OR)
|
|
if (args.periodoInicio !== undefined || args.periodoFim !== undefined) {
|
|
const inicio = args.periodoInicio;
|
|
const fim = args.periodoFim;
|
|
|
|
const byCriado =
|
|
inicio !== undefined && fim !== undefined
|
|
? await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_criadoEm', (q) => q.gte('criadoEm', inicio).lte('criadoEm', fim))
|
|
.collect()
|
|
: inicio !== undefined
|
|
? await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_criadoEm', (q) => q.gte('criadoEm', inicio))
|
|
.collect()
|
|
: await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_criadoEm', (q) => q.lte('criadoEm', fim as number))
|
|
.collect();
|
|
|
|
const byConcluido =
|
|
inicio !== undefined && fim !== undefined
|
|
? await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_concluidoEm', (q) =>
|
|
q.gte('concluidoEm', inicio).lte('concluidoEm', fim)
|
|
)
|
|
.collect()
|
|
: inicio !== undefined
|
|
? await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_concluidoEm', (q) => q.gte('concluidoEm', inicio))
|
|
.collect()
|
|
: await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_concluidoEm', (q) => q.lte('concluidoEm', fim as number))
|
|
.collect();
|
|
|
|
return dedupePedidos(byCriado.concat(byConcluido));
|
|
}
|
|
|
|
// 2) Se há statuses selecionados, usar o índice by_status
|
|
if (args.statuses && args.statuses.length > 0) {
|
|
let out: Doc<'pedidos'>[] = [];
|
|
for (const status of args.statuses) {
|
|
const part = await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_status', (q) => q.eq('status', status))
|
|
.collect();
|
|
out = out.concat(part);
|
|
}
|
|
return dedupePedidos(out);
|
|
}
|
|
|
|
// 3) Se há criador, usar by_criadoPor
|
|
if (args.criadoPor) {
|
|
return await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_criadoPor', (q) => q.eq('criadoPor', args.criadoPor!))
|
|
.collect();
|
|
}
|
|
|
|
// 4) Se há aceitoPor, usar by_aceitoPor
|
|
if (args.aceitoPor) {
|
|
return await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_aceitoPor', (q) => q.eq('aceitoPor', args.aceitoPor!))
|
|
.collect();
|
|
}
|
|
|
|
// 5) fallback: varrer (caso sem índices úteis)
|
|
return await ctx.db.query('pedidos').collect();
|
|
}
|
|
|
|
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
|
|
const user = await getCurrentUserFunction(ctx);
|
|
if (!user) throw new Error('Unauthorized');
|
|
return user;
|
|
}
|
|
|
|
// Garante que todos os itens de um pedido utilizem a mesma ata (quando houver).
|
|
// Nota: Modalidade não é mais validada aqui, pois é definida apenas pelo Setor de Compras.
|
|
async function ensurePedidoAtaConsistency(
|
|
ctx: MutationCtx,
|
|
pedidoId: Id<'pedidos'>,
|
|
ataId: Id<'atas'> | undefined,
|
|
ignoreItemId?: Id<'objetoItems'>
|
|
) {
|
|
const normalizedNewAtaId = ataId ?? null;
|
|
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedidoId))
|
|
.collect();
|
|
|
|
if (items.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const item of items) {
|
|
if (ignoreItemId && item._id === ignoreItemId) continue;
|
|
|
|
const normalizedItemAtaId = (('ataId' in item ? item.ataId : undefined) ??
|
|
null) as Id<'atas'> | null;
|
|
|
|
const ataMismatch = normalizedItemAtaId !== normalizedNewAtaId;
|
|
|
|
if (ataMismatch) {
|
|
throw new Error(
|
|
'Todos os itens do pedido devem usar a mesma ata (quando houver). Ajuste os itens existentes ou crie um novo pedido para a nova ata.'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
type AtaObjetoUsageInfo = {
|
|
linkId: Id<'atasObjetos'>;
|
|
quantidadeTotal: number;
|
|
limitePercentual: number;
|
|
limitePermitido: number;
|
|
quantidadeUsada: number;
|
|
restante: number;
|
|
};
|
|
|
|
function normalizeLimitePercentual(value: number | undefined): number {
|
|
const fallback = 50;
|
|
if (value === undefined) return fallback;
|
|
if (!Number.isFinite(value)) return fallback;
|
|
// Normalizar para faixa 0..100
|
|
if (value < 0) return 0;
|
|
if (value > 100) return 100;
|
|
return value;
|
|
}
|
|
|
|
function computeLimitePermitido(quantidadeTotal: number, limitePercentual: number): number {
|
|
if (!Number.isFinite(quantidadeTotal) || quantidadeTotal <= 0) return 0;
|
|
const pct = normalizeLimitePercentual(limitePercentual);
|
|
return Math.floor(quantidadeTotal * (pct / 100));
|
|
}
|
|
|
|
async function getAtaObjetoLinkOrThrow(
|
|
ctx: MutationCtx,
|
|
ataId: Id<'atas'>,
|
|
objetoId: Id<'objetos'>
|
|
): Promise<Doc<'atasObjetos'>> {
|
|
const link = await ctx.db
|
|
.query('atasObjetos')
|
|
.withIndex('by_ataId_and_objetoId', (q) => q.eq('ataId', ataId).eq('objetoId', objetoId))
|
|
.unique();
|
|
|
|
if (!link) {
|
|
throw new Error('Esta ata não está vinculada a este objeto.');
|
|
}
|
|
return link;
|
|
}
|
|
|
|
async function computeQuantidadeUsadaFromDb(
|
|
ctx: MutationCtx,
|
|
ataId: Id<'atas'>,
|
|
objetoId: Id<'objetos'>
|
|
): Promise<number> {
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_ataId_and_objetoId', (q) => q.eq('ataId', ataId).eq('objetoId', objetoId))
|
|
.collect();
|
|
|
|
const sumByPedidoId = new Map<Id<'pedidos'>, number>();
|
|
for (const item of items) {
|
|
const prev = sumByPedidoId.get(item.pedidoId) ?? 0;
|
|
sumByPedidoId.set(item.pedidoId, prev + item.quantidade);
|
|
}
|
|
|
|
let total = 0;
|
|
for (const [pedidoId, sum] of sumByPedidoId.entries()) {
|
|
const pedido = await ctx.db.get(pedidoId);
|
|
if (pedido && pedido.status !== 'cancelado') {
|
|
total += sum;
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
async function getAtaObjetoUsageInfo(
|
|
ctx: MutationCtx,
|
|
ataId: Id<'atas'>,
|
|
objetoId: Id<'objetos'>,
|
|
requireConfigured: boolean
|
|
): Promise<AtaObjetoUsageInfo> {
|
|
const link = await getAtaObjetoLinkOrThrow(ctx, ataId, objetoId);
|
|
|
|
const quantidadeTotalRaw = link.quantidadeTotal;
|
|
const hasQuantidadeTotal =
|
|
quantidadeTotalRaw !== undefined &&
|
|
Number.isFinite(quantidadeTotalRaw) &&
|
|
quantidadeTotalRaw > 0;
|
|
|
|
if (requireConfigured && !hasQuantidadeTotal) {
|
|
throw new Error(
|
|
'Esta ata está vinculada ao objeto, mas a quantidade do produto na ata não foi configurada. Configure a quantidade/limite antes de usar esta ata.'
|
|
);
|
|
}
|
|
|
|
const limitePercentual = normalizeLimitePercentual(link.limitePercentual);
|
|
const quantidadeTotal = hasQuantidadeTotal ? (quantidadeTotalRaw as number) : 0;
|
|
const limitePermitido = computeLimitePermitido(quantidadeTotal, limitePercentual);
|
|
|
|
let quantidadeUsada = link.quantidadeUsada;
|
|
if (quantidadeUsada === undefined) {
|
|
quantidadeUsada = await computeQuantidadeUsadaFromDb(ctx, ataId, objetoId);
|
|
await ctx.db.patch(link._id, { quantidadeUsada });
|
|
}
|
|
|
|
const restante = Math.max(0, limitePermitido - quantidadeUsada);
|
|
|
|
return {
|
|
linkId: link._id,
|
|
quantidadeTotal,
|
|
limitePercentual,
|
|
limitePermitido,
|
|
quantidadeUsada,
|
|
restante
|
|
};
|
|
}
|
|
|
|
async function assertAtaObjetoCanConsume(
|
|
ctx: MutationCtx,
|
|
ataId: Id<'atas'>,
|
|
objetoId: Id<'objetos'>,
|
|
delta: number
|
|
) {
|
|
if (!Number.isFinite(delta) || delta <= 0) return;
|
|
|
|
// Bloqueia consumo se a ata estiver fora da vigência (considerando prorrogação).
|
|
const ata = await ctx.db.get(ataId);
|
|
if (!ata) {
|
|
throw new Error('Ata não encontrada.');
|
|
}
|
|
const dataProrrogacao = (ata as { dataProrrogacao?: string }).dataProrrogacao ?? null;
|
|
const dataFimEfetiva = maxYMD(ata.dataFim ?? null, dataProrrogacao);
|
|
const today = getTodayYMD();
|
|
const vigenteHoje = isWithinRangeYMD(today, ata.dataInicio ?? null, dataFimEfetiva);
|
|
if (!vigenteHoje) {
|
|
const inicioLabel = ata.dataInicio ?? 'sem início';
|
|
const fimLabel = dataFimEfetiva ?? 'sem fim';
|
|
throw new Error(
|
|
`Não é possível consumir esta ata pois ela está fora da vigência. Vigência: ${inicioLabel} até ${fimLabel}.`
|
|
);
|
|
}
|
|
|
|
const info = await getAtaObjetoUsageInfo(ctx, ataId, objetoId, true);
|
|
if (info.quantidadeUsada + delta > info.limitePermitido) {
|
|
throw new Error(
|
|
`Limite de uso da ata atingido para este objeto. Limite permitido: ${info.limitePermitido}. Usado: ${info.quantidadeUsada}. Tentativa de adicionar: ${delta}. Restante: ${info.restante}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
async function applyAtaObjetoUsageDelta(
|
|
ctx: MutationCtx,
|
|
ataId: Id<'atas'>,
|
|
objetoId: Id<'objetos'>,
|
|
delta: number
|
|
) {
|
|
if (!Number.isFinite(delta) || delta === 0) return;
|
|
const info = await getAtaObjetoUsageInfo(ctx, ataId, objetoId, delta > 0);
|
|
|
|
const novoUsadoRaw = info.quantidadeUsada + delta;
|
|
const novoUsado = Math.max(0, novoUsadoRaw);
|
|
|
|
if (delta > 0 && novoUsado > info.limitePermitido) {
|
|
throw new Error(
|
|
`Limite de uso da ata atingido para este objeto. Limite permitido: ${info.limitePermitido}. Usado: ${info.quantidadeUsada}. Tentativa de adicionar: ${delta}. Restante: ${info.restante}.`
|
|
);
|
|
}
|
|
|
|
await ctx.db.patch(info.linkId, { quantidadeUsada: novoUsado });
|
|
}
|
|
|
|
async function isFuncionarioInComprasSector(
|
|
ctx: QueryCtx | MutationCtx,
|
|
funcionarioId: Id<'funcionarios'>
|
|
) {
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) return false;
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
return !!isInSector;
|
|
}
|
|
|
|
async function isUsuarioEnvolvidoNoPedido(
|
|
ctx: QueryCtx | MutationCtx,
|
|
pedidoId: Id<'pedidos'>,
|
|
user: Awaited<ReturnType<typeof getUsuarioAutenticado>>
|
|
) {
|
|
const pedido = await ctx.db.get(pedidoId);
|
|
if (!pedido) return false;
|
|
|
|
// Criador do pedido (por usuarioId)
|
|
if (pedido.criadoPor === user._id) return true;
|
|
|
|
// Envolvimento por itens (requer funcionarioId)
|
|
if (!user.funcionarioId) return false;
|
|
|
|
const hasItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedidoId))
|
|
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
|
|
.first();
|
|
|
|
return !!hasItem;
|
|
}
|
|
|
|
async function assertPodeGerenciarDocumentosDoPedido(
|
|
ctx: QueryCtx | MutationCtx,
|
|
pedidoId: Id<'pedidos'>,
|
|
user: Awaited<ReturnType<typeof getUsuarioAutenticado>>
|
|
) {
|
|
const isEnvolvido = await isUsuarioEnvolvidoNoPedido(ctx, pedidoId, user);
|
|
if (isEnvolvido) return;
|
|
|
|
if (user.funcionarioId) {
|
|
const isCompras = await isFuncionarioInComprasSector(ctx, user.funcionarioId);
|
|
if (isCompras) return;
|
|
}
|
|
|
|
throw new Error('Acesso negado.');
|
|
}
|
|
|
|
// ========== QUERIES ==========
|
|
|
|
export const list = query({
|
|
args: {
|
|
statuses: v.optional(v.array(pedidoStatusValidator)),
|
|
numeroSei: v.optional(v.string()),
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
periodoInicio: v.optional(v.number()),
|
|
periodoFim: v.optional(v.number())
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: pedidoStatusValidator,
|
|
// acaoId removed from return
|
|
criadoPor: v.id('usuarios'),
|
|
criadoPorNome: v.string(),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
aceitoPorNome: v.optional(v.string()),
|
|
descricaoAjuste: v.optional(v.string()),
|
|
concluidoEm: v.optional(v.number()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const base = await fetchPedidosBase(ctx, args);
|
|
const pedidos = applyPedidoFilters(base, args);
|
|
|
|
return await Promise.all(
|
|
pedidos.map(async (p) => {
|
|
const creator = await ctx.db.get(p.criadoPor);
|
|
const aceito = p.aceitoPor ? await ctx.db.get(p.aceitoPor) : null;
|
|
return {
|
|
_id: p._id,
|
|
_creationTime: p._creationTime,
|
|
numeroSei: p.numeroSei,
|
|
numeroDfd: p.numeroDfd,
|
|
status: p.status,
|
|
criadoPor: p.criadoPor,
|
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
|
aceitoPor: p.aceitoPor,
|
|
aceitoPorNome: aceito?.nome || undefined,
|
|
descricaoAjuste: p.descricaoAjuste,
|
|
concluidoEm: p.concluidoEm,
|
|
criadoEm: p.criadoEm,
|
|
atualizadoEm: p.atualizadoEm
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const get = query({
|
|
args: { id: v.id('pedidos') },
|
|
returns: v.union(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: pedidoStatusValidator,
|
|
acaoId: v.optional(v.id('acoes')),
|
|
criadoPor: v.id('usuarios'),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
descricaoAjuste: v.optional(v.string()),
|
|
concluidoEm: v.optional(v.number()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
}),
|
|
v.null()
|
|
),
|
|
handler: async (ctx, args) => {
|
|
return await ctx.db.get(args.id);
|
|
}
|
|
});
|
|
|
|
export const getItems = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('objetoItems'),
|
|
_creationTime: v.number(),
|
|
pedidoId: v.id('pedidos'),
|
|
objetoId: v.id('objetos'),
|
|
ataId: v.optional(v.id('atas')),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
modalidade: v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
),
|
|
valorEstimado: v.string(),
|
|
valorReal: v.optional(v.string()),
|
|
quantidade: v.number(),
|
|
adicionadoPor: v.id('funcionarios'),
|
|
adicionadoPorNome: v.string(),
|
|
criadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.collect();
|
|
|
|
// Get employee names
|
|
const itemsWithNames = await Promise.all(
|
|
items.map(async (item) => {
|
|
const funcionario = await ctx.db.get(item.adicionadoPor);
|
|
return {
|
|
...item,
|
|
// Se a modalidade ainda não foi definida, expõe como "consumo" para manter compatibilidade com UI existente
|
|
modalidade: (item.modalidade ?? 'consumo') as
|
|
| 'dispensa'
|
|
| 'inexgibilidade'
|
|
| 'adesao'
|
|
| 'consumo',
|
|
adicionadoPorNome: funcionario?.nome || 'Desconhecido'
|
|
};
|
|
})
|
|
);
|
|
|
|
return itemsWithNames;
|
|
}
|
|
});
|
|
|
|
export const getHistory = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
handler: async (ctx, args) => {
|
|
const history = await ctx.db
|
|
.query('historicoPedidos')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.order('desc')
|
|
.collect();
|
|
|
|
// Get user names
|
|
const historyWithNames = await Promise.all(
|
|
history.map(async (entry) => {
|
|
const usuario = await ctx.db.get(entry.usuarioId);
|
|
return {
|
|
_id: entry._id,
|
|
_creationTime: entry._creationTime,
|
|
pedidoId: entry.pedidoId,
|
|
usuarioId: entry.usuarioId,
|
|
usuarioNome: usuario?.nome || 'Desconhecido',
|
|
acao: entry.acao,
|
|
detalhes: entry.detalhes,
|
|
data: entry.data
|
|
};
|
|
})
|
|
);
|
|
|
|
return historyWithNames;
|
|
}
|
|
});
|
|
|
|
export const checkExisting = query({
|
|
args: {
|
|
numeroSei: v.optional(v.string()),
|
|
// Modalidade removida do filtro - agora busca apenas por objetoId
|
|
itensFiltro: v.optional(
|
|
v.array(
|
|
v.object({
|
|
objetoId: v.id('objetos')
|
|
})
|
|
)
|
|
)
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: v.union(
|
|
v.literal('em_rascunho'),
|
|
v.literal('aguardando_aceite'),
|
|
v.literal('em_analise'),
|
|
v.literal('precisa_ajustes'),
|
|
v.literal('cancelado'),
|
|
v.literal('concluido')
|
|
),
|
|
criadoPor: v.id('usuarios'),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
descricaoAjuste: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number(),
|
|
matchingItems: v.optional(
|
|
v.array(
|
|
v.object({
|
|
objetoId: v.id('objetos'),
|
|
quantidade: v.number()
|
|
})
|
|
)
|
|
)
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const user = await getCurrentUserFunction(ctx);
|
|
if (!user) return [];
|
|
|
|
const openStatuses: Array<
|
|
'em_rascunho' | 'aguardando_aceite' | 'em_analise' | 'precisa_ajustes'
|
|
> = ['em_rascunho', 'aguardando_aceite', 'em_analise', 'precisa_ajustes'];
|
|
|
|
// 1) Buscar todos os pedidos "abertos" usando o índice by_status
|
|
let pedidosAbertos: Doc<'pedidos'>[] = [];
|
|
for (const status of openStatuses) {
|
|
const partial = await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_status', (q) => q.eq('status', status))
|
|
.collect();
|
|
pedidosAbertos = pedidosAbertos.concat(partial);
|
|
}
|
|
|
|
// 2) Filtros opcionais: numeroSei
|
|
pedidosAbertos = pedidosAbertos.filter((p) => {
|
|
if (args.numeroSei && p.numeroSei !== args.numeroSei) return false;
|
|
return true;
|
|
});
|
|
|
|
// 3) Filtro por itens (apenas objetoId), se informado, e coleta de matchingItems
|
|
const resultados = [];
|
|
|
|
const itensFiltro = args.itensFiltro ?? [];
|
|
|
|
for (const pedido of pedidosAbertos) {
|
|
let include = true;
|
|
let matchingItems: {
|
|
objetoId: Id<'objetos'>;
|
|
quantidade: number;
|
|
}[] = [];
|
|
|
|
// Se houver filtro de itens, verificamos se o pedido tem ALGUM dos itens (apenas objetoId)
|
|
if (itensFiltro.length > 0) {
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedido._id))
|
|
.collect();
|
|
|
|
const matching = items.filter((i) => itensFiltro.some((f) => f.objetoId === i.objetoId));
|
|
|
|
if (matching.length > 0) {
|
|
matchingItems = matching.map((i) => ({
|
|
objetoId: i.objetoId,
|
|
quantidade: i.quantidade
|
|
}));
|
|
} else {
|
|
include = false;
|
|
}
|
|
}
|
|
|
|
if (include) {
|
|
resultados.push({
|
|
_id: pedido._id,
|
|
_creationTime: pedido._creationTime,
|
|
numeroSei: pedido.numeroSei,
|
|
numeroDfd: pedido.numeroDfd,
|
|
status: pedido.status,
|
|
criadoPor: pedido.criadoPor,
|
|
aceitoPor: pedido.aceitoPor,
|
|
descricaoAjuste: pedido.descricaoAjuste,
|
|
criadoEm: pedido.criadoEm,
|
|
atualizadoEm: pedido.atualizadoEm,
|
|
matchingItems: matchingItems.length > 0 ? matchingItems : undefined
|
|
});
|
|
}
|
|
}
|
|
|
|
return resultados;
|
|
}
|
|
});
|
|
|
|
export const listForAcceptance = query({
|
|
args: {},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: v.string(),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoPorNome: v.string(),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// Security Check: Must be in Compras Sector
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) return [];
|
|
|
|
if (!user.funcionarioId) return [];
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) return [];
|
|
|
|
// Fetch orders waiting for acceptance
|
|
const orders = await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_status', (q) => q.eq('status', 'aguardando_aceite'))
|
|
.collect();
|
|
|
|
// Enrich with creator name
|
|
return await Promise.all(
|
|
orders.map(async (o) => {
|
|
const creator = await ctx.db.get(o.criadoPor);
|
|
return {
|
|
_id: o._id,
|
|
_creationTime: o._creationTime,
|
|
numeroSei: o.numeroSei,
|
|
numeroDfd: o.numeroDfd,
|
|
status: o.status,
|
|
criadoPor: o.criadoPor,
|
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
|
aceitoPor: o.aceitoPor,
|
|
descricaoAjuste: o.descricaoAjuste,
|
|
criadoEm: o.criadoEm,
|
|
atualizadoEm: o.atualizadoEm
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const listMyAnalysis = query({
|
|
args: {},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: v.string(),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoPorNome: v.string(),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
descricaoAjuste: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// Security Check: Must be in Compras Sector
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) return [];
|
|
|
|
if (!user.funcionarioId) return [];
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) return [];
|
|
|
|
// Fetch orders accepted by this user
|
|
// We don't have an index on aceitoPor yet, but we can filter or add index.
|
|
// Ideally we should add an index, but for now let's filter since volume might be low per user.
|
|
// Wait, we can't filter efficiently without index if table is huge.
|
|
// Let's assume we should add index or filter in memory if small.
|
|
// Given the schema change was just adding the field, let's filter.
|
|
// Actually, let's add the index in the schema update if possible, but I already did that step.
|
|
// I'll filter for now.
|
|
|
|
const orders = await ctx.db
|
|
.query('pedidos')
|
|
.filter((q) => q.eq(q.field('aceitoPor'), user.funcionarioId))
|
|
.collect();
|
|
|
|
return await Promise.all(
|
|
orders.map(async (o) => {
|
|
const creator = await ctx.db.get(o.criadoPor);
|
|
return {
|
|
_id: o._id,
|
|
_creationTime: o._creationTime,
|
|
numeroSei: o.numeroSei,
|
|
numeroDfd: o.numeroDfd,
|
|
status: o.status,
|
|
criadoPor: o.criadoPor,
|
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
|
aceitoPor: o.aceitoPor,
|
|
descricaoAjuste: o.descricaoAjuste,
|
|
criadoEm: o.criadoEm,
|
|
atualizadoEm: o.atualizadoEm
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const listByItemCreator = query({
|
|
args: {
|
|
statuses: v.optional(v.array(pedidoStatusValidator)),
|
|
numeroSei: v.optional(v.string()),
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
periodoInicio: v.optional(v.number()),
|
|
periodoFim: v.optional(v.number())
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: pedidoStatusValidator,
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoPorNome: v.string(),
|
|
aceitoPorNome: v.optional(v.string()),
|
|
descricaoAjuste: v.optional(v.string()),
|
|
concluidoEm: v.optional(v.number()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
if (!user.funcionarioId) return [];
|
|
|
|
// Find all items added by this user
|
|
const myItems = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_adicionadoPor', (q) => q.eq('adicionadoPor', user.funcionarioId!))
|
|
.collect();
|
|
|
|
// Get unique pedidoIds
|
|
const pedidoIds = [...new Set(myItems.map((i) => i.pedidoId))];
|
|
|
|
// Fetch orders
|
|
const orders = await Promise.all(pedidoIds.map((id) => ctx.db.get(id)));
|
|
|
|
// Filter out nulls and enrich
|
|
const validOrders = orders.filter((o) => o !== null);
|
|
const filtered = applyPedidoFilters(validOrders as Doc<'pedidos'>[], args);
|
|
|
|
return await Promise.all(
|
|
filtered.map(async (o) => {
|
|
const creator = await ctx.db.get(o.criadoPor);
|
|
const aceito = o.aceitoPor ? await ctx.db.get(o.aceitoPor) : null;
|
|
return {
|
|
_id: o._id,
|
|
_creationTime: o._creationTime,
|
|
numeroSei: o.numeroSei,
|
|
numeroDfd: o.numeroDfd,
|
|
status: o.status,
|
|
criadoPor: o.criadoPor,
|
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
|
aceitoPor: o.aceitoPor,
|
|
aceitoPorNome: aceito?.nome || undefined,
|
|
descricaoAjuste: o.descricaoAjuste,
|
|
concluidoEm: o.concluidoEm,
|
|
criadoEm: o.criadoEm,
|
|
atualizadoEm: o.atualizadoEm
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const gerarRelatorio = query({
|
|
args: {
|
|
statuses: v.optional(v.array(pedidoStatusValidator)),
|
|
numeroSei: v.optional(v.string()),
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
periodoInicio: v.optional(v.number()),
|
|
periodoFim: v.optional(v.number())
|
|
},
|
|
returns: v.object({
|
|
filtros: v.object({
|
|
statuses: v.optional(v.array(pedidoStatusValidator)),
|
|
numeroSei: v.optional(v.string()),
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
periodoInicio: v.optional(v.number()),
|
|
periodoFim: v.optional(v.number())
|
|
}),
|
|
resumo: v.object({
|
|
totalPedidos: v.number(),
|
|
totalItens: v.number(),
|
|
totalDocumentos: v.number(),
|
|
totalPorStatus: v.array(v.object({ status: pedidoStatusValidator, count: v.number() })),
|
|
totalValorEstimado: v.number(),
|
|
totalValorReal: v.number()
|
|
}),
|
|
pedidos: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string()),
|
|
status: pedidoStatusValidator,
|
|
criadoPor: v.id('usuarios'),
|
|
criadoPorNome: v.string(),
|
|
aceitoPor: v.optional(v.id('funcionarios')),
|
|
aceitoPorNome: v.optional(v.string()),
|
|
descricaoAjuste: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
concluidoEm: v.optional(v.number()),
|
|
atualizadoEm: v.number(),
|
|
itensCount: v.number(),
|
|
documentosCount: v.number(),
|
|
valorEstimadoTotal: v.number(),
|
|
valorRealTotal: v.number()
|
|
})
|
|
),
|
|
itens: v.array(
|
|
v.object({
|
|
_id: v.id('objetoItems'),
|
|
pedidoId: v.id('pedidos'),
|
|
pedidoNumeroSei: v.optional(v.string()),
|
|
pedidoStatus: pedidoStatusValidator,
|
|
objetoId: v.id('objetos'),
|
|
objetoNome: v.optional(v.string()),
|
|
ataId: v.optional(v.id('atas')),
|
|
ataNumero: v.optional(v.string()),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
acaoNome: v.optional(v.string()),
|
|
modalidade: v.optional(
|
|
v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
)
|
|
),
|
|
quantidade: v.number(),
|
|
valorEstimado: v.string(),
|
|
valorReal: v.optional(v.string()),
|
|
adicionadoPor: v.id('funcionarios'),
|
|
adicionadoPorNome: v.string(),
|
|
criadoEm: v.number()
|
|
})
|
|
)
|
|
}),
|
|
handler: async (ctx, args) => {
|
|
// Para relatório "por período", exigimos pelo menos um limite (início ou fim)
|
|
if (args.periodoInicio === undefined && args.periodoFim === undefined) {
|
|
throw new Error('Informe um período (início e/ou fim) para gerar o relatório.');
|
|
}
|
|
|
|
const base = await fetchPedidosBase(ctx, args);
|
|
const pedidosFiltrados = applyPedidoFilters(base, args);
|
|
|
|
// Guardrail para evitar timeouts/relatórios gigantescos
|
|
if (pedidosFiltrados.length > 500) {
|
|
throw new Error(
|
|
`Relatório muito grande (${pedidosFiltrados.length} pedidos). Reduza o período/filtros e tente novamente.`
|
|
);
|
|
}
|
|
|
|
// Cache para evitar múltiplos gets repetidos
|
|
const cacheUsuarios = new Map<string, Doc<'usuarios'> | null>();
|
|
const cacheFuncionarios = new Map<string, Doc<'funcionarios'> | null>();
|
|
const cacheObjetos = new Map<string, Doc<'objetos'> | null>();
|
|
const cacheAtas = new Map<string, Doc<'atas'> | null>();
|
|
const cacheAcoes = new Map<string, Doc<'acoes'> | null>();
|
|
|
|
async function getUsuario(id: Id<'usuarios'>): Promise<Doc<'usuarios'> | null> {
|
|
const k = String(id);
|
|
if (cacheUsuarios.has(k)) return cacheUsuarios.get(k) ?? null;
|
|
const doc = await ctx.db.get(id);
|
|
cacheUsuarios.set(k, doc);
|
|
return doc;
|
|
}
|
|
async function getFuncionario(id: Id<'funcionarios'>): Promise<Doc<'funcionarios'> | null> {
|
|
const k = String(id);
|
|
if (cacheFuncionarios.has(k)) return cacheFuncionarios.get(k) ?? null;
|
|
const doc = await ctx.db.get(id);
|
|
cacheFuncionarios.set(k, doc);
|
|
return doc;
|
|
}
|
|
async function getObjeto(id: Id<'objetos'>): Promise<Doc<'objetos'> | null> {
|
|
const k = String(id);
|
|
if (cacheObjetos.has(k)) return cacheObjetos.get(k) ?? null;
|
|
const doc = await ctx.db.get(id);
|
|
cacheObjetos.set(k, doc);
|
|
return doc;
|
|
}
|
|
async function getAta(id: Id<'atas'>): Promise<Doc<'atas'> | null> {
|
|
const k = String(id);
|
|
if (cacheAtas.has(k)) return cacheAtas.get(k) ?? null;
|
|
const doc = await ctx.db.get(id);
|
|
cacheAtas.set(k, doc);
|
|
return doc;
|
|
}
|
|
async function getAcao(id: Id<'acoes'>): Promise<Doc<'acoes'> | null> {
|
|
const k = String(id);
|
|
if (cacheAcoes.has(k)) return cacheAcoes.get(k) ?? null;
|
|
const doc = await ctx.db.get(id);
|
|
cacheAcoes.set(k, doc);
|
|
return doc;
|
|
}
|
|
|
|
function parseValorMoeda(input: string | undefined): number {
|
|
if (!input) return 0;
|
|
const s = input
|
|
.replace(/\s/g, '')
|
|
.replace(/[Rr]\$?/g, '')
|
|
.replace(/\./g, '')
|
|
.replace(',', '.')
|
|
.replace(/[^0-9.-]/g, '');
|
|
const n = Number(s);
|
|
return Number.isFinite(n) ? n : 0;
|
|
}
|
|
|
|
const itensOut: Array<{
|
|
_id: Id<'objetoItems'>;
|
|
pedidoId: Id<'pedidos'>;
|
|
pedidoNumeroSei?: string;
|
|
pedidoStatus: PedidoStatus;
|
|
objetoId: Id<'objetos'>;
|
|
objetoNome?: string;
|
|
ataId?: Id<'atas'>;
|
|
ataNumero?: string;
|
|
acaoId?: Id<'acoes'>;
|
|
acaoNome?: string;
|
|
modalidade: Doc<'objetoItems'>['modalidade'];
|
|
quantidade: number;
|
|
valorEstimado: string;
|
|
valorReal?: string;
|
|
adicionadoPor: Id<'funcionarios'>;
|
|
adicionadoPorNome: string;
|
|
criadoEm: number;
|
|
}> = [];
|
|
|
|
let totalItens = 0;
|
|
let totalDocumentos = 0;
|
|
let totalValorEstimado = 0;
|
|
let totalValorReal = 0;
|
|
|
|
const statusCounts = new Map<PedidoStatus, number>();
|
|
for (const s of [
|
|
'em_rascunho',
|
|
'aguardando_aceite',
|
|
'em_analise',
|
|
'precisa_ajustes',
|
|
'cancelado',
|
|
'concluido'
|
|
] as PedidoStatus[]) {
|
|
statusCounts.set(s, 0);
|
|
}
|
|
|
|
const pedidosOut = await Promise.all(
|
|
pedidosFiltrados.map(async (p) => {
|
|
statusCounts.set(p.status, (statusCounts.get(p.status) ?? 0) + 1);
|
|
|
|
const creator = await getUsuario(p.criadoPor);
|
|
const aceito = p.aceitoPor ? await getFuncionario(p.aceitoPor) : null;
|
|
|
|
const itens = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', p._id))
|
|
.collect();
|
|
|
|
const docs = await ctx.db
|
|
.query('pedidoDocumentos')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', p._id))
|
|
.collect();
|
|
|
|
totalItens += itens.length;
|
|
totalDocumentos += docs.length;
|
|
|
|
let valorEstimadoTotal = 0;
|
|
let valorRealTotal = 0;
|
|
|
|
for (const it of itens) {
|
|
valorEstimadoTotal += parseValorMoeda(it.valorEstimado);
|
|
valorRealTotal += parseValorMoeda(it.valorReal);
|
|
|
|
const funcionario = await getFuncionario(it.adicionadoPor);
|
|
const objeto = await getObjeto(it.objetoId);
|
|
const ata = it.ataId ? await getAta(it.ataId) : null;
|
|
const acao = it.acaoId ? await getAcao(it.acaoId) : null;
|
|
|
|
itensOut.push({
|
|
_id: it._id,
|
|
pedidoId: it.pedidoId,
|
|
pedidoNumeroSei: p.numeroSei,
|
|
pedidoStatus: p.status,
|
|
objetoId: it.objetoId,
|
|
objetoNome: objeto?.nome ?? undefined,
|
|
ataId: it.ataId,
|
|
ataNumero: ata?.numero ?? undefined,
|
|
acaoId: it.acaoId,
|
|
acaoNome: acao?.nome ?? undefined,
|
|
modalidade: (it.modalidade ?? 'consumo') as
|
|
| 'dispensa'
|
|
| 'inexgibilidade'
|
|
| 'adesao'
|
|
| 'consumo',
|
|
quantidade: it.quantidade,
|
|
valorEstimado: it.valorEstimado,
|
|
valorReal: it.valorReal,
|
|
adicionadoPor: it.adicionadoPor,
|
|
adicionadoPorNome: funcionario?.nome || 'Desconhecido',
|
|
criadoEm: it.criadoEm
|
|
});
|
|
}
|
|
|
|
totalValorEstimado += valorEstimadoTotal;
|
|
totalValorReal += valorRealTotal;
|
|
|
|
return {
|
|
_id: p._id,
|
|
_creationTime: p._creationTime,
|
|
numeroSei: p.numeroSei,
|
|
numeroDfd: p.numeroDfd,
|
|
status: p.status,
|
|
criadoPor: p.criadoPor,
|
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
|
aceitoPor: p.aceitoPor,
|
|
aceitoPorNome: aceito?.nome || undefined,
|
|
descricaoAjuste: p.descricaoAjuste,
|
|
criadoEm: p.criadoEm,
|
|
concluidoEm: p.concluidoEm,
|
|
atualizadoEm: p.atualizadoEm,
|
|
itensCount: itens.length,
|
|
documentosCount: docs.length,
|
|
valorEstimadoTotal,
|
|
valorRealTotal
|
|
};
|
|
})
|
|
);
|
|
|
|
// Ordenar itens por data (mais recente primeiro)
|
|
itensOut.sort((a, b) => b.criadoEm - a.criadoEm);
|
|
|
|
const totalPorStatus = [...statusCounts.entries()].map(([status, count]) => ({
|
|
status,
|
|
count
|
|
}));
|
|
|
|
return {
|
|
filtros: {
|
|
statuses: args.statuses,
|
|
numeroSei: args.numeroSei,
|
|
criadoPor: args.criadoPor,
|
|
aceitoPor: args.aceitoPor,
|
|
periodoInicio: args.periodoInicio,
|
|
periodoFim: args.periodoFim
|
|
},
|
|
resumo: {
|
|
totalPedidos: pedidosFiltrados.length,
|
|
totalItens,
|
|
totalDocumentos,
|
|
totalPorStatus,
|
|
totalValorEstimado,
|
|
totalValorReal
|
|
},
|
|
pedidos: pedidosOut,
|
|
itens: itensOut
|
|
};
|
|
}
|
|
});
|
|
|
|
export const acceptOrder = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// Security Check: Must be in Compras Sector
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) {
|
|
throw new Error('Setor de compras não configurado.');
|
|
}
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário sem funcionário vinculado.');
|
|
}
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) {
|
|
throw new Error('Você não tem permissão para aceitar pedidos (Setor inválido).');
|
|
}
|
|
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
// Só é permitido adicionar itens em pedidos em rascunho, aguardando aceite ou em análise.
|
|
// Nos demais status (ex.: precisa_ajustes, concluído, cancelado etc.) a inclusão é bloqueada.
|
|
if (
|
|
pedido.status !== 'em_rascunho' &&
|
|
pedido.status !== 'aguardando_aceite' &&
|
|
pedido.status !== 'em_analise'
|
|
) {
|
|
throw new Error('Não é possível adicionar itens neste status de pedido.');
|
|
}
|
|
|
|
if (pedido.status !== 'aguardando_aceite') {
|
|
throw new Error('Este pedido não está aguardando aceite.');
|
|
}
|
|
|
|
if (pedido.aceitoPor) {
|
|
throw new Error('Este pedido já foi aceito por outro funcionário.');
|
|
}
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: 'em_analise',
|
|
aceitoPor: user.funcionarioId,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'aceite_pedido',
|
|
detalhes: JSON.stringify({ aceitoPor: user.funcionarioId }),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== MUTATIONS ==========
|
|
|
|
export const create = mutation({
|
|
args: {
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string())
|
|
// acaoId removed
|
|
},
|
|
returns: v.id('pedidos'),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// 1. Check Config
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) {
|
|
throw new Error('Setor de Compras não configurado. Contate o administrador.');
|
|
}
|
|
|
|
// 2. Check Existing (Double check) - Removed acaoId check here as it's now per item
|
|
|
|
// 3. Create Order
|
|
const pedidoId = await ctx.db.insert('pedidos', {
|
|
numeroSei: args.numeroSei,
|
|
numeroDfd: args.numeroDfd,
|
|
status: 'em_rascunho',
|
|
criadoPor: user._id,
|
|
criadoEm: Date.now(),
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
// 4. Create History
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'criacao',
|
|
detalhes: JSON.stringify({ numeroSei: args.numeroSei }),
|
|
data: Date.now()
|
|
});
|
|
|
|
// 5. Iniciar Fluxo se houver etapas
|
|
await iniciarFluxoPedidoInternal(ctx, pedidoId, user._id);
|
|
|
|
return pedidoId;
|
|
}
|
|
});
|
|
|
|
export const updateSeiNumber = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
numeroSei: v.string()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido not found');
|
|
|
|
// Check if SEI number is already taken by another order
|
|
const existing = await ctx.db
|
|
.query('pedidos')
|
|
.filter((q) =>
|
|
q.and(q.eq(q.field('numeroSei'), args.numeroSei), q.neq(q.field('_id'), args.pedidoId))
|
|
)
|
|
.first();
|
|
|
|
if (existing) {
|
|
throw new Error('Este número SEI já está em uso por outro pedido.');
|
|
}
|
|
|
|
const oldSei = pedido.numeroSei;
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
numeroSei: args.numeroSei,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'atualizacao_sei',
|
|
detalhes: JSON.stringify({ de: oldSei, para: args.numeroSei }),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const updateDfdNumber = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
numeroDfd: v.string()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
// Verificar se o pedido está em um status que permite edição
|
|
if (pedido.status !== 'em_rascunho' && pedido.status !== 'precisa_ajustes') {
|
|
throw new Error(
|
|
'Apenas pedidos em rascunho ou que precisam de ajustes podem ter o número DFD atualizado.'
|
|
);
|
|
}
|
|
|
|
const oldDfd = pedido.numeroDfd;
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
numeroDfd: args.numeroDfd,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'atualizacao_dfd',
|
|
detalhes: JSON.stringify({ de: oldDfd, para: args.numeroDfd }),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const addItem = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
objetoId: v.id('objetos'),
|
|
ataId: v.optional(v.id('atas')),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
// Opcional: permite criar itens sem definir modalidade upfront (ex: geração via planejamento).
|
|
modalidade: v.optional(
|
|
v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
)
|
|
),
|
|
valorEstimado: v.string(),
|
|
quantidade: v.number()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// Ensure user has a funcionarioId linked
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
// Get all items of this product added by this user in this order
|
|
const userProductItems = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) =>
|
|
q.and(
|
|
q.eq(q.field('objetoId'), args.objetoId),
|
|
q.eq(q.field('adicionadoPor'), user.funcionarioId)
|
|
)
|
|
)
|
|
.collect();
|
|
|
|
// Se não foi informada, tenta inferir a modalidade a partir de itens já adicionados por este usuário
|
|
// (evita conflito de "mesmo produto com outra combinação" quando modalidade estiver vazia).
|
|
const modalidade =
|
|
args.modalidade ?? userProductItems.find((i) => !!i.modalidade)?.modalidade ?? undefined;
|
|
|
|
// Regra global: todos os itens do pedido devem ter a mesma ata (quando houver).
|
|
await ensurePedidoAtaConsistency(ctx, args.pedidoId, args.ataId);
|
|
|
|
// Bloqueia ataId se não for Compras em análise
|
|
if (args.ataId) {
|
|
const config = await ctx.db.query('config').first();
|
|
let isInComprasSector = false;
|
|
if (config?.comprasSetorId) {
|
|
const funcionarioSetores = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
isInComprasSector = !!funcionarioSetores;
|
|
}
|
|
if (!(pedido.status === 'em_analise' && isInComprasSector)) {
|
|
throw new Error(
|
|
'Apenas funcionários do Setor de Compras podem vincular uma Ata, e somente quando o pedido está em análise.'
|
|
);
|
|
}
|
|
}
|
|
|
|
// --- CHECK ANALYSIS MODE ---
|
|
// Em pedidos em análise, a inclusão de itens deve passar por fluxo de aprovação.
|
|
// Em rascunho ou aguardando aceite, a inclusão é direta, sem necessidade de aprovação.
|
|
if (pedido.status === 'em_analise') {
|
|
if (args.ataId) {
|
|
// Não altera consumo aqui (ainda é só solicitação), mas valida limite/configuração.
|
|
await assertAtaObjetoCanConsume(ctx, args.ataId, args.objetoId, args.quantidade);
|
|
}
|
|
await ctx.db.insert('solicitacoesItens', {
|
|
pedidoId: args.pedidoId,
|
|
tipo: 'adicao',
|
|
dados: JSON.stringify({ ...args, modalidade }),
|
|
status: 'pendente',
|
|
solicitadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
return;
|
|
}
|
|
|
|
// --- CHECK DUPLICATES (Same Product + User, Different Config) ---
|
|
const conflict = userProductItems.find(
|
|
(i) => i.modalidade !== modalidade || i.ataId !== args.ataId
|
|
);
|
|
|
|
if (conflict) {
|
|
throw new Error(
|
|
'Este pedido já possui este produto com outra combinação de modalidade e/ou ata para você. Todos os itens do mesmo produto devem usar a mesma modalidade e ata neste pedido. Ajuste o item existente ou crie um novo pedido para a nova combinação.'
|
|
);
|
|
}
|
|
|
|
// Check if item already exists with SAME parameters (exact match) to increment
|
|
const existingItem = userProductItems.find(
|
|
(i) => i.acaoId === args.acaoId && i.ataId === args.ataId && i.modalidade === modalidade
|
|
);
|
|
|
|
if (existingItem) {
|
|
if (args.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, args.ataId, args.objetoId, args.quantidade);
|
|
}
|
|
// Increment quantity
|
|
const novaQuantidade = existingItem.quantidade + args.quantidade;
|
|
await ctx.db.patch(existingItem._id, { quantidade: novaQuantidade });
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'adicao_item_incremento',
|
|
detalhes: JSON.stringify({
|
|
objetoId: args.objetoId,
|
|
quantidadeAdicionada: args.quantidade,
|
|
novaQuantidade
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
} else {
|
|
if (args.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, args.ataId, args.objetoId, args.quantidade);
|
|
}
|
|
// Insert new item
|
|
await ctx.db.insert('objetoItems', {
|
|
pedidoId: args.pedidoId,
|
|
objetoId: args.objetoId,
|
|
ataId: args.ataId,
|
|
acaoId: args.acaoId,
|
|
...(modalidade ? { modalidade } : {}),
|
|
valorEstimado: args.valorEstimado,
|
|
quantidade: args.quantidade,
|
|
adicionadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'adicao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: args.objetoId,
|
|
valor: args.valorEstimado,
|
|
quantidade: args.quantidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId,
|
|
modalidade: modalidade ?? null
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
|
|
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
|
|
}
|
|
});
|
|
|
|
export const updateItemQuantity = mutation({
|
|
args: {
|
|
itemId: v.id('objetoItems'),
|
|
novaQuantidade: v.number()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
const item = await ctx.db.get(args.itemId);
|
|
if (!item) throw new Error('Item não encontrado.');
|
|
|
|
const pedido = await ctx.db.get(item.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
const delta = args.novaQuantidade - item.quantidade;
|
|
|
|
// --- CHECK ANALYSIS / ACCEPTANCE MODE ---
|
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
|
if (delta > 0 && item.ataId) {
|
|
// Não altera consumo aqui (ainda é só solicitação), mas valida limite/configuração.
|
|
await assertAtaObjetoCanConsume(ctx, item.ataId, item.objetoId, delta);
|
|
}
|
|
await ctx.db.insert('solicitacoesItens', {
|
|
pedidoId: item.pedidoId,
|
|
tipo: 'alteracao_quantidade',
|
|
dados: JSON.stringify({ itemId: args.itemId, novaQuantidade: args.novaQuantidade }),
|
|
status: 'pendente',
|
|
solicitadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
return;
|
|
}
|
|
|
|
const quantidadeAnterior = item.quantidade;
|
|
|
|
// Check permission: only item owner can change quantity
|
|
const isOwner = item.adicionadoPor === user.funcionarioId;
|
|
|
|
if (!isOwner) {
|
|
throw new Error('Apenas quem adicionou este item pode alterar a quantidade.');
|
|
}
|
|
|
|
// Atualiza consumo (ata+objeto) antes de persistir a nova quantidade
|
|
if (item.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, item.ataId, item.objetoId, delta);
|
|
}
|
|
|
|
// Update quantity
|
|
await ctx.db.patch(args.itemId, { quantidade: args.novaQuantidade });
|
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
// Create history entry
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: item.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_quantidade',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
quantidadeAnterior,
|
|
novaQuantidade: args.novaQuantidade
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const removeItem = mutation({
|
|
args: {
|
|
itemId: v.id('objetoItems')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
const item = await ctx.db.get(args.itemId);
|
|
if (!item) throw new Error('Item not found');
|
|
|
|
const pedido = await ctx.db.get(item.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
// --- CHECK ANALYSIS / ACCEPTANCE MODE ---
|
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
await ctx.db.insert('solicitacoesItens', {
|
|
pedidoId: item.pedidoId,
|
|
tipo: 'exclusao',
|
|
dados: JSON.stringify({ itemId: args.itemId }),
|
|
status: 'pendente',
|
|
solicitadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (item.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, item.ataId, item.objetoId, -item.quantidade);
|
|
}
|
|
|
|
await ctx.db.delete(args.itemId);
|
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: item.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'remocao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
valor: item.valorEstimado
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const removeItemsBatch = mutation({
|
|
args: {
|
|
itemIds: v.array(v.id('objetoItems'))
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
if (args.itemIds.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const firstItem = await ctx.db.get(args.itemIds[0]);
|
|
if (!firstItem) {
|
|
throw new Error('Item não encontrado.');
|
|
}
|
|
|
|
const pedidoId = firstItem.pedidoId;
|
|
const pedido = await ctx.db.get(pedidoId);
|
|
if (!pedido) {
|
|
throw new Error('Pedido não encontrado.');
|
|
}
|
|
|
|
if (
|
|
pedido.status !== 'em_rascunho' &&
|
|
pedido.status !== 'precisa_ajustes' &&
|
|
pedido.status !== 'em_analise'
|
|
) {
|
|
throw new Error(
|
|
'Só é possível remover itens em pedidos em rascunho, em análise ou que precisam de ajustes.'
|
|
);
|
|
}
|
|
|
|
for (const itemId of args.itemIds) {
|
|
const item = await ctx.db.get(itemId);
|
|
if (!item) continue;
|
|
|
|
if (item.pedidoId !== pedidoId) {
|
|
throw new Error('Todos os itens devem pertencer ao mesmo pedido.');
|
|
}
|
|
|
|
if (item.adicionadoPor !== user.funcionarioId) {
|
|
throw new Error('Você só pode remover itens que você adicionou.');
|
|
}
|
|
|
|
if (item.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, item.ataId, item.objetoId, -item.quantidade);
|
|
}
|
|
|
|
await ctx.db.delete(itemId);
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'remocao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
valor: item.valorEstimado
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
|
|
await ctx.db.patch(pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
export const splitPedido = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
itemIds: v.array(v.id('objetoItems')),
|
|
numeroSei: v.optional(v.string()),
|
|
numeroDfd: v.optional(v.string())
|
|
},
|
|
returns: v.id('pedidos'),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
if (args.itemIds.length === 0) {
|
|
throw new Error('Selecione ao menos um item para dividir o pedido.');
|
|
}
|
|
|
|
const pedidoOriginal = await ctx.db.get(args.pedidoId);
|
|
if (!pedidoOriginal) {
|
|
throw new Error('Pedido não encontrado.');
|
|
}
|
|
|
|
if (pedidoOriginal.status !== 'em_rascunho' && pedidoOriginal.status !== 'precisa_ajustes') {
|
|
throw new Error('Só é possível dividir pedidos em rascunho ou que precisam de ajustes.');
|
|
}
|
|
|
|
const itens = [];
|
|
for (const itemId of args.itemIds) {
|
|
const item = await ctx.db.get(itemId);
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
if (item.pedidoId !== args.pedidoId) {
|
|
throw new Error('Todos os itens devem pertencer ao mesmo pedido.');
|
|
}
|
|
if (item.adicionadoPor !== user.funcionarioId) {
|
|
throw new Error('Você só pode mover itens que você adicionou.');
|
|
}
|
|
itens.push(item);
|
|
}
|
|
|
|
if (itens.length === 0) {
|
|
throw new Error('Nenhum dos itens selecionados pôde ser usado para divisão.');
|
|
}
|
|
|
|
const novoPedidoId = await ctx.db.insert('pedidos', {
|
|
numeroSei: args.numeroSei,
|
|
numeroDfd: args.numeroDfd,
|
|
status: 'em_rascunho',
|
|
criadoPor: user._id,
|
|
criadoEm: Date.now(),
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
for (const item of itens) {
|
|
await ctx.db.patch(item._id, {
|
|
pedidoId: novoPedidoId
|
|
});
|
|
}
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'divisao_pedido_origem',
|
|
detalhes: JSON.stringify({
|
|
itensMovidos: itens.map((i) => i._id),
|
|
novoPedidoId
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: novoPedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'divisao_pedido_destino',
|
|
detalhes: JSON.stringify({
|
|
pedidoOriginalId: args.pedidoId,
|
|
itensRecebidos: itens.map((i) => i._id)
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
return novoPedidoId;
|
|
}
|
|
});
|
|
|
|
export const updateItem = mutation({
|
|
args: {
|
|
itemId: v.id('objetoItems'),
|
|
valorEstimado: v.string(),
|
|
modalidade: v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
ataId: v.optional(v.id('atas'))
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
const item = await ctx.db.get(args.itemId);
|
|
if (!item) throw new Error('Item não encontrado.');
|
|
|
|
const pedido = await ctx.db.get(item.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
// Apenas quem adicionou o item pode editá-lo (outras propriedades)
|
|
const isOwner = item.adicionadoPor === user.funcionarioId;
|
|
|
|
// Verificar permissão para editar modalidade e ata
|
|
// Somente Setor de Compras pode editar modalidade e ata, e apenas quando em análise
|
|
const config = await ctx.db.query('config').first();
|
|
let isInComprasSector = false;
|
|
if (config?.comprasSetorId) {
|
|
const funcionarioSetores = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
isInComprasSector = !!funcionarioSetores;
|
|
}
|
|
|
|
const canEditComprasFields = pedido.status === 'em_analise' && isInComprasSector;
|
|
|
|
// Se está tentando editar a modalidade ou a ata, verificar permissão
|
|
const isChangingModalidade = item.modalidade !== args.modalidade;
|
|
const isChangingAta = (item.ataId ?? null) !== (args.ataId ?? null);
|
|
if ((isChangingModalidade || isChangingAta) && !canEditComprasFields) {
|
|
throw new Error(
|
|
'Apenas funcionários do Setor de Compras podem editar a modalidade ou a ata, e somente quando o pedido está em análise.'
|
|
);
|
|
}
|
|
|
|
// Para outras propriedades, apenas o dono do item pode editar
|
|
if (!isOwner && !canEditComprasFields) {
|
|
throw new Error('Apenas quem adicionou este item pode editá-lo.');
|
|
}
|
|
|
|
const oldValues = {
|
|
valorEstimado: item.valorEstimado,
|
|
modalidade: item.modalidade,
|
|
acaoId: 'acaoId' in item ? item.acaoId : undefined,
|
|
ataId: 'ataId' in item ? item.ataId : undefined
|
|
};
|
|
|
|
// Regra global: ao alterar um item, garantir que os demais itens do pedido
|
|
// continuem (ou passem a estar) com a mesma ata.
|
|
await ensurePedidoAtaConsistency(ctx, item.pedidoId, args.ataId, args.itemId);
|
|
|
|
// Em pedidos em análise ou aguardando aceite, geramos uma solicitação em vez de alterar diretamente
|
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
|
const oldAtaId = oldValues.ataId;
|
|
const newAtaId = args.ataId;
|
|
if (newAtaId && newAtaId !== oldAtaId) {
|
|
// Não altera consumo aqui (ainda é só solicitação), mas valida limite/configuração.
|
|
await assertAtaObjetoCanConsume(ctx, newAtaId, item.objetoId, item.quantidade);
|
|
}
|
|
|
|
await ctx.db.insert('solicitacoesItens', {
|
|
pedidoId: item.pedidoId,
|
|
tipo: 'alteracao_detalhes',
|
|
dados: JSON.stringify({
|
|
itemId: args.itemId,
|
|
de: oldValues,
|
|
para: {
|
|
valorEstimado: args.valorEstimado,
|
|
modalidade: args.modalidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId
|
|
}
|
|
}),
|
|
status: 'pendente',
|
|
solicitadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Se a ata mudou, mover consumo entre vínculos (ata antiga -> nova ata)
|
|
const oldAtaId = oldValues.ataId;
|
|
const newAtaId = args.ataId;
|
|
if ((oldAtaId ?? null) !== (newAtaId ?? null)) {
|
|
if (newAtaId) {
|
|
await applyAtaObjetoUsageDelta(ctx, newAtaId, item.objetoId, item.quantidade);
|
|
}
|
|
if (oldAtaId) {
|
|
await applyAtaObjetoUsageDelta(ctx, oldAtaId, item.objetoId, -item.quantidade);
|
|
}
|
|
}
|
|
|
|
await ctx.db.patch(args.itemId, {
|
|
valorEstimado: args.valorEstimado,
|
|
modalidade: args.modalidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId
|
|
});
|
|
|
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: item.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'edicao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
de: oldValues,
|
|
para: {
|
|
valorEstimado: args.valorEstimado,
|
|
modalidade: args.modalidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId
|
|
}
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const getPermissions = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
|
|
if (!pedido || !user.funcionarioId) {
|
|
return {
|
|
canSendToAcceptance: false,
|
|
canStartAnalysis: false,
|
|
canConclude: false,
|
|
canRequestAdjustments: false,
|
|
canCancel: false,
|
|
canCompleteAdjustments: false,
|
|
canManageRequests: false,
|
|
canEditModalidade: false,
|
|
canEditAta: false,
|
|
currentFuncionarioId: user?.funcionarioId ?? null
|
|
};
|
|
}
|
|
|
|
// Check Compras Sector
|
|
let isInComprasSector = false;
|
|
const config = await ctx.db.query('config').first();
|
|
if (config && config.comprasSetorId) {
|
|
const funcionarioSetores = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
isInComprasSector = !!funcionarioSetores;
|
|
}
|
|
|
|
// Check if user has added items
|
|
// Optimized: Check if ANY item in this order was added by this user
|
|
const userItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
|
|
.first();
|
|
const hasAddedItems = !!userItem;
|
|
|
|
// Verificar se existem itens adicionados por OUTROS usuários.
|
|
const foreignItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) => q.neq(q.field('adicionadoPor'), user.funcionarioId))
|
|
.first();
|
|
const hasOnlyCreatorItems = !foreignItem;
|
|
|
|
const isCreator = pedido.criadoPor === user._id;
|
|
|
|
return {
|
|
canSendToAcceptance: pedido.status === 'em_rascunho' && hasAddedItems,
|
|
canStartAnalysis: pedido.status === 'aguardando_aceite' && isInComprasSector,
|
|
canConclude: pedido.status === 'em_analise' && isInComprasSector,
|
|
canRequestAdjustments:
|
|
pedido.status === 'em_analise' &&
|
|
isInComprasSector &&
|
|
pedido.aceitoPor === user.funcionarioId,
|
|
canCompleteAdjustments: pedido.status === 'precisa_ajustes' && hasAddedItems,
|
|
canCancel:
|
|
pedido.status !== 'cancelado' &&
|
|
pedido.status !== 'concluido' &&
|
|
isCreator &&
|
|
hasOnlyCreatorItems,
|
|
canManageRequests: pedido.status === 'em_analise' && isInComprasSector,
|
|
// Somente Setor de Compras pode editar modalidade e ata, e apenas quando o pedido está em análise
|
|
canEditModalidade: pedido.status === 'em_analise' && isInComprasSector,
|
|
canEditAta: pedido.status === 'em_analise' && isInComprasSector,
|
|
currentFuncionarioId: user.funcionarioId
|
|
};
|
|
}
|
|
});
|
|
|
|
export const enviarParaAceite = mutation({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
if (pedido.status !== 'em_rascunho') {
|
|
throw new Error('Status inválido para envio.');
|
|
}
|
|
|
|
// Check if user has added items
|
|
const userItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
|
|
.first();
|
|
|
|
if (!userItem) {
|
|
throw new Error(
|
|
'Você precisa ter adicionado ao menos um item ao pedido para enviá-lo para aceite.'
|
|
);
|
|
}
|
|
|
|
// Validar que numeroSei e numeroDfd estão preenchidos
|
|
if (!pedido.numeroSei || !pedido.numeroSei.trim()) {
|
|
throw new Error('O número SEI deve ser informado antes de enviar o pedido para aceite.');
|
|
}
|
|
if (!pedido.numeroDfd || !pedido.numeroDfd.trim()) {
|
|
throw new Error('O número DFD deve ser informado antes de enviar o pedido para aceite.');
|
|
}
|
|
|
|
const oldStatus = pedido.status;
|
|
const newStatus = 'aguardando_aceite';
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: newStatus,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_status',
|
|
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
|
|
data: Date.now()
|
|
});
|
|
|
|
// Garantir que o fluxo foi iniciado
|
|
await iniciarFluxoPedidoInternal(ctx, args.pedidoId, user._id);
|
|
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus,
|
|
actorId: user._id
|
|
});
|
|
}
|
|
});
|
|
|
|
export const iniciarAnalise = mutation({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
if (pedido.status !== 'aguardando_aceite') {
|
|
throw new Error('O pedido não está aguardando aceite.');
|
|
}
|
|
|
|
// Security Check: Must be in Compras Sector
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) throw new Error('Setor de compras não configurado.');
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) throw new Error('Acesso não autorizado (Setor de Compras).');
|
|
|
|
const oldStatus = pedido.status;
|
|
const newStatus = 'em_analise';
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: newStatus,
|
|
aceitoPor: user.funcionarioId, // Also mark as accepted by this user
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_status',
|
|
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus,
|
|
actorId: user._id
|
|
});
|
|
}
|
|
});
|
|
|
|
export const concluirPedido = mutation({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
if (pedido.status !== 'em_analise') {
|
|
throw new Error('O pedido deve estar em análise para ser concluído.');
|
|
}
|
|
|
|
// Security Check: Must be in Compras Sector
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) throw new Error('Setor de compras não configurado.');
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) throw new Error('Acesso não autorizado (Setor de Compras).');
|
|
|
|
const oldStatus = pedido.status;
|
|
const newStatus = 'concluido';
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: newStatus,
|
|
concluidoEm: Date.now(),
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_status',
|
|
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus,
|
|
actorId: user._id
|
|
});
|
|
}
|
|
});
|
|
|
|
export const solicitarAjustes = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
descricao: v.string()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
if (pedido.status !== 'em_analise') {
|
|
throw new Error('O pedido deve estar em análise para solicitar ajustes.');
|
|
}
|
|
|
|
// Security Check: Only the user who accepted the order can request adjustments
|
|
if (pedido.aceitoPor !== user.funcionarioId) {
|
|
throw new Error('Apenas o funcionário que aceitou o pedido pode solicitar ajustes.');
|
|
}
|
|
|
|
const oldStatus = pedido.status;
|
|
const newStatus = 'precisa_ajustes';
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: newStatus,
|
|
descricaoAjuste: args.descricao,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'solicitacao_ajuste',
|
|
detalhes: JSON.stringify({
|
|
de: oldStatus,
|
|
para: newStatus,
|
|
descricao: args.descricao
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus,
|
|
actorId: user._id,
|
|
customMessage: args.descricao
|
|
});
|
|
}
|
|
});
|
|
|
|
export const concluirAjustes = mutation({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
if (pedido.status !== 'precisa_ajustes') {
|
|
throw new Error('O pedido não está aguardando ajustes.');
|
|
}
|
|
|
|
// Security Check: Must have items in the order
|
|
const requestorItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
|
|
.first();
|
|
|
|
if (!requestorItem) {
|
|
throw new Error('Você deve ter itens no pedido para concluir os ajustes.');
|
|
}
|
|
|
|
const oldStatus = pedido.status;
|
|
const newStatus = 'em_analise';
|
|
const descricaoAnterior = pedido.descricaoAjuste;
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: newStatus,
|
|
descricaoAjuste: undefined, // Clear the adjustment description from the active field
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'conclusao_ajustes',
|
|
detalhes: JSON.stringify({
|
|
de: oldStatus,
|
|
para: newStatus,
|
|
descricaoResolvida: descricaoAnterior
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus,
|
|
actorId: user._id,
|
|
customMessage: 'Ajustes concluídos. O pedido retornou para análise.'
|
|
});
|
|
}
|
|
});
|
|
|
|
export const cancelarPedido = mutation({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
if (pedido.status === 'concluido' || pedido.status === 'cancelado') {
|
|
throw new Error('Pedido já finalizado.');
|
|
}
|
|
|
|
// Regra: apenas o criador pode cancelar o pedido
|
|
const isCreator = pedido.criadoPor === user._id;
|
|
if (!isCreator) {
|
|
throw new Error('Apenas quem criou este pedido pode cancelá-lo.');
|
|
}
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário sem funcionário vinculado. Não é possível cancelar este pedido.');
|
|
}
|
|
|
|
// Regra extra: o pedido só pode ser cancelado se todos os itens
|
|
// tiverem sido adicionados pelo mesmo funcionário do criador.
|
|
const foreignItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) => q.neq(q.field('adicionadoPor'), user.funcionarioId))
|
|
.first();
|
|
|
|
if (foreignItem) {
|
|
throw new Error(
|
|
'Não é possível cancelar este pedido porque há itens adicionados por outros usuários.'
|
|
);
|
|
}
|
|
|
|
const oldStatus = pedido.status;
|
|
const newStatus = 'cancelado';
|
|
|
|
// Ao cancelar, liberar consumo de todas as combinações (ataId,objetoId) deste pedido
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.collect();
|
|
|
|
for (const item of items) {
|
|
if (item.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, item.ataId, item.objetoId, -item.quantidade);
|
|
}
|
|
}
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: newStatus,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_status',
|
|
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus,
|
|
actorId: user._id
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== INTERNAL (NOTIFICATIONS) ==========
|
|
|
|
export const notifyStatusChange = internalMutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
oldStatus: v.string(),
|
|
newStatus: v.string(),
|
|
actorId: v.id('usuarios'),
|
|
customMessage: v.optional(v.string())
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) return;
|
|
|
|
const actor = await ctx.db.get(args.actorId);
|
|
const actorName = actor ? actor.nome : 'Alguém';
|
|
|
|
const recipients = new Set<string>(); // Set of User IDs
|
|
|
|
// 1. If status is "aguardando_aceite", notify Purchasing Sector
|
|
if (args.newStatus === 'aguardando_aceite') {
|
|
const config = await ctx.db.query('config').first();
|
|
if (config && config.comprasSetorId) {
|
|
// Find all employees in this sector
|
|
const funcionarioSetores = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_setorId', (q) => q.eq('setorId', config.comprasSetorId!))
|
|
.collect();
|
|
|
|
const funcionarioIds = funcionarioSetores.map((fs) => fs.funcionarioId);
|
|
|
|
// Find users linked to these employees
|
|
for (const fId of funcionarioIds) {
|
|
const user = await ctx.db
|
|
.query('usuarios')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', fId))
|
|
.first();
|
|
if (user) recipients.add(user._id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Notify "Involved" users (Creator + Item Adders)
|
|
// Always notify creator (unless they are the actor)
|
|
if (pedido.criadoPor !== args.actorId) {
|
|
recipients.add(pedido.criadoPor);
|
|
}
|
|
|
|
// Notify item adders
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.collect();
|
|
|
|
for (const item of items) {
|
|
const user = await ctx.db
|
|
.query('usuarios')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', item.adicionadoPor))
|
|
.first();
|
|
if (user && user._id !== args.actorId) {
|
|
recipients.add(user._id);
|
|
}
|
|
}
|
|
|
|
let description = `Status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`;
|
|
if (args.customMessage) {
|
|
description += `\n\nDetalhes: ${args.customMessage}`;
|
|
}
|
|
|
|
// Send Notifications
|
|
for (const recipientId of recipients) {
|
|
const recipientIdTyped = recipientId as Id<'usuarios'>;
|
|
|
|
// 1. In-App Notification
|
|
await ctx.db.insert('notificacoes', {
|
|
usuarioId: recipientIdTyped,
|
|
tipo: 'alerta_seguranca', // Using alerta_seguranca as the closest match for system notifications
|
|
titulo: `Pedido ${pedido.numeroSei || 'sem número SEI'} atualizado`,
|
|
descricao: description,
|
|
lida: false,
|
|
criadaEm: Date.now(),
|
|
remetenteId: args.actorId
|
|
});
|
|
|
|
// 2. Email Notification (Async)
|
|
const recipientUser = await ctx.db.get(recipientIdTyped);
|
|
if (recipientUser && recipientUser.email) {
|
|
// Using enfileirarEmail directly
|
|
await ctx.scheduler.runAfter(0, api.email.enfileirarEmail, {
|
|
destinatario: recipientUser.email,
|
|
destinatarioId: recipientIdTyped,
|
|
assunto: `Atualização no Pedido ${pedido.numeroSei || 'sem número SEI'}`,
|
|
corpo: `O pedido ${pedido.numeroSei || 'sem número SEI'} teve seu status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.<br/><br/>${args.customMessage ? `<strong>Detalhes:</strong> ${args.customMessage}` : ''}`,
|
|
enviadoPor: args.actorId
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
// ========== REQUESTS MANAGEMENT ==========
|
|
|
|
export const getItemRequests = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
handler: async (ctx, args) => {
|
|
const requests = await ctx.db
|
|
.query('solicitacoesItens')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) => q.eq(q.field('status'), 'pendente')) // Only pending for now? Or history? User said "manter um historico"
|
|
// Actually the table has status. I should probably return all, or just pending for the main list.
|
|
// The prompt says "quando aceita... exclua as solicitacoes" (or "manter um historico"?).
|
|
// Prompt: "quando aceita... adicione os items automaticamente, e exclua as solicitacoes. deve tambem manter um historico."
|
|
// "Exclua as solicitacoes" implies deleting the row from 'solicitacoesItens' OR just marking as processed.
|
|
// "Manter um historico" might refer to 'historicoPedidos' (the central history).
|
|
// I'll stick to marking as approved/rejected in 'solicitacoesItens' OR deleting and logging to 'historicoPedidos'.
|
|
// But for now, let's just return pending ones for the UI to be clean.
|
|
.collect();
|
|
|
|
// Enrich names
|
|
return await Promise.all(
|
|
requests.map(async (r) => {
|
|
const func = await ctx.db.get(r.solicitadoPor);
|
|
return {
|
|
...r,
|
|
solicitadoPorNome: func?.nome || 'Desconhecido'
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const approveItemRequest = mutation({
|
|
args: {
|
|
requestId: v.id('solicitacoesItens')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const funcionarioId = user.funcionarioId;
|
|
if (!funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
const pedido = await ctx.db.get(request.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
// Security: Must be Compras Sector AND accepted by this user (or just in sector?)
|
|
// Usually Analysis rights.
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) throw new Error('Setor de compras não configurado.');
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) throw new Error('Acesso negado.');
|
|
|
|
// Documentos anexados à solicitação (se houver) devem migrar para o pedido ao aprovar.
|
|
const solicitacaoDocs = await ctx.db
|
|
.query('solicitacoesItensDocumentos')
|
|
.withIndex('by_requestId', (q) => q.eq('requestId', request._id))
|
|
.collect();
|
|
|
|
// Apply the change
|
|
const data = JSON.parse(request.dados);
|
|
|
|
if (request.tipo === 'adicao') {
|
|
// Reuse addItem logic (simplified: insert or increment)
|
|
// We trust the request data structure matches addItem args
|
|
const newItem = data;
|
|
|
|
// Regra global: todos os itens do pedido devem ter a mesma ata (quando houver).
|
|
await ensurePedidoAtaConsistency(ctx, request.pedidoId, newItem.ataId);
|
|
// Note: We MUST use the original requester's ID (request.solicitadoPor) as addedBy?
|
|
// Or should we attribute it to the requester? YES.
|
|
// BUT `addItem` logic usually checks if `existingItem.adicionadoPor === user`.
|
|
// Since we want to merge with the requester's items, we should check `request.solicitadoPor`.
|
|
|
|
const userProductItems = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', request.pedidoId))
|
|
.filter((q) =>
|
|
q.and(
|
|
q.eq(q.field('objetoId'), newItem.objetoId),
|
|
q.eq(q.field('adicionadoPor'), request.solicitadoPor)
|
|
)
|
|
)
|
|
.collect();
|
|
|
|
// Duplicate check (should have been caught at request time? Not necessarily if state changed, but let's re-verify or just proceed)
|
|
// If conflict exists now, we might error or just merge.
|
|
// Let's assume strict check:
|
|
const conflict = userProductItems.find(
|
|
(i) => i.modalidade !== newItem.modalidade || i.ataId !== newItem.ataId
|
|
);
|
|
if (conflict) {
|
|
// Reject strictly? Or throw error?
|
|
throw new Error('Conflito detectado: Item com configuração diferente já existe.');
|
|
}
|
|
|
|
const existingItem = userProductItems.find(
|
|
(i) =>
|
|
i.acaoId === newItem.acaoId &&
|
|
i.ataId === newItem.ataId &&
|
|
i.modalidade === newItem.modalidade
|
|
);
|
|
|
|
if (newItem.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, newItem.ataId, newItem.objetoId, newItem.quantidade);
|
|
}
|
|
|
|
if (existingItem) {
|
|
await ctx.db.patch(existingItem._id, {
|
|
quantidade: existingItem.quantidade + newItem.quantidade
|
|
});
|
|
// Logs attached to REQUESTER or APPROVER? Usually Approver took action, but it was on behalf of requester.
|
|
// Let's log 'aprovacao_solicitacao'
|
|
} else {
|
|
await ctx.db.insert('objetoItems', {
|
|
pedidoId: request.pedidoId,
|
|
objetoId: newItem.objetoId,
|
|
ataId: newItem.ataId,
|
|
acaoId: newItem.acaoId,
|
|
modalidade: newItem.modalidade,
|
|
valorEstimado: newItem.valorEstimado,
|
|
quantidade: newItem.quantidade,
|
|
adicionadoPor: request.solicitadoPor, // Important: Attribute to requester
|
|
criadoEm: Date.now()
|
|
});
|
|
}
|
|
} else if (request.tipo === 'alteracao_quantidade') {
|
|
const { itemId, novaQuantidade } = data as {
|
|
itemId: Id<'objetoItems'>;
|
|
novaQuantidade: number;
|
|
};
|
|
const item = (await ctx.db.get(itemId)) as Doc<'objetoItems'> | null;
|
|
if (item) {
|
|
const delta = novaQuantidade - item.quantidade;
|
|
if (item.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, item.ataId, item.objetoId, delta);
|
|
}
|
|
await ctx.db.patch(itemId, { quantidade: novaQuantidade });
|
|
}
|
|
} else if (request.tipo === 'exclusao') {
|
|
const { itemId } = data as { itemId: Id<'objetoItems'> };
|
|
const item = (await ctx.db.get(itemId)) as Doc<'objetoItems'> | null;
|
|
if (item) {
|
|
if (item.ataId) {
|
|
await applyAtaObjetoUsageDelta(ctx, item.ataId, item.objetoId, -item.quantidade);
|
|
}
|
|
await ctx.db.delete(itemId);
|
|
}
|
|
} else if (request.tipo === 'alteracao_detalhes') {
|
|
const { itemId, para } = data as {
|
|
itemId: Id<'objetoItems'>;
|
|
para: {
|
|
valorEstimado: string;
|
|
modalidade: Doc<'objetoItems'>['modalidade'];
|
|
acaoId?: Id<'acoes'>;
|
|
ataId?: Id<'atas'>;
|
|
};
|
|
};
|
|
|
|
const item = await ctx.db.get(itemId);
|
|
if (item) {
|
|
// Regra global também se aplica na alteração de detalhes aprovada
|
|
await ensurePedidoAtaConsistency(ctx, item.pedidoId, para.ataId, itemId);
|
|
|
|
const oldAtaId = ('ataId' in item ? item.ataId : undefined) ?? undefined;
|
|
const newAtaId = para.ataId;
|
|
if ((oldAtaId ?? null) !== (newAtaId ?? null)) {
|
|
if (newAtaId) {
|
|
await applyAtaObjetoUsageDelta(ctx, newAtaId, item.objetoId, item.quantidade);
|
|
}
|
|
if (oldAtaId) {
|
|
await applyAtaObjetoUsageDelta(ctx, oldAtaId, item.objetoId, -item.quantidade);
|
|
}
|
|
}
|
|
|
|
await ctx.db.patch(itemId, {
|
|
valorEstimado: para.valorEstimado,
|
|
modalidade: para.modalidade,
|
|
acaoId: para.acaoId,
|
|
ataId: para.ataId
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update request status
|
|
await ctx.db.patch(request._id, { status: 'aprovado' });
|
|
// OR delete it? "exclua as solicitacoes".
|
|
// I'll delete it to keep table clean as requested.
|
|
await ctx.db.delete(request._id);
|
|
|
|
// Migrar docs: cria em pedidoDocumentos e remove os registros da solicitacao
|
|
const documentosMigrados: Array<{ nome: string; descricao: string }> = [];
|
|
if (solicitacaoDocs.length > 0) {
|
|
for (const doc of solicitacaoDocs) {
|
|
await ctx.db.insert('pedidoDocumentos', {
|
|
pedidoId: request.pedidoId,
|
|
descricao: doc.descricao,
|
|
nome: doc.nome,
|
|
storageId: doc.storageId,
|
|
tipo: doc.tipo,
|
|
tamanho: doc.tamanho,
|
|
criadoPor: doc.criadoPor,
|
|
criadoEm: doc.criadoEm,
|
|
origemSolicitacaoId: request._id
|
|
});
|
|
documentosMigrados.push({ nome: doc.nome, descricao: doc.descricao });
|
|
await ctx.db.delete(doc._id);
|
|
}
|
|
}
|
|
|
|
// History
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: request.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'aprovacao_solicitacao',
|
|
detalhes: JSON.stringify({
|
|
requestId: request._id,
|
|
tipo: request.tipo,
|
|
solicitadoPor: request.solicitadoPor,
|
|
dados: data,
|
|
documentosMigrados
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
|
|
await ctx.db.patch(request.pedidoId, { atualizadoEm: Date.now() });
|
|
}
|
|
});
|
|
|
|
export const rejectItemRequest = mutation({
|
|
args: {
|
|
requestId: v.id('solicitacoesItens')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const funcionarioId = user.funcionarioId;
|
|
if (!funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
const request = await ctx.db.get(args.requestId);
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) throw new Error('Config Error');
|
|
|
|
const isInSector = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
.first();
|
|
|
|
if (!isInSector) throw new Error('Acesso negado.');
|
|
|
|
// Remover documentos anexados à solicitação (para não deixar órfãos no storage)
|
|
const solicitacaoDocs = await ctx.db
|
|
.query('solicitacoesItensDocumentos')
|
|
.withIndex('by_requestId', (q) => q.eq('requestId', request._id))
|
|
.collect();
|
|
|
|
const documentosRemovidos: Array<{ nome: string; descricao: string }> = [];
|
|
for (const doc of solicitacaoDocs) {
|
|
documentosRemovidos.push({ nome: doc.nome, descricao: doc.descricao });
|
|
await ctx.storage.delete(doc.storageId);
|
|
await ctx.db.delete(doc._id);
|
|
}
|
|
|
|
// Delete request
|
|
await ctx.db.delete(args.requestId);
|
|
|
|
// History
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: request.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'rejeicao_solicitacao',
|
|
detalhes: JSON.stringify({
|
|
requestId: request._id,
|
|
tipo: request.tipo,
|
|
solicitadoPor: request.solicitadoPor,
|
|
documentosRemovidos
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== DOCUMENTOS (PEDIDO / SOLICITAÇÃO) ==========
|
|
|
|
export const generatePedidoUploadUrl = mutation({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.string(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
|
return await ctx.storage.generateUploadUrl();
|
|
}
|
|
});
|
|
|
|
export const addPedidoDocumento = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
descricao: v.string(),
|
|
nome: v.string(),
|
|
storageId: v.id('_storage'),
|
|
tipo: v.string(),
|
|
tamanho: v.number(),
|
|
origemSolicitacaoId: v.optional(v.id('solicitacoesItens'))
|
|
},
|
|
returns: v.id('pedidoDocumentos'),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário sem funcionário vinculado.');
|
|
}
|
|
|
|
// Garantir que o pedido existe
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
return await ctx.db.insert('pedidoDocumentos', {
|
|
pedidoId: args.pedidoId,
|
|
descricao: args.descricao,
|
|
nome: args.nome,
|
|
storageId: args.storageId,
|
|
tipo: args.tipo,
|
|
tamanho: args.tamanho,
|
|
criadoPor: user.funcionarioId,
|
|
criadoEm: Date.now(),
|
|
origemSolicitacaoId: args.origemSolicitacaoId
|
|
});
|
|
}
|
|
});
|
|
|
|
export const listPedidoDocumentos = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
|
|
|
const docs = await ctx.db
|
|
.query('pedidoDocumentos')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.order('desc')
|
|
.collect();
|
|
|
|
return await Promise.all(
|
|
docs.map(async (doc) => {
|
|
const url = await ctx.storage.getUrl(doc.storageId);
|
|
const func = await ctx.db.get(doc.criadoPor);
|
|
return {
|
|
...doc,
|
|
criadoPorNome: func?.nome ?? 'Desconhecido',
|
|
url
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const removePedidoDocumento = mutation({
|
|
args: { id: v.id('pedidoDocumentos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
const doc = await ctx.db.get(args.id);
|
|
if (!doc) throw new Error('Documento não encontrado.');
|
|
|
|
// Pode remover se for o autor OU se for envolvido/compras
|
|
if (!user.funcionarioId || doc.criadoPor !== user.funcionarioId) {
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, doc.pedidoId, user);
|
|
}
|
|
|
|
await ctx.storage.delete(doc.storageId);
|
|
await ctx.db.delete(doc._id);
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
export const generateSolicitacaoUploadUrl = mutation({
|
|
args: { requestId: v.id('solicitacoesItens') },
|
|
returns: v.string(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
if (request.tipo !== 'adicao') {
|
|
throw new Error('Apenas solicitações de adição permitem anexar documentos.');
|
|
}
|
|
if (request.status !== 'pendente') {
|
|
throw new Error('Não é possível anexar documentos em uma solicitação não pendente.');
|
|
}
|
|
if (request.solicitadoPor !== user.funcionarioId) {
|
|
throw new Error('Apenas quem criou a solicitação pode anexar documentos.');
|
|
}
|
|
|
|
return await ctx.storage.generateUploadUrl();
|
|
}
|
|
});
|
|
|
|
export const addSolicitacaoDocumento = mutation({
|
|
args: {
|
|
requestId: v.id('solicitacoesItens'),
|
|
descricao: v.string(),
|
|
nome: v.string(),
|
|
storageId: v.id('_storage'),
|
|
tipo: v.string(),
|
|
tamanho: v.number()
|
|
},
|
|
returns: v.id('solicitacoesItensDocumentos'),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
if (request.tipo !== 'adicao') {
|
|
throw new Error('Apenas solicitações de adição permitem anexar documentos.');
|
|
}
|
|
if (request.status !== 'pendente') {
|
|
throw new Error('Não é possível anexar documentos em uma solicitação não pendente.');
|
|
}
|
|
if (request.solicitadoPor !== user.funcionarioId) {
|
|
throw new Error('Apenas quem criou a solicitação pode anexar documentos.');
|
|
}
|
|
|
|
return await ctx.db.insert('solicitacoesItensDocumentos', {
|
|
requestId: args.requestId,
|
|
pedidoId: request.pedidoId,
|
|
descricao: args.descricao,
|
|
nome: args.nome,
|
|
storageId: args.storageId,
|
|
tipo: args.tipo,
|
|
tamanho: args.tamanho,
|
|
criadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const listSolicitacaoDocumentos = query({
|
|
args: { requestId: v.id('solicitacoesItens') },
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const request = await ctx.db.get(args.requestId);
|
|
if (!request) return [];
|
|
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, request.pedidoId, user);
|
|
|
|
const docs = await ctx.db
|
|
.query('solicitacoesItensDocumentos')
|
|
.withIndex('by_requestId', (q) => q.eq('requestId', args.requestId))
|
|
.order('desc')
|
|
.collect();
|
|
|
|
return await Promise.all(
|
|
docs.map(async (doc) => {
|
|
const url = await ctx.storage.getUrl(doc.storageId);
|
|
const func = await ctx.db.get(doc.criadoPor);
|
|
return {
|
|
...doc,
|
|
criadoPorNome: func?.nome ?? 'Desconhecido',
|
|
url
|
|
};
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
export const removeSolicitacaoDocumento = mutation({
|
|
args: { id: v.id('solicitacoesItensDocumentos') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
const doc = await ctx.db.get(args.id);
|
|
if (!doc) throw new Error('Documento não encontrado.');
|
|
|
|
const request = await ctx.db.get(doc.requestId);
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
if (request.tipo !== 'adicao') {
|
|
throw new Error('Apenas solicitações de adição permitem documentos.');
|
|
}
|
|
if (request.status !== 'pendente') {
|
|
throw new Error('Não é possível remover documentos de uma solicitação não pendente.');
|
|
}
|
|
|
|
if (doc.criadoPor !== user.funcionarioId || request.solicitadoPor !== user.funcionarioId) {
|
|
throw new Error('Apenas quem criou a solicitação pode remover documentos.');
|
|
}
|
|
|
|
await ctx.storage.delete(doc.storageId);
|
|
await ctx.db.delete(doc._id);
|
|
|
|
return null;
|
|
}
|
|
});
|