434 lines
11 KiB
TypeScript
434 lines
11 KiB
TypeScript
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<typeof a> => 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<Id<'objetos'>, (typeof existingLinks)[number]>();
|
|
for (const link of existingLinks) {
|
|
existingByObjeto.set(link.objetoId, link);
|
|
}
|
|
|
|
const desiredObjetoIds = new Set<Id<'objetos'>>(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<Id<'pedidos'>>();
|
|
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)
|
|
}))
|
|
);
|
|
}
|
|
});
|