From 7469d3b5e6cebc73a69811aa912dad5df4671e57 Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Fri, 5 Dec 2025 08:24:56 -0300 Subject: [PATCH] Add USB policy improvements and emprestimos details modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cron job to cleanup stale pending USB policies every 30 min - Add cleanupStalePendingPolicies mutation to usbPolicy.ts - Add USB policy fields to machines listByTenant query - Display USB status chip in device details and bulk control modal - Add details modal for emprestimos with all loan information - Add observacoesDevolucao field to preserve original observations - Fix status text size in details modal title 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- convex/crons.ts | 8 + convex/emprestimos.ts | 5 +- convex/machines.ts | 3 + convex/schema.ts | 1 + convex/usbPolicy.ts | 51 +++ .../emprestimos/emprestimos-page-client.tsx | 300 ++++++++++++++---- .../admin/devices/admin-devices-overview.tsx | 44 +++ 7 files changed, 346 insertions(+), 66 deletions(-) diff --git a/convex/crons.ts b/convex/crons.ts index 6da0d2b..eb4b1be 100644 --- a/convex/crons.ts +++ b/convex/crons.ts @@ -25,4 +25,12 @@ if (autoPauseCronEnabled) { ) } +// Cleanup de policies USB pendentes por mais de 1 hora (sem flag, sempre ativo) +crons.interval( + "cleanup-stale-usb-policies", + { minutes: 30 }, + api.usbPolicy.cleanupStalePendingPolicies, + {} +) + export default crons diff --git a/convex/emprestimos.ts b/convex/emprestimos.ts index d2e9177..3b3985e 100644 --- a/convex/emprestimos.ts +++ b/convex/emprestimos.ts @@ -68,8 +68,10 @@ export const list = query({ 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, @@ -78,6 +80,7 @@ export const list = query({ dataDevolucao: emprestimo.dataDevolucao, status: emprestimo.status, observacoes: emprestimo.observacoes, + observacoesDevolucao: emprestimo.observacoesDevolucao, multaDiaria: emprestimo.multaDiaria, multaCalculada: emprestimo.multaCalculada, createdAt: emprestimo.createdAt, @@ -231,7 +234,7 @@ export const devolver = mutation({ status: "DEVOLVIDO", dataDevolucao: now, multaCalculada, - observacoes: args.observacoes ?? emprestimo.observacoes, + observacoesDevolucao: args.observacoes, updatedBy: args.updatedBy, updatedAt: now, }) diff --git a/convex/machines.ts b/convex/machines.ts index df81bd1..58a9913 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -1015,6 +1015,9 @@ export const listByTenant = query({ lastPostureAt, remoteAccess: machine.remoteAccess ?? null, customFields: machine.customFields ?? [], + usbPolicy: machine.usbPolicy ?? null, + usbPolicyStatus: machine.usbPolicyStatus ?? null, + usbPolicyError: machine.usbPolicyError ?? null, } }) ) diff --git a/convex/schema.ts b/convex/schema.ts index 6186306..e119888 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -801,6 +801,7 @@ export default defineSchema({ dataDevolucao: v.optional(v.number()), status: v.string(), observacoes: v.optional(v.string()), + observacoesDevolucao: v.optional(v.string()), multaDiaria: v.optional(v.number()), multaCalculada: v.optional(v.number()), createdBy: v.id("users"), diff --git a/convex/usbPolicy.ts b/convex/usbPolicy.ts index 5b53da4..ed3fc8f 100644 --- a/convex/usbPolicy.ts +++ b/convex/usbPolicy.ts @@ -249,3 +249,54 @@ export const bulkSetUsbPolicy = mutation({ return { results, total: args.machineIds.length, successful: results.filter((r) => r.success).length } }, }) + +/** + * Cleanup de policies USB pendentes por mais de 1 hora. + * Marca como FAILED com mensagem de timeout. + */ +export const cleanupStalePendingPolicies = mutation({ + args: { + staleThresholdMs: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const thresholdMs = args.staleThresholdMs ?? 3600000 // 1 hora por padrao + const now = Date.now() + const cutoff = now - thresholdMs + + // Buscar maquinas com status PENDING e appliedAt antigo + const allMachines = await ctx.db.query("machines").collect() + const staleMachines = allMachines.filter( + (m) => + m.usbPolicyStatus === "PENDING" && + m.usbPolicyAppliedAt !== undefined && + m.usbPolicyAppliedAt < cutoff + ) + + let cleaned = 0 + for (const machine of staleMachines) { + await ctx.db.patch(machine._id, { + usbPolicyStatus: "FAILED", + usbPolicyError: "Timeout: Agent nao reportou status apos 1 hora. Verifique se o agent esta ativo.", + updatedAt: now, + }) + + // Atualizar evento correspondente + const latestEvent = await ctx.db + .query("usbPolicyEvents") + .withIndex("by_machine_created", (q) => q.eq("machineId", machine._id)) + .order("desc") + .first() + + if (latestEvent && latestEvent.status === "PENDING") { + await ctx.db.patch(latestEvent._id, { + status: "FAILED", + error: "Timeout automatico", + }) + } + + cleaned++ + } + + return { cleaned, checked: allMachines.length } + }, +}) diff --git a/src/app/emprestimos/emprestimos-page-client.tsx b/src/app/emprestimos/emprestimos-page-client.tsx index 2441504..d6d7746 100644 --- a/src/app/emprestimos/emprestimos-page-client.tsx +++ b/src/app/emprestimos/emprestimos-page-client.tsx @@ -89,8 +89,10 @@ type EmprestimoListItem = { clienteId: string clienteNome: string responsavelNome: string + responsavelContato?: string tecnicoId: string tecnicoNome: string + tecnicoEmail?: string equipamentos: Equipamento[] quantidade: number valor?: number @@ -99,46 +101,29 @@ type EmprestimoListItem = { dataDevolucao?: number status: string observacoes?: string + observacoesDevolucao?: string multaDiaria?: number multaCalculada?: number createdAt: number updatedAt: number } -function getStatusBadge(status: string, dataFimPrevisto: number) { +function getStatusText(status: string, dataFimPrevisto: number, size: "sm" | "base" = "sm") { const now = Date.now() const isAtrasado = status === "ATIVO" && now > dataFimPrevisto + const sizeClass = size === "sm" ? "text-sm" : "text-base" if (isAtrasado) { - return ( - - - Atrasado - - ) + return Atrasado } switch (status) { case "ATIVO": - return ( - - - Ativo - - ) + return Ativo case "DEVOLVIDO": - return ( - - - Devolvido - - ) + return Devolvido case "CANCELADO": - return ( - - Cancelado - - ) + return Cancelado default: return null } @@ -155,7 +140,9 @@ export function EmprestimosPageClient() { const [dateRange, setDateRange] = useState({ from: null, to: null }) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isDevolverDialogOpen, setIsDevolverDialogOpen] = useState(false) + const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false) const [selectedEmprestimoId, setSelectedEmprestimoId] = useState(null) + const [selectedEmprestimoForDetails, setSelectedEmprestimoForDetails] = useState(null) // Form states const [formClienteId, setFormClienteId] = useState(null) @@ -384,6 +371,11 @@ export function EmprestimosPageClient() { setIsDevolverDialogOpen(true) }, []) + const openDetailsDialog = useCallback((emp: EmprestimoListItem) => { + setSelectedEmprestimoForDetails(emp) + setIsDetailsDialogOpen(true) + }, []) + const handleClearFilters = useCallback(() => { setSearchQuery("") setStatusFilter("all") @@ -408,7 +400,7 @@ export function EmprestimosPageClient() { "inline-flex h-full flex-1 items-center justify-center rounded-full first:rounded-full last:rounded-full px-4 text-sm font-semibold text-neutral-600 transition-colors hover:bg-slate-100 data-[state=on]:bg-neutral-900 data-[state=on]:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-300" return ( -
+
{/* Stats Cards */}
@@ -530,27 +522,31 @@ export function EmprestimosPageClient() { ) : (
- - - - Ref - Cliente - Responsável - Equipamentos - Data empréstimo - Data prevista - Status - Valor - Ações +
+ + + Ref + Cliente + Responsável + Equipamentos + Empréstimo + Prevista + Status + Valor + Ações {filteredEmprestimos.map((emp) => ( - - #{emp.reference} - {emp.clienteNome} - {emp.responsavelNome} - + openDetailsDialog(emp)} + > + #{emp.reference} + {emp.clienteNome} + {emp.responsavelNome} + {emp.quantidade} item(s):{" "} {emp.equipamentos @@ -560,24 +556,27 @@ export function EmprestimosPageClient() { {emp.equipamentos.length > 2 && "..."} - + {format(new Date(emp.dataEmprestimo), "dd/MM/yyyy", { locale: ptBR })} - + {format(new Date(emp.dataFimPrevisto), "dd/MM/yyyy", { locale: ptBR })} - {getStatusBadge(emp.status, emp.dataFimPrevisto)} - + {getStatusText(emp.status, emp.dataFimPrevisto)} + {emp.valor ? `R$ ${emp.valor.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}` : "—"} - + {emp.status === "ATIVO" && ( + {selectedEmprestimoForDetails?.status === "ATIVO" && ( + + )} + + + ) } diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index db37ee4..3a3d0cb 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -2270,6 +2270,14 @@ export function AdminDevicesOverview({
{windowsDevices.map((device) => { const checked = usbPolicySelection.includes(device.id) + const policyLabels: Record = { + ALLOW: "Permitido", + BLOCK_ALL: "Bloqueado", + READONLY: "Leitura", + } + const currentPolicy = device.usbPolicy ? policyLabels[device.usbPolicy] ?? device.usbPolicy : null + const isPending = device.usbPolicyStatus === "PENDING" + const isFailed = device.usbPolicyStatus === "FAILED" return (