feat: Introduce financial goal and asset management with SQLite and NativeWind integration, new tab screens, and updated dependencies.
parent
60360e8eb9
commit
e002d0d1fd
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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%',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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%',
|
||||
},
|
||||
});
|
||||
|
|
@ -4,13 +4,14 @@ import { useFonts } from 'expo-font';
|
|||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
import { initDatabase } from '../db';
|
||||
import '../global.css';
|
||||
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
|
||||
export {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
ErrorBoundary,
|
||||
ErrorBoundary
|
||||
} from 'expo-router';
|
||||
|
||||
export const unstable_settings = {
|
||||
|
|
@ -32,6 +33,12 @@ export default function RootLayout() {
|
|||
if (error) throw error;
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
initDatabase()
|
||||
.then(() => console.log('Database initialized'))
|
||||
.catch(e => console.error('Database init failed', e));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { router } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { getDB } from '../db';
|
||||
|
||||
export default function AddAssetScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState('stock');
|
||||
const [value, setValue] = useState('');
|
||||
const [quantity, setQuantity] = useState('');
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name || !value) {
|
||||
alert('Please fill in name and value');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDB();
|
||||
await db.runAsync(
|
||||
'INSERT INTO assets (name, type, value, quantity, purchase_date) VALUES (?, ?, ?, ?, ?)',
|
||||
[name, type, parseFloat(value), quantity ? parseFloat(quantity) : null, new Date().toISOString()]
|
||||
);
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Failed to save asset');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100 dark:bg-gray-900">
|
||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||
|
||||
<View className="bg-purple-600 pt-4 pb-6 px-6 rounded-b-3xl shadow-lg">
|
||||
<Text className="text-white text-2xl font-bold mt-8 text-center">New Asset</Text>
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
<ScrollView className="flex-1 px-6 pt-6">
|
||||
|
||||
{/* Type Selector */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="flex-row space-x-2 mb-6">
|
||||
{['stock', 'crypto', 'real_estate', 'fund', 'other'].map((t) => (
|
||||
<TouchableOpacity
|
||||
key={t}
|
||||
onPress={() => setType(t)}
|
||||
className={`px-4 py-2 rounded-full border ${type === t ? 'bg-purple-600 border-purple-600' : 'bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700'}`}
|
||||
>
|
||||
<Text className={`capitalize ${type === t ? 'text-white' : 'text-gray-900 dark:text-gray-300'}`}>{t.replace('_', ' ')}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Value Input */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-4 shadow-sm">
|
||||
<Text className="text-gray-500 text-sm mb-2">Current Value</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-3xl font-bold text-gray-900 dark:text-white mr-2">€</Text>
|
||||
<TextInput
|
||||
className="flex-1 text-4xl font-bold text-gray-900 dark:text-white"
|
||||
placeholder="0.00"
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Details Form */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-sm space-y-4">
|
||||
<View>
|
||||
<Text className="text-gray-500 text-sm mb-2">Asset Name</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="e.g. Apple Stock, Bitcoin"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Text className="text-gray-500 text-sm mb-2">Quantity (Optional)</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="0"
|
||||
keyboardType="numeric"
|
||||
value={quantity}
|
||||
onChangeText={setQuantity}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<View className="p-6 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800" style={{ paddingBottom: Math.max(insets.bottom, 24) }}>
|
||||
<TouchableOpacity
|
||||
className="w-full bg-purple-600 py-4 rounded-2xl shadow-lg active:bg-purple-700"
|
||||
onPress={handleSave}
|
||||
>
|
||||
<Text className="text-white text-center font-bold text-lg">Save Asset</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { router } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { getDB } from '../db';
|
||||
|
||||
export default function AddGoalScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [name, setName] = useState('');
|
||||
const [targetAmount, setTargetAmount] = useState('');
|
||||
const [currentAmount, setCurrentAmount] = useState('');
|
||||
const [deadline, setDeadline] = useState('');
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name || !targetAmount) {
|
||||
alert('Please fill in name and target amount');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDB();
|
||||
await db.runAsync(
|
||||
'INSERT INTO goals (name, target_amount, current_amount, deadline) VALUES (?, ?, ?, ?)',
|
||||
[name, parseFloat(targetAmount), currentAmount ? parseFloat(currentAmount) : 0, deadline]
|
||||
);
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Failed to save goal');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100 dark:bg-gray-900">
|
||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||
|
||||
<View className="bg-teal-600 pt-4 pb-6 px-6 rounded-b-3xl shadow-lg">
|
||||
<Text className="text-white text-2xl font-bold mt-8 text-center">New Goal</Text>
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
<ScrollView className="flex-1 px-6 pt-6">
|
||||
|
||||
{/* Target Amount Input */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-4 shadow-sm">
|
||||
<Text className="text-gray-500 text-sm mb-2">Target Amount</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-3xl font-bold text-gray-900 dark:text-white mr-2">€</Text>
|
||||
<TextInput
|
||||
className="flex-1 text-4xl font-bold text-gray-900 dark:text-white"
|
||||
placeholder="1000.00"
|
||||
keyboardType="numeric"
|
||||
value={targetAmount}
|
||||
onChangeText={setTargetAmount}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Details Form */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-sm space-y-4">
|
||||
<View>
|
||||
<Text className="text-gray-500 text-sm mb-2">Goal Name</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="e.g. New Car, Vacation"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Text className="text-gray-500 text-sm mb-2">Current Savings (Optional)</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="0.00"
|
||||
keyboardType="numeric"
|
||||
value={currentAmount}
|
||||
onChangeText={setCurrentAmount}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Text className="text-gray-500 text-sm mb-2">Deadline (Optional)</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="YYYY-MM-DD"
|
||||
value={deadline}
|
||||
onChangeText={setDeadline}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<View className="p-6 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800" style={{ paddingBottom: Math.max(insets.bottom, 24) }}>
|
||||
<TouchableOpacity
|
||||
className="w-full bg-teal-600 py-4 rounded-2xl shadow-lg active:bg-teal-700"
|
||||
onPress={handleSave}
|
||||
>
|
||||
<Text className="text-white text-center font-bold text-lg">Save Goal</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
162
app/modal.tsx
162
app/modal.tsx
|
|
@ -1,35 +1,143 @@
|
|||
import { router } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { getDB } from '../db';
|
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
export default function AddTransactionScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [amount, setAmount] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [type, setType] = useState<'income' | 'expense'>('expense');
|
||||
const [date, setDate] = useState(new Date().toISOString().split('T')[0]); // YYYY-MM-DD
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!amount || !category) {
|
||||
alert('Please fill in amount and category');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDB();
|
||||
await db.runAsync(
|
||||
'INSERT INTO transactions (amount, category, date, description, type) VALUES (?, ?, ?, ?, ?)',
|
||||
[parseFloat(amount), category, date, description, type]
|
||||
);
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Failed to save transaction');
|
||||
}
|
||||
};
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Modal</Text>
|
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
||||
<EditScreenInfo path="app/modal.tsx" />
|
||||
|
||||
{/* Use a light status bar on iOS to account for the black space above the modal */}
|
||||
<View className="flex-1 bg-gray-100 dark:bg-gray-900">
|
||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||
|
||||
{/* Header */}
|
||||
<View className="bg-blue-600 pt-4 pb-6 px-6 rounded-b-3xl shadow-lg">
|
||||
<Text className="text-white text-2xl font-bold mt-8 text-center">New Transaction</Text>
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
<ScrollView className="flex-1 px-6 pt-6">
|
||||
|
||||
{/* Type Selector */}
|
||||
<View className="flex-row bg-white dark:bg-gray-800 rounded-xl p-1 mb-6 shadow-sm">
|
||||
<TouchableOpacity
|
||||
className={`flex-1 py-3 rounded-lg ${type === 'expense' ? 'bg-red-500' : 'bg-transparent'}`}
|
||||
onPress={() => setType('expense')}
|
||||
>
|
||||
<Text className={`text-center font-semibold ${type === 'expense' ? 'text-white' : 'text-gray-500'}`}>Expense</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className={`flex-1 py-3 rounded-lg ${type === 'income' ? 'bg-green-500' : 'bg-transparent'}`}
|
||||
onPress={() => setType('income')}
|
||||
>
|
||||
<Text className={`text-center font-semibold ${type === 'income' ? 'text-white' : 'text-gray-500'}`}>Income</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Amount Input */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-4 shadow-sm">
|
||||
<Text className="text-gray-500 text-sm mb-2">Amount</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-3xl font-bold text-gray-900 dark:text-white mr-2">€</Text>
|
||||
<TextInput
|
||||
className="flex-1 text-4xl font-bold text-gray-900 dark:text-white"
|
||||
placeholder="0.00"
|
||||
keyboardType="numeric"
|
||||
value={amount}
|
||||
onChangeText={setAmount}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Details Form */}
|
||||
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-sm space-y-4">
|
||||
<View>
|
||||
<Text className="text-gray-500 text-sm mb-2">Category</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="flex-row space-x-2 mb-2">
|
||||
{['Food', 'Transport', 'Shopping', 'Entertainment', 'Bills', 'Health', 'Salary', 'Investment', 'Other'].map((cat) => (
|
||||
<TouchableOpacity
|
||||
key={cat}
|
||||
onPress={() => setCategory(cat)}
|
||||
className={`px-4 py-2 rounded-full border ${category === cat ? 'bg-blue-600 border-blue-600' : 'bg-gray-50 border-gray-200 dark:bg-gray-700 dark:border-gray-600'}`}
|
||||
>
|
||||
<Text className={`${category === cat ? 'text-white' : 'text-gray-900 dark:text-gray-300'}`}>{cat}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="Or type custom category..."
|
||||
value={category}
|
||||
onChangeText={setCategory}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Text className="text-gray-500 text-sm mb-2">Date</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="YYYY-MM-DD"
|
||||
value={date}
|
||||
onChangeText={setDate}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Text className="text-gray-500 text-sm mb-2">Description (Optional)</Text>
|
||||
<TextInput
|
||||
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||
placeholder="Add a note..."
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
{/* Save Button */}
|
||||
<View className="p-6 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800" style={{ paddingBottom: Math.max(insets.bottom, 24) }}>
|
||||
<TouchableOpacity
|
||||
className="w-full bg-blue-600 py-4 rounded-2xl shadow-lg active:bg-blue-700"
|
||||
onPress={handleSave}
|
||||
>
|
||||
<Text className="text-white text-center font-bold text-lg">Save Transaction</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 30,
|
||||
height: 1,
|
||||
width: '80%',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
],
|
||||
plugins: ["react-native-reanimated/plugin"],
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { FontAwesome } from '@expo/vector-icons';
|
||||
import { FlatList, Text, View } from 'react-native';
|
||||
import { Transaction } from '../types';
|
||||
|
||||
interface TransactionListProps {
|
||||
transactions: Transaction[];
|
||||
onDelete?: (id: number) => void;
|
||||
}
|
||||
|
||||
export default function TransactionList({ transactions, onDelete }: TransactionListProps) {
|
||||
if (transactions.length === 0) {
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center py-10">
|
||||
<Text className="text-gray-400 text-lg">No transactions yet</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={transactions}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={{ paddingBottom: 20 }}
|
||||
renderItem={({ item }) => (
|
||||
<View 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 items-center justify-center mr-4 ${item.type === 'expense' ? 'bg-red-100' : 'bg-green-100'}`}>
|
||||
<FontAwesome
|
||||
name={item.type === 'expense' ? 'arrow-down' : 'arrow-up'}
|
||||
size={16}
|
||||
color={item.type === 'expense' ? '#EF4444' : '#10B981'}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-bold text-gray-900 dark:text-white text-base">{item.category}</Text>
|
||||
<Text className="text-gray-500 text-xs">{item.date}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<Text className={`font-bold text-base ${item.type === 'expense' ? 'text-red-500' : 'text-green-500'}`}>
|
||||
{item.type === 'expense' ? '-' : '+'}€{item.amount.toFixed(2)}
|
||||
</Text>
|
||||
{item.description && (
|
||||
<Text className="text-gray-400 text-xs max-w-[100px]" numberOfLines={1}>{item.description}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { openDatabaseSync } from 'expo-sqlite';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
let db: any;
|
||||
if (Platform.OS !== 'web') {
|
||||
db = openDatabaseSync('finance.db');
|
||||
} else {
|
||||
// Mock DB for web to prevent crash
|
||||
db = {
|
||||
execSync: () => { },
|
||||
getAllAsync: async () => [],
|
||||
runAsync: async () => { },
|
||||
getFirstAsync: async () => null,
|
||||
};
|
||||
}
|
||||
|
||||
export const initDatabase = () => {
|
||||
try {
|
||||
db.execSync(`
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
amount REAL NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL -- 'income' or 'expense'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- 'stock', 'crypto', 'real_estate', etc.
|
||||
value REAL NOT NULL,
|
||||
quantity REAL,
|
||||
purchase_date TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS goals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
target_amount REAL NOT NULL,
|
||||
current_amount REAL NOT NULL DEFAULT 0,
|
||||
deadline TEXT
|
||||
);
|
||||
`);
|
||||
console.log('Database initialized successfully');
|
||||
return Promise.resolve(true);
|
||||
} catch (error) {
|
||||
console.error('Error initializing database', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDB = () => db;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
config.resolver.assetExts.push("wasm");
|
||||
|
||||
module.exports = withNativeWind(config, { input: "./global.css" });
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/// <reference types="nativewind/types" />
|
||||
|
||||
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
|
@ -11,22 +11,30 @@
|
|||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"clsx": "^2.1.1",
|
||||
"expo": "~54.0.25",
|
||||
"expo-constants": "~18.0.10",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.9",
|
||||
"expo-router": "~6.0.15",
|
||||
"expo-splash-screen": "~31.0.11",
|
||||
"expo-sqlite": "^16.0.9",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-web-browser": "~15.0.9",
|
||||
"nativewind": "^4.2.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"react-native-gifted-charts": "^1.4.66",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "~0.21.0"
|
||||
"react-native-svg": "^15.12.1",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
|
||||
presets: [require("nativewind/preset")],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
"expo-env.d.ts",
|
||||
"nativewind-env.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
export interface Transaction {
|
||||
id: number;
|
||||
amount: number;
|
||||
category: string;
|
||||
date: string; // ISO 8601 YYYY-MM-DD
|
||||
description?: string;
|
||||
type: 'income' | 'expense';
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'stock' | 'crypto' | 'real_estate' | 'fund' | 'other';
|
||||
value: number;
|
||||
quantity?: number;
|
||||
purchase_date?: string;
|
||||
}
|
||||
|
||||
export interface Goal {
|
||||
id: number;
|
||||
name: string;
|
||||
target_amount: number;
|
||||
current_amount: number;
|
||||
deadline?: string;
|
||||
}
|
||||
Loading…
Reference in New Issue