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:
parent
e493ec9d5d
commit
7469d3b5e6
7 changed files with 346 additions and 66 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue