"use client"; import { createInstallmentAnticipationAction, getEligibleInstallmentsAction, } from "@/app/(dashboard)/lancamentos/anticipation-actions"; import { CategoryIcon } from "@/components/categorias/category-icon"; import { InstallmentSelectionTable } from "./installment-selection-table"; 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 { RiLoader4Line } from "@remixicon/react"; import { useCallback, useEffect, useMemo, useState, useTransition, } from "react"; import { toast } from "sonner"; import MoneyValues from "@/components/money-values"; 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: number; pagadorId: string; categoriaId: string; note: string; }; type SelectOption = { value: string; label: string; }; const monthFormatter = new Intl.DateTimeFormat("pt-BR", { month: "long", year: "numeric", }); const formatPeriodLabel = (period: string) => { const [year, month] = period.split("-").map(Number); if (!year || !month) { return period; } const date = new Date(year, month - 1, 1); if (Number.isNaN(date.getTime())) { return period; } const label = monthFormatter.format(date); return label.charAt(0).toUpperCase() + label.slice(1); }; const buildPeriodOptions = (currentValue?: string): SelectOption[] => { const now = new Date(); const options: SelectOption[] = []; // Adiciona opções de 3 meses no passado até 6 meses no futuro for (let offset = -3; offset <= 6; offset += 1) { const date = new Date(now.getFullYear(), now.getMonth() + offset, 1); const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart( 2, "0" )}`; options.push({ value, label: formatPeriodLabel(value) }); } // Adiciona o valor atual se não estiver na lista if ( currentValue && !options.some((option) => option.value === currentValue) ) { options.push({ value: currentValue, label: formatPeriodLabel(currentValue), }); } return options.sort((a, b) => a.value.localeCompare(b.value)); }; 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: "", }); const periodOptions = useMemo( () => buildPeriodOptions(formState.anticipationPeriod), [formState.anticipationPeriod] ); // Buscar parcelas elegíveis ao abrir o dialog useEffect(() => { if (dialogOpen) { setIsLoadingInstallments(true); setSelectedIds([]); setErrorMessage(null); getEligibleInstallmentsAction(seriesId) .then((result) => { if (result.success && result.data) { setEligibleInstallments(result.data); // Pré-preencher pagador e categoria da primeira parcela if (result.data.length > 0) { const first = result.data[0]; setFormState({ anticipationPeriod: defaultPeriod, discount: 0, pagadorId: first.pagadorId ?? "", categoriaId: first.categoriaId ?? "", note: "", }); } } else { toast.error(result.error || "Erro ao carregar parcelas"); setEligibleInstallments([]); } }) .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] ); 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 Desconto updateField("discount", value ?? 0) } placeholder="R$ 0,00" disabled={isPending} /> Pagador Categoria Observação