feat: Introduce financial goal and asset management with SQLite and NativeWind integration, new tab screens, and updated dependencies.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||
import { Link, Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Pressable } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||
function TabBarIcon(props: {
|
||||
@@ -29,14 +29,14 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Tab One',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||
title: 'Dashboard',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="dashboard" color={color} />,
|
||||
headerRight: () => (
|
||||
<Link href="/modal" asChild>
|
||||
<Pressable>
|
||||
{({ pressed }) => (
|
||||
<FontAwesome
|
||||
name="info-circle"
|
||||
name="plus-circle"
|
||||
size={25}
|
||||
color={Colors[colorScheme ?? 'light'].text}
|
||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||
@@ -48,10 +48,45 @@ export default function TabLayout() {
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="two"
|
||||
name="investments"
|
||||
options={{
|
||||
title: 'Tab Two',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||
title: 'Investments',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="line-chart" color={color} />,
|
||||
headerRight: () => (
|
||||
<Link href="/add-asset" asChild>
|
||||
<Pressable>
|
||||
{({ pressed }) => (
|
||||
<FontAwesome
|
||||
name="plus-circle"
|
||||
size={25}
|
||||
color={Colors[colorScheme ?? 'light'].text}
|
||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="goals"
|
||||
options={{
|
||||
title: 'Goals',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="flag" color={color} />,
|
||||
headerRight: () => (
|
||||
<Link href="/add-goal" asChild>
|
||||
<Pressable>
|
||||
{({ pressed }) => (
|
||||
<FontAwesome
|
||||
name="plus-circle"
|
||||
size={25}
|
||||
color={Colors[colorScheme ?? 'light'].text}
|
||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
81
app/(tabs)/goals.tsx
Normal file
81
app/(tabs)/goals.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ScrollView, Text, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { getDB } from '../../db';
|
||||
import { Goal } from '../../types';
|
||||
|
||||
export default function GoalsScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [goals, setGoals] = useState<Goal[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const result = await db.getAllAsync<Goal>('SELECT * FROM goals ORDER BY deadline ASC');
|
||||
setGoals(result);
|
||||
} catch (error) {
|
||||
console.error('Error fetching goals', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchData();
|
||||
}, [fetchData])
|
||||
);
|
||||
|
||||
const calculateProgress = (current: number, target: number) => {
|
||||
if (target === 0) return 0;
|
||||
return Math.min((current / target) * 100, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100 dark:bg-gray-900" style={{ paddingTop: insets.top }}>
|
||||
<ScrollView className="flex-1 px-6">
|
||||
<View className="py-6">
|
||||
<Text className="text-3xl font-bold text-gray-900 dark:text-white">Financial Goals</Text>
|
||||
<Text className="text-gray-500 mt-1">Stay disciplined and reach your targets.</Text>
|
||||
</View>
|
||||
|
||||
{goals.length === 0 ? (
|
||||
<View className="items-center py-10">
|
||||
<Text className="text-gray-400">No goals set yet.</Text>
|
||||
</View>
|
||||
) : (
|
||||
goals.map((item) => {
|
||||
const progress = calculateProgress(item.current_amount, item.target_amount);
|
||||
return (
|
||||
<View key={item.id} className="bg-white dark:bg-gray-800 p-6 mb-4 rounded-2xl shadow-sm">
|
||||
<View className="flex-row justify-between items-start mb-4">
|
||||
<View>
|
||||
<Text className="text-xl font-bold text-gray-900 dark:text-white">{item.name}</Text>
|
||||
{item.deadline && (
|
||||
<Text className="text-gray-500 text-xs">Deadline: {item.deadline}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="bg-blue-100 px-3 py-1 rounded-full">
|
||||
<Text className="text-blue-600 font-bold text-xs">{progress.toFixed(0)}%</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex-row justify-between mb-2">
|
||||
<Text className="text-gray-500 text-sm">€{item.current_amount.toFixed(2)} saved</Text>
|
||||
<Text className="text-gray-900 dark:text-white font-bold text-sm">Target: €{item.target_amount.toFixed(2)}</Text>
|
||||
</View>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<View className="h-3 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<View
|
||||
className="h-full bg-blue-600 rounded-full"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,129 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { FontAwesome } from '@expo/vector-icons';
|
||||
import { Link, useFocusEffect } from 'expo-router';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { BarChart } from 'react-native-gifted-charts';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import TransactionList from '../../components/TransactionList';
|
||||
import { getDB } from '../../db';
|
||||
import { Transaction } from '../../types';
|
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
export default function DashboardScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [balance, setBalance] = useState(0);
|
||||
const [income, setIncome] = useState(0);
|
||||
const [expense, setExpense] = useState(0);
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const result = await db.getAllAsync<Transaction>('SELECT * FROM transactions ORDER BY date DESC');
|
||||
setTransactions(result);
|
||||
|
||||
let totalIncome = 0;
|
||||
let totalExpense = 0;
|
||||
const expensesByCategory: Record<string, number> = {};
|
||||
|
||||
result.forEach(t => {
|
||||
if (t.type === 'income') {
|
||||
totalIncome += t.amount;
|
||||
} else {
|
||||
totalExpense += t.amount;
|
||||
expensesByCategory[t.category] = (expensesByCategory[t.category] || 0) + t.amount;
|
||||
}
|
||||
});
|
||||
|
||||
setIncome(totalIncome);
|
||||
setExpense(totalExpense);
|
||||
setBalance(totalIncome - totalExpense);
|
||||
|
||||
// Prepare Chart Data
|
||||
const data = Object.keys(expensesByCategory).map(cat => ({
|
||||
value: expensesByCategory[cat],
|
||||
label: cat.substring(0, 3), // Short label
|
||||
frontColor: '#EF4444',
|
||||
}));
|
||||
setChartData(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching data', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchData();
|
||||
}, [fetchData])
|
||||
);
|
||||
|
||||
export default function TabOneScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Tab One</Text>
|
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
||||
<EditScreenInfo path="app/(tabs)/index.tsx" />
|
||||
<View className="flex-1 bg-gray-100 dark:bg-gray-900" style={{ paddingTop: insets.top }}>
|
||||
<ScrollView className="flex-1 px-6">
|
||||
{/* Header */}
|
||||
<View className="flex-row justify-between items-center py-6">
|
||||
<View>
|
||||
<Text className="text-gray-500 text-sm">Total Balance</Text>
|
||||
<Text className="text-4xl font-bold text-gray-900 dark:text-white">€{balance.toFixed(2)}</Text>
|
||||
</View>
|
||||
<Link href="/modal" asChild>
|
||||
<TouchableOpacity className="bg-blue-600 w-12 h-12 rounded-full items-center justify-center shadow-lg">
|
||||
<FontAwesome name="plus" size={20} color="white" />
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<View className="flex-row space-x-4 mb-8">
|
||||
<View className="flex-1 bg-green-500 p-4 rounded-2xl shadow-sm">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<View className="bg-white/20 w-8 h-8 rounded-full items-center justify-center mr-2">
|
||||
<FontAwesome name="arrow-up" size={12} color="white" />
|
||||
</View>
|
||||
<Text className="text-white/80 text-sm font-medium">Income</Text>
|
||||
</View>
|
||||
<Text className="text-white text-xl font-bold">€{income.toFixed(2)}</Text>
|
||||
</View>
|
||||
<View className="flex-1 bg-red-500 p-4 rounded-2xl shadow-sm">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<View className="bg-white/20 w-8 h-8 rounded-full items-center justify-center mr-2">
|
||||
<FontAwesome name="arrow-down" size={12} color="white" />
|
||||
</View>
|
||||
<Text className="text-white/80 text-sm font-medium">Expenses</Text>
|
||||
</View>
|
||||
<Text className="text-white text-xl font-bold">€{expense.toFixed(2)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Spending Chart */}
|
||||
{chartData.length > 0 && (
|
||||
<View className="bg-white dark:bg-gray-800 p-4 rounded-2xl shadow-sm mb-6">
|
||||
<Text className="text-lg font-bold text-gray-900 dark:text-white mb-4">Expenses by Category</Text>
|
||||
<BarChart
|
||||
data={chartData}
|
||||
barWidth={30}
|
||||
noOfSections={3}
|
||||
barBorderRadius={4}
|
||||
frontColor="#EF4444"
|
||||
yAxisThickness={0}
|
||||
xAxisThickness={0}
|
||||
hideRules
|
||||
isAnimated
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Recent Transactions */}
|
||||
<View className="mb-4 flex-row justify-between items-end">
|
||||
<Text className="text-xl font-bold text-gray-900 dark:text-white">Recent Transactions</Text>
|
||||
<TouchableOpacity>
|
||||
<Text className="text-blue-600 font-medium">See All</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TransactionList transactions={transactions} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 30,
|
||||
height: 1,
|
||||
width: '80%',
|
||||
},
|
||||
});
|
||||
|
||||
114
app/(tabs)/investments.tsx
Normal file
114
app/(tabs)/investments.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { FontAwesome } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ScrollView, Text, View } from 'react-native';
|
||||
import { PieChart } from 'react-native-gifted-charts';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { getDB } from '../../db';
|
||||
import { Asset } from '../../types';
|
||||
|
||||
export default function InvestmentsScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [totalValue, setTotalValue] = useState(0);
|
||||
const [pieData, setPieData] = useState<any[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const result = await db.getAllAsync<Asset>('SELECT * FROM assets ORDER BY value DESC');
|
||||
setAssets(result);
|
||||
|
||||
const total = result.reduce((acc, asset) => acc + asset.value, 0);
|
||||
setTotalValue(total);
|
||||
|
||||
// Prepare Pie Data
|
||||
const colors = ['#9333EA', '#C084FC', '#A855F7', '#7E22CE', '#6B21A8'];
|
||||
const data = result.map((asset, index) => ({
|
||||
value: asset.value,
|
||||
color: colors[index % colors.length],
|
||||
text: `${Math.round((asset.value / total) * 100)}%`,
|
||||
}));
|
||||
setPieData(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching assets', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchData();
|
||||
}, [fetchData])
|
||||
);
|
||||
|
||||
const getIconName = (type: string) => {
|
||||
switch (type) {
|
||||
case 'stock': return 'line-chart';
|
||||
case 'crypto': return 'bitcoin';
|
||||
case 'real_estate': return 'home';
|
||||
case 'fund': return 'pie-chart';
|
||||
default: return 'money';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100 dark:bg-gray-900" style={{ paddingTop: insets.top }}>
|
||||
<ScrollView className="flex-1 px-6">
|
||||
{/* Header */}
|
||||
<View className="py-6">
|
||||
<Text className="text-gray-500 text-sm">Total Portfolio Value</Text>
|
||||
<Text className="text-4xl font-bold text-gray-900 dark:text-white">€{totalValue.toFixed(2)}</Text>
|
||||
</View>
|
||||
|
||||
{/* Asset Distribution */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-8 shadow-sm items-center">
|
||||
<Text className="text-lg font-bold text-gray-900 dark:text-white mb-4 self-start">Asset Allocation</Text>
|
||||
{pieData.length > 0 ? (
|
||||
<PieChart
|
||||
data={pieData}
|
||||
donut
|
||||
showText
|
||||
textColor="white"
|
||||
radius={100}
|
||||
innerRadius={60}
|
||||
textSize={12}
|
||||
focusOnPress
|
||||
/>
|
||||
) : (
|
||||
<Text className="text-gray-500">No assets to display.</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Assets List */}
|
||||
<Text className="text-xl font-bold text-gray-900 dark:text-white mb-4">Your Assets</Text>
|
||||
|
||||
{assets.length === 0 ? (
|
||||
<View className="items-center py-10">
|
||||
<Text className="text-gray-400">No assets added yet.</Text>
|
||||
</View>
|
||||
) : (
|
||||
assets.map((item) => (
|
||||
<View key={item.id} className="bg-white dark:bg-gray-800 p-4 mb-3 rounded-2xl shadow-sm flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-10 h-10 rounded-full bg-purple-100 items-center justify-center mr-4">
|
||||
<FontAwesome name={getIconName(item.type)} size={16} color="#9333EA" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-bold text-gray-900 dark:text-white text-base">{item.name}</Text>
|
||||
<Text className="text-gray-500 text-xs capitalize">{item.type.replace('_', ' ')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<Text className="font-bold text-base text-gray-900 dark:text-white">€{item.value.toFixed(2)}</Text>
|
||||
{item.quantity && (
|
||||
<Text className="text-gray-400 text-xs">{item.quantity} units</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Tab Two</Text>
|
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
||||
<EditScreenInfo path="app/(tabs)/two.tsx" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 30,
|
||||
height: 1,
|
||||
width: '80%',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user