sistema-de-chamados/convex/emprestimos.ts
esdrasrenan 638faeb287 fix(convex): corrigir memory leak com .collect() sem limite e adicionar otimizacoes
Problema: Convex backend consumindo 16GB+ de RAM causando OOM kills

Correcoes aplicadas:
- Substituido todos os .collect() por .take(LIMIT) em 27+ arquivos
- Adicionado indice by_usbPolicyStatus para otimizar query de maquinas
- Corrigido N+1 problem em alerts.ts usando Map lookup
- Corrigido full table scan em usbPolicy.ts
- Corrigido subscription leaks no frontend (tickets-view, use-ticket-categories)
- Atualizado versao do Convex backend para precompiled-2025-12-04-cc6af4c

Arquivos principais modificados:
- convex/*.ts - limites em todas as queries .collect()
- convex/schema.ts - novo indice by_usbPolicyStatus
- convex/alerts.ts - N+1 fix com Map
- convex/usbPolicy.ts - uso do novo indice
- src/components/tickets/tickets-view.tsx - skip condicional
- src/hooks/use-ticket-categories.ts - skip condicional

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 21:41:30 -03:00

359 lines
10 KiB
TypeScript

import { v } from "convex/values"
import { mutation, query, type QueryCtx, type MutationCtx } from "./_generated/server"
import type { Id } from "./_generated/dataModel"
const EMPRESTIMO_STATUS = ["ATIVO", "DEVOLVIDO", "ATRASADO", "CANCELADO"] as const
type EmprestimoStatus = (typeof EMPRESTIMO_STATUS)[number]
const EQUIPAMENTO_TIPOS = [
"NOTEBOOK",
"DESKTOP",
"MONITOR",
"TECLADO",
"MOUSE",
"HEADSET",
"WEBCAM",
"IMPRESSORA",
"SCANNER",
"PROJETOR",
"TABLET",
"CELULAR",
"ROTEADOR",
"SWITCH",
"OUTRO",
] as const
async function getNextReference(ctx: MutationCtx, tenantId: string): Promise<number> {
const last = await ctx.db
.query("emprestimos")
.withIndex("by_tenant_reference", (q) => q.eq("tenantId", tenantId))
.order("desc")
.first()
return (last?.reference ?? 0) + 1
}
export const list = query({
args: {
tenantId: v.string(),
viewerId: v.id("users"),
status: v.optional(v.string()),
clienteId: v.optional(v.id("companies")),
tecnicoId: v.optional(v.id("users")),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const { tenantId, status, clienteId, tecnicoId, limit = 100 } = args
let baseQuery = ctx.db
.query("emprestimos")
.withIndex("by_tenant_created", (q) => q.eq("tenantId", tenantId))
.order("desc")
const all = await baseQuery.take(limit * 2)
let filtered = all
if (status) {
filtered = filtered.filter((e) => e.status === status)
}
if (clienteId) {
filtered = filtered.filter((e) => e.clienteId === clienteId)
}
if (tecnicoId) {
filtered = filtered.filter((e) => e.tecnicoId === tecnicoId)
}
return filtered.slice(0, limit).map((emprestimo) => ({
id: emprestimo._id,
reference: emprestimo.reference,
clienteId: emprestimo.clienteId,
clienteNome: emprestimo.clienteSnapshot.name,
responsavelNome: emprestimo.responsavelNome,
responsavelContato: emprestimo.responsavelContato,
tecnicoId: emprestimo.tecnicoId,
tecnicoNome: emprestimo.tecnicoSnapshot.name,
tecnicoEmail: emprestimo.tecnicoSnapshot.email,
equipamentos: emprestimo.equipamentos,
quantidade: emprestimo.quantidade,
valor: emprestimo.valor,
dataEmprestimo: emprestimo.dataEmprestimo,
dataFimPrevisto: emprestimo.dataFimPrevisto,
dataDevolucao: emprestimo.dataDevolucao,
status: emprestimo.status,
observacoes: emprestimo.observacoes,
observacoesDevolucao: emprestimo.observacoesDevolucao,
multaDiaria: emprestimo.multaDiaria,
multaCalculada: emprestimo.multaCalculada,
createdAt: emprestimo.createdAt,
updatedAt: emprestimo.updatedAt,
}))
},
})
export const getById = query({
args: {
id: v.id("emprestimos"),
viewerId: v.id("users"),
},
handler: async (ctx, args) => {
const emprestimo = await ctx.db.get(args.id)
if (!emprestimo) return null
return {
id: emprestimo._id,
reference: emprestimo.reference,
clienteId: emprestimo.clienteId,
clienteSnapshot: emprestimo.clienteSnapshot,
responsavelNome: emprestimo.responsavelNome,
responsavelContato: emprestimo.responsavelContato,
tecnicoId: emprestimo.tecnicoId,
tecnicoSnapshot: emprestimo.tecnicoSnapshot,
equipamentos: emprestimo.equipamentos,
quantidade: emprestimo.quantidade,
valor: emprestimo.valor,
dataEmprestimo: emprestimo.dataEmprestimo,
dataFimPrevisto: emprestimo.dataFimPrevisto,
dataDevolucao: emprestimo.dataDevolucao,
status: emprestimo.status,
observacoes: emprestimo.observacoes,
multaDiaria: emprestimo.multaDiaria,
multaCalculada: emprestimo.multaCalculada,
createdBy: emprestimo.createdBy,
createdAt: emprestimo.createdAt,
updatedAt: emprestimo.updatedAt,
}
},
})
export const create = mutation({
args: {
tenantId: v.string(),
createdBy: v.id("users"),
clienteId: v.id("companies"),
responsavelNome: v.string(),
responsavelContato: v.optional(v.string()),
tecnicoId: v.id("users"),
equipamentos: v.array(v.object({
id: v.string(),
tipo: v.string(),
marca: v.string(),
modelo: v.string(),
serialNumber: v.optional(v.string()),
patrimonio: v.optional(v.string()),
})),
valor: v.optional(v.number()),
dataEmprestimo: v.number(),
dataFimPrevisto: v.number(),
observacoes: v.optional(v.string()),
multaDiaria: v.optional(v.number()),
},
handler: async (ctx, args) => {
const now = Date.now()
const reference = await getNextReference(ctx, args.tenantId)
const cliente = await ctx.db.get(args.clienteId)
if (!cliente) {
throw new Error("Cliente nao encontrado")
}
const tecnico = await ctx.db.get(args.tecnicoId)
if (!tecnico) {
throw new Error("Tecnico nao encontrado")
}
const emprestimoId = await ctx.db.insert("emprestimos", {
tenantId: args.tenantId,
reference,
clienteId: args.clienteId,
clienteSnapshot: {
name: cliente.name,
slug: cliente.slug,
},
responsavelNome: args.responsavelNome,
responsavelContato: args.responsavelContato,
tecnicoId: args.tecnicoId,
tecnicoSnapshot: {
name: tecnico.name,
email: tecnico.email,
},
equipamentos: args.equipamentos,
quantidade: args.equipamentos.length,
valor: args.valor,
dataEmprestimo: args.dataEmprestimo,
dataFimPrevisto: args.dataFimPrevisto,
status: "ATIVO",
observacoes: args.observacoes,
multaDiaria: args.multaDiaria,
createdBy: args.createdBy,
createdAt: now,
updatedAt: now,
})
const creator = await ctx.db.get(args.createdBy)
await ctx.db.insert("emprestimoHistorico", {
tenantId: args.tenantId,
emprestimoId,
tipo: "CRIADO",
descricao: `Emprestimo #${reference} criado`,
autorId: args.createdBy,
autorSnapshot: {
name: creator?.name ?? "Sistema",
email: creator?.email,
},
createdAt: now,
})
return { id: emprestimoId, reference }
},
})
export const devolver = mutation({
args: {
id: v.id("emprestimos"),
updatedBy: v.id("users"),
observacoes: v.optional(v.string()),
},
handler: async (ctx, args) => {
const emprestimo = await ctx.db.get(args.id)
if (!emprestimo) {
throw new Error("Emprestimo nao encontrado")
}
if (emprestimo.status === "DEVOLVIDO") {
throw new Error("Emprestimo ja foi devolvido")
}
const now = Date.now()
let multaCalculada: number | undefined
if (emprestimo.multaDiaria && now > emprestimo.dataFimPrevisto) {
const diasAtraso = Math.ceil((now - emprestimo.dataFimPrevisto) / (1000 * 60 * 60 * 24))
multaCalculada = diasAtraso * emprestimo.multaDiaria
}
await ctx.db.patch(args.id, {
status: "DEVOLVIDO",
dataDevolucao: now,
multaCalculada,
observacoesDevolucao: args.observacoes,
updatedBy: args.updatedBy,
updatedAt: now,
})
const updater = await ctx.db.get(args.updatedBy)
await ctx.db.insert("emprestimoHistorico", {
tenantId: emprestimo.tenantId,
emprestimoId: args.id,
tipo: "DEVOLVIDO",
descricao: `Emprestimo #${emprestimo.reference} devolvido${multaCalculada ? ` com multa de R$ ${multaCalculada.toFixed(2)}` : ""}`,
alteracoes: { multaCalculada },
autorId: args.updatedBy,
autorSnapshot: {
name: updater?.name ?? "Sistema",
email: updater?.email,
},
createdAt: now,
})
return { ok: true, multaCalculada }
},
})
export const update = mutation({
args: {
id: v.id("emprestimos"),
updatedBy: v.id("users"),
responsavelNome: v.optional(v.string()),
responsavelContato: v.optional(v.string()),
dataFimPrevisto: v.optional(v.number()),
observacoes: v.optional(v.string()),
multaDiaria: v.optional(v.number()),
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
const emprestimo = await ctx.db.get(args.id)
if (!emprestimo) {
throw new Error("Emprestimo nao encontrado")
}
const now = Date.now()
const updates: Record<string, unknown> = {
updatedBy: args.updatedBy,
updatedAt: now,
}
if (args.responsavelNome !== undefined) updates.responsavelNome = args.responsavelNome
if (args.responsavelContato !== undefined) updates.responsavelContato = args.responsavelContato
if (args.dataFimPrevisto !== undefined) updates.dataFimPrevisto = args.dataFimPrevisto
if (args.observacoes !== undefined) updates.observacoes = args.observacoes
if (args.multaDiaria !== undefined) updates.multaDiaria = args.multaDiaria
if (args.status !== undefined) updates.status = args.status
await ctx.db.patch(args.id, updates)
const updater = await ctx.db.get(args.updatedBy)
await ctx.db.insert("emprestimoHistorico", {
tenantId: emprestimo.tenantId,
emprestimoId: args.id,
tipo: "MODIFICADO",
descricao: `Emprestimo #${emprestimo.reference} atualizado`,
alteracoes: updates,
autorId: args.updatedBy,
autorSnapshot: {
name: updater?.name ?? "Sistema",
email: updater?.email,
},
createdAt: now,
})
return { ok: true }
},
})
export const getHistorico = query({
args: {
emprestimoId: v.id("emprestimos"),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const historico = await ctx.db
.query("emprestimoHistorico")
.withIndex("by_emprestimo_created", (q) => q.eq("emprestimoId", args.emprestimoId))
.order("desc")
.take(args.limit ?? 50)
return historico.map((h) => ({
id: h._id,
tipo: h.tipo,
descricao: h.descricao,
alteracoes: h.alteracoes,
autorNome: h.autorSnapshot.name,
createdAt: h.createdAt,
}))
},
})
export const getStats = query({
args: {
tenantId: v.string(),
viewerId: v.id("users"),
},
handler: async (ctx, args) => {
const all = await ctx.db
.query("emprestimos")
.withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
.take(200)
const now = Date.now()
const ativos = all.filter((e) => e.status === "ATIVO")
const atrasados = ativos.filter((e) => e.dataFimPrevisto < now)
const devolvidos = all.filter((e) => e.status === "DEVOLVIDO")
return {
total: all.length,
ativos: ativos.length,
atrasados: atrasados.length,
devolvidos: devolvidos.length,
valorTotalAtivo: ativos.reduce((sum, e) => sum + (e.valor ?? 0), 0),
}
},
})