From e6d6f5c2b32c8bdcac75eb9bf6d73c0f81dd8d41 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 09:54:05 +0000 Subject: [PATCH] Refactor: Split TransactionForm into smaller components Refactors TransactionForm.tsx into smaller, reusable components and a custom hook to improve code organization and maintainability. --- src/components/dashboard/TransactionForm.tsx | 361 +++--------------- .../transaction/GroupSelectField.tsx | 45 +++ .../transaction/TransactionDateField.tsx | 24 ++ .../transaction/TransactionFormActions.tsx | 34 ++ .../transaction/TransactionTextField.tsx | 38 ++ .../transaction/TransactionTypeField.tsx | 38 ++ src/hooks/useTransactionForm.ts | 191 +++++++++ 7 files changed, 418 insertions(+), 313 deletions(-) create mode 100644 src/components/transaction/GroupSelectField.tsx create mode 100644 src/components/transaction/TransactionDateField.tsx create mode 100644 src/components/transaction/TransactionFormActions.tsx create mode 100644 src/components/transaction/TransactionTextField.tsx create mode 100644 src/components/transaction/TransactionTypeField.tsx create mode 100644 src/hooks/useTransactionForm.ts diff --git a/src/components/dashboard/TransactionForm.tsx b/src/components/dashboard/TransactionForm.tsx index 64a1267..eb77e9a 100644 --- a/src/components/dashboard/TransactionForm.tsx +++ b/src/components/dashboard/TransactionForm.tsx @@ -1,42 +1,13 @@ -import { useState, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import * as z from 'zod'; -import { useToast } from '@/components/ui/use-toast'; -import { supabase } from '@/integrations/supabase/client'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select'; -import { useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; import { Transaction } from '@/types/financialTypes'; -import { updateTransacao } from '@/services/transacaoService'; - -const transactionSchema = z.object({ - estabelecimento: z.string().min(1, { message: 'Estabelecimento é obrigatório' }), - valor: z.string().min(1, { message: 'Valor é obrigatório' }), - detalhes: z.string().min(1, { message: 'Detalhes são obrigatórios' }), - categoria: z.string().min(1, { message: 'Categoria é obrigatória' }), - tipo: z.string().min(1, { message: 'Tipo é obrigatório' }), - quando: z.string().min(1, { message: 'Data é obrigatória' }), - grupo_id: z.string().nullable().optional() -}); - -type TransactionFormValues = z.infer; +import { Form } from '@/components/ui/form'; +import { useTransactionForm } from '@/hooks/useTransactionForm'; +import { TransactionTextField } from '@/components/transaction/TransactionTextField'; +import { TransactionTypeField } from '@/components/transaction/TransactionTypeField'; +import { TransactionDateField } from '@/components/transaction/TransactionDateField'; +import { GroupSelectField } from '@/components/transaction/GroupSelectField'; +import { TransactionFormActions } from '@/components/transaction/TransactionFormActions'; interface TransactionFormProps { onSuccess: () => void; @@ -55,305 +26,69 @@ export function TransactionForm({ transaction = null, isEditing = false }: TransactionFormProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - const [grupos, setGrupos] = useState<{remote_jid: string, nome_grupo: string | null}[]>([]); - const { toast } = useToast(); - const navigate = useNavigate(); + const { + form, + grupos, + isSubmitting, + onSubmit, + fetchGrupos + } = useTransactionForm( + onSuccess, + onCancel, + defaultTipo, + transaction, + isEditing + ); // Fetch user groups when the form loads useEffect(() => { - const fetchGrupos = async () => { - const userEmail = localStorage.getItem('userEmail'); - if (!userEmail) { - console.error('Email do usuário não encontrado'); - return; - } - - const normalizedEmail = userEmail.trim().toLowerCase(); - - const { data, error } = await supabase - .from('grupos_whatsapp') - .select('remote_jid, nome_grupo') - .eq('login', normalizedEmail); - - if (error) { - console.error('Erro ao buscar grupos:', error); - } else if (data) { - setGrupos(data); - } - }; - fetchGrupos(); }, []); - // Set up form with default values or transaction data if editing - const form = useForm({ - resolver: zodResolver(transactionSchema), - defaultValues: { - estabelecimento: transaction?.estabelecimento || '', - valor: transaction ? String(Math.abs(transaction.valor)) : '', - detalhes: transaction?.detalhes || '', - categoria: transaction?.categoria || '', - tipo: transaction?.tipo as 'receita' | 'despesa' || defaultTipo, - quando: transaction?.quando ? - new Date(transaction.quando).toISOString().split('T')[0] : - new Date().toISOString().split('T')[0], - grupo_id: transaction?.grupo_id || grupoId || null - } - }); - - async function onSubmit(data: TransactionFormValues) { - setIsSubmitting(true); - - try { - // Get user email from localStorage - principal identifier now - const userEmail = localStorage.getItem('userEmail'); - - if (!userEmail) { - toast({ - title: "Erro no formulário", - description: "Email do usuário não encontrado", - variant: "destructive" - }); - return; - } - - // Normalize the email (lowercase and trim spaces) - const normalizedEmail = userEmail.trim().toLowerCase(); - - // Get user ID from localStorage - kept for compatibility - const userId = localStorage.getItem('userId') || ''; - - const valorNumerico = parseFloat(data.valor.replace(',', '.')); - - if (isNaN(valorNumerico)) { - toast({ - title: "Erro no formulário", - description: "O valor precisa ser um número válido", - variant: "destructive" - }); - return; - } - - // Adjust the value based on transaction type - const valorFinal = data.tipo.toLowerCase() === 'receita' - ? Math.abs(valorNumerico) - : Math.abs(valorNumerico); - - console.log(`Salvando transação para usuário: ${normalizedEmail} (ID: ${userId})`); - - // Handle editing vs creating new transaction - if (isEditing && transaction) { - // Update existing transaction - const updatedTransaction = { - ...transaction, - estabelecimento: data.estabelecimento, - valor: valorFinal, - detalhes: data.detalhes, - categoria: data.categoria, - tipo: data.tipo, - quando: data.quando, - grupo_id: data.grupo_id || null - }; - - await updateTransacao(updatedTransaction); - toast({ - title: "Transação atualizada", - description: "Transação atualizada com sucesso", - }); - onSuccess(); - } else { - // Create new transaction - // Prepare transaction data including login and grupo_id - const transactionData = { - user: userId, // Kept for compatibility - login: normalizedEmail, // Main field for identification - estabelecimento: data.estabelecimento, - valor: valorFinal, - detalhes: data.detalhes, - categoria: data.categoria, - tipo: data.tipo, - quando: data.quando, - grupo_id: data.grupo_id || null - }; - - console.log('Dados da transação a serem salvos:', transactionData); - - // With RLS disabled, we can insert directly to the fixed table - const { error } = await supabase - .from('transacoes') - .insert([transactionData]); - - if (error) { - console.error('Erro ao salvar transação:', error); - - toast({ - title: "Erro ao salvar", - description: `Não foi possível salvar a transação: ${error.message}`, - variant: "destructive" - }); - } else { - toast({ - title: "Transação salva", - description: "Transação registrada com sucesso", - }); - onSuccess(); - } - } - } catch (error) { - console.error('Erro ao processar formulário:', error); - toast({ - title: "Erro inesperado", - description: "Ocorreu um erro ao processar sua solicitação", - variant: "destructive" - }); - } finally { - setIsSubmitting(false); - } - } - return (
- ( - - Estabelecimento - - - - - - )} + label="Estabelecimento" + placeholder="Nome do estabelecimento" /> - ( - - Valor - - - - - - )} + label="Valor" + placeholder="0,00" + type="text" + inputMode="decimal" /> - ( - - Tipo - - - - )} - /> + - ( - - Categoria - - - - - - )} + label="Categoria" + placeholder="Categoria" /> - ( - - Detalhes - - - - - - )} + label="Detalhes" + placeholder="Detalhes da transação" /> - ( - - Data - - - - - - )} + + + + + - - {grupos.length > 0 && ( - ( - - Grupo WhatsApp (opcional) - - - - )} - /> - )} - -
- - -
); diff --git a/src/components/transaction/GroupSelectField.tsx b/src/components/transaction/GroupSelectField.tsx new file mode 100644 index 0000000..2cd0509 --- /dev/null +++ b/src/components/transaction/GroupSelectField.tsx @@ -0,0 +1,45 @@ + +import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { UseFormReturn } from "react-hook-form"; +import { TransactionFormValues } from "@/hooks/useTransactionForm"; + +interface GroupSelectFieldProps { + form: UseFormReturn; + grupos: {remote_jid: string, nome_grupo: string | null}[]; +} + +export function GroupSelectField({ form, grupos }: GroupSelectFieldProps) { + if (grupos.length === 0) return null; + + return ( + + Grupo WhatsApp (opcional) + + + + ); +} diff --git a/src/components/transaction/TransactionDateField.tsx b/src/components/transaction/TransactionDateField.tsx new file mode 100644 index 0000000..486f3c4 --- /dev/null +++ b/src/components/transaction/TransactionDateField.tsx @@ -0,0 +1,24 @@ + +import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { UseFormReturn } from "react-hook-form"; +import { TransactionFormValues } from "@/hooks/useTransactionForm"; + +interface TransactionDateFieldProps { + form: UseFormReturn; +} + +export function TransactionDateField({ form }: TransactionDateFieldProps) { + return ( + + Data + + + + + + ); +} diff --git a/src/components/transaction/TransactionFormActions.tsx b/src/components/transaction/TransactionFormActions.tsx new file mode 100644 index 0000000..a8e7923 --- /dev/null +++ b/src/components/transaction/TransactionFormActions.tsx @@ -0,0 +1,34 @@ + +import { Button } from "@/components/ui/button"; + +interface TransactionFormActionsProps { + onCancel: () => void; + isSubmitting: boolean; + defaultTipo?: 'receita' | 'despesa'; +} + +export function TransactionFormActions({ + onCancel, + isSubmitting, + defaultTipo = 'despesa' +}: TransactionFormActionsProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/transaction/TransactionTextField.tsx b/src/components/transaction/TransactionTextField.tsx new file mode 100644 index 0000000..e5c9b65 --- /dev/null +++ b/src/components/transaction/TransactionTextField.tsx @@ -0,0 +1,38 @@ + +import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { UseFormReturn } from "react-hook-form"; +import { TransactionFormValues } from "@/hooks/useTransactionForm"; + +interface TransactionTextFieldProps { + form: UseFormReturn; + name: keyof TransactionFormValues; + label: string; + placeholder: string; + type?: string; + inputMode?: "text" | "numeric" | "decimal" | "tel" | "search" | "email" | "url" | undefined; +} + +export function TransactionTextField({ + form, + name, + label, + placeholder, + type = "text", + inputMode +}: TransactionTextFieldProps) { + return ( + + {label} + + + + + + ); +} diff --git a/src/components/transaction/TransactionTypeField.tsx b/src/components/transaction/TransactionTypeField.tsx new file mode 100644 index 0000000..b0f28d6 --- /dev/null +++ b/src/components/transaction/TransactionTypeField.tsx @@ -0,0 +1,38 @@ + +import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { UseFormReturn } from "react-hook-form"; +import { TransactionFormValues } from "@/hooks/useTransactionForm"; + +interface TransactionTypeFieldProps { + form: UseFormReturn; +} + +export function TransactionTypeField({ form }: TransactionTypeFieldProps) { + return ( + + Tipo + + + + ); +} diff --git a/src/hooks/useTransactionForm.ts b/src/hooks/useTransactionForm.ts new file mode 100644 index 0000000..c8a1bbb --- /dev/null +++ b/src/hooks/useTransactionForm.ts @@ -0,0 +1,191 @@ + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { useToast } from '@/hooks/use-toast'; +import { supabase } from '@/integrations/supabase/client'; +import { Transaction } from '@/types/financialTypes'; +import { updateTransacao } from '@/services/transacaoService'; + +const transactionSchema = z.object({ + estabelecimento: z.string().min(1, { message: 'Estabelecimento é obrigatório' }), + valor: z.string().min(1, { message: 'Valor é obrigatório' }), + detalhes: z.string().min(1, { message: 'Detalhes são obrigatórios' }), + categoria: z.string().min(1, { message: 'Categoria é obrigatória' }), + tipo: z.string().min(1, { message: 'Tipo é obrigatório' }), + quando: z.string().min(1, { message: 'Data é obrigatória' }), + grupo_id: z.string().nullable().optional() +}); + +export type TransactionFormValues = z.infer; + +export const useTransactionForm = ( + onSuccess: () => void, + onCancel: () => void, + defaultTipo: 'receita' | 'despesa' = 'despesa', + transaction: Transaction | null = null, + isEditing: boolean = false +) => { + const [isSubmitting, setIsSubmitting] = useState(false); + const [grupos, setGrupos] = useState<{remote_jid: string, nome_grupo: string | null}[]>([]); + const { toast } = useToast(); + + // Fetch user groups when the form loads + const fetchGrupos = async () => { + const userEmail = localStorage.getItem('userEmail'); + if (!userEmail) { + console.error('Email do usuário não encontrado'); + return; + } + + const normalizedEmail = userEmail.trim().toLowerCase(); + + const { data, error } = await supabase + .from('grupos_whatsapp') + .select('remote_jid, nome_grupo') + .eq('login', normalizedEmail); + + if (error) { + console.error('Erro ao buscar grupos:', error); + } else if (data) { + setGrupos(data); + } + }; + + // Set up form with default values or transaction data if editing + const form = useForm({ + resolver: zodResolver(transactionSchema), + defaultValues: { + estabelecimento: transaction?.estabelecimento || '', + valor: transaction ? String(Math.abs(transaction.valor)) : '', + detalhes: transaction?.detalhes || '', + categoria: transaction?.categoria || '', + tipo: transaction?.tipo as 'receita' | 'despesa' || defaultTipo, + quando: transaction?.quando ? + new Date(transaction.quando).toISOString().split('T')[0] : + new Date().toISOString().split('T')[0], + grupo_id: transaction?.grupo_id || null + } + }); + + async function onSubmit(data: TransactionFormValues) { + setIsSubmitting(true); + + try { + // Get user email from localStorage - principal identifier now + const userEmail = localStorage.getItem('userEmail'); + + if (!userEmail) { + toast({ + title: "Erro no formulário", + description: "Email do usuário não encontrado", + variant: "destructive" + }); + return; + } + + // Normalize the email (lowercase and trim spaces) + const normalizedEmail = userEmail.trim().toLowerCase(); + + // Get user ID from localStorage - kept for compatibility + const userId = localStorage.getItem('userId') || ''; + + const valorNumerico = parseFloat(data.valor.replace(',', '.')); + + if (isNaN(valorNumerico)) { + toast({ + title: "Erro no formulário", + description: "O valor precisa ser um número válido", + variant: "destructive" + }); + return; + } + + // Adjust the value based on transaction type + const valorFinal = data.tipo.toLowerCase() === 'receita' + ? Math.abs(valorNumerico) + : Math.abs(valorNumerico); + + console.log(`Salvando transação para usuário: ${normalizedEmail} (ID: ${userId})`); + + // Handle editing vs creating new transaction + if (isEditing && transaction) { + // Update existing transaction + const updatedTransaction = { + ...transaction, + estabelecimento: data.estabelecimento, + valor: valorFinal, + detalhes: data.detalhes, + categoria: data.categoria, + tipo: data.tipo, + quando: data.quando, + grupo_id: data.grupo_id || null + }; + + await updateTransacao(updatedTransaction); + toast({ + title: "Transação atualizada", + description: "Transação atualizada com sucesso", + }); + onSuccess(); + } else { + // Create new transaction + // Prepare transaction data including login and grupo_id + const transactionData = { + user: userId, // Kept for compatibility + login: normalizedEmail, // Main field for identification + estabelecimento: data.estabelecimento, + valor: valorFinal, + detalhes: data.detalhes, + categoria: data.categoria, + tipo: data.tipo, + quando: data.quando, + grupo_id: data.grupo_id || null + }; + + console.log('Dados da transação a serem salvos:', transactionData); + + // With RLS disabled, we can insert directly to the fixed table + const { error } = await supabase + .from('transacoes') + .insert([transactionData]); + + if (error) { + console.error('Erro ao salvar transação:', error); + + toast({ + title: "Erro ao salvar", + description: `Não foi possível salvar a transação: ${error.message}`, + variant: "destructive" + }); + } else { + toast({ + title: "Transação salva", + description: "Transação registrada com sucesso", + }); + onSuccess(); + } + } + } catch (error) { + console.error('Erro ao processar formulário:', error); + toast({ + title: "Erro inesperado", + description: "Ocorreu um erro ao processar sua solicitação", + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + } + + return { + form, + grupos, + isSubmitting, + onSubmit, + fetchGrupos + }; +}; + +export { transactionSchema };