"use client"; import { createBudgetAction, updateBudgetAction, } from "@/app/(dashboard)/orcamentos/actions"; import { CategoryIcon } from "@/components/categorias/category-icon"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { CurrencyInput } from "@/components/ui/currency-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; import { useCallback, useEffect, useMemo, useState, useTransition, } from "react"; import { toast } from "sonner"; import type { Budget, BudgetCategory, BudgetFormValues } from "./types"; interface BudgetDialogProps { mode: "create" | "update"; trigger?: React.ReactNode; budget?: Budget; categories: BudgetCategory[]; defaultPeriod: string; open?: boolean; onOpenChange?: (open: boolean) => void; } 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[] = []; for (let offset = -3; offset <= 3; 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) }); } 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)) .map((option) => ({ value: option.value, label: option.label, })); }; const buildInitialValues = ({ budget, defaultPeriod, }: { budget?: Budget; defaultPeriod: string; }): BudgetFormValues => ({ categoriaId: budget?.category?.id ?? "", period: budget?.period ?? defaultPeriod, amount: budget ? (Math.round(budget.amount * 100) / 100).toFixed(2) : "", }); export function BudgetDialog({ mode, trigger, budget, categories, defaultPeriod, open, onOpenChange, }: BudgetDialogProps) { const [errorMessage, setErrorMessage] = useState(null); const [isPending, startTransition] = useTransition(); // Use controlled state hook for dialog open state const [dialogOpen, setDialogOpen] = useControlledState( open, false, onOpenChange ); const initialState = useMemo( () => buildInitialValues({ budget, defaultPeriod, }), [budget, defaultPeriod] ); // Use form state hook for form management const { formState, updateField, setFormState } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { setFormState(initialState); setErrorMessage(null); } }, [dialogOpen, initialState, setFormState]); // Clear error when dialog closes useEffect(() => { if (!dialogOpen) { setErrorMessage(null); } }, [dialogOpen]); const periodOptions = useMemo( () => buildPeriodOptions(formState.period), [formState.period] ); const handleSubmit = useCallback( (event: React.FormEvent) => { event.preventDefault(); setErrorMessage(null); if (mode === "update" && !budget?.id) { const message = "Orçamento inválido."; setErrorMessage(message); toast.error(message); return; } if (formState.categoriaId.length === 0) { const message = "Selecione uma categoria."; setErrorMessage(message); toast.error(message); return; } if (formState.period.length === 0) { const message = "Informe o período."; setErrorMessage(message); toast.error(message); return; } if (formState.amount.length === 0) { const message = "Informe o valor limite."; setErrorMessage(message); toast.error(message); return; } const payload = { categoriaId: formState.categoriaId, period: formState.period, amount: formState.amount, }; startTransition(async () => { const result = mode === "create" ? await createBudgetAction(payload) : await updateBudgetAction({ id: budget?.id ?? "", ...payload, }); if (result.success) { toast.success(result.message); setDialogOpen(false); setFormState(initialState); return; } setErrorMessage(result.error); toast.error(result.error); }); }, [budget?.id, formState, initialState, mode, setDialogOpen, setFormState] ); const title = mode === "create" ? "Novo orçamento" : "Editar orçamento"; const description = mode === "create" ? "Defina um limite de gastos para acompanhar suas despesas." : "Atualize os detalhes do orçamento selecionado."; const submitLabel = mode === "create" ? "Salvar orçamento" : "Atualizar orçamento"; const disabled = categories.length === 0; return ( {trigger ? {trigger} : null} {title} {description} {disabled ? (
Cadastre pelo menos uma categoria de despesa para criar um orçamento.
) : (
updateField("amount", value)} />
{errorMessage ? (

{errorMessage}

) : null}
)}
); }