restore repo

This commit is contained in:
PanSi21 2024-11-19 02:04:30 +01:00
parent 97e8ee6eb8
commit f2a798ab18
Signed by untrusted user who does not match committer: PanSi21
GPG key ID: 755F8874C65EF462
30 changed files with 1627 additions and 2 deletions

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
)
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View 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',
},
});

View 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);
}
}}
/>
);
}

View 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
View 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} />;
}

View 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();
});

View 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,
},
});

View 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
View 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;

View 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;

View 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>
)
}

View 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;
}

View 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;
}

View file

@ -0,0 +1 @@
export { useColorScheme } from 'react-native';

View 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
View 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
View 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
}