aboutsummaryrefslogtreecommitdiff
path: root/src/screens/dashboard-screen.tsx
diff options
context:
space:
mode:
authorLucas Faria Mendes <lucas.fariamo08@gmail.com>2026-08-14 18:06:35 +0000
committerLucas Faria Mendes <lucas.fariamo08@gmail.com>2026-08-14 18:06:35 +0000
commit2c2dfba45357bbd205a59145ae55a2779c911ae0 (patch)
tree0736b848aed56ec2da3c2489c49da0519447703d /src/screens/dashboard-screen.tsx
downloadyaba-2c2dfba45357bbd205a59145ae55a2779c911ae0.tar.gz
yaba-2c2dfba45357bbd205a59145ae55a2779c911ae0.zip
remake
Signed-off-by: Lucas Faria Mendes <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/screens/dashboard-screen.tsx')
-rw-r--r--src/screens/dashboard-screen.tsx440
1 files changed, 440 insertions, 0 deletions
diff --git a/src/screens/dashboard-screen.tsx b/src/screens/dashboard-screen.tsx
new file mode 100644
index 0000000..c4a533c
--- /dev/null
+++ b/src/screens/dashboard-screen.tsx
@@ -0,0 +1,440 @@
+import React, { useCallback, useState } from 'react';
+import {
+ ActivityIndicator,
+ RefreshControl,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import { Feather } from '@expo/vector-icons';
+import { LinearGradient } from 'expo-linear-gradient';
+import Animated, {
+ FadeInDown,
+ FadeInUp,
+ LinearTransition,
+} from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { Card, CurrencyDisplay, Divider, SectionLabel } from '@/components/ui/primitives';
+import { ExpensesChart } from '@/components/ui/expenses-chart';
+import { Colors, Spacing, Typography } from '@/constants/theme';
+import { getDashboard } from '@/services/api';
+import type { DashboardData } from '@/types';
+import { useAuth } from '@/contexts/AuthContext';
+import { useRouter, useFocusEffect } from 'expo-router';
+import { useIsFocused } from "expo-router/react-navigation";
+
+const MONTH_NAMES = [
+ '', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
+ 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro',
+];
+
+export default function DashboardScreen() {
+ const insets = useSafeAreaInsets();
+ const [refreshing, setRefreshing] = useState(false);
+ const [data, setData] = useState<DashboardData | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+ const { user, logout } = useAuth();
+ const router = useRouter();
+ const isFocused = useIsFocused();
+
+ const [targetMonth, setTargetMonth] = useState(() => new Date().getMonth() + 1);
+ const [targetYear, setTargetYear] = useState(() => new Date().getFullYear());
+
+ const fetchDashboard = useCallback(async () => {
+ if (!user) return;
+ try {
+ setError(null);
+ const res = await getDashboard(user.id, targetMonth, targetYear);
+ setData(res);
+ } catch (err) {
+ setError('Não foi possível carregar os dados.');
+ } finally {
+ setLoading(false);
+ }
+ }, [targetMonth, targetYear, user]);
+
+ useFocusEffect(
+ useCallback(() => {
+ fetchDashboard();
+ }, [fetchDashboard])
+ );
+
+ async function handleRefresh() {
+ setRefreshing(true);
+ await fetchDashboard();
+ setRefreshing(false);
+ }
+
+ if (loading) {
+ return (
+ <View style={[styles.container, styles.centered]}>
+ <ActivityIndicator size="large" color={Colors.primary} />
+ </View>
+ );
+ }
+
+ if (error || !data) {
+ return (
+ <View style={[styles.container, styles.centered]}>
+ <Text style={styles.errorText}>{error}</Text>
+ </View>
+ );
+ }
+
+ const isPositive = data.balance.available >= 0;
+ const monthName = MONTH_NAMES[data.period.month];
+
+ return (
+ <View style={[styles.container, { paddingTop: insets.top }]}>
+ <LinearGradient
+ colors={['#0D2818', Colors.background]}
+ style={styles.headerGradient}
+ start={{ x: 0, y: 0 }}
+ end={{ x: 0, y: 1 }}
+ />
+
+ {isFocused && (
+ <ScrollView
+ contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 80 }]}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+ <RefreshControl
+ refreshing={refreshing}
+ onRefresh={handleRefresh}
+ tintColor={Colors.primaryLight}
+ />
+ }
+ >
+ <Animated.View entering={FadeInUp.duration(500)} style={styles.headerTop}>
+ <View style={styles.greeting}>
+ <Text style={styles.greetingText}>Olá, {data.user.name} 👋</Text>
+
+ <View style={styles.monthSelector}>
+ <TouchableOpacity
+ onPress={() => {
+ let m = targetMonth - 1;
+ let y = targetYear;
+ if (m < 1) { m = 12; y -= 1; }
+ setTargetMonth(m);
+ setTargetYear(y);
+ setLoading(true);
+ }}
+ style={styles.monthButton}
+ >
+ <Feather name="chevron-left" size={20} color={Colors.textSecondary} />
+ </TouchableOpacity>
+
+ <Text style={styles.periodText}>
+ {monthName} {data.period.year}
+ </Text>
+
+ <TouchableOpacity
+ onPress={() => {
+ let m = targetMonth + 1;
+ let y = targetYear;
+ if (m > 12) { m = 1; y += 1; }
+ setTargetMonth(m);
+ setTargetYear(y);
+ setLoading(true);
+ }}
+ style={styles.monthButton}
+ >
+ <Feather name="chevron-right" size={20} color={Colors.textSecondary} />
+ </TouchableOpacity>
+ </View>
+ </View>
+
+ <TouchableOpacity
+ style={styles.logoutBtn}
+ onPress={async () => {
+ await logout();
+ router.replace('/login');
+ }}
+ >
+ <Feather name="log-out" size={20} color={Colors.textSecondary} />
+ </TouchableOpacity>
+ </Animated.View>
+
+ <Animated.View entering={FadeInUp.delay(100).duration(500)} layout={LinearTransition.springify()}>
+ <Card style={[styles.heroCard, isPositive ? styles.heroPositive : styles.heroNegative]}>
+ <SectionLabel
+ text="Saldo Disponível"
+ color={isPositive ? Colors.positive : Colors.negative}
+ />
+ <View style={styles.heroBalance}>
+ <CurrencyDisplay amount={data.balance.available} size="display" />
+ </View>
+
+ <View style={styles.ratioSection}>
+ <View style={styles.ratioBar}>
+ <View
+ style={[
+ styles.ratioFill,
+ {
+ width: `${Math.min(data.balance.expense_ratio, 100)}%` as `${number}%`,
+ backgroundColor:
+ data.balance.expense_ratio > 80 ? Colors.negative : Colors.primary,
+ },
+ ]}
+ />
+ </View>
+ <Text style={styles.ratioText}>
+ {data.balance.expense_ratio.toFixed(1)}% do salário comprometido
+ </Text>
+ </View>
+ </Card>
+ </Animated.View>
+
+ <Animated.View entering={FadeInDown.delay(200).duration(500)} layout={LinearTransition.springify()}>
+ <Card style={styles.card}>
+ <SectionLabel text="Salário" />
+ <View style={styles.salaryRow}>
+ <View style={styles.salaryItem}>
+ <Text style={styles.salaryLabel}>Bruto</Text>
+ <Text style={styles.salaryValue}>
+ {data.salary.gross.toLocaleString('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ minimumFractionDigits: 0,
+ })}
+ </Text>
+ </View>
+ <View style={styles.salaryArrow}>
+ <Text style={styles.salaryArrowText}>→</Text>
+ </View>
+ <View style={styles.salaryItem}>
+ <Text style={styles.salaryLabel}>Líquido</Text>
+ <Text style={[styles.salaryValue, { color: Colors.positive }]}>
+ {data.salary.effective_net.toLocaleString('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ minimumFractionDigits: 0,
+ })}
+ </Text>
+ </View>
+ </View>
+ <Divider />
+ <View style={styles.taxRow}>
+ <Text style={styles.taxLabel}>INSS</Text>
+ <Text style={styles.taxValue}>
+ -{data.salary.inss_discount.toLocaleString('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ })}
+ </Text>
+ </View>
+ <View style={styles.taxRow}>
+ <Text style={styles.taxLabel}>IRRF</Text>
+ <Text style={styles.taxValue}>
+ -{data.salary.irrf_discount.toLocaleString('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ })}
+ </Text>
+ </View>
+ </Card>
+ </Animated.View>
+
+ <Animated.View entering={FadeInDown.delay(250).duration(500)} layout={LinearTransition.springify()}>
+ <ExpensesChart
+ recurring={data.expenses.recurring_list}
+ oneTime={data.expenses.one_time_list}
+ />
+ </Animated.View>
+
+ <Animated.View entering={FadeInDown.delay(300).duration(500)} layout={LinearTransition.springify()}>
+ <Card style={styles.card}>
+ <View style={[styles.sectionHeader, { marginBottom: 0 }]}>
+ <SectionLabel text="Lançamentos Fixos" />
+ <Text
+ style={[
+ styles.totalBadge,
+ data.expenses.recurring_total < 0 ? { color: Colors.positive } : { color: Colors.negative }
+ ]}
+ >
+ {data.expenses.recurring_total < 0 ? '+ ' : (data.expenses.recurring_total > 0 ? '- ' : '')}
+ {Math.abs(data.expenses.recurring_total).toLocaleString('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ minimumFractionDigits: 0,
+ })}
+ </Text>
+ </View>
+ </Card>
+ </Animated.View>
+
+ <Animated.View entering={FadeInDown.delay(400).duration(500)} layout={LinearTransition.springify()}>
+ <Card style={styles.card}>
+ <View style={[styles.sectionHeader, { marginBottom: 0 }]}>
+ <SectionLabel text="Lançamentos do Mês" />
+ <Text
+ style={[
+ styles.totalBadge,
+ data.expenses.one_time_total < 0 ? { color: Colors.positive } : { color: Colors.negative }
+ ]}
+ >
+ {data.expenses.one_time_total < 0 ? '+ ' : (data.expenses.one_time_total > 0 ? '- ' : '')}
+ {Math.abs(data.expenses.one_time_total).toLocaleString('pt-BR', {
+ style: 'currency',
+ currency: 'BRL',
+ minimumFractionDigits: 0,
+ })}
+ </Text>
+ </View>
+ </Card>
+ </Animated.View>
+ </ScrollView>
+ )}
+ </View>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ centered: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorText: {
+ ...Typography.body,
+ color: Colors.negative,
+ },
+ headerGradient: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 200,
+ },
+ scroll: {
+ padding: Spacing.md,
+ gap: Spacing.md,
+ },
+ headerTop: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ paddingVertical: Spacing.sm,
+ },
+ greeting: {
+ flex: 1,
+ },
+ logoutBtn: {
+ padding: 8,
+ backgroundColor: 'rgba(255,255,255,0.05)',
+ borderRadius: 12,
+ },
+ greetingText: {
+ ...Typography.h2,
+ color: Colors.textPrimary,
+ },
+ periodText: {
+ ...Typography.caption,
+ color: Colors.textSecondary,
+ textTransform: 'uppercase',
+ letterSpacing: 1.5,
+ },
+ monthSelector: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 6,
+ gap: Spacing.xs,
+ },
+ monthButton: {
+ padding: 4,
+ },
+ card: {
+ gap: Spacing.sm,
+ },
+ heroCard: {
+ gap: Spacing.md,
+ borderWidth: 1,
+ },
+ heroPositive: {
+ borderColor: `${Colors.positive}40`,
+ },
+ heroNegative: {
+ borderColor: `${Colors.negative}40`,
+ },
+ heroBalance: {
+ marginTop: Spacing.xs,
+ },
+ ratioSection: {
+ gap: Spacing.xs,
+ },
+ ratioBar: {
+ height: 4,
+ backgroundColor: Colors.border,
+ borderRadius: 2,
+ overflow: 'hidden',
+ },
+ ratioFill: {
+ height: '100%',
+ borderRadius: 2,
+ },
+ ratioText: {
+ ...Typography.caption,
+ color: Colors.textMuted,
+ },
+ salaryRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginTop: Spacing.sm,
+ },
+ salaryItem: {
+ flex: 1,
+ gap: 4,
+ },
+ salaryLabel: {
+ ...Typography.caption,
+ color: Colors.textMuted,
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ },
+ salaryValue: {
+ ...Typography.h2,
+ color: Colors.textPrimary,
+ },
+ salaryArrow: {
+ paddingHorizontal: Spacing.md,
+ },
+ salaryArrowText: {
+ ...Typography.h2,
+ color: Colors.textMuted,
+ },
+ taxRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ taxLabel: {
+ ...Typography.body,
+ color: Colors.textSecondary,
+ },
+ taxValue: {
+ ...Typography.body,
+ color: Colors.negative,
+ fontFamily: 'DMSans_600SemiBold',
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: Spacing.xs,
+ },
+ totalBadge: {
+ ...Typography.body,
+ color: Colors.negative,
+ fontFamily: 'DMSans_600SemiBold',
+ },
+ rowDivider: {
+ marginVertical: 0,
+ },
+});