diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 6dc7691..4eb7245 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -34,12 +34,13 @@ export default function Layout() { return ( - ( ), unmountOnBlur: true, + href: "(tabs)/budget" } }/> + + + + ); +} \ No newline at end of file diff --git a/app/(tabs)/budget/addCategory.tsx b/app/(tabs)/budget/addCategory.tsx new file mode 100644 index 0000000..e8edbbc --- /dev/null +++ b/app/(tabs)/budget/addCategory.tsx @@ -0,0 +1,8 @@ + +const addCategory = () => { + return ( + <> + ); +} + +export default addCategory; \ No newline at end of file diff --git a/app/(tabs)/budget/index.tsx b/app/(tabs)/budget/index.tsx index 62b3162..4ca010e 100644 --- a/app/(tabs)/budget/index.tsx +++ b/app/(tabs)/budget/index.tsx @@ -1,28 +1,70 @@ -import { SafeAreaView, StyleSheet, Switch, Text } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { router } from 'expo-router'; +import { useEffect, useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { BudgetHeader, LoadingSymbol, Plus } from '../../../components'; +import CategoryItem from '../../../components/budget/categoryItem'; +import useFetch from '../../../hooks/useFetch'; +import { useTheme } from '../../contexts/ThemeContext'; export default function Page() { + const {colors} = useTheme() + const containerColor = colors.containerColor; - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - switch: { - transform: [{ scaleX: 1.3 }, { scaleY: 1.3 }], - }, - text: { - color: "red", - fontSize: 40, + const [selectedPage, setSelectedPage] = useState("noPageLoaded"); + + useEffect(() => { + AsyncStorage.getItem("currentBudgetPage").then((page) => { + if(page === "expenses" || page === "savings") { + setSelectedPage(page); + } + }).catch((error) => { + console.log("Error fetching previous page from Async Storage:", error); + }) + }, []); + + const {data, isLoading, reFetch} = useFetch({sql: "SELECT c.guid AS category_guid, c.name AS category_name, c.color AS category_color, c.type AS category_type, SUM(e.amount) as total_expenses, c.allocated_amount as allocated_amount FROM expense e RIGHT JOIN category c ON e.category_guid = c.guid WHERE c.type = ? GROUP BY c.guid", args: selectedPage === "expenses" ? ["expense"] : selectedPage === "savings" ? ["saving"] : []}); + + useEffect(() => { + reFetch(); + }, [selectedPage]); + + const handlePageSelection = (page: string) => { + if(page !== selectedPage) { + setSelectedPage(page); + AsyncStorage.setItem("currentBudgetPage", page); } - }); - - // const {data, isLoading, reFetch} = useFetch(); + }; return ( - - Hallo wo bin ich?! - + + + + { + router.push("/(tabs)/budget/addCategory") + }}/> + + {isLoading ? () : ( + } + keyExtractor={item => item.category_guid} + ItemSeparatorComponent={() => { + return (); + }} + /> + )} ); -} \ No newline at end of file +} + +const styles = StyleSheet.create({ + safeAreaViewStyle: { + flex: 1, + }, + itemSeperatorStyle: { + marginVertical: 5, + }, +}); \ No newline at end of file diff --git a/app/(tabs)/home/index.tsx b/app/(tabs)/home/index.tsx index 4a510a1..eaa0b22 100644 --- a/app/(tabs)/home/index.tsx +++ b/app/(tabs)/home/index.tsx @@ -5,7 +5,8 @@ import { FlatList } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ExpenseItem, LoadingSymbol, Plus, SearchBar, Welcome } from '../../../components'; import useFetch from '../../../hooks/useFetch'; -import { addExpense } from "../../../services/database"; + +import { addExpense, executeQuery } from "../../../services/database"; import { useAuth } from '../../contexts/AuthContext'; import { useRouter } from "expo-router"; import { SimpleDate } from '../../../util/SimpleDate'; @@ -37,26 +38,7 @@ const constructMarkedDates = (data : {[column: string]: any}) => { export default function Page() { const { colors, theme } = useTheme() - //Styles - const styles = StyleSheet.create({ - safeAreaViewStyle: { - flex: 1, - backgroundColor: colors.backgroundColor - }, - container: { - flex: 1, - alignItems: "center", - justifyContent: "center", - }, - text: { - color: colors.primaryText, - fontSize: 70, - fontWeight: "bold" - }, - loading: { - color: "red", - } - }); + const router = useRouter(); const [plusShow, setPlusShow] = useState(true); @@ -81,7 +63,7 @@ export default function Page() { } } - const {data, isLoading, reFetch} = useFetch(); + const {data, isLoading, reFetch} = useFetch({sql: "SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid;", args: []}); const expenseDates = useMemo(()=> constructMarkedDates(data) @@ -90,15 +72,20 @@ export default function Page() { return ( - + {plusShow && { // router.push("/(tabs)/home/addItem"); - newExpense("Test Title", "3b33b8ac-5fc1-43e5-81fc-cf61628861f7", "69.69.1234", 100).then(() => { - reFetch(); - }); + + executeQuery({sql: "SELECT guid FROM category", args: []}).then((result) => { + if("rows" in result[0]) { + newExpense("Test Title", result[0]["rows"][0]["guid"], "69.69.1234", 100).then(() => { + reFetch(); + }); + } + }) }}/>} - {isLoading && } + {isLoading && } } - renderItem = {({item}) => } + renderItem = {({item}) => } keyExtractor={item => item.expense_guid} - ItemSeparatorComponent={()=>} + ItemSeparatorComponent={() => { + return (); + }} onScroll={handleScroll} scrollEventThrottle={20} - > - + /> ); -} \ No newline at end of file +} + +const styles = StyleSheet.create({ + safeAreaViewStyle: { + flex: 1, + }, + itemSeperatorStyle: { + marginVertical: 5, + } +}); \ No newline at end of file diff --git a/app/(tabs)/stats/index.tsx b/app/(tabs)/stats/index.tsx index 85c1636..9dd775a 100644 --- a/app/(tabs)/stats/index.tsx +++ b/app/(tabs)/stats/index.tsx @@ -1,22 +1,20 @@ +import { Query } from 'expo-sqlite'; import { StyleSheet, Text, View } from 'react-native'; -import { deleteExpenses } from '../../../services/database'; +import { addCategory, deleteDatabase, deleteExpenses, executeQuery, initDatabase } from '../../../services/database'; export default function Page() { const styles = StyleSheet.create({ container: { flex: 1, - justifyContent: 'center', + justifyContent: 'space-evenly', alignItems: 'center', }, text: { - textAlign: 'center', fontSize: 40, - color: "red", + color: "yellow", } }); - - return ( { @@ -24,5 +22,40 @@ export default function Page() { console.log("Expenses Deleted!"); }) }}>Reset Expenses + + { + deleteDatabase(); + console.log("Database Deleted!"); + }}>Reset Database + + { + initDatabase().then(() => { + console.log("Database Initialized!"); + }); + }}>Init Database + + { + 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 + + { + 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 ); } diff --git a/components/budget/budgetHeader.tsx b/components/budget/budgetHeader.tsx new file mode 100644 index 0000000..eed9624 --- /dev/null +++ b/components/budget/budgetHeader.tsx @@ -0,0 +1,84 @@ +import { StyleSheet, Text, TouchableHighlight, View } from "react-native"; +import SearchBar from "../common/SearchBar"; +import { useTheme } from "../../app/contexts/ThemeContext"; + +type BudgetHeaderProperties = { + selectedPage: string, + handlePageSelection: (page: string) => void, +} + +type PageSelectorButtonProperties = { + isSelected: boolean, + onPress: () => void, + label: string, +} + +const BudgetHeader = (properties: BudgetHeaderProperties) => { + const {colors} = useTheme(); + const backgroundColor = colors.backgroundColor; + + return (<> + + { + properties.handlePageSelection("expenses") + }} + /> + { + properties.handlePageSelection("savings"); + }} + /> + + + ); +} + +const PageSelectorButton = (properties: PageSelectorButtonProperties) => { + const {colors} = useTheme(); + + const primaryTextColor = colors.primaryText; + const secondaryTextColor = colors.secondaryText; + const elementSelectedColor = colors.elementSelectedColor; + const elementDefaultColor = colors.elementDefaultColor; + const accentColor = colors.accentColor; + + return ( + + + {properties.label} + + + + ); +} + +export default BudgetHeader; + +const styles = StyleSheet.create({ + headerContainerStyle: { + width: "50%", + borderRadius: 10, + marginHorizontal: 30, + }, + headerTextStyle: { + fontSize: 30, + textAlign: "center", + textAlignVertical: "center", + }, + containerStyle: { + flexDirection: "row", + justifyContent: "space-evenly", + marginHorizontal: 20, + marginBottom: 20, + marginTop: 10, + }, +}); \ No newline at end of file diff --git a/components/budget/categoryItem.tsx b/components/budget/categoryItem.tsx new file mode 100644 index 0000000..e651caf --- /dev/null +++ b/components/budget/categoryItem.tsx @@ -0,0 +1,56 @@ +import { ColorValue, StyleSheet, Text, View } from "react-native"; +import { useTheme } from "../../app/contexts/ThemeContext"; +import CustomCard from "../common/CustomCard"; + +export type CategoryItemProps = { + category: string, + color: ColorValue, + allocated_amount: number, + total_expenses: number, + category_guid: string, +} + +const CategoryItem = (properties: CategoryItemProps) => { + + const { colors } = useTheme(); + + const subText = `${properties.total_expenses} / ${properties.allocated_amount} €`; + + return ( + + + + + {properties.category} + + + {subText} + + + + ); +}; + +export default CategoryItem; + +const styles = StyleSheet.create({ + colorTipStyle: { + width: 25, + borderTopLeftRadius: 10, + borderBottomLeftRadius: 10, + }, + textViewStyle: { + flex: 2, + flexDirection: "column", + paddingVertical: 5, + paddingHorizontal: 10, + alignSelf: "stretch", + }, + categoryNameStyle: { + fontSize: 30, + fontWeight: "bold", + }, + subTextStyle: { + fontSize: 17.5, + } + }) diff --git a/components/common/CustomCard.tsx b/components/common/CustomCard.tsx index d81020e..5358d51 100644 --- a/components/common/CustomCard.tsx +++ b/components/common/CustomCard.tsx @@ -42,7 +42,7 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "stretch", alignContent: "space-between", - borderRadius: 20, + borderRadius: 10, marginHorizontal: 10, }, boxShadow: {}, diff --git a/components/home/userSettings/Setting.tsx b/components/home/Setting.tsx similarity index 85% rename from components/home/userSettings/Setting.tsx rename to components/home/Setting.tsx index 6b480d5..9089f82 100644 --- a/components/home/userSettings/Setting.tsx +++ b/components/home/Setting.tsx @@ -1,7 +1,7 @@ -import { View, Text, StyleSheet, Switch, SwitchProps, useColorScheme, TouchableOpacityProps, TouchableOpacity } from 'react-native' -import React from 'react' -import { SIZES } from '../../../constants/theme' -import { useTheme } from '../../../app/contexts/ThemeContext'; +import React from 'react'; +import { StyleSheet, Switch, SwitchProps, Text, TouchableOpacity, TouchableOpacityProps, View } from 'react-native'; +import { useTheme } from '../../app/contexts/ThemeContext'; +import { SIZES } from '../../constants/theme'; interface ToggleSettingProps extends SwitchProps { settingsTitle: string; diff --git a/components/home/Welcome/Welcome.tsx b/components/home/Welcome.tsx similarity index 87% rename from components/home/Welcome/Welcome.tsx rename to components/home/Welcome.tsx index 4c766b1..2c53e36 100644 --- a/components/home/Welcome/Welcome.tsx +++ b/components/home/Welcome.tsx @@ -1,8 +1,8 @@ import React from 'react' import { Image, Text, View, ViewProps } from 'react-native' import { TouchableOpacity } from 'react-native-gesture-handler' -import { MARGINS, SIZES } from '../../../constants/theme' -import { useTheme } from '../../../app/contexts/ThemeContext' +import { MARGINS, SIZES } from '../../constants/theme' +import { useTheme } from '../../app/contexts/ThemeContext' type WelcomeProps = ViewProps & {name: string, image : any, onPress: () => void | undefined} @@ -28,13 +28,13 @@ function getTimeOfDay(date: Date) : string { export default function Welcome(props: WelcomeProps) { - const {colors} = useTheme() + const { colors } = useTheme(); + const date = new Date() const dateString = formatDate(date) const timeOfDay = getTimeOfDay(date) const onpress = props.onPress - const textcolor = colors.primaryText return ( {dateString} Good {timeOfDay}, {props.name} diff --git a/components/home/expenseItem/expenseItem.tsx b/components/home/expenseItem.tsx similarity index 60% rename from components/home/expenseItem/expenseItem.tsx rename to components/home/expenseItem.tsx index c5146bd..d541d48 100644 --- a/components/home/expenseItem/expenseItem.tsx +++ b/components/home/expenseItem.tsx @@ -1,39 +1,33 @@ import React from 'react'; import { ColorValue, StyleSheet, Text, View } from 'react-native'; -import { SIZES } from '../../../constants/theme'; -import { useTheme } from '../../../app/contexts/ThemeContext'; -import CustomCard from "../../common/CustomCard"; -import { SimpleDate } from '../../../util/SimpleDate'; +import { SIZES } from '../../constants/theme'; +import CustomCard from "../common/CustomCard"; +import { useTheme } from '../../app/contexts/ThemeContext'; -type ISOdateString = string - -export type ExpenseItemProps = {color: ColorValue, category: string, title: string, date: ISOdateString, value : string} +export type ExpenseItemProps = {color: ColorValue, category: string, title: string, date: string, value : string} export default function ExpenseItem(itemProps : ExpenseItemProps) { - const {colors} = useTheme() - const textColor = colors.primaryText - const backgroundColor = colors.containerColor - const date: SimpleDate = new SimpleDate(new Date(itemProps.date)) + const { colors } = useTheme(); return ( - + {itemProps.category} {itemProps.title} {date.format("DD.MM.YYYY")} + color: colors.primaryText + }} numberOfLines={1}>{itemProps.date} - + {itemProps.value} diff --git a/components/index.tsx b/components/index.tsx index 61d8ef7..4d74a43 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -1,7 +1,7 @@ //home -import ExpenseItem from "./home/expenseItem/expenseItem" -import Welcome from "./home/Welcome/Welcome" -import { ToggleSetting, ButtonSetting } from "./home/userSettings/Setting" +import { ButtonSetting, ToggleSetting } from "./home/Setting" +import Welcome from "./home/Welcome" +import ExpenseItem from "./home/expenseItem" //common import LoadingSymbol from "./common/loadingSymbol" @@ -11,13 +11,12 @@ import CustomCard from "./common/CustomCard" //login +import BudgetHeader from "./budget/budgetHeader" import Input from "./login/input" export { - ExpenseItem, Input, + BudgetHeader, ButtonSetting, CustomCard, ExpenseItem, Input, LoadingSymbol, Plus, - SearchBar, Welcome, - ToggleSetting, CustomCard, - ButtonSetting + SearchBar, ToggleSetting, Welcome } diff --git a/components/login/input.tsx b/components/login/input.tsx index 75f2ea4..c6c05aa 100644 --- a/components/login/input.tsx +++ b/components/login/input.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {View, Text, TextInput, StyleSheet} from 'react-native'; -import { NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes'; +import { StyleSheet, Text, TextInput, View } from 'react-native'; import { TextInputEndEditingEventData } from 'react-native/Libraries/Components/TextInput/TextInput'; +import { NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes'; interface InputProps { label? : string, diff --git a/hooks/useFetch.ts b/hooks/useFetch.ts index 790e4f2..496beba 100644 --- a/hooks/useFetch.ts +++ b/hooks/useFetch.ts @@ -1,20 +1,21 @@ +import { Query } from "expo-sqlite"; import { useEffect, useState } from "react"; import { executeQuery } from "../services/database"; -const useFetch = () => { +const useFetch = (query: Query) => { const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState<{[column: string]: any;}[]>([]); const reFetch = () => { setIsLoading(true); - executeQuery("SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid;").then((result) => { + executeQuery(query).then((result) => { if("rows" in result[0]) { setData(result[0]["rows"]); } }).catch((error: any) => { console.error("Fetching data from database has failed: ", error); - }).finally(() => { + }).then(() => { setIsLoading(false); }); } diff --git a/hooks/useThemeColor.ts b/hooks/useThemeColor.ts index c679987..0a6143c 100644 --- a/hooks/useThemeColor.ts +++ b/hooks/useThemeColor.ts @@ -7,7 +7,7 @@ import colors from "../constants/colors"; * @returns */ export function useThemeColor(colorName: keyof typeof colors.light & keyof typeof colors.dark): string { - console.warn("useThemeColor is depreciated. Use useTheme().colors instead") + console.log("useThemeColor is depreciated. Use useTheme().colors instead") const theme = useColorScheme() ?? "light"; return colors[theme][colorName]; } diff --git a/services/database.ts b/services/database.ts index ec1528b..fbb9b6c 100644 --- a/services/database.ts +++ b/services/database.ts @@ -1,4 +1,3 @@ -//created by thschleicher import * as SQLite from "expo-sqlite"; import uuid from "react-native-uuid"; @@ -6,27 +5,26 @@ import uuid from "react-native-uuid"; import { Query } from "expo-sqlite"; import { SimpleDate } from "../util/SimpleDate"; -const db = SQLite.openDatabase("interactive_systeme.db"); +let db: SQLite.SQLiteDatabase; export const initDatabase = async () => { + db = SQLite.openDatabase("interactive_systeme.db"); try { await db.transactionAsync(async (tx: SQLite.SQLTransactionAsync) => { await tx.executeSqlAsync( - "CREATE TABLE IF NOT EXISTS category (guid VARCHAR(36) PRIMARY KEY, name TEXT, color TEXT, type TEXT);" + "CREATE TABLE IF NOT EXISTS category (guid VARCHAR(36) PRIMARY KEY, name TEXT, color TEXT, type TEXT, allocated_amount DOUBLE);" ); await tx.executeSqlAsync( "CREATE TABLE IF NOT EXISTS expense (guid VARCHAR(36) PRIMARY KEY, name TEXT, category_guid VARCHAR(36), datetime DATETIME, amount DOUBLE, FOREIGN KEY (category_guid) REFERENCES category(guid));" ); - console.log("Successfully initialized Tables!"); }); } catch (error) { - - console.log("Error initializing the Tables!"); + console.log("Error initializing the Database!"); throw (error); } }; -export const addCategory = async (name: string, color: string, type: string) => { +export const addCategory = async (name: string, color: string, type: string, allocated_amount: number) => { //needs user input validation for type and color (should be validated by this function) @@ -34,18 +32,17 @@ export const addCategory = async (name: string, color: string, type: string) => try { await db.transactionAsync(async (tx) => { - await tx.executeSqlAsync("INSERT INTO category (guid, name, color, type) VALUES (?, ?, ?, ?);", - [UUID.toString(), name, color, type] + await tx.executeSqlAsync("INSERT INTO category (guid, name, color, type, allocated_amount) VALUES (?, ?, ?, ?, ?);", + [UUID.toString(), name, color, type, allocated_amount] ); }); - console.log("Category added successfully!"); } catch (error) { console.log("Error adding category: ", error); throw error; } } -export const addExpense = async (name: string, category_guid: string,datetime: string, amount: number) => { +export const addExpense = async (name: string, category_guid: string, datetime: string, amount: number) => { //needs user input validation for type and color (should be validated by this function) @@ -56,7 +53,6 @@ export const addExpense = async (name: string, category_guid: string,datetime: s await tx.executeSqlAsync("INSERT INTO expense (guid, name, category_guid, datetime, amount) VALUES (?, ?, ?, ?, ?);", [expenseUUID.toString(), name, category_guid, datetime, amount] ); }); - console.log("Expense added successfully!"); } catch (error) { console.log("Error adding expense: ", error); throw error; @@ -85,15 +81,15 @@ export const deleteExpense = async (guid: string) => { } } -export const executeQuery = async (query: string) => { - const sqliteQuary: Query[] = [{sql: query, args: []}]; - const result = await db.execAsync(sqliteQuary, true); +export const executeQuery = async (query: Query) => { + const result = await db.execAsync([query], true); if("error" in result[0]){ throw result[0].error } - return result; + return result; } + export const deleteExpenses = async () => { try { await db.transactionAsync(async (tx: SQLite.SQLTransactionAsync) => { @@ -125,9 +121,7 @@ export const deleteDatabase = () => { console.log("Error deleting the Database: ", error); throw error; } - }); - console.log("Database Deleted!") } const closeDatabase = async () => { @@ -149,9 +143,9 @@ export const DEV_populateDatabase = async () => { for(let i=0; i < 5; i++){ let random = Math.floor(Math.random() * colors.length); - await addCategory(`Category ${i}`, colors[random], "budget") + await addCategory(`Category ${i}`, colors[random], "budget", 50) } - const result = await executeQuery("SELECT * from category") + const result = await executeQuery({sql:"SELECT * from category", args:[]}) let categories: {[column: string]: any}[]; if("rows" in result[0]){ categories = result[0]["rows"]