Merge branch '58-stats-page-database-connection' into 'main'

Resolve "stats page, database connection"

Closes #58

See merge request thschleicher/interaktive-systeme!47
This commit is contained in:
jastornig 2024-01-05 13:56:58 +00:00
commit 21ea0bfe07
7 changed files with 216 additions and 59 deletions

View file

@ -6,6 +6,8 @@ import Widget from '../../../components/stats/Widget';
import CategoryProgressBarList from '../../../components/stats/CategoryProgressBarList'; import CategoryProgressBarList from '../../../components/stats/CategoryProgressBarList';
import BudgetRemaining from '../../../components/stats/BudgetRemaining'; import BudgetRemaining from '../../../components/stats/BudgetRemaining';
import DebugMenu from '../../../services/DebugMenu'; import DebugMenu from '../../../services/DebugMenu';
import SavingsOverview from '../../../components/stats/SavingsOverview';
import FinancialAdvice from '../../../components/stats/FinancialAdvice';
export default function Page() { export default function Page() {
const { colors } = useTheme(); const { colors } = useTheme();
@ -16,25 +18,7 @@ export default function Page() {
const spent = 120.75; const spent = 120.75;
const budget = 696.96; const budget = 696.96;
const BudgetData = [
{ name: 'Utilities', color: '#20B2AA', maxValue: 80, currentValue: 46 },
{ name: 'Food', color: '#FF6347', maxValue: 88, currentValue: 31 },
];
const SavingsData = [
{ name: 'Education', color: '#FF6347', maxValue: 135, currentValue: 0 },
{ name: 'Rent', color: '#DA70D6', maxValue: 140, currentValue: 96 },
{ name: 'Food', color: '#F08080', maxValue: 84, currentValue: 78 },
{ name: 'Healthcare', color: '#20B2AA', maxValue: 134, currentValue: 48 },
{ name: 'Healthcare', color: '#32CD32', maxValue: 119, currentValue: 69 },
{ name: 'Clothing', color: '#32CD32', maxValue: 115, currentValue: 99 },
];
const categoryData = [
{ category: 'Food', value: 50 },
{ category: 'Rent', value: 300 },
{ category: 'Utilities', value: 100 },
];
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -49,13 +33,19 @@ export default function Page() {
<View style={styles.container}> <View style={styles.container}>
<ScrollView> <ScrollView>
<DebugMenu /> <DebugMenu />
<Widget title='"Financial" Tips' backgroundColor='orange'>
<FinancialAdvice/>
</Widget>
<Widget title="Budget Overview" /> <Widget title="Budget Overview" />
<Widget> <Widget>
<BudgetRemaining budget={budget} spent={spent} /> <BudgetRemaining/>
</Widget> </Widget>
<Widget title='Your Expenses so far' image={require('../../../assets/images/8b14el.jpg')}/> <Widget title='Your Expenses so far' image={require('../../../assets/images/8b14el.jpg')}/>
<Widget> <Widget>
<BudgetOverview budget={budget} spent={spent} /> <BudgetOverview/>
</Widget>
<Widget>
<SavingsOverview/>
</Widget> </Widget>
</ScrollView> </ScrollView>
</View> </View>

View file

@ -1,31 +1,55 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext'; import { useTheme } from '../../app/contexts/ThemeContext';
import useFetch from '../../hooks/useFetch';
interface StatsBudgetProps { const BudgetOverview = () => {
spent: number;
budget: number;
}
const BudgetOverview: React.FC<StatsBudgetProps> = ({ spent, budget }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const [spent, setSpent] = useState(0);
const [budget, setBudget] = useState(0);
const spentQuery = {
sql: "SELECT SUM(e.amount) as total FROM expense e LEFT JOIN category c ON e.category_guid = c.guid WHERE c.type = 'budget'",
args: []
};
const budgetQuery = {
sql: "SELECT SUM(allocated_amount) as total FROM category WHERE type = 'budget'",
args: []
};
const { data: spentData, isLoading: spentLoading } = useFetch(spentQuery);
const { data: budgetData, isLoading: budgetLoading } = useFetch(budgetQuery);
useEffect(() => {
if (spentData) {
setSpent(spentData[0]?.total || 0);
}
if (budgetData) {
setBudget(budgetData[0]?.total || 0);
}
}, [spentData, budgetData]);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
margin: 10, margin: 10,
borderRadius: 5, borderRadius: 5,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center'
}, },
text: { text: {
fontSize: 26, fontSize: 26,
color: colors.primaryText, color: colors.primaryText
}, },
boldText: { boldText: {
fontWeight: 'bold', fontWeight: 'bold'
}, }
}); });
if (spentLoading || budgetLoading) {
return <Text>Loading...</Text>;
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}> <Text style={styles.text}>
@ -35,4 +59,4 @@ const BudgetOverview: React.FC<StatsBudgetProps> = ({ spent, budget }) => {
); );
}; };
export default BudgetOverview export default BudgetOverview;

