restore repo
This commit is contained in:
parent
97e8ee6eb8
commit
f2a798ab18
30 changed files with 1627 additions and 2 deletions
88
README.md
88
README.md
|
|
@ -1,3 +1,87 @@
|
|||
# MoneyListato
|
||||
# Money-LIST
|
||||
|
||||
App per la gestione delle finanze
|
||||
## Introduzione
|
||||
|
||||
**Money-LIST** è un'app semplice e intuitiva per la gestione delle proprie finanze. Progettata per essere facile da usare, ti permette di tenere traccia dei tuoi conti, delle transazioni e delle spese ricorsive in modo efficiente.
|
||||
|
||||
## Target
|
||||
|
||||
Questa applicazione è stata sviluppata per utenti che cercano una soluzione rapida e pratica per gestire le proprie finanze quotidiane.
|
||||
|
||||
## Funzionalità
|
||||
|
||||
### 1. Gestione dei Conti
|
||||
|
||||
- **Aggiungere Conto**: Inserisci un nuovo conto con nome, descrizione e saldo disponibile.
|
||||
- **Modificare Conto**: Aggiorna i dettagli del conto esistente.
|
||||
- **Eliminare Conto**: Rimuovi un conto esistente dall'app.
|
||||
- **Visualizzare Saldo**: Monitora il saldo disponibile per ogni conto.
|
||||
|
||||
### 2. Gestione delle Transazioni
|
||||
|
||||
- **Aggiungere Transazione**: Registra una nuova transazione specificando il conto, titolo, importo (positivo per entrate, negativo per uscite), data (preimpostata su oggi), descrizione, categorie e tag.
|
||||
- **Modificare Transazione**: Modifica i dettagli di una transazione esistente.
|
||||
- **Eliminare Transazione**: Rimuovi una transazione dal registro.
|
||||
- **Visualizzare Transazioni**: Consulta il saldo aggiornato e la lista delle transazioni per ogni conto.
|
||||
|
||||
### 3. Gestione delle Spese Ricorsive
|
||||
|
||||
- **Aggiungere Spesa Ricorsiva**: Registra una spesa che si ripete automaticamente a intervalli regolari.
|
||||
- **Modificare Spesa Ricorsiva**: Aggiorna i dettagli di una spesa ricorsiva esistente.
|
||||
- **Eliminare Spesa Ricorsiva**: Cancella una spesa ricorsiva dall'app.
|
||||
- **Visualizzare Spese Ricorsive**: Monitora le spese ricorsive impostate.
|
||||
|
||||
### 4. Foto delle Transazioni (Versione 2.0)
|
||||
|
||||
- **Salvare Foto dello Scontrino**: Associa una foto dello scontrino o del documento alla transazione per una documentazione più completa.
|
||||
|
||||
## Struttura del Progetto
|
||||
|
||||
- **Conti**: Modulo per la gestione dei conti finanziari.
|
||||
- **Transazioni**: Modulo per la gestione di entrate e uscite finanziarie.
|
||||
- **Spese Ricorsive**: Modulo per la gestione automatizzata delle spese ricorrenti.
|
||||
- **Foto delle Transazioni**: Funzionalità avanzata per allegare foto a ogni transazione.
|
||||
|
||||
## Tecnologie Utilizzate
|
||||
|
||||
- **Frontend**: React Native
|
||||
- **Backend**: TBD
|
||||
- **Database**: Sqlite
|
||||
- **Gestione delle Immagini**: TBD (per la versione 2.0)
|
||||
|
||||
## Installazione
|
||||
|
||||
1. Clona il repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.pansi21.xyz/PanSi21/MoneyListato.git
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] **Versione 1.0**
|
||||
- [x] Gestione dei Conti
|
||||
- [x] Aggiungere, modificare, eliminare conti
|
||||
- [x] Visualizzare saldo dei conti
|
||||
- [x] Gestione delle Transazioni
|
||||
- [x] Aggiungere, modificare, eliminare transazioni
|
||||
- [x] Visualizzare lista delle transazioni e saldo aggiornato
|
||||
- [x] Spese Ricorsive
|
||||
- [x] Aggiungere, modificare, eliminare spese ricorsive
|
||||
- [x] Visualizzare spese ricorsive impostate
|
||||
|
||||
- [ ] **Versione 2.0**
|
||||
- [ ] Foto delle Transazioni
|
||||
- [ ] Associare foto a ogni transazione
|
||||
- [ ] Visualizzare foto allegate nelle transazioni
|
||||
- [ ] Sincronizzazione Cloud
|
||||
- [ ] Backup automatico dei dati su cloud
|
||||
- [ ] Sincronizzazione multi-dispositivo
|
||||
|
||||
- [ ] **Versione Futura**
|
||||
- [ ] Reportistica Avanzata
|
||||
- [ ] Grafici e statistiche sui conti e le transazioni
|
||||
- [ ] Esportazione dati in CSV o PDF
|
||||
- [ ] Notifiche Push
|
||||
- [ ] Promemoria per le spese ricorsive
|
||||
- [ ] Notifiche per transazioni insolite
|
||||
36
app.json
Normal file
36
app.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "moneyApp",
|
||||
"slug": "moneyApp",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "myapp",
|
||||
"userInterfaceStyle": "dark",
|
||||
"splash": {
|
||||
"image": "./assets/images/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
59
app/(tabs)/_layout.tsx
Normal file
59
app/(tabs)/_layout.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||
import { Link, Tabs } from 'expo-router';
|
||||
import { Pressable } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
|
||||
|
||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||
function TabBarIcon(props: {
|
||||
name: React.ComponentProps<typeof FontAwesome>['name'];
|
||||
color: string;
|
||||
}) {
|
||||
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
|
||||
}
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
||||
// Disable the static render of the header on web
|
||||
// to prevent a hydration error in React Navigation v6.
|
||||
headerShown: useClientOnlyValue(false, true),
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Tab One',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||
headerRight: () => (
|
||||
<Link href="/modal" asChild>
|
||||
<Pressable>
|
||||
{({ pressed }) => (
|
||||
<FontAwesome
|
||||
name="info-circle"
|
||||
size={25}
|
||||
color={Colors[colorScheme ?? 'light'].text}
|
||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="two"
|
||||
options={{
|
||||
title: 'Tab Two',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
30
app/(tabs)/index.tsx
Normal file
30
app/(tabs)/index.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { FlatList, StyleSheet, Text } from 'react-native';
|
||||
import { View } from '@/components/Themed';
|
||||
import TransactionItem from '@/components/transaction/TransactionItem';
|
||||
|
||||
export default function TabOneScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.testo}>Lista Transazioni</Text>
|
||||
<FlatList
|
||||
data={[{ key: 'transactions' }]}
|
||||
renderItem={() => <TransactionItem />}
|
||||
keyExtractor={item => item.key}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
testo: {
|
||||
color: '#fff',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
paddingVertical: 10,
|
||||
}
|
||||
});
|
||||
20
app/(tabs)/two.tsx
Normal file
20
app/(tabs)/two.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
import TransactionAdd from '@/components/transaction/TransactionAdd';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const handleTransactionAdded = () => {
|
||||
setRefreshKey(oldKey => oldKey + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TransactionAdd onTransactionAdded={handleTransactionAdded} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
});
|
||||
38
app/+html.tsx
Normal file
38
app/+html.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { ScrollViewStyleReset } from 'expo-router/html';
|
||||
|
||||
// This file is web-only and used to configure the root HTML for every
|
||||
// web page during static rendering.
|
||||
// The contents of this function only run in Node.js environments and
|
||||
// do not have access to the DOM or browser APIs.
|
||||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
|
||||
{/*
|
||||
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
|
||||
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
|
||||
*/}
|
||||
<ScrollViewStyleReset />
|
||||
|
||||
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
|
||||
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
|
||||
{/* Add any additional <head> elements that you want globally available on web... */}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const responsiveBackground = `
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
}`;
|
||||
40
app/+not-found.tsx
Normal file
40
app/+not-found.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Link, Stack } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { Text, View } from '@/components/Themed';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>This screen doesn't exist.</Text>
|
||||
|
||||
<Link href="/" style={styles.link}>
|
||||
<Text style={styles.linkText}>Go to home screen!</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
color: '#2e78b7',
|
||||
},
|
||||
});
|
||||
59
app/_layout.tsx
Normal file
59
app/_layout.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
|
||||
export {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
ErrorBoundary,
|
||||
} from 'expo-router';
|
||||
|
||||
export const unstable_settings = {
|
||||
// Ensure that reloading on `/modal` keeps a back button present.
|
||||
initialRouteName: '(tabs)',
|
||||
};
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function RootLayout() {
|
||||
const [loaded, error] = useFonts({
|
||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||
...FontAwesome.font,
|
||||
});
|
||||
|
||||
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
||||
useEffect(() => {
|
||||
if (error) throw error;
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded]);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RootLayoutNav />;
|
||||
}
|
||||
|
||||
function RootLayoutNav() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
|
||||
</Stack>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
11
app/modal.tsx
Normal file
11
app/modal.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { View, Text } from 'react-native'
|
||||
import React from 'react'
|
||||
import NewGrafico from '@/components/ui/NewGrafico'
|
||||
|
||||
export default function modal() {
|
||||
return (
|
||||
<View>
|
||||
<NewGrafico />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
BIN
assets/fonts/SpaceMono-Regular.ttf
Normal file
BIN
assets/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/images/adaptive-icon.png
Normal file
BIN
assets/images/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/favicon.png
Normal file
BIN
assets/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/icon.png
Normal file
BIN
assets/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/splash.png
Normal file
BIN
assets/images/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
77
components/EditScreenInfo.tsx
Normal file
77
components/EditScreenInfo.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ExternalLink } from './ExternalLink';
|
||||
import { MonoText } from './StyledText';
|
||||
import { Text, View } from './Themed';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
export default function EditScreenInfo({ path }: { path: string }) {
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.getStartedContainer}>
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)">
|
||||
Open up the code for this screen:
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
|
||||
darkColor="rgba(255,255,255,0.05)"
|
||||
lightColor="rgba(0,0,0,0.05)">
|
||||
<MonoText>{path}</MonoText>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)">
|
||||
Change any of the text, save the file, and your app will automatically update.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpContainer}>
|
||||
<ExternalLink
|
||||
style={styles.helpLink}
|
||||
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet">
|
||||
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
|
||||
Tap here if your app doesn't automatically update after making changes
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
getStartedContainer: {
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 50,
|
||||
},
|
||||
homeScreenFilename: {
|
||||
marginVertical: 7,
|
||||
},
|
||||
codeHighlightContainer: {
|
||||
borderRadius: 3,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
getStartedText: {
|
||||
fontSize: 17,
|
||||
lineHeight: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
helpContainer: {
|
||||
marginTop: 15,
|
||||
marginHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
helpLink: {
|
||||
paddingVertical: 15,
|
||||
},
|
||||
helpLinkText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
25
components/ExternalLink.tsx
Normal file
25
components/ExternalLink.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Link } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export function ExternalLink(
|
||||
props: Omit<React.ComponentProps<typeof Link>, 'href'> & { href: string }
|
||||
) {
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
{...props}
|
||||
// @ts-expect-error: External URLs are not typed.
|
||||
href={props.href}
|
||||
onPress={(e) => {
|
||||
if (Platform.OS !== 'web') {
|
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
e.preventDefault();
|
||||
// Open the link in an in-app browser.
|
||||
WebBrowser.openBrowserAsync(props.href as string);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
components/StyledText.tsx
Normal file
5
components/StyledText.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Text, TextProps } from './Themed';
|
||||
|
||||
export function MonoText(props: TextProps) {
|
||||
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />;
|
||||
}
|
||||
45
components/Themed.tsx
Normal file
45
components/Themed.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Learn more about Light and Dark modes:
|
||||
* https://docs.expo.io/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Text as DefaultText, View as DefaultView } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from './useColorScheme';
|
||||
|
||||
type ThemeProps = {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
};
|
||||
|
||||
export type TextProps = ThemeProps & DefaultText['props'];
|
||||
export type ViewProps = ThemeProps & DefaultView['props'];
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
||||
|
||||
export function Text(props: TextProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
return <DefaultText style={[{ color }, style]} {...otherProps} />;
|
||||
}
|
||||
|
||||
export function View(props: ViewProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
|
||||
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
|
||||
}
|
||||
10
components/__tests__/StyledText-test.js
Normal file
10
components/__tests__/StyledText-test.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import * as React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { MonoText } from '../StyledText';
|
||||
|
||||
it(`renders correctly`, () => {
|
||||
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
205
components/transaction/TransactionAdd.jsx
Normal file
205
components/transaction/TransactionAdd.jsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, TextInput, StyleSheet, Text, TouchableOpacity } from 'react-native';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
|
||||
export default function TransactionAdd({ onTransactionAdded }) {
|
||||
const [description, setDescription] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [category, setCategory] = useState(''); // Default category
|
||||
const [transactionType, setTransactionType] = useState('expense'); // Default to expense
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [datePlaceholder, setDatePlaceholder] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
async function setupDatabase() {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
// await db.execAsync('DROP TABLE IF EXISTS transactions'); // Elimina la tabella esistente
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
description TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
setupDatabase();
|
||||
|
||||
const currentDate = new Date();
|
||||
const formattedDate = `${currentDate.getDate().toString().padStart(2, '0')}-${(currentDate.getMonth() + 1).toString().padStart(2, '0')}-${currentDate.getFullYear()}`;
|
||||
setDatePlaceholder(formattedDate);
|
||||
}, []);
|
||||
|
||||
const addTransaction = async () => {
|
||||
if (description.trim() === '' || amount.trim() === '') {
|
||||
alert('Descrizione e/o Importo mancanti');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate amount format
|
||||
const amountRegex = /^\d+(\.\d{1,2})?$/;
|
||||
if (!amountRegex.test(amount)) {
|
||||
alert('Formato importo non valido. Usa il formato 0.00');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected date in DD-MM-YYYY format
|
||||
const formattedDate = `${date.getDate().toString().padStart(2, '0')}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getFullYear()}`;
|
||||
|
||||
try {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
await db.runAsync(
|
||||
'INSERT INTO transactions (description, amount, category, date, type) VALUES (?, ?, ?, ?, ?)',
|
||||
[description, parseFloat(amount), category, formattedDate, transactionType]
|
||||
);
|
||||
alert('Transazione aggiunta con successo.');
|
||||
setDescription('');
|
||||
setAmount('');
|
||||
setCategory(''); // Reset to default category
|
||||
setTransactionType('expense'); // Reset to default type
|
||||
if (onTransactionAdded) {
|
||||
onTransactionAdded();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding transaction:', error);
|
||||
alert('Errore. Inserisci correttamente i dati.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.testo}>Descrizione:</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
/>
|
||||
<Text style={styles.testo}>Importo:</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={amount}
|
||||
onChangeText={setAmount}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<Text style={styles.testo}>Categoria:</Text>
|
||||
|
||||
<View style={{borderColor: 'gray', borderWidth: 1, marginBottom: 20}}>
|
||||
<Picker
|
||||
selectedValue={category}
|
||||
style={styles.picker}
|
||||
onValueChange={(itemValue) => setCategory(itemValue)}
|
||||
>
|
||||
<Picker.Item label="Spesa" value="spesa" />
|
||||
<Picker.Item label="Svago" value="svago" />
|
||||
<Picker.Item label="Trasporti" value="trasporti" />
|
||||
<Picker.Item label="Cibo" value="cibo" />
|
||||
<Picker.Item label="Varie" value="varie" />
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<Text style={styles.testo}>Data:</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={datePlaceholder}
|
||||
onFocus={() => setShowDatePicker(true)}
|
||||
/>
|
||||
{showDatePicker && (
|
||||
<DateTimePicker
|
||||
value={date}
|
||||
mode="date"
|
||||
display="default"
|
||||
onChange={(event, selectedDate) => {
|
||||
const currentDate = selectedDate || date;
|
||||
setShowDatePicker(false);
|
||||
setDate(currentDate);
|
||||
const formattedDate = `${currentDate.getDate().toString().padStart(2, '0')}-${(currentDate.getMonth() + 1).toString().padStart(2, '0')}-${currentDate.getFullYear()}`;
|
||||
setDatePlaceholder(formattedDate);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={{marginTop: 10}}>
|
||||
<Text style={styles.testo}>Tipo di Transazione:</Text>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, transactionType === 'income' && styles.selectedButton]}
|
||||
onPress={() => setTransactionType('income')}
|
||||
>
|
||||
<Text style={styles.buttonText}>Entrata (+)</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, transactionType === 'expense' && styles.selectedButton]}
|
||||
onPress={() => setTransactionType('expense')}
|
||||
>
|
||||
<Text style={styles.buttonText}>Uscita (-)</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.addButton} onPress={addTransaction}>
|
||||
<Text style={styles.addButtonText}>Aggiungi</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 20,
|
||||
},
|
||||
input: {
|
||||
color: '#fff',
|
||||
height: 50,
|
||||
borderColor: 'gray',
|
||||
borderWidth: 1,
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 15,
|
||||
fontSize: 16
|
||||
},
|
||||
picker: {
|
||||
color: '#fff',
|
||||
borderColor: 'gray',
|
||||
borderWidth: 2,
|
||||
},
|
||||
testo: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 20,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
backgroundColor: '#3c3c3c',
|
||||
borderRadius: 5,
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 3,
|
||||
},
|
||||
selectedButton: {
|
||||
backgroundColor: '#f57242',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: '#fff',
|
||||
padding: 15, // Aumenta l'altezza del pulsante
|
||||
borderRadius: 5,
|
||||
alignItems: 'center',
|
||||
},
|
||||
addButtonText: {
|
||||
color: '#000',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
341
components/transaction/TransactionItem.jsx
Normal file
341
components/transaction/TransactionItem.jsx
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, Text, FlatList, StyleSheet, Modal, TouchableOpacity, TextInput, Alert } from 'react-native';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { format } from 'date-fns';
|
||||
import { it } from 'date-fns/locale';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
|
||||
export default function TransactionItem() {
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [selectedTransaction, setSelectedTransaction] = useState(null);
|
||||
const [editDescription, setEditDescription] = useState('');
|
||||
const [editAmount, setEditAmount] = useState('');
|
||||
const [editCategory, setEditCategory] = useState('');
|
||||
const [editDate, setEditDate] = useState(new Date());
|
||||
const [editType, setEditType] = useState('expense');
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
|
||||
const loadTransactions = useCallback(async () => {
|
||||
try {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
const result = await db.getAllAsync('SELECT * FROM transactions ORDER BY date DESC, id DESC');
|
||||
setTransactions(result);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sortedTransactions = transactions.sort((a, b) => {
|
||||
const dateA = new Date(a.date.split('-').reverse().join('-'));
|
||||
const dateB = new Date(b.date.split('-').reverse().join('-'));
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
const groupedTransactions = sortedTransactions.reduce((acc, transaction) => {
|
||||
const date = new Date(transaction.date.split('-').reverse().join('-'));
|
||||
const formattedDate = format(date, 'dd MMMM yyyy', { locale: it });
|
||||
if (!acc[formattedDate]) {
|
||||
acc[formattedDate] = [];
|
||||
}
|
||||
acc[formattedDate].push(transaction);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadTransactions();
|
||||
}, [loadTransactions]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadTransactions();
|
||||
}, [loadTransactions])
|
||||
);
|
||||
|
||||
const openModal = (transaction) => {
|
||||
setSelectedTransaction(transaction);
|
||||
setEditDescription(transaction.description);
|
||||
setEditAmount(transaction.amount.toString());
|
||||
setEditCategory(transaction.category);
|
||||
setEditDate(new Date(transaction.date.split('-').reverse().join('-')));
|
||||
setEditType(transaction.type);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const updateTransaction = async () => {
|
||||
try {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
await db.runAsync(
|
||||
'UPDATE transactions SET description = ?, amount = ?, category = ?, date = ?, type = ? WHERE id = ?',
|
||||
[editDescription, parseFloat(editAmount), editCategory, format(editDate, 'dd-MM-yyyy'), editType, selectedTransaction.id]
|
||||
);
|
||||
setModalVisible(false);
|
||||
loadTransactions();
|
||||
Alert.alert('Successo', 'Transazione aggiornata con successo');
|
||||
} catch (error) {
|
||||
console.error('Error updating transaction:', error);
|
||||
Alert.alert('Errore', 'Impossibile aggiornare la transazione');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTransaction = async () => {
|
||||
Alert.alert(
|
||||
'Conferma eliminazione',
|
||||
'Sei sicuro di voler eliminare questa transazione?',
|
||||
[
|
||||
{ text: 'Annulla', style: 'cancel' },
|
||||
{
|
||||
text: 'Elimina',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
await db.runAsync('DELETE FROM transactions WHERE id = ?', [selectedTransaction.id]);
|
||||
setModalVisible(false);
|
||||
loadTransactions();
|
||||
Alert.alert('Successo', 'Transazione eliminata con successo');
|
||||
} catch (error) {
|
||||
console.error('Error deleting transaction:', error);
|
||||
Alert.alert('Errore', 'Impossibile eliminare la transazione');
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const [day, month, year] = dateString.split('-');
|
||||
const date = new Date(`${year}-${month}-${day}`);
|
||||
return format(date, 'dd MMMM yyyy', { locale: it });
|
||||
};
|
||||
|
||||
const sortedGroupedTransactions = Object.keys(groupedTransactions)
|
||||
.sort((a, b) => new Date(b.split(' ').reverse().join(' ')) - new Date(a.split(' ').reverse().join(' ')))
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = groupedTransactions[key];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<TouchableOpacity onPress={() => openModal(item)} style={styles.item}>
|
||||
<View>
|
||||
<Text style={styles.description}>{item.description}</Text>
|
||||
<Text style={styles.date}>{item.date}</Text>
|
||||
</View>
|
||||
<Text style={[styles.amount, { color: item.type === 'expense' ? 'red' : 'green' }]}>
|
||||
{item.type === 'expense' ? '-' : '+'}{Math.abs(item.amount).toFixed(2)} €
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<><FlatList
|
||||
data={Object.keys(groupedTransactions)}
|
||||
renderItem={({ item: date }) => (
|
||||
<View key={date}>
|
||||
<Text style={styles.dateHeader}>{date}</Text>
|
||||
<FlatList
|
||||
data={groupedTransactions[date]}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id.toString()} />
|
||||
</View>
|
||||
)}
|
||||
keyExtractor={item => item} /><Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.centeredView}>
|
||||
<View style={styles.modalView}>
|
||||
<Text style={styles.modalTitle}>Dettagli Transazione</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={editDescription}
|
||||
onChangeText={setEditDescription}
|
||||
placeholder="Descrizione" />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={editAmount}
|
||||
onChangeText={setEditAmount}
|
||||
placeholder="Importo"
|
||||
placeholderTextColor="#fff"
|
||||
keyboardType="decimal-pad" />
|
||||
<View style={{ borderColor: 'gray', borderWidth: 1, marginBottom: 10 }}>
|
||||
<Picker
|
||||
selectedValue={editCategory}
|
||||
style={styles.picker}
|
||||
onValueChange={(itemValue) => setEditCategory(itemValue)}
|
||||
>
|
||||
<Picker.Item label="Spesa" value="spesa" />
|
||||
<Picker.Item label="Svago" value="svago" />
|
||||
<Picker.Item label="Trasporti" value="trasporti" />
|
||||
<Picker.Item label="Cibo" value="cibo" />
|
||||
<Picker.Item label="Varie" value="varie" />
|
||||
</Picker>
|
||||
</View>
|
||||
<View style={{ borderColor: 'gray', borderWidth: 1, marginBottom: 10 }}>
|
||||
<Picker
|
||||
selectedValue={editType}
|
||||
style={styles.picker}
|
||||
onValueChange={(itemValue) => setEditType(itemValue)}
|
||||
>
|
||||
<Picker.Item label="Entrata" value="income" />
|
||||
<Picker.Item label="Uscita" value="expense" />
|
||||
</Picker>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setShowDatePicker(true)}>
|
||||
<Text style={styles.input}>{format(editDate, 'dd-MM-yyyy')}</Text>
|
||||
</TouchableOpacity>
|
||||
{showDatePicker && (
|
||||
<DateTimePicker
|
||||
value={editDate}
|
||||
mode="date"
|
||||
display="default"
|
||||
onChange={(event, selectedDate) => {
|
||||
const currentDate = selectedDate || editDate;
|
||||
setShowDatePicker(false);
|
||||
setEditDate(currentDate);
|
||||
} } />
|
||||
)}
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity style={styles.button} onPress={updateTransaction}>
|
||||
<Text style={styles.buttonText}>Aggiorna</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={deleteTransaction}>
|
||||
<Text style={styles.buttonText}>Elimina</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={() => setModalVisible(false)}>
|
||||
<Text style={styles.buttonText}>Chiudi</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal></>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
color: '',
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
marginVertical: 5,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#1a1a1a',
|
||||
},
|
||||
description: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
amount: {
|
||||
color: 'red',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
date: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
centeredView: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
modalView: {
|
||||
margin: 20,
|
||||
backgroundColor: '#000',
|
||||
borderRadius: 20,
|
||||
padding: 35,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
},
|
||||
modalTitle: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 15,
|
||||
},
|
||||
input: {
|
||||
height: 60,
|
||||
width: 302,
|
||||
borderColor: 'gray',
|
||||
borderWidth: 1,
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 15,
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
backgroundColor: '#1a1a1a'
|
||||
},
|
||||
picker: {
|
||||
height: 50,
|
||||
width: 300,
|
||||
color: '#fff',
|
||||
borderColor: 'gray',
|
||||
borderWidth: 1,
|
||||
backgroundColor: '#1a1a1a'
|
||||
},
|
||||
buttonContainer: {
|
||||
paddingTop: 20,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2196F3',
|
||||
borderRadius: 20,
|
||||
padding: 10,
|
||||
elevation: 2,
|
||||
marginTop: 10,
|
||||
minWidth: 100,
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#FF0000',
|
||||
},
|
||||
closeButton: {
|
||||
marginTop: 20,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
date: {
|
||||
color: '#8c8c8c',
|
||||
fontSize: 14,
|
||||
},
|
||||
dateHeader: {
|
||||
fontSize: 16,
|
||||
color: '#fff',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
112
components/ui/Grafico.tsx
Normal file
112
components/ui/Grafico.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, Dimensions, Pressable, StyleSheet, ScrollView } from "react-native";
|
||||
import { LineChart } from 'react-native-chart-kit';
|
||||
|
||||
const screenWidth = Dimensions.get("window").width;
|
||||
|
||||
const Grafico = () => {
|
||||
const [timeFrame, setTimeFrame] = useState("1D");
|
||||
|
||||
const data = {
|
||||
labels: ["Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Dom"],
|
||||
datasets: [
|
||||
{
|
||||
data: [20, 45, 28, 80, 70, 43, 100, 100],
|
||||
color: (opacity = 1) => `rgba(134, 65, 244, ${opacity})`,
|
||||
strokeWidth: 2
|
||||
}
|
||||
],
|
||||
legend: ["Money"]
|
||||
};
|
||||
|
||||
const handleTimeFrameChange = (newTimeFrame: React.SetStateAction<string>) => {
|
||||
setTimeFrame(newTimeFrame);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Contenitore con bordo per il grafico */}
|
||||
<View style={styles.chartContainer}>
|
||||
<LineChart
|
||||
data={data}
|
||||
width={screenWidth - 40}
|
||||
height={180}
|
||||
verticalLabelRotation={0}
|
||||
chartConfig={chartConfig}
|
||||
bezier
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* ScrollView per bottoni */}
|
||||
<ScrollView horizontal contentContainerStyle={styles.scrollContainer}>
|
||||
<View style={styles.buttonRow}>
|
||||
<Pressable onPress={() => handleTimeFrameChange("1D")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "1D" && styles.activeButton]}>1D</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => handleTimeFrameChange("1W")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "1W" && styles.activeButton]}>1W</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => handleTimeFrameChange("1M")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "1M" && styles.activeButton]}>1M</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => handleTimeFrameChange("July")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "July" && styles.activeButton]}>July</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => handleTimeFrameChange("June")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "June" && styles.activeButton]}>June</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => handleTimeFrameChange("May")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "May" && styles.activeButton]}>May</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => handleTimeFrameChange("All")}>
|
||||
<Text style={[styles.buttonText, timeFrame === "All" && styles.activeButton]}>All</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
|
||||
strokeWidth: 1,
|
||||
barPercentage: 0.5,
|
||||
useShadowColorFromDataset: false
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 20,
|
||||
},
|
||||
chartContainer: {
|
||||
borderWidth: 2,
|
||||
borderColor: 'black',
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
marginBottom: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
scrollContainer: {
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 16,
|
||||
color: 'white', // Cambia il colore del testo per contrasto
|
||||
backgroundColor: '#1a1a1a', // Aggiungi un background color per i bottoni
|
||||
padding: 10, // Rendi il bottone più compatto
|
||||
marginHorizontal: 5, // Spaziatura laterale più piccola
|
||||
borderRadius: 5, // Arrotonda i bordi
|
||||
},
|
||||
activeButton: {
|
||||
color: 'purple',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: 'lightgray', // Cambia il colore di sfondo per il bottone attivo
|
||||
}
|
||||
});
|
||||
|
||||
export default Grafico;
|
||||
324
components/ui/NewGrafico.jsx
Normal file
324
components/ui/NewGrafico.jsx
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, StyleSheet, Dimensions, Text, FlatList } from 'react-native';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { LineChart } from 'react-native-chart-kit';
|
||||
import { Modal, TouchableOpacity } from 'react-native';
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/it'; // Import Italian locale for moment
|
||||
|
||||
const VisualizzaDati = () => {
|
||||
const [data, setData] = useState([]);
|
||||
const [selectedTransactions, setSelectedTransactions] = useState([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState('');
|
||||
const [currentWeek, setCurrentWeek] = useState(moment().startOf('week'));
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetchData();
|
||||
}, 1000); // Aggiorna ogni 1 secondi
|
||||
|
||||
return () => clearInterval(interval); // Pulisce l'intervallo quando il componente viene smontato
|
||||
}, []);
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
|
||||
const result = await db.getAllAsync('SELECT * FROM transactions');
|
||||
const fetchedData = [];
|
||||
for (const row of result) {
|
||||
fetchedData.push(row);
|
||||
}
|
||||
|
||||
// Raggruppa i dati per data e calcola la somma degli importi per il grafico
|
||||
const groupedData = fetchedData.reduce((acc, curr) => {
|
||||
const date = curr.date;
|
||||
if (!acc[date]) {
|
||||
acc[date] = 0;
|
||||
}
|
||||
acc[date] += curr.amount;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Trasforma l'oggetto raggruppato in un array di oggetti e ordina per data
|
||||
const chartDataArray = Object.keys(groupedData)
|
||||
.map(date => ({
|
||||
date,
|
||||
amount: groupedData[date],
|
||||
}))
|
||||
.sort((a, b) => new Date(a.date.split('-').reverse().join('-')) - new Date(b.date.split('-').reverse().join('-')));
|
||||
|
||||
setData(chartDataArray);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const getWeekData = () => {
|
||||
const startOfWeek = currentWeek.clone().startOf('week');
|
||||
const endOfWeek = currentWeek.clone().endOf('week');
|
||||
const weekDates = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
weekDates.push(startOfWeek.clone().add(i, 'days').format('DD-MM-YYYY'));
|
||||
}
|
||||
const weekData = weekDates.map(date => {
|
||||
const item = data.find(d => d.date === date);
|
||||
return {
|
||||
date,
|
||||
amount: item ? item.amount : 0,
|
||||
};
|
||||
});
|
||||
return weekData;
|
||||
};
|
||||
|
||||
const getCurrentWeekText = () => {
|
||||
moment.locale('it'); // Set moment locale to Italian
|
||||
const startOfWeek = currentWeek.clone().startOf('week').format('DD MMMM');
|
||||
const endOfWeek = currentWeek.clone().endOf('week').format('DD MMMM');
|
||||
return `${startOfWeek} al ${endOfWeek}`;
|
||||
};
|
||||
|
||||
const chartData = {
|
||||
labels: getWeekData().map(item => item.date),
|
||||
datasets: [
|
||||
{
|
||||
data: getWeekData().map(item => item.amount),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const handleDataPointClick = async (dataPoint) => {
|
||||
const date = chartData.labels[dataPoint.index];
|
||||
setSelectedDate(date);
|
||||
try {
|
||||
const db = await SQLite.openDatabaseAsync('moneyAppDB');
|
||||
const result = await db.getAllAsync(`SELECT * FROM transactions WHERE date = ?`, [date]);
|
||||
setSelectedTransactions(result);
|
||||
setModalVisible(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions for selected date:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevWeek = () => {
|
||||
setCurrentWeek(currentWeek.clone().subtract(1, 'week'));
|
||||
};
|
||||
|
||||
const handleNextWeek = () => {
|
||||
setCurrentWeek(currentWeek.clone().add(1, 'week'));
|
||||
};
|
||||
|
||||
const getTotalForWeek = () => {
|
||||
const weekData = getWeekData();
|
||||
const total = weekData.reduce((acc, curr) => acc + curr.amount, 0);
|
||||
return total;
|
||||
};
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={[{ key: 'content' }]}
|
||||
renderItem={() => (
|
||||
<View style={styles.container}>
|
||||
{data.length === 0 ? (
|
||||
<Text style={styles.noDataText}>Nessun dato disponibile</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.weekText}>{getCurrentWeekText()}</Text>
|
||||
<Text style={[styles.totalText, { color: getTotalForWeek() >= 0 ? 'green' : 'red' }]}>
|
||||
Totale: {getTotalForWeek().toFixed(2)} €
|
||||
</Text>
|
||||
<LineChart
|
||||
bezier
|
||||
data={chartData}
|
||||
width={Dimensions.get('window').width - 20} // from react-native
|
||||
height={300}
|
||||
verticalLabelRotation={30}
|
||||
yAxisLabel="€"
|
||||
chartConfig={{
|
||||
backgroundColor: '#fff',
|
||||
backgroundGradientFrom: '#000',
|
||||
backgroundGradientTo: '#000',
|
||||
decimalPlaces: 2, // optional, defaults to 2dp
|
||||
color: (opacity = 1) => `#f57242`,
|
||||
labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
|
||||
style: {
|
||||
borderRadius: 10,
|
||||
},
|
||||
propsForDots: {
|
||||
r: '6',
|
||||
strokeWidth: '3',
|
||||
stroke: '#3956e6',
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 15,
|
||||
}}
|
||||
onDataPointClick={handleDataPointClick}
|
||||
/>
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.navButton}
|
||||
onPress={handlePrevWeek}
|
||||
>
|
||||
<Text style={styles.navButtonText}>Settimana Precedente</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.navButton}
|
||||
onPress={handleNextWeek}
|
||||
>
|
||||
<Text style={styles.navButtonText}>Settimana Successiva</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.centeredView}>
|
||||
<View style={styles.modalView}>
|
||||
<Text style={styles.modalTitle}>Transazioni del {selectedDate}</Text>
|
||||
<FlatList
|
||||
data={selectedTransactions}
|
||||
keyExtractor={(item, index) => index.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.transactionItem}>
|
||||
<View><Text style={styles.transactionDescription}>{item.description}</Text></View>
|
||||
<View>
|
||||
<Text style={[styles.transactionAmount, { color: item.type === 'expense' ? 'red' : 'green' }]}>
|
||||
{item.type === 'expense' ? '-' : '+'}{Math.abs(item.amount).toFixed(2)} €
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={() => setModalVisible(false)}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>Chiudi</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
keyExtractor={item => item.key}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 20,
|
||||
padding: 10,
|
||||
},
|
||||
buttonContainer: {
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row',
|
||||
marginVertical: 10,
|
||||
},
|
||||
noDataText: {
|
||||
color: '#000',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginTop: 20,
|
||||
},
|
||||
transactionsContainer: {
|
||||
},
|
||||
transactionsTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
marginVertical: 15,
|
||||
textAlign: 'center',
|
||||
},
|
||||
transactionItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 15,
|
||||
marginVertical: 5,
|
||||
borderRadius: 10,
|
||||
width: '100%',
|
||||
backgroundColor: '#1a1a1a',
|
||||
},
|
||||
transactionDate: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
transactionDescription: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
transactionAmount: {
|
||||
color: 'red',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
centeredView: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 22,
|
||||
},
|
||||
modalView: {
|
||||
margin: 20,
|
||||
height: 500,
|
||||
backgroundColor: '#000',
|
||||
borderRadius: 20,
|
||||
borderColor: '#3c3c3c',
|
||||
borderWidth: 2,
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 15,
|
||||
textAlign: 'center',
|
||||
color: '#fff'
|
||||
},
|
||||
navButton: {
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
margin: 2,
|
||||
backgroundColor: '#f57242',
|
||||
},
|
||||
navButtonText: {
|
||||
color: '#000',
|
||||
fontSize: 16,
|
||||
},
|
||||
closeButton: {
|
||||
marginTop: 20,
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
closeButtonText: {
|
||||
color: '#000',
|
||||
fontSize: 16,
|
||||
},
|
||||
weekText: {
|
||||
color: '#fff',
|
||||
fontSize: 22,
|
||||
textAlign: 'center',
|
||||
paddingBottom: 20
|
||||
},
|
||||
totalText: {
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default VisualizzaDati;
|
||||
|
||||
|
||||
|
||||
11
components/ui/TransactionCard.jsx
Normal file
11
components/ui/TransactionCard.jsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { View, Text } from 'react-native'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
export default function TransactionCard() {
|
||||
return (
|
||||
<View>
|
||||
<Text>TransactionCard</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
4
components/useClientOnlyValue.ts
Normal file
4
components/useClientOnlyValue.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
||||
return client;
|
||||
}
|
||||
12
components/useClientOnlyValue.web.ts
Normal file
12
components/useClientOnlyValue.web.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
// `useEffect` is not invoked during server rendering, meaning
|
||||
// we can use this to determine if we're on the server or not.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
||||
const [value, setValue] = React.useState<S | C>(server);
|
||||
React.useEffect(() => {
|
||||
setValue(client);
|
||||
}, [client]);
|
||||
|
||||
return value;
|
||||
}
|
||||
1
components/useColorScheme.ts
Normal file
1
components/useColorScheme.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useColorScheme } from 'react-native';
|
||||
8
components/useColorScheme.web.ts
Normal file
8
components/useColorScheme.web.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// NOTE: The default React Native styling doesn't support server rendering.
|
||||
// Server rendered styles should not change between the first render of the HTML
|
||||
// and the first render on the client. Typically, web developers will use CSS media queries
|
||||
// to render different styles on the client and server, these aren't directly supported in React Native
|
||||
// but can be achieved using a styling library like Nativewind.
|
||||
export function useColorScheme() {
|
||||
return 'dark';
|
||||
}
|
||||
19
constants/Colors.ts
Normal file
19
constants/Colors.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const tintColorLight = '#2f95dc';
|
||||
const tintColorDark = '#fff';
|
||||
|
||||
export default {
|
||||
light: {
|
||||
text: '#000',
|
||||
background: '#fff',
|
||||
tint: tintColorLight,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#fff',
|
||||
background: '#000',
|
||||
tint: tintColorDark,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
49
package.json
Normal file
49
package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "moneyapp",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"test": "jest --watchAll"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-native-community/datetimepicker": "^8.2.0",
|
||||
"@react-native-picker/picker": "2.7.5",
|
||||
"@react-navigation/native": "^6.0.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"expo": "~51.0.28",
|
||||
"expo-font": "~12.0.9",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-router": "~3.5.23",
|
||||
"expo-splash-screen": "~0.27.5",
|
||||
"expo-sqlite": "~14.0.6",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"moment": "^2.30.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.5",
|
||||
"react-native-chart-kit": "^6.12.0",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"react-native-screens": "3.31.1",
|
||||
"react-native-web": "~0.19.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~18.2.45",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~51.0.3",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "~5.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue