344 lines
9.3 KiB
TypeScript
344 lines
9.3 KiB
TypeScript
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<Id<'atas'>>(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<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;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
});
|