Improve loan page and add company filter to USB bulk control

- Update Next.js to 16.0.7
- Fix accent on menu item "Emprestimos" to "Empréstimos"
- Standardize loan page with project patterns (DateRangeButton, cyan color scheme, ToggleGroup)
- Add company filter to USB bulk policy dialog
- Update CardDescription text in devices overview
- Fix useEffect dependency warning in desktop main.tsx

🤖 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-04 14:52:27 -03:00
parent 063c5dfde7
commit 38995b95c6
8 changed files with 1123 additions and 1184 deletions

View file

@ -22,9 +22,10 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"png-to-ico": "^3.0.1",
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"baseline-browser-mapping": "^2.9.2",
"png-to-ico": "^3.0.1",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^6.0.3" "vite": "^6.0.3"
} }

View file

@ -79,6 +79,7 @@ dependencies = [
"tauri-plugin-updater", "tauri-plugin-updater",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"winreg",
] ]
[[package]] [[package]]

View file

@ -916,7 +916,7 @@ const resolvedAppUrl = useMemo(() => {
syncRemoteAccessNow(rustdeskInfo) syncRemoteAccessNow(rustdeskInfo)
} }
} }
}, [store, rustdeskInfo, ensureRustdesk, syncRemoteAccessNow, isRustdeskProvisioning]) }, [store, config?.machineId, rustdeskInfo, ensureRustdesk, syncRemoteAccessNow, isRustdeskProvisioning])
async function register() { async function register() {
if (!profile) return if (!profile) return

1693
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -32,8 +32,8 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
"@noble/hashes": "^1.5.0", "@noble/hashes": "^1.5.0",
"@prisma/adapter-better-sqlite3": "^7.0.0",
"@paper-design/shaders-react": "^0.0.55", "@paper-design/shaders-react": "^0.0.55",
"@prisma/adapter-better-sqlite3": "^7.0.0",
"@prisma/client": "^7.0.0", "@prisma/client": "^7.0.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
@ -69,13 +69,13 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"next": "^16.0.3", "next": "^16.0.7",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "19.2.0", "react": "^19.2.1",
"react-day-picker": "^9.4.2", "react-day-picker": "^9.4.2",
"react-dom": "19.2.0", "react-dom": "^19.2.1",
"react-hook-form": "^7.64.0", "react-hook-form": "^7.64.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
@ -101,24 +101,28 @@
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"@vitest/browser-playwright": "^4.0.1", "@vitest/browser-playwright": "^4.0.1",
"baseline-browser-mapping": "^2.9.2",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^16.0.3", "eslint-config-next": "^16.0.7",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"jsdom": "^27.0.1", "jsdom": "^27.0.1",
"playwright": "^1.56.1", "playwright": "^1.56.1",
"prisma": "^7.0.0", "prisma": "^7.0.0",
"tailwindcss": "^4", "tailwindcss": "^4",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"tsx": "^4.19.1",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.3.8",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.46.2", "typescript-eslint": "^8.46.2",
"tsx": "^4.19.1",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.1" "vitest": "^4.0.1"
}, },
"workspaces": [ "workspaces": [
".", ".",
"apps/desktop" "apps/desktop"
] ],
"overrides": {
"baseline-browser-mapping": "^2.9.2"
}
} }

View file

