import { useEffect, useState } from "react"; import { X } from "lucide-react"; import { useCategories } from "../hooks/useCategories"; import { useCreateTransaction, useUpdateTransaction, } from "../hooks/useTransactions"; import { eurosToCents, centsToEuros } from "../utils/format"; import { useToast } from "../context/ToastContext"; import type { Transaction, TransactionType } from "../api/types"; interface Props { isOpen: boolean; onClose: () => void; transaction?: Transaction; } interface FormState { amount: string; type: TransactionType; category_id: string; description: string; transaction_date: string; } function todayISO(): string { return new Date().toISOString().slice(0, 10); } const DEFAULT_FORM: FormState = { amount: "", type: "expense", category_id: "", description: "", transaction_date: todayISO(), }; export default function TransactionForm({ isOpen, onClose, transaction }: Props) { const { data: categories = [] } = useCategories(); const createMutation = useCreateTransaction(); const updateMutation = useUpdateTransaction(); const { addToast } = useToast(); const [form, setForm] = useState(DEFAULT_FORM); const [errors, setErrors] = useState>>({}); const isEditing = !!transaction; const isPending = createMutation.isPending || updateMutation.isPending; // Populate form when editing useEffect(() => { if (transaction) { setForm({ amount: String(centsToEuros(transaction.amount_cents)), type: transaction.type, category_id: transaction.category.id, description: transaction.description ?? "", transaction_date: transaction.transaction_date, }); } else { setForm(DEFAULT_FORM); } setErrors({}); }, [transaction, isOpen]); const filteredCategories = categories.filter( (c) => c.type === form.type || c.type === "both", ); function set(key: K, value: FormState[K]) { setForm((prev) => { const next = { ...prev, [key]: value }; // Reset category when type changes and current category is incompatible if (key === "type") { const cat = categories.find((c) => c.id === prev.category_id); if (cat && cat.type !== "both" && cat.type !== value) { next.category_id = ""; } } return next; }); if (errors[key]) setErrors((e) => ({ ...e, [key]: undefined })); } function validate(): boolean { const errs: Partial> = {}; const amountNum = parseFloat(form.amount); if (!form.amount || isNaN(amountNum) || amountNum <= 0) errs.amount = "Montant invalide (doit être > 0)"; if (!form.category_id) errs.category_id = "Catégorie requise"; if (!form.transaction_date) errs.transaction_date = "Date requise"; setErrors(errs); return Object.keys(errs).length === 0; } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!validate()) return; const payload = { amount_cents: eurosToCents(parseFloat(form.amount)), type: form.type, category_id: form.category_id, description: form.description || undefined, transaction_date: form.transaction_date, }; if (isEditing && transaction) { updateMutation.mutate( { id: transaction.id, payload }, { onSuccess: () => { addToast({ type: "success", message: "Transaction modifiée" }); onClose(); }, onError: () => addToast({ type: "error", message: "Erreur lors de la modification" }), }, ); } else { createMutation.mutate(payload, { onSuccess: () => { addToast({ type: "success", message: "Transaction ajoutée" }); onClose(); }, onError: () => addToast({ type: "error", message: "Erreur lors de l'ajout" }), }); } } if (!isOpen) return null; return (
e.stopPropagation()} > {/* Header */}

{isEditing ? "Modifier la transaction" : "Nouvelle transaction"}

{/* Type toggle */}
{(["expense", "income"] as TransactionType[]).map((t) => ( ))}
{/* Amount */}
set("amount", e.target.value)} placeholder="0,00" className={`w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30 ${ errors.amount ? "border-red-400" : "border-slate-200" }`} /> {errors.amount && (

{errors.amount}

)}
{/* Date */}
set("transaction_date", e.target.value)} className={`w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30 ${ errors.transaction_date ? "border-red-400" : "border-slate-200" }`} /> {errors.transaction_date && (

{errors.transaction_date}

)}
{/* Category */}
{errors.category_id && (

{errors.category_id}

)}
{/* Description */}
set("description", e.target.value)} placeholder="Ex. : Courses Carrefour" className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30" />
{/* Actions */}
); }