From 2c2dfba45357bbd205a59145ae55a2779c911ae0 Mon Sep 17 00:00:00 2001 From: Lucas Faria Mendes Date: Fri, 14 Aug 2026 15:06:35 -0300 Subject: remake Signed-off-by: Lucas Faria Mendes --- src/screens/recurring-expenses-screen.tsx | 442 ++++++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 src/screens/recurring-expenses-screen.tsx (limited to 'src/screens/recurring-expenses-screen.tsx') diff --git a/src/screens/recurring-expenses-screen.tsx b/src/screens/recurring-expenses-screen.tsx new file mode 100644 index 0000000..ea6f165 --- /dev/null +++ b/src/screens/recurring-expenses-screen.tsx @@ -0,0 +1,442 @@ +import React, { useCallback, useState } from 'react'; +import { + Alert, + Platform, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import Animated, { FadeInDown, LinearTransition } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import * as Haptics from 'expo-haptics'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, Divider, SectionLabel } from '@/components/ui/primitives'; +import { RecurringExpenseRow } from '@/components/expense-row'; +import { Colors, Spacing, Typography } from '@/constants/theme'; +import type { RecurringExpense } from '@/types'; +import { + createRecurringExpense, + deleteRecurringExpense, + getRecurringExpenses, + toggleRecurringExpense, + updateRecurringExpense, +} from '@/services/api'; +import { useAuth } from '@/contexts/AuthContext'; +import { ActionModal } from '@/components/ui/action-modal'; +import { OptionsModal } from '@/components/ui/options-modal'; +import { useFocusEffect } from 'expo-router'; +import { useIsFocused } from "expo-router/react-navigation"; + +export default function RecurringExpensesScreen() { + const insets = useSafeAreaInsets(); + const { user } = useAuth(); + const [expenses, setExpenses] = useState([]); + const [showForm, setShowForm] = useState(false); + const [loading, setLoading] = useState(true); + + const [name, setName] = useState(''); + const [amount, setAmount] = useState(''); + const [expenseType, setExpenseType] = useState<'expense' | 'income'>('expense'); + const [dueDay, setDueDay] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [expenseToDelete, setExpenseToDelete] = useState(null); + const [selectedExpense, setSelectedExpense] = useState(null); + const [editingExpenseId, setEditingExpenseId] = useState(null); + + const isFocused = useIsFocused(); + + const fetchExpenses = useCallback(async () => { + if (!user) return; + try { + setLoading(true); + const data = await getRecurringExpenses(user.id); + setExpenses(data); + } catch (err) { + Alert.alert('Erro', 'Não foi possível carregar os gastos.'); + } finally { + setLoading(false); + } + }, [user]); + + useFocusEffect( + useCallback(() => { + fetchExpenses(); + }, [fetchExpenses]) + ); + + const activeTotal = expenses + .filter((e) => e.is_active) + .reduce((sum, e) => { + return sum + (e.type === 'income' ? -e.amount : e.amount); + }, 0); + + const isTotalIncome = activeTotal < 0; + const absTotal = Math.abs(activeTotal); + const formattedTotal = absTotal.toLocaleString('pt-BR', { + style: 'currency', + currency: 'BRL', + }); + + async function handleToggle(id: number) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + + setExpenses((prev) => + prev.map((e) => (e.id === id ? { ...e, is_active: !e.is_active } : e)), + ); + try { + if (!user) return; + await toggleRecurringExpense(user.id, id); + } catch (err) { + setExpenses((prev) => + prev.map((e) => (e.id === id ? { ...e, is_active: !e.is_active } : e)), + ); + Alert.alert('Erro', 'Não foi possível alterar o status do gasto.'); + } + } + + async function handleAdd() { + const amountNum = parseFloat(amount.replace(',', '.')); + if (!name.trim() || isNaN(amountNum) || amountNum <= 0) { + Alert.alert('Dados inválidos', 'Preencha o nome e um valor válido.'); + return; + } + + try { + if (!user) return; + setIsSubmitting(true); + + if (editingExpenseId) { + const updated = await updateRecurringExpense(user.id, editingExpenseId, { + name: name.trim(), + amount: amountNum, + category: 'outros', + type: expenseType, + due_day: dueDay ? parseInt(dueDay, 10) : null, + }); + setExpenses((prev) => prev.map((e) => e.id === editingExpenseId ? updated : e)); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else { + const newExpense = await createRecurringExpense(user.id, { + name: name.trim(), + amount: amountNum, + category: 'outros', + type: expenseType, + due_day: dueDay ? parseInt(dueDay, 10) : null, + is_active: true, + starts_at: new Date().toISOString().split('T')[0], + ends_at: null, + }); + setExpenses((prev) => [...prev, newExpense]); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } + + setName(''); + setAmount(''); + setExpenseType('expense'); + setDueDay(''); + setShowForm(false); + setEditingExpenseId(null); + } catch (err) { + Alert.alert('Erro', 'Não foi possível salvar o lançamento.'); + } finally { + setIsSubmitting(false); + } + } + + function handleDelete(id: number) { + setExpenseToDelete(id); + setSelectedExpense(null); + } + + function handleEdit(expense: RecurringExpense) { + setName(expense.name); + setAmount(expense.amount.toString()); + setExpenseType(expense.type); + setDueDay(expense.due_day ? expense.due_day.toString() : ''); + setEditingExpenseId(expense.id); + setSelectedExpense(null); + setShowForm(true); + } + + async function confirmDelete() { + if (!user || expenseToDelete === null) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + try { + await deleteRecurringExpense(user.id, expenseToDelete); + setExpenses((prev) => prev.filter((e) => e.id !== expenseToDelete)); + } catch (err) { + Alert.alert('Erro', 'Não foi possível remover o gasto.'); + } finally { + setExpenseToDelete(null); + } + } + + return ( + + {isFocused && ( + + + + Lançamentos Fixos + + Saldo líquido:{' '} + + {isTotalIncome ? '+ ' : (activeTotal > 0 ? '- ' : '')}{formattedTotal} + + + + { + if (showForm) { + setEditingExpenseId(null); + setName(''); + setAmount(''); + setDueDay(''); + } + setShowForm((v) => !v); + }} + accessibilityLabel="Adicionar lançamento fixo" + accessibilityRole="button" + > + {showForm ? '✕' : '+'} + + + + {showForm && ( + + + + + + + + setExpenseType('expense')} + > + + Despesa + + + setExpenseType('income')} + > + + Receita + + + + + +