@ -2,19 +2,21 @@
import { useState, useMemo, useCallback } from "react" import { useState, useMemo, useCallback } from "react"
import { useMutation, useQuery } from "convex/react" import { useMutation, useQuery } from "convex/react"
import { format, formatDistanceToNow, isAfter } from "date-fns" import { format } from "date-fns"
import { ptBR } from "date-fns/locale" import { ptBR } from "date-fns/locale"
import { toast } from "sonner" import { toast } from "sonner"
import { import {
Plus, IconPlus,
Package, IconPackage,
CheckCircle2, IconCircleCheck,
Clock, IconClock,
AlertTriangle, IconAlertTriangle,
Search, IconSearch,
ChevronDown, IconRefresh,
RotateCcw, IconBuilding,
} from "lucide-react" IconUser,
IconTrash,
} from "@tabler/icons-react"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
@ -22,7 +24,6 @@ import { useAuth } from "@/lib/auth-client"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { import {
Select, Select,
SelectContent, SelectContent,
@ -48,9 +49,10 @@ import {
} from "@/components/ui/table" } from "@/components/ui/table"
import { Spinner } from "@/components/ui/spinner" import { Spinner } from "@/components/ui/spinner"
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox" import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
import { DateRangeButton, type DateRangeValue } from "@/components/date-range-button"
import { DatePicker } from "@/components/ui/date-picker" import { DatePicker } from "@/components/ui/date-picker"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const EQUIPAMENTO_TIPOS = [ const EQUIPAMENTO_TIPOS = [
"NOTEBOOK", "NOTEBOOK",
@ -70,8 +72,6 @@ const EQUIPAMENTO_TIPOS = [
"OUTRO", "OUTRO",
] as const ] as const
type EmprestimoStatus = "ATIVO" | "DEVOLVIDO" | "ATRASADO" | "CANCELADO"
type Equipamento = { type Equipamento = {
id: string id: string
tipo: string tipo: string
@ -109,8 +109,8 @@ function getStatusBadge(status: string, dataFimPrevisto: number) {
if (isAtrasado) { if (isAtrasado) {
return ( return (
<Badge variant="outline" className="gap-1 border-red-200 bg-red-50 text-red-700"> <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">
<AlertTriangle className="size-3" /> <IconAlertTriangle className="size-3" />
Atrasado Atrasado
</Badge> </Badge>
) )
@ -119,21 +119,21 @@ function getStatusBadge(status: string, dataFimPrevisto: number) {
switch (status) { switch (status) {
case "ATIVO": case "ATIVO":
return ( return (
<Badge variant="outline" className="gap-1 border-blue-200 bg-blue-50 text-blue-700"> <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">
<Clock className="size-3" /> <IconClock className="size-3" />
Ativo Ativo
</Badge> </Badge>
) )
case "DEVOLVIDO": case "DEVOLVIDO":
return ( return (
<Badge variant="outline" className="gap-1 border-emerald-200 bg-emerald-50 text-emerald-700"> <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">
<CheckCircle2 className="size-3" /> <IconCircleCheck className="size-3" />
Devolvido Devolvido
</Badge> </Badge>
) )
case "CANCELADO": case "CANCELADO":
return ( return (
<Badge variant="outline" className="gap-1 border-neutral-200 bg-neutral-50 text-neutral-600"> <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 Cancelado
</Badge> </Badge>
) )
@ -142,12 +142,15 @@ function getStatusBadge(status: string, dataFimPrevisto: number) {
} }
} }
const ALL_VALUE = "ALL"
export function EmprestimosPageClient() { export function EmprestimosPageClient() {
const { session, convexUserId, role } = useAuth() const { session, convexUserId } = useAuth()
const tenantId = session?.user?.tenantId ?? null const tenantId = session?.user?.tenantId ?? null
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("all") const [statusFilter, setStatusFilter] = useState<string>("all")
const [clienteFilter, setClienteFilter] = useState<string | null>(null) const [clienteFilter, setClienteFilter] = useState<string | null>(null)
const [dateRange, setDateRange] = useState<DateRangeValue>({ from: null, to: null })
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isDevolverDialogOpen, setIsDevolverDialogOpen] = useState(false) const [isDevolverDialogOpen, setIsDevolverDialogOpen] = useState(false)
const [selectedEmprestimoId, setSelectedEmprestimoId] = useState<string | null>(null) const [selectedEmprestimoId, setSelectedEmprestimoId] = useState<string | null>(null)
@ -224,23 +227,37 @@ export function EmprestimosPageClient() {
const filteredEmprestimos = useMemo<EmprestimoListItem[]>(() => { const filteredEmprestimos = useMemo<EmprestimoListItem[]>(() => {
if (!emprestimos) return [] if (!emprestimos) return []
const list = emprestimos as EmprestimoListItem[] let list = emprestimos as EmprestimoListItem[]
const q = searchQuery.toLowerCase().trim()
if (!q) return list
return list.filter((e) => { // Filtro por busca
const searchFields = [ const q = searchQuery.toLowerCase().trim()
e.clienteNome, if (q) {
e.responsavelNome, list = list.filter((e) => {
e.tecnicoNome, const searchFields = [
String(e.reference), e.clienteNome,
...e.equipamentos.map((eq) => `${eq.tipo} ${eq.marca} ${eq.modelo}`), e.responsavelNome,
] e.tecnicoNome,
.join(" ") String(e.reference),
.toLowerCase() ...e.equipamentos.map((eq) => `${eq.tipo} ${eq.marca} ${eq.modelo}`),
return searchFields.includes(q) ]
}) .join(" ")
}, [emprestimos, searchQuery]) .toLowerCase()
return searchFields.includes(q)
})
}
// Filtro por data
if (dateRange.from) {
const fromDate = new Date(dateRange.from).getTime()
list = list.filter((e) => e.dataEmprestimo >= fromDate)
}
if (dateRange.to) {
const toDate = new Date(dateRange.to).getTime() + 86400000 // +1 dia para incluir o dia todo
list = list.filter((e) => e.dataEmprestimo <= toDate)
}
return list
}, [emprestimos, searchQuery, dateRange])
const handleAddEquipamento = useCallback(() => { const handleAddEquipamento = useCallback(() => {
setFormEquipamentos((prev) => [ setFormEquipamentos((prev) => [
@ -283,11 +300,11 @@ export function EmprestimosPageClient() {
const handleCreate = useCallback(async () => { const handleCreate = useCallback(async () => {
if (!tenantId || !convexUserId) return if (!tenantId || !convexUserId) return
if (!formClienteId || !formTecnicoId || !formDataFim || formEquipamentos.length === 0) { if (!formClienteId || !formTecnicoId || !formDataFim || formEquipamentos.length === 0) {
toast.error("Preencha todos os campos obrigatorios.") toast.error("Preencha todos os campos obrigatórios.")
return return
} }
if (!formResponsavelNome.trim()) { if (!formResponsavelNome.trim()) {
toast.error("Informe o nome do responsavel.") toast.error("Informe o nome do responsável.")
return return
} }
@ -307,12 +324,12 @@ export function EmprestimosPageClient() {
observacoes: formObservacoes || undefined, observacoes: formObservacoes || undefined,
multaDiaria: formMultaDiaria ? parseFloat(formMultaDiaria) : undefined, multaDiaria: formMultaDiaria ? parseFloat(formMultaDiaria) : undefined,
}) })
toast.success(`Emprestimo #${result.reference} criado com sucesso.`) toast.success(`Empréstimo #${result.reference} criado com sucesso.`)
setIsCreateDialogOpen(false) setIsCreateDialogOpen(false)
resetForm() resetForm()
} catch (error) { } catch (error) {
console.error("[emprestimos] Falha ao criar", error) console.error("[emprestimos] Falha ao criar", error)
toast.error("Falha ao criar emprestimo.") toast.error("Falha ao criar empréstimo.")
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@ -344,16 +361,16 @@ export function EmprestimosPageClient() {
observacoes: formObservacoes || undefined, observacoes: formObservacoes || undefined,
}) })
if (result.multaCalculada) { if (result.multaCalculada) {
toast.success(`Emprestimo devolvido com multa de R$ ${result.multaCalculada.toFixed(2)}.`) toast.success(`Empréstimo devolvido com multa de R$ ${result.multaCalculada.toFixed(2)}.`)
} else { } else {
toast.success("Emprestimo devolvido com sucesso.") toast.success("Empréstimo devolvido com sucesso.")
} }
setIsDevolverDialogOpen(false) setIsDevolverDialogOpen(false)
setSelectedEmprestimoId(null) setSelectedEmprestimoId(null)
setFormObservacoes("") setFormObservacoes("")
} catch (error) { } catch (error) {
console.error("[emprestimos] Falha ao devolver", error) console.error("[emprestimos] Falha ao devolver", error)
toast.error("Falha ao registrar devolucao.") toast.error("Falha ao registrar devolução.")
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@ -365,6 +382,13 @@ export function EmprestimosPageClient() {
setIsDevolverDialogOpen(true) setIsDevolverDialogOpen(true)
}, []) }, [])
const handleClearFilters = useCallback(() => {
setSearchQuery("")
setStatusFilter("all")
setClienteFilter(null)
setDateRange({ from: null, to: null })
}, [])
if (!tenantId || !convexUserId) { if (!tenantId || !convexUserId) {
return ( return (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
@ -373,173 +397,205 @@ export function EmprestimosPageClient() {
) )
} }
const fieldWrap = "min-w-[180px] flex-1"
const fieldTrigger =
"h-10 w-full rounded-2xl border border-slate-300 bg-slate-50/80 px-3 text-left text-sm font-semibold text-neutral-700 focus:ring-neutral-300 flex items-center gap-2"
const segmentedRoot =
"flex h-10 min-w-[200px] items-stretch rounded-full border border-slate-200 bg-slate-50/70 p-1 gap-1"
const segmentedItem =
"inline-flex h-full flex-1 items-center justify-center rounded-full px-4 text-sm font-semibold text-neutral-600 transition-colors hover:bg-slate-100 data-[state=on]:bg-cyan-600 data-[state=on]:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-300"
return ( return (
<div className="space-y-6"> <div className="space-y-6 px-4 lg:px-6">
{/* Stats */} {/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-4"> <div className="grid gap-4 md:grid-cols-4">
<Card> <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">
<CardHeader className="pb-2"> <p className="text-xs font-medium uppercase tracking-wide text-neutral-500">Total</p>
<CardDescription>Total</CardDescription> <p className="mt-1 text-2xl font-bold text-neutral-900">{stats?.total ?? 0}</p>
<CardTitle className="text-2xl">{stats?.total ?? 0}</CardTitle> </div>
</CardHeader> <div className="rounded-2xl border border-cyan-200/60 bg-cyan-50/40 p-4 shadow-sm">
</Card> <p className="text-xs font-medium uppercase tracking-wide text-cyan-600">Ativos</p>
<Card> <p className="mt-1 text-2xl font-bold text-cyan-700">{stats?.ativos ?? 0}</p>
<CardHeader className="pb-2"> </div>
<CardDescription>Ativos</CardDescription> <div className="rounded-2xl border border-red-200/60 bg-red-50/40 p-4 shadow-sm">
<CardTitle className="text-2xl text-blue-600">{stats?.ativos ?? 0}</CardTitle> <p className="text-xs font-medium uppercase tracking-wide text-red-600">Atrasados</p>
</CardHeader> <p className="mt-1 text-2xl font-bold text-red-700">{stats?.atrasados ?? 0}</p>
</Card> </div>
<Card> <div className="rounded-2xl border border-emerald-200/60 bg-emerald-50/40 p-4 shadow-sm">
<CardHeader className="pb-2"> <p className="text-xs font-medium uppercase tracking-wide text-emerald-600">Valor ativo</p>
<CardDescription>Atrasados</CardDescription> <p className="mt-1 text-2xl font-bold text-emerald-700">
<CardTitle className="text-2xl text-red-600">{stats?.atrasados ?? 0}</CardTitle> R$ {(stats?.valorTotalAtivo ?? 0).toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
</CardHeader> </p>
</Card> </div>
<Card>
<CardHeader className="pb-2">
<CardDescription>Valor ativo</CardDescription>
<CardTitle className="text-2xl">
R$ {(stats?.valorTotalAtivo ?? 0).toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
</CardTitle>
</CardHeader>
</Card>
</div> </div>
{/* Filters and Actions */} {/* Filters Section */}
<Card> <section className="rounded-3xl border border-slate-200 bg-white/90 p-4 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-4"> <div className="flex flex-col gap-4">
<div> {/* Header with title and action */}
<CardTitle>Emprestimos</CardTitle> <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<CardDescription>Gerencie o emprestimo de equipamentos para clientes.</CardDescription> <div>
</div> <h2 className="text-lg font-semibold text-neutral-900">Empréstimos de Equipamentos</h2>
<Button onClick={() => setIsCreateDialogOpen(true)}> <p className="text-sm text-neutral-500">Gerencie o empréstimo de equipamentos para clientes.</p>
<Plus className="mr-2 size-4" />
Novo emprestimo
</Button>
</CardHeader>
<CardContent>
<div className="mb-4 flex flex-wrap gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Buscar por cliente, responsavel, equipamento..."
className="pl-9"
/>
</div> </div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos status</SelectItem>
<SelectItem value="ATIVO">Ativo</SelectItem>
<SelectItem value="DEVOLVIDO">Devolvido</SelectItem>
</SelectContent>
</Select>
<SearchableCombobox
value={clienteFilter}
onValueChange={setClienteFilter}
options={companyOptions}
placeholder="Filtrar por cliente"
className="w-[200px]"
/>
<Button <Button
variant="outline" onClick={() => setIsCreateDialogOpen(true)}
onClick={() => { className="h-10 gap-2 rounded-full bg-cyan-600 px-5 font-semibold text-white shadow-sm transition-colors hover:bg-cyan-700"
setSearchQuery("")
setStatusFilter("all")
setClienteFilter(null)
}}
> >
<RotateCcw className="mr-2 size-4" /> <IconPlus className="size-4" />
Limpar Novo empréstimo
</Button> </Button>
</div> </div>
{/* Table */} {/* Search and Date Range */}
{!emprestimos ? ( <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:gap-4">
<div className="flex items-center justify-center py-10"> <div className="flex flex-1 flex-col gap-2">
<Spinner className="size-8" /> <div className="relative">
<IconSearch className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-neutral-400" />
<Input
placeholder="Buscar por cliente, responsável, equipamento..."
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
className="w-full rounded-2xl border-slate-300 bg-white/95 pl-9"
/>
</div>
</div> </div>
) : filteredEmprestimos.length === 0 ? ( <div className="flex flex-wrap items-center gap-2">
<div className="flex flex-col items-center justify-center py-10 text-center"> <DateRangeButton
<Package className="mb-4 size-12 text-muted-foreground/50" /> from={dateRange.from}
<p className="text-muted-foreground">Nenhum emprestimo encontrado.</p> to={dateRange.to}
onChange={setDateRange}
className="w-full min-w-[200px] rounded-2xl border-slate-300 bg-white/95 text-left text-sm font-semibold text-neutral-700 lg:w-auto"
align="center"
/>
<Button
variant="ghost"
className="h-10 gap-2 rounded-full text-sm font-medium text-neutral-700 hover:bg-slate-100"
onClick={handleClearFilters}
>
<IconRefresh className="size-4" />
Limpar
</Button>
</div> </div>
) : ( </div>
<div className="overflow-x-auto rounded-md border">
<Table> {/* Additional Filters */}
<TableHeader> <div className="flex flex-wrap gap-3">
<TableRow> <div className={fieldWrap}>
<TableHead>Ref</TableHead> <SearchableCombobox
<TableHead>Cliente</TableHead> value={clienteFilter}
<TableHead>Responsavel</TableHead> onValueChange={setClienteFilter}
<TableHead>Equipamentos</TableHead> options={companyOptions}
<TableHead>Data emprestimo</TableHead> placeholder="Cliente"
<TableHead>Data fim</TableHead> allowClear
<TableHead>Status</TableHead> clearLabel="Todos os clientes"
<TableHead>Valor</TableHead> triggerClassName={fieldTrigger}
<TableHead className="text-right">Acoes</TableHead> prefix={<IconBuilding className="size-4 text-neutral-400" />}
align="center"
/>
</div>
<ToggleGroup
type="single"
value={statusFilter}
onValueChange={(value) => value && setStatusFilter(value)}
className={segmentedRoot}
>
<ToggleGroupItem value="all" className={segmentedItem}>
Todos
</ToggleGroupItem>
<ToggleGroupItem value="ATIVO" className={segmentedItem}>
Ativos
</ToggleGroupItem>
<ToggleGroupItem value="DEVOLVIDO" className={segmentedItem}>
Devolvidos
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
</section>
{/* Table Section */}
<section className="rounded-3xl border border-slate-200 bg-white/90 shadow-sm overflow-hidden">
{!emprestimos ? (
<div className="flex items-center justify-center py-16">
<Spinner className="size-8" />
</div>
) : filteredEmprestimos.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<IconPackage className="mb-4 size-12 text-neutral-300" />
<p className="text-neutral-500">Nenhum empréstimo encontrado.</p>
</div>
) : (
<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>
</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>
<span className="text-sm text-neutral-600">
{emp.quantidade} item(s):{" "}
{emp.equipamentos
.slice(0, 2)
.map((eq) => eq.tipo)
.join(", ")}
{emp.equipamentos.length > 2 && "..."}
</span>
</TableCell>
<TableCell>
{format(new Date(emp.dataEmprestimo), "dd/MM/yyyy", { locale: ptBR })}
</TableCell>
<TableCell>
{format(new Date(emp.dataFimPrevisto), "dd/MM/yyyy", { locale: ptBR })}
</TableCell>
<TableCell>{getStatusBadge(emp.status, emp.dataFimPrevisto)}</TableCell>
<TableCell>
{emp.valor
? `R$ ${emp.valor.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`
: "—"}
</TableCell>
<TableCell className="text-right">
{emp.status === "ATIVO" && (
<Button
size="sm"
className="h-8 gap-1.5 rounded-full bg-emerald-600 px-3 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-emerald-700"
onClick={() => openDevolverDialog(emp.id)}
>
<IconCircleCheck className="size-3.5" />
Devolver
</Button>
)}
</TableCell>
</TableRow> </TableRow>
</TableHeader> ))}
<TableBody> </TableBody>
{filteredEmprestimos.map((emp) => ( </Table>
<TableRow key={emp.id}> </div>
<TableCell className="font-medium">#{emp.reference}</TableCell> )}
<TableCell>{emp.clienteNome}</TableCell> </section>
<TableCell>{emp.responsavelNome}</TableCell>
<TableCell>
<span className="text-sm">
{emp.quantidade} item(s):{" "}
{emp.equipamentos
.slice(0, 2)
.map((eq) => eq.tipo)
.join(", ")}
{emp.equipamentos.length > 2 && "..."}
</span>
</TableCell>
<TableCell>
{format(new Date(emp.dataEmprestimo), "dd/MM/yyyy", { locale: ptBR })}
</TableCell>
<TableCell>
{format(new Date(emp.dataFimPrevisto), "dd/MM/yyyy", { locale: ptBR })}
</TableCell>
<TableCell>{getStatusBadge(emp.status, emp.dataFimPrevisto)}</TableCell>
<TableCell>
{emp.valor
? `R$ ${emp.valor.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`
: "—"}
</TableCell>
<TableCell className="text-right">
{emp.status === "ATIVO" && (
<Button
size="sm"
variant="outline"
onClick={() => openDevolverDialog(emp.id)}
>
<CheckCircle2 className="mr-1 size-3" />
Devolver
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
{/* Create Dialog */} {/* Create Dialog */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Novo emprestimo</DialogTitle> <DialogTitle>Novo empréstimo</DialogTitle>
<DialogDescription> <DialogDescription>
Registre um novo emprestimo de equipamentos. Registre um novo empréstimo de equipamentos.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -552,30 +608,32 @@ export function EmprestimosPageClient() {
onValueChange={setFormClienteId} onValueChange={setFormClienteId}
options={companyOptions} options={companyOptions}
placeholder="Selecione o cliente" placeholder="Selecione o cliente"
prefix={<IconBuilding className="size-4 text-neutral-400" />}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Tecnico responsavel *</label> <label className="text-sm font-medium">Técnico responsável *</label>
<SearchableCombobox <SearchableCombobox
value={formTecnicoId} value={formTecnicoId}
onValueChange={setFormTecnicoId} onValueChange={setFormTecnicoId}
options={userOptions} options={userOptions}
placeholder="Selecione o tecnico" placeholder="Selecione o técnico"
prefix={<IconUser className="size-4 text-neutral-400" />}
/> />
</div> </div>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Nome do responsavel (cliente) *</label> <label className="text-sm font-medium">Nome do responsável (cliente) *</label>
<Input <Input
value={formResponsavelNome} value={formResponsavelNome}
onChange={(e) => setFormResponsavelNome(e.target.value)} onChange={(e) => setFormResponsavelNome(e.target.value)}
placeholder="Nome do responsavel" placeholder="Nome do responsável"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Contato do responsavel</label> <label className="text-sm font-medium">Contato do responsável</label>
<Input <Input
value={formResponsavelContato} value={formResponsavelContato}
onChange={(e) => setFormResponsavelContato(e.target.value)} onChange={(e) => setFormResponsavelContato(e.target.value)}
@ -586,11 +644,11 @@ export function EmprestimosPageClient() {
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Data do emprestimo</label> <label className="text-sm font-medium">Data do empréstimo</label>
<DatePicker value={formDataEmprestimo} onChange={setFormDataEmprestimo} /> <DatePicker value={formDataEmprestimo} onChange={setFormDataEmprestimo} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Data prevista devolucao *</label> <label className="text-sm font-medium">Data prevista devolução *</label>
<DatePicker value={formDataFim} onChange={setFormDataFim} /> <DatePicker value={formDataFim} onChange={setFormDataFim} />
</div> </div>
</div> </div>
@ -607,7 +665,7 @@ export function EmprestimosPageClient() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Multa diaria por atraso (R$)</label> <label className="text-sm font-medium">Multa diária por atraso (R$)</label>
<Input <Input
type="number" type="number"
step="0.01" step="0.01"
@ -621,28 +679,36 @@ export function EmprestimosPageClient() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium">Equipamentos *</label> <label className="text-sm font-medium">Equipamentos *</label>
<Button type="button" size="sm" variant="outline" onClick={handleAddEquipamento}> <Button
<Plus className="mr-1 size-3" /> type="button"
size="sm"
variant="outline"
onClick={handleAddEquipamento}
className="h-8 gap-1.5 rounded-full"
>
<IconPlus className="size-3.5" />
Adicionar Adicionar
</Button> </Button>
</div> </div>
{formEquipamentos.length === 0 ? ( {formEquipamentos.length === 0 ? (
<p className="py-4 text-center text-sm text-muted-foreground"> <div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-slate-200 py-8">
Nenhum equipamento adicionado. <IconPackage className="mb-2 size-8 text-neutral-300" />
</p> <p className="text-sm text-neutral-500">Nenhum equipamento adicionado.</p>
</div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{formEquipamentos.map((eq, idx) => ( {formEquipamentos.map((eq, idx) => (
<div key={eq.id} className="rounded-md border p-3 space-y-2"> <div key={eq.id} className="rounded-xl border border-slate-200 bg-slate-50/50 p-3 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">Equipamento {idx + 1}</span> <span className="text-sm font-semibold text-neutral-700">Equipamento {idx + 1}</span>
<Button <Button
type="button" type="button"
size="sm" size="sm"
variant="ghost" variant="ghost"
className="text-red-600" className="h-7 gap-1 text-red-600 hover:bg-red-50 hover:text-red-700"
onClick={() => handleRemoveEquipamento(eq.id)} onClick={() => handleRemoveEquipamento(eq.id)}
> >
<IconTrash className="size-3.5" />
Remover Remover
</Button> </Button>
</div> </div>
@ -651,7 +717,7 @@ export function EmprestimosPageClient() {
value={eq.tipo} value={eq.tipo}
onValueChange={(v) => handleEquipamentoChange(eq.id, "tipo", v)} onValueChange={(v) => handleEquipamentoChange(eq.id, "tipo", v)}
> >
<SelectTrigger> <SelectTrigger className="h-9">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -666,18 +732,21 @@ export function EmprestimosPageClient() {
placeholder="Marca" placeholder="Marca"
value={eq.marca} value={eq.marca}
onChange={(e) => handleEquipamentoChange(eq.id, "marca", e.target.value)} onChange={(e) => handleEquipamentoChange(eq.id, "marca", e.target.value)}
className="h-9"
/> />
<Input <Input
placeholder="Modelo" placeholder="Modelo"
value={eq.modelo} value={eq.modelo}
onChange={(e) => handleEquipamentoChange(eq.id, "modelo", e.target.value)} onChange={(e) => handleEquipamentoChange(eq.id, "modelo", e.target.value)}
className="h-9"
/> />
<Input <Input
placeholder="Serial/Patrimonio" placeholder="Serial/Patrimônio"
value={eq.serialNumber ?? ""} value={eq.serialNumber ?? ""}
onChange={(e) => onChange={(e) =>
handleEquipamentoChange(eq.id, "serialNumber", e.target.value) handleEquipamentoChange(eq.id, "serialNumber", e.target.value)
} }
className="h-9"
/> />
</div> </div>
</div> </div>
@ -687,28 +756,32 @@ export function EmprestimosPageClient() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Observacoes</label> <label className="text-sm font-medium">Observações</label>
<Textarea <Textarea
value={formObservacoes} value={formObservacoes}
onChange={(e) => setFormObservacoes(e.target.value)} onChange={(e) => setFormObservacoes(e.target.value)}
placeholder="Observacoes sobre o emprestimo..." placeholder="Observações sobre o empréstimo..."
rows={3} rows={3}
/> />
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}> <Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} className="rounded-full">
Cancelar Cancelar
</Button> </Button>
<Button onClick={handleCreate} disabled={isSubmitting}> <Button
onClick={handleCreate}
disabled={isSubmitting}
className="gap-2 rounded-full bg-cyan-600 font-semibold text-white hover:bg-cyan-700"
>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Spinner className="mr-2 size-4" /> <Spinner className="size-4" />
Criando... Criando...
</> </>
) : ( ) : (
"Criar emprestimo" "Criar empréstimo"
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
@ -719,38 +792,42 @@ export function EmprestimosPageClient() {
<Dialog open={isDevolverDialogOpen} onOpenChange={setIsDevolverDialogOpen}> <Dialog open={isDevolverDialogOpen} onOpenChange={setIsDevolverDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Registrar devolucao</DialogTitle> <DialogTitle>Registrar devolução</DialogTitle>
<DialogDescription> <DialogDescription>
Confirme a devolucao dos equipamentos emprestados. Confirme a devolução dos equipamentos emprestados.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Observacoes da devolucao</label> <label className="text-sm font-medium">Observações da devolução</label>
<Textarea <Textarea
value={formObservacoes} value={formObservacoes}
onChange={(e) => setFormObservacoes(e.target.value)} onChange={(e) => setFormObservacoes(e.target.value)}
placeholder="Condicao dos equipamentos, observacoes..." placeholder="Condição dos equipamentos, observações..."
rows={3} rows={3}
/> />
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsDevolverDialogOpen(false)}> <Button variant="outline" onClick={() => setIsDevolverDialogOpen(false)} className="rounded-full">
Cancelar Cancelar
</Button> </Button>
<Button onClick={handleDevolver} disabled={isSubmitting}> <Button
onClick={handleDevolver}
disabled={isSubmitting}
className="gap-2 rounded-full bg-emerald-600 font-semibold text-white hover:bg-emerald-700"
>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Spinner className="mr-2 size-4" /> <Spinner className="size-4" />
Registrando... Registrando...
</> </>
) : ( ) : (
<> <>
<CheckCircle2 className="mr-2 size-4" /> <IconCircleCheck className="size-4" />
Confirmar devolucao Confirmar devolução
</> </>
)} )}
</Button> </Button>

View file

@ -1269,6 +1269,7 @@ export function AdminDevicesOverview({
const [selectedUsbPolicy, setSelectedUsbPolicy] = useState<"ALLOW" | "BLOCK_ALL" | "READONLY">("ALLOW") const [selectedUsbPolicy, setSelectedUsbPolicy] = useState<"ALLOW" | "BLOCK_ALL" | "READONLY">("ALLOW")
const [isApplyingUsbPolicy, setIsApplyingUsbPolicy] = useState(false) const [isApplyingUsbPolicy, setIsApplyingUsbPolicy] = useState(false)
const [usbPolicySelection, setUsbPolicySelection] = useState<string[]>([]) const [usbPolicySelection, setUsbPolicySelection] = useState<string[]>([])
const [usbCompanyFilter, setUsbCompanyFilter] = useState<string>("all")
const [isCreateDeviceOpen, setIsCreateDeviceOpen] = useState(false) const [isCreateDeviceOpen, setIsCreateDeviceOpen] = useState(false)
const [createDeviceLoading, setCreateDeviceLoading] = useState(false) const [createDeviceLoading, setCreateDeviceLoading] = useState(false)
const [newDeviceName, setNewDeviceName] = useState("") const [newDeviceName, setNewDeviceName] = useState("")
@ -1713,15 +1714,18 @@ export function AdminDevicesOverview({
}, []) }, [])
const handleSelectAllUsbDevices = useCallback((checked: boolean) => { const handleSelectAllUsbDevices = useCallback((checked: boolean) => {
const windowsDevices = filteredDevices.filter( const allWindowsDevices = filteredDevices.filter(
(m) => (m.devicePlatform ?? "").toLowerCase() === "windows" (m) => (m.devicePlatform ?? "").toLowerCase() === "windows"
) )
const windowsDevices = usbCompanyFilter === "all"
? allWindowsDevices
: allWindowsDevices.filter((m) => (m.companySlug ?? "") === usbCompanyFilter)
if (checked) { if (checked) {
setUsbPolicySelection(windowsDevices.map((m) => m.id)) setUsbPolicySelection(windowsDevices.map((m) => m.id))
} else { } else {
setUsbPolicySelection([]) setUsbPolicySelection([])
} }
}, [filteredDevices]) }, [filteredDevices, usbCompanyFilter])
const handleApplyBulkUsbPolicy = useCallback(async () => { const handleApplyBulkUsbPolicy = useCallback(async () => {
if (usbPolicySelection.length === 0) { if (usbPolicySelection.length === 0) {
@ -1853,7 +1857,7 @@ export function AdminDevicesOverview({
<CardHeader className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between"> <CardHeader className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div> <div>
<CardTitle>Dispositivos registrados</CardTitle> <CardTitle>Dispositivos registrados</CardTitle>
<CardDescription>Sincronizadas via agente local instalado nas máquinas. Atualiza em tempo quase real.</CardDescription> <CardDescription>Sincronizadas via agente local instalado nas máquinas.</CardDescription>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Button size="sm" onClick={handleOpenCreateDevice}> <Button size="sm" onClick={handleOpenCreateDevice}>
@ -2199,20 +2203,55 @@ export function AdminDevicesOverview({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{(() => { {(() => {
const windowsDevices = filteredDevices.filter( const allWindowsDevices = filteredDevices.filter(
(m) => (m.devicePlatform ?? "").toLowerCase() === "windows" (m) => (m.devicePlatform ?? "").toLowerCase() === "windows"
) )
if (windowsDevices.length === 0) { const windowsDevices = usbCompanyFilter === "all"
? allWindowsDevices
: allWindowsDevices.filter((m) => (m.companySlug ?? "") === usbCompanyFilter)
const usbCompanyOptions = Array.from(
new Map(
allWindowsDevices
.filter((m) => m.companySlug)
.map((m) => [m.companySlug, m.companyName ?? m.companySlug])
)
).sort((a, b) => (a[1] ?? "").localeCompare(b[1] ?? "", "pt-BR"))
if (allWindowsDevices.length === 0) {
return ( return (
<div className="rounded-md border border-dashed border-slate-200 px-4 py-8 text-center text-sm text-muted-foreground"> <div className="rounded-md border border-dashed border-slate-200 px-4 py-8 text-center text-sm text-muted-foreground">
Nenhum dispositivo Windows disponível com os filtros atuais. Nenhum dispositivo Windows disponível com os filtros atuais.
</div> </div>
) )
} }
const allSelected = usbPolicySelection.length === windowsDevices.length const allSelected = windowsDevices.length > 0 && usbPolicySelection.length === windowsDevices.length
const someSelected = usbPolicySelection.length > 0 && usbPolicySelection.length < windowsDevices.length const someSelected = usbPolicySelection.length > 0 && usbPolicySelection.length < windowsDevices.length
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Filtrar por empresa</label>
<Select
value={usbCompanyFilter}
onValueChange={(value) => {
setUsbCompanyFilter(value)
setUsbPolicySelection([])
}}
disabled={isApplyingUsbPolicy}
>
<SelectTrigger>
<SelectValue placeholder="Selecione uma empresa" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
<span>Todas as empresas</span>
</SelectItem>
{usbCompanyOptions.map(([slug, name]) => (
<SelectItem key={slug} value={slug ?? ""}>
<span>{name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600"> <div className="flex items-center justify-between rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
<span> <span>
{usbPolicySelection.length} de {windowsDevices.length} dispositivos Windows selecionados {usbPolicySelection.length} de {windowsDevices.length} dispositivos Windows selecionados
@ -2220,9 +2259,14 @@ export function AdminDevicesOverview({
<Checkbox <Checkbox
checked={allSelected ? true : someSelected ? "indeterminate" : false} checked={allSelected ? true : someSelected ? "indeterminate" : false}
onCheckedChange={(value) => handleSelectAllUsbDevices(value === true || value === "indeterminate")} onCheckedChange={(value) => handleSelectAllUsbDevices(value === true || value === "indeterminate")}
disabled={isApplyingUsbPolicy} disabled={isApplyingUsbPolicy || windowsDevices.length === 0}
/> />
</div> </div>
{windowsDevices.length === 0 ? (
<div className="rounded-md border border-dashed border-slate-200 px-4 py-6 text-center text-sm text-muted-foreground">
Nenhum dispositivo Windows nesta empresa.
</div>
) : (
<div className="max-h-48 space-y-1 overflow-y-auto rounded-md border border-slate-200 p-2"> <div className="max-h-48 space-y-1 overflow-y-auto rounded-md border border-slate-200 p-2">
{windowsDevices.map((device) => { {windowsDevices.map((device) => {
const checked = usbPolicySelection.includes(device.id) const checked = usbPolicySelection.includes(device.id)
@ -2249,6 +2293,7 @@ export function AdminDevicesOverview({
) )
})} })}
</div> </div>
)}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Política USB</label> <label className="text-sm font-medium text-slate-700">Política USB</label>
<Select <Select

View file

@ -88,7 +88,7 @@ const navigation: NavigationGroup[] = [
{ title: "Modo Play", url: "/play", icon: PlayCircle, requiredRole: "staff" }, { title: "Modo Play", url: "/play", icon: PlayCircle, requiredRole: "staff" },
{ title: "Agenda", url: "/agenda", icon: CalendarDays, requiredRole: "staff" }, { title: "Agenda", url: "/agenda", icon: CalendarDays, requiredRole: "staff" },
{ title: "Dispositivos", url: "/admin/devices", icon: MonitorCog, requiredRole: "admin" }, { title: "Dispositivos", url: "/admin/devices", icon: MonitorCog, requiredRole: "admin" },
{ title: "Emprestimos", url: "/emprestimos", icon: Package, requiredRole: "staff" }, { title: "Empréstimos", url: "/emprestimos", icon: Package, requiredRole: "staff" },
], ],
}, },
{ {