Compare commits
83 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f1753df3f | ||
|
|
3ff8127347 | ||
|
|
675ffe97bc | ||
|
|
f44a42779f | ||
|
|
93757d52d7 | ||
|
|
2e0b92d5b8 | ||
|
|
899b50ded4 | ||
|
|
7a5ace403e | ||
|
|
85e92c85b4 | ||
|
|
bdc2ca22f6 | ||
|
|
6ca505d505 | ||
|
|
114ad92818 | ||
|
|
305e110da5 | ||
|
|
4ec0f214f7 | ||
|
|
8149ec234f | ||
|
|
0edc962bc9 | ||
|
|
0e22e63ce9 | ||
|
|
4b06a217c5 | ||
|
|
84dbae49c1 | ||
|
|
ab2ef25615 | ||
|
|
e0f3cf947c | ||
|
|
716a41cffd | ||
|
|
79405f318d | ||
|
|
62a1267f1e | ||
|
|
c46eec06a5 | ||
|
|
7fecf8c5d9 | ||
|
|
4ddc76faa4 | ||
|
|
7a77b9ca79 | ||
|
|
bc3d243ffc | ||
|
|
b27b178308 | ||
|
|
1beee68bff | ||
|
|
ea64b54de2 | ||
|
|
457b098883 | ||
|
|
050446f971 | ||
|
|
bb37035e3b | ||
|
|
7dd17798c4 | ||
|
|
a59958d618 | ||
|
|
d7bb318c24 | ||
|
|
497c556a87 | ||
|
|
0ddfe3e606 | ||
|
|
67cc3d3c29 | ||
|
|
8ff3435c2b | ||
|
|
123e358f9e | ||
|
|
d3b7c61c67 | ||
|
|
bf939fb807 | ||
|
|
5b71fa74b1 | ||
|
|
21ea0bfe07 | ||
|
|
c0a70a0ed5 | ||
|
|
8b68a7b7b0 | ||
|
|
7e29e4f016 | ||
|
|
d9db03af61 | ||
|
|
9544b3eabb | ||
|
|
cec3a4d238 | ||
|
|
99f3fbcaa7 | ||
|
|
412e88d007 | ||
|
|
b927b0e8a0 | ||
|
|
7606c5d1b8 | ||
|
|
deda54152b | ||
|
|
404ad5a928 | ||
|
|
afb52998d9 | ||
|
|
c240febec8 | ||
|
|
36679279c1 | ||
|
|
e1efed5b21 | ||
|
|
5876fcaf8e | ||
|
|
93d16fc08a | ||
|
|
0ea9acde38 | ||
|
|
9204d6f235 | ||
|
|
3f864c8922 | ||
|
|
d2837c12b3 | ||
|
|
a51dee6795 | ||
|
|
62e71d1b49 | ||
|
|
e0b99b41f3 | ||
|
|
5ba46cb3b3 | ||
|
|
0240eb2562 | ||
|
|
69610de100 | ||
|
|
645b805aa7 | ||
|
|
e209918232 | ||
|
|
9dceb2e6ae | ||
|
|
b036fb51a1 | ||
|
|
953d2f2a56 | ||
|
|
8ba8f036ef | ||
|
|
91da00a15a | ||
|
|
9de2678922 |
64 changed files with 3413 additions and 528 deletions
|
|
@ -20,17 +20,20 @@
|
||||||
stages:
|
stages:
|
||||||
- lint # List of stages for jobs, and their order of execution
|
- lint # List of stages for jobs, and their order of execution
|
||||||
- build
|
- build
|
||||||
|
- deploy
|
||||||
- release
|
- release
|
||||||
|
|
||||||
lint_job:
|
lint_job:
|
||||||
stage: lint
|
stage: lint
|
||||||
image: node
|
image: "reactnativecommunity/react-native-android:latest"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
when: manual
|
when: manual
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||||
script:
|
script:
|
||||||
- npm install
|
- npm install
|
||||||
|
- npx expo install
|
||||||
|
- npx expo customize tsconfig.json
|
||||||
- npx tsc
|
- npx tsc
|
||||||
cache:
|
cache:
|
||||||
policy: pull-push
|
policy: pull-push
|
||||||
|
|
@ -48,29 +51,60 @@ build_job:
|
||||||
when: manual
|
when: manual
|
||||||
script:
|
script:
|
||||||
- echo BUILD_JOB_ID=$CI_JOB_ID >> environment.env
|
- 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
|
- npx expo prebuild --platform android
|
||||||
- cd android && ./gradlew assembleRelease
|
- cd android && ./gradlew assembleRelease
|
||||||
- cd $CI_PROJECT_DIR
|
- cd $CI_PROJECT_DIR
|
||||||
- cp android/app/build/outputs/apk/release/app-release.apk finanzfuchs-is1-group-g-$CI_COMMIT_REF_NAME.apk
|
- cp android/app/build/outputs/apk/release/app-release.apk finanzfuchs-is1-group-g-v0.$CI_PIPELINE_IID.apk
|
||||||
- echo RELEASE_APK=finanzfuchs-is1-group-g-$CI_COMMIT_REF_NAME.apk >> environment.env
|
- echo RELEASE_APK=finanzfuchs-is1-group-g-v0.$CI_PIPELINE_IID.apk >> environment.env
|
||||||
|
# - echo "hello World" >> finanzfuchs-is1-group-g-v0.$CI_PIPELINE_IID.apk
|
||||||
artifacts:
|
artifacts:
|
||||||
untracked: false
|
untracked: false
|
||||||
name: $CI_PROJECT_NAME-group-g-finanzfuchs-$CI_COMMIT_REF_NAME
|
name: $CI_PROJECT_NAME-group-g-finanzfuchs-$CI_COMMIT_REF_NAME
|
||||||
paths:
|
paths:
|
||||||
- "finanzfuchs-is1-group-g-$CI_COMMIT_REF_NAME.apk"
|
- "finanzfuchs-is1-group-g-v0.$CI_PIPELINE_IID.apk"
|
||||||
reports:
|
reports:
|
||||||
dotenv: environment.env
|
dotenv: environment.env
|
||||||
when: on_success
|
when: on_success
|
||||||
expire_in: 60 days
|
expire_in: 60 days
|
||||||
|
|
||||||
|
deploy_job:
|
||||||
|
stage: deploy
|
||||||
|
image: "alpine:latest"
|
||||||
|
needs:
|
||||||
|
- job: build_job
|
||||||
|
artifacts: true
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"
|
||||||
|
when: on_success
|
||||||
|
script:
|
||||||
|
- apk add curl jq
|
||||||
|
- content=$(curl https://api.dropbox.com/oauth2/token -d grant_type=refresh_token -d refresh_token=$REFRESH_TOKEN -d client_id=$CLIENT_KEY -d client_secret=$CLIENT_SECRET)
|
||||||
|
- token=$(echo $content | jq -r ".access_token")
|
||||||
|
- echo 'Dropbox-API-Arg:' {\"path\"':' \"/release/v0_$CI_PIPELINE_IID/$RELEASE_APK\"} > arg.json
|
||||||
|
- |
|
||||||
|
curl -X POST https://content.dropboxapi.com/2/files/upload --header "Authorization: Bearer $token" --header "Content-Type: application/octet-stream" --header @arg.json --data-binary @"$RELEASE_APK"
|
||||||
|
- echo {\"path\":\"/release/v0_$CI_PIPELINE_IID/$RELEASE_APK\", \"settings\":{\"access\":\"viewer\", \"allow_download\":true}} >> data.json
|
||||||
|
- |
|
||||||
|
content=$(curl -X POST https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings --header "Authorization: Bearer $token" --header "Content-Type: application/json" --data @data.json)
|
||||||
|
- url=$(echo $content | jq -r ".url")
|
||||||
|
- echo APK_URL=$url >> urlEnv.env
|
||||||
|
artifacts:
|
||||||
|
untracked: false
|
||||||
|
when: on_success
|
||||||
|
expire_in: 30 days
|
||||||
|
reports:
|
||||||
|
dotenv: urlEnv.env
|
||||||
|
|
||||||
|
|
||||||
release_job:
|
release_job:
|
||||||
stage: release
|
stage: release
|
||||||
image: "registry.gitlab.com/gitlab-org/release-cli:latest"
|
image: "registry.gitlab.com/gitlab-org/release-cli:latest"
|
||||||
needs:
|
needs:
|
||||||
- job: build_job
|
- job: deploy_job
|
||||||
|
artifacts: true
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"
|
||||||
when: on_success
|
when: on_success
|
||||||
|
|
@ -80,7 +114,9 @@ release_job:
|
||||||
ref: '$CI_COMMIT_SHA'
|
ref: '$CI_COMMIT_SHA'
|
||||||
assets:
|
assets:
|
||||||
links:
|
links:
|
||||||
- name: APK
|
- name: Download APK
|
||||||
url: '${CI_PROJECT_URL}/-/jobs/${BUILD_JOB_ID}/artifacts/file/${RELEASE_APK}'
|
url: $APK_URL
|
||||||
script:
|
script:
|
||||||
- echo "Deploying"
|
- echo deploying
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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.
|
- **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.
|
- **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.
|
- **Refining Components**: Enhancing component functionality and aesthetics.
|
||||||
- **Optimization and Debugging**: Identifying and fixing bugs, optimizing code for better performance.
|
- **Optimization and Debugging**: Identifying and fixing bugs, optimizing code for better performance.
|
||||||
- **Expanding Features**: Implementing additional pages and components to enrich the application.
|
- **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.
|
- **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.
|
- **Finalization**: Wrapping up the project, finalizing drafts, and completing outstanding tasks.
|
||||||
|
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -25,9 +25,16 @@ A more convenient way to run Finanzfuchs is to download the prebuilt apk which c
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- **Adding Expenses**: Press the plus button to add expenses.
|
- **Adding Expenses(/Savings)**: 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.
|
- **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.
|
||||||
- **Login Screen**: Go to the login screen by pressing the profile picture in the top left of the home screen.
|
- **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
|
## Team
|
||||||
|
|
||||||
|
|
|
||||||
12
app/(tabs)/(budget)/_layout.tsx
Normal file
12
app/(tabs)/(budget)/_layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
|
export default function _Layout() {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
app/(tabs)/(budget)/addCategory.tsx
Normal file
99
app/(tabs)/(budget)/addCategory.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
const parameters = useLocalSearchParams();
|
||||||
|
|
||||||
|
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}]}>Add 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:"} onValueChange={(value) => {
|
||||||
|
setAmount(!Number.isNaN(Number.parseFloat(value)) ? Number.parseFloat(value) : 0);
|
||||||
|
}}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TypeSelectorSwitch
|
||||||
|
currentSelected={selectedType}
|
||||||
|
handleButtonPress={(type: CategoryType) => {
|
||||||
|
setSelectedType(type);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<CustomColorPicker color={categoryColor} handleColorChange={(color) => {
|
||||||
|
setCategoryColor(color);
|
||||||
|
}}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.navigationButtonViewStyle}>
|
||||||
|
<NavigationButton text="Back" onPress={() => {
|
||||||
|
router.back();
|
||||||
|
}}/>
|
||||||
|
<NavigationButton text="Save" onPress={() => {
|
||||||
|
addCategory(categoryName, categoryColor, selectedType, amount).then(() => router.back());
|
||||||
|
}}/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
});
|
||||||
132
app/(tabs)/(budget)/category.tsx
Normal file
132
app/(tabs)/(budget)/category.tsx
Normal 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",
|
||||||
|
}
|
||||||
|
});
|
||||||
118
app/(tabs)/(budget)/editCategory.tsx
Normal file
118
app/(tabs)/(budget)/editCategory.tsx
Normal 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,
|
||||||
|
}
|
||||||
|
});
|
||||||
111
app/(tabs)/(budget)/index.tsx
Normal file
111
app/(tabs)/(budget)/index.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -2,7 +2,6 @@ import { Stack } from "expo-router";
|
||||||
|
|
||||||
import { View, Text } from 'react-native'
|
import { View, Text } from 'react-native'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useThemeColor } from "../../../hooks/useThemeColor";
|
|
||||||
import { useTheme } from "../../contexts/ThemeContext";
|
import { useTheme } from "../../contexts/ThemeContext";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,10 +12,10 @@ export default function _Layout() {
|
||||||
initialRouteName="index"
|
initialRouteName="index"
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor:colors.backgroundColor,
|
backgroundColor:colors.containerColor,
|
||||||
},
|
},
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: colors.backgroundColor
|
backgroundColor: colors.containerColor
|
||||||
},
|
},
|
||||||
headerTintColor: colors.primaryText
|
headerTintColor: colors.primaryText
|
||||||
|
|
||||||
|
|
@ -25,7 +24,6 @@ export default function _Layout() {
|
||||||
title: "test",
|
title: "test",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}/>
|
}}/>
|
||||||
<Stack.Screen name="addItem"/>
|
|
||||||
<Stack.Screen name="userSettings" options={{
|
<Stack.Screen name="userSettings" options={{
|
||||||
animation: "slide_from_left",
|
animation: "slide_from_left",
|
||||||
title: "User Settings",
|
title: "User Settings",
|
||||||
218
app/(tabs)/(home)/index.tsx
Normal file
218
app/(tabs)/(home)/index.tsx
Normal 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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,26 +1,20 @@
|
||||||
import { View, Text, StyleSheet, Image, Appearance } from 'react-native'
|
import { View, Text, StyleSheet, Image, Appearance } from 'react-native'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useThemeColor } from '../../../hooks/useThemeColor'
|
|
||||||
import { SIZES } from '../../../constants/theme'
|
import { SIZES } from '../../../constants/theme'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { ButtonSetting, ToggleSetting } from '../../../components'
|
import { ButtonSetting, ToggleSetting } from '../../../components'
|
||||||
import { useTheme } from '../../contexts/ThemeContext'
|
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 { useAuth } from '../../contexts/AuthContext'
|
||||||
import { TouchableOpacity } from 'react-native-gesture-handler'
|
import { TouchableOpacity } from 'react-native-gesture-handler'
|
||||||
|
|
||||||
const generateStyles = (): void => {
|
|
||||||
styles.text = {
|
|
||||||
color: useThemeColor('primaryText')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function userSettings() {
|
export default function userSettings() {
|
||||||
const {onLogout} = useAuth();
|
const {onLogout} = useAuth();
|
||||||
const {theme, colors, isSystemTheme, applyTheme, applySystemTheme} = useTheme();
|
const {theme, colors, isSystemTheme, applyTheme, applySystemTheme} = useTheme();
|
||||||
|
|
||||||
const backgroundColor = useThemeColor("backgroundColor");
|
const backgroundColor = colors.backgroundColor
|
||||||
styles.text = {...styles.text, color: useThemeColor("primaryText")}
|
styles.text = {...styles.text, color: colors.primaryText}
|
||||||
|
|
||||||
const [systemTheme, setSystemTheme] = useState<boolean>(isSystemTheme!)
|
const [systemTheme, setSystemTheme] = useState<boolean>(isSystemTheme!)
|
||||||
const [darkMode, setDarkMode] = useState<boolean>(theme === "dark" ? true : false)
|
const [darkMode, setDarkMode] = useState<boolean>(theme === "dark" ? true : false)
|
||||||
|
|
@ -56,17 +50,14 @@ export default function userSettings() {
|
||||||
<View style={styles.settingsContainer}>
|
<View style={styles.settingsContainer}>
|
||||||
<ToggleSetting settingsTitle='Use System Theme' value={systemTheme} onChange={handleSystemTheme}/>
|
<ToggleSetting settingsTitle='Use System Theme' value={systemTheme} onChange={handleSystemTheme}/>
|
||||||
<ToggleSetting settingsTitle='Dark Mode' disabled={systemTheme} onChange={handleDarkMode} value={darkMode}/>
|
<ToggleSetting settingsTitle='Dark Mode' disabled={systemTheme} onChange={handleDarkMode} value={darkMode}/>
|
||||||
<ButtonSetting settingsTitle='Reset Expenses' onPress={() => {
|
<ButtonSetting settingsTitle='Reset Database' onPress={()=> {
|
||||||
deleteExpenses().then(() => {
|
const deleteAll = async () => {
|
||||||
console.log("Expenses Deleted!");
|
await deleteExpenses();
|
||||||
})}}
|
await deleteCategories();
|
||||||
/>
|
|
||||||
<ButtonSetting settingsTitle='Populate Database' onPress={() => {
|
|
||||||
const del = async () => {
|
|
||||||
await DEV_populateDatabase()
|
|
||||||
}
|
}
|
||||||
del()
|
deleteAll();
|
||||||
}}/>
|
}}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
39
app/(tabs)/(stats)/index.tsx
Normal file
39
app/(tabs)/(stats)/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,32 +3,29 @@ import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
import { Redirect } from "expo-router";
|
import { Redirect } from "expo-router";
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { useThemeColor } from "../../hooks/useThemeColor";
|
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
import { useTheme } from "../contexts/ThemeContext";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
// const selectedColor: string = useThemeColor( "tabIconSelected");
|
|
||||||
// const defaultColor: string = useThemeColor("tabIconDefault");
|
|
||||||
// const backgroundColor: string = useThemeColor("backgroundColor");
|
|
||||||
// const tabBarColor: string = useThemeColor("tabBarColor");
|
|
||||||
const {authState} = useAuth()
|
const {authState} = useAuth()
|
||||||
|
const {colors} = useTheme()
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
sceneContainer: {
|
sceneContainer: {
|
||||||
backgroundColor: useThemeColor("containerColor"),
|
backgroundColor: colors.containerColor,
|
||||||
},
|
},
|
||||||
tabBar: {
|
tabBar: {
|
||||||
backgroundColor: useThemeColor("backgroundColor"),
|
backgroundColor: colors.backgroundColor,
|
||||||
borderTopColor: useThemeColor("backgroundColor"),
|
borderTopColor: colors.backgroundColor
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const screenOptions = {
|
const screenOptions = {
|
||||||
tabBarActiveTintColor: useThemeColor( "tabIconSelected"),
|
tabBarActiveTintColor: colors.tabIconSelected,
|
||||||
tabBarInactiveTintColor: useThemeColor("tabIconDefault"),
|
tabBarInactiveTintColor: colors.tabIconDefault,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarStyle: styles.tabBar,
|
tabBarStyle: styles.tabBar,
|
||||||
|
tabBarHideOnKeyboard: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!authState?.authenticated){
|
if(!authState?.authenticated){
|
||||||
|
|
@ -37,30 +34,27 @@ export default function Layout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs sceneContainerStyle={styles.sceneContainer} screenOptions={screenOptions}>
|
<Tabs sceneContainerStyle={styles.sceneContainer} backBehavior="initialRoute" initialRouteName="(home)" screenOptions={screenOptions}>
|
||||||
<Tabs.Screen name="budget/index" options={
|
<Tabs.Screen name="(budget)" options={
|
||||||
{
|
{
|
||||||
tabBarLabel: "Budget",
|
tabBarLabel: "Budget",
|
||||||
tabBarIcon: ({size, color}) => (
|
tabBarIcon: ({size, color}) => (
|
||||||
<FontAwesome name="money" size={size} color={color}/>),
|
<FontAwesome name="money" size={size} color={color}/>),
|
||||||
unmountOnBlur: true,
|
|
||||||
}
|
}
|
||||||
}/>
|
}/>
|
||||||
<Tabs.Screen name="home" options={
|
<Tabs.Screen name="(home)" options={
|
||||||
{
|
{
|
||||||
tabBarLabel: "Home",
|
tabBarLabel: "Home",
|
||||||
tabBarIcon: ({size, color}) => (
|
tabBarIcon: ({size, color}) => (
|
||||||
<FontAwesome name="home" size={size} color={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",
|
tabBarLabel: "Stats",
|
||||||
|
unmountOnBlur: true,
|
||||||
tabBarIcon: ({size, color}) => (
|
tabBarIcon: ({size, color}) => (
|
||||||
<FontAwesome name="bar-chart" size={size} color={color}/>),
|
<FontAwesome name="bar-chart" size={size} color={color}/>),
|
||||||
unmountOnBlur: true,
|
|
||||||
}
|
}
|
||||||
}/>
|
}/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { SafeAreaView, StyleSheet, Switch, Text } from 'react-native';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
switch: {
|
|
||||||
transform: [{ scaleX: 1.3 }, { scaleY: 1.3 }],
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: "red",
|
|
||||||
fontSize: 40,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// const {data, isLoading, reFetch} = useFetch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView>
|
|
||||||
<Text style={styles.text}>Hallo wo bin ich?!</Text>
|
|
||||||
<Switch style={styles.switch}/>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { View, Text } from 'react-native'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default function addItem() {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Text>addItem</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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 } from 'react-native-gesture-handler';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { ExpenseItem, LoadingSymbol, Plus, SearchBar, Welcome } from '../../../components';
|
|
||||||
import useFetch from '../../../hooks/useFetch';
|
|
||||||
import { useThemeColor } from "../../../hooks/useThemeColor";
|
|
||||||
import { addExpense } 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()
|
|
||||||
|
|
||||||
//Styles
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
safeAreaViewStyle: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: colors.backgroundColor
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: colors.primaryText,
|
|
||||||
fontSize: 70,
|
|
||||||
fontWeight: "bold"
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
color: "red",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const [plusShow, setPlusShow] = useState(true);
|
|
||||||
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();
|
|
||||||
|
|
||||||
const expenseDates = useMemo(()=>
|
|
||||||
constructMarkedDates(data)
|
|
||||||
, [data])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView edges={["left", "right", "top"]} style={styles.safeAreaViewStyle}>
|
|
||||||
{plusShow && <Plus onPress={()=>{
|
|
||||||
// router.push("/(tabs)/home/addItem");
|
|
||||||
newExpense("Test Title", "3b33b8ac-5fc1-43e5-81fc-cf61628861f7", "69.69.1234", 100).then(() => {
|
|
||||||
reFetch();
|
|
||||||
});
|
|
||||||
}}/>}
|
|
||||||
|
|
||||||
{isLoading && <LoadingSymbol></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={{margin: 10, 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>
|
|
||||||
<SearchBar placeholder='Type to Search...'></SearchBar>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
renderItem = {({item}) => <ExpenseItem category={item.category_name} color={item.category_color} date={item.expense_datetime} title={item.expense_name} value={"10,00$"}/>}
|
|
||||||
keyExtractor={item => item.expense_guid}
|
|
||||||
ItemSeparatorComponent={()=><View style={{marginVertical: 5}}></View>}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
scrollEventThrottle={20}
|
|
||||||
>
|
|
||||||
</FlatList>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { StyleSheet, Text, View } from 'react-native';
|
|
||||||
import { deleteExpenses } from '../../../services/database';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 40,
|
|
||||||
color: "red",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.text} onPress={() => {
|
|
||||||
deleteExpenses().then(() => {
|
|
||||||
console.log("Expenses Deleted!");
|
|
||||||
})
|
|
||||||
}}>Reset Expenses</Text>
|
|
||||||
</View>);
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +1,37 @@
|
||||||
import { Slot } from 'expo-router';
|
import { SplashScreen, Stack } from 'expo-router';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { addCategory, initDatabase } from '../services/database';
|
import { addCategory, initDatabase } from '../services/database';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
|
import { useTheme } from './contexts/ThemeContext';
|
||||||
|
|
||||||
|
|
||||||
export default function _layout() {
|
export default function _layout() {
|
||||||
|
const {colors} = useTheme();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initDatabase();
|
initDatabase();
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
console.log(colors.backgroundColor)
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<ThemeProvider>
|
||||||
<ThemeProvider>
|
<AuthProvider>
|
||||||
<Slot />
|
<Stack
|
||||||
</ThemeProvider>
|
screenOptions={{
|
||||||
</AuthProvider>
|
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
137
app/expense/[expense].tsx
Normal 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
40
app/expense/_layout.tsx
Normal 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({})
|
||||||
108
app/expense/new.tsx
Normal file
108
app/expense/new.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { View, Text, StyleSheet, Alert } from 'react-native'
|
||||||
|
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, 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>()
|
||||||
|
const [expenseName, setExpenseName] = useState<string>("");
|
||||||
|
const [datePickerShown, setDatePickerShown] = useState<boolean>(false);
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date())
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
const insert = async () => {
|
||||||
|
await addExpense(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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'/>
|
||||||
|
<CategorySelector onPress={()=>{setSelecorModalVisible(true)}} selectedCategory={selectedCategory}/>
|
||||||
|
<TextInputBar placeholder='Name' value={expenseName} onChangeText={(text)=>setExpenseName(text)}/>
|
||||||
|
<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.accentColor} style={styles.save} onPress={submit}>
|
||||||
|
<Text style={[styles.submitText, {color: colors.primaryText}]}>Save</Text>
|
||||||
|
</RoundedButton>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
save: {
|
||||||
|
marginTop: 40,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
margin: SIZES.normal,
|
||||||
|
display: "flex",
|
||||||
|
gap: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
submitText: {
|
||||||
|
fontSize: SIZES.large
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
@ -7,7 +7,7 @@ export default function index() {
|
||||||
const {authState} = useAuth()
|
const {authState} = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Redirect href={authState?.authenticated ? "/home/" : "/login"}></Redirect>
|
<Redirect href={authState?.authenticated ? "/(tabs)/(home)" : "/login"}></Redirect>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -3,16 +3,17 @@ import { Redirect } from 'expo-router';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button, SafeAreaView } from 'react-native';
|
import { Button, SafeAreaView } from 'react-native';
|
||||||
import { Input } from '../components';
|
import { Input } from '../components';
|
||||||
import { useThemeColor } from '../hooks/useThemeColor';
|
import { useTheme } from "./contexts/ThemeContext";
|
||||||
import { useAuth } from './contexts/AuthContext';
|
import { useAuth } from './contexts/AuthContext';
|
||||||
|
|
||||||
export default function login() {
|
export default function login() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
const {authState, onLogin} = useAuth()
|
const {authState, onLogin} = useAuth()
|
||||||
const backgroundColor = useThemeColor("backgroundColor")
|
const {colors} = useTheme()
|
||||||
const textColor = useThemeColor("primaryText");
|
const backgroundColor = colors.backgroundColor;
|
||||||
const elementDefaultColor = useThemeColor("elementDefaultColor")
|
const textColor = colors.primaryText
|
||||||
|
const elementDefaultColor = colors.elementDefaultColor
|
||||||
|
|
||||||
|
|
||||||
// const {authState, onLogin} = useAuth();
|
// const {authState, onLogin} = useAuth();
|
||||||
|
|
|
||||||
BIN
assets/images/8b14el.jpg
Normal file
BIN
assets/images/8b14el.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
|
|
@ -4,6 +4,7 @@ module.exports = function (api) {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
plugins: [
|
plugins: [
|
||||||
'expo-router/babel',
|
'expo-router/babel',
|
||||||
|
'react-native-reanimated/plugin',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
86
components/budget/budgetHeader.tsx
Normal file
86
components/budget/budgetHeader.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
||||||
|
import { useTheme } from "../../app/contexts/ThemeContext";
|
||||||
|
import TextInputBar from "../common/TextInputBar";
|
||||||
|
|
||||||
|
type BudgetHeaderProperties = {
|
||||||
|
selectedPage: string,
|
||||||
|
handlePageSelection: (page: "expense" | "saving") => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageSelectorButtonProperties = {
|
||||||
|
isSelected: boolean,
|
||||||
|
onPress: () => void,
|
||||||
|
label: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetHeader = (properties: BudgetHeaderProperties) => {
|
||||||
|
const {colors} = useTheme();
|
||||||
|
const backgroundColor = colors.backgroundColor;
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<View style={styles.containerStyle}>
|
||||||
|
<PageSelectorButton
|
||||||
|
label="Expenses"
|
||||||
|
isSelected={properties.selectedPage === "expense"}
|
||||||
|
onPress={() => {
|
||||||
|
properties.handlePageSelection("expense")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PageSelectorButton
|
||||||
|
label="Savings"
|
||||||
|
isSelected={properties.selectedPage === "saving"}
|
||||||
|
onPress={() => {
|
||||||
|
properties.handlePageSelection("saving");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageSelectorButton = (properties: PageSelectorButtonProperties) => {
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
const primaryTextColor = colors.primaryText;
|
||||||
|
const secondaryTextColor = colors.secondaryText;
|
||||||
|
const elementSelectedColor = colors.elementSelectedColor;
|
||||||
|
const elementDefaultColor = colors.elementDefaultColor;
|
||||||
|
const accentColor = colors.accentColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableHighlight
|
||||||
|
underlayColor={elementDefaultColor}
|
||||||
|
onPress={properties.onPress}
|
||||||
|
style={[styles.headerContainerStyle, properties.isSelected ? {backgroundColor: accentColor} : {backgroundColor: elementDefaultColor}]}>
|
||||||
|
<Text
|
||||||
|
style={[styles.headerTextStyle, properties.isSelected ? {color: primaryTextColor} : {color: secondaryTextColor}]}>
|
||||||
|
{properties.label}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
</TouchableHighlight>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BudgetHeader;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
headerContainerStyle: {
|
||||||
|
width: "50%",
|
||||||
|
borderRadius: 10,
|
||||||
|
marginHorizontal: 30,
|
||||||
|
},
|
||||||
|
headerTextStyle: {
|
||||||
|
fontSize: 30,
|
||||||
|
textAlign: "center",
|
||||||
|
textAlignVertical: "center",
|
||||||
|
},
|
||||||
|
containerStyle: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-evenly",
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
searchBarStyle: {
|
||||||
|
marginBottom: 20,
|
||||||
|
}
|
||||||
|
});
|
||||||
64
components/budget/categoryItem.tsx
Normal file
64
components/budget/categoryItem.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export type CategoryItemProps = {
|
||||||
|
category: string,
|
||||||
|
color: ColorValue,
|
||||||
|
allocated_amount: number,
|
||||||
|
total_expenses: number,
|
||||||
|
category_guid: string,
|
||||||
|
onPress?: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryItem = (properties: CategoryItemProps) => {
|
||||||
|
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const subText = `${properties.total_expenses.toFixed(2)} / ${properties.allocated_amount} €`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
textViewStyle: {
|
||||||
|
flex: 2,
|
||||||
|
flexDirection: "column",
|
||||||
|
paddingVertical: 5,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
alignSelf: "stretch",
|
||||||
|
},
|
||||||
|
categoryNameStyle: {
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
subTextStyle: {
|
||||||
|
fontSize: 17.5,
|
||||||
|
}
|
||||||
|
});
|
||||||
47
components/budget/customColorPicker.tsx
Normal file
47
components/budget/customColorPicker.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
import ColorPicker, { BrightnessSlider, HueSlider, Preview, SaturationSlider, } from "reanimated-color-picker";
|
||||||
|
|
||||||
|
export type CustomColorPickerProperties = {
|
||||||
|
color: string,
|
||||||
|
handleColorChange: (color: string) => void | undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomColorPicker = (properties: CustomColorPickerProperties) => {
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
value={properties.color}
|
||||||
|
onChange={(color) => {
|
||||||
|
properties.handleColorChange(color["hex"])
|
||||||
|
}}
|
||||||
|
style={styles.colorPickerStyle}
|
||||||
|
sliderThickness={30}
|
||||||
|
thumbSize={40}
|
||||||
|
thumbShape= "circle">
|
||||||
|
<Preview
|
||||||
|
style={[styles.previewStyle]}
|
||||||
|
textStyle={{ fontSize: 18 }}
|
||||||
|
colorFormat="hex"
|
||||||
|
hideInitialColor/>
|
||||||
|
|
||||||
|
<HueSlider style={[styles.sliderStyle]}/>
|
||||||
|
<BrightnessSlider style={[styles.sliderStyle]}/>
|
||||||
|
<SaturationSlider style={[styles.sliderStyle]}/>
|
||||||
|
</ColorPicker>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomColorPicker;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
colorPickerStyle: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
sliderStyle: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
previewStyle: {
|
||||||
|
margin: 10,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 10,
|
||||||
|
}
|
||||||
|
});
|
||||||
54
components/budget/typeSelectorSwitch.tsx
Normal file
54
components/budget/typeSelectorSwitch.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||||
|
import { useTheme } from "../../app/contexts/ThemeContext";
|
||||||
|
import { CategoryType } from "../../types/dbItems";
|
||||||
|
|
||||||
|
export type TypeSelectorSwitchProperties = {
|
||||||
|
handleButtonPress: (type: CategoryType) => void,
|
||||||
|
currentSelected: CategoryType,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypeSelectorSwitch = (properties: TypeSelectorSwitchProperties) => {
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.containerStyle}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
properties.handleButtonPress(CategoryType.EXPENSE);
|
||||||
|
}}
|
||||||
|
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(CategoryType.SAVING);
|
||||||
|
}}
|
||||||
|
style={[styles.touchableOpacityStyle, properties.currentSelected == CategoryType.SAVING ? {backgroundColor: colors.accentColor} : {backgroundColor: colors.elementDefaultColor}]
|
||||||
|
}>
|
||||||
|
<Text style={[styles.textStyle, {color: colors.primaryText}]}>Savings</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypeSelectorSwitch;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
containerStyle: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
touchableOpacityStyle: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
textStyle: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
fontSize: 25,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingVertical: 5,
|
||||||
|
}
|
||||||
|
});
|
||||||
106
components/common/AutoDecimalInput.tsx
Normal file
106
components/common/AutoDecimalInput.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { View, Text, TouchableOpacity, TextInput, StyleSheet, NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native'
|
||||||
|
import React, {LegacyRef, MutableRefObject, useEffect, useRef, useState} from 'react'
|
||||||
|
import { SIZES } from '../../constants/theme';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
const formatDecimal = (value: string)=>{
|
||||||
|
switch(value.length){
|
||||||
|
case 0:
|
||||||
|
return "";
|
||||||
|
case 1:
|
||||||
|
return "0.0"+value
|
||||||
|
case 2:
|
||||||
|
return "0."+value
|
||||||
|
default:
|
||||||
|
return value.substring(0, value.length - 2) + "." + value.substring(value.length - 2, value.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoDecimalInputProps{
|
||||||
|
onValueChange?: (formattedValue: string) => void | undefined
|
||||||
|
label: string,
|
||||||
|
initialValue? : number
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
setPressedNumbers(newValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init()
|
||||||
|
}, [initialValue])
|
||||||
|
|
||||||
|
const handleInput = (e: NativeSyntheticEvent<TextInputKeyPressEventData>)=>{
|
||||||
|
const pressedKey:string = e.nativeEvent.key
|
||||||
|
if(Number.isInteger(Number.parseInt(pressedKey))){
|
||||||
|
if(pressedNumbers.length === 0 && pressedKey === "0"){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
update(pressedNumbers + pressedKey)
|
||||||
|
}else if(pressedKey === "Backspace"){
|
||||||
|
update(pressedNumbers.substring(0, pressedNumbers.length - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TouchableOpacity activeOpacity={1} style={[styles.inputContainer, {backgroundColor: colors.elementDefaultColor}]} onPress={()=>{
|
||||||
|
if(inputRef.current)
|
||||||
|
inputRef.current.focus()
|
||||||
|
}}>
|
||||||
|
<Text style={[styles.text, {color: colors.primaryText}]}>{label}</Text>
|
||||||
|
<View style={styles.currencyWrapper}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.text, {color: colors.primaryText}]}
|
||||||
|
keyboardType='number-pad'
|
||||||
|
ref={inputRef}
|
||||||
|
onKeyPress={handleInput}
|
||||||
|
placeholder='0.00'
|
||||||
|
textAlign='right'
|
||||||
|
placeholderTextColor={colors.secondaryText}
|
||||||
|
value={formatDecimal(pressedNumbers)}
|
||||||
|
/>
|
||||||
|
<Text style={styles.currency}>EUR</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
inputContainer: {
|
||||||
|
minHeight: 50,
|
||||||
|
borderRadius: 20,
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
text:{
|
||||||
|
fontSize: SIZES.large,
|
||||||
|
marginHorizontal: 15,
|
||||||
|
},
|
||||||
|
currency: {
|
||||||
|
fontSize: SIZES.normal,
|
||||||
|
color: "#007acc",
|
||||||
|
marginRight: 15,
|
||||||
|
},
|
||||||
|
currencyWrapper: {
|
||||||
|
flexDirection:"row",
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: "center"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AutoDecimalInput;
|
||||||
66
components/common/CategoryListItem.tsx
Normal file
66
components/common/CategoryListItem.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { StyleSheet, Text, View, Pressable } from 'react-native'
|
||||||
|
import React from 'react'
|
||||||
|
import { Category } from '../../types/dbItems'
|
||||||
|
import CustomCard from './CustomCard';
|
||||||
|
import { SIZES } from '../../constants/theme';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface CategoryListItemProps{
|
||||||
|
category: Category;
|
||||||
|
onPress?: (category: Category) =>void | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const CategoryListItem: React.FC<CategoryListItemProps> = (props: CategoryListItemProps) => {
|
||||||
|
const {category, onPress} = props;
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
const handlePress = ()=>{
|
||||||
|
if(onPress){
|
||||||
|
onPress(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable onPress={handlePress}>
|
||||||
|
<View style={[styles.tile, {backgroundColor: colors.backgroundColor}]}>
|
||||||
|
<View style={[styles.colorTip, {backgroundColor: category.color}]}/>
|
||||||
|
<View style={[styles.textWrapper]}>
|
||||||
|
<Text style={[styles.text, {color: colors.primaryText}]}>{category.name ?? "#noData#"}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.tileTail}/>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryListItem
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tile: {
|
||||||
|
height: 60,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
borderRadius: 20
|
||||||
|
},
|
||||||
|
colorTip:{
|
||||||
|
height: "100%",
|
||||||
|
width: 30,
|
||||||
|
borderTopLeftRadius:20,
|
||||||
|
borderBottomLeftRadius: 20
|
||||||
|
},
|
||||||
|
textWrapper: {
|
||||||
|
|
||||||
|
},
|
||||||
|
tileTail:{
|
||||||
|
height: "100%",
|
||||||
|
width: 30,
|
||||||
|
borderTopRightRadius:20,
|
||||||
|
borderBottomRightRadius: 20
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: SIZES.large,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Platform, StyleSheet, View } from 'react-native'
|
import { Platform, StyleSheet, View } from 'react-native'
|
||||||
import { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes'
|
import { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes'
|
||||||
import { useThemeColor } from '../../hooks/useThemeColor'
|
import { useTheme } from '../../app/contexts/ThemeContext'
|
||||||
|
|
||||||
function generateBoxShadowStyle(
|
function generateBoxShadowStyle(
|
||||||
xOffset: number,
|
xOffset: number,
|
||||||
|
|
@ -18,7 +18,7 @@ function generateBoxShadowStyle(
|
||||||
shadowOffset : {width: xOffset, height: yOffset},
|
shadowOffset : {width: xOffset, height: yOffset},
|
||||||
shadowOpacity,
|
shadowOpacity,
|
||||||
shadowRadius,
|
shadowRadius,
|
||||||
backgroundColor: useThemeColor("backgroundColor")
|
backgroundColor: useTheme().colors.backgroundColor
|
||||||
}
|
}
|
||||||
}else if (Platform.OS === 'android'){
|
}else if (Platform.OS === 'android'){
|
||||||
styles.boxShadow = {
|
styles.boxShadow = {
|
||||||
|
|
@ -39,11 +39,7 @@ export default function CustomCard(props : ViewProps) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container:{
|
container:{
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "stretch",
|
|
||||||
alignContent: "space-between",
|
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
marginHorizontal: 10,
|
|
||||||
},
|
},
|
||||||
boxShadow: {},
|
boxShadow: {},
|
||||||
})
|
})
|
||||||
24
components/common/EmptyListCompenent.tsx
Normal file
24
components/common/EmptyListCompenent.tsx
Normal 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
|
||||||
32
components/common/RoundedButton.tsx
Normal file
32
components/common/RoundedButton.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { StyleSheet, Text, View, ViewProps, TouchableOpacity } from 'react-native'
|
||||||
|
import React from 'react'
|
||||||
|
import { SIZES } from '../../constants/theme';
|
||||||
|
|
||||||
|
interface RoundedButtonProps extends ViewProps{
|
||||||
|
onPress?: ()=> void | undefined;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoundedButton: React.FC<RoundedButtonProps> = (props: RoundedButtonProps) => {
|
||||||
|
const {onPress, color, style, ...restProps} = props;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={onPress}>
|
||||||
|
<View style={[{backgroundColor: color}, styles.btn, style]}{...restProps}>
|
||||||
|
{restProps.children}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RoundedButton
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
btn:{
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 80
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: SIZES.normal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
import { AntDesign } from '@expo/vector-icons';
|
import { AntDesign } from '@expo/vector-icons';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { StyleSheet, TextInput, TouchableOpacity, View, ViewProps } from 'react-native';
|
import { StyleSheet, TextInput, TouchableOpacity, View, ViewProps } from 'react-native';
|
||||||
import { SIZES } from '../../constants/theme';
|
import { SIZES } from '../../constants/theme';
|
||||||
import { useThemeColor } from '../../hooks/useThemeColor';
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
type SearchBarProps = {placeholder: string} & ViewProps
|
interface SearchBarProps extends ViewProps {
|
||||||
|
placeholder? : string;
|
||||||
|
onChangeText? : (text: string) => void | undefined
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function SearchBar(props: SearchBarProps) {
|
export default function TextInputBar(props: SearchBarProps) {
|
||||||
const [isActive, setIsactive] = React.useState(false);
|
const [isActive, setIsactive] = React.useState(false);
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
const textColor = useThemeColor("interactiveText")
|
const backgroundColor = colors.elementDefaultColor;
|
||||||
const backgroundColor = useThemeColor("elementDefaultColor");
|
|
||||||
|
|
||||||
const handleChange = (text:string) : void => {
|
const handleChange = (text:string) : void => {
|
||||||
if(text !== ""){
|
if(text !== ""){
|
||||||
|
|
@ -22,6 +26,10 @@ export default function SearchBar(props: SearchBarProps) {
|
||||||
setIsactive(false)
|
setIsactive(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(props.onChangeText){
|
||||||
|
props.onChangeText(text)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cant apply the background color otherwise
|
// cant apply the background color otherwise
|
||||||
|
|
@ -33,12 +41,14 @@ export default function SearchBar(props: SearchBarProps) {
|
||||||
//TODO: Handle textCancel
|
//TODO: Handle textCancel
|
||||||
// changed styles.container to containerStyle
|
// changed styles.container to containerStyle
|
||||||
return (
|
return (
|
||||||
<View style={containerStyle}>
|
<View style={[containerStyle, props.style]}>
|
||||||
<TextInput onChangeText = {handleChange} style={[{fontSize: SIZES.normal, height: "100%"}, styles.TextInput]} autoCorrect={false} keyboardType='web-search' placeholder={props.placeholder}></TextInput>
|
<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 &&
|
{isActive &&
|
||||||
<TouchableOpacity style={styles.cancel}>
|
<TouchableOpacity style={styles.cancel} onPress={()=>{
|
||||||
<AntDesign size={15} name='closecircle'></AntDesign>
|
console.log("cancel")
|
||||||
|
handleChange("")}}>
|
||||||
|
<AntDesign size={15} name='closecircle' color={colors.primaryText}></AntDesign>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -47,8 +57,6 @@ export default function SearchBar(props: SearchBarProps) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
marginHorizontal: 10,
|
|
||||||
marginBottom: 20,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
39
components/common/button.tsx
Normal file
39
components/common/button.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
import { StyleSheet, Text, TouchableHighlight } from "react-native";
|
||||||
|
import { useTheme } from "../../app/contexts/ThemeContext";
|
||||||
|
|
||||||
|
export type NavigationButtonProperties = {
|
||||||
|
onPress?: () => void | undefined,
|
||||||
|
text: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigationButton = (properties: NavigationButtonProperties) => {
|
||||||
|
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableHighlight
|
||||||
|
onPress={properties.onPress}
|
||||||
|
underlayColor={colors.elementSelectedColor}
|
||||||
|
style={[styles.touchableHighlightStyle, {backgroundColor: colors.elementDefaultColor}]}>
|
||||||
|
|
||||||
|
<Text style={[styles.buttonTextStyle, {color: colors.primaryText}]}>{properties.text}</Text>
|
||||||
|
</TouchableHighlight>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavigationButton;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
touchableHighlightStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
marginVertical: 10,
|
||||||
|
marginHorizontal: 15,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 5,
|
||||||
|
},
|
||||||
|
buttonTextStyle: {
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 30,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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 LoadingSymbol = () => {
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
const color = ["blue", "red", "purple", "green", "yellow", "orange"];
|
const spinValue = useRef(new Animated.Value(0)).current;
|
||||||
const random = Math.floor(Math.random() * color.length);
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
useEffect(() => {
|
||||||
container: {
|
Animated.loop(
|
||||||
backgroundColor: color[random],
|
Animated.timing(spinValue, {
|
||||||
width: "100%",
|
toValue: 1,
|
||||||
height: "100%",
|
duration: 2000,
|
||||||
position: "absolute",
|
easing: Easing.linear,
|
||||||
}
|
useNativeDriver: true,
|
||||||
});
|
})
|
||||||
|
).start();
|
||||||
|
}, [spinValue]);
|
||||||
|
|
||||||
return (
|
const styles = StyleSheet.create({
|
||||||
<View style={styles.container}></View>
|
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;
|
||||||
export default LoadingSymbol;
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { AntDesign } from '@expo/vector-icons'
|
import { AntDesign } from '@expo/vector-icons'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, TouchableOpacity, ViewProps } from 'react-native'
|
import { StyleSheet, TouchableOpacity, ViewProps } from 'react-native'
|
||||||
import { useThemeColor } from '../../hooks/useThemeColor'
|
import { useTheme } from '../../app/contexts/ThemeContext'
|
||||||
|
|
||||||
type PlusProps = ViewProps & {onPress? : ()=> void | undefined}
|
type PlusProps = ViewProps & {onPress? : ()=> void | undefined}
|
||||||
|
|
||||||
const Plus = (props : PlusProps) => {
|
const Plus = (props : PlusProps) => {
|
||||||
const accentColor = useThemeColor("accentColor");
|
const {colors} = useTheme()
|
||||||
const primaryText = useThemeColor("primaryText");
|
const accentColor = colors.accentColor;
|
||||||
|
const primaryText = colors.primaryText;
|
||||||
|
|
||||||
const style = StyleSheet.create({
|
const style = StyleSheet.create({
|
||||||
plus:{
|
plus:{
|
||||||
|
|
|
||||||
60
components/expense/CategorySelector.tsx
Normal file
60
components/expense/CategorySelector.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
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 CategorySelectorModal from './CategorySelectorModal';
|
||||||
|
|
||||||
|
interface CategorySelectorProps {
|
||||||
|
onPress? : () => void | undefined;
|
||||||
|
selectedCategory? : Category;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategorySelector: React.FC<CategorySelectorProps> = (props : CategorySelectorProps) => {
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity style={[styles.tile, {backgroundColor: colors.backgroundColor}]} onPress={props.onPress}>
|
||||||
|
<View style={[styles.colorTip, {backgroundColor: props.selectedCategory?.color}]}>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
<View style={[styles.textWrapper]}>
|
||||||
|
<Text style={[styles.text, {color: colors.primaryText}]}>{props.selectedCategory?.name ?? "Tap to select Categroy"}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.tileTail}></View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategorySelector
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tile: {
|
||||||
|
height: 60,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
borderRadius: 20
|
||||||
|
},
|
||||||
|
colorTip:{
|
||||||
|
height: "100%",
|
||||||
|
width: 30,
|
||||||
|
borderTopLeftRadius:20,
|
||||||
|
borderBottomLeftRadius: 20
|
||||||
|
},
|
||||||
|
textWrapper: {
|
||||||
|
|
||||||
|
},
|
||||||
|
tileTail:{
|
||||||
|
height: "100%",
|
||||||
|
width: 30,
|
||||||
|
borderTopRightRadius:20,
|
||||||
|
borderBottomRightRadius: 20
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: SIZES.large,
|
||||||
|
}
|
||||||
|
})
|
||||||
95
components/expense/CategorySelectorModal.tsx
Normal file
95
components/expense/CategorySelectorModal.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
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 EmptyListCompenent from '../common/EmptyListCompenent';
|
||||||
|
|
||||||
|
|
||||||
|
interface CategorySelectorModalProps{
|
||||||
|
visible: boolean;
|
||||||
|
onCategoryTap?: (category : Category) => void | undefined;
|
||||||
|
selectMulitple?: boolean;
|
||||||
|
onRequestClose?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: select Multiple
|
||||||
|
|
||||||
|
const CategorySelectorModal: React.FC<CategorySelectorModalProps> = (props : CategorySelectorModalProps) => {
|
||||||
|
const {visible, onCategoryTap, selectMulitple} = props;
|
||||||
|
const {data, reFetch} = useFetch({sql: "SELECT * FROM category;", args:[]});
|
||||||
|
const {colors} = useTheme();
|
||||||
|
const [searchtext, setSearchtext] = useState<string>("");
|
||||||
|
|
||||||
|
const handleSearchText = (text : string) => {
|
||||||
|
setSearchtext(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = useMemo<Category[]>(()=>{
|
||||||
|
return data.map((elem) => {
|
||||||
|
return {name: elem["name"], color: elem["color"], guid: elem["guid"]}
|
||||||
|
})
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const filteredCategories = categories.filter((category) => category.name?.toLowerCase().includes(searchtext.toLowerCase()))
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(visible){
|
||||||
|
//reFetch(); Uncomment if newly added categories do not appear
|
||||||
|
handleSearchText("");
|
||||||
|
}
|
||||||
|
}, [visible])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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' value={searchtext} onChangeText={handleSearchText} style={{marginBottom: 10}}></TextInputBar>
|
||||||
|
<FlatList
|
||||||
|
data={filteredCategories}
|
||||||
|
keyExtractor={(item) => item.guid!}
|
||||||
|
renderItem={({item})=> <CategoryListItem category={item} onPress={onCategoryTap}/>}
|
||||||
|
ItemSeparatorComponent={() => <View style={styles.itemSeperatorStyle}/>}
|
||||||
|
ListFooterComponent={() => <View style={styles.itemSeperatorStyle}/>}
|
||||||
|
keyboardShouldPersistTaps="always"
|
||||||
|
ListEmptyComponent={EmptyListCompenent}
|
||||||
|
>
|
||||||
|
</FlatList>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategorySelectorModal
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
main: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
height: "70%",
|
||||||
|
width: "90%",
|
||||||
|
top: "10%",
|
||||||
|
borderRadius: 30,
|
||||||
|
paddingVertical: 20,
|
||||||
|
paddingHorizontal: 20
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
fontSize: SIZES.xlarge,
|
||||||
|
fontWeight: "bold"
|
||||||
|
},
|
||||||
|
itemSeperatorStyle: {
|
||||||
|
paddingBottom: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
38
components/expense/DateSelectorButton.tsx
Normal file
38
components/expense/DateSelectorButton.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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 { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes';
|
||||||
|
|
||||||
|
interface DateSelectorProps extends ViewProps {
|
||||||
|
onPress?: ()=>void | undefined;
|
||||||
|
selectedDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateSelectorButton:React.FC<DateSelectorProps> = (props: DateSelectorProps) => {
|
||||||
|
const {onPress, selectedDate, ...restProps} = props;
|
||||||
|
const {colors} = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={onPress} {...restProps} style={[styles.inputContainer, {backgroundColor: colors.elementDefaultColor}]}>
|
||||||
|
<Text style={[styles.text, {color: colors.primaryText}]}>Date:</Text>
|
||||||
|
<Text style={[styles.text, {color: colors.primaryText}]}>{new SimpleDate(selectedDate).format("DD.MM.YYYY")}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
inputContainer: {
|
||||||
|
minHeight: 50,
|
||||||
|
borderRadius: 20,
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
text:{
|
||||||
|
fontSize: SIZES.large,
|
||||||
|
marginHorizontal: 15,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DateSelectorButton
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { View, Text, StyleSheet, Switch, SwitchProps, useColorScheme, TouchableOpacityProps, TouchableOpacity, ViewProps } from 'react-native'
|
import React from 'react';
|
||||||
import React from 'react'
|
import { StyleSheet, Switch, SwitchProps, Text, TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
|
||||||
import { SIZES } from '../../../constants/theme'
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
import { useThemeColor } from '../../../hooks/useThemeColor';
|
import { SIZES } from '../../constants/theme';
|
||||||
import { CustomCard } from "../../"
|
|
||||||
import { useTheme } from '../../../app/contexts/ThemeContext';
|
|
||||||
|
|
||||||
interface ToggleSettingProps extends SwitchProps {
|
interface ToggleSettingProps extends SwitchProps {
|
||||||
settingsTitle: string;
|
settingsTitle: string;
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Image, Text, View, ViewProps } from 'react-native'
|
import { Image, Text, View, ViewProps } from 'react-native'
|
||||||
import { TouchableOpacity } from 'react-native-gesture-handler'
|
import { TouchableOpacity } from 'react-native-gesture-handler'
|
||||||
import { MARGINS, SIZES } from '../../../constants/theme'
|
import { MARGINS, SIZES } from '../../constants/theme'
|
||||||
import { useThemeColor } from '../../../hooks/useThemeColor'
|
import { useTheme } from '../../app/contexts/ThemeContext'
|
||||||
import { useTheme } from '../../../app/contexts/ThemeContext'
|
|
||||||
|
|
||||||
type WelcomeProps = ViewProps & {name: string, image : any, onPress: () => void | undefined}
|
type WelcomeProps = ViewProps & {name: string, image : any, onPress: () => void | undefined}
|
||||||
|
|
||||||
|
|
@ -29,14 +28,13 @@ function getTimeOfDay(date: Date) : string {
|
||||||
|
|
||||||
|
|
||||||
export default function Welcome(props: WelcomeProps) {
|
export default function Welcome(props: WelcomeProps) {
|
||||||
const {colors} = useTheme()
|
const { colors } = useTheme();
|
||||||
|
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const dateString = formatDate(date)
|
const dateString = formatDate(date)
|
||||||
const timeOfDay = getTimeOfDay(date)
|
const timeOfDay = getTimeOfDay(date)
|
||||||
const onpress = props.onPress
|
const onpress = props.onPress
|
||||||
|
|
||||||
const textcolor = colors.primaryText
|
|
||||||
//const backgroundColor: string = useThemeColor("backgroundColor")
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View style={{
|
||||||
|
|
@ -59,7 +57,7 @@ export default function Welcome(props: WelcomeProps) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
fontSize: SIZES.xlarge,
|
fontSize: SIZES.xlarge,
|
||||||
color: textcolor
|
color: colors.primaryText
|
||||||
}}>{dateString}</Text>
|
}}>{dateString}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{
|
<View style={{
|
||||||
|
|
@ -67,7 +65,7 @@ export default function Welcome(props: WelcomeProps) {
|
||||||
}}>
|
}}>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
fontSize: SIZES.xlarge,
|
fontSize: SIZES.xlarge,
|
||||||
color: textcolor
|
color: colors.primaryText
|
||||||
}}>Good {timeOfDay}, {props.name}</Text>
|
}}>Good {timeOfDay}, {props.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
86
components/home/expenseItem.tsx
Normal file
86
components/home/expenseItem.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
//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>
|
||||||
|
<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>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</CustomCard>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
colorTip: {
|
||||||
|
width: 20,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderBottomLeftRadius: 20,
|
||||||
|
},
|
||||||
|
|
||||||
|
tile: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "stretch",
|
||||||
|
alignContent: "space-between",
|
||||||
|
},
|
||||||
|
|
||||||
|
textSection: {
|
||||||
|
flexDirection: "column",
|
||||||
|
alignContent: "space-between",
|
||||||
|
alignItems:"flex-start",
|
||||||
|
paddingLeft: 10,
|
||||||
|
flex:1,
|
||||||
|
alignSelf: "stretch",
|
||||||
|
paddingVertical: 5
|
||||||
|
},
|
||||||
|
valueSection: {
|
||||||
|
justifyContent:"center",
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
borderBottomRightRadius: 20,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { ColorValue, StyleSheet, Text, View } from 'react-native';
|
|
||||||
import { SIZES } from '../../../constants/theme';
|
|
||||||
import { useThemeColor } from '../../../hooks/useThemeColor';
|
|
||||||
import { useTheme } from '../../../app/contexts/ThemeContext';
|
|
||||||
import CustomCard from "../../common/CustomCard";
|
|
||||||
import { SimpleDate } from '../../../util/SimpleDate';
|
|
||||||
|
|
||||||
type ISOdateString = string
|
|
||||||
|
|
||||||
export type ExpenseItemProps = {color: ColorValue, category: string, title: string, date: ISOdateString, value : string}
|
|
||||||
export default function ExpenseItem(itemProps : ExpenseItemProps) {
|
|
||||||
const {colors} = useTheme()
|
|
||||||
const textColor = colors.primaryText
|
|
||||||
const backgroundColor = colors.containerColor
|
|
||||||
const date: SimpleDate = new SimpleDate(new Date(itemProps.date))
|
|
||||||
return (
|
|
||||||
<CustomCard>
|
|
||||||
<View style={[styles.colorTip, {backgroundColor: itemProps.color}]}></View>
|
|
||||||
<View style={[styles.textSection, {backgroundColor: backgroundColor}]}>
|
|
||||||
<Text style={{
|
|
||||||
fontSize: SIZES.normal,
|
|
||||||
color: textColor
|
|
||||||
}} numberOfLines={1}>{itemProps.category}</Text>
|
|
||||||
<Text style={{
|
|
||||||
fontSize: SIZES.large,
|
|
||||||
color: textColor
|
|
||||||
}} numberOfLines={1}>{itemProps.title}</Text>
|
|
||||||
<Text style={{
|
|
||||||
fontSize: SIZES.small,
|
|
||||||
color: textColor
|
|
||||||
}} numberOfLines={1}>{date.format("DD.MM.YYYY")}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.valueSection, {backgroundColor: backgroundColor}]}>
|
|
||||||
<Text style={{
|
|
||||||
fontSize: SIZES.xxLarge,
|
|
||||||
color: textColor
|
|
||||||
}} numberOfLines={1}>{itemProps.value}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
</CustomCard>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
colorTip: {
|
|
||||||
width: 20,
|
|
||||||
borderTopLeftRadius: 20,
|
|
||||||
borderBottomLeftRadius: 20,
|
|
||||||
},
|
|
||||||
|
|
||||||
textSection: {
|
|
||||||
flexDirection: "column",
|
|
||||||
alignContent: "space-between",
|
|
||||||
alignItems:"flex-start",
|
|
||||||
paddingLeft: 10,
|
|
||||||
flex:1,
|
|
||||||
alignSelf: "stretch",
|
|
||||||
paddingVertical: 5
|
|
||||||
},
|
|
||||||
valueSection: {
|
|
||||||
justifyContent:"center",
|
|
||||||
borderTopRightRadius: 20,
|
|
||||||
borderBottomRightRadius: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,23 +1,56 @@
|
||||||
//home
|
//home
|
||||||
import ExpenseItem from "./home/expenseItem/expenseItem"
|
import { ButtonSetting, ToggleSetting } from "./home/Setting"
|
||||||
import Welcome from "./home/Welcome/Welcome"
|
import Welcome from "./home/Welcome"
|
||||||
import { ToggleSetting, ButtonSetting } from "./home/userSettings/Setting"
|
import ExpenseItem from "./home/expenseItem"
|
||||||
|
|
||||||
|
//home/addItem
|
||||||
|
import CategorySelector from "./expense/CategorySelector"
|
||||||
|
import CategorySelectorModal from "./expense/CategorySelectorModal"
|
||||||
|
import DateSelectorButton from "./expense/DateSelectorButton"
|
||||||
|
|
||||||
//common
|
//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 LoadingSymbol from "./common/loadingSymbol"
|
||||||
import Plus from "./common/plus"
|
import Plus from "./common/plus"
|
||||||
import SearchBar from "./common/SearchBar"
|
import EmptyListCompenent from "./common/EmptyListCompenent"
|
||||||
import CustomCard from "./common/CustomCard"
|
|
||||||
|
|
||||||
|
//login
|
||||||
|
import BudgetHeader from "./budget/budgetHeader"
|
||||||
|
import CategoryItem from "./budget/categoryItem"
|
||||||
|
import CustomColorPicker from "./budget/customColorPicker"
|
||||||
|
import TypeSelectorSwitch from "./budget/typeSelectorSwitch"
|
||||||
|
|
||||||
//login
|
//login
|
||||||
import Input from "./login/input"
|
import Input from "./login/input"
|
||||||
|
|
||||||
|
//stats
|
||||||
|
import Graph from "./stats/Graph"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ExpenseItem, Input,
|
AutoDecimalInput,
|
||||||
LoadingSymbol, Plus,
|
BudgetHeader,
|
||||||
SearchBar, Welcome,
|
ButtonSetting,
|
||||||
ToggleSetting, CustomCard,
|
CategoryItem,
|
||||||
ButtonSetting
|
CategorySelector,
|
||||||
|
CategorySelectorModal,
|
||||||
|
CustomCard,
|
||||||
|
CustomColorPicker,
|
||||||
|
DateSelectorButton,
|
||||||
|
EmptyListCompenent,
|
||||||
|
ExpenseItem,
|
||||||
|
Graph,
|
||||||
|
Input,
|
||||||
|
LoadingSymbol,
|
||||||
|
NavigationButton,
|
||||||
|
Plus,
|
||||||
|
RoundedButton,
|
||||||
|
TextInputBar,
|
||||||
|
ToggleSetting,
|
||||||
|
TypeSelectorSwitch,
|
||||||
|
Welcome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {View, Text, TextInput, StyleSheet} from 'react-native';
|
import { StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes';
|
|
||||||
import { TextInputEndEditingEventData } from 'react-native/Libraries/Components/TextInput/TextInput';
|
import { TextInputEndEditingEventData } from 'react-native/Libraries/Components/TextInput/TextInput';
|
||||||
|
import { NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes';
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
label? : string,
|
label? : string,
|
||||||
|
|
|
||||||
56
components/stats/BudgetOverview.tsx
Normal file
56
components/stats/BudgetOverview.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
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 }]}>
|
||||||
|
<>
|
||||||
|
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 BudgetTotal;
|
||||||
62
components/stats/BudgetTotal.tsx
Normal file
62
components/stats/BudgetTotal.tsx
Normal 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;
|
||||||
69
components/stats/CategoryProgressBar.tsx
Normal file
69
components/stats/CategoryProgressBar.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface CategoryProgressBarProps {
|
||||||
|
categoryName: string;
|
||||||
|
color?: string;
|
||||||
|
maxValue: number;
|
||||||
|
currentValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryProgressBar: React.FC<CategoryProgressBarProps> = ({
|
||||||
|
categoryName,
|
||||||
|
color,
|
||||||
|
maxValue,
|
||||||
|
currentValue,
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const progress = (currentValue / maxValue) * 100;
|
||||||
|
const progressText = `${currentValue}€ / ${maxValue}€`;
|
||||||
|
|
||||||
|
const dynamicStyles = StyleSheet.create({
|
||||||
|
progressBarFill: {
|
||||||
|
height: '100%',
|
||||||
|
width: `${progress}%`,
|
||||||
|
backgroundColor: color || colors.accentColor,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
progressText: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
progressBarContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: 50,
|
||||||
|
backgroundColor: colors.elementSelectedColor,
|
||||||
|
borderRadius: 15,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
categoryName: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.categoryName}>{categoryName}</Text>
|
||||||
|
<View style={styles.progressBarContainer}>
|
||||||
|
<View style={dynamicStyles.progressBarFill}>
|
||||||
|
<Text style={dynamicStyles.progressText}>{progressText}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryProgressBar;
|
||||||
60
components/stats/CategoryProgressBarList.tsx
Normal file
60
components/stats/CategoryProgressBarList.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, Button, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
|
import CategoryProgressBar from './CategoryProgressBar';
|
||||||
|
|
||||||
|
interface CategoryItem {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
maxValue: number;
|
||||||
|
currentValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryProgressBarListProps {
|
||||||
|
categories: CategoryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE_BARS = 4;
|
||||||
|
|
||||||
|
const CategoryProgressBarList: React.FC<CategoryProgressBarListProps> = ({ categories }) => {
|
||||||
|
const [visibleBars, setVisibleBars] = useState(MAX_VISIBLE_BARS);
|
||||||
|
|
||||||
|
const showMore = () => {
|
||||||
|
setVisibleBars(prevVisibleBars => prevVisibleBars + MAX_VISIBLE_BARS);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{categories.slice(0, visibleBars).map((category, index) => (
|
||||||
|
<CategoryProgressBar
|
||||||
|
key={index}
|
||||||
|
categoryName={category.name}
|
||||||
|
color={category.color}
|
||||||
|
maxValue={category.maxValue}
|
||||||
|
currentValue={category.currentValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{visibleBars < categories.length && (
|
||||||
|
<TouchableOpacity style={styles.showMoreButton} onPress={showMore}>
|
||||||
|
<Text style={styles.buttonText}>Show More</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
showMoreButton: {
|
||||||
|
backgroundColor: '#EF6C00',
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
margin: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CategoryProgressBarList;
|
||||||
59
components/stats/FinancialAdvice.tsx
Normal file
59
components/stats/FinancialAdvice.tsx
Normal 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;
|
||||||
30
components/stats/Graph.tsx
Normal file
30
components/stats/Graph.tsx
Normal 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;
|
||||||
62
components/stats/SavingsOverview.tsx
Normal file
62
components/stats/SavingsOverview.tsx
Normal 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;
|
||||||
67
components/stats/Widget.tsx
Normal file
67
components/stats/Widget.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { View, StyleSheet, Text, Image } from 'react-native';
|
||||||
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
interface WidgetProps {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
image?: any;
|
||||||
|
backgroundColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: actualBackgroundColor,
|
||||||
|
borderColor: colors.widgetBorderColor,
|
||||||
|
borderRadius: 5,
|
||||||
|
padding: 16,
|
||||||
|
marginVertical: 8,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.22,
|
||||||
|
shadowRadius: 2.22,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
widgetTitle: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
widgetText: {
|
||||||
|
color: colors.primaryText,
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
imageStyle: {
|
||||||
|
width: '100%',
|
||||||
|
height: 500,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.widgetContainer}>
|
||||||
|
{!!title && <Text style={styles.widgetTitle}>{title}</Text>}
|
||||||
|
{!!text && <Text style={styles.widgetText}>{text}</Text>}
|
||||||
|
{!!image && <Image source={image} style={styles.imageStyle} />}
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Widget;
|
||||||
|
|
@ -12,6 +12,9 @@ export default {
|
||||||
elementDefaultColor: "#E0E0E0",
|
elementDefaultColor: "#E0E0E0",
|
||||||
elementSelectedColor: "#9E9E9E",
|
elementSelectedColor: "#9E9E9E",
|
||||||
accentColor: "#EF6C00",
|
accentColor: "#EF6C00",
|
||||||
|
|
||||||
|
widgetBackgroundColor: "#F7F7F7",
|
||||||
|
widgetBorderColor: "#E0E0E0",
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
primaryText: "#FFFFFF",
|
primaryText: "#FFFFFF",
|
||||||
|
|
@ -26,5 +29,8 @@ export default {
|
||||||
elementDefaultColor: "#535353",
|
elementDefaultColor: "#535353",
|
||||||
elementSelectedColor: "#B3B3B3",
|
elementSelectedColor: "#B3B3B3",
|
||||||
accentColor: "#EF6C00",
|
accentColor: "#EF6C00",
|
||||||
|
|
||||||
|
widgetBackgroundColor: "#252525",
|
||||||
|
widgetBorderColor: "#535353",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
33
hooks/useCategoryData.ts
Normal file
33
hooks/useCategoryData.ts
Normal 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 };
|
||||||
|
};
|
||||||
|
|
@ -1,20 +1,44 @@
|
||||||
|
import { Query } from "expo-sqlite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { executeQuery } from "../services/database";
|
import { executeQuery } from "../services/database";
|
||||||
|
|
||||||
const useFetch = () => {
|
const useFetch = (query: Query) => {
|
||||||
|
|
||||||
|
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 [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [data, setData] = useState<{[column: string]: any;}[]>([]);
|
|
||||||
|
|
||||||
const reFetch = () => {
|
const reFetch = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
executeQuery("SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid;").then((result) => {
|
executeQuery(query).then((result) => {
|
||||||
if("rows" in result[0]) {
|
if("rows" in result[0]) {
|
||||||
setData(result[0]["rows"]);
|
setData(result[0]["rows"]);
|
||||||
|
if(result[0]["rows"].length == 0){
|
||||||
|
setIsEmptyResult(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
console.error("Fetching data from database has failed: ", error);
|
console.error("Fetching data from database has failed: ", error);
|
||||||
}).finally(() => {
|
}).then(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +47,7 @@ const useFetch = () => {
|
||||||
reFetch();
|
reFetch();
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {data, isLoading, reFetch};
|
return {...fetchState, reFetch};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useFetch;
|
export default useFetch;
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { useColorScheme } from "react-native";
|
import { useColorScheme } from "react-native";
|
||||||
import colors from "../constants/colors";
|
import colors from "../constants/colors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use Theme context instead
|
||||||
|
* @param colorName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function useThemeColor(colorName: keyof typeof colors.light & keyof typeof colors.dark): string {
|
export function useThemeColor(colorName: keyof typeof colors.light & keyof typeof colors.dark): string {
|
||||||
|
console.log("useThemeColor is depreciated. Use useTheme().colors instead")
|
||||||
const theme = useColorScheme() ?? "light";
|
const theme = useColorScheme() ?? "light";
|
||||||
return colors[theme][colorName];
|
return colors[theme][colorName];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
776
package-lock.json
generated
776
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^13.0.0",
|
"@expo/vector-icons": "^13.0.0",
|
||||||
"@react-native-async-storage/async-storage": "1.18.2",
|
"@react-native-async-storage/async-storage": "1.18.2",
|
||||||
|
"@react-native-community/datetimepicker": "7.2.0",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
"expo": "~49.0.15",
|
"expo": "~49.0.15",
|
||||||
"expo-font": "~11.4.0",
|
"expo-font": "~11.4.0",
|
||||||
|
|
@ -35,11 +36,15 @@
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.72.6",
|
"react-native": "0.72.6",
|
||||||
"react-native-calendars": "^1.1303.0",
|
"react-native-calendars": "^1.1303.0",
|
||||||
|
"react-native-chart-kit": "^6.12.0",
|
||||||
"react-native-gesture-handler": "~2.12.0",
|
"react-native-gesture-handler": "~2.12.0",
|
||||||
|
"react-native-reanimated": "~3.3.0",
|
||||||
"react-native-safe-area-context": "4.6.3",
|
"react-native-safe-area-context": "4.6.3",
|
||||||
"react-native-screens": "~3.22.0",
|
"react-native-screens": "~3.22.0",
|
||||||
|
"react-native-svg": "13.9.0",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.1",
|
||||||
"react-native-web": "~0.19.6"
|
"react-native-web": "~0.19.6",
|
||||||
|
"reanimated-color-picker": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
|
|
||||||
80
services/DebugMenu.tsx
Normal file
80
services/DebugMenu.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
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"];
|
||||||
|
|
||||||
|
const getRandomColor = () => {
|
||||||
|
return randomColors[Math.floor(Math.random() * randomColors.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRandomName = () => {
|
||||||
|
return `RandomName-${Math.floor(Math.random() * 1000)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRandomNumber = () => {
|
||||||
|
return Math.floor(Math.random() * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DebugMenu = () => {
|
||||||
|
|
||||||
|
const deleteDBFile = () => {
|
||||||
|
console.warn("Deleting DB. App Restart is required")
|
||||||
|
return deleteDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNukeDatabase = () => {
|
||||||
|
return deleteExpenses(), deleteCategories()
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePopulateDatabase = () => {
|
||||||
|
return DEV_populateDatabase()
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteExpenses = () => {
|
||||||
|
return deleteExpenses();
|
||||||
|
}
|
||||||
|
const handleDeleteCategories = () => {
|
||||||
|
return deleteCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
//for some reason this function does not work
|
||||||
|
const handleAddCategory = () => {
|
||||||
|
const name = getRandomName();
|
||||||
|
const color = getRandomColor();
|
||||||
|
const allocated_amount = getRandomNumber();
|
||||||
|
const type = "expense";
|
||||||
|
|
||||||
|
addCategory(name, color, CategoryType.EXPENSE, allocated_amount)
|
||||||
|
.then(() => Alert.alert("Category Added", `Name: ${name}, Color: ${color}`))
|
||||||
|
.catch((error: any) => console.error("Error adding category: ", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
//for some reason this function does not work
|
||||||
|
const handleAddExpense = () => {
|
||||||
|
const name = getRandomName();
|
||||||
|
const categoryGuid = uuid.v4().toString();
|
||||||
|
const datetime = new Date().toISOString();
|
||||||
|
const amount = Math.floor(Math.random() * 1000);
|
||||||
|
|
||||||
|
addExpense(name, categoryGuid, datetime, amount)
|
||||||
|
.then(() => Alert.alert("Expense Added", `Name: ${name}, Amount: ${amount}`))
|
||||||
|
.catch((error: any) => console.error("Error adding expense: ", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Button title="Delete DB file" onPress={deleteDBFile}/>
|
||||||
|
<Button title="Nuke Database" onPress={handleNukeDatabase} />
|
||||||
|
<Button title="DEV_populateDatabase" onPress={handlePopulateDatabase} />
|
||||||
|
<Button title="Delete All Expenses" onPress={handleDeleteExpenses} />
|
||||||
|
<Button title="Delete All Categories" onPress={handleDeleteCategories} />
|
||||||
|
<Button title="Add Random Category" onPress={handleAddCategory} />
|
||||||
|
<Button title="Add Random Expense" onPress={handleAddExpense} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebugMenu;
|
||||||
|
|
@ -1,32 +1,45 @@
|
||||||
//created by thschleicher
|
|
||||||
|
|
||||||
import * as SQLite from "expo-sqlite";
|
import * as SQLite from "expo-sqlite";
|
||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
|
|
||||||
import { Query } from "expo-sqlite";
|
import { Query } from "expo-sqlite";
|
||||||
import { SimpleDate } from "../util/SimpleDate";
|
import { SimpleDate } from "../util/SimpleDate";
|
||||||
|
import { CategoryType } from "../types/dbItems";
|
||||||
|
|
||||||
|
let db: SQLite.SQLiteDatabase;
|
||||||
|
|
||||||
|
|
||||||
const db = SQLite.openDatabase("interactive_systeme.db");
|
|
||||||
|
|
||||||
export const initDatabase = async () => {
|
export const initDatabase = async () => {
|
||||||
|
db = SQLite.openDatabase("interactive_systeme.db");
|
||||||
try {
|
try {
|
||||||
await db.transactionAsync(async (tx: SQLite.SQLTransactionAsync) => {
|
await db.transactionAsync(async (tx: SQLite.SQLTransactionAsync) => {
|
||||||
await tx.executeSqlAsync(
|
await tx.executeSqlAsync(
|
||||||
"CREATE TABLE IF NOT EXISTS category (guid VARCHAR(36) PRIMARY KEY, name TEXT, color TEXT, type TEXT);"
|
"CREATE TABLE IF NOT EXISTS category (guid VARCHAR(36) PRIMARY KEY, name TEXT, color TEXT, type string, allocated_amount DOUBLE);"
|
||||||
);
|
);
|
||||||
await tx.executeSqlAsync(
|
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));"
|
"CREATE TABLE IF NOT EXISTS expense (guid VARCHAR(36) PRIMARY KEY, name TEXT, category_guid VARCHAR(36), datetime DATETIME, amount DOUBLE, FOREIGN KEY (category_guid) REFERENCES category(guid));"
|
||||||
);
|
);
|
||||||
console.log("Successfully initialized Tables!");
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("Error initializing the Database!");
|
||||||
console.log("Error initializing the Tables!");
|
|
||||||
throw (error);
|
throw (error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addCategory = async (name: string, color: string, type: string) => {
|
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)
|
//needs user input validation for type and color (should be validated by this function)
|
||||||
|
|
||||||
|
|
@ -34,18 +47,33 @@ export const addCategory = async (name: string, color: string, type: string) =>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.transactionAsync(async (tx) => {
|
await db.transactionAsync(async (tx) => {
|
||||||
await tx.executeSqlAsync("INSERT INTO category (guid, name, color, type) VALUES (?, ?, ?, ?);",
|
await tx.executeSqlAsync("INSERT INTO category (guid, name, color, type, allocated_amount) VALUES (?, ?, ?, ?, ?);",
|
||||||
[UUID.toString(), name, color, type]
|
[UUID.toString(), name, color, CategoryType, allocated_amount]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
console.log("Category added successfully!");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error adding category: ", error);
|
console.log("Error adding category: ", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addExpense = async (name: string, category_guid: string,datetime: string, amount: number) => {
|
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)
|
//needs user input validation for type and color (should be validated by this function)
|
||||||
|
|
||||||
|
|
@ -56,7 +84,6 @@ export const addExpense = async (name: string, category_guid: string,datetime: s
|
||||||
await tx.executeSqlAsync("INSERT INTO expense (guid, name, category_guid, datetime, amount) VALUES (?, ?, ?, ?, ?);", [expenseUUID.toString(), name, category_guid, datetime, amount]
|
await tx.executeSqlAsync("INSERT INTO expense (guid, name, category_guid, datetime, amount) VALUES (?, ?, ?, ?, ?);", [expenseUUID.toString(), name, category_guid, datetime, amount]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
console.log("Expense added successfully!");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error adding expense: ", error);
|
console.log("Error adding expense: ", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -85,15 +112,15 @@ export const deleteExpense = async (guid: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const executeQuery = async (query: string) => {
|
export const executeQuery = async (query: Query) => {
|
||||||
const sqliteQuary: Query[] = [{sql: query, args: []}];
|
const result = await db.execAsync([query], true);
|
||||||
const result = await db.execAsync(sqliteQuary, true);
|
|
||||||
if("error" in result[0]){
|
if("error" in result[0]){
|
||||||
throw result[0].error
|
throw result[0].error
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const deleteExpenses = async () => {
|
export const deleteExpenses = async () => {
|
||||||
try {
|
try {
|
||||||
await db.transactionAsync(async (tx: SQLite.SQLTransactionAsync) => {
|
await db.transactionAsync(async (tx: SQLite.SQLTransactionAsync) => {
|
||||||
|
|
@ -125,9 +152,7 @@ export const deleteDatabase = () => {
|
||||||
console.log("Error deleting the Database: ", error);
|
console.log("Error deleting the Database: ", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
console.log("Database Deleted!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeDatabase = async () => {
|
const closeDatabase = async () => {
|
||||||
|
|
@ -149,9 +174,11 @@ export const DEV_populateDatabase = async () => {
|
||||||
|
|
||||||
for(let i=0; i < 5; i++){
|
for(let i=0; i < 5; i++){
|
||||||
let random = Math.floor(Math.random() * colors.length);
|
let random = Math.floor(Math.random() * colors.length);
|
||||||
await addCategory(`Category ${i}`, colors[random], "budget")
|
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("SELECT * from category")
|
const result = await executeQuery({sql:"SELECT * from category", args:[]})
|
||||||
let categories: {[column: string]: any}[];
|
let categories: {[column: string]: any}[];
|
||||||
if("rows" in result[0]){
|
if("rows" in result[0]){
|
||||||
categories = result[0]["rows"]
|
categories = result[0]["rows"]
|
||||||
|
|
@ -166,7 +193,7 @@ export const DEV_populateDatabase = async () => {
|
||||||
let randomDay = Math.floor(Math.random() * 20)
|
let randomDay = Math.floor(Math.random() * 20)
|
||||||
date.setDate(randomDay)
|
date.setDate(randomDay)
|
||||||
let string = new SimpleDate(date).toISOString()
|
let string = new SimpleDate(date).toISOString()
|
||||||
await addExpense(`Expense ${i}`, categories[random].guid, string, 15)
|
await addExpense(`Expense ${i}`, categories[random].guid, string, 30)
|
||||||
}
|
}
|
||||||
} catch(e){
|
} catch(e){
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
|
||||||
19
types/dbItems.ts
Normal file
19
types/dbItems.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
export enum CategoryType {
|
||||||
|
SAVING = "saving",
|
||||||
|
EXPENSE = "expense",
|
||||||
|
}
|
||||||
|
export interface Category {
|
||||||
|
guid? : string;
|
||||||
|
name? : string;
|
||||||
|
color? :string;
|
||||||
|
type? : "expense" | "saving"
|
||||||
|
allocatedAmount? : number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Expense {
|
||||||
|
guid? : string;
|
||||||
|
name? : string;
|
||||||
|
dateTime? : string;
|
||||||
|
amount?: number;
|
||||||
|
category_guid?: string;
|
||||||
|
}
|
||||||
Reference in a new issue