"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 ( Atrasado ) } switch (status) { case "ATIVO": return ( Ativo ) case "DEVOLVIDO": return ( Devolvido ) case "CANCELADO": return ( Cancelado ) 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("all") const [clienteFilter, setClienteFilter] = useState(null) const [dateRange, setDateRange] = useState({ from: null, to: null }) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isDevolverDialogOpen, setIsDevolverDialogOpen] = useState(false) const [selectedEmprestimoId, setSelectedEmprestimoId] = useState(null) // Form states const [formClienteId, setFormClienteId] = useState(null) const [formResponsavelNome, setFormResponsavelNome] = useState("") const [formResponsavelContato, setFormResponsavelContato] = useState("") const [formTecnicoId, setFormTecnicoId] = useState(null) const [formDataEmprestimo, setFormDataEmprestimo] = useState(format(new Date(), "yyyy-MM-dd")) const [formDataFim, setFormDataFim] = useState(null) const [formValor, setFormValor] = useState("") const [formMultaDiaria, setFormMultaDiaria] = useState("") const [formObservacoes, setFormObservacoes] = useState("") const [formEquipamentos, setFormEquipamentos] = useState>([]) 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(() => { return (companies ?? []).map((c) => ({ value: c.id, label: c.name, })) }, [companies]) const userOptions = useMemo(() => { return (agents ?? []).map((u: { _id: string; name: string }) => ({ value: u._id, label: u.name, })) }, [agents]) const filteredEmprestimos = useMemo(() => { 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 (
) } 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 (
{/* Stats Cards */}

Total

{stats?.total ?? 0}

Ativos

{stats?.ativos ?? 0}

Atrasados

{stats?.atrasados ?? 0}

Valor ativo

R$ {(stats?.valorTotalAtivo ?? 0).toLocaleString("pt-BR", { minimumFractionDigits: 2 })}

{/* Filters Section */}
{/* Header with title and action */}

Empréstimos de Equipamentos

Gerencie o empréstimo de equipamentos para clientes.

{/* Search and Date Range */}
setSearchQuery(event.target.value)} className="w-full rounded-2xl border-slate-300 bg-white/95 pl-9" />
{/* Additional Filters */}
} align="center" />
value && setStatusFilter(value)} className={segmentedRoot} > Todos Ativos Devolvidos
{/* Table Section */}
{!emprestimos ? (
) : filteredEmprestimos.length === 0 ? (

Nenhum empréstimo encontrado.

) : (
Ref Cliente Responsável Equipamentos Data empréstimo Data prevista Status Valor Ações {filteredEmprestimos.map((emp) => ( #{emp.reference} {emp.clienteNome} {emp.responsavelNome} {emp.quantidade} item(s):{" "} {emp.equipamentos .slice(0, 2) .map((eq) => eq.tipo) .join(", ")} {emp.equipamentos.length > 2 && "..."} {format(new Date(emp.dataEmprestimo), "dd/MM/yyyy", { locale: ptBR })} {format(new Date(emp.dataFimPrevisto), "dd/MM/yyyy", { locale: ptBR })} {getStatusBadge(emp.status, emp.dataFimPrevisto)} {emp.valor ? `R$ ${emp.valor.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}` : "—"} {emp.status === "ATIVO" && ( )} ))}
)}
{/* Create Dialog */} Novo empréstimo Registre um novo empréstimo de equipamentos.
} />
} />
setFormResponsavelNome(e.target.value)} placeholder="Nome do responsável" />
setFormResponsavelContato(e.target.value)} placeholder="Telefone ou e-mail" />
setFormValor(e.target.value)} placeholder="0,00" />
setFormMultaDiaria(e.target.value)} placeholder="0,00" />
{formEquipamentos.length === 0 ? (

Nenhum equipamento adicionado.

) : (
{formEquipamentos.map((eq, idx) => (
Equipamento {idx + 1}
handleEquipamentoChange(eq.id, "marca", e.target.value)} className="h-9" /> handleEquipamentoChange(eq.id, "modelo", e.target.value)} className="h-9" /> handleEquipamentoChange(eq.id, "serialNumber", e.target.value) } className="h-9" />
))}
)}