sistema-de-chamados/src/app/emprestimos/emprestimos-page-client.tsx
rever-tecnologia 38995b95c6 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>
2025-12-04 14:52:27 -03:00

839 lines
31 KiB
TypeScript

"use client"
import { useState, useMemo, useCallback } from "react"
import { useMutation, useQuery } from "convex/react"
import { format } from "date-fns"
import { ptBR } from "date-fns/locale"
import { toast } from "sonner"
import {
IconPlus,
IconPackage,
IconCircleCheck,
IconClock,
IconAlertTriangle,
IconSearch,
IconRefresh,
IconBuilding,
IconUser,
IconTrash,
} from "@tabler/icons-react"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { useAuth } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Spinner } from "@/components/ui/spinner"
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 { Textarea } from "@/components/ui/textarea"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const EQUIPAMENTO_TIPOS = [
"NOTEBOOK",
"DESKTOP",
"MONITOR",
"TECLADO",
"MOUSE",
"HEADSET",
"WEBCAM",
"IMPRESSORA",
"SCANNER",
"PROJETOR",
"TABLET",
"CELULAR",
"ROTEADOR",
"SWITCH",
"OUTRO",
] as const
type Equipamento = {
id: string
tipo: string
marca: string
modelo: string
serialNumber?: string
patrimonio?: string
}
type EmprestimoListItem = {
id: string
reference: number
clienteId: string
clienteNome: string
responsavelNome: string
tecnicoId: string
tecnicoNome: string
equipamentos: Equipamento[]
quantidade: number
valor?: number
dataEmprestimo: number
dataFimPrevisto: number
dataDevolucao?: number
status: string
observacoes?: string
multaDiaria?: number
multaCalculada?: number
createdAt: number
updatedAt: number
}
function getStatusBadge(status: string, dataFimPrevisto: number) {
const now = Date.now()
const isAtrasado = status === "ATIVO" && now > dataFimPrevisto
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>
)
}
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>
)
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>
)
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>
)
default:
return null
}
}
const ALL_VALUE = "ALL"
export function EmprestimosPageClient() {
const { session, convexUserId } = useAuth()
const tenantId = session?.user?.tenantId ?? null
const [searchQuery, setSearchQuery] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("all")
const [clienteFilter, setClienteFilter] = useState<string | null>(null)
const [dateRange, setDateRange] = useState<DateRangeValue>({ from: null, to: null })
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isDevolverDialogOpen, setIsDevolverDialogOpen] = useState(false)
const [selectedEmprestimoId, setSelectedEmprestimoId] = useState<string | null>(null)
// Form states
const [formClienteId, setFormClienteId] = useState<string | null>(null)
const [formResponsavelNome, setFormResponsavelNome] = useState("")
const [formResponsavelContato, setFormResponsavelContato] = useState("")
const [formTecnicoId, setFormTecnicoId] = useState<string | null>(null)
const [formDataEmprestimo, setFormDataEmprestimo] = useState<string | null>(format(new Date(), "yyyy-MM-dd"))
const [formDataFim, setFormDataFim] = useState<string | null>(null)
const [formValor, setFormValor] = useState("")
const [formMultaDiaria, setFormMultaDiaria] = useState("")
const [formObservacoes, setFormObservacoes] = useState("")
const [formEquipamentos, setFormEquipamentos] = useState<Array<{
id: string
tipo: string
marca: string
modelo: string
serialNumber?: string
patrimonio?: string
}>>([])
const [isSubmitting, setIsSubmitting] = useState(false)
// Queries
const emprestimos = useQuery(
api.emprestimos.list,
tenantId && convexUserId
? {
tenantId,
viewerId: convexUserId as Id<"users">,
status: statusFilter !== "all" ? statusFilter : undefined,
clienteId: clienteFilter ? (clienteFilter as Id<"companies">) : undefined,
}
: "skip"
)
const stats = useQuery(
api.emprestimos.getStats,
tenantId && convexUserId
? { tenantId, viewerId: convexUserId as Id<"users"> }
: "skip"
)
const companies = useQuery(
api.companies.list,
tenantId && convexUserId
? { tenantId, viewerId: convexUserId as Id<"users"> }
: "skip"
) as Array<{ id: string; name: string; slug?: string }> | undefined
const agents = useQuery(
api.users.listAgents,
tenantId ? { tenantId } : "skip"
)
// Mutations
const createEmprestimo = useMutation(api.emprestimos.create)
const devolverEmprestimo = useMutation(api.emprestimos.devolver)
const companyOptions = useMemo<SearchableComboboxOption[]>(() => {
return (companies ?? []).map((c) => ({
value: c.id,
label: c.name,
}))
}, [companies])
const userOptions = useMemo<SearchableComboboxOption[]>(() => {
return (agents ?? []).map((u: { _id: string; name: string }) => ({
value: u._id,
label: u.name,
}))
}, [agents])
const filteredEmprestimos = useMemo<EmprestimoListItem[]>(() => {
if (!emprestimos) return []
let list = emprestimos as EmprestimoListItem[]
// Filtro por busca
const q = searchQuery.toLowerCase().trim()
if (q) {
list = list.filter((e) => {
const searchFields = [
e.clienteNome,
e.responsavelNome,
e.tecnicoNome,
String(e.reference),
...e.equipamentos.map((eq) => `${eq.tipo} ${eq.marca} ${eq.modelo}`),
]
.join(" ")
.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(() => {
setFormEquipamentos((prev) => [
...prev,
{
id: crypto.randomUUID(),
tipo: "NOTEBOOK",
marca: "",
modelo: "",
},
])
}, [])
const handleRemoveEquipamento = useCallback((id: string) => {
setFormEquipamentos((prev) => prev.filter((eq) => eq.id !== id))
}, [])
const handleEquipamentoChange = useCallback(
(id: string, field: string, value: string) => {
setFormEquipamentos((prev) =>
prev.map((eq) => (eq.id === id ? { ...eq, [field]: value } : eq))
)
},
[]
)
const resetForm = useCallback(() => {
setFormClienteId(null)
setFormResponsavelNome("")
setFormResponsavelContato("")
setFormTecnicoId(null)
setFormDataEmprestimo(format(new Date(), "yyyy-MM-dd"))
setFormDataFim(null)
setFormValor("")
setFormMultaDiaria("")
setFormObservacoes("")
setFormEquipamentos([])
}, [])
const handleCreate = useCallback(async () => {
if (!tenantId || !convexUserId) return
if (!formClienteId || !formTecnicoId || !formDataFim || formEquipamentos.length === 0) {
toast.error("Preencha todos os campos obrigatórios.")
return
}
if (!formResponsavelNome.trim()) {
toast.error("Informe o nome do responsável.")
return
}
setIsSubmitting(true)
try {
const result = await createEmprestimo({
tenantId,
createdBy: convexUserId as Id<"users">,
clienteId: formClienteId as Id<"companies">,
responsavelNome: formResponsavelNome,
responsavelContato: formResponsavelContato || undefined,
tecnicoId: formTecnicoId as Id<"users">,
equipamentos: formEquipamentos,
valor: formValor ? parseFloat(formValor) : undefined,
dataEmprestimo: formDataEmprestimo ? new Date(formDataEmprestimo).getTime() : Date.now(),
dataFimPrevisto: new Date(formDataFim).getTime(),
observacoes: formObservacoes || undefined,
multaDiaria: formMultaDiaria ? parseFloat(formMultaDiaria) : undefined,
})
toast.success(`Empréstimo #${result.reference} criado com sucesso.`)
setIsCreateDialogOpen(false)
resetForm()
} catch (error) {
console.error("[emprestimos] Falha ao criar", error)
toast.error("Falha ao criar empréstimo.")
} finally {
setIsSubmitting(false)
}
}, [
tenantId,
convexUserId,
formClienteId,
formTecnicoId,
formDataFim,
formEquipamentos,
formResponsavelNome,
formResponsavelContato,
formDataEmprestimo,
formValor,
formMultaDiaria,
formObservacoes,
createEmprestimo,
resetForm,
])
const handleDevolver = useCallback(async () => {
if (!selectedEmprestimoId || !convexUserId) return
setIsSubmitting(true)
try {
const result = await devolverEmprestimo({
id: selectedEmprestimoId as Id<"emprestimos">,
updatedBy: convexUserId as Id<"users">,
observacoes: formObservacoes || undefined,
})
if (result.multaCalculada) {
toast.success(`Empréstimo devolvido com multa de R$ ${result.multaCalculada.toFixed(2)}.`)
} else {
toast.success("Empréstimo devolvido com sucesso.")
}
setIsDevolverDialogOpen(false)
setSelectedEmprestimoId(null)
setFormObservacoes("")
} catch (error) {
console.error("[emprestimos] Falha ao devolver", error)
toast.error("Falha ao registrar devolução.")
} finally {
setIsSubmitting(false)
}
}, [selectedEmprestimoId, convexUserId, formObservacoes, devolverEmprestimo])
const openDevolverDialog = useCallback((id: string) => {
setSelectedEmprestimoId(id)
setFormObservacoes("")
setIsDevolverDialogOpen(true)
}, [])
const handleClearFilters = useCallback(() => {
setSearchQuery("")
setStatusFilter("all")
setClienteFilter(null)
setDateRange({ from: null, to: null })
}, [])
if (!tenantId || !convexUserId) {
return (
<div className="flex items-center justify-center py-20">
<Spinner className="size-8" />
</div>
)
}
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 (
<div className="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">
<p className="text-xs font-medium uppercase tracking-wide text-neutral-500">Total</p>
<p className="mt-1 text-2xl font-bold text-neutral-900">{stats?.total ?? 0}</p>
</div>
<div className="rounded-2xl border border-cyan-200/60 bg-cyan-50/40 p-4 shadow-sm">
<p className="text-xs font-medium uppercase tracking-wide text-cyan-600">Ativos</p>
<p className="mt-1 text-2xl font-bold text-cyan-700">{stats?.ativos ?? 0}</p>
</div>
<div className="rounded-2xl border border-red-200/60 bg-red-50/40 p-4 shadow-sm">
<p className="text-xs font-medium uppercase tracking-wide text-red-600">Atrasados</p>
<p className="mt-1 text-2xl font-bold text-red-700">{stats?.atrasados ?? 0}</p>
</div>
<div className="rounded-2xl border border-emerald-200/60 bg-emerald-50/40 p-4 shadow-sm">
<p className="text-xs font-medium uppercase tracking-wide text-emerald-600">Valor ativo</p>
<p className="mt-1 text-2xl font-bold text-emerald-700">
R$ {(stats?.valorTotalAtivo ?? 0).toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
</p>
</div>
</div>
{/* Filters Section */}
<section className="rounded-3xl border border-slate-200 bg-white/90 p-4 shadow-sm">
<div className="flex flex-col gap-4">
{/* Header with title and action */}
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="text-lg font-semibold text-neutral-900">Empréstimos de Equipamentos</h2>
<p className="text-sm text-neutral-500">Gerencie o empréstimo de equipamentos para clientes.</p>
</div>
<Button
onClick={() => setIsCreateDialogOpen(true)}
className="h-10 gap-2 rounded-full bg-cyan-600 px-5 font-semibold text-white shadow-sm transition-colors hover:bg-cyan-700"
>
<IconPlus className="size-4" />
Novo empréstimo
</Button>
</div>
{/* Search and Date Range */}
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:gap-4">
<div className="flex flex-1 flex-col gap-2">
<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 className="flex flex-wrap items-center gap-2">
<DateRangeButton
from={dateRange.from}
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>
{/* Additional Filters */}
<div className="flex flex-wrap gap-3">
<div className={fieldWrap}>
<SearchableCombobox
value={clienteFilter}
onValueChange={setClienteFilter}
options={companyOptions}
placeholder="Cliente"
allowClear
clearLabel="Todos os clientes"
triggerClassName={fieldTrigger}
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>
))}
</TableBody>
</Table>
</div>
)}
</section>
{/* Create Dialog */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Novo empréstimo</DialogTitle>
<DialogDescription>
Registre um novo empréstimo de equipamentos.
</DialogDescription>
</DialogHeader>
<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>
<SearchableCombobox
value={formClienteId}
onValueChange={setFormClienteId}
options={companyOptions}
placeholder="Selecione o cliente"
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>
<SearchableCombobox
value={formTecnicoId}
onValueChange={setFormTecnicoId}
options={userOptions}
placeholder="Selecione o técnico"
prefix={<IconUser className="size-4 text-neutral-400" />}
/>
</div>
</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>
<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>
<Input
value={formResponsavelContato}
onChange={(e) => setFormResponsavelContato(e.target.value)}
placeholder="Telefone ou e-mail"
/>
</div>
</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>
<DatePicker value={formDataEmprestimo} onChange={setFormDataEmprestimo} />
</div>
<div className="space-y-2">
<label className="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>
<Input
type="number"
step="0.01"
value={formValor}
onChange={(e) => setFormValor(e.target.value)}
placeholder="0,00"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Multa diária por atraso (R$)</label>
<Input
type="number"
step="0.01"
value={formMultaDiaria}
onChange={(e) => setFormMultaDiaria(e.target.value)}
placeholder="0,00"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Equipamentos *</label>
<Button
type="button"
size="sm"
variant="outline"
onClick={handleAddEquipamento}
className="h-8 gap-1.5 rounded-full"
>
<IconPlus className="size-3.5" />
Adicionar
</Button>
</div>
{formEquipamentos.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-slate-200 py-8">
<IconPackage className="mb-2 size-8 text-neutral-300" />
<p className="text-sm text-neutral-500">Nenhum equipamento adicionado.</p>
</div>
) : (
<div className="space-y-3">
{formEquipamentos.map((eq, idx) => (
<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">
<span className="text-sm font-semibold text-neutral-700">Equipamento {idx + 1}</span>
<Button
type="button"
size="sm"
variant="ghost"
className="h-7 gap-1 text-red-600 hover:bg-red-50 hover:text-red-700"
onClick={() => handleRemoveEquipamento(eq.id)}
>
<IconTrash className="size-3.5" />
Remover
</Button>
</div>
<div className="grid gap-2 md:grid-cols-4">
<Select
value={eq.tipo}
onValueChange={(v) => handleEquipamentoChange(eq.id, "tipo", v)}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EQUIPAMENTO_TIPOS.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="Marca"
value={eq.marca}
onChange={(e) => handleEquipamentoChange(eq.id, "marca", e.target.value)}
className="h-9"
/>
<Input
placeholder="Modelo"
value={eq.modelo}
onChange={(e) => handleEquipamentoChange(eq.id, "modelo", e.target.value)}
className="h-9"
/>
<Input
placeholder="Serial/Patrimônio"
value={eq.serialNumber ?? ""}
onChange={(e) =>
handleEquipamentoChange(eq.id, "serialNumber", e.target.value)
}
className="h-9"
/>
</div>
</div>
))}
</div>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Observações</label>
<Textarea
value={formObservacoes}
onChange={(e) => setFormObservacoes(e.target.value)}
placeholder="Observações sobre o empréstimo..."
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} className="rounded-full">
Cancelar
</Button>
<Button
onClick={handleCreate}
disabled={isSubmitting}
className="gap-2 rounded-full bg-cyan-600 font-semibold text-white hover:bg-cyan-700"
>
{isSubmitting ? (
<>
<Spinner className="size-4" />
Criando...
</>
) : (
"Criar empréstimo"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Devolver Dialog */}
<Dialog open={isDevolverDialogOpen} onOpenChange={setIsDevolverDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Registrar devolução</DialogTitle>
<DialogDescription>
Confirme a devolução dos equipamentos emprestados.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Observações da devolução</label>
<Textarea
value={formObservacoes}
onChange={(e) => setFormObservacoes(e.target.value)}
placeholder="Condição dos equipamentos, observações..."
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDevolverDialogOpen(false)} className="rounded-full">
Cancelar
</Button>
<Button
onClick={handleDevolver}
disabled={isSubmitting}
className="gap-2 rounded-full bg-emerald-600 font-semibold text-white hover:bg-emerald-700"
>
{isSubmitting ? (
<>
<Spinner className="size-4" />
Registrando...
</>
) : (
<>
<IconCircleCheck className="size-4" />
Confirmar devolução
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}