View file

@ -1,35 +1,52 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Text, StyleSheet } from 'react-native'; import { Text, StyleSheet } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext'; import { useTheme } from '../../app/contexts/ThemeContext';
import useFetch from '../../hooks/useFetch';
interface BudgetRemainingProps { const styles = StyleSheet.create({
budget: number; text: {
spent: number; fontSize: 26,
} },
boldText: {
fontWeight: 'bold',
},
});
const BudgetRemaining: React.FC<BudgetRemainingProps> = ({ budget, spent }) => { const BudgetRemaining = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const [spent, setSpent] = useState(0);
const [budget, setBudget] = useState(0);
const spentQuery = {
sql: "SELECT SUM(amount) as total FROM expense",
args: []
};
const budgetQuery = {
sql: "SELECT SUM(allocated_amount) as total FROM category WHERE type = 'budget'",
args: []
};
const { data: spentData, isLoading: spentLoading } = useFetch(spentQuery);
const { data: budgetData, isLoading: budgetLoading } = useFetch(budgetQuery);
useEffect(() => {
if (spentData) {
setSpent(spentData[0]?.total || 0);
}
if (budgetData) {
setBudget(budgetData[0]?.total || 0);
}
}, [spentData, budgetData]);
const remaining = budget - spent; const remaining = budget - spent;
const styles = StyleSheet.create({ if (spentLoading || budgetLoading) {
container: { return <Text>Loading...</Text>;
margin: 10, }
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 26,
color: colors.primaryText,
},
boldText: {
fontWeight: 'bold',
},
});
return ( return (
<Text style={styles.text}> <Text style={[styles.text, {color: colors.primaryText}]}>
You have <Text style={styles.boldText}>{remaining.toFixed(2)}</Text> left. You have <Text style={styles.boldText}>{remaining.toFixed(2)}</Text> left.
</Text> </Text>
); );

View file

@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext';
const FinancialAdvice = () => {
const tips = [
"Maybe you shouldn't have bought that full-price video game. But who needs savings when you have high scores, right?",
"That daily gourmet coffee is essential, isn't it? Who needs a retirement fund when you've got caffeine!",
"Oh, another pair of designer shoes? Because the other twenty pairs just aren't enough.",
"A luxury car to drive two blocks? Obviously, walking is for peasants.",
"Sure, subscribe to all streaming services. Who needs to socialize outside when you can binge-watch shows alone forever?",
"A gym membership you never use? At least your wallet's getting a workout.",
"Booking another expensive vacation? It's not like you need to save for a rainy day or anything.",
"Another impulse purchase online? Because 'limited time offer' is definitely not a marketing tactic.",
"Eating out for every meal? Clearly, cooking at home is way too mainstream.",
"Upgrading to the latest phone model again? It must be tough having a phone that's 6 months old."
];
const { colors } = useTheme();
const [tip, setTip] = useState('');
useEffect(() => {
// Change the tip every 10 seconds
const intervalId = setInterval(() => {
const randomTip = tips[Math.floor(Math.random() * tips.length)];
setTip(randomTip);
}, 10000);
// Clear the interval on component unmount
return () => clearInterval(intervalId);
}, [tips]);
const styles = StyleSheet.create({
container: {
margin: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center'
},
text: {
fontSize: 26,
color: colors.primaryText
},
boldText: {
fontWeight: 'bold'
}
});
return (
<View style={styles.container}>
<Text style={styles.text}>
{tip}
</Text>
</View>
);
};
export default FinancialAdvice;

View file

@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext';
import useFetch from '../../hooks/useFetch';
const SavingsOverview = () => {
const { colors } = useTheme();
const [saved, setSaved] = useState(0);
const [goal, setGoal] = useState(0);
const savedQuery = {
sql: "SELECT SUM(e.amount) as total FROM expense e LEFT JOIN category c ON e.category_guid = c.guid WHERE c.type = 'savings'",
args: []
};
const goalQuery = {
sql: "SELECT SUM(allocated_amount) as total FROM category WHERE type = 'budget'",
args: []
};
const { data: savedData, isLoading: savedLoading } = useFetch(savedQuery);
const { data: goalData, isLoading: goalLoading } = useFetch(goalQuery);
useEffect(() => {
if (savedData) {
setSaved(savedData[0]?.total || 0);
}
if (goalData) {
setGoal(goalData[0]?.total || 0);
}
}, [savedData, goalData]);
const styles = StyleSheet.create({
container: {
margin: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center'
},
text: {
fontSize: 26,
color: colors.primaryText
},
boldText: {
fontWeight: 'bold'
}
});
if (savedLoading || goalLoading) {
return <Text>Loading...</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.text}>
You have saved <Text style={styles.boldText}>{saved.toFixed(2)}</Text> out of your goal of <Text style={styles.boldText}>{goal.toFixed(2)}</Text>.
</Text>
</View>
);
};
export default SavingsOverview;

