Code Cleanup
parent
5901774fc7
commit
3435b1917c
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class Account {
|
||||||
|
String name;
|
||||||
|
double balance;
|
||||||
|
|
||||||
|
Account({
|
||||||
|
required this.name,
|
||||||
|
required this.balance,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Account.fromJson(String json) {
|
||||||
|
final Map<String, dynamic> map = jsonDecode(json);
|
||||||
|
return Account(
|
||||||
|
name: map['name'],
|
||||||
|
balance: map['balance'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'balance': balance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,377 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:circular_seek_bar/circular_seek_bar.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:tests/saving_tips.dart';
|
||||||
|
import 'package:tests/transaction/transaction_dialog.dart';
|
||||||
|
import 'package:tests/transaction/transaction.dart';
|
||||||
|
import 'account.dart';
|
||||||
|
import '../chart/expense_chart.dart';
|
||||||
|
import '../chart/expense_data.dart';
|
||||||
|
import 'package:tab_indicator_styler/tab_indicator_styler.dart';
|
||||||
|
|
||||||
|
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<AccountDetailPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
List<Transaction> transactions = [];
|
||||||
|
List<Transaction> incomeTransactions = [];
|
||||||
|
List<Transaction> expenseTransactions = [];
|
||||||
|
List<ExpenseData> expenseData = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
|
loadTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
_budgetController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadTransactions() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
String? transactionsJson = prefs.getString(widget.account.name);
|
||||||
|
if (transactionsJson != null) {
|
||||||
|
List<dynamic> 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<String> 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<ExpenseData> calculateMonthlyExpenses() {
|
||||||
|
Map<String, double> monthlyExpenses = {};
|
||||||
|
for (var transaction in expenseTransactions) {
|
||||||
|
String month = DateFormat('yyyy-MM').format(transaction.date);
|
||||||
|
monthlyExpenses[month] =
|
||||||
|
(monthlyExpenses[month] ?? 0) + transaction.amount;
|
||||||
|
}
|
||||||
|
List<ExpenseData> expenseData = [];
|
||||||
|
monthlyExpenses.forEach((month, amount) {
|
||||||
|
expenseData.add(ExpenseData(month: month, amount: amount));
|
||||||
|
});
|
||||||
|
return expenseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
final _budgetController = TextEditingController();
|
||||||
|
double progress = 0;
|
||||||
|
|
||||||
|
double submitbudget() {
|
||||||
|
return progress = double.parse(_budgetController.text.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10.0),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.info,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => SavingsTipsDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
toolbarHeight: 80,
|
||||||
|
title: Text(
|
||||||
|
widget.account.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
shape: const 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: const NeumorphicBoxShape.circle(),
|
||||||
|
depth: 6,
|
||||||
|
intensity: 0.9,
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: const Icon(Icons.arrow_back, color: Colors.black38),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
labelColor: Colors.black,
|
||||||
|
unselectedLabelColor: Colors.black54,
|
||||||
|
indicator: MaterialIndicator(
|
||||||
|
height: 4,
|
||||||
|
color: Colors.black54,
|
||||||
|
topLeftRadius: 8,
|
||||||
|
topRightRadius: 8,
|
||||||
|
horizontalPadding: 45,
|
||||||
|
tabPosition: TabPosition.bottom,
|
||||||
|
),
|
||||||
|
tabs: [
|
||||||
|
Tab(text: 'income'.tr()),
|
||||||
|
Tab(text: 'expenditures'.tr()),
|
||||||
|
Tab(text: 'budget'.tr())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
_buildTransactionsList(incomeTransactions),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTransactionsList(expenseTransactions)),
|
||||||
|
if (expenseTransactions.isNotEmpty) _buildExpenseChart(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
monthlybudgetplanner(),
|
||||||
|
Neumorphic(
|
||||||
|
margin: const EdgeInsets.all(14),
|
||||||
|
style: NeumorphicStyle(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
boxShape: NeumorphicBoxShape.roundRect(
|
||||||
|
BorderRadius.circular(15)),
|
||||||
|
depth: -5,
|
||||||
|
intensity: 0.8,
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'enteramount'.tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16, bottom: 8, top: 8),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
NeumorphicButton(
|
||||||
|
onPressed: () {
|
||||||
|
progress = submitbudget();
|
||||||
|
},
|
||||||
|
style: NeumorphicStyle(
|
||||||
|
boxShape: NeumorphicBoxShape.roundRect(
|
||||||
|
BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
depth: 8,
|
||||||
|
intensity: 0.9,
|
||||||
|
),
|
||||||
|
child: Text('add'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: const NeumorphicBoxShape.circle(),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.black12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ValueNotifier<double> _valueNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
|
Widget monthlybudgetplanner() {
|
||||||
|
return CircularSeekBar(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 300,
|
||||||
|
trackColor: Colors.black12,
|
||||||
|
progress: 500,
|
||||||
|
minProgress: 0,
|
||||||
|
maxProgress: 800,
|
||||||
|
barWidth: 17,
|
||||||
|
startAngle: 45,
|
||||||
|
sweepAngle: 270,
|
||||||
|
strokeCap: StrokeCap.butt,
|
||||||
|
progressGradientColors: const [
|
||||||
|
Colors.lightGreenAccent,
|
||||||
|
Colors.lightGreen,
|
||||||
|
Colors.green,
|
||||||
|
Colors.yellowAccent,
|
||||||
|
Colors.yellow,
|
||||||
|
Colors.orangeAccent,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.deepOrangeAccent,
|
||||||
|
Colors.deepOrange,
|
||||||
|
Colors.redAccent,
|
||||||
|
Colors.red
|
||||||
|
],
|
||||||
|
innerThumbRadius: 0,
|
||||||
|
innerThumbStrokeWidth: 12,
|
||||||
|
innerThumbColor: Colors.white,
|
||||||
|
outerThumbRadius: 0,
|
||||||
|
outerThumbStrokeWidth: 15,
|
||||||
|
outerThumbColor: Colors.blueAccent,
|
||||||
|
dashWidth: 1.5,
|
||||||
|
dashGap: 1.9,
|
||||||
|
animation: true,
|
||||||
|
animDurationMillis: 2200,
|
||||||
|
curves: Curves.fastOutSlowIn,
|
||||||
|
valueNotifier: _valueNotifier,
|
||||||
|
child: Center(
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: _valueNotifier,
|
||||||
|
builder: (_, double value, __) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${value.round()}' '€',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'progress'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTransactionsList(List<Transaction> 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
||||||
|
import 'account.dart';
|
||||||
|
|
||||||
|
class AddAccountDialog extends StatefulWidget {
|
||||||
|
final Function addAccount;
|
||||||
|
|
||||||
|
const AddAccountDialog({super.key, required this.addAccount});
|
||||||
|
|
||||||
|
@override
|
||||||
|
AddAccountDialogState createState() => AddAccountDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddAccountDialogState extends State<AddAccountDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
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: Text(
|
||||||
|
'addaccount'.tr(),
|
||||||
|
),
|
||||||
|
titleTextStyle: const 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: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(12, 16, 12, 16),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.isEmpty) {
|
||||||
|
return 'entername'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const 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'.tr(),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(12, 16, 12, 16),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.isEmpty) {
|
||||||
|
return 'enterbalance'.tr();
|
||||||
|
}
|
||||||
|
if (double.tryParse(value) == null) {
|
||||||
|
return 'entervalidnumber'.tr();
|
||||||
|
}
|
||||||
|
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: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'cancel'.tr(),
|
||||||
|
style: const 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: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'add'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"title" : "Finanzplaner",
|
"title" : "Finanzplaner",
|
||||||
"delete" : "Löschen",
|
"delete" : "Löschen",
|
||||||
"deleteaccount" : "Account Löschen",
|
"deleteaccount" : "Account löschen",
|
||||||
"sure" : "Sind Sie sicher, dass Sie dieses Konto löschen möchten?",
|
"sure" : "Sind Sie sicher, dass Sie dieses Konto löschen möchten?",
|
||||||
"balance" : "Bilanz",
|
"balance" : "Bilanz",
|
||||||
"addaccount" : "Konto hinzufügen",
|
"addaccount" : "Konto hinzufügen",
|
||||||
|
@ -18,7 +18,9 @@
|
||||||
"enteramount": "Bitte geben Sie einen Betrag ein",
|
"enteramount": "Bitte geben Sie einen Betrag ein",
|
||||||
"expense": "Ausgabe",
|
"expense": "Ausgabe",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"darkmode": "Dunkel Modus",
|
"darkmode": "Dark Mode",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"currency": "Währung"
|
"currency": "Währung",
|
||||||
|
"budget": "Budgetcheck",
|
||||||
|
"progress": "von deinem Budget ausgegeben"
|
||||||
}
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"title" : "Financial Planner",
|
"title" : "Financial Planner",
|
||||||
"delete" : "Delete",
|
"delete" : "Delete",
|
||||||
"deleteaccount" : "Delete Account",
|
"deleteaccount" : "Delete account",
|
||||||
"sure" : "Are you sure you want to delete this account?",
|
"sure" : "Are you sure you want to delete this account?",
|
||||||
"balance" : "Balance",
|
"balance" : "Balance",
|
||||||
"addaccount" : "Add Account",
|
"addaccount" : "Add account",
|
||||||
"entername" : "Please enter a name",
|
"entername" : "Please enter a name",
|
||||||
"enterbalance" : "Please enter a balance",
|
"enterbalance" : "Please enter a balance",
|
||||||
"entervalidnumber" : "Please enter a valid number",
|
"entervalidnumber" : "Please enter a valid number",
|
||||||
"cancel" : "Cancel",
|
"cancel" : "Cancel",
|
||||||
"add" : "Add",
|
"add" : "Add",
|
||||||
"income": "Income",
|
"income": "Income",
|
||||||
"expenditures": "Expenditures",
|
"expenditures": "Expenses",
|
||||||
"addtrans": "Add Transaction",
|
"addtrans": "Add transaction",
|
||||||
"entertitle": "Please enter a title",
|
"entertitle": "Please enter a title",
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"enteramount": "Please enter an amount",
|
"enteramount": "Please enter an amount",
|
||||||
|
@ -20,5 +20,7 @@
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"darkmode": "Dark Mode",
|
"darkmode": "Dark Mode",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"currency": "Currency"
|
"currency": "Currency",
|
||||||
|
"budget": "Budget check",
|
||||||
|
"progress": "of your budget spent"
|
||||||
}
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||||
|
import 'expense_data.dart';
|
||||||
|
|
||||||
|
class MonthlyExpensesChart extends StatelessWidget {
|
||||||
|
final List<ExpenseData> data;
|
||||||
|
|
||||||
|
const MonthlyExpensesChart({super.key, required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: SfCartesianChart(
|
||||||
|
primaryXAxis: CategoryAxis(),
|
||||||
|
series: <ChartSeries>[
|
||||||
|
ColumnSeries<ExpenseData, String>(
|
||||||
|
dataSource: data,
|
||||||
|
xValueMapper: (ExpenseData expense, _) => expense.month,
|
||||||
|
yValueMapper: (ExpenseData expense, _) => expense.amount,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
class ExpenseData {
|
||||||
|
final String month;
|
||||||
|
final double amount;
|
||||||
|
|
||||||
|
ExpenseData({required this.month, required this.amount});
|
||||||
|
}
|
735
lib/main.dart
735
lib/main.dart
|
@ -2,25 +2,31 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:awesome_dialog/awesome_dialog.dart';
|
import 'package:awesome_dialog/awesome_dialog.dart';
|
||||||
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
||||||
import 'package:tests/preferences.dart';
|
import 'package:tests/preferences.dart';
|
||||||
import 'package:tests/saving_tips.dart';
|
|
||||||
import 'package:tests/theme/theme_constants.dart';
|
import 'package:tests/theme/theme_constants.dart';
|
||||||
import 'package:tests/theme/theme_manager.dart';
|
import 'package:tests/theme/theme_manager.dart';
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
|
|
||||||
|
import 'account/account_dialog.dart';
|
||||||
|
import 'account/account.dart';
|
||||||
|
import 'account/account_detail.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
runApp(EasyLocalization(
|
runApp(
|
||||||
supportedLocales: [Locale('en', 'US'), Locale('de', 'DE')],
|
EasyLocalization(
|
||||||
path: 'lib/assets/translations', // <-- change the path of the translation files
|
supportedLocales: const [Locale('en', 'US'), Locale('de', 'DE')],
|
||||||
fallbackLocale: Locale('en', 'US'),
|
path: 'lib/assets/translations',
|
||||||
child: const FinancialPlannerApp()
|
// <-- change the path of the translation files
|
||||||
),);
|
fallbackLocale: const Locale('en', 'US'),
|
||||||
|
child: const FinancialPlannerApp()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeManager _themeManager = ThemeManager();
|
ThemeManager _themeManager = ThemeManager();
|
||||||
|
|
||||||
class FinancialPlannerApp extends StatelessWidget {
|
class FinancialPlannerApp extends StatelessWidget {
|
||||||
|
@ -57,32 +63,35 @@ class HomePageState extends State<HomePage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_themeManager.addListener(themeListener);
|
_themeManager.addListener(themeListener);
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_themeManager.removeListener(themeListener);
|
_themeManager.removeListener(themeListener);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
themeListener(){
|
|
||||||
if(mounted){
|
|
||||||
setState(() {
|
|
||||||
|
|
||||||
});
|
themeListener() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadAccounts() async {
|
Future<void> loadAccounts() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
final accountList = prefs.getStringList('accounts') ?? [];
|
final accountList = prefs.getStringList('accounts') ?? [];
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
accounts = accountList.map((accountJson) => Account.fromJson(accountJson)).toList();
|
accounts = accountList
|
||||||
|
.map((accountJson) => Account.fromJson(accountJson))
|
||||||
|
.toList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveAccounts() async {
|
Future<void> saveAccounts() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
List<String> accountJsonList = accounts.map((account) => json.encode(account.toJson())).toList();
|
List<String> accountJsonList =
|
||||||
|
accounts.map((account) => json.encode(account.toJson())).toList();
|
||||||
await prefs.setStringList('accounts', accountJsonList);
|
await prefs.setStringList('accounts', accountJsonList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,23 +135,20 @@ class HomePageState extends State<HomePage> {
|
||||||
padding: const EdgeInsets.only(right: 16.0, top: 3, bottom: 7),
|
padding: const EdgeInsets.only(right: 16.0, top: 3, bottom: 7),
|
||||||
child: NeumorphicButton(
|
child: NeumorphicButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
await Navigator.push(
|
||||||
String value = await Navigator.push(
|
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => Settings()),
|
MaterialPageRoute(builder: (context) => const Settings()),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {});
|
||||||
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
style: NeumorphicStyle(
|
style: NeumorphicStyle(
|
||||||
shape: NeumorphicShape.convex,
|
shape: NeumorphicShape.convex,
|
||||||
boxShape: NeumorphicBoxShape.circle(),
|
boxShape: const NeumorphicBoxShape.circle(),
|
||||||
depth: 6,
|
depth: 6,
|
||||||
intensity: 0.9,
|
intensity: 0.9,
|
||||||
color: Colors.grey.shade100,
|
color: Colors.grey.shade100,
|
||||||
),
|
),
|
||||||
child: Icon(Icons.settings, color: Colors.black38),
|
child: const Icon(Icons.settings, color: Colors.black38),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -168,7 +174,6 @@ class HomePageState extends State<HomePage> {
|
||||||
deleteAccount(accounts[index]);
|
deleteAccount(accounts[index]);
|
||||||
},
|
},
|
||||||
).show();
|
).show();
|
||||||
|
|
||||||
},
|
},
|
||||||
child: Neumorphic(
|
child: Neumorphic(
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
|
@ -177,11 +182,13 @@ class HomePageState extends State<HomePage> {
|
||||||
intensity: 1,
|
intensity: 1,
|
||||||
shadowDarkColor: Colors.grey.shade300,
|
shadowDarkColor: Colors.grey.shade300,
|
||||||
color: Colors.grey.shade100,
|
color: Colors.grey.shade100,
|
||||||
boxShape: NeumorphicBoxShape.roundRect(BorderRadius.circular(15)),
|
boxShape:
|
||||||
|
NeumorphicBoxShape.roundRect(BorderRadius.circular(15)),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(accounts[index].name),
|
title: Text(accounts[index].name),
|
||||||
subtitle: Text('${'balance'.tr()}: \$${accounts[index].balance.toStringAsFixed(2)}'),
|
subtitle: Text(
|
||||||
|
'${'balance'.tr()}: \$${accounts[index].balance.toStringAsFixed(2)}'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
@ -216,682 +223,12 @@ class HomePageState extends State<HomePage> {
|
||||||
color: Colors.grey.shade100,
|
color: Colors.grey.shade100,
|
||||||
boxShape: const NeumorphicBoxShape.circle(),
|
boxShape: const NeumorphicBoxShape.circle(),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.add,size:60,color: Colors.black12,),
|
child: const Icon(
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Account {
|
|
||||||
String name;
|
|
||||||
double balance;
|
|
||||||
|
|
||||||
Account({
|
|
||||||
required this.name,
|
|
||||||
required this.balance,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Account.fromJson(String json) {
|
|
||||||
final Map<String, dynamic> map = jsonDecode(json);
|
|
||||||
return Account(
|
|
||||||
name: map['name'],
|
|
||||||
balance: map['balance'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> 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<AddAccountDialog> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
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: Text(
|
|
||||||
'addaccount'.tr(),
|
|
||||||
),
|
|
||||||
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 'entername'.tr();
|
|
||||||
}
|
|
||||||
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'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(12, 16, 12, 16),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return 'enterbalance'.tr();
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return 'entervalidnumber'.tr();
|
|
||||||
}
|
|
||||||
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'.tr(),
|
|
||||||
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'.tr(),
|
|
||||||
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<AccountDetailPage> with SingleTickerProviderStateMixin {
|
|
||||||
late TabController _tabController;
|
|
||||||
List<Transaction> transactions = [];
|
|
||||||
List<Transaction> incomeTransactions = [];
|
|
||||||
List<Transaction> expenseTransactions = [];
|
|
||||||
List<ExpenseData> 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<dynamic> 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<String> 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<ExpenseData> calculateMonthlyExpenses() {
|
|
||||||
Map<String, double> monthlyExpenses = {};
|
|
||||||
for (var transaction in expenseTransactions) {
|
|
||||||
String month = DateFormat('yyyy-MM').format(transaction.date);
|
|
||||||
monthlyExpenses[month] = (monthlyExpenses[month] ?? 0) + transaction.amount;
|
|
||||||
}
|
|
||||||
List<ExpenseData> 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,
|
|
||||||
actions: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(right: 10.0),
|
|
||||||
child: IconButton(
|
|
||||||
icon: Icon(Icons.info,color: Colors.grey,),
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) => SavingsTipsDialog(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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: [
|
|
||||||
Tab(text: 'income'.tr()),
|
|
||||||
Tab(text: 'expenditures'.tr()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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,
|
Icons.add,
|
||||||
size: 40,
|
size: 60,
|
||||||
color: Colors.black12,
|
color: Colors.black12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTransactionsList(List<Transaction> 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<String, dynamic> map = jsonDecode(json);
|
|
||||||
return Transaction(
|
|
||||||
title: map['title'],
|
|
||||||
amount: map['amount'],
|
|
||||||
isExpense: map['isExpense'],
|
|
||||||
date: DateTime.parse(map['date']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> 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<AddTransactionDialog> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
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(
|
|
||||||
'addtrans'.tr(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
content: Form(
|
|
||||||
key: _formKey, // Hier wird das _formKey dem Form-Widget zugewiesen
|
|
||||||
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: _titleController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Title',
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
vertical: 12,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return 'entertitle'.tr();
|
|
||||||
}
|
|
||||||
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'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
vertical: 12,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return 'enteramount'.tr();
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return 'entervalidnumber'.tr();
|
|
||||||
}
|
|
||||||
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'.tr(), 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'.tr(),
|
|
||||||
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'.tr(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MonthlyExpensesChart extends StatelessWidget {
|
|
||||||
final List<ExpenseData> data;
|
|
||||||
|
|
||||||
const MonthlyExpensesChart({super.key, required this.data});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 300,
|
|
||||||
child: SfCartesianChart(
|
|
||||||
primaryXAxis: CategoryAxis(),
|
|
||||||
series: <ChartSeries>[
|
|
||||||
ColumnSeries<ExpenseData, String>(
|
|
||||||
dataSource: data,
|
|
||||||
xValueMapper: (ExpenseData expense, _) => expense.month,
|
|
||||||
yValueMapper: (ExpenseData expense, _) => expense.amount,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
|
||||||
|
|
||||||
|
|
||||||
class MonthlyExpensesChart extends StatelessWidget {
|
|
||||||
final List<ExpenseData> data;
|
|
||||||
|
|
||||||
const MonthlyExpensesChart({super.key, required this.data});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List<charts.Series<ExpenseData, String>> series = [
|
|
||||||
charts.Series(
|
|
||||||
id: 'Expenses',
|
|
||||||
data: data,
|
|
||||||
domainFn: (ExpenseData expense, _) => expense.month,
|
|
||||||
measureFn: (ExpenseData expense, _) => expense.amount,
|
|
||||||
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
height: 200,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: charts.BarChart(
|
|
||||||
series,
|
|
||||||
animate: true,
|
|
||||||
domainAxis: const charts.OrdinalAxisSpec(
|
|
||||||
renderSpec: charts.SmallTickRendererSpec(
|
|
||||||
labelStyle: charts.TextStyleSpec(
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
primaryMeasureAxis: const charts.NumericAxisSpec(
|
|
||||||
renderSpec: charts.GridlineRendererSpec(
|
|
||||||
labelStyle: charts.TextStyleSpec(
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpenseData {
|
|
||||||
final String month;
|
|
||||||
final double amount;
|
|
||||||
|
|
||||||
ExpenseData({required this.month, required this.amount});
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
||||||
import 'package:tests/theme/theme_manager.dart';
|
import 'package:tests/theme/theme_manager.dart';
|
||||||
|
|
||||||
|
@ -12,19 +13,24 @@ class Settings extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsState extends State<Settings> {
|
class SettingsState extends State<Settings> {
|
||||||
|
|
||||||
String _selectedLanguage = 'English';
|
String _selectedLanguage = 'English';
|
||||||
String _selectedCurrency = 'Euro';
|
String _selectedCurrency = 'Euro';
|
||||||
|
|
||||||
final List<String> _languages = ['English', 'Deutsch'];
|
final List<String> _languages = ['English', 'Deutsch'];
|
||||||
final List<String> _currencies = ['Euro', 'Dollar', 'CHF'];
|
final List<String> _currencies = ['Euro', 'Dollar', 'CHF'];
|
||||||
|
|
||||||
void checkForLanguage(BuildContext context){
|
void checkForLanguage(BuildContext context) {
|
||||||
String language = context.locale.toString();
|
String language = context.locale.toString();
|
||||||
|
if (kDebugMode) {
|
||||||
print(language);
|
print(language);
|
||||||
switch(language){
|
}
|
||||||
case "en_US": _selectedLanguage = "English"; break;
|
switch (language) {
|
||||||
case "de_DE": _selectedLanguage = "Deutsch"; break;
|
case "en_US":
|
||||||
|
_selectedLanguage = "English";
|
||||||
|
break;
|
||||||
|
case "de_DE":
|
||||||
|
_selectedLanguage = "Deutsch";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,11 +39,10 @@ class SettingsState extends State<Settings> {
|
||||||
_themeManager.removeListener(themeListener);
|
_themeManager.removeListener(themeListener);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
themeListener(){
|
|
||||||
if(mounted){
|
|
||||||
setState(() {
|
|
||||||
|
|
||||||
});
|
themeListener() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,14 +64,14 @@ class SettingsState extends State<Settings> {
|
||||||
toolbarHeight: 80,
|
toolbarHeight: 80,
|
||||||
title: Text(
|
title: Text(
|
||||||
'settings'.tr(),
|
'settings'.tr(),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(15),
|
bottomLeft: Radius.circular(15),
|
||||||
bottomRight: Radius.circular(15),
|
bottomRight: Radius.circular(15),
|
||||||
|
@ -78,17 +83,16 @@ class SettingsState extends State<Settings> {
|
||||||
child: NeumorphicButton(
|
child: NeumorphicButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, "Change");
|
Navigator.pop(context, "Change");
|
||||||
|
|
||||||
},
|
},
|
||||||
style: NeumorphicStyle(
|
style: NeumorphicStyle(
|
||||||
shape: NeumorphicShape.flat,
|
shape: NeumorphicShape.flat,
|
||||||
boxShape: NeumorphicBoxShape.circle(),
|
boxShape: const NeumorphicBoxShape.circle(),
|
||||||
depth: 6,
|
depth: 6,
|
||||||
intensity: 0.9,
|
intensity: 0.9,
|
||||||
color: Colors.grey.shade100,
|
color: Colors.grey.shade100,
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: Icon(Icons.arrow_back, color: Colors.black38),
|
child: const Icon(Icons.arrow_back, color: Colors.black38),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -118,10 +122,14 @@ class SettingsState extends State<Settings> {
|
||||||
trackDepth: 5,
|
trackDepth: 5,
|
||||||
// Weitere Style-Eigenschaften hier anpassen
|
// Weitere Style-Eigenschaften hier anpassen
|
||||||
// Um den Switch heller zu machen, kannst du die Farben anpassen
|
// Um den Switch heller zu machen, kannst du die Farben anpassen
|
||||||
activeTrackColor: Colors.lightGreen, // Farbe für den aktiven Track
|
activeTrackColor: Colors.lightGreen,
|
||||||
inactiveTrackColor: Colors.grey.shade300, // Farbe für den inaktiven Track
|
// Farbe für den aktiven Track
|
||||||
activeThumbColor: Colors.grey.shade100, // Farbe für den aktiven Thumb
|
inactiveTrackColor: Colors.grey.shade300,
|
||||||
inactiveThumbColor: Colors.grey.shade200, // Farbe für den inaktiven Thumb
|
// Farbe für den inaktiven Track
|
||||||
|
activeThumbColor: Colors.grey.shade100,
|
||||||
|
// Farbe für den aktiven Thumb
|
||||||
|
inactiveThumbColor:
|
||||||
|
Colors.grey.shade200, // Farbe für den inaktiven Thumb
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -142,9 +150,13 @@ class SettingsState extends State<Settings> {
|
||||||
onChanged: (String? newValue) {
|
onChanged: (String? newValue) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedLanguage = newValue!;
|
_selectedLanguage = newValue!;
|
||||||
switch(_selectedLanguage){
|
switch (_selectedLanguage) {
|
||||||
case "English": context.setLocale(Locale('en', 'US')); break;
|
case "English":
|
||||||
case "Deutsch": context.setLocale(Locale('de', 'DE')); break;
|
context.setLocale(const Locale('en', 'US'));
|
||||||
|
break;
|
||||||
|
case "Deutsch":
|
||||||
|
context.setLocale(const Locale('de', 'DE'));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -177,7 +189,8 @@ class SettingsState extends State<Settings> {
|
||||||
// z.B. mit einer Funktion, die die Währung ändert.
|
// z.B. mit einer Funktion, die die Währung ändert.
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
items: _currencies.map<DropdownMenuItem<String>>((String value) {
|
items:
|
||||||
|
_currencies.map<DropdownMenuItem<String>>((String value) {
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
value: value,
|
value: value,
|
||||||
child: Text(value),
|
child: Text(value),
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class SavingsTipsDialog extends StatelessWidget {
|
class SavingsTipsDialog extends StatelessWidget {
|
||||||
final List<String> savingsTips = [
|
final List<String> savingsTips = [
|
||||||
|
@ -24,6 +20,8 @@ class SavingsTipsDialog extends StatelessWidget {
|
||||||
'Nutze kostenlose Online-Ressourcen für Weiterbildung und Hobbys.',
|
'Nutze kostenlose Online-Ressourcen für Weiterbildung und Hobbys.',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
SavingsTipsDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
|
@ -38,49 +36,48 @@ class SavingsTipsDialog extends StatelessWidget {
|
||||||
CarouselSlider.builder(
|
CarouselSlider.builder(
|
||||||
itemCount: savingsTips.length,
|
itemCount: savingsTips.length,
|
||||||
itemBuilder: (BuildContext context, int index, int realIndex) {
|
itemBuilder: (BuildContext context, int index, int realIndex) {
|
||||||
return Padding(
|
return Neumorphic(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
margin: const EdgeInsets.only(bottom: 5),
|
||||||
child: Neumorphic(
|
|
||||||
style: NeumorphicStyle(
|
style: NeumorphicStyle(
|
||||||
|
boxShape: NeumorphicBoxShape.roundRect(
|
||||||
boxShape: NeumorphicBoxShape.roundRect(BorderRadius.circular(13.0)),
|
BorderRadius.circular(13.0)),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
color: Colors.grey.shade100, // Setze die Hintergrundfarbe auf rot
|
color: Colors.grey
|
||||||
|
.shade100, // Setze die Hintergrundfarbe auf rot
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(14.0),
|
||||||
child: Column( mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
savingsTips[index],
|
savingsTips[index],
|
||||||
style: TextStyle(fontSize: 18.0),
|
style: const TextStyle(fontSize: 18.0),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
options: CarouselOptions(
|
options: CarouselOptions(
|
||||||
height: 250.0,
|
height: 250.0,
|
||||||
enlargeCenterPage: true,
|
enlargeCenterPage: true,
|
||||||
viewportFraction: 0.85,
|
viewportFraction: 0.76,
|
||||||
initialPage: 0,
|
initialPage: 0,
|
||||||
enableInfiniteScroll: true,
|
enableInfiniteScroll: true,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
autoPlayInterval: Duration(seconds: 4),
|
autoPlayInterval: const Duration(seconds: 4),
|
||||||
autoPlayAnimationDuration: Duration(milliseconds: 700),
|
autoPlayAnimationDuration: const Duration(milliseconds: 700),
|
||||||
autoPlayCurve: Curves.fastOutSlowIn,
|
autoPlayCurve: Curves.fastOutSlowIn,
|
||||||
pauseAutoPlayOnTouch: true,
|
pauseAutoPlayOnTouch: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.0),
|
const SizedBox(height: 16.0),
|
||||||
NeumorphicButton(
|
NeumorphicButton(
|
||||||
child: Text('Close'),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
@ -89,10 +86,11 @@ class SavingsTipsDialog extends StatelessWidget {
|
||||||
depth: 10,
|
depth: 10,
|
||||||
intensity: 0.9,
|
intensity: 0.9,
|
||||||
shape: NeumorphicShape.flat,
|
shape: NeumorphicShape.flat,
|
||||||
boxShape: NeumorphicBoxShape.roundRect(BorderRadius.circular(15.0)),
|
boxShape:
|
||||||
|
NeumorphicBoxShape.roundRect(BorderRadius.circular(15.0)),
|
||||||
),
|
),
|
||||||
|
child: const Text('Close'),
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const COLOR_PRIMARY = Colors.deepOrangeAccent;
|
const COLOR_PRIMARY = Colors.deepOrangeAccent;
|
||||||
|
@ -6,9 +5,7 @@ const COLOR_PRIMARY = Colors.deepOrangeAccent;
|
||||||
ThemeData lightTheme = ThemeData(
|
ThemeData lightTheme = ThemeData(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primaryColor: COLOR_PRIMARY,
|
primaryColor: COLOR_PRIMARY,
|
||||||
fontFamily: "Montserrat"
|
fontFamily: "Montserrat");
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
ThemeData darkTheme = ThemeData(
|
ThemeData darkTheme = ThemeData(
|
||||||
fontFamily: "Montserrat",
|
fontFamily: "Montserrat",
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
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<String, dynamic> map = jsonDecode(json);
|
||||||
|
return Transaction(
|
||||||
|
title: map['title'],
|
||||||
|
amount: map['amount'],
|
||||||
|
isExpense: map['isExpense'],
|
||||||
|
date: DateTime.parse(map['date']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'amount': amount,
|
||||||
|
'isExpense': isExpense,
|
||||||
|
'date': date.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
|
||||||
|
import 'package:tests/transaction/transaction.dart';
|
||||||
|
|
||||||
|
class AddTransactionDialog extends StatefulWidget {
|
||||||
|
final Function addTransaction;
|
||||||
|
const AddTransactionDialog({super.key, required this.addTransaction});
|
||||||
|
|
||||||
|
@override
|
||||||
|
AddTransactionDialogState createState() => AddTransactionDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddTransactionDialogState extends State<AddTransactionDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
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(
|
||||||
|
'addtrans'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Form(
|
||||||
|
key: _formKey, // Hier wird das _formKey dem Form-Widget zugewiesen
|
||||||
|
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: _titleController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Title',
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.isEmpty) {
|
||||||
|
return 'entertitle'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const 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'.tr(),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.isEmpty) {
|
||||||
|
return 'enteramount'.tr();
|
||||||
|
}
|
||||||
|
if (double.tryParse(value) == null) {
|
||||||
|
return 'entervalidnumber'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const 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!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'expense'.tr(),
|
||||||
|
style: const 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: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'cancel'.tr(),
|
||||||
|
style: const 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: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'add'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
16
pubspec.lock
16
pubspec.lock
|
@ -65,6 +65,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.0"
|
version: "0.12.0"
|
||||||
|
circular_seek_bar:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: circular_seek_bar
|
||||||
|
sha256: "78ff5e54b6012523f3ce19b33c870ab75433ea96977f44606f1641a8ac5c1873"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0+2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -450,6 +458,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "21.2.10"
|
version: "21.2.10"
|
||||||
|
tab_indicator_styler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: tab_indicator_styler
|
||||||
|
sha256: "9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -42,7 +42,8 @@ dependencies:
|
||||||
easy_localization: ^3.0.2
|
easy_localization: ^3.0.2
|
||||||
carousel_slider: ^4.2.1
|
carousel_slider: ^4.2.1
|
||||||
flutter_svg: ^2.0.0
|
flutter_svg: ^2.0.0
|
||||||
|
circular_seek_bar: ^1.1.0
|
||||||
|
tab_indicator_styler: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in New Issue