Add equipment loan feature and USB bulk control
- Add emprestimos (equipment loan) module in Convex with queries/mutations - Create emprestimos page with full CRUD and status tracking - Add USB bulk control to admin devices overview - Fix Portuguese accents in USB policy control component - Fix dead code warnings in Rust agent - Fix tiptap type error in rich text editor
This commit is contained in:
parent
49aa143a80
commit
063c5dfde7
11 changed files with 1448 additions and 26 deletions
4
convex/_generated/api.d.ts
vendored
4
convex/_generated/api.d.ts
vendored
|
|
@ -20,6 +20,7 @@ import type * as deviceExportTemplates from "../deviceExportTemplates.js";
|
|||
import type * as deviceFieldDefaults from "../deviceFieldDefaults.js";
|
||||
import type * as deviceFields from "../deviceFields.js";
|
||||
import type * as devices from "../devices.js";
|
||||
import type * as emprestimos from "../emprestimos.js";
|
||||
import type * as fields from "../fields.js";
|
||||
import type * as files from "../files.js";
|
||||
import type * as incidents from "../incidents.js";
|
||||
|
|
@ -38,6 +39,7 @@ import type * as ticketFormSettings from "../ticketFormSettings.js";
|
|||
import type * as ticketFormTemplates from "../ticketFormTemplates.js";
|
||||
import type * as ticketNotifications from "../ticketNotifications.js";
|
||||
import type * as tickets from "../tickets.js";
|
||||
import type * as usbPolicy from "../usbPolicy.js";
|
||||
import type * as users from "../users.js";
|
||||
|
||||
import type {
|
||||
|
|
@ -67,6 +69,7 @@ declare const fullApi: ApiFromModules<{
|
|||
deviceFieldDefaults: typeof deviceFieldDefaults;
|
||||
deviceFields: typeof deviceFields;
|
||||
devices: typeof devices;
|
||||
emprestimos: typeof emprestimos;
|
||||
fields: typeof fields;
|
||||
files: typeof files;
|
||||
incidents: typeof incidents;
|
||||
|
|
@ -85,6 +88,7 @@ declare const fullApi: ApiFromModules<{
|
|||
ticketFormTemplates: typeof ticketFormTemplates;
|
||||
ticketNotifications: typeof ticketNotifications;
|
||||
tickets: typeof tickets;
|
||||
usbPolicy: typeof usbPolicy;
|
||||
users: typeof users;
|
||||
}>;
|
||||
declare const fullApiWithMounts: typeof fullApi;
|
||||
|
|
|
|||
356
convex/emprestimos.ts
Normal file
356
convex/emprestimos.ts
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
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,
|
||||
tecnicoId: emprestimo.tecnicoId,
|
||||
tecnicoNome: emprestimo.tecnicoSnapshot.name,
|
||||
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,
|
||||
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,
|
||||
observacoes: args.observacoes ?? emprestimo.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))
|
||||
.collect()
|
||||
|
||||
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),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -767,4 +767,68 @@ export default defineSchema({
|
|||
_ttl: v.optional(v.number()),
|
||||
})
|
||||
.index("by_key", ["tenantId", "cacheKey"]),
|
||||
|
||||
// ================================
|
||||
// Emprestimo de Equipamentos
|
||||
// ================================
|
||||
emprestimos: defineTable({
|
||||
tenantId: v.string(),
|
||||
reference: v.number(),
|
||||
clienteId: v.id("companies"),
|
||||
clienteSnapshot: v.object({
|
||||
name: v.string(),
|
||||
slug: v.optional(v.string()),
|
||||
}),
|
||||
responsavelNome: v.string(),
|
||||
responsavelContato: v.optional(v.string()),
|
||||
tecnicoId: v.id("users"),
|
||||
tecnicoSnapshot: v.object({
|
||||
name: v.string(),
|
||||
email: v.optional(v.string()),
|
||||
}),
|
||||
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()),
|
||||
})),
|
||||
quantidade: v.number(),
|
||||
valor: v.optional(v.number()),
|
||||
dataEmprestimo: v.number(),
|
||||
dataFimPrevisto: v.number(),
|
||||
dataDevolucao: v.optional(v.number()),
|
||||
status: v.string(),
|
||||
observacoes: v.optional(v.string()),
|
||||
multaDiaria: v.optional(v.number()),
|
||||
multaCalculada: v.optional(v.number()),
|
||||
createdBy: v.id("users"),
|
||||
updatedBy: v.optional(v.id("users")),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_tenant", ["tenantId"])
|
||||
.index("by_tenant_status", ["tenantId", "status"])
|
||||
.index("by_tenant_cliente", ["tenantId", "clienteId"])
|
||||
.index("by_tenant_tecnico", ["tenantId", "tecnicoId"])
|
||||
.index("by_tenant_reference", ["tenantId", "reference"])
|
||||
.index("by_tenant_created", ["tenantId", "createdAt"])
|
||||
.index("by_tenant_data_fim", ["tenantId", "dataFimPrevisto"]),
|
||||
|
||||
emprestimoHistorico: defineTable({
|
||||
tenantId: v.string(),
|
||||
emprestimoId: v.id("emprestimos"),
|
||||
tipo: v.string(),
|
||||
descricao: v.string(),
|
||||
alteracoes: v.optional(v.any()),
|
||||
autorId: v.id("users"),
|
||||
autorSnapshot: v.object({
|
||||
name: v.string(),
|
||||
email: v.optional(v.string()),
|
||||
}),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_emprestimo", ["emprestimoId"])
|
||||
.index("by_emprestimo_created", ["emprestimoId", "createdAt"]),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue