import 'dart:convert'; import 'package:awesome_dialog/awesome_dialog.dart'; import 'package:flutter_neumorphic/flutter_neumorphic.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:tests/preferences.dart'; import 'package:tests/theme/theme_constants.dart'; import 'package:tests/theme/theme_manager.dart'; void main() { runApp(const FinancialPlannerApp()); } ThemeManager _themeManager = ThemeManager(); class FinancialPlannerApp extends StatelessWidget { const FinancialPlannerApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Financial Planner', theme: lightTheme, darkTheme: darkTheme, themeMode: _themeManager.themeMode, home: const HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override HomePageState createState() => HomePageState(); } class HomePageState extends State { List accounts = []; @override void initState() { super.initState(); _themeManager.addListener(themeListener); loadAccounts(); } @override void dispose() { _themeManager.removeListener(themeListener); super.dispose(); } themeListener(){ if(mounted){ setState(() { }); } } Future loadAccounts() async { SharedPreferences prefs = await SharedPreferences.getInstance(); final accountList = prefs.getStringList('accounts') ?? []; setState(() { accounts = accountList.map((accountJson) => Account.fromJson(accountJson)).toList(); }); } Future saveAccounts() async { SharedPreferences prefs = await SharedPreferences.getInstance(); List accountJsonList = accounts.map((account) => json.encode(account.toJson())).toList(); await prefs.setStringList('accounts', accountJsonList); } void addAccount(Account account) { setState(() { accounts.add(account); saveAccounts(); }); } void deleteAccount(Account account) { setState(() { accounts.remove(account); saveAccounts(); }); } void updateAccountBalance(Account account) { setState(() { saveAccounts(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, backgroundColor: Colors.transparent, elevation: 0, title: const Text( 'Financial Planner', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black54, ), ), actions: [ Padding( padding: const EdgeInsets.only(right: 16.0, top: 3, bottom: 7), child: NeumorphicButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => Settings()), ); }, style: NeumorphicStyle( shape: NeumorphicShape.convex, boxShape: NeumorphicBoxShape.circle(), depth: 6, intensity: 0.9, color: Colors.grey.shade100, ), child: Icon(Icons.settings, color: Colors.black38), ), ), ], ), body: ListView.builder( physics: const BouncingScrollPhysics(), itemCount: accounts.length, itemBuilder: (context, index) { return GestureDetector( onLongPress: () { AwesomeDialog( btnOkText: "Delete", btnOkColor: Colors.lightGreen, btnCancelColor: Colors.grey, context: context, animType: AnimType.bottomSlide, dialogType: DialogType.info, title: 'Delete Account', headerAnimationLoop: false, desc: 'Are you sure you want to delete this account?', btnCancelOnPress: () {}, btnOkOnPress: () { deleteAccount(accounts[index]); }, ).show(); }, child: Neumorphic( margin: const EdgeInsets.all(16), style: NeumorphicStyle( depth: 7, intensity: 1, shadowDarkColor: Colors.grey.shade300, color: Colors.grey.shade100, boxShape: NeumorphicBoxShape.roundRect(BorderRadius.circular(15)), ), child: ListTile( title: Text(accounts[index].name), subtitle: Text('Balance: \$${accounts[index].balance.toStringAsFixed(2)}'), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => AccountDetailPage( account: accounts[index], updateAccountBalance: updateAccountBalance, ), ), ); }, ), ), ); }, ), floatingActionButton: NeumorphicButton( onPressed: () { showDialog( context: context, builder: (BuildContext context) { return AddAccountDialog( addAccount: addAccount, ); }, ); }, style: NeumorphicStyle( depth: 8, intensity: 1, shadowDarkColor: Colors.grey.shade400, color: Colors.grey.shade100, boxShape: const NeumorphicBoxShape.circle(), ), child: const Icon(Icons.add,size:60,color: Colors.black12,), ), ); } } class Account { String name; double balance; Account({ required this.name, required this.balance, }); factory Account.fromJson(String json) { final Map map = jsonDecode(json); return Account( name: map['name'], balance: map['balance'], ); } Map toJson() { return { 'name': name, 'balance': balance, }; } } class AddAccountDialog extends StatefulWidget { final Function addAccount; const AddAccountDialog({super.key, required this.addAccount}); @override AddAccountDialogState createState() => AddAccountDialogState(); } class AddAccountDialogState extends State { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _balanceController = TextEditingController(); @override void dispose() { _nameController.dispose(); _balanceController.dispose(); super.dispose(); } void _submitForm() { if (_formKey.currentState!.validate()) { String name = _nameController.text.trim(); double balance = double.parse(_balanceController.text.trim()); Account account = Account( name: name, balance: balance, ); widget.addAccount(account); Navigator.of(context).pop(); } } @override Widget build(BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), title: const Text( 'Add Account', ), titleTextStyle: TextStyle( color: Colors.black54, fontSize: 20, ), content: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ Neumorphic( style: NeumorphicStyle( depth: -5, intensity: 0.8, color: Colors.grey.shade100, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), ), child: TextFormField( controller: _nameController, decoration: InputDecoration( labelText: 'Name', border: InputBorder.none, contentPadding: EdgeInsets.fromLTRB(12, 16, 12, 16), ), validator: (value) { if (value!.isEmpty) { return 'Please enter a name'; } return null; }, ), ), SizedBox(height: 16), Neumorphic( style: NeumorphicStyle( depth: -5, intensity: 0.8, color: Colors.grey.shade100, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), ), child: TextFormField( controller: _balanceController, decoration: InputDecoration( labelText: 'Balance', border: InputBorder.none, contentPadding: EdgeInsets.fromLTRB(12, 16, 12, 16), ), keyboardType: TextInputType.number, validator: (value) { if (value!.isEmpty) { return 'Please enter a balance'; } if (double.tryParse(value) == null) { return 'Please enter a valid number'; } return null; }, ), ), ], ), ), actions: [ NeumorphicButton( onPressed: () { Navigator.of(context).pop(); }, style: NeumorphicStyle( shape: NeumorphicShape.concave, intensity: 0.9, depth: 9, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), color: Colors.grey.shade100, ), padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: Text( 'Cancel', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), NeumorphicButton( onPressed: _submitForm, style: NeumorphicStyle( shape: NeumorphicShape.concave, intensity: 0.8, depth: 9, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), color: Colors.grey.shade100, ), padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: Text( 'Add', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ); } } class AccountDetailPage extends StatefulWidget { final Account account; final Function(Account) updateAccountBalance; const AccountDetailPage({super.key, required this.account, required this.updateAccountBalance}); @override AccountDetailPageState createState() => AccountDetailPageState(); } class AccountDetailPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; List transactions = []; List incomeTransactions = []; List expenseTransactions = []; List expenseData = []; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); loadTransactions(); } @override void dispose() { _tabController.dispose(); super.dispose(); } void loadTransactions() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String? transactionsJson = prefs.getString(widget.account.name); if (transactionsJson != null) { List decodedJson = jsonDecode(transactionsJson); setState(() { transactions = decodedJson.map((json) => Transaction.fromJson(json)).toList(); incomeTransactions = transactions.where((transaction) => !transaction.isExpense).toList(); expenseTransactions = transactions.where((transaction) => transaction.isExpense).toList(); expenseData = calculateMonthlyExpenses(); }); } } void saveTransactions() async { SharedPreferences prefs = await SharedPreferences.getInstance(); List transactionsJsonList = transactions.map((transaction) => json.encode(transaction.toJson())).toList(); prefs.setString(widget.account.name, jsonEncode(transactionsJsonList)); } void addTransaction(Transaction transaction) { setState(() { transactions.add(transaction); if (transaction.isExpense) { widget.account.balance -= transaction.amount; expenseTransactions.add(transaction); } else { widget.account.balance += transaction.amount; incomeTransactions.add(transaction); } saveTransactions(); widget.updateAccountBalance(widget.account); expenseData = calculateMonthlyExpenses(); }); } void deleteTransaction(Transaction transaction) { setState(() { transactions.remove(transaction); if (transaction.isExpense) { widget.account.balance += transaction.amount; expenseTransactions.remove(transaction); } else { widget.account.balance -= transaction.amount; incomeTransactions.remove(transaction); } saveTransactions(); widget.updateAccountBalance(widget.account); expenseData = calculateMonthlyExpenses(); }); } List calculateMonthlyExpenses() { Map monthlyExpenses = {}; for (var transaction in expenseTransactions) { String month = DateFormat('yyyy-MM').format(transaction.date); monthlyExpenses[month] = (monthlyExpenses[month] ?? 0) + transaction.amount; } List expenseData = []; monthlyExpenses.forEach((month, amount) { expenseData.add(ExpenseData(month: month, amount: amount)); }); return expenseData; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, toolbarHeight: 80, title: Text( widget.account.name, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black54, ), ), centerTitle: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15), ), ), shadowColor: Colors.grey.shade300, leading: Padding( padding: const EdgeInsets.only(left: 16.0), child: NeumorphicButton( onPressed: () { Navigator.pop(context); }, style: NeumorphicStyle( shape: NeumorphicShape.flat, boxShape: NeumorphicBoxShape.circle(), depth: 6, intensity: 0.9, color: Colors.grey.shade100, ), padding: EdgeInsets.all(10), child: Icon(Icons.arrow_back, color: Colors.black38), ), ), ), body: Column( children: [ TabBar( controller: _tabController, labelColor: Colors.black, tabs: const [ Tab(text: 'Einnahmen'), Tab(text: 'Ausgaben'), ], ), Expanded( child: TabBarView( controller: _tabController, children: [ _buildTransactionsList(incomeTransactions), Column( children: [ Expanded(child: _buildTransactionsList(expenseTransactions)), if (expenseTransactions.isNotEmpty) _buildExpenseChart(), ], ), ], ), ), ], ), floatingActionButton: NeumorphicButton( onPressed: () => showDialog( context: context, builder: (_) => AddTransactionDialog(addTransaction: addTransaction), ), style: NeumorphicStyle( depth: 8, intensity: 1, shadowDarkColor: Colors.grey.shade400, color: Colors.grey.shade100, boxShape: NeumorphicBoxShape.circle(), ), padding: EdgeInsets.all(16), child: Icon( Icons.add, size: 40, color: Colors.black12, ), ), ); } Widget _buildTransactionsList(List transactionsList) { return ListView.builder( itemCount: transactionsList.length, itemBuilder: (context, index) { return ListTile( title: Text(transactionsList[index].title), subtitle: Text(transactionsList[index].title), trailing: Text( transactionsList[index].isExpense ? '-\$${transactionsList[index].amount.toStringAsFixed(2)}' : '+\$${transactionsList[index].amount.toStringAsFixed(2)}', style: TextStyle( color: transactionsList[index].isExpense ? Colors.red : Colors.green, fontWeight: FontWeight.bold, ), ), onLongPress: () => deleteTransaction(transactionsList[index]), ); }, ); } Widget _buildExpenseChart() { return Padding( padding: const EdgeInsets.all(8.0), child: Card( elevation: 4.0, child: Padding( padding: const EdgeInsets.all(16.0), child: MonthlyExpensesChart(data: expenseData), ), ), ); } } class ExpenseData { final String month; final double amount; ExpenseData({required this.month, required this.amount}); } class Transaction { String title; double amount; bool isExpense; DateTime date; Transaction({ required this.title, required this.amount, required this.isExpense, required this.date, }); factory Transaction.fromJson(String json) { final Map map = jsonDecode(json); return Transaction( title: map['title'], amount: map['amount'], isExpense: map['isExpense'], date: DateTime.parse(map['date']), ); } Map toJson() { return { 'title': title, 'amount': amount, 'isExpense': isExpense, 'date': date.toString(), }; } } class AddTransactionDialog extends StatefulWidget { final Function addTransaction; const AddTransactionDialog({super.key, required this.addTransaction}); @override AddTransactionDialogState createState() => AddTransactionDialogState(); } class AddTransactionDialogState extends State { final _formKey = GlobalKey(); final _titleController = TextEditingController(); final _amountController = TextEditingController(); bool _isExpense = true; @override void dispose() { _titleController.dispose(); _amountController.dispose(); super.dispose(); } void _submitForm() { if (_formKey.currentState!.validate()) { String title = _titleController.text.trim(); double amount = double.parse(_amountController.text.trim()); Transaction transaction = Transaction( title: title, amount: amount, isExpense: _isExpense, date: DateTime.now(), ); widget.addTransaction(transaction); Navigator.of(context).pop(); } } @override Widget build(BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), title: Text( 'Add Transaction', style: TextStyle( fontSize: 20, color: Colors.black54, ), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Neumorphic( style: NeumorphicStyle( depth: -5, intensity: 0.8, color: Colors.grey.shade100, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), ), child: TextFormField( controller: _titleController, decoration: InputDecoration( labelText: 'Title', border: InputBorder.none, contentPadding: EdgeInsets.symmetric( vertical: 12, horizontal: 16, ), ), validator: (value) { if (value!.isEmpty) { return 'Please enter a title'; } return null; }, ), ), SizedBox(height: 16), Neumorphic( style: NeumorphicStyle( depth: -5, intensity: 0.8, color: Colors.grey.shade100, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), ), child: TextFormField( controller: _amountController, decoration: InputDecoration( labelText: 'Amount', border: InputBorder.none, contentPadding: EdgeInsets.symmetric( vertical: 12, horizontal: 16, ), ), keyboardType: TextInputType.number, validator: (value) { if (value!.isEmpty) { return 'Please enter an amount'; } if (double.tryParse(value) == null) { return 'Please enter a valid number'; } return null; }, ), ), SizedBox(height: 16), Row( children: [ NeumorphicCheckbox( style: NeumorphicCheckboxStyle(selectedColor: Colors.lightGreen, disabledColor: Colors.grey.shade200,selectedDepth: -10, unselectedDepth: 8), value: _isExpense, onChanged: (value) { setState(() { _isExpense = value!; }); }, ), SizedBox(width: 8), Text('Expense', style: TextStyle(color: Colors.black87),), ], ), ], ), actions: [ NeumorphicButton( onPressed: () { Navigator.of(context).pop(); }, style: NeumorphicStyle( shape: NeumorphicShape.concave, intensity: 0.8, depth: 9, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), color: Colors.grey.shade100, ), padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: Text( 'Cancel', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), NeumorphicButton( onPressed: _submitForm, style: NeumorphicStyle( shape: NeumorphicShape.concave, intensity: 0.8, depth: 9, boxShape: NeumorphicBoxShape.roundRect( BorderRadius.circular(12), ), color: Colors.grey.shade100, ), padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: Text( 'Add', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ); } } class MonthlyExpensesChart extends StatelessWidget { final List data; const MonthlyExpensesChart({super.key, required this.data}); @override Widget build(BuildContext context) { return SizedBox( height: 300, child: SfCartesianChart( primaryXAxis: CategoryAxis(), series: [ ColumnSeries( dataSource: data, xValueMapper: (ExpenseData expense, _) => expense.month, yValueMapper: (ExpenseData expense, _) => expense.amount, color: Colors.blue, ), ], ), ); } }