Merge branch '51-skill-issue' into 'main'
Resolve "skill issue" Closes #51 See merge request thschleicher/interaktive-systeme!36
This commit is contained in:
commit
d2837c12b3
10 changed files with 407 additions and 50 deletions
|
|
@ -1,61 +1,69 @@
|
||||||
import { Query } from 'expo-sqlite';
|
import React from 'react';
|
||||||
import { StyleSheet, Text, View } from 'react-native';
|
import { StyleSheet, View, ScrollView } from 'react-native';
|
||||||
import { addCategory, deleteDatabase, deleteExpenses, executeQuery, initDatabase } from '../../../services/database';
|
import BudgetOverview from '../../../components/stats/BudgetOverview';
|
||||||
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import Widget from '../../../components/stats/Widget';
|
||||||
|
import CategoryProgressBarList from '../../../components/stats/CategoryProgressBarList';
|
||||||
|
import BudgetRemaining from '../../../components/stats/BudgetRemaining';
|
||||||
|
import DebugMenu from '../../../services/DebugMenu';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
// Mock data #TODO Database einbinden
|
||||||
|
// what to do when amount too small?
|
||||||
|
|
||||||
|
const spent = 120.75;
|
||||||
|
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: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'space-evenly',
|
backgroundColor: colors.backgroundColor,
|
||||||
|
marginTop: 50,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
text: {
|
|
||||||
fontSize: 40,
|
|
||||||
color: "yellow",
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.text} onPress={() => {
|
<ScrollView>
|
||||||
deleteExpenses().then(() => {
|
<DebugMenu />
|
||||||
console.log("Expenses Deleted!");
|
<Widget title="Budget Overview" />
|
||||||
})
|
<Widget>
|
||||||
}}>Reset Expenses</Text>
|
<BudgetRemaining budget={budget} spent={spent} />
|
||||||
|
</Widget>
|
||||||
<Text style={styles.text} onPress={() => {
|
<Widget text='#TODO Insert Pie-Chart' image={require('../../../assets/images/8b14el.jpg')}/>
|
||||||
deleteDatabase();
|
<Widget>
|
||||||
console.log("Database Deleted!");
|
<BudgetOverview budget={budget} spent={spent} />
|
||||||
}}>Reset Database</Text>
|
</Widget>
|
||||||
|
<Widget title="Budget Progress">
|
||||||
<Text style={styles.text} onPress={() => {
|
<CategoryProgressBarList categories={BudgetData} />
|
||||||
initDatabase().then(() => {
|
</Widget>
|
||||||
console.log("Database Initialized!");
|
<Widget title="Savings Progress">
|
||||||
});
|
<CategoryProgressBarList categories={SavingsData} />
|
||||||
}}>Init Database</Text>
|
</Widget>
|
||||||
|
</ScrollView>
|
||||||
<Text style={styles.text} onPress={() => {
|
</View>
|
||||||
addCategory("Category", "green", "expense", 500).then(() => {
|
);
|
||||||
const getCategoryQuery: Query = {sql: "SELECT guid FROM category", args: []};
|
|
||||||
executeQuery(getCategoryQuery).then((result) => {
|
|
||||||
if("rows" in result[0]) {
|
|
||||||
console.log(result[0]["rows"]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log("Category added with success!");
|
|
||||||
})
|
|
||||||
}}>Add new Category Expense</Text>
|
|
||||||
|
|
||||||
<Text style={styles.text} onPress={() => {
|
|
||||||
addCategory("Category", "yellow", "saving", 420).then(() => {
|
|
||||||
const getCategoryQuery: Query = {sql: "SELECT guid FROM category", args: []};
|
|
||||||
executeQuery(getCategoryQuery).then((result) => {
|
|
||||||
if("rows" in result[0]) {
|
|
||||||
console.log(result[0]["rows"]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log("Category added with success!");
|
|
||||||
})
|
|
||||||
}}>Add new Category Savings</Text>
|
|
||||||
</View>);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
assets/images/8b14el.jpg
Normal file
BIN
assets/images/8b14el.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
38
components/stats/BudgetOverview.tsx
Normal file
38
components/stats/BudgetOverview.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface StatsBudgetProps {
|
||||||
|
spent: number;
|
||||||
|
budget: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetOverview: React.FC<StatsBudgetProps> = ({ spent, budget }) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
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}>
|
||||||
|
You have spent <Text style={styles.boldText}>{spent.toFixed(2)}€</Text> out of your budget of <Text style={styles.boldText}>{budget.toFixed(2)}€</Text>.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetOverview
|
||||||
38
components/stats/BudgetRemaining.tsx
Normal file
38
components/stats/BudgetRemaining.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, StyleSheet } from 'react-native';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface BudgetRemainingProps {
|
||||||
|
budget: number;
|
||||||
|
spent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetRemaining: React.FC<BudgetRemainingProps> = ({ budget, spent }) => {
|
||||||
|
const { colors, theme } = useTheme();
|
||||||
|
|
||||||
|
const remaining = budget - spent;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
margin: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 26,
|
||||||
|
color: colors.primaryText,
|
||||||
|
},
|
||||||
|
boldText: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text style={styles.text}>
|
||||||
|
You have <Text style={styles.boldText}>{remaining.toFixed(2)}€</Text> left.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetRemaining;
|
||||||
69
components/stats/CategoryProgressBar.tsx
Normal file
69
components/stats/CategoryProgressBar.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface CategoryProgressBarProps {
|
||||||
|
categoryName: string;
|
||||||
|
color?: string;
|
||||||
|
maxValue: number;
|
||||||
|
currentValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryProgressBar: React.FC<CategoryProgressBarProps> = ({
|
||||||
|
categoryName,
|
||||||
|
color,
|
||||||
|
maxValue,
|
||||||
|
currentValue,
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const progress = (currentValue / maxValue) * 100;
|
||||||
|
const progressText = `${currentValue}€ / ${maxValue}€`;
|
||||||
|
|
||||||
|
const dynamicStyles = StyleSheet.create({
|
||||||
|
progressBarFill: {
|
||||||
|
height: '100%',
|
||||||
|
width: `${progress}%`,
|
||||||
|
backgroundColor: color || colors.accentColor,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
progressText: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
progressBarContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: 50,
|
||||||
|
backgroundColor: colors.elementSelectedColor,
|
||||||
|
borderRadius: 15,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
categoryName: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.categoryName}>{categoryName}</Text>
|
||||||
|
<View style={styles.progressBarContainer}>
|
||||||
|
<View style={dynamicStyles.progressBarFill}>
|
||||||
|
<Text style={dynamicStyles.progressText}>{progressText}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryProgressBar;
|
||||||
61
components/stats/CategoryProgressBarList.tsx
Normal file
61
components/stats/CategoryProgressBarList.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, Button, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
|
import CategoryProgressBar from './CategoryProgressBar';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface CategoryItem {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
maxValue: number;
|
||||||
|
currentValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryProgressBarListProps {
|
||||||
|
categories: CategoryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE_BARS = 4;
|
||||||
|
|
||||||
|
const CategoryProgressBarList: React.FC<CategoryProgressBarListProps> = ({ categories }) => {
|
||||||
|
const [visibleBars, setVisibleBars] = useState(MAX_VISIBLE_BARS);
|
||||||
|
|
||||||
|
const showMore = () => {
|
||||||
|
setVisibleBars(prevVisibleBars => prevVisibleBars + MAX_VISIBLE_BARS);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{categories.slice(0, visibleBars).map((category, index) => (
|
||||||
|
<CategoryProgressBar
|
||||||
|
key={index}
|
||||||
|
categoryName={category.name}
|
||||||
|
color={category.color}
|
||||||
|
maxValue={category.maxValue}
|
||||||
|
currentValue={category.currentValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{visibleBars < categories.length && (
|
||||||
|
<TouchableOpacity style={styles.showMoreButton} onPress={showMore}>
|
||||||
|
<Text style={styles.buttonText}>Show More</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
showMoreButton: {
|
||||||
|
backgroundColor: '#EF6C00',
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
margin: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CategoryProgressBarList;
|
||||||
1
components/stats/PieChart.tsx
Normal file
1
components/stats/PieChart.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
//honestly just fuck graphs
|
||||||
63
components/stats/Widget.tsx
Normal file
63
components/stats/Widget.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { View, StyleSheet, Text, Image } from 'react-native'; // Add the missing import statement for Image
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface WidgetProps {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
image?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Widget: React.FC<WidgetProps> = ({ title, text, children, image }) => { // Add the 'image' prop to the destructuring
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
widgetContainer: {
|
||||||
|
backgroundColor: colors.widgetBackgroundColor,
|
||||||
|
borderColor: colors.widgetBorderColor,
|
||||||
|
borderRadius: 5,
|
||||||
|
padding: 16,
|
||||||
|
marginVertical: 8,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.22,
|
||||||
|
shadowRadius: 2.22,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
widgetTitle: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
widgetText: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
imageStyle: {
|
||||||
|
width: '100%',
|
||||||
|
height: 200,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.widgetContainer}>
|
||||||
|
{!!title && <Text style={styles.widgetTitle}>{title}</Text>}
|
||||||
|
{!!text && <Text style={styles.widgetText}>{text}</Text>}
|
||||||
|
{!!image && <Image source={image} style={styles.imageStyle} />}
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Widget;
|
||||||
|
|
@ -12,6 +12,9 @@ export default {
|
||||||
elementDefaultColor: "#E0E0E0",
|
elementDefaultColor: "#E0E0E0",
|
||||||
elementSelectedColor: "#9E9E9E",
|
elementSelectedColor: "#9E9E9E",
|
||||||
accentColor: "#EF6C00",
|
accentColor: "#EF6C00",
|
||||||
|
|
||||||
|
widgetBackgroundColor: "#F7F7F7",
|
||||||
|
widgetBorderColor: "#E0E0E0",
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
primaryText: "#FFFFFF",
|
primaryText: "#FFFFFF",
|
||||||
|
|
@ -26,5 +29,8 @@ export default {
|
||||||
elementDefaultColor: "#535353",
|
elementDefaultColor: "#535353",
|
||||||
elementSelectedColor: "#B3B3B3",
|
elementSelectedColor: "#B3B3B3",
|
||||||
accentColor: "#EF6C00",
|
accentColor: "#EF6C00",
|
||||||
|
|
||||||
|
widgetBackgroundColor: "#252525",
|
||||||
|
widgetBorderColor: "#535353",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
73
services/DebugMenu.tsx
Normal file
73
services/DebugMenu.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Button, Alert } from 'react-native';
|
||||||
|
import { addCategory, addExpense, deleteExpenses, deleteCategories, DEV_populateDatabase } from './database';
|
||||||
|
import uuid from 'react-native-uuid';
|
||||||
|
|
||||||
|
const randomColors = ["red", "blue", "green", "purple", "yellow"];
|
||||||
|
|
||||||
|
const getRandomColor = () => {
|
||||||
|
return randomColors[Math.floor(Math.random() * randomColors.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRandomName = () => {
|
||||||
|
return `RandomName-${Math.floor(Math.random() * 1000)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRandomNumber = () => {
|
||||||
|
return Math.floor(Math.random() * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DebugMenu = () => {
|
||||||
|
|
||||||
|
const handleNukeDatabase = () => {
|
||||||
|
return deleteExpenses(), deleteCategories()
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePopulateDatabase = () => {
|
||||||
|
return DEV_populateDatabase()
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteExpenses = () => {
|
||||||
|
return deleteExpenses();
|
||||||
|
}
|
||||||
|
const handleDeleteCategories = () => {
|
||||||
|
return deleteCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
//for some reason this function does not work
|
||||||
|
const handleAddCategory = () => {
|
||||||
|
const name = getRandomName();
|
||||||
|
const color = getRandomColor();
|
||||||
|
const allocated_amount = getRandomNumber();
|
||||||
|
const type = "expense";
|
||||||
|
|
||||||
|
addCategory(name, color, type, allocated_amount)
|
||||||
|
.then(() => Alert.alert("Category Added", `Name: ${name}, Color: ${color}`))
|
||||||
|
.catch((error: any) => console.error("Error adding category: ", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
//for some reason this function does not work
|
||||||
|
const handleAddExpense = () => {
|
||||||
|
const name = getRandomName();
|
||||||
|
const categoryGuid = uuid.v4().toString();
|
||||||
|
const datetime = new Date().toISOString();
|
||||||
|
const amount = Math.floor(Math.random() * 1000);
|
||||||
|
|
||||||
|
addExpense(name, categoryGuid, datetime, amount)
|
||||||
|
.then(() => Alert.alert("Expense Added", `Name: ${name}, Amount: ${amount}`))
|
||||||
|
.catch((error: any) => console.error("Error adding expense: ", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Button title="Nuke Database" onPress={handleNukeDatabase} />
|
||||||
|
<Button title="DEV_populateDatabase" onPress={handlePopulateDatabase} />
|
||||||
|
<Button title="Delete All Expenses" onPress={handleDeleteExpenses} />
|
||||||
|
<Button title="Delete All Categories" onPress={handleDeleteCategories} />
|
||||||
|
<Button title="Add Random Category" onPress={handleAddCategory} />
|
||||||
|
<Button title="Add Random Expense" onPress={handleAddExpense} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebugMenu;
|
||||||
Reference in a new issue