"use client"; import { RiLoader4Line } from "@remixicon/react"; import { useCallback, useEffect, useMemo, useState, useTransition, } from "react"; import { toast } from "sonner"; import { createInstallmentAnticipationAction, getEligibleInstallmentsAction, } from "@/app/(dashboard)/lancamentos/anticipation-actions"; import { CategoryIcon } from "@/components/categorias/category-icon"; import MoneyValues from "@/components/money-values"; import { PeriodPicker } from "@/components/period-picker"; import { Button } from "@/components/ui/button"; import { CurrencyInput } from "@/components/ui/currency-input"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Field, FieldContent, FieldGroup, FieldLabel, FieldLegend, } from "@/components/ui/field"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; import type { EligibleInstallment } from "@/lib/installments/anticipation-types"; import { InstallmentSelectionTable } from "./installment-selection-table"; interface AnticipateInstallmentsDialogProps { trigger?: React.ReactNode; seriesId: string; lancamentoName: string; categorias: Array<{ id: string; name: string; icon: string | null }>; pagadores: Array<{ id: string; name: string }>; defaultPeriod: string; open?: boolean; onOpenChange?: (open: boolean) => void; } type AnticipationFormValues = { anticipationPeriod: string; discount: string; pagadorId: string; categoriaId: string; note: string; }; export function AnticipateInstallmentsDialog({ trigger, seriesId, lancamentoName, categorias, pagadores, defaultPeriod, open, onOpenChange, }: AnticipateInstallmentsDialogProps) { const [errorMessage, setErrorMessage] = useState(null); const [isPending, startTransition] = useTransition(); const [isLoadingInstallments, setIsLoadingInstallments] = useState(false); const [eligibleInstallments, setEligibleInstallments] = useState< EligibleInstallment[] >([]); const [selectedIds, setSelectedIds] = useState([]); // Use controlled state hook for dialog open state const [dialogOpen, setDialogOpen] = useControlledState( open, false, onOpenChange, ); // Use form state hook for form management const { formState, updateField, setFormState } = useFormState({ anticipationPeriod: defaultPeriod, discount: "0", pagadorId: "", categoriaId: "", note: "", }); // Buscar parcelas elegíveis ao abrir o dialog useEffect(() => { if (dialogOpen) { setIsLoadingInstallments(true); setSelectedIds([]); setErrorMessage(null); getEligibleInstallmentsAction(seriesId) .then((result) => { if (!result.success) { toast.error(result.error || "Erro ao carregar parcelas"); setEligibleInstallments([]); return; } const installments = result.data ?? []; setEligibleInstallments(installments); // Pré-preencher pagador e categoria da primeira parcela if (installments.length > 0) { const first = installments[0]; setFormState({ anticipationPeriod: defaultPeriod, discount: "0", pagadorId: first.pagadorId ?? "", categoriaId: first.categoriaId ?? "", note: "", }); } }) .catch((error) => { console.error("Erro ao buscar parcelas:", error); toast.error("Erro ao carregar parcelas elegíveis"); setEligibleInstallments([]); }) .finally(() => { setIsLoadingInstallments(false); }); } }, [dialogOpen, seriesId, defaultPeriod, setFormState]); const totalAmount = useMemo(() => { return eligibleInstallments .filter((inst) => selectedIds.includes(inst.id)) .reduce((sum, inst) => sum + Number(inst.amount), 0); }, [eligibleInstallments, selectedIds]); const finalAmount = useMemo(() => { // Se for despesa (negativo), soma o desconto para reduzir // Se for receita (positivo), subtrai o desconto const discount = Number(formState.discount) || 0; return totalAmount < 0 ? totalAmount + discount : totalAmount - discount; }, [totalAmount, formState.discount]); const handleSubmit = useCallback( (event: React.FormEvent) => { event.preventDefault(); setErrorMessage(null); if (selectedIds.length === 0) { const message = "Selecione pelo menos uma parcela para antecipar."; setErrorMessage(message); toast.error(message); return; } if (formState.anticipationPeriod.length === 0) { const message = "Informe o período da antecipação."; setErrorMessage(message); toast.error(message); return; } const discount = Number(formState.discount) || 0; if (discount > Math.abs(totalAmount)) { const message = "O desconto não pode ser maior que o valor total das parcelas."; setErrorMessage(message); toast.error(message); return; } startTransition(async () => { const result = await createInstallmentAnticipationAction({ seriesId, installmentIds: selectedIds, anticipationPeriod: formState.anticipationPeriod, discount: Number(formState.discount) || 0, pagadorId: formState.pagadorId || undefined, categoriaId: formState.categoriaId || undefined, note: formState.note || undefined, }); if (result.success) { toast.success(result.message); setDialogOpen(false); } else { const errorMsg = result.error || "Erro ao criar antecipação"; setErrorMessage(errorMsg); toast.error(errorMsg); } }); }, [selectedIds, formState, seriesId, setDialogOpen, totalAmount], ); const handleCancel = useCallback(() => { setDialogOpen(false); }, [setDialogOpen]); return ( {trigger && {trigger}} Antecipar Parcelas {lancamentoName}
{/* Seção 1: Seleção de Parcelas */} Parcelas Disponíveis {isLoadingInstallments ? (
Carregando parcelas...
) : (
)}
{/* Seção 2: Configuração da Antecipação */} Configuração
Período updateField("anticipationPeriod", value) } disabled={isPending} className="w-full" /> Desconto updateField("discount", value)} placeholder="R$ 0,00" disabled={isPending} /> Pagador Categoria Observação