Compare commits

..

58 commits
v0.79 ... main

Author SHA1 Message Date
jastornig
8f1753df3f Merge branch '68-documentation' into 'main'
Resolve "Documentation"

Closes #68

See merge request thschleicher/interaktive-systeme!59
2024-01-26 23:22:54 +00:00
jastornig
3ff8127347 Merge branch 'fix-release' into 'main'
missing deps

See merge request thschleicher/interaktive-systeme!60
2024-01-26 23:08:37 +00:00
Jakob Stornig
675ffe97bc missing deps 2024-01-27 00:06:35 +01:00
Ghost_Element
f44a42779f documentation 2024-01-26 23:53:59 +01:00
jastornig
93757d52d7 Merge branch '60-create-enum-for-type-of-category' into 'main'
Resolve "create enum for type of category"

Closes #60

See merge request thschleicher/interaktive-systeme!51
2024-01-25 19:43:07 +00:00
Jakob Stornig
2e0b92d5b8 deleteEverything kept in userSettings 2024-01-25 20:41:19 +01:00
Jakob Stornig
899b50ded4 last changes 2024-01-25 20:35:09 +01:00
Walcher
7a5ace403e made hook to streamline getting totals and expense by type of category 2024-01-25 19:43:29 +01:00
Walcher
85e92c85b4 hold my beer des wird a katastrophe 2024-01-25 19:36:39 +01:00
Walcher
bdc2ca22f6 Refactor category type to use enum 2024-01-25 19:34:44 +01:00
Tocuro
6ca505d505 updated Database.ts to use ENUM for Category Type 2024-01-25 19:32:55 +01:00
Tocuro
114ad92818 ?? 2024-01-25 19:32:55 +01:00
jastornig
305e110da5 Merge branch '67-date-filtering-on-home-screen' into 'main'
Resolve "date filtering on home screen"

Closes #67

See merge request thschleicher/interaktive-systeme!58
2024-01-25 18:28:17 +00:00
jastornig
4ec0f214f7 Merge branch 'main' into '67-date-filtering-on-home-screen'
# Conflicts:
#   components/index.tsx
2024-01-25 18:26:46 +00:00
Jakob Stornig
8149ec234f A few changes:
Auto fill amount category edit
Empty list component
calendar filtering
in category screen, expenses can be added directly
2024-01-25 19:18:15 +01:00
Thomas Schleicher
0edc962bc9 Merge branch '66-graph' into 'main'
Resolve "graph"

Closes #66

See merge request thschleicher/interaktive-systeme!57
2024-01-25 17:50:49 +00:00
thschleicher
0e22e63ce9 graph works + unmount on blur 2024-01-25 18:49:24 +01:00
thschleicher
4b06a217c5 graph works 2024-01-25 18:39:16 +01:00
thschleicher
84dbae49c1 graph working kinda 2024-01-25 17:33:02 +01:00
thschleicher
ab2ef25615 first attempt at graph 2024-01-25 17:20:56 +01:00
jastornig
e0f3cf947c Merge branch '65-filter-on-budget-screen' into 'main'
Resolve "filter on budget screen"

Closes #65

See merge request thschleicher/interaktive-systeme!56
2024-01-25 15:44:07 +00:00
Jakob Stornig
716a41cffd feat: filter by name category 2024-01-25 16:42:43 +01:00
jastornig
79405f318d Merge branch '63-routing-back-refresh' into 'main'
Resolve "Routing Back Refresh"

Closes #63

See merge request thschleicher/interaktive-systeme!54
2024-01-25 15:16:13 +00:00
Thomas Schleicher
62a1267f1e Merge branch '64-delete-cateogry-button-on-edit-screen' into 'main'
Resolve "delete-cateogry-button-on-edit-screen"

Closes #64

See merge request thschleicher/interaktive-systeme!55
2024-01-25 15:15:15 +00:00
Jakob Stornig
c46eec06a5 Fix: refresh 2024-01-25 16:11:04 +01:00
thschleicher
7fecf8c5d9 implemented delete button and fixed routing interaction with db 2024-01-25 16:06:52 +01:00
thschleicher
4ddc76faa4 fixed euro sign on expense item (dont judge me I know this isnt the correct branch) and fixed the async storage so it acctually loads on the info 2024-01-25 15:38:50 +01:00
jastornig
7a77b9ca79 Merge branch 'filter-on-home-screen' into 'main'
feat: filter functionality on Home screen

See merge request thschleicher/interaktive-systeme!53
2024-01-20 16:04:09 +00:00
Jakob Stornig
bc3d243ffc feat: filter functionality on Home screen
Long press on any day to remove filter
2024-01-20 17:01:08 +01:00
jastornig
b27b178308 Merge branch 'fix-navigation' into 'main'
Major refactoring: navigation does not break anymore. the user can now...

See merge request thschleicher/interaktive-systeme!52
2024-01-20 11:03:48 +00:00
Jakob Stornig
1beee68bff Major refactoring: navigation does not break anymore. the user can now navigate between the tabs without loosing context 2024-01-20 11:54:25 +01:00
jastornig
ea64b54de2 Merge branch 'fix-category-search-on-home' into 'main'
fix: category search input cannot be cleared

See merge request thschleicher/interaktive-systeme!50
2024-01-05 21:47:19 +00:00
Jakob Stornig
457b098883 fix: category search input cannot be cleared 2024-01-05 22:44:21 +01:00
jastornig
050446f971 Merge branch 'fix-loading-symbol' into 'main'
Fix loading symbol

See merge request thschleicher/interaktive-systeme!49
2024-01-05 20:35:22 +00:00
jastornig
bb37035e3b Merge branch 'main' into 'fix-loading-symbol'
# Conflicts:
#   app/(tabs)/budget/category.tsx
#   components/common/loadingSymbol.tsx
2024-01-05 20:34:49 +00:00
Jakob Stornig
7dd17798c4 fix last issues 2024-01-05 21:33:13 +01:00
Thomas Schleicher
a59958d618 Merge branch '59-implement-updating-a-category' into 'main'
Resolve "implement updating a category"

Closes #59

See merge request thschleicher/interaktive-systeme!48
2024-01-05 19:56:55 +00:00
Thomas Schleicher
d7bb318c24 Resolve "implement updating a category" 2024-01-05 19:56:55 +00:00
thschleicher
497c556a87 removed "entered" log, fixed loading symbol, implemented category update with editing 2024-01-05 20:53:47 +01:00
jastornig
0ddfe3e606 Update .gitlab-ci.yml file 2024-01-05 18:41:42 +00:00
Jakob Stornig
67cc3d3c29 update ci 2024-01-05 19:20:54 +01:00
jastornig
8ff3435c2b Merge branch '53-loading-simple' into 'main'
Resolve "loading simple"

Closes #53

See merge request thschleicher/interaktive-systeme!40
2024-01-05 16:46:33 +00:00
jastornig
123e358f9e Merge branch '57-implement-and-split-edit-category-from-add-category' into 'main'
Resolve "Implement and Split edit Category from add Category"

Closes #57

See merge request thschleicher/interaktive-systeme!46
2024-01-05 16:45:25 +00:00
Thomas Schleicher
d3b7c61c67 Resolve "Implement and Split edit Category from add Category" 2024-01-05 16:45:25 +00:00
jastornig
bf939fb807 Merge branch '56-edit-expense' into 'main'
Resolve "edit Expense"

Closes #56

See merge request thschleicher/interaktive-systeme!43
2024-01-05 14:37:15 +00:00
Jakob Stornig
5b71fa74b1 feat: edit expense 2024-01-05 15:26:09 +01:00
jastornig
21ea0bfe07 Merge branch '58-stats-page-database-connection' into 'main'
Resolve "stats page, database connection"

Closes #58

See merge request thschleicher/interaktive-systeme!47
2024-01-05 13:56:58 +00:00
Walcher
c0a70a0ed5 finishing up 2024-01-05 14:52:20 +01:00
Walcher
8b68a7b7b0 made it possible to pass an override backgroundcolor to widget 2024-01-05 14:50:33 +01:00
Walcher
7e29e4f016 added "Financial" Advice 2024-01-05 14:43:15 +01:00
Walcher
d9db03af61 added SavingsOverview 2024-01-05 14:32:41 +01:00
Walcher
9544b3eabb added database connection 2024-01-05 14:32:30 +01:00
Walcher
cec3a4d238 add savings mock data to DEV_populateDatabase 2024-01-05 14:23:47 +01:00
Thomas Schleicher
99f3fbcaa7 Merge branch '54-categories-detailed-view-on-press' into 'main'
Resolve "Categories, detailed view on Press"

Closes #54

See merge request thschleicher/interaktive-systeme!45
2024-01-05 12:27:56 +00:00
Thomas Schleicher
412e88d007 Resolve "Categories, detailed view on Press" 2024-01-05 12:27:56 +00:00
Thomas Schleicher
b927b0e8a0 Merge branch 'repair' into 'main'
fixed the budget element and implemented onPress

See merge request thschleicher/interaktive-systeme!44
2024-01-05 10:05:27 +00:00
thschleicher
7606c5d1b8 fixed the budget element and implemented onPress 2024-01-05 10:52:02 +01:00
carol
3f864c8922 circle 2024-01-02 16:31:34 +01:00
51 changed files with 2124 additions and 650 deletions

View file

@ -25,13 +25,15 @@ stages:
lint_job:
stage: lint
image: node
image: "reactnativecommunity/react-native-android:latest"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- npm install
- npx expo install
- npx expo customize tsconfig.json
- npx tsc
cache:
policy: pull-push
@ -49,7 +51,9 @@ build_job:
when: manual
script:
- echo BUILD_JOB_ID=$CI_JOB_ID >> environment.env
- yarn add expo
- npm install -g sharp-cli
- npm install expo
- npx expo customize tsconfig.json
- npx expo prebuild --platform android
- cd android && ./gradlew assembleRelease
- cd $CI_PROJECT_DIR

View file

@ -24,12 +24,12 @@ Our design philosophy was rooted in simplicity and functionality. We aimed to cr
- **Component Development**: Built reusable components for uniformity and efficiency.
- **State Management and Hooks**: Implemented custom hooks for state management, ensuring smooth data flow and component reusability.
- [ ] **Stage 4: Implementation-Phase 2**
- [x] **Stage 4: Implementation-Phase 2**
- **Refining Components**: Enhancing component functionality and aesthetics.
- **Optimization and Debugging**: Identifying and fixing bugs, optimizing code for better performance.
- **Expanding Features**: Implementing additional pages and components to enrich the application.
- [ ] **Stage 5: Final Implementation-Phase**
- [x] **Stage 5: Final Implementation-Phase**
- **UI Touch-Ups**: Final tweaks to the user interface, ensuring a polished look and feel.
- **Finalization**: Wrapping up the project, finalizing drafts, and completing outstanding tasks.

View file

