import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import type { Id } from './_generated/dataModel'; import { getCurrentUserFunction } from './auth'; import { internal } from './_generated/api'; function normalizeLimitePercentual(value: number | undefined): number { const fallback = 50; if (value === undefined) return fallback; if (!Number.isFinite(value)) return fallback; if (value < 0) return 0; if (value > 100) return 100; return value; } function assertQuantidadeTotalValida(value: number) { if (!Number.isFinite(value) || value <= 0) { throw new Error('Quantidade do produto na ata deve ser maior que zero.'); } } export const list = query({ args: { periodoInicio: v.optional(v.string()), periodoFim: v.optional(v.string()), numero: v.optional(v.string()), numeroSei: v.optional(v.string()) }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'listar' }); const numero = args.numero?.trim().toLowerCase(); const numeroSei = args.numeroSei?.trim().toLowerCase(); const periodoInicio = args.periodoInicio || undefined; const periodoFim = args.periodoFim || undefined; const atas = await ctx.db.query('atas').collect(); return atas.filter((ata) => { const numeroOk = !numero || (ata.numero || '').toLowerCase().includes(numero); const seiOk = !numeroSei || (ata.numeroSei || '').toLowerCase().includes(numeroSei); // Filtro por intervalo (range): retorna atas cuja vigência intersecta o período informado. // Considera datas como strings "YYYY-MM-DD" (lexicograficamente comparáveis). const ataInicio = ata.dataInicio ?? '0000-01-01'; const ataFimEfetivo = (() => { const a = ata.dataFim; const b = (ata as { dataProrrogacao?: string }).dataProrrogacao; if (!a && !b) return '9999-12-31'; if (!a) return b!; if (!b) return a; return a >= b ? a : b; })(); const periodoOk = (!periodoInicio && !periodoFim) || (periodoInicio && periodoFim && ataInicio <= periodoFim && ataFimEfetivo >= periodoInicio) || (periodoInicio && !periodoFim && ataFimEfetivo >= periodoInicio) || (!periodoInicio && periodoFim && ataInicio <= periodoFim); return numeroOk && seiOk && periodoOk; }); } }); export const get = query({ args: { id: v.id('atas') }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'ver' }); return await ctx.db.get(args.id); } }); export const getObjetos = query({ args: { id: v.id('atas') }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'ver' }); const links = await ctx.db .query('atasObjetos') .withIndex('by_ataId', (q) => q.eq('ataId', args.id)) .collect(); const objetos = await Promise.all(links.map((link) => ctx.db.get(link.objetoId))); return objetos.filter((obj) => obj !== null); } }); export const getObjetosConfig = query({ args: { id: v.id('atas') }, returns: v.array( v.object({ objetoId: v.id('objetos'), quantidadeTotal: v.union(v.number(), v.null()), limitePercentual: v.union(v.number(), v.null()) }) ), handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'ver' }); const links = await ctx.db .query('atasObjetos') .withIndex('by_ataId', (q) => q.eq('ataId', args.id)) .collect(); return links.map((l) => ({ objetoId: l.objetoId, quantidadeTotal: l.quantidadeTotal ?? null, limitePercentual: l.limitePercentual ?? null })); } }); export const listByObjetoIds = query({ args: { objetoIds: v.array(v.id('objetos')) }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'listar' }); if (args.objetoIds.length === 0) return []; // Buscar todos os vínculos ata-objeto para os objetos informados const links = []; for (const objetoId of args.objetoIds) { const partial = await ctx.db .query('atasObjetos') .withIndex('by_objetoId', (q) => q.eq('objetoId', objetoId as Id<'objetos'>)) .collect(); links.push(...partial); } const ataIds = Array.from(new Set(links.map((l) => l.ataId as Id<'atas'>))); if (ataIds.length === 0) return []; const atas = await Promise.all(ataIds.map((id) => ctx.db.get(id))); return atas.filter((a): a is NonNullable => a !== null); } }); export const create = mutation({ args: { numero: v.string(), dataInicio: v.optional(v.string()), dataFim: v.optional(v.string()), dataProrrogacao: v.optional(v.string()), empresaId: v.id('empresas'), numeroSei: v.string(), objetos: v.array( v.object({ objetoId: v.id('objetos'), quantidadeTotal: v.number(), limitePercentual: v.optional(v.number()) }) ) }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'criar' }); const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); const ataId = await ctx.db.insert('atas', { numero: args.numero, numeroSei: args.numeroSei, empresaId: args.empresaId, dataInicio: args.dataInicio, dataFim: args.dataFim, dataProrrogacao: args.dataProrrogacao, criadoPor: user._id, criadoEm: Date.now(), atualizadoEm: Date.now() }); // Vincular objetos for (const cfg of args.objetos) { assertQuantidadeTotalValida(cfg.quantidadeTotal); const limitePercentual = normalizeLimitePercentual(cfg.limitePercentual); await ctx.db.insert('atasObjetos', { ataId, objetoId: cfg.objetoId, quantidadeTotal: cfg.quantidadeTotal, limitePercentual, quantidadeUsada: 0 }); } return ataId; } }); export const update = mutation({ args: { id: v.id('atas'), numero: v.string(), numeroSei: v.string(), empresaId: v.id('empresas'), dataInicio: v.optional(v.string()), dataFim: v.optional(v.string()), dataProrrogacao: v.optional(v.string()), objetos: v.array( v.object({ objetoId: v.id('objetos'), quantidadeTotal: v.number(), limitePercentual: v.optional(v.number()) }) ) }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'editar' }); const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); await ctx.db.patch(args.id, { numero: args.numero, numeroSei: args.numeroSei, empresaId: args.empresaId, dataInicio: args.dataInicio, dataFim: args.dataFim, dataProrrogacao: args.dataProrrogacao, atualizadoEm: Date.now() }); // Atualizar objetos vinculados const existingLinks = await ctx.db .query('atasObjetos') .withIndex('by_ataId', (q) => q.eq('ataId', args.id)) .collect(); const existingByObjeto = new Map, (typeof existingLinks)[number]>(); for (const link of existingLinks) { existingByObjeto.set(link.objetoId, link); } const desiredObjetoIds = new Set>(args.objetos.map((o) => o.objetoId)); // Upsert dos vínculos desejados (preserva quantidadeUsada quando já existe) for (const cfg of args.objetos) { assertQuantidadeTotalValida(cfg.quantidadeTotal); const limitePercentual = normalizeLimitePercentual(cfg.limitePercentual); const existing = existingByObjeto.get(cfg.objetoId); if (existing) { await ctx.db.patch(existing._id, { quantidadeTotal: cfg.quantidadeTotal, limitePercentual }); } else { await ctx.db.insert('atasObjetos', { ataId: args.id, objetoId: cfg.objetoId, quantidadeTotal: cfg.quantidadeTotal, limitePercentual, quantidadeUsada: 0 }); } } // Remoção de vínculos não selecionados (somente se não houver uso em pedidos não-cancelados) for (const link of existingLinks) { if (desiredObjetoIds.has(link.objetoId)) continue; const items = await ctx.db .query('objetoItems') .withIndex('by_ataId_and_objetoId', (q) => q.eq('ataId', args.id).eq('objetoId', link.objetoId) ) .collect(); // Se existe qualquer item em pedido não cancelado, bloquear remoção do vínculo let inUse = false; const seenPedidos = new Set>(); for (const item of items) { if (seenPedidos.has(item.pedidoId)) continue; seenPedidos.add(item.pedidoId); const pedido = await ctx.db.get(item.pedidoId); if (pedido && pedido.status !== 'cancelado') { inUse = true; break; } } if (inUse) { throw new Error( 'Não é possível remover este objeto da ata porque já existe uso em pedidos não cancelados.' ); } await ctx.db.delete(link._id); } } }); export const remove = mutation({ args: { id: v.id('atas') }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'excluir' }); const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); // Remover vínculos com objetos const links = await ctx.db .query('atasObjetos') .withIndex('by_ataId', (q) => q.eq('ataId', args.id)) .collect(); for (const link of links) { await ctx.db.delete(link._id); } // Remover documentos vinculados const docs = await ctx.db .query('atasDocumentos') .withIndex('by_ataId', (q) => q.eq('ataId', args.id)) .collect(); for (const doc of docs) { await ctx.storage.delete(doc.storageId); await ctx.db.delete(doc._id); } await ctx.db.delete(args.id); } }); export const generateUploadUrl = mutation({ args: {}, handler: async (ctx) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'editar' }); return await ctx.storage.generateUploadUrl(); } }); export const saveDocumento = mutation({ args: { ataId: v.id('atas'), nome: v.string(), storageId: v.id('_storage'), tipo: v.string(), tamanho: v.number() }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'editar' }); const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); return await ctx.db.insert('atasDocumentos', { ataId: args.ataId, nome: args.nome, storageId: args.storageId, tipo: args.tipo, tamanho: args.tamanho, criadoPor: user._id, criadoEm: Date.now() }); } }); export const removeDocumento = mutation({ args: { id: v.id('atasDocumentos') }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'editar' }); const user = await getCurrentUserFunction(ctx); if (!user) throw new Error('Unauthorized'); const doc = await ctx.db.get(args.id); if (!doc) throw new Error('Documento não encontrado'); await ctx.storage.delete(doc.storageId); await ctx.db.delete(args.id); } }); export const getDocumentos = query({ args: { ataId: v.id('atas') }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'atas', acao: 'ver' }); const docs = await ctx.db .query('atasDocumentos') .withIndex('by_ataId', (q) => q.eq('ataId', args.ataId)) .collect(); return await Promise.all( docs.map(async (doc) => ({ ...doc, url: await ctx.storage.getUrl(doc.storageId) })) ); } });