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>
359 lines
10 KiB
TypeScript
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),
|
|
}
|
|
},
|
|
})
|