import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import type { Id } from './_generated/dataModel'; import type { QueryCtx } from './_generated/server'; import { getCurrentUserFunction } from './auth'; import { addMonthsClampedYMD, getTodayYMD, isWithinRangeYMD, maxYMD } from './utils/datas'; const ataComLimiteValidator = v.object({ _id: v.id('atas'), numero: v.string(), numeroSei: v.string(), dataInicio: v.union(v.string(), v.null()), dataFim: v.union(v.string(), v.null()), dataProrrogacao: v.union(v.string(), v.null()), dataFimEfetiva: v.union(v.string(), v.null()), quantidadeTotal: v.union(v.number(), v.null()), limitePercentual: v.number(), quantidadeUsada: v.number(), limitePermitido: v.number(), restante: v.number(), isLocked: v.boolean(), isVencidaRecentemente: v.boolean(), lockReason: v.union( v.literal('vigencia_expirada'), v.literal('limite_atingido'), v.literal('nao_configurada'), v.null() ) }); async function computeAtasComLimiteForObjeto( ctx: QueryCtx, objetoId: Id<'objetos'>, includeAtaIds?: Id<'atas'>[] ) { const today = getTodayYMD(); const threeMonthsAgo = addMonthsClampedYMD(today, -3); const includeSet = new Set>(includeAtaIds ?? []); const links = await ctx.db .query('atasObjetos') .withIndex('by_objetoId', (q) => q.eq('objetoId', objetoId)) .collect(); const results: Array<{ _id: Id<'atas'>; numero: string; numeroSei: string; dataInicio: string | null; dataFim: string | null; dataProrrogacao: string | null; dataFimEfetiva: string | null; quantidadeTotal: number | null; limitePercentual: number; quantidadeUsada: number; limitePermitido: number; restante: number; isLocked: boolean; isVencidaRecentemente: boolean; lockReason: 'vigencia_expirada' | 'limite_atingido' | 'nao_configurada' | null; }> = []; for (const link of links) { const ata = await ctx.db.get(link.ataId); if (!ata) continue; const dataProrrogacao = (ata as { dataProrrogacao?: string }).dataProrrogacao ?? null; const dataFimEfetiva = maxYMD(ata.dataFim ?? null, dataProrrogacao); const vigenteHoje = isWithinRangeYMD(today, ata.dataInicio ?? null, dataFimEfetiva); const isVencidaRecentemente = !!dataFimEfetiva && dataFimEfetiva < today && dataFimEfetiva >= threeMonthsAgo; // Para seleção em pedidos: manter somente vigentes e vencidas recentemente, // mas permitir incluir atas específicas (ex.: já selecionadas em pedido antigo). const shouldForceInclude = includeSet.has(ata._id); if (!shouldForceInclude && !vigenteHoje && !isVencidaRecentemente) { continue; } const isForaDaVigencia = !vigenteHoje; let quantidadeUsada = link.quantidadeUsada ?? 0; if (link.quantidadeUsada === undefined) { const items = await ctx.db .query('objetoItems') .withIndex('by_ataId_and_objetoId', (q) => q.eq('ataId', link.ataId).eq('objetoId', link.objetoId) ) .collect(); const sumByPedidoId = new Map, 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; } } quantidadeUsada = total; } const quantidadeTotal = link.quantidadeTotal ?? null; const limitePercentualRaw = link.limitePercentual ?? 50; const limitePercentual = Number.isFinite(limitePercentualRaw) ? Math.min(100, Math.max(0, limitePercentualRaw)) : 50; const limitePermitido = quantidadeTotal && quantidadeTotal > 0 ? Math.floor(quantidadeTotal * (limitePercentual / 100)) : 0; const restante = Math.max(0, limitePermitido - quantidadeUsada); const lockReason: 'nao_configurada' | 'limite_atingido' | 'vigencia_expirada' | null = !quantidadeTotal || quantidadeTotal <= 0 ? 'nao_configurada' : quantidadeUsada >= limitePermitido ? 'limite_atingido' : isForaDaVigencia ? 'vigencia_expirada' : null; const isLocked = lockReason !== null; results.push({ _id: ata._id, numero: ata.numero, numeroSei: ata.numeroSei, dataInicio: ata.dataInicio ?? null, dataFim: ata.dataFim ?? null, dataProrrogacao, dataFimEfetiva, quantidadeTotal, limitePercentual, quantidadeUsada, limitePermitido, restante, isLocked, isVencidaRecentemente, lockReason }); } return results; } export const list = query({ args: { nome: v.optional(v.string()), tipo: v.optional(v.union(v.literal('material'), v.literal('servico'))), codigos: v.optional(v.string()) }, handler: async (ctx, args) => { const nome = args.nome?.trim(); const codigos = args.codigos?.trim().toLowerCase(); const base = nome && nome.length > 0 ? await ctx.db .query('objetos') .withSearchIndex('search_nome', (q) => q.search('nome', nome)) .collect() : await ctx.db.query('objetos').collect(); return base.filter((objeto) => { const tipoOk = !args.tipo || objeto.tipo === args.tipo; const codigosOk = !codigos || (objeto.codigoEfisco || '').toLowerCase().includes(codigos) || (objeto.codigoCatmat || '').toLowerCase().includes(codigos) || (objeto.codigoCatserv || '').toLowerCase().includes(codigos); return tipoOk && codigosOk; }); } }); export const search = query({ args: { query: v.string() }, handler: async (ctx, args) => { return await ctx.db .query('objetos') .withSearchIndex('search_nome', (q) => q.search('nome', args.query)) .take(10); } }); export const create = mutation({ args: { nome: v.string(), valorEstimado: v.string(), tipo: v.union(v.literal('material'), v.literal('servico')), codigoEfisco: v.string(), codigoCatmat: v.optional(v.string()), codigoCatserv: v.optional(v.string()), unidade: v.string(), atas: v.optional(v.array(v.id('atas'))) }, handler: async (ctx, args) => { const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); const objetoId = await ctx.db.insert('objetos', { nome: args.nome, valorEstimado: args.valorEstimado, tipo: args.tipo, codigoEfisco: args.codigoEfisco, codigoCatmat: args.codigoCatmat, codigoCatserv: args.codigoCatserv, unidade: args.unidade, criadoPor: user._id, criadoEm: Date.now() }); if (args.atas) { for (const ataId of args.atas) { await ctx.db.insert('atasObjetos', { ataId, objetoId }); } } return objetoId; } }); export const update = mutation({ args: { id: v.id('objetos'), nome: v.string(), valorEstimado: v.string(), tipo: v.union(v.literal('material'), v.literal('servico')), codigoEfisco: v.string(), codigoCatmat: v.optional(v.string()), codigoCatserv: v.optional(v.string()), unidade: v.string(), atas: v.optional(v.array(v.id('atas'))) }, handler: async (ctx, args) => { const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); await ctx.db.patch(args.id, { nome: args.nome, valorEstimado: args.valorEstimado, tipo: args.tipo, codigoEfisco: args.codigoEfisco, codigoCatmat: args.codigoCatmat, codigoCatserv: args.codigoCatserv, unidade: args.unidade }); if (args.atas !== undefined) { // Remove existing links const existingLinks = await ctx.db .query('atasObjetos') .withIndex('by_objetoId', (q) => q.eq('objetoId', args.id)) .collect(); for (const link of existingLinks) { await ctx.db.delete(link._id); } // Add new links for (const ataId of args.atas) { await ctx.db.insert('atasObjetos', { ataId, objetoId: args.id }); } } } }); export const getAtas = query({ args: { objetoId: v.id('objetos') }, handler: async (ctx, args) => { const links = await ctx.db .query('atasObjetos') .withIndex('by_objetoId', (q) => q.eq('objetoId', args.objetoId)) .collect(); const atas = await Promise.all(links.map((link) => ctx.db.get(link.ataId))); return atas.filter((ata) => ata !== null); } }); export const getAtasComLimite = query({ args: { objetoId: v.id('objetos'), // Permite incluir atas específicas (ex.: já selecionadas em um pedido antigo), // mesmo que não estejam vigentes ou não tenham vencido nos últimos 3 meses. includeAtaIds: v.optional(v.array(v.id('atas'))) }, returns: v.array(ataComLimiteValidator), handler: async (ctx, args) => { return await computeAtasComLimiteForObjeto(ctx, args.objetoId, args.includeAtaIds); } }); export const getAtasComLimiteBatch = query({ args: { objetoIds: v.array(v.id('objetos')), includeAtaIds: v.optional(v.array(v.id('atas'))) }, returns: v.array( v.object({ objetoId: v.id('objetos'), atas: v.array(ataComLimiteValidator) }) ), handler: async (ctx, args) => { if (args.objetoIds.length === 0) return []; const include = args.includeAtaIds ?? []; const out = []; for (const objetoId of args.objetoIds) { const atas = await computeAtasComLimiteForObjeto(ctx, objetoId, include); out.push({ objetoId, atas }); } return out; } }); export const remove = mutation({ args: { id: v.id('objetos') }, handler: async (ctx, args) => { const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); await ctx.db.delete(args.id); } });