From add7ea82d3e335187f2eaa1f4f3dbeb4a4dac35c Mon Sep 17 00:00:00 2001 From: Dita Aji Pratama Date: Sat, 18 Apr 2026 12:22:56 +0700 Subject: [PATCH] feat: create Explore/StatsScreen - Add monthly summary with useMemo - Add category expenses analysis - Display total balance overview --- app/(tabs)/explore.tsx | 262 ++++++++++++++++++++++++----------------- 1 file changed, 157 insertions(+), 105 deletions(-) diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 71518f9..1e1473f 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -1,112 +1,164 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; +import React, { useMemo } from 'react'; +import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from 'react-native'; +import { COLORS, FONTS } from '@/constants/theme'; +import { Header } from '@/components/Header'; +import { useTransactions } from '@/hooks/useTransactions'; +import { calculateBalance, formatRupiah } from '@/utils/helpers'; -import { Collapsible } from '@/components/ui/collapsible'; -import { ExternalLink } from '@/components/external-link'; -import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { IconSymbol } from '@/components/ui/icon-symbol'; -import { Fonts } from '@/constants/theme'; +export default function ExploreScreen() { + const { transactions, loading } = useTransactions(); + + const monthlyData = useMemo(() => { + const grouped: Record = {}; + + transactions.forEach((t) => { + const monthKey = t.date.substring(0, 7); + if (!grouped[monthKey]) { + grouped[monthKey] = { income: 0, expense: 0 }; + } + if (t.type === 'income') { + grouped[monthKey].income += t.amount; + } else { + grouped[monthKey].expense += t.amount; + } + }); + + return Object.entries(grouped) + .map(([month, data]) => ({ + month, + income: data.income, + expense: data.expense, + balance: data.income - data.expense, + })) + .sort((a, b) => b.month.localeCompare(a.month)) + .slice(0, 6); + }, [transactions]); + + const categoryData = useMemo(() => { + const grouped: Record = {}; + + transactions + .filter((t) => t.type === 'expense') + .forEach((t) => { + if (!grouped[t.category]) grouped[t.category] = 0; + grouped[t.category] += t.amount; + }); + + return Object.entries(grouped) + .map(([category, amount]) => ({ category, amount })) + .sort((a, b) => b.amount - a.amount) + .slice(0, 5); + }, [transactions]); + + const balance = calculateBalance(transactions); + + if (loading) { + return ( + + + + ); + } + + const formatMonth = (monthStr: string) => { + const [year, month] = monthStr.split('-'); + const date = new Date(parseInt(year), parseInt(month) - 1); + return new Intl.DateTimeFormat('id-ID', { month: 'long', year: 'numeric' }).format(date); + }; -export default function TabTwoScreen() { return ( - - }> - - - Explore - - - This app includes example code to help you get started. - - - This app has two screens:{' '} - app/(tabs)/index.tsx and{' '} - app/(tabs)/explore.tsx - - - The layout file in app/(tabs)/_layout.tsx{' '} - sets up the tab navigator. - - - Learn more - - - - - You can open this project on Android, iOS, and the web. To open the web version, press{' '} - w in the terminal running this project. - - - - - For static images, you can use the @2x and{' '} - @3x suffixes to provide files for - different screen densities - - - - Learn more - - - - - This template has light and dark mode support. The{' '} - useColorScheme() hook lets you inspect - what the user's current color scheme is, and so you can adjust UI colors accordingly. - - - Learn more - - - - - This template includes an example of an animated component. The{' '} - components/HelloWave.tsx component uses - the powerful{' '} - - react-native-reanimated - {' '} - library to create a waving hand animation. - - {Platform.select({ - ios: ( - - The components/ParallaxScrollView.tsx{' '} - component provides a parallax effect for the header image. - - ), - })} - - + +
+ + + Ringkasan Keseluruhan + + + Total Pemasukan + + {formatRupiah(balance.income)} + + + + Total Pengeluaran + + {formatRupiah(balance.expense)} + + + + + + Saldo Akhir + + {formatRupiah(balance.total)} + + + + + + Ringkasan Bulanan + {monthlyData.length === 0 ? ( + Belum ada data + ) : ( + monthlyData.map((item) => ( + + {formatMonth(item.month)} + + + +{formatRupiah(item.income)} + + + -{formatRupiah(item.expense)} + + + + )) + )} + + + + Kategori Pengeluaran + {categoryData.length === 0 ? ( + Belum ada data + ) : ( + categoryData.map((item) => ( + + {item.category} + + {formatRupiah(item.amount)} + + + )) + )} + + + ); } const styles = StyleSheet.create({ - headerImage: { - color: '#808080', - bottom: -90, - left: -35, - position: 'absolute', - }, - titleContainer: { - flexDirection: 'row', - gap: 8, - }, -}); + container: { flex: 1, backgroundColor: COLORS.background }, + content: { flex: 1, padding: 16 }, + loading: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: COLORS.background }, + card: { backgroundColor: COLORS.card, borderRadius: 12, padding: 16, marginBottom: 16, elevation: 3 }, + sectionTitle: { fontSize: FONTS.regular, fontWeight: 'bold', color: COLORS.text, marginBottom: 16 }, + summaryRow: { flexDirection: 'row', justifyContent: 'space-between' }, + summaryItem: { flex: 1, alignItems: 'center' }, + summaryLabel: { fontSize: FONTS.small, color: COLORS.textSecondary, marginBottom: 4 }, + summaryAmount: { fontSize: FONTS.regular, fontWeight: '600' }, + income: { color: COLORS.income }, + expense: { color: COLORS.expense }, + divider: { height: 1, backgroundColor: COLORS.border, marginVertical: 16 }, + totalRow: { alignItems: 'center' }, + totalLabel: { fontSize: FONTS.small, color: COLORS.textSecondary, marginBottom: 4 }, + totalAmount: { fontSize: FONTS.large, fontWeight: 'bold', color: COLORS.text }, + monthItem: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: COLORS.border }, + monthLabel: { fontSize: FONTS.small, color: COLORS.text }, + monthAmounts: { flexDirection: 'row', gap: 12 }, + incomeSmall: { fontSize: FONTS.small, color: COLORS.income }, + expenseSmall: { fontSize: FONTS.small, color: COLORS.expense }, + categoryItem: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: COLORS.border }, + categoryLabel: { fontSize: FONTS.small, color: COLORS.text }, + categoryAmount: { fontSize: FONTS.small, fontWeight: '600', color: COLORS.expense }, + emptyText: { fontSize: FONTS.small, color: COLORS.textSecondary, textAlign: 'center', paddingVertical: 16 }, +}); \ No newline at end of file