@ -25,9 +25,16 @@ A more convenient way to run Finanzfuchs is to download the prebuilt apk which c
## Usage
- **Adding Expenses**: Press the plus button to add expenses.
- **Removing Expenses**: Go to the budget tab and press the red button in the middle to reset expenses.
- **Login Screen**: Go to the login screen by pressing the profile picture in the top left of the home screen.
- **Adding Expenses(/Savings)**: Press the plus button to add expenses.
- **Removing Expenses(/Savings)**: Go to the certain expense and press it. You will then be directed to the edit expense tab, where you can change the expense as you wish.
- **Adding Categories**: Go to the budget tab and press the plus button to add a new category with custom color.
- **Removing Categories**: Go to a certain category by pressing on the budget tab and choosing one. At the top is a big button with a pencil on it, which when you press it takes you to the edit category tab, where you can customize the category or remove it completely with all expenses it contains.
- **Searching for Expenses, Savings and Categories**: At the Home Screen, the Budget tab or in a certain category you can search for certain expenses, categories or savings by writing its name in the Search bar.
- **Home Screen**: Go to the login screen by pressing the profile picture in the top left of the home screen. Furthermore, at the bottom there are the last expenses, which were added.
- **Calendar**: The calendar has little colored dots under certain days indicating, that at that day an expense has been added. By pressing at a day the list of expenses below it will be sorted out and only showing the expenses which were added at that day.
- **List of Expenses**: The list shows the most recently added expenses which can be searched through and filtered by using the calendar.
- **Profile**: By pressing at the profile picture on the home screen you get to the My Profile tab where you can activate the dark mode, reset the database and sign out of the app.
- **Stats**: Here is a Graph showing an Overview of your Budget and the sums of all expenses and savings.
## Team

View file

@ -5,6 +5,8 @@ export default function _Layout() {
<Stack initialRouteName="index" screenOptions={{headerShown: false}}>
<Stack.Screen name="index"/>
<Stack.Screen name="addCategory" options={{presentation: "modal"}}/>
<Stack.Screen name="editCategory" options={{presentation: "modal"}}/>
<Stack.Screen name="category" options={{headerShown: false, animation: "slide_from_right"}}/>
</Stack>
);
}

View file

