feat: Add expense screen
This commit is contained in:
parent
e1efed5b21
commit
36679279c1
18 changed files with 459 additions and 57 deletions
|
|
@ -12,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,93 @@
|
||||||
import { View, Text, StyleSheet, TextInput, NativeSyntheticEvent, TextInputKeyPressEventData, TouchableOpacity } from 'react-native'
|
import { View, Text, StyleSheet, Alert } from 'react-native'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { SIZES } from '../../../constants/theme'
|
import { SIZES } from '../../../constants/theme'
|
||||||
import { useTheme } from '../../contexts/ThemeContext'
|
import { useTheme } from '../../contexts/ThemeContext'
|
||||||
import { AutoDecimalInput } from '../../../components'
|
import { AutoDecimalInput, CategorySelector, CategorySelectorModal, DateSelectorButton, RoundedButton, TextInputBar } from '../../../components'
|
||||||
|
import { Category } from '../../../types/dbItems'
|
||||||
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
|
import { addExpense } from '../../../services/database'
|
||||||
|
import { SimpleDate } from '../../../util/SimpleDate'
|
||||||
|
import { useRouter } from 'expo-router'
|
||||||
|
|
||||||
export default function AddItem() {
|
export default function AddItem() {
|
||||||
const value = useRef<string>("");
|
const {colors} = useTheme();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [formatedValue, setFormatedValue] = useState<string>("");
|
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) => {
|
const handleValueChange = (formatedValue: string) => {
|
||||||
setFormatedValue(formatedValue);
|
setFormatedValue(formatedValue);
|
||||||
}
|
}
|
||||||
console.log(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();
|
||||||
|
router.back();
|
||||||
|
}else {
|
||||||
|
Alert.alert("Invalid input", "One of the Props is not properly defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
<CategorySelectorModal visible={selectorModalVisible} onRequestClose={()=>{setSelecorModalVisible(false)}} onCategoryTap={handleCategorySelect}></CategorySelectorModal>
|
||||||
<AutoDecimalInput onValueChange={handleValueChange} label='Amount'></AutoDecimalInput>
|
<AutoDecimalInput onValueChange={handleValueChange} label='Amount'></AutoDecimalInput>
|
||||||
|
<CategorySelector onPress={()=>{setSelecorModalVisible(true)}} selectedCategory={selectedCategory}/>
|
||||||
|
<TextInputBar placeholder='Name' 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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
save: {
|
||||||
|
marginTop: 40,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
margin: SIZES.normal,
|
margin: SIZES.normal,
|
||||||
|
display: "flex",
|
||||||
|
gap: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
submitText: {
|
||||||
|
fontSize: SIZES.large
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useRef, useState, useMemo } from 'react';
|
import React, { useRef, useState, useMemo } from 'react';
|
||||||
import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native';
|
import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native';
|
||||||
import { Calendar } from 'react-native-calendars';
|
import { Calendar } from 'react-native-calendars';
|
||||||
import { FlatList } from 'react-native-gesture-handler';
|
import { FlatList, RefreshControl } from 'react-native-gesture-handler';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { ExpenseItem, LoadingSymbol, Plus, SearchBar, Welcome } from '../../../components';
|
import { ExpenseItem, LoadingSymbol, Plus, TextInputBar, Welcome } from '../../../components';
|
||||||
import useFetch from '../../../hooks/useFetch';
|
import useFetch from '../../../hooks/useFetch';
|
||||||
|
|
||||||
import { addExpense, executeQuery } from "../../../services/database";
|
import { addExpense, executeQuery } from "../../../services/database";
|
||||||
|
|
@ -63,7 +63,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {data, isLoading, reFetch} = useFetch({sql: "SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid;", args: []});
|
const {data, isLoading, reFetch} = useFetch({sql: "SELECT e.guid AS expense_guid, c.guid AS category_guid, e.name AS expense_name, c.name AS category_name, e.datetime AS expense_datetime, e.amount AS expense_amount, c.color AS category_color, c.type AS category_type FROM expense e JOIN category c ON e.category_guid = c.guid ORDER BY expense_datetime desc;", args: []});
|
||||||
|
|
||||||
const expenseDates = useMemo(()=>
|
const expenseDates = useMemo(()=>
|
||||||
constructMarkedDates(data)
|
constructMarkedDates(data)
|
||||||
|
|
@ -92,7 +92,7 @@ export default function Page() {
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<>
|
<>
|
||||||
<Welcome name="My Dude" image={profile} onPress={() => {router.push("/home/userSettings")}}/>
|
<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={{
|
<Calendar key={theme} maxDate={SimpleDate.now().format("YYYY-MM-DD")} style={{borderRadius: 20, padding:10}} theme={{
|
||||||
dayTextColor: colors.primaryText,
|
dayTextColor: colors.primaryText,
|
||||||
textDisabledColor: colors.secondaryText,
|
textDisabledColor: colors.secondaryText,
|
||||||
todayTextColor: colors.accentColor,
|
todayTextColor: colors.accentColor,
|
||||||
|
|
@ -106,7 +106,7 @@ export default function Page() {
|
||||||
>
|
>
|
||||||
|
|
||||||
</Calendar>
|
</Calendar>
|
||||||
<SearchBar placeholder='Type to Search...'></SearchBar>
|
<TextInputBar placeholder='Type to Search...' style={{marginBottom: 20}}></TextInputBar>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
renderItem = {({item}) => <ExpenseItem category={item.category_name} color={item.category_color} date={item.expense_datetime} title={item.expense_name} value={item.expense_amount}/>}
|
renderItem = {({item}) => <ExpenseItem category={item.category_name} color={item.category_color} date={item.expense_datetime} title={item.expense_name} value={item.expense_amount}/>}
|
||||||
|
|
@ -114,6 +114,9 @@ export default function Page() {
|
||||||
ItemSeparatorComponent={() => {
|
ItemSeparatorComponent={() => {
|
||||||
return (<View style={styles.itemSeperatorStyle}/>);
|
return (<View style={styles.itemSeperatorStyle}/>);
|
||||||
}}
|
}}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={isLoading} onRefresh={reFetch}/>
|
||||||
|
}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
scrollEventThrottle={20}
|
scrollEventThrottle={20}
|
||||||
/>
|
/>
|
||||||
|
|
@ -124,6 +127,7 @@ export default function Page() {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safeAreaViewStyle: {
|
safeAreaViewStyle: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
paddingHorizontal: 10
|
||||||
},
|
},
|
||||||
itemSeperatorStyle: {
|
itemSeperatorStyle: {
|
||||||
marginVertical: 5,
|
marginVertical: 5,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
import { StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
||||||
import SearchBar from "../common/SearchBar";
|
import TextInputBar from "../common/TextInputBar";
|
||||||
import { useTheme } from "../../app/contexts/ThemeContext";
|
import { useTheme } from "../../app/contexts/ThemeContext";
|
||||||
|
|
||||||
type BudgetHeaderProperties = {
|
type BudgetHeaderProperties = {
|
||||||
|
|
@ -34,7 +34,7 @@ const BudgetHeader = (properties: BudgetHeaderProperties) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<SearchBar placeholder='Search...'></SearchBar>
|
<TextInputBar placeholder='Search...'></TextInputBar>
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { View, Text, TouchableOpacity, TextInput, StyleSheet, NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native'
|
import { View, Text, TouchableOpacity, TextInput, StyleSheet, NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native'
|
||||||
import React, {LegacyRef, MutableRefObject, useRef, useState} from 'react'
|
import React, {LegacyRef, MutableRefObject, useRef, useState} from 'react'
|
||||||
import colors from '../../constants/colors';
|
|
||||||
import { SIZES } from '../../constants/theme';
|
import { SIZES } from '../../constants/theme';
|
||||||
import { useTheme } from '../../app/contexts/ThemeContext';
|
import { useTheme } from '../../app/contexts/ThemeContext';
|
||||||
|
|
||||||
|
|
@ -69,17 +68,15 @@ const AutoDecimalInput: React.FC<AutoDecimalInputProps> = ({onValueChange, label
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
|
||||||
margin: SIZES.normal,
|
|
||||||
},
|
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
|
minHeight: 50,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between',
|
||||||
|
alignItems: "center"
|
||||||
},
|
},
|
||||||
text:{
|
text:{
|
||||||
fontSize: SIZES.large,
|
fontSize: SIZES.large,
|
||||||
marginVertical: 12,
|
|
||||||
marginHorizontal: 15,
|
marginHorizontal: 15,
|
||||||
},
|
},
|
||||||
currency: {
|
currency: {
|
||||||
|
|
|
||||||
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -39,10 +39,7 @@ export default function CustomCard(props : ViewProps) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container:{
|
container:{
|
||||||
flexDirection: "row",
|
borderRadius: 20,
|
||||||
alignItems: "stretch",
|
|
||||||
alignContent: "space-between",
|
|
||||||
borderRadius: 10,
|
|
||||||
marginHorizontal: 10,
|
marginHorizontal: 10,
|
||||||
},
|
},
|
||||||
boxShadow: {},
|
boxShadow: {},
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -42,12 +42,12 @@ export default function TextInputBar(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='default' placeholder={props.placeholder} value={text}/>
|
<TextInput placeholderTextColor={colors.secondaryText} onChangeText = {handleChange} style={[{fontSize: SIZES.normal, height: "100%", color:colors.primaryText}, styles.TextInput]} autoCorrect={false} keyboardType='default' placeholder={props.placeholder} value={text} onFocus={()=>handleChange(text)} onEndEditing={()=>setIsactive(false)}/>
|
||||||
|
|
||||||
{isActive &&
|
{isActive &&
|
||||||
<TouchableOpacity style={styles.cancel} onPress={()=>{handleChange("")}}>
|
<TouchableOpacity style={styles.cancel} onPress={()=>{handleChange("")}}>
|
||||||
<AntDesign size={15} name='closecircle'></AntDesign>
|
<AntDesign size={15} name='closecircle' color={colors.primaryText}></AntDesign>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -56,8 +56,6 @@ export default function TextInputBar(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",
|
||||||
|
|
|
||||||
60
components/home/addItem/CategorySelector.tsx
Normal file
60
components/home/addItem/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,
|
||||||
|
}
|
||||||
|
})
|
||||||
93
components/home/addItem/CategorySelectorModal.tsx
Normal file
93
components/home/addItem/CategorySelectorModal.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<View style={styles.main}>
|
||||||
|
<View style={[styles.modal, {backgroundColor: colors.containerColor}]}>
|
||||||
|
<View>
|
||||||
|
<Text style={[styles.heading, {color: colors.primaryText}]}>{selectMulitple ? "Categories" : "Category"}</Text>
|
||||||
|
</View>
|
||||||
|
<TextInputBar placeholder='TypeToSearch' onChangeText={handleSearchText}></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"
|
||||||
|
>
|
||||||
|
</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/home/addItem/DateSelectorButton.tsx
Normal file
38
components/home/addItem/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
|
||||||
|
|
@ -9,6 +9,7 @@ export default function ExpenseItem(itemProps : ExpenseItemProps) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return (
|
return (
|
||||||
<CustomCard>
|
<CustomCard>
|
||||||
|
<View style={styles.tile}>
|
||||||
<View style={[styles.colorTip, {backgroundColor: itemProps.color}]}></View>
|
<View style={[styles.colorTip, {backgroundColor: itemProps.color}]}></View>
|
||||||
<View style={[styles.textSection, {backgroundColor: colors.backgroundColor}]}>
|
<View style={[styles.textSection, {backgroundColor: colors.backgroundColor}]}>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
|
|
@ -30,7 +31,7 @@ export default function ExpenseItem(itemProps : ExpenseItemProps) {
|
||||||
color: colors.primaryText
|
color: colors.primaryText
|
||||||
}} numberOfLines={1}>{itemProps.value}</Text>
|
}} numberOfLines={1}>{itemProps.value}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
</CustomCard>
|
</CustomCard>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
@ -43,6 +44,12 @@ const styles = StyleSheet.create({
|
||||||
borderBottomLeftRadius: 20,
|
borderBottomLeftRadius: 20,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
tile: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "stretch",
|
||||||
|
alignContent: "space-between",
|
||||||
|
},
|
||||||
|
|
||||||
textSection: {
|
textSection: {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignContent: "space-between",
|
alignContent: "space-between",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ import { ButtonSetting, ToggleSetting } from "./home/Setting"
|
||||||
import Welcome from "./home/Welcome"
|
import Welcome from "./home/Welcome"
|
||||||
import ExpenseItem from "./home/expenseItem"
|
import ExpenseItem from "./home/expenseItem"
|
||||||
|
|
||||||
|
//home/addItem
|
||||||
|
import CategorySelector from "./home/addItem/CategorySelector"
|
||||||
|
import CategorySelectorModal from "./home/addItem/CategorySelectorModal"
|
||||||
|
import DateSelectorButton from "./home/addItem/DateSelectorButton"
|
||||||
|
|
||||||
//common
|
//common
|
||||||
import CustomCard from "./common/CustomCard"
|
import CustomCard from "./common/CustomCard"
|
||||||
import NavigationButton from "./common/button"
|
import NavigationButton from "./common/button"
|
||||||
|
|
@ -10,8 +15,7 @@ import LoadingSymbol from "./common/loadingSymbol"
|
||||||
import Plus from "./common/plus"
|
import Plus from "./common/plus"
|
||||||
import TextInputBar from "./common/TextInputBar"
|
import TextInputBar from "./common/TextInputBar"
|
||||||
import AutoDecimalInput from "./common/AutoDecimalInput"
|
import AutoDecimalInput from "./common/AutoDecimalInput"
|
||||||
import CategorySelector from "./common/CategorySelector"
|
import RoundedButton from "./common/RoundedButton"
|
||||||
import CategorySelectorModal from "./common/CategorySelectorModal"
|
|
||||||
|
|
||||||
//login
|
//login
|
||||||
import BudgetHeader from "./budget/budgetHeader"
|
import BudgetHeader from "./budget/budgetHeader"
|
||||||
|
|
@ -23,8 +27,24 @@ import TypeSelectorSwitch from "./budget/typeSelectorSwitch"
|
||||||
import Input from "./login/input"
|
import Input from "./login/input"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BudgetHeader, ButtonSetting, CustomCard, ExpenseItem, Input,
|
BudgetHeader,
|
||||||
LoadingSymbol, Plus,
|
ButtonSetting,
|
||||||
TextInputBar, ToggleSetting, Welcome, AutoDecimalInput
|
CustomCard,
|
||||||
|
ExpenseItem,
|
||||||
|
Input,
|
||||||
|
LoadingSymbol,
|
||||||
|
Plus,
|
||||||
|
TextInputBar,
|
||||||
|
ToggleSetting,
|
||||||
|
Welcome,
|
||||||
|
AutoDecimalInput,
|
||||||
|
CategorySelector,
|
||||||
|
CategorySelectorModal,
|
||||||
|
DateSelectorButton,
|
||||||
|
RoundedButton,
|
||||||
|
CategoryItem,
|
||||||
|
TypeSelectorSwitch,
|
||||||
|
NavigationButton,
|
||||||
|
CustomColorPicker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
9
package-lock.json
generated
9
package-lock.json
generated
|
|
@ -10,6 +10,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",
|
||||||
|
|
@ -6275,6 +6276,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-community/datetimepicker": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-dO1sQy83M/EvnHE2egto05iwXZX7EYn5f/VDMp6afZFRFXRiRo7CzB3VFg4B55gJRJMNBv06NYMLPM3SlpnEGQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.72.0",
|
"version": "0.72.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@
|
||||||
"react-native-screens": "~3.22.0",
|
"react-native-screens": "~3.22.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"
|
"reanimated-color-picker": "^2.4.2",
|
||||||
|
"@react-native-community/datetimepicker": "7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Button, Alert } from 'react-native';
|
import { View, Button, Alert } from 'react-native';
|
||||||
import { addCategory, addExpense, deleteExpenses, deleteCategories, DEV_populateDatabase } from './database';
|
import { addCategory, addExpense, deleteExpenses, deleteCategories, DEV_populateDatabase, deleteDatabase } from './database';
|
||||||
import uuid from 'react-native-uuid';
|
import uuid from 'react-native-uuid';
|
||||||
|
|
||||||
const randomColors = ["red", "blue", "green", "purple", "yellow"];
|
const randomColors = ["red", "blue", "green", "purple", "yellow"];
|
||||||
|
|
@ -19,6 +19,11 @@ const getRandomNumber = () => {
|
||||||
|
|
||||||
const DebugMenu = () => {
|
const DebugMenu = () => {
|
||||||
|
|
||||||
|
const deleteDBFile = () => {
|
||||||
|
console.warn("Deleting DB. App Restart is required")
|
||||||
|
return deleteDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
const handleNukeDatabase = () => {
|
const handleNukeDatabase = () => {
|
||||||
return deleteExpenses(), deleteCategories()
|
return deleteExpenses(), deleteCategories()
|
||||||
};
|
};
|
||||||
|
|
@ -60,6 +65,7 @@ const DebugMenu = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Button title="Delete DB file" onPress={deleteDBFile}/>
|
||||||
<Button title="Nuke Database" onPress={handleNukeDatabase} />
|
<Button title="Nuke Database" onPress={handleNukeDatabase} />
|
||||||
<Button title="DEV_populateDatabase" onPress={handlePopulateDatabase} />
|
<Button title="DEV_populateDatabase" onPress={handlePopulateDatabase} />
|
||||||
<Button title="Delete All Expenses" onPress={handleDeleteExpenses} />
|
<Button title="Delete All Expenses" onPress={handleDeleteExpenses} />
|
||||||
|
|
|
||||||
7
types/dbItems.ts
Normal file
7
types/dbItems.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Category {
|
||||||
|
guid? : string;
|
||||||
|
name? : string;
|
||||||
|
color? :string;
|
||||||
|
type? : "expense" | "saving"
|
||||||
|
allocatedAmount? : number
|
||||||
|
}
|
||||||
Reference in a new issue