feat: create Explore/StatsScreen
- Add monthly summary with useMemo - Add category expenses analysis - Display total balance overview
This commit is contained in:
parent
f0b2cf9f7e
commit
add7ea82d3
@ -1,112 +1,164 @@
|
|||||||
import { Image } from 'expo-image';
|
import React, { useMemo } from 'react';
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
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';
|
export default function ExploreScreen() {
|
||||||
import { ExternalLink } from '@/components/external-link';
|
const { transactions, loading } = useTransactions();
|
||||||
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 TabTwoScreen() {
|
const monthlyData = useMemo(() => {
|
||||||
|
const grouped: Record<string, { income: number; expense: number }> = {};
|
||||||
|
|
||||||
|
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<string, number> = {};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<ParallaxScrollView
|
<View style={styles.loading}>
|
||||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||||
headerImage={
|
</View>
|
||||||
<IconSymbol
|
);
|
||||||
size={310}
|
}
|
||||||
color="#808080"
|
|
||||||
name="chevron.left.forwardslash.chevron.right"
|
const formatMonth = (monthStr: string) => {
|
||||||
style={styles.headerImage}
|
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);
|
||||||
<ThemedView style={styles.titleContainer}>
|
};
|
||||||
<ThemedText
|
|
||||||
type="title"
|
return (
|
||||||
style={{
|
<View style={styles.container}>
|
||||||
fontFamily: Fonts.rounded,
|
<Header title="Statistik Keuangan" />
|
||||||
}}>
|
<ScrollView style={styles.content}>
|
||||||
Explore
|
<View style={styles.card}>
|
||||||
</ThemedText>
|
<Text style={styles.sectionTitle}>Ringkasan Keseluruhan</Text>
|
||||||
</ThemedView>
|
<View style={styles.summaryRow}>
|
||||||
<ThemedText>This app includes example code to help you get started.</ThemedText>
|
<View style={styles.summaryItem}>
|
||||||
<Collapsible title="File-based routing">
|
<Text style={styles.summaryLabel}>Total Pemasukan</Text>
|
||||||
<ThemedText>
|
<Text style={[styles.summaryAmount, styles.income]}>
|
||||||
This app has two screens:{' '}
|
{formatRupiah(balance.income)}
|
||||||
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
|
</Text>
|
||||||
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
|
</View>
|
||||||
</ThemedText>
|
<View style={styles.summaryItem}>
|
||||||
<ThemedText>
|
<Text style={styles.summaryLabel}>Total Pengeluaran</Text>
|
||||||
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
|
<Text style={[styles.summaryAmount, styles.expense]}>
|
||||||
sets up the tab navigator.
|
{formatRupiah(balance.expense)}
|
||||||
</ThemedText>
|
</Text>
|
||||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
</View>
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
</View>
|
||||||
</ExternalLink>
|
<View style={styles.divider} />
|
||||||
</Collapsible>
|
<View style={styles.totalRow}>
|
||||||
<Collapsible title="Android, iOS, and web support">
|
<Text style={styles.totalLabel}>Saldo Akhir</Text>
|
||||||
<ThemedText>
|
<Text style={styles.totalAmount}>
|
||||||
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
|
{formatRupiah(balance.total)}
|
||||||
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
|
</Text>
|
||||||
</ThemedText>
|
</View>
|
||||||
</Collapsible>
|
</View>
|
||||||
<Collapsible title="Images">
|
|
||||||
<ThemedText>
|
<View style={styles.card}>
|
||||||
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
|
<Text style={styles.sectionTitle}>Ringkasan Bulanan</Text>
|
||||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
{monthlyData.length === 0 ? (
|
||||||
different screen densities
|
<Text style={styles.emptyText}>Belum ada data</Text>
|
||||||
</ThemedText>
|
) : (
|
||||||
<Image
|
monthlyData.map((item) => (
|
||||||
source={require('@/assets/images/react-logo.png')}
|
<View key={item.month} style={styles.monthItem}>
|
||||||
style={{ width: 100, height: 100, alignSelf: 'center' }}
|
<Text style={styles.monthLabel}>{formatMonth(item.month)}</Text>
|
||||||
/>
|
<View style={styles.monthAmounts}>
|
||||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
<Text style={styles.incomeSmall}>
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
+{formatRupiah(item.income)}
|
||||||
</ExternalLink>
|
</Text>
|
||||||
</Collapsible>
|
<Text style={styles.expenseSmall}>
|
||||||
<Collapsible title="Light and dark mode components">
|
-{formatRupiah(item.expense)}
|
||||||
<ThemedText>
|
</Text>
|
||||||
This template has light and dark mode support. The{' '}
|
</View>
|
||||||
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
|
</View>
|
||||||
what the user's current color scheme is, and so you can adjust UI colors accordingly.
|
))
|
||||||
</ThemedText>
|
)}
|
||||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
</View>
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
<View style={styles.card}>
|
||||||
</Collapsible>
|
<Text style={styles.sectionTitle}>Kategori Pengeluaran</Text>
|
||||||
<Collapsible title="Animations">
|
{categoryData.length === 0 ? (
|
||||||
<ThemedText>
|
<Text style={styles.emptyText}>Belum ada data</Text>
|
||||||
This template includes an example of an animated component. The{' '}
|
) : (
|
||||||
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
|
categoryData.map((item) => (
|
||||||
the powerful{' '}
|
<View key={item.category} style={styles.categoryItem}>
|
||||||
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
|
<Text style={styles.categoryLabel}>{item.category}</Text>
|
||||||
react-native-reanimated
|
<Text style={styles.categoryAmount}>
|
||||||
</ThemedText>{' '}
|
{formatRupiah(item.amount)}
|
||||||
library to create a waving hand animation.
|
</Text>
|
||||||
</ThemedText>
|
</View>
|
||||||
{Platform.select({
|
))
|
||||||
ios: (
|
)}
|
||||||
<ThemedText>
|
</View>
|
||||||
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
|
</ScrollView>
|
||||||
component provides a parallax effect for the header image.
|
</View>
|
||||||
</ThemedText>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</Collapsible>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
headerImage: {
|
container: { flex: 1, backgroundColor: COLORS.background },
|
||||||
color: '#808080',
|
content: { flex: 1, padding: 16 },
|
||||||
bottom: -90,
|
loading: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: COLORS.background },
|
||||||
left: -35,
|
card: { backgroundColor: COLORS.card, borderRadius: 12, padding: 16, marginBottom: 16, elevation: 3 },
|
||||||
position: 'absolute',
|
sectionTitle: { fontSize: FONTS.regular, fontWeight: 'bold', color: COLORS.text, marginBottom: 16 },
|
||||||
},
|
summaryRow: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||||
titleContainer: {
|
summaryItem: { flex: 1, alignItems: 'center' },
|
||||||
flexDirection: 'row',
|
summaryLabel: { fontSize: FONTS.small, color: COLORS.textSecondary, marginBottom: 4 },
|
||||||
gap: 8,
|
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 },
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue
Block a user