@ -1,39 +1,51 @@
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { SafeAreaView, StyleSheet, Text, TextInput, View } from "react-native";
import { CustomColorPicker, NavigationButton, TypeSelectorSwitch } from "../../../components";
import { StyleSheet, Text, TextInput, View } from "react-native";
import { SafeAreaView } from 'react-native-safe-area-context';
import { AutoDecimalInput, CustomColorPicker, NavigationButton, TypeSelectorSwitch } from "../../../components";
import { addCategory } from "../../../services/database";
import { useTheme } from "../../contexts/ThemeContext";
import { CategoryType } from "../../../types/dbItems";
const addCategory = () => {
export default function Page() {
const {colors} = useTheme();
const parameters = useLocalSearchParams();
const [categoryName, setCartegoryName] = useState<string>("Enter Category Name...");
const [categoryColor, setCartegoryColor] = useState<null|string>(null);
const [selectedType, setSelectedType] = useState<string>("expense");
const [categoryName, setCategoryName] = useState<string>("");
const [categoryColor, setCategoryColor] = useState<string>('#' + Math.floor(Math.random()*16777215).toString(16));
const [selectedType, setSelectedType] = useState<CategoryType>(CategoryType.EXPENSE);
const [amount, setAmount] = useState(0);
return (
<SafeAreaView style={[styles.safeAreaViewStyle, {backgroundColor: colors.backgroundColor}]}>
<Text style={[styles.headingTextStyle, {color: colors.primaryText}]}>Category Editor</Text>
<SafeAreaView style={[styles.safeAreaViewStyle, {backgroundColor: colors.backgroundColor}]}>
<Text style={[styles.headingTextStyle, {color: colors.primaryText}]}>Add Category</Text>
<View style={[styles.containerStyle, {backgroundColor: colors.containerColor}]}>
<View style={[styles.textInputViewStyle, {backgroundColor: colors.elementDefaultColor}]}>
<TextInput placeholder={categoryName} placeholderTextColor={colors.secondaryText} style={[styles.textInputStyle, {color: colors.primaryText}]} onChangeText={(newName: string) => {
setCartegoryName(newName);
<TextInput placeholder={"Enter Category Name..."} value={categoryName} placeholderTextColor={colors.secondaryText} style={[styles.textInputStyle, {color: colors.primaryText}]} onChangeText={(newName: string) => {
setCategoryName(newName);
}}/>
</View>
<View style={styles.budgetInput}>
<AutoDecimalInput label={"Allocated:"} onValueChange={(value) => {
setAmount(!Number.isNaN(Number.parseFloat(value)) ? Number.parseFloat(value) : 0);
}}/>
</View>
<TypeSelectorSwitch
currentSelected={selectedType}
handleButtonPress={(type) => {
handleButtonPress={(type: CategoryType) => {
setSelectedType(type);
}}
/>
<View>
<CustomColorPicker currentColor={categoryColor} handleColorChange={(color) => {
setCartegoryColor(color);
<CustomColorPicker color={categoryColor} handleColorChange={(color) => {
setCategoryColor(color);
}}/>
</View>
@ -42,8 +54,7 @@ const addCategory = () => {
router.back();
}}/>
<NavigationButton text="Save" onPress={() => {
console.log("Implement Saving here!");
router.back();
addCategory(categoryName, categoryColor, selectedType, amount).then(() => router.back());
}}/>
</View>
</View>
@ -51,8 +62,6 @@ const addCategory = () => {
);
}
export default addCategory;
const styles = StyleSheet.create({
containerStyle: {
flex: 1,
@ -82,5 +91,9 @@ const styles = StyleSheet.create({
textInputStyle: {
paddingHorizontal: 10,
fontSize: 25,
},
budgetInput: {
marginBottom: 10,
marginHorizontal: 10,
}
});

View file

@ -0,0 +1,132 @@
import { FontAwesome } from "@expo/vector-icons";
import { useRouter, useLocalSearchParams } from "expo-router";
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { ExpenseItem, LoadingSymbol, TextInputBar, EmptyListCompenent, Plus } from "../../../components";
import useFetch from "../../../hooks/useFetch";
import { useTheme } from "../../contexts/ThemeContext";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "expo-router/src/useNavigation";
import { useEffect } from "react";
export default function Page() {
const router = useRouter();
const navigation = useNavigation();
const {colors} = useTheme();
const {category_guid} = useLocalSearchParams();
const {category_amount, category_color, category_name, category_type} = fetchCategoryInformation(category_guid.toString());
const {data, isLoading, reFetch} = useFetch({sql: "SELECT e.guid AS expense_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 FROM expense e JOIN category c ON e.category_guid = c.guid WHERE c.guid = ? ORDER BY expense_datetime desc;", args: [category_guid]});
const handleEditCategory = () => {
router.push({pathname: "/editCategory", params: {category_guid: category_guid, category_color: category_color, category_amount: category_amount, category_name: category_name, category_type: category_type}});
}
const handleBackButton = () => {
router.back();
}
useEffect(() => {
console.log("useEffect called")
const unsubscribe = navigation.addListener("focus", () => {
reFetch();
})
return unsubscribe;
}, [navigation])
return (
<SafeAreaView style={[styles.safeAreaView, {backgroundColor: colors.containerColor}]}>
<Plus onPress={()=> router.push(`/expense/new?category=${category_guid}`)}/>
<TouchableOpacity style={styles.backContainer} onPress={handleBackButton}>
<FontAwesome style={styles.iconBack} name="arrow-left" size={35} color={colors.primaryText}/>
<Text style={[styles.backText, {color: colors.secondaryText}]}>Back</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.categoryEditTouchableOpacity, {backgroundColor: colors.elementDefaultColor}]}
onPress={handleEditCategory}>
<Text style={[styles.editButtonText, {color: colors.primaryText}]}>{category_name}</Text>
<FontAwesome style={styles.iconEdit} name="edit" size={35} color={colors.primaryText}/>
</TouchableOpacity>
<TextInputBar style={styles.searchBar} placeholder="Search..."/>
{isLoading ? (<LoadingSymbol/>) : (
<FlatList style={{marginHorizontal: 10}}
data={data}
renderItem = {({item}) => <ExpenseItem
color={item.category_color}
category={item.category_name}
title={item.expense_name}
date={item.expense_datetime}
value={item.expense_amount}
onPress={()=>router.push(`/expense/${item.expense_guid}`)}
/>}
keyExtractor={item => item.expense_guid}
ItemSeparatorComponent={() => {
return (<View style={styles.itemSeperator}/>);
}}
ListEmptyComponent={EmptyListCompenent}
/>
)}
</SafeAreaView>
);
}
const fetchCategoryInformation = (guid: string) => {
const {data} = useFetch({sql: "SELECT * FROM category WHERE guid = ?", args: [guid]});
let category_name = "";
let category_color = "";
let category_amount = 0;
let category_type = "";
if (data && data[0]) {
if ("name" in data[0]) category_name = data[0].name as string;
if ("color" in data[0]) category_color = data[0].color as string;
if ("allocated_amount" in data[0]) category_amount = data[0].allocated_amount as number;
if ("type" in data[0]) category_type = data[0].type as string;
}
return {category_name, category_color, category_amount, category_type};
}
const styles = StyleSheet.create({
safeAreaView: {
flex: 1,
},
itemSeperator: {
margin: 5,
},
categoryEditTouchableOpacity: {
borderRadius: 10,
flexDirection: "row",
justifyContent: "space-around",
margin: 10,
},
editButtonText: {
fontSize: 30,
padding: 10,
fontWeight: "bold",
},
iconEdit: {
paddingVertical: 10,
paddingRight: 10,
},
searchBar: {
marginHorizontal: 10,
marginBottom:20,
},
iconBack: {
paddingLeft: 10,
paddingRight: 5,
},
backText: {
fontSize: 20,
padding: 5,
},
backContainer: {
flexDirection: "row",
justifyContent: "flex-start",
}
});

View file

@ -0,0 +1,118 @@
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
import { AutoDecimalInput, CustomColorPicker, NavigationButton, TypeSelectorSwitch, Plus } from "../../../components";
import useFetch from "../../../hooks/useFetch";
import { deleteCategory, deleteExpense, updateCategory } from "../../../services/database";
import { useTheme } from "../../contexts/ThemeContext";
import { SafeAreaView } from 'react-native-safe-area-context';
import { CategoryType } from "../../../types/dbItems";
const addCategory = () => {
const {colors} = useTheme();
const {category_guid, category_amount, category_color, category_name, category_type} = useLocalSearchParams();
const [categoryName, setCategoryName] = useState(category_name.toString());
const [categoryColor, setCategoryColor] = useState(category_color.toString());
const [selectedType, setSelectedType] = useState<CategoryType>(category_type === "expense" ? CategoryType.EXPENSE : CategoryType.SAVING);
const [amount, setAmount] = useState(Number.parseFloat(category_amount.toString()));
const {data} = useFetch({sql: "SELECT * FROM expense WHERE category_guid = ?", args: [category_guid.toString()]});
return (
<SafeAreaView style={[styles.safeAreaViewStyle, {backgroundColor: colors.backgroundColor}]}>
<Text style={[styles.headingTextStyle, {color: colors.primaryText}]}>Edit Category</Text>
<View style={[styles.containerStyle, {backgroundColor: colors.containerColor}]}>
<View style={[styles.textInputViewStyle, {backgroundColor: colors.elementDefaultColor}]}>
<TextInput placeholder={"Enter Category Name..."} value={categoryName} placeholderTextColor={colors.secondaryText} style={[styles.textInputStyle, {color: colors.primaryText}]} onChangeText={(newName: string) => {
setCategoryName(newName);
}}/>
</View>
<View style={styles.budgetInput}>
<AutoDecimalInput label={"Allocated:"} initialValue={amount} onValueChange={(value) => {
setAmount(!Number.isNaN(Number.parseFloat(value)) ? Number.parseFloat(value) : 0);
}}/>
</View>
<TypeSelectorSwitch
currentSelected={selectedType}
handleButtonPress={(type) => {
setSelectedType(type);
}}
/>
<View>
<CustomColorPicker color={categoryColor} handleColorChange={(color) => {
setCategoryColor(color);
}}/>
</View>
<View style={styles.deleteButtonView}>
<NavigationButton text={"Delete Cateogry"} onPress={() => {
for (let i = 0; i < data.length;) {
deleteExpense(data[i].guid).then(() => {
i++
});
}
deleteCategory(category_guid.toString()).then(() => {
router.push("/(tabs)/(budget)");
});
}}/>
</View>
<View style={styles.navigationButtonViewStyle}>
<NavigationButton text="Back" onPress={() => {
router.back();
}}/>
<NavigationButton text="Save" onPress={() => {
updateCategory(category_guid.toString(), categoryName, categoryColor, selectedType, amount).then(() => {
router.back();
});
}}/>
</View>
</View>
</SafeAreaView>
);
}
export default addCategory;
const styles = StyleSheet.create({
deleteButtonView: {
},
containerStyle: {
flex: 1,
margin: 10,
borderRadius: 10,
},
safeAreaViewStyle: {
flex: 1,
flexDirection: "column"
},
headingTextStyle: {
fontSize: 40,
fontWeight: "bold",
alignSelf: "center",
marginVertical: 10,
},
navigationButtonViewStyle: {
flexDirection: "row",
justifyContent: "center",
marginBottom: 10,
},
textInputViewStyle: {
borderRadius: 10,
paddingVertical: 10,
margin: 10,
},
textInputStyle: {
paddingHorizontal: 10,
fontSize: 25,
},
budgetInput: {
marginBottom: 10,
marginHorizontal: 10,
}
});

View file

@ -0,0 +1,111 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { router, useNavigation } from 'expo-router';
import React, { useEffect, useMemo, 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, CategoryItem, EmptyListCompenent, LoadingSymbol, Plus, TextInputBar } from '../../../components';
import useFetch from '../../../hooks/useFetch';
import { useTheme } from '../../contexts/ThemeContext';
import { useFocusEffect } from 'expo-router/src/useFocusEffect';
export default function Page() {
const {colors} = useTheme()
const containerColor = colors.containerColor;
const navigation = useNavigation();
const [selectedPage, setSelectedPage] = useState<"expense"|"saving">("expense");
const [searchString, setSearchString] = useState("");
// useEffect(() => {
// console.log("initial effect called")
// 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 category c LEFT JOIN expense e ON e.category_guid = c.guid GROUP BY c.guid", args: []});
useEffect(() => {
reFetch()
}, [selectedPage]);
useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
reFetch();
})
const t = () => {
console.log("unsubscribed")
unsubscribe();
}
return t;
}, [navigation])
const handlePageSelection = (page: "expense" | "saving") => {
setSelectedPage(page);
};
const handleCategoryPress = (item: {[column: string]: any;}) => {
router.push({pathname: "/category", params: {category_guid: item.category_guid, category_name: item.category_name}})
}
const filteredData = useMemo(() => {
return data.filter((item) => {
if(item.category_type !== selectedPage) {
return false
}
return item.category_name.toLowerCase().includes(searchString.toLowerCase());
})
}, [data, searchString, selectedPage]);
return (
<SafeAreaView style={[styles.safeAreaViewStyle, {backgroundColor: containerColor}]}>
<View style={{flex: 1, marginHorizontal: 10}}>
<BudgetHeader selectedPage={selectedPage} handlePageSelection={handlePageSelection}/>
<TextInputBar style={{marginBottom: 20}} value={searchString} onChangeText={setSearchString} placeholder='Search...'></TextInputBar>
<Plus onPress={() => {
router.push({pathname: '/addCategory'});
}}/>
{isLoading ? (<LoadingSymbol/>) : (
<FlatList
data={filteredData}
renderItem = {({item}) => <CategoryItem
category={item.category_name}
allocated_amount={item.allocated_amount ?? 0}
color={item.category_color}
category_guid={item.category_guid}
total_expenses={item.total_expenses ?? 0}
onPress={() => {
handleCategoryPress(item);
}}/>}
keyExtractor={item => item.category_guid}
ItemSeparatorComponent={() => {
return (<View style={styles.itemSeperatorStyle}/>);
}}
ListEmptyComponent={EmptyListCompenent}
/>
)}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeAreaViewStyle: {
flex: 1,
},
itemSeperatorStyle: {
marginVertical: 5,
},
});

View file

@ -24,9 +24,6 @@ export default function _Layout() {
title: "test",
headerShown: false,
}}/>
<Stack.Screen name="addItem" options={{
title: "new Expense",
}}/>
<Stack.Screen name="userSettings" options={{
animation: "slide_from_left",
title: "User Settings",

218
app/(tabs)/(home)/index.tsx Normal file
View file

@ -0,0 +1,218 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native';
import { Calendar } from 'react-native-calendars';
import { FlatList, RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { EmptyListCompenent, ExpenseItem, LoadingSymbol, Plus, TextInputBar, Welcome } from '../../../components';
import useFetch from '../../../hooks/useFetch';
import { useRouter } from "expo-router";
import { addExpense } from "../../../services/database";
import { SimpleDate } from '../../../util/SimpleDate';
import { useTheme } from '../../contexts/ThemeContext';
import { useNavigation } from 'expo-router';
import { Category, Expense } from '../../../types/dbItems';
interface MarkingProps {
dots?:{color:string, selectedColor?:string, key?:string}[];
selected?: boolean;
selectedColor?: string;
}
type MarkedDates = {
[key: string]: MarkingProps;
}
interface ExpenseEntry extends Expense {
category_name?: string;
color?: string;
}
type Filters = {
search?: string;
month?: string;
day?: string;
}
const constructMarkedDates = (data : {[column: string]: any}) => {
let markedDates: MarkedDates = {};
data.forEach((value: any) => {
const dateKey: string = String(value["expense_datetime"]).split(" ")[0]
if(markedDates[dateKey] === undefined){
markedDates[dateKey] = {dots: []}
}
markedDates[dateKey].dots?.push({color: value["category_color"]})
})
return markedDates;
}
export default function Page() {
const { colors, theme } = useTheme()
const navigation = useNavigation();
const router = useRouter();
const [plusShow, setPlusShow] = useState(true);
const prevOffset = useRef(0);
const [filter, setFilter] = useState<Filters>({})
const profile = require("../../../assets/images/profile.jpg")
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>)=>{
const currentOffset = event.nativeEvent.contentOffset.y >= 0 ? event.nativeEvent.contentOffset.y : 0
const isScrollingUp : boolean = currentOffset <= prevOffset.current;
const isTop : boolean = currentOffset === 0
prevOffset.current = currentOffset
setPlusShow(isScrollingUp || isTop)
}
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 ORDER BY expense_datetime desc;", args: []});
const expenses = useMemo<ExpenseEntry[]>(
() => {
console.log("expenses updated")
return data.map((elem) => {
return {
guid: elem["expense_guid"],
name: elem["expense_name"],
amount: elem["expense_amount"],
dateTime: elem["expense_datetime"],
category_name: elem["category_name"],
color: elem["category_color"]}
})
}, [data])
const filteredExpenses = useMemo<ExpenseEntry[]>(
() => {
console.log("filter called")
return expenses.filter((elem) => {
if(filter.month && filter.month.substring(5, 7) !== elem.dateTime?.substring(5, 7)){
return false;
}
if(filter.day && filter.day.substring(8, 10) !== elem.dateTime?.substring(8, 10)){
return false;
}
if(filter.search && !elem.name?.toLowerCase().includes(filter.search.toLowerCase())){
return false;
}
return true;
})
}, [expenses, filter]
)
const expenseDates = useMemo(
() => {
let markedDates = constructMarkedDates(data)
if(filter.day) {
const dateKey: string = String(filter.day).split(" ")[0]
if(markedDates[dateKey] === undefined){
markedDates[dateKey] = {}
}
markedDates[dateKey].selected = true;
markedDates[dateKey].selectedColor = colors.accentColor;
}
return markedDates;
}
, [data, filter.day])
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
console.log("focus event triggered")
reFetch();
});
return unsubscribe;
}, [navigation]);
const hanldeDaySelect = (date: string | undefined) => {
if(filter.day === date)
setFilter({...filter, day: undefined});
else
setFilter({...filter, day: date});
}
const handleMonthSelect = (date: string | undefined) => {
setFilter({...filter, month: date, day: undefined});
}
const handleSearch = (text: string) => {
setFilter({...filter, search: text});
}
console.log("rendered")
return (
<SafeAreaView edges={["left", "right", "top"]} style={[styles.safeAreaViewStyle, {backgroundColor: colors.containerColor}]}>
{plusShow && <Plus onPress={()=>{
router.push("/expense/new");
// 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 && <LoadingSymbol/>} */}
<FlatList
data={filteredExpenses}
ListHeaderComponent={
<>
<Welcome name="My Dude" image={profile} onPress={() => {router.push("/userSettings")}}/>
<Calendar key={theme} maxDate={SimpleDate.now().format("YYYY-MM-DD")} style={{borderRadius: 20, padding:10}} theme={{
dayTextColor: colors.primaryText,
textDisabledColor: colors.secondaryText,
todayTextColor: colors.accentColor,
calendarBackground: colors.containerColor,
arrowColor: colors.accentColor,
monthTextColor: colors.accentColor
}}
markingType='multi-dot'
markedDates={expenseDates}
onDayPress={(date) => {
hanldeDaySelect(date.dateString)
}}
onMonthChange={(date) => {
handleMonthSelect(date.dateString)
}}
/>
<TextInputBar placeholder='Type to Search...' value={filter.search} onChangeText={(text) => handleSearch(text)} style={{marginBottom: 20}}></TextInputBar>
</>
}
renderItem = {({item}) =>
<ExpenseItem
category={item.category_name!}
color={item.color!}
date={item.dateTime!}
title={item.name!}
value={String(item.amount!)}
guid={item.guid!}
onPress={(guid) => {router.push(`/expense/${guid}`)}}
/>}
keyExtractor={item => item.guid!}
ItemSeparatorComponent={() => {
return (<View style={styles.itemSeperatorStyle}/>);
}}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={reFetch}/>
}
onScroll={handleScroll}
scrollEventThrottle={20}
ListEmptyComponent={EmptyListCompenent}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeAreaViewStyle: {
flex: 1,
paddingHorizontal: 10
},
itemSeperatorStyle: {
marginVertical: 5,
}
});

View file

@ -4,7 +4,7 @@ import { SIZES } from '../../../constants/theme'
import { SafeAreaView } from 'react-native-safe-area-context'
import { ButtonSetting, ToggleSetting } from '../../../components'
import { useTheme } from '../../contexts/ThemeContext'
import { deleteExpenses, DEV_populateDatabase } from '../../../services/database'
import { deleteCategories, deleteExpenses, DEV_populateDatabase } from '../../../services/database'
import { useAuth } from '../../contexts/AuthContext'
import { TouchableOpacity } from 'react-native-gesture-handler'
@ -50,17 +50,14 @@ export default function userSettings() {
<View style={styles.settingsContainer}>
<ToggleSetting settingsTitle='Use System Theme' value={systemTheme} onChange={handleSystemTheme}/>
<ToggleSetting settingsTitle='Dark Mode' disabled={systemTheme} onChange={handleDarkMode} value={darkMode}/>
<ButtonSetting settingsTitle='Reset Expenses' onPress={() => {
deleteExpenses().then(() => {
console.log("Expenses Deleted!");
})}}
/>
<ButtonSetting settingsTitle='Populate Database' onPress={() => {
const del = async () => {
await DEV_populateDatabase()
<ButtonSetting settingsTitle='Reset Database' onPress={()=> {
const deleteAll = async () => {
await deleteExpenses();
await deleteCategories();
}
del()
}}/>
deleteAll();
}}
/>
</View>
</View>

View file

@ -0,0 +1,39 @@
import React from 'react';
import { Graph } from '../../../components';
import BudgetOverview from '../../../components/stats/BudgetOverview';
import BudgetRemaining from '../../../components/stats/SavingsOverview';
import SavingsOverview from '../../../components/stats/SavingsOverview';
import Widget from '../../../components/stats/Widget';
import FinancialAdvice from '../../../components/stats/FinancialAdvice';
import BudgetTotal from '../../../components/stats/BudgetTotal';
import { ScrollView } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import DebugMenu from '../../../services/DebugMenu';
export default function Page() {
return (
<SafeAreaView style={{flex:1}}>
<ScrollView>
{/* <DebugMenu/> */}
<Widget title="Budget Overview"/>
<Graph/>
<Widget>
<BudgetOverview/>
</Widget>
<Widget>
<SavingsOverview/>
</Widget>
<Widget>
<BudgetTotal/>
</Widget>
{/* <Widget title='"Financial" Tips' backgroundColor='orange'>
<FinancialAdvice/>
</Widget> */}
</ScrollView>
</SafeAreaView>
);
}

View file

@ -3,9 +3,9 @@ import { StyleSheet } from "react-native";
import { FontAwesome } from "@expo/vector-icons";
import { Redirect } from "expo-router";
import React, { useEffect } from "react";
import { useTheme } from "../contexts/ThemeContext";
import React from "react";
import { useAuth } from "../contexts/AuthContext";
import { useTheme } from "../contexts/ThemeContext";
export default function Layout() {
const {authState} = useAuth()
@ -34,31 +34,27 @@ export default function Layout() {
}
return (
<Tabs sceneContainerStyle={styles.sceneContainer} screenOptions={screenOptions}>
<Tabs.Screen name="budget" options={
<Tabs sceneContainerStyle={styles.sceneContainer} backBehavior="initialRoute" initialRouteName="(home)" screenOptions={screenOptions}>
<Tabs.Screen name="(budget)" options={
{
tabBarLabel: "Budget",
tabBarIcon: ({size, color}) => (
<FontAwesome name="money" size={size} color={color}/>),
unmountOnBlur: true,
href: "(tabs)/budget"
}
}/>
<Tabs.Screen name="home" options={
<Tabs.Screen name="(home)" options={
{
tabBarLabel: "Home",
tabBarIcon: ({size, color}) => (
<FontAwesome name="home" size={size} color={color}/>),
unmountOnBlur: true,
href: "(tabs)/home/"
}
}/>
<Tabs.Screen name="stats/index" options={
<Tabs.Screen name="(stats)/index" options={
{
tabBarLabel: "Stats",
unmountOnBlur: true,
tabBarIcon: ({size, color}) => (
<FontAwesome name="bar-chart" size={size} color={color}/>),
unmountOnBlur: true,
}
}/>
</Tabs>

View file

@ -1,69 +0,0 @@
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, CategoryItem, LoadingSymbol, Plus } from '../../../components';
import useFetch from '../../../hooks/useFetch';
import { useTheme } from '../../contexts/ThemeContext';
export default function Page() {
const {colors} = useTheme()
const containerColor = colors.containerColor;
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);
}
};
return (
<SafeAreaView style={[styles.safeAreaViewStyle, {backgroundColor: containerColor}]}>
<BudgetHeader selectedPage={selectedPage} handlePageSelection={handlePageSelection}/>
<Plus onPress={() => {
router.push({pathname: '/(tabs)/budget/addCategory', params: { guid: '123'}}); //This needs to be changed to a regular push, without parameters
}}/>
{isLoading ? (<LoadingSymbol/>) : (
<FlatList
data={data}
renderItem = {({item}) => <CategoryItem category={item.category_name} allocated_amount={item.allocated_amount ?? 0} color={item.category_color} category_guid={item.category_guid} total_expenses={item.total_expenses ?? 0}/>}
keyExtractor={item => item.category_guid}
ItemSeparatorComponent={() => {
return (<View style={styles.itemSeperatorStyle}/>);
}}
/>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeAreaViewStyle: {
flex: 1,
},
itemSeperatorStyle: {
marginVertical: 5,
},
});

View file

@ -1,135 +0,0 @@
import React, { useRef, useState, useMemo } from 'react';
import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native';
import { Calendar } from 'react-native-calendars';
import { FlatList, RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { ExpenseItem, LoadingSymbol, Plus, TextInputBar, Welcome } from '../../../components';
import useFetch from '../../../hooks/useFetch';
import { addExpense, executeQuery } from "../../../services/database";
import { useAuth } from '../../contexts/AuthContext';
import { useRouter } from "expo-router";
import { SimpleDate } from '../../../util/SimpleDate';
import { useTheme } from '../../contexts/ThemeContext';
interface MarkingProps {
dots?:{color:string, selectedColor?:string, key?:string}[];
}
type MarkedDates = {
[key: string]: MarkingProps;
}
const constructMarkedDates = (data : {[column: string]: any}) => {
console.log("entered")
let markedDates: MarkedDates = {};
data.forEach((value: any) => {
const dateKey: string = String(value["expense_datetime"]).split(" ")[0]
if(markedDates[dateKey] === undefined){
markedDates[dateKey] = {dots: []}
}
markedDates[dateKey].dots?.push({color: value["category_color"]})
})
return markedDates;
}
export default function Page() {
const { colors, theme } = useTheme()
const router = useRouter();
const [plusShow, setPlusShow] = useState(true);
const prevOffset = useRef(0);
const profile = require("../../../assets/images/profile.jpg")
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>)=>{
const currentOffset = event.nativeEvent.contentOffset.y >= 0 ? event.nativeEvent.contentOffset.y : 0
const isScrollingUp : boolean = currentOffset <= prevOffset.current;
const isTop : boolean = currentOffset === 0
prevOffset.current = currentOffset
setPlusShow(isScrollingUp || isTop)
}
const newExpense = async (title: string, category_guid: string, date: string, amount: number) => {
try {
await addExpense(title, category_guid, date, amount);
} catch (error: any) {
console.error("Adding new expense has failed: ", error);
}
}
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 ORDER BY expense_datetime desc;", args: []});
const expenseDates = useMemo(()=>
constructMarkedDates(data)
, [data])
return (
<SafeAreaView edges={["left", "right", "top"]} style={[styles.safeAreaViewStyle, {backgroundColor: colors.containerColor}]}>
{plusShow && <Plus onPress={()=>{
router.push("/(tabs)/home/addItem");
// 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 && <LoadingSymbol/>}
<FlatList
data={data}
ListHeaderComponent={
<>
<Welcome name="My Dude" image={profile} onPress={() => {router.push("/home/userSettings")}}/>
<Calendar key={theme} maxDate={SimpleDate.now().format("YYYY-MM-DD")} style={{borderRadius: 20, padding:10}} theme={{
dayTextColor: colors.primaryText,
textDisabledColor: colors.secondaryText,
todayTextColor: colors.accentColor,
calendarBackground: colors.containerColor,
arrowColor: colors.accentColor,
monthTextColor: colors.accentColor
}}
markingType='multi-dot'
markedDates={expenseDates}
>
</Calendar>
<TextInputBar placeholder='Type to Search...' style={{marginBottom: 20}}></TextInputBar>
</>
}
renderItem = {({item}) => <ExpenseItem category={item.category_name} color={item.category_color} date={item.expense_datetime} title={item.expense_name} value={item.expense_amount}/>}
keyExtractor={item => item.expense_guid}
ItemSeparatorComponent={() => {
return (<View style={styles.itemSeperatorStyle}/>);
}}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={reFetch}/>
}
onScroll={handleScroll}
scrollEventThrottle={20}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeAreaViewStyle: {
flex: 1,
paddingHorizontal: 10
},
itemSeperatorStyle: {
marginVertical: 5,
}
});

View file

@ -1,69 +0,0 @@
import React from 'react';
import { StyleSheet, View, ScrollView } from 'react-native';
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() {
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({
container: {
flex: 1,
backgroundColor: colors.backgroundColor,
marginTop: 50,
alignItems: 'center',
},
});
return (
<View style={styles.container}>
<ScrollView>
<DebugMenu />
<Widget title="Budget Overview" />
<Widget>
<BudgetRemaining budget={budget} spent={spent} />
</Widget>
<Widget text='#TODO Insert Pie-Chart' image={require('../../../assets/images/8b14el.jpg')}/>
<Widget>
<BudgetOverview budget={budget} spent={spent} />
</Widget>
<Widget title="Budget Progress">
<CategoryProgressBarList categories={BudgetData} />
</Widget>
<Widget title="Savings Progress">
<CategoryProgressBarList categories={SavingsData} />
</Widget>
</ScrollView>
</View>
);
}

View file

@ -1,20 +1,37 @@
import { Slot } from 'expo-router';
import { SplashScreen, Stack } from 'expo-router';
import React, { useEffect } from 'react';
import { addCategory, initDatabase } from '../services/database';
import { AuthProvider } from './contexts/AuthContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { useTheme } from './contexts/ThemeContext';
export default function _layout() {
const {colors} = useTheme();
useEffect(() => {
initDatabase();
}, []);
console.log(colors.backgroundColor)
return (
<AuthProvider>
<ThemeProvider>
<Slot />
</ThemeProvider>
</AuthProvider>
<ThemeProvider>
<AuthProvider>
<Stack
screenOptions={{
headerShown: false,
navigationBarHidden: true,
animation: 'none',
contentStyle: {backgroundColor: colors.backgroundColor}
}}
>
<Stack.Screen name="index"
options={{
contentStyle: {backgroundColor: colors.backgroundColor}
}}/>
</Stack>
</AuthProvider>
</ThemeProvider>
)
}

137
app/expense/[expense].tsx Normal file
View file

@ -0,0 +1,137 @@
import { View, Text, Alert, StyleSheet } from 'react-native'
import React, { useEffect, useState } from 'react'
import { router, useLocalSearchParams, useRouter } from 'expo-router'
import useFetch from '../../hooks/useFetch';
import { Category, Expense } from '../../types/dbItems';
import { CategorySelectorModal, AutoDecimalInput, CategorySelector, TextInputBar, DateSelectorButton, RoundedButton } from '../../components';
import colors from '../../constants/colors';
import { addExpense, deleteExpense, executeQuery, updateExpense } from '../../services/database';
import { SimpleDate } from '../../util/SimpleDate';
import DateTimePicker from '@react-native-community/datetimepicker';
import { SIZES } from '../../constants/theme';
import { useTheme } from '../contexts/ThemeContext';
export default function Page() {
const router = useRouter();
const {colors} = useTheme();
const {expense} = useLocalSearchParams();
const {data, isEmptyResult} = useFetch({sql: "SELECT e.guid as e_guid, e.name as e_name, e.datetime as e_datetime, e.amount as e_amount, c.guid as c_guid, c.name as c_name, c.color as c_color FROM expense e INNER JOIN category c on e.category_guid = c.guid WHERE e.guid = ?", args: [expense]});
const [selectedExpense, setSelectedExpense] = useState<Expense|undefined>();
const [selectedCategory, setSelectedCategory] = useState<Category|undefined>();
const [formatedValue, setFormatedValue] = useState<string>("");
const [initialValue, setInitialValue] = useState<number>();
const [selectorModalVisible, setSelecorModalVisible] = useState<boolean>(false);
const [expenseName, setExpenseName] = useState<string>("");
const [datePickerShown, setDatePickerShown] = useState<boolean>(false);
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
useEffect(()=>{
const entry = data[0];
console.log(entry)
if(entry){
console.log(entry)
const extractedExpense: Expense = {name: entry["e_name"], amount: entry["e_amount"], dateTime: entry["e_datetime"], guid: entry["e_guid"]}
const extractedCategory: Category = {name: entry["c_name"], color: entry["c_color"], guid: entry["c_guid"]}
console.log(extractedCategory.color)
setSelectedExpense(extractedExpense);
setSelectedCategory(extractedCategory);
setInitialValue(extractedExpense.amount)
setExpenseName(extractedExpense.name ?? "")
setSelectedDate(extractedExpense.dateTime? new Date(extractedExpense.dateTime) : new Date());
}
}, [data])
const handleValueChange = (formatedValue: string) => {
setFormatedValue(formatedValue);
}
const handleCategorySelect = (category : Category) => {
setSelecorModalVisible(false);
setSelectedCategory(category);
}
const validateInput = ():boolean => {
if(formatedValue == "" || expenseName == "" || selectedCategory === undefined || selectedDate === null){
return false;
}
return true;
}
const submit = () => {
console.log(selectedExpense?.guid)
const insert = async () => {
await updateExpense(selectedExpense!.guid!, expenseName, selectedCategory!.guid!, new SimpleDate(selectedDate).format("YYYY-MM-DD"), Number(formatedValue))
}
if(validateInput()){
insert().then( () => router.back())
}else {
Alert.alert("Invalid input", "One of the Props is not properly defined")
}
}
const handleDelete = () => {
const del = async () => {
await deleteExpense(selectedExpense!.guid!)
router.back();
}
del();
}
return (
<View style={styles.container}>
<CategorySelectorModal visible={selectorModalVisible} onRequestClose={()=>{setSelecorModalVisible(false)}} onCategoryTap={handleCategorySelect}></CategorySelectorModal>
<AutoDecimalInput onValueChange={handleValueChange} label='Amount' initialValue={initialValue}/>
<CategorySelector onPress={()=>{setSelecorModalVisible(true)}} selectedCategory={selectedCategory}/>
<TextInputBar placeholder='Name' onChangeText={(text)=>setExpenseName(text)} value={expenseName}/>
<DateSelectorButton selectedDate={selectedDate} onPress={()=>{setDatePickerShown(true)}}/>
{datePickerShown &&
<DateTimePicker
value={new Date()}
maximumDate={new Date()}
onChange={(event, date)=>{
setDatePickerShown(false);
if(date){
setSelectedDate(date);
}
}}
/>}
<RoundedButton color={colors.elementDefaultColor} style={{padding: 10, marginTop: 40}} onPress={handleDelete}>
<Text style={[styles.submitText, {color: colors.primaryText}]}>Delete Expense</Text>
</RoundedButton>
<RoundedButton color={colors.accentColor} style={styles.save} onPress={submit}>
<Text style={[styles.submitText, {color: colors.primaryText}]}>Save</Text>
</RoundedButton>
</View>
)
}
const styles = StyleSheet.create({
notFound: {
alignItems: "center",
justifyContent: "center"
},
notFoundMessageContainer: {
backgroundColor: "#e37b7b",
padding: 50,
},
save: {
padding: 10,
},
container: {
margin: SIZES.normal,
display: "flex",
gap: 10
},
submitText: {
fontSize: SIZES.large
}
})

40
app/expense/_layout.tsx Normal file
View file

@ -0,0 +1,40 @@
import { StyleSheet, Text, View } from 'react-native'
import { Stack } from 'expo-router'
import React from 'react'
import { useTheme } from '../contexts/ThemeContext'
const _layout = () => {
const {colors} = useTheme();
return (
<Stack
initialRouteName="new"
screenOptions={{
contentStyle: {
backgroundColor:colors.containerColor,
},
headerStyle: {
backgroundColor: colors.containerColor
},
headerTintColor: colors.primaryText
}}>
<Stack.Screen name='new'
options={{
title: "New Expense"
}}
/>
<Stack.Screen name="[expense]"
options={{
headerBackButtonMenuEnabled: true,
headerBackVisible: true,
title: "edit Expense"
}}
getId={(params) => String(Date.now())}
/>
</Stack>
)
}
export default _layout
const styles = StyleSheet.create({})

View file

@ -1,18 +1,20 @@
import { View, Text, StyleSheet, Alert } from 'react-native'
import React, { useRef, useState } from 'react'
import { SIZES } from '../../../constants/theme'
import { useTheme } from '../../contexts/ThemeContext'
import { AutoDecimalInput, CategorySelector, CategorySelectorModal, DateSelectorButton, RoundedButton, TextInputBar } from '../../../components'
import { Category } from '../../../types/dbItems'
import React, { useEffect, useRef, useState } from 'react'
import { SIZES } from '../../constants/theme'
import { useTheme } from '../contexts/ThemeContext'
import { AutoDecimalInput, CategorySelector, CategorySelectorModal, DateSelectorButton, RoundedButton, TextInputBar } from '../../components'
import { Category } from '../../types/dbItems'
import DateTimePicker from '@react-native-community/datetimepicker';
import { addExpense } from '../../../services/database'
import { SimpleDate } from '../../../util/SimpleDate'
import { useRouter } from 'expo-router'
import { addExpense, executeQuery } from '../../services/database'
import { SimpleDate } from '../../util/SimpleDate'
import { useLocalSearchParams, useRouter } from 'expo-router'
export default function AddItem() {
const {colors} = useTheme();
const router = useRouter();
const searchParams = useLocalSearchParams()
const [formatedValue, setFormatedValue] = useState<string>("");
const [selectorModalVisible, setSelecorModalVisible] = useState<boolean>(false);
const [selectedCategory, setSelectedCategory] = useState<Category|undefined>()
@ -41,20 +43,33 @@ export default function AddItem() {
await addExpense(expenseName, selectedCategory?.guid!, new SimpleDate(selectedDate).format("YYYY-MM-DD"), Number(formatedValue))
}
if(validateInput()){
insert();
router.back();
insert().then(() => {
router.back();
})
}else {
Alert.alert("Invalid input", "One of the Props is not properly defined")
}
}
useEffect(()=>{
if(searchParams.category !== undefined){
executeQuery({sql: "SELECT * FROM category WHERE guid = ?", args: [searchParams.category]}).then((result) =>{
if("rows" in result[0]){
const category = result[0]["rows"][0];
setSelectedCategory({name: category["name"], color: category["color"], guid: category["guid"]})
}
//setSelectedCategory({name: category["name"], color: category["color"], guid: category["guid"]})
})
}
}, [])
return (
<View style={styles.container}>
<CategorySelectorModal visible={selectorModalVisible} onRequestClose={()=>{setSelecorModalVisible(false)}} onCategoryTap={handleCategorySelect}></CategorySelectorModal>
<AutoDecimalInput onValueChange={handleValueChange} label='Amount'></AutoDecimalInput>
<AutoDecimalInput onValueChange={handleValueChange} label='Amount'/>
<CategorySelector onPress={()=>{setSelecorModalVisible(true)}} selectedCategory={selectedCategory}/>
<TextInputBar placeholder='Name' onChangeText={(text)=>setExpenseName(text)}/>
<TextInputBar placeholder='Name' value={expenseName} onChangeText={(text)=>setExpenseName(text)}/>
<DateSelectorButton selectedDate={selectedDate} onPress={()=>{setDatePickerShown(true)}}/>
{datePickerShown &&
<DateTimePicker

View file

@ -7,7 +7,7 @@ export default function index() {
const {authState} = useAuth()
return (
<Redirect href={authState?.authenticated ? "/home/" : "/login"}></Redirect>
<Redirect href={authState?.authenticated ? "/(tabs)/(home)" : "/login"}></Redirect>
)
}

View file

@ -1,10 +1,10 @@
import { StyleSheet, Text, TouchableHighlight, View } from "react-native";
import TextInputBar from "../common/TextInputBar";
import { useTheme } from "../../app/contexts/ThemeContext";
import TextInputBar from "../common/TextInputBar";
type BudgetHeaderProperties = {
selectedPage: string,
handlePageSelection: (page: string) => void,
handlePageSelection: (page: "expense" | "saving") => void,
}
type PageSelectorButtonProperties = {
@ -21,20 +21,19 @@ const BudgetHeader = (properties: BudgetHeaderProperties) => {
<View style={styles.containerStyle}>
<PageSelectorButton
label="Expenses"
isSelected={properties.selectedPage === "expenses"}
isSelected={properties.selectedPage === "expense"}
onPress={() => {
properties.handlePageSelection("expenses")
properties.handlePageSelection("expense")
}}
/>
<PageSelectorButton
label="Savings"
isSelected={properties.selectedPage === "savings"}
isSelected={properties.selectedPage === "saving"}
onPress={() => {
properties.handlePageSelection("savings");
properties.handlePageSelection("saving");
}}
/>
</View>
<TextInputBar placeholder='Search...'></TextInputBar>
</>);
}
@ -77,8 +76,11 @@ const styles = StyleSheet.create({
containerStyle: {
flexDirection: "row",
justifyContent: "space-evenly",
marginHorizontal: 20,
marginBottom: 20,
marginHorizontal: 10,
marginTop: 10,
},
searchBarStyle: {
marginBottom: 20,
}
});

View file

@ -1,4 +1,5 @@
import { ColorValue, StyleSheet, Text, View } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { useTheme } from "../../app/contexts/ThemeContext";
import CustomCard from "../common/CustomCard";
@ -8,49 +9,56 @@ export type CategoryItemProps = {
allocated_amount: number,
total_expenses: number,
category_guid: string,
onPress?: () => void,
}
const CategoryItem = (properties: CategoryItemProps) => {
const { colors } = useTheme();
const subText = `${properties.total_expenses} / ${properties.allocated_amount}`;
const subText = `${properties.total_expenses.toFixed(2)} / ${properties.allocated_amount}`;
return (
<CustomCard>
<View style={[styles.colorTipStyle, {backgroundColor: properties.color}]}></View>
<View style={[styles.textViewStyle]}>
<Text style={[styles.categoryNameStyle, {color: colors.primaryText}]}>
{properties.category}
</Text>
<Text style={[styles.subTextStyle, {color: colors.secondaryText}]}>
{subText}
</Text>
</View>
</CustomCard>
<TouchableOpacity onPress={properties.onPress}>
<CustomCard style={styles.customCardStyle}>
<View style={[styles.colorTipStyle, {backgroundColor: properties.color}]}/>
<View style={[styles.textViewStyle]}>
<Text style={[styles.categoryNameStyle, {color: colors.primaryText}]}>
{properties.category}
</Text>
<Text style={[styles.subTextStyle, {color: colors.secondaryText}]}>
{subText}
</Text>
</View>
</CustomCard>
</TouchableOpacity>
);
};
export default CategoryItem;
const styles = StyleSheet.create({
customCardStyle: {
flexDirection: "row",
justifyContent: "space-between",
},
colorTipStyle: {
width: 25,
borderTopLeftRadius: 10,
borderBottomLeftRadius: 10,
width: 25,
borderTopLeftRadius: 10,
borderBottomLeftRadius: 10,
},
textViewStyle: {
flex: 2,
flexDirection: "column",
paddingVertical: 5,
paddingHorizontal: 10,
alignSelf: "stretch",
flex: 2,
flexDirection: "column",
paddingVertical: 5,
paddingHorizontal: 10,
alignSelf: "stretch",
},
categoryNameStyle: {
fontSize: 30,
fontWeight: "bold",
fontSize: 30,
fontWeight: "bold",
},
subTextStyle: {
fontSize: 17.5,
fontSize: 17.5,
}
})
});

View file

@ -2,14 +2,14 @@ import { StyleSheet } from "react-native";
import ColorPicker, { BrightnessSlider, HueSlider, Preview, SaturationSlider, } from "reanimated-color-picker";
export type CustomColorPickerProperties = {
currentColor?: string | null,
color: string,
handleColorChange: (color: string) => void | undefined,
}
const CustomColorPicker = (properties: CustomColorPickerProperties) => {
return (
<ColorPicker
value={properties.currentColor ?? generateRandomColor()}
value={properties.color}
onChange={(color) => {
properties.handleColorChange(color["hex"])
}}
@ -30,10 +30,6 @@ const CustomColorPicker = (properties: CustomColorPickerProperties) => {
);
}
const generateRandomColor = (): string => {
return '#' + Math.floor(Math.random()*16777215).toString(16);
}
export default CustomColorPicker;
const styles = StyleSheet.create({

View file

@ -1,10 +1,11 @@
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { useTheme } from "../../app/contexts/ThemeContext";
import { CategoryType } from "../../types/dbItems";
export type TypeSelectorSwitchProperties = {
handleButtonPress: (type: string) => void,
currentSelected: string,
handleButtonPress: (type: CategoryType) => void,
currentSelected: CategoryType,
}
const TypeSelectorSwitch = (properties: TypeSelectorSwitchProperties) => {
@ -14,17 +15,17 @@ const TypeSelectorSwitch = (properties: TypeSelectorSwitchProperties) => {
<View style={styles.containerStyle}>
<TouchableOpacity
onPress={() => {
properties.handleButtonPress("expense");
properties.handleButtonPress(CategoryType.EXPENSE);
}}
style={[styles.touchableOpacityStyle, properties.currentSelected == "expense" ? {backgroundColor: colors.accentColor} : {backgroundColor: colors.elementDefaultColor}]
style={[styles.touchableOpacityStyle, properties.currentSelected == CategoryType.EXPENSE ? {backgroundColor: colors.accentColor} : {backgroundColor: colors.elementDefaultColor}]
}>
<Text style={[styles.textStyle, {color: colors.primaryText}]}>Expenses</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
properties.handleButtonPress("saving");
onPress={() => {
properties.handleButtonPress(CategoryType.SAVING);
}}
style={[styles.touchableOpacityStyle, properties.currentSelected == "saving" ? {backgroundColor: colors.accentColor} : {backgroundColor: colors.elementDefaultColor}]
style={[styles.touchableOpacityStyle, properties.currentSelected == CategoryType.SAVING ? {backgroundColor: colors.accentColor} : {backgroundColor: colors.elementDefaultColor}]
}>
<Text style={[styles.textStyle, {color: colors.primaryText}]}>Savings</Text>
</TouchableOpacity>

View file

@ -1,5 +1,5 @@
import { View, Text, TouchableOpacity, TextInput, StyleSheet, NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native'
import React, {LegacyRef, MutableRefObject, useRef, useState} from 'react'
import React, {LegacyRef, MutableRefObject, useEffect, useRef, useState} from 'react'
import { SIZES } from '../../constants/theme';
import { useTheme } from '../../app/contexts/ThemeContext';
@ -19,13 +19,21 @@ const formatDecimal = (value: string)=>{
interface AutoDecimalInputProps{
onValueChange?: (formattedValue: string) => void | undefined
label: string,
initialValue? : number
}
const AutoDecimalInput: React.FC<AutoDecimalInputProps> = ({onValueChange, label}) => {
const AutoDecimalInput: React.FC<AutoDecimalInputProps> = ({onValueChange, label, initialValue}) => {
const { colors } = useTheme();
const inputRef = useRef<TextInput>(null);
const [pressedNumbers, setPressedNumbers] = useState<string>("");
const init = () => {
if(initialValue){
const pressedNumber = initialValue.toFixed(2).replace(".", "")
update(pressedNumber);
}
}
const update = (newValues : string) => {
if(onValueChange){
onValueChange(formatDecimal(newValues))
@ -33,6 +41,10 @@ const AutoDecimalInput: React.FC<AutoDecimalInputProps> = ({onValueChange, label
setPressedNumbers(newValues);
}
useEffect(() => {
init()
}, [initialValue])
const handleInput = (e: NativeSyntheticEvent<TextInputKeyPressEventData>)=>{
const pressedKey:string = e.nativeEvent.key
if(Number.isInteger(Number.parseInt(pressedKey))){

View file

@ -40,7 +40,6 @@ export default function CustomCard(props : ViewProps) {
const styles = StyleSheet.create({
container:{
borderRadius: 20,
marginHorizontal: 10,
},
boxShadow: {},
})

View file

@ -0,0 +1,24 @@
import { View, Text, StyleSheet } from 'react-native'
import React from 'react'
import { useTheme } from '../../app/contexts/ThemeContext'
const EmptyListCompenent:React.FC = () => {
const {colors} = useTheme();
return (
<View style={[styles.container, {backgroundColor: colors.backgroundColor}]}>
<Text style={[{fontSize: 20}, {color: colors.primaryText}]}>No matching Data</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 70,
borderRadius: 20,
}
})
export default EmptyListCompenent

View file

@ -7,14 +7,13 @@ import { useTheme } from '../../app/contexts/ThemeContext';
interface SearchBarProps extends ViewProps {
placeholder? : string;
onChangeText? : (text: string) => void | undefined
value?: string
}
export default function TextInputBar(props: SearchBarProps) {
const [isActive, setIsactive] = React.useState(false);
const { colors } = useTheme();
const [text, setText] = useState<string>("");
const textColor = colors
const backgroundColor = colors.elementDefaultColor;
const handleChange = (text:string) : void => {
@ -30,7 +29,7 @@ export default function TextInputBar(props: SearchBarProps) {
if(props.onChangeText){
props.onChangeText(text)
}
setText(text)
}
// cant apply the background color otherwise
@ -43,10 +42,12 @@ export default function TextInputBar(props: SearchBarProps) {
// changed styles.container to containerStyle
return (
<View style={[containerStyle, props.style]}>
<TextInput placeholderTextColor={colors.secondaryText} onChangeText = {handleChange} style={[{fontSize: SIZES.normal, height: "100%", color:colors.primaryText}, styles.TextInput]} autoCorrect={false} keyboardType='default' placeholder={props.placeholder} value={text} onFocus={()=>handleChange(text)} onEndEditing={()=>setIsactive(false)}/>
<TextInput placeholderTextColor={colors.secondaryText} onChangeText = {handleChange} style={[{fontSize: SIZES.normal, height: "100%", color:colors.primaryText}, styles.TextInput]} autoCorrect={false} keyboardType='default' placeholder={props.placeholder} value={props.value} onPressIn={()=>(setIsactive(true))} onEndEditing={()=>setIsactive(false)}/>
{isActive &&
<TouchableOpacity style={styles.cancel} onPress={()=>{handleChange("")}}>
<TouchableOpacity style={styles.cancel} onPress={()=>{
console.log("cancel")
handleChange("")}}>
<AntDesign size={15} name='closecircle' color={colors.primaryText}></AntDesign>
</TouchableOpacity>
}

View file

@ -1,23 +1,43 @@
import { StyleSheet, View } from "react-native";
import React, { useEffect, useRef } from "react";
import { ActivityIndicator, Animated, Easing, StyleSheet, View } from "react-native";
import { useTheme } from "../../app/contexts/ThemeContext";
const LoadingSymbol = () => {
const {colors} = useTheme();
const color = ["blue", "red", "purple", "green", "yellow", "orange"];
const random = Math.floor(Math.random() * color.length);
const spinValue = useRef(new Animated.Value(0)).current;
const styles = StyleSheet.create({
container: {
backgroundColor: color[random],
width: "100%",
height: "100%",
position: "absolute",
}
});
useEffect(() => {
Animated.loop(
Animated.timing(spinValue, {
toValue: 1,
duration: 2000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
}, [spinValue]);
return (
<View style={styles.container}></View>
const styles = StyleSheet.create({
container: {
width: "100%",
height: "100%",
position: "absolute",
justifyContent: "center",
alignItems: "center",
},
loader: {
width: 100,
height: 100,
zIndex: 999,
},
});
return (
<View style={styles.container}>
<ActivityIndicator size="large" color={colors.accentColor} style={styles.loader} />
</View>
);
}
};
export default LoadingSymbol;

View file

@ -1,8 +1,8 @@
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'
import React, { useState } from 'react'
import { useTheme } from '../../../app/contexts/ThemeContext'
import { SIZES } from '../../../constants/theme';
import { Category } from '../../../types/dbItems';
import { useTheme } from '../../app/contexts/ThemeContext'
import { SIZES } from '../../constants/theme';
import { Category } from '../../types/dbItems';
import CategorySelectorModal from './CategorySelectorModal';
interface CategorySelectorProps {

View file

@ -1,11 +1,12 @@
import { Modal, NativeSyntheticEvent, StyleSheet, Text, View, FlatList } from 'react-native'
import React, { useEffect, useMemo, useState } from 'react'
import { Category } from '../../../types/dbItems';
import { useTheme } from '../../../app/contexts/ThemeContext';
import CategoryListItem from '../../common/CategoryListItem';
import { SIZES } from '../../../constants/theme';
import useFetch from '../../../hooks/useFetch';
import TextInputBar from '../../common/TextInputBar';
import { Category } from '../../types/dbItems';
import { useTheme } from '../../app/contexts/ThemeContext';
import CategoryListItem from '../common/CategoryListItem';
import { SIZES } from '../../constants/theme';
import useFetch from '../../hooks/useFetch';
import TextInputBar from '../common/TextInputBar';
import EmptyListCompenent from '../common/EmptyListCompenent';
interface CategorySelectorModalProps{
@ -43,13 +44,13 @@ const CategorySelectorModal: React.FC<CategorySelectorModalProps> = (props : Cat
}, [visible])
return (
<Modal visible={visible} transparent={true} onRequestClose={props.onRequestClose}>
<Modal visible={visible} transparent={true} onRequestClose={props.onRequestClose} animationType='slide'>
<View style={styles.main}>
<View style={[styles.modal, {backgroundColor: colors.containerColor}]}>
<View>
<Text style={[styles.heading, {color: colors.primaryText}]}>{selectMulitple ? "Categories" : "Category"}</Text>
</View>
<TextInputBar placeholder='TypeToSearch' onChangeText={handleSearchText} style={{marginBottom: 10}}></TextInputBar>
<TextInputBar placeholder='TypeToSearch' value={searchtext} onChangeText={handleSearchText} style={{marginBottom: 10}}></TextInputBar>
<FlatList
data={filteredCategories}
keyExtractor={(item) => item.guid!}
@ -57,6 +58,7 @@ const CategorySelectorModal: React.FC<CategorySelectorModalProps> = (props : Cat
ItemSeparatorComponent={() => <View style={styles.itemSeperatorStyle}/>}
ListFooterComponent={() => <View style={styles.itemSeperatorStyle}/>}
keyboardShouldPersistTaps="always"
ListEmptyComponent={EmptyListCompenent}
>
</FlatList>

View file

@ -1,8 +1,8 @@
import { View, Text, StyleSheet, TouchableOpacity, TouchableOpacityProps } from 'react-native'
import React, { useState } from 'react'
import { useTheme } from '../../../app/contexts/ThemeContext';
import { SIZES } from '../../../constants/theme';
import { SimpleDate } from '../../../util/SimpleDate';
import { useTheme } from '../../app/contexts/ThemeContext';
import { SIZES } from '../../constants/theme';
import { SimpleDate } from '../../util/SimpleDate';
import { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes';
interface DateSelectorProps extends ViewProps {

View file

@ -1,37 +1,56 @@
import React from 'react';
import { ColorValue, StyleSheet, Text, View } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { useTheme } from '../../app/contexts/ThemeContext';
import { SIZES } from '../../constants/theme';
import CustomCard from "../common/CustomCard";
import { useTheme } from '../../app/contexts/ThemeContext';
export type ExpenseItemProps = {color: ColorValue, category: string, title: string, date: string, value : string}
//export type ExpenseItemProps = {color: ColorValue, category: string, title: string, date: string, value : string}
interface ExpenseItemProps {
color: ColorValue;
category: string;
title: string;
date: string;
value : string;
guid? : string;
onPress? : (guid?: string) => void
}
export default function ExpenseItem(itemProps : ExpenseItemProps) {
const { colors } = useTheme();
const handlePress = ()=>{
if(itemProps.onPress){
itemProps.onPress(itemProps.guid);
}
}
return (
<CustomCard>
<View style={styles.tile}>
<View style={[styles.colorTip, {backgroundColor: itemProps.color}]}></View>
<View style={[styles.textSection, {backgroundColor: colors.backgroundColor}]}>
<Text style={{
fontSize: SIZES.normal,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.category}</Text>
<Text style={{
fontSize: SIZES.large,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.title}</Text>
<Text style={{
fontSize: SIZES.small,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.date}</Text>
<TouchableOpacity onPress={handlePress}>
<View style={styles.tile}>
<View style={[styles.colorTip, {backgroundColor: itemProps.color}]}></View>
<View style={[styles.textSection, {backgroundColor: colors.backgroundColor}]}>
<Text style={{
fontSize: SIZES.normal,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.category}</Text>
<Text style={{
fontSize: SIZES.large,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.title}</Text>
<Text style={{
fontSize: SIZES.small,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.date}</Text>
</View>
<View style={[styles.valueSection, {backgroundColor: colors.backgroundColor}]}>
<Text style={{
paddingRight: 10,
fontSize: SIZES.xxLarge,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.value + " €"}</Text>
</View>
</View>
<View style={[styles.valueSection, {backgroundColor: colors.backgroundColor}]}>
<Text style={{
fontSize: SIZES.xxLarge,
color: colors.primaryText
}} numberOfLines={1}>{itemProps.value}</Text>
</View>
</View>
</TouchableOpacity>
</CustomCard>
)

View file

@ -4,18 +4,19 @@ import Welcome from "./home/Welcome"
import ExpenseItem from "./home/expenseItem"
//home/addItem
import CategorySelector from "./home/addItem/CategorySelector"
import CategorySelectorModal from "./home/addItem/CategorySelectorModal"
import DateSelectorButton from "./home/addItem/DateSelectorButton"
import CategorySelector from "./expense/CategorySelector"
import CategorySelectorModal from "./expense/CategorySelectorModal"
import DateSelectorButton from "./expense/DateSelectorButton"
//common
import AutoDecimalInput from "./common/AutoDecimalInput"
import CustomCard from "./common/CustomCard"
import RoundedButton from "./common/RoundedButton"
import TextInputBar from "./common/TextInputBar"
import NavigationButton from "./common/button"
import LoadingSymbol from "./common/loadingSymbol"
import Plus from "./common/plus"
import TextInputBar from "./common/TextInputBar"
import AutoDecimalInput from "./common/AutoDecimalInput"
import RoundedButton from "./common/RoundedButton"
import EmptyListCompenent from "./common/EmptyListCompenent"
//login
import BudgetHeader from "./budget/budgetHeader"
@ -26,25 +27,30 @@ import TypeSelectorSwitch from "./budget/typeSelectorSwitch"
//login
import Input from "./login/input"
//stats
import Graph from "./stats/Graph"
export {
AutoDecimalInput,
BudgetHeader,
ButtonSetting,
CustomCard,
ExpenseItem,
Input,
LoadingSymbol,
Plus,
TextInputBar,
ToggleSetting,
Welcome,
AutoDecimalInput,
CategoryItem,
CategorySelector,
CategorySelectorModal,
CustomCard,
CustomColorPicker,
DateSelectorButton,
RoundedButton,
CategoryItem,
TypeSelectorSwitch,
EmptyListCompenent,
ExpenseItem,
Graph,
Input,
LoadingSymbol,
NavigationButton,
CustomColorPicker
Plus,
RoundedButton,
TextInputBar,
ToggleSetting,
TypeSelectorSwitch,
Welcome
}

View file

@ -1,38 +1,56 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import React, { useState, useEffect } from 'react';
import { Text, StyleSheet } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext';
import useFetch from '../../hooks/useFetch';
import { CategoryType } from '../../types/dbItems';
import {useCategoryData} from '../../hooks/useCategoryData';
interface StatsBudgetProps {
spent: number;
budget: number;
const styles = StyleSheet.create({
container: {
margin: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center'
},
text: {
fontSize: 26,
color: `black`
},
boldText: {
fontWeight: 'bold'
},
negativeText: {
color: 'red',
},
positiveText: {
color: 'green',
},
});
interface BudgetTotalProps {
goodColor?: string;
badColor?: string;
}
const BudgetOverview: React.FC<StatsBudgetProps> = ({ spent, budget }) => {
const BudgetTotal: React.FC<BudgetTotalProps> = ({ goodColor = 'green', badColor = 'red' }) => {
const { colors } = useTheme();
const { data, isLoading } = useCategoryData(CategoryType.EXPENSE);
const styles = StyleSheet.create({
container: {
margin: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 26,
color: colors.primaryText,
},
boldText: {
fontWeight: 'bold',
},
});
const { total, expenseTotal } = data;
const remaining = total - expenseTotal;
if (isLoading) {
return <Text>Loading...</Text>;
}
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>
<Text style={[styles.text, { color: colors.primaryText }]}>
<>
You have spent <Text style={[styles.boldText, { color: goodColor }]}>{expenseTotal.toFixed(2)}</Text> out of your Budget of <Text style={[styles.boldText]}>{total.toFixed(2)} </Text>
</>
</Text>
);
};
export default BudgetOverview
export default BudgetTotal;

View file

@ -1,38 +0,0 @@
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;

View file

@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import { Text, StyleSheet } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext';
import useFetch from '../../hooks/useFetch';
import { CategoryType } from '../../types/dbItems';
import {useCategoryData} from '../../hooks/useCategoryData';
const styles = StyleSheet.create({
container: {
margin: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center'
},
text: {
fontSize: 26,
color: `black`
},
boldText: {
fontWeight: 'bold'
},
negativeText: {
color: 'red',
},
positiveText: {
color: 'green',
},
});
interface BudgetTotalProps {
goodColor?: string;
badColor?: string;
}
const BudgetTotal: React.FC<BudgetTotalProps> = ({ goodColor = 'green', badColor = 'red' }) => {
const { colors } = useTheme();
const { data, isLoading } = useCategoryData(CategoryType.EXPENSE);
const { total, expenseTotal } = data;
const remaining = total - expenseTotal;
if (isLoading) {
return <Text>Loading...</Text>;
}
return (
<Text style={[styles.text, { color: colors.primaryText }]}>
{remaining >= 0 ? (
<>
Your remaining overall Budget is <Text style={[styles.boldText, { color: goodColor }]}>{remaining.toFixed(2)}</Text>
</>
) : (
<>
Your Budget is overspent by <Text style={[styles.boldText, { color: badColor }]}>-{Math.abs(remaining).toFixed(2)}</Text>
</>
)}
</Text>
);
};
export default BudgetTotal;

View file

@ -1,7 +1,6 @@
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;

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,30 @@
import { Dimensions, View } from "react-native";
import { PieChart } from "react-native-chart-kit";
import { useTheme } from "../../app/contexts/ThemeContext";
import useFetch from "../../hooks/useFetch";
const Graph = () => {
const {colors} = useTheme();
const {data} = useFetch({sql: "SELECT c.name AS name, c.color AS color, SUM(e.amount) as total FROM category c LEFT JOIN expense e ON e.category_guid = c.guid GROUP BY c.guid", args: []});
const acctual_data = data.map(item => ({...item, legendFontColor: colors.primaryText, legendFontSize: 14}));
return (
<View style={{backgroundColor: colors.backgroundColor, borderRadius: 10, margin: 10}}>
<PieChart
data={acctual_data}
width={Dimensions.get("window").width}
height={240}
chartConfig={{
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
}}
backgroundColor="transparent"
accessor="total"
paddingLeft="15"
avoidFalseZero={true}
/>
</View>
);
}
export default Graph;

View file

@ -1 +0,0 @@
//honestly just fuck graphs

View file

@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import { Text, StyleSheet } from 'react-native';
import { useTheme } from '../../app/contexts/ThemeContext';
import useFetch from '../../hooks/useFetch';
import { CategoryType } from '../../types/dbItems';
import {useCategoryData} from '../../hooks/useCategoryData';
const styles = StyleSheet.create({
container: {
margin: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center'
},
text: {
fontSize: 26,
color: `black`
},
boldText: {
fontWeight: 'bold'
},
negativeText: {
color: 'red',
},
positiveText: {
color: 'green',
},
});
interface BudgetTotalProps {
goodColor?: string;
badColor?: string;
}
const BudgetTotal: React.FC<BudgetTotalProps> = ({ goodColor = 'green', badColor = 'red' }) => {
const { colors } = useTheme();
const { data, isLoading } = useCategoryData(CategoryType.SAVING);
const { total, expenseTotal } = data;
const remaining = total - expenseTotal;
if (isLoading) {
return <Text>Loading...</Text>;
}
return (
<Text style={[styles.text, { color: colors.primaryText }]}>
{remaining >= 0 ? (
<>
You have saved <Text style={[styles.boldText, { color: badColor }]}>{expenseTotal.toFixed(2)}</Text> out of your Goal of <Text style={[styles.boldText]}>{total.toFixed(2)}</Text>
</>
) : (
<>
You have surpassed your Savings Goal of <Text style={[styles.boldText]}>{total.toFixed(2)}</Text> by <Text style={[styles.boldText, { color: goodColor }]}>{Math.abs(remaining).toFixed(2)}</Text>
</>
)}
</Text>
);
};
export default BudgetTotal;

View file

@ -1,5 +1,5 @@
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';
interface WidgetProps {
@ -7,14 +7,18 @@ interface WidgetProps {
text?: string;
children?: ReactNode;
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 actualBackgroundColor = backgroundColor ? backgroundColor : colors.widgetBackgroundColor;
const styles = StyleSheet.create({
widgetContainer: {
backgroundColor: colors.widgetBackgroundColor,
backgroundColor: actualBackgroundColor,
borderColor: colors.widgetBorderColor,
borderRadius: 5,
padding: 16,
@ -44,7 +48,7 @@ const Widget: React.FC<WidgetProps> = ({ title, text, children, image }) => { //
},
imageStyle: {
width: '100%',
height: 200,
height: 500,
borderRadius: 5,
marginBottom: 8,
},

33
hooks/useCategoryData.ts Normal file
View file

@ -0,0 +1,33 @@
import { useState, useEffect } from 'react';
import useFetch from './useFetch';
import { CategoryType } from '../types/dbItems';
export const useCategoryData = (CategoryType: string) => {
const [data, setData] = useState({ total: 0, expenseTotal: 0 });
const [isLoading, setLoading] = useState(true);
const categoryQuery = {
sql: `SELECT SUM(allocated_amount) as total FROM category WHERE type = '${CategoryType.toString()}'`,
args: []
};
const expenseQuery = {
sql: `SELECT SUM(e.amount) as total FROM expense e JOIN category c ON e.category_guid = c.guid WHERE c.type = '${CategoryType.toString()}'`,
args: []
};
const { data: categoryData, isLoading: categoryLoading } = useFetch(categoryQuery);
const { data: expenseData, isLoading: expenseLoading } = useFetch(expenseQuery);
useEffect(() => {
if (categoryData && expenseData) {
setData({
total: categoryData[0]?.total || 0,
expenseTotal: expenseData[0]?.total || 0
});
}
setLoading(categoryLoading || expenseLoading);
}, [categoryData, categoryLoading, expenseData, expenseLoading]);
return { categoryData, expenseData, isLoading, data };
};

View file

@ -4,14 +4,37 @@ import { executeQuery } from "../services/database";
const useFetch = (query: Query) => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<{[column: string]: any;}[]>([]);
const [fetchState, setFetchState] = useState<{
data: {[column: string]: any;}[];
isLoading: boolean;
isEmptyResult: boolean | undefined;
}>({
data: [],
isLoading: false,
isEmptyResult: undefined
});
const setIsLoading = (isLoading: boolean) => {
setFetchState((prevState) => ( {...prevState, isLoading} ));
}
const setData = (data: {[column: string]: any;}[]) => {
setFetchState((prevState) => ( {...prevState, data} ));
}
const setIsEmptyResult = (isEmptyResult: boolean) => {
setFetchState((prevState) => ( {...prevState, isEmptyResult} ));
}
const reFetch = () => {
setIsLoading(true);
executeQuery(query).then((result) => {
if("rows" in result[0]) {
setData(result[0]["rows"]);
if(result[0]["rows"].length == 0){
setIsEmptyResult(true);
}
}
}).catch((error: any) => {
console.error("Fetching data from database has failed: ", error);
@ -24,7 +47,7 @@ const useFetch = (query: Query) => {
reFetch();
}, [])
return {data, isLoading, reFetch};
return {...fetchState, reFetch};
}
export default useFetch;

713
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@
"dependencies": {
"@expo/vector-icons": "^13.0.0",
"@react-native-async-storage/async-storage": "1.18.2",
"@react-native-community/datetimepicker": "7.2.0",
"@react-navigation/native": "^6.0.2",
"expo": "~49.0.15",
"expo-font": "~11.4.0",
@ -35,14 +36,15 @@
"react-dom": "18.2.0",
"react-native": "0.72.6",
"react-native-calendars": "^1.1303.0",
"react-native-chart-kit": "^6.12.0",
"react-native-gesture-handler": "~2.12.0",
"react-native-reanimated": "^3.3.0",
"react-native-reanimated": "~3.3.0",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-svg": "13.9.0",
"react-native-uuid": "^2.0.1",
"react-native-web": "~0.19.6",
"reanimated-color-picker": "^2.4.2",
"@react-native-community/datetimepicker": "7.2.0"
"reanimated-color-picker": "^2.4.2"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View file

@ -2,6 +2,7 @@ import React from 'react';
import { View, Button, Alert } from 'react-native';
import { addCategory, addExpense, deleteExpenses, deleteCategories, DEV_populateDatabase, deleteDatabase } from './database';
import uuid from 'react-native-uuid';
import { CategoryType } from '../types/dbItems';
const randomColors = ["red", "blue", "green", "purple", "yellow"];
@ -46,7 +47,7 @@ const DebugMenu = () => {
const allocated_amount = getRandomNumber();
const type = "expense";
addCategory(name, color, type, allocated_amount)
addCategory(name, color, CategoryType.EXPENSE, allocated_amount)
.then(() => Alert.alert("Category Added", `Name: ${name}, Color: ${color}`))
.catch((error: any) => console.error("Error adding category: ", error));
};

View file

@ -4,15 +4,18 @@ import uuid from "react-native-uuid";
import { Query } from "expo-sqlite";
import { SimpleDate } from "../util/SimpleDate";
import { CategoryType } from "../types/dbItems";
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, allocated_amount DOUBLE);"
"CREATE TABLE IF NOT EXISTS category (guid VARCHAR(36) PRIMARY KEY, name TEXT, color TEXT, type string, 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));"
@ -24,7 +27,19 @@ export const initDatabase = async () => {
}
};
export const addCategory = async (name: string, color: string, type: string, allocated_amount: number) => {
export const updateCategory = async (guid: string, name: string, color: string, CategoryType: CategoryType, allocated_amount: number) => {
try {
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync("UPDATE category SET name = ?, color = ?, type = ?, allocated_amount = ? WHERE guid = ?", [name, color, CategoryType, allocated_amount, guid]);
});
} catch (error) {
console.log("Error updating category: ", error);
throw error;
}
};
export const addCategory = async (name: string, color: string, CategoryType: CategoryType, allocated_amount: number) => {
//needs user input validation for type and color (should be validated by this function)
@ -33,7 +48,7 @@ export const addCategory = async (name: string, color: string, type: string, all
try {
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync("INSERT INTO category (guid, name, color, type, allocated_amount) VALUES (?, ?, ?, ?, ?);",
[UUID.toString(), name, color, type, allocated_amount]
[UUID.toString(), name, color, CategoryType, allocated_amount]
);
});
} catch (error) {
@ -42,6 +57,22 @@ export const addCategory = async (name: string, color: string, type: string, all
}
}
export const updateExpense = async (guid: string, name: string, category_guid: string, datetime: string, amount: number) => {
//needs user input validation for type and color (should be validated by this function)
console.log("update expense called")
try {
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync("UPDATE expense SET name = ?, category_guid = ?, datetime = ?, amount = ? WHERE guid = ?", [name, category_guid, datetime, amount, guid]
);
});
} catch (error) {
console.log("Error updating expense: ", error);
throw error;
}
console.log("update expense finished")
};
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)
@ -143,7 +174,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", 50)
await addCategory(`Category ${i}`, colors[random], CategoryType.EXPENSE, 50)
random = Math.floor(Math.random() * colors.length);
await addCategory(`Category ${i+6}`, colors[random], CategoryType.SAVING, 50)
}
const result = await executeQuery({sql:"SELECT * from category", args:[]})
let categories: {[column: string]: any}[];
@ -160,7 +193,7 @@ export const DEV_populateDatabase = async () => {
let randomDay = Math.floor(Math.random() * 20)
date.setDate(randomDay)
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){
console.error(e)

View file

@ -1,3 +1,7 @@
export enum CategoryType {
SAVING = "saving",
EXPENSE = "expense",
}
export interface Category {
guid? : string;
name? : string;
@ -5,3 +9,11 @@ export interface Category {
type? : "expense" | "saving"
allocatedAmount? : number
}
export interface Expense {
guid? : string;
name? : string;
dateTime? : string;
amount?: number;
category_guid?: string;
}