Add USB policy improvements and emprestimos details modal

- 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 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-05 08:24:56 -03:00
parent e493ec9d5d
commit 7469d3b5e6
7 changed files with 346 additions and 66 deletions

View file

@ -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

View file

@ -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,
})

View file

@ -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,
}
})
)

View file

@ -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"),

View file

@ -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 }
},
})

View file

@ -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 (
<Badge className="gap-1.5 rounded-full border border-red-200 bg-red-50 px-2.5 py-0.5 text-xs font-medium text-red-700">
<IconAlertTriangle className="size-3" />
Atrasado
</Badge>
)
return <span className={`${sizeClass} font-semibold text-red-600`}>Atrasado</span>
}
switch (status) {
case "ATIVO":
return (
<Badge className="gap-1.5 rounded-full border border-cyan-200 bg-cyan-50 px-2.5 py-0.5 text-xs font-medium text-cyan-700">
<IconClock className="size-3" />
Ativo
</Badge>
)
return <span className={`${sizeClass} font-semibold text-amber-700`}>Ativo</span>
case "DEVOLVIDO":
return (
<Badge className="gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-2.5 py-0.5 text-xs font-medium text-emerald-700">
<IconCircleCheck className="size-3" />
Devolvido
</Badge>
)
return <span className={`${sizeClass} font-semibold text-emerald-600`}>Devolvido</span>
case "CANCELADO":
return (
<Badge className="gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-2.5 py-0.5 text-xs font-medium text-slate-600">
Cancelado
</Badge>
)
return <span className={`${sizeClass} font-semibold text-slate-500`}>Cancelado</span>
default:
return null
}
@ -155,7 +140,9 @@ export function EmprestimosPageClient() {
const [dateRange, setDateRange] = useState<DateRangeValue>({ from: null, to: null })
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isDevolverDialogOpen, setIsDevolverDialogOpen] = useState(false)
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false)
const [selectedEmprestimoId, setSelectedEmprestimoId] = useState<string | null>(null)
const [selectedEmprestimoForDetails, setSelectedEmprestimoForDetails] = useState<EmprestimoListItem | null>(null)
// Form states
const [formClienteId, setFormClienteId] = useState<string | null>(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 (
<div className="mx-auto max-w-6xl space-y-6 px-4 lg:px-6">
<div className="mx-auto max-w-7xl space-y-6 px-4 lg:px-6">
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-4">
<div className="rounded-2xl border border-slate-200 bg-white/90 p-4 shadow-sm transition-colors hover:border-cyan-200/60 hover:bg-cyan-50/30">
@ -530,27 +522,31 @@ export function EmprestimosPageClient() {
</Empty>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-slate-50/80 hover:bg-slate-50/80">
<TableHead className="font-semibold text-neutral-600">Ref</TableHead>
<TableHead className="font-semibold text-neutral-600">Cliente</TableHead>
<TableHead className="font-semibold text-neutral-600">Responsável</TableHead>
<TableHead className="font-semibold text-neutral-600">Equipamentos</TableHead>
<TableHead className="font-semibold text-neutral-600">Data empréstimo</TableHead>
<TableHead className="font-semibold text-neutral-600">Data prevista</TableHead>
<TableHead className="font-semibold text-neutral-600">Status</TableHead>
<TableHead className="font-semibold text-neutral-600">Valor</TableHead>
<TableHead className="text-right font-semibold text-neutral-600">Ações</TableHead>
<Table className="w-full table-fixed">
<TableHeader className="bg-slate-100/80">
<TableRow className="bg-transparent text-[11px] uppercase tracking-wide text-neutral-600 hover:bg-transparent">
<TableHead className="w-[6%] px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Ref</TableHead>
<TableHead className="w-[14%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Cliente</TableHead>
<TableHead className="w-[12%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Responsável</TableHead>
<TableHead className="w-[16%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Equipamentos</TableHead>
<TableHead className="w-[11%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Empréstimo</TableHead>
<TableHead className="w-[11%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Prevista</TableHead>
<TableHead className="w-[9%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Status</TableHead>
<TableHead className="w-[10%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Valor</TableHead>
<TableHead className="w-[11%] border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredEmprestimos.map((emp) => (
<TableRow key={emp.id} className="transition-colors hover:bg-cyan-50/30">
<TableCell className="font-semibold text-cyan-700">#{emp.reference}</TableCell>
<TableCell className="font-medium">{emp.clienteNome}</TableCell>
<TableCell>{emp.responsavelNome}</TableCell>
<TableCell>
<TableRow
key={emp.id}
className="cursor-pointer transition-colors hover:bg-cyan-50/30"
onClick={() => openDetailsDialog(emp)}
>
<TableCell className="text-center font-semibold text-neutral-900">#{emp.reference}</TableCell>
<TableCell className="text-center font-medium">{emp.clienteNome}</TableCell>
<TableCell className="text-center">{emp.responsavelNome}</TableCell>
<TableCell className="text-center">
<span className="text-sm text-neutral-600">
{emp.quantidade} item(s):{" "}
{emp.equipamentos
@ -560,24 +556,27 @@ export function EmprestimosPageClient() {
{emp.equipamentos.length > 2 && "..."}
</span>
</TableCell>
<TableCell>
<TableCell className="text-center">
{format(new Date(emp.dataEmprestimo), "dd/MM/yyyy", { locale: ptBR })}
</TableCell>
<TableCell>
<TableCell className="text-center">
{format(new Date(emp.dataFimPrevisto), "dd/MM/yyyy", { locale: ptBR })}
</TableCell>
<TableCell>{getStatusBadge(emp.status, emp.dataFimPrevisto)}</TableCell>
<TableCell>
<TableCell className="text-center">{getStatusText(emp.status, emp.dataFimPrevisto)}</TableCell>
<TableCell className="text-center">
{emp.valor
? `R$ ${emp.valor.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`
: "—"}
</TableCell>
<TableCell className="text-right">
<TableCell className="text-center">
{emp.status === "ATIVO" && (
<button
type="button"
onClick={() => openDevolverDialog(emp.id)}
className="inline-flex h-9 items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 text-sm font-semibold text-emerald-700 transition hover:border-emerald-300 hover:bg-emerald-100"
onClick={(e) => {
e.stopPropagation()
openDevolverDialog(emp.id)
}}
className="inline-flex h-9 items-center gap-2 rounded-full border border-[#00e8fc]/30 bg-[#00e8fc]/10 px-3 text-sm font-semibold text-neutral-900 transition hover:border-[#00e8fc]/50 hover:bg-[#00e8fc]/20"
>
<IconCircleCheck className="size-4" />
Devolver
@ -604,8 +603,8 @@ export function EmprestimosPageClient() {
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium">Cliente *</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Cliente *</label>
<SearchableCombobox
value={formClienteId}
onValueChange={setFormClienteId}
@ -614,8 +613,8 @@ export function EmprestimosPageClient() {
prefix={<IconBuilding className="size-4 text-neutral-400" />}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Técnico responsável *</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Técnico responsável *</label>
<SearchableCombobox
value={formTecnicoId}
onValueChange={setFormTecnicoId}
@ -627,16 +626,16 @@ export function EmprestimosPageClient() {
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium">Nome do responsável (cliente) *</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Nome do responsável (cliente) *</label>
<Input
value={formResponsavelNome}
onChange={(e) => setFormResponsavelNome(e.target.value)}
placeholder="Nome do responsável"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Contato do responsável</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Contato do responsável</label>
<Input
value={formResponsavelContato}
onChange={(e) => setFormResponsavelContato(e.target.value)}
@ -646,19 +645,19 @@ export function EmprestimosPageClient() {
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium">Data do empréstimo</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Data do empréstimo</label>
<DatePicker value={formDataEmprestimo} onChange={setFormDataEmprestimo} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Data prevista devolução *</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Data prevista devolução *</label>
<DatePicker value={formDataFim} onChange={setFormDataFim} />
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium">Valor total (R$)</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Valor total (R$)</label>
<Input
type="number"
step="0.01"
@ -667,8 +666,8 @@ export function EmprestimosPageClient() {
placeholder="0,00"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Multa diária por atraso (R$)</label>
<div className="space-y-2.5">
<label className="mb-1 block text-sm font-medium">Multa diária por atraso (R$)</label>
<Input
type="number"
step="0.01"
@ -829,6 +828,177 @@ export function EmprestimosPageClient() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Details Dialog */}
<Dialog open={isDetailsDialogOpen} onOpenChange={setIsDetailsDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-3">
<span>Empréstimo #{selectedEmprestimoForDetails?.reference}</span>
{selectedEmprestimoForDetails && getStatusText(selectedEmprestimoForDetails.status, selectedEmprestimoForDetails.dataFimPrevisto, "base")}
</DialogTitle>
<DialogDescription>
Detalhes completos do empréstimo.
</DialogDescription>
</DialogHeader>
{selectedEmprestimoForDetails && (
<div className="space-y-5">
{/* Cliente e Responsável */}
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
<IconBuilding className="size-3.5" />
Cliente
</div>
<p className="text-sm font-medium text-neutral-900">{selectedEmprestimoForDetails.clienteNome}</p>
</div>
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
<IconUser className="size-3.5" />
Responsável
</div>
<p className="text-sm font-medium text-neutral-900">{selectedEmprestimoForDetails.responsavelNome}</p>
{selectedEmprestimoForDetails.responsavelContato && (
<p className="text-xs text-muted-foreground">{selectedEmprestimoForDetails.responsavelContato}</p>
)}
</div>
</div>
{/* Técnico */}
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-2">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">Técnico responsável</div>
<p className="text-sm font-medium text-neutral-900">{selectedEmprestimoForDetails.tecnicoNome}</p>
{selectedEmprestimoForDetails.tecnicoEmail && (
<p className="text-xs text-muted-foreground">{selectedEmprestimoForDetails.tecnicoEmail}</p>
)}
</div>
{/* Datas */}
<div className="grid gap-4 md:grid-cols-3">
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">Data do empréstimo</div>
<p className="text-sm font-medium text-neutral-900">
{format(new Date(selectedEmprestimoForDetails.dataEmprestimo), "dd/MM/yyyy", { locale: ptBR })}
</p>
</div>
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">Devolução prevista</div>
<p className="text-sm font-medium text-neutral-900">
{format(new Date(selectedEmprestimoForDetails.dataFimPrevisto), "dd/MM/yyyy", { locale: ptBR })}
</p>
</div>
{selectedEmprestimoForDetails.dataDevolucao && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50/50 p-3 space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-emerald-600">Data da devolução</div>
<p className="text-sm font-medium text-emerald-700">
{format(new Date(selectedEmprestimoForDetails.dataDevolucao), "dd/MM/yyyy", { locale: ptBR })}
</p>
</div>
)}
</div>
{/* Valores */}
<div className="grid gap-4 md:grid-cols-3">
{selectedEmprestimoForDetails.valor && (
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">Valor total</div>
<p className="text-sm font-medium text-neutral-900">
R$ {selectedEmprestimoForDetails.valor.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
</p>
</div>
)}
{selectedEmprestimoForDetails.multaDiaria && (
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">Multa diária</div>
<p className="text-sm font-medium text-neutral-900">
R$ {selectedEmprestimoForDetails.multaDiaria.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
</p>
</div>
)}
{selectedEmprestimoForDetails.multaCalculada && (
<div className="rounded-xl border border-red-200 bg-red-50/50 p-3 space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-red-600">Multa aplicada</div>
<p className="text-sm font-medium text-red-700">
R$ {selectedEmprestimoForDetails.multaCalculada.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
</p>
</div>
)}
</div>
{/* Equipamentos */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
<IconPackage className="size-3.5" />
Equipamentos ({selectedEmprestimoForDetails.quantidade})
</div>
<div className="space-y-2">
{selectedEmprestimoForDetails.equipamentos.map((eq, idx) => (
<div key={eq.id} className="rounded-xl border border-slate-200 bg-white p-3">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs font-medium">
{eq.tipo}
</Badge>
<span className="text-sm font-medium text-neutral-900">
{eq.marca} {eq.modelo}
</span>
</div>
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
{eq.serialNumber && <span>S/N: {eq.serialNumber}</span>}
{eq.patrimonio && <span>Patrimônio: {eq.patrimonio}</span>}
</div>
</div>
<span className="text-xs font-medium text-slate-400">#{idx + 1}</span>
</div>
</div>
))}
</div>
</div>
{/* Observações */}
{selectedEmprestimoForDetails.observacoes && (
<div className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-2">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">Observações do empréstimo</div>
<p className="text-sm text-neutral-700 whitespace-pre-wrap">{selectedEmprestimoForDetails.observacoes}</p>
</div>
)}
{/* Observações da devolução */}
{selectedEmprestimoForDetails.observacoesDevolucao && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50/50 p-3 space-y-2">
<div className="text-xs font-semibold uppercase tracking-wide text-emerald-600">Observações da devolução</div>
<p className="text-sm text-emerald-700 whitespace-pre-wrap">{selectedEmprestimoForDetails.observacoesDevolucao}</p>
</div>
)}
{/* Metadados */}
<div className="border-t border-slate-200 pt-3 text-xs text-muted-foreground">
<p>Criado em: {format(new Date(selectedEmprestimoForDetails.createdAt), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}</p>
<p>Última atualização: {format(new Date(selectedEmprestimoForDetails.updatedAt), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}</p>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setIsDetailsDialogOpen(false)}>
Fechar
</Button>
{selectedEmprestimoForDetails?.status === "ATIVO" && (
<Button
onClick={() => {
setIsDetailsDialogOpen(false)
openDevolverDialog(selectedEmprestimoForDetails.id)
}}
>
<IconCircleCheck className="size-4" />
Registrar devolução
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View file

@ -2270,6 +2270,14 @@ export function AdminDevicesOverview({
<div className="max-h-48 space-y-1 overflow-y-auto rounded-md border border-slate-200 p-2">
{windowsDevices.map((device) => {
const checked = usbPolicySelection.includes(device.id)
const policyLabels: Record<string, string> = {
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 (
<label
key={device.id}
@ -2286,6 +2294,16 @@ export function AdminDevicesOverview({
<span className="flex-1 truncate">
{device.displayName ?? device.hostname}
</span>
{currentPolicy && (
<span className={cn(
"rounded-full px-1.5 py-0.5 text-[10px] font-medium",
isFailed ? "bg-red-100 text-red-700" :
isPending ? "bg-amber-100 text-amber-700" :
"bg-slate-100 text-slate-600"
)}>
{currentPolicy}{isPending ? " ..." : isFailed ? " !" : ""}
</span>
)}
<span className="text-xs text-muted-foreground">
{device.companyName ?? "—"}
</span>
@ -3166,6 +3184,30 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
icon: <Key className="size-4 text-neutral-500" />,
})
}
const isWindowsDevice = (device?.osName ?? "").toLowerCase().includes("windows")
if (isWindowsDevice && device?.usbPolicy) {
const policyLabels: Record<string, string> = {
ALLOW: "Permitido",
BLOCK_ALL: "Bloqueado",
READONLY: "Somente leitura",
}
const statusLabels: Record<string, string> = {
PENDING: "Pendente",
APPLIED: "Aplicado",
FAILED: "Falhou",
}
const policyLabel = policyLabels[device.usbPolicy] ?? device.usbPolicy
const statusLabel = device.usbPolicyStatus ? ` (${statusLabels[device.usbPolicyStatus] ?? device.usbPolicyStatus})` : ""
const isPending = device.usbPolicyStatus === "PENDING"
const isFailed = device.usbPolicyStatus === "FAILED"
chips.push({
key: "usb-policy",
label: "USB",
value: `${policyLabel}${statusLabel}`,
icon: <Usb className={`size-4 ${isFailed ? "text-red-500" : isPending ? "text-amber-500" : "text-neutral-500"}`} />,
tone: isFailed ? "warning" : isPending ? "warning" : undefined,
})
}
return chips
}, [
osNameDisplay,
@ -3179,6 +3221,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
personaLabel,
device?.osName,
remoteAccessEntries,
device?.usbPolicy,
device?.usbPolicyStatus,
])
const companyName = device?.companyName ?? device?.companySlug ?? null