View file

@ -1,5 +1,5 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { View, StyleSheet, Text, Image } from 'react-native'; // Add the missing import statement for Image import { View, StyleSheet, Text, Image } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext'; import { useTheme } from '../../app/contexts/ThemeContext';
interface WidgetProps { interface WidgetProps {
@ -7,14 +7,18 @@ interface WidgetProps {
text?: string; text?: string;
children?: ReactNode; children?: ReactNode;
image?: any; image?: any;
backgroundColor?: string;
} }
const Widget: React.FC<WidgetProps> = ({ title, text, children, image }) => { // Add the 'image' prop to the destructuring const Widget: React.FC<WidgetProps> = ({ title, text, children, image, backgroundColor }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const actualBackgroundColor = backgroundColor ? backgroundColor : colors.widgetBackgroundColor;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
widgetContainer: { widgetContainer: {
backgroundColor: colors.widgetBackgroundColor, backgroundColor: actualBackgroundColor,
borderColor: colors.widgetBorderColor, borderColor: colors.widgetBorderColor,
borderRadius: 5, borderRadius: 5,
padding: 16, padding: 16,

View file

@ -144,6 +144,7 @@ export const DEV_populateDatabase = async () => {
for(let i=0; i < 5; i++){ for(let i=0; i < 5; i++){
let random = Math.floor(Math.random() * colors.length); let random = Math.floor(Math.random() * colors.length);
await addCategory(`Category ${i}`, colors[random], "budget", 50) await addCategory(`Category ${i}`, colors[random], "budget", 50)
await addCategory(`Category ${i+6}`, colors[random], "budget", 50)
} }
const result = await executeQuery({sql:"SELECT * from category", args:[]}) const result = await executeQuery({sql:"SELECT * from category", args:[]})
let categories: {[column: string]: any}[]; let categories: {[column: string]: any}[];
@ -160,7 +161,7 @@ export const DEV_populateDatabase = async () => {
let randomDay = Math.floor(Math.random() * 20) let randomDay = Math.floor(Math.random() * 20)
date.setDate(randomDay) date.setDate(randomDay)
let string = new SimpleDate(date).toISOString() let string = new SimpleDate(date).toISOString()
await addExpense(`Expense ${i}`, categories[random].guid, string, 15) await addExpense(`Expense ${i}`, categories[random].guid, string, 30)
} }
} catch(e){ } catch(e){
console.error(e) console.error(e)