Feat: Macht die Dashboard-Seite funktional

This commit is contained in:
2025-12-31 16:44:29 +01:00
parent f765ba6268
commit 4014757319
9 changed files with 845 additions and 239 deletions

View File

@@ -116,7 +116,8 @@ class RecurringTransactionController {
Account? _selectedAccount; Account? _selectedAccount;
/// Gibt die gespeicherten wiederkehrenden Transaktionen als Liste zurück /// Aktualisiert die gespeicherten wiederkehrenden Transaktionen
/// in der internen Liste.
Future<void> updateRecurringTransactions() async { Future<void> updateRecurringTransactions() async {
if (_selectedAccount != null) { if (_selectedAccount != null) {
final List<RecurringTransaction> recurringTransactions = final List<RecurringTransaction> recurringTransactions =
@@ -134,7 +135,7 @@ class RecurringTransactionController {
unawaited(_newRecurringTransactionDialog?.show()); unawaited(_newRecurringTransactionDialog?.show());
} }
/// Startet den Prozess, um eine neue wiederkehrende Transaktion zu bearbeiten /// Startet den Prozess, um eine wiederkehrende Transaktion zu bearbeiten
Future<void> editRecurringTransaction( Future<void> editRecurringTransaction(
final int recurringTransactionId, final int recurringTransactionId,
) async { ) async {

View File

@@ -0,0 +1,250 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import '../Entities/drift_database.dart';
import '../Pages/Dialog/dialog_action.dart';
import '../Pages/Dialog/dialog_input_field.dart';
import '../Pages/Dialog/dialog_input_field_type_enum.dart';
import '../Pages/Dialog/dialog_type_enum.dart';
import '../Pages/Dialog/dynamic_dialog.dart';
import '../Repositories/transaction_repository.dart';
import 'account_controller.dart';
/// Steuert die Interaktion mit den Transaktionen
class TransactionController {
/// Gibt die aktuell gültige Instanz der Klasse zurück
factory TransactionController() => _instance;
/// Erstellt eine neue Instanz dieser Klasse
TransactionController._internal() {
_newTransactionDialog = DynamicDialog(
title: 'Neue Transaktion erstellen',
icon: Icons.swap_horiz,
inputFields: [
const DialogInputField(id: 'name', label: 'Name', autoFocus: true),
DialogInputField(
id: 'date',
label: 'Transaktionsdatum',
keyboardType: TextInputType.datetime,
inputType: DialogInputFieldTypeEnum.date,
initialValue: DateTime.now(),
),
const DialogInputField(
id: 'amount',
label: 'Betrag €',
keyboardType: TextInputType.number,
),
],
actions: [
DialogAction(label: 'Abbruch'),
DialogAction(
label: 'Speichern',
isPrimary: true,
onPressed: _saveNewTransaction,
),
],
);
_errorTransactionValueEmptyDialog = DynamicDialog(
title: 'Fehler!',
icon: Icons.error,
content: const Text('Es wurden nicht alle Werte eingetragen!'),
dialogType: DialogTypeEnum.error,
);
_transactionCreatedDialog = DynamicDialog(
title: 'Transaktion erstellt!',
icon: Icons.check_circle,
content: const Text('Die Transaktion wurde erfolgreich erstellt!'),
dialogType: DialogTypeEnum.success,
);
_selectedAccount = _accountController.selected.value;
_accountController.selected.addListener(() {
_selectedAccount = _accountController.selected.value;
unawaited(updateTransactions());
});
unawaited(updateTransactions());
}
static final TransactionController _instance =
TransactionController._internal();
final TransactionRepository _transactionRepository = TransactionRepository();
final AccountController _accountController = AccountController();
DynamicDialog? _newTransactionDialog;
DynamicDialog? _errorTransactionValueEmptyDialog;
DynamicDialog? _transactionCreatedDialog;
final ValueNotifier<List<Transaction>> _transactions =
ValueNotifier<List<Transaction>>([]);
/// Stellt die Liste der Transaktionen dar
ValueNotifier<List<Transaction>> get transactions {
if (_transactions.value == []) {
unawaited(updateTransactions());
}
return _transactions;
}
set transactions(final List<Transaction> transactions) {
_transactions.value = transactions;
}
Account? _selectedAccount;
/// Aktualisiert die Transaktionen in der internen Liste
Future<void> updateTransactions() async {
if (_selectedAccount != null) {
final List<Transaction> transactions = await _transactionRepository
.findBy(account: _selectedAccount, orderBy: 'dateDesc');
_transactions.value = transactions;
}
}
/// Startet den Prozess, um eine neue Transaktion anzulegen
void newTransactionHandler() {
unawaited(_newTransactionDialog?.show());
}
/// Startet den Prozess, um eine Transaktion zu bearbeiten
Future<void> editTransaction(final int transactionId) async {
final Transaction? transaction = await _transactionRepository.find(
transactionId,
);
if (transaction != null) {
final editTransactionDialog = DynamicDialog(
title: '${transaction.name} umbenennen',
icon: Icons.edit,
inputFields: [
DialogInputField(
id: 'name',
label: 'Name',
autoFocus: true,
initialValue: transaction.name,
),
DialogInputField(
id: 'date',
label: 'Transaktionsdatum',
keyboardType: TextInputType.datetime,
inputType: DialogInputFieldTypeEnum.date,
initialValue: transaction.date,
),
DialogInputField(
id: 'amount',
label: 'Betrag €',
keyboardType: TextInputType.number,
initialValue: transaction.amount.toString(),
),
],
actions: [
DialogAction(label: 'Abbruch'),
DialogAction(
label: 'Speichern',
isPrimary: true,
onPressed: _editTransaction,
),
],
hiddenValues: {'transaction': transaction},
);
unawaited(editTransactionDialog.show());
}
}
/// Startet den Prozess, um eine Transaktion zu löschen
Future<void> deleteTransactionHandler(final int transactionId) async {
final Transaction? transaction = await _transactionRepository.find(
transactionId,
);
if (transaction != null) {
final deleteTransactionDialog = DynamicDialog(
dialogType: DialogTypeEnum.error,
title: '${transaction.name} löschen',
content: Text('Willst du ${transaction.name} wirklich löschen?'),
icon: Icons.delete_forever,
actions: [
DialogAction(label: 'Abbruch', isPrimary: true),
DialogAction(
label: 'Transaktion löschen',
onPressed: _deleteTransaction,
),
],
hiddenValues: {'transaction': transaction},
);
unawaited(deleteTransactionDialog.show());
}
}
Future<void> _saveNewTransaction(final Map<String, dynamic> values) async {
if (values['name'] == null ||
values['name'] == '' ||
values['date'] == null ||
values['date'] == '' ||
values['amount'] == null ||
values['amount'] == '' ||
_selectedAccount == null) {
await _errorTransactionValueEmptyDialog?.show();
} else {
final String amount = values['amount'];
values['amount'] = double.tryParse(amount);
if (values['amount'] == null || values['amount'] == 0) {
await _errorTransactionValueEmptyDialog?.show();
} else {
final transaction = TransactionsCompanion(
name: Value(values['name']),
date: Value(values['date']),
amount: Value(values['amount']),
accountId: Value(_selectedAccount!.id),
);
await _transactionRepository.add(transaction);
await _transactionCreatedDialog?.show();
await updateTransactions();
}
}
}
Future<void> _editTransaction(final Map<String, dynamic> values) async {
if (values['transaction'] != null &&
values['transaction'] != null &&
values['date'] != null &&
values['date'] != '' &&
values['amount'] != null &&
values['amount'] != '') {
final String amount = values['amount'];
values['amount'] = double.tryParse(amount);
if (values['amount'] != null && values['amount'] != 0) {
final Transaction transaction = values['transaction'];
final rtc = TransactionsCompanion(
id: Value(transaction.id),
name: Value(values['name']),
date: Value(values['date']),
amount: Value(values['amount']),
accountId: Value(transaction.accountId),
);
await _transactionRepository.update(rtc);
await updateTransactions();
}
}
}
Future<void> _deleteTransaction(final Map<String, dynamic> values) async {
if (values['transaction'] != null) {
final Transaction transaction = values['transaction'];
await _transactionRepository.remove(transaction);
await updateTransactions();
}
}
}

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import '../../Controller/account_controller.dart';
import '../../Controller/transaction_controller.dart';
import '../../Repositories/transaction_repository.dart';
/// Gibt eine Übersicht über den aktuellen Kontostand
/// und der Veränderung zum Vormonat zurück
class CurrentBalance extends StatefulWidget {
/// Erstellt eine neue Instanz dieser Klasse
const CurrentBalance({super.key});
@override
State<StatefulWidget> createState() => _CurrentBalanceState();
}
class _CurrentBalanceState extends State<CurrentBalance> {
final TransactionController _transactionController = TransactionController();
final AccountController _accountController = AccountController();
final TransactionRepository _transactionRepository = TransactionRepository();
@override
void initState() {
super.initState();
_transactionController.transactions.addListener(() {
if (mounted) {
setState(() {});
}
});
}
@override
Widget build(final BuildContext context) => FutureBuilder(
future: _getBalance(),
builder:
(
final BuildContext context,
final AsyncSnapshot<(double, double, double)> snapshot,
) {
final ThemeData theme = Theme.of(context);
Widget widget;
if (snapshot.hasData) {
final double balanceOfLastMonth = snapshot.data!.$1;
final double balanceNow = snapshot.data!.$2;
final double balanceOfThisMonth = snapshot.data!.$3;
final double diff = balanceOfThisMonth - balanceOfLastMonth;
widget = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Aktuell', style: theme.textTheme.bodyMedium),
const SizedBox(height: 8),
Text(
'${balanceNow.toStringAsFixed(2)}',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
'Differenz zum Vormonat',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Row(
children: <Widget>[
Icon(
diff >= 0 ? Icons.arrow_upward : Icons.arrow_downward,
color: diff >= 0 ? Colors.green : Colors.red,
),
const SizedBox(width: 4),
Text(
'${diff.abs().toStringAsFixed(2)}',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: diff >= 0 ? Colors.green : Colors.red,
),
),
],
),
],
),
],
);
} else if (snapshot.hasError) {
widget = Column(
children: [
Icon(Icons.error, color: theme.colorScheme.error),
const Text('Fehler beim holen der letzten Transaktionen!'),
],
);
} else {
widget = const Center(child: CircularProgressIndicator());
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: widget,
);
},
);
Future<(double, double, double)> _getBalance() async {
final now = DateTime.now();
final dateOfLastMonth = DateTime(now.year, now.month, 0);
final dateOfThisMonth = DateTime(now.year, now.month + 1, 0);
final double balanceOfLastMonth = await _transactionRepository.balance(
account: _accountController.selected.value,
until: dateOfLastMonth,
);
final double balanceNow = await _transactionRepository.balance(
account: _accountController.selected.value,
until: now,
);
final double balanceOfThisMonth = await _transactionRepository.balance(
account: _accountController.selected.value,
until: dateOfThisMonth,
);
return (balanceOfLastMonth, balanceNow, balanceOfThisMonth);
}
}

View File

@@ -1,6 +1,9 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../Misc/monthly_balance_chart.dart';
import 'current_balance.dart';
import 'recent_transactions_list.dart';
/// Eine Seite, die das Dashboard der App darstellt. /// Eine Seite, die das Dashboard der App darstellt.
/// ///
/// Diese Seite zeigt eine Übersicht über den aktuellen Kontostand, /// Diese Seite zeigt eine Übersicht über den aktuellen Kontostand,
@@ -13,227 +16,33 @@ class Dashboard extends StatelessWidget {
/// Baut das Dashboard-Widget auf. /// Baut das Dashboard-Widget auf.
/// [context] ist der Build-Kontext /// [context] ist der Build-Kontext
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) => const Scaffold(
const currentBalance = 4820.75; body: SafeArea(
const double previousMonthBalance = 4300; child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
CurrentBalance(),
SizedBox(height: 32),
final monthlyBalance = <String, double>{ Text(
'Jan': 1200.0, 'Kontostand pro Monat',
'Feb': 900.0, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
'Mär': 1100.0,
'Apr': 950.0,
'Mai': 1300.0,
'Jun': 1050.0,
};
final recentTransactions = <Map<String, Object>>[
<String, Object>{'name': 'Supermarkt', 'amount': -45.50},
<String, Object>{'name': 'Gehalt', 'amount': 2500.00},
<String, Object>{'name': 'Miete', 'amount': -900.00},
<String, Object>{'name': 'Streaming', 'amount': -12.99},
<String, Object>{'name': 'Kaffee', 'amount': -4.50},
];
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_currentBalance(currentBalance, previousMonthBalance, context),
const SizedBox(height: 32),
const Text(
'Kontostand pro Monat',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
_monthlyBalance(monthlyBalance, context),
const SizedBox(height: 32),
const Text(
'Letzte Transaktionen',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
..._recentTransactions(recentTransactions),
],
),
),
),
);
}
/// Baut das Widget für den aktuellen Kontostand
/// und die Differenz zum Vormonat auf.
///
/// [currentBalance] ist der aktuelle Kontostand
/// [previousBalance] ist der Kontostand des Vormonats
/// [context] ist der Build-Kontext
Widget _currentBalance(
final double currentBalance,
final double previousBalance,
final BuildContext context,
) {
final ThemeData theme = Theme.of(context);
final double diff = currentBalance - previousBalance;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Aktuell', style: theme.textTheme.bodyMedium),
const SizedBox(height: 8),
Text(
'${currentBalance.toStringAsFixed(2)}',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
), ),
), SizedBox(height: 12),
], MonthlyBalanceChart(),
),
Column( SizedBox(height: 32),
crossAxisAlignment: CrossAxisAlignment.end, Text(
children: <Widget>[ 'Letzte Transaktionen',
Text( style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
'Differenz zum Vormonat',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Row(
children: <Widget>[
Icon(
diff >= 0 ? Icons.arrow_upward : Icons.arrow_downward,
color: diff >= 0 ? Colors.green : Colors.red,
),
const SizedBox(width: 4),
Text(
'${diff.abs().toStringAsFixed(2)}',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: diff >= 0 ? Colors.green : Colors.red,
),
),
],
),
],
),
],
),
);
}
/// Baut das Widget für die Entwicklung
/// des Kontostands der letzten Monate auf.
///
/// [monthlyBalance] ist ein Map mit den Monaten als Schlüssel und
/// den dazugehörigen Kontoständen als Werte.
/// [context] ist der Build-Kontext.
Widget _monthlyBalance(
final Map<String, double> monthlyBalance,
final BuildContext context,
) {
final ThemeData theme = Theme.of(context);
final double maxY =
monthlyBalance.values.reduce(
(final double a, final double b) => a > b ? a : b,
) +
200;
return SizedBox(
height: 180,
child: LineChart(
LineChartData(
minY: 0,
maxY: maxY,
titlesData: FlTitlesData(
topTitles: const AxisTitles(),
rightTitles: const AxisTitles(),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 1,
getTitlesWidget: (final double value, final TitleMeta meta) {
final List<String> months = monthlyBalance.keys.toList();
if (value.toInt() >= 0 && value.toInt() < months.length) {
return Text(months[value.toInt()]);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 50,
getTitlesWidget: (final double value, final TitleMeta meta) =>
Text(
'${value.toInt()}',
style: const TextStyle(fontSize: 12),
),
),
),
),
lineBarsData: <LineChartBarData>[
LineChartBarData(
spots: List<FlSpot>.generate(
monthlyBalance.length,
(final int index) => FlSpot(
index.toDouble(),
monthlyBalance.values.elementAt(index),
),
),
isCurved: true,
barWidth: 3,
color: theme.colorScheme.primary,
), ),
SizedBox(height: 12),
RecentTransactionsList(),
], ],
), ),
), ),
); ),
} );
/// Erstellt Widgets für die letzten Transaktionen.
///
/// [recentTransactions] ist eine Liste von Transaktionen,
/// wobei jede Transaktion ein Map mit den Keys 'name' (String)
/// und 'amount' (double) ist.
/// Die Funktion gibt eine Liste von Widgets zurück,
/// die die Transaktionen anzeigen.
List<Widget> _recentTransactions(
final List<Map<String, Object>> recentTransactions,
) => recentTransactions
.map(
(final Map<String, Object> tx) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text((tx['name'] ?? '') as String),
trailing: Text(
'${((tx['amount'] ?? 0) as double).abs().toStringAsFixed(2)}',
style: TextStyle(
color: ((tx['amount'] ?? 0) as double) >= 0
? Colors.green
: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
),
)
.toList();
} }

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import '../../Controller/account_controller.dart';
import '../../Controller/transaction_controller.dart';
import '../../Entities/drift_database.dart';
import '../../Repositories/transaction_repository.dart';
/// Eine Liste mit den zuletzt getätigten Transaktionen
class RecentTransactionsList extends StatefulWidget {
/// Erstellt eine neue Instanz dieser Klasse
const RecentTransactionsList({super.key});
@override
State<StatefulWidget> createState() => _RecentTransactionsListState();
}
class _RecentTransactionsListState extends State<RecentTransactionsList> {
final TransactionController _transactionController = TransactionController();
final AccountController _accountController = AccountController();
final TransactionRepository _transactionRepository = TransactionRepository();
@override
void initState() {
super.initState();
_transactionController.transactions.addListener(() {
if (mounted) {
setState(() {});
}
});
}
@override
Widget build(final BuildContext context) {
final Future<List<Transaction>> recentTransactions = _transactionRepository
.findBy(
account: _accountController.selected.value,
limit: 5,
orderBy: 'dateDesc',
);
return FutureBuilder(
future: recentTransactions,
builder:
(
final BuildContext context,
final AsyncSnapshot<List<Transaction>> snapshot,
) {
final ThemeData theme = Theme.of(context);
if (snapshot.hasData) {
final List<Padding>? recentTransactionsWidgetList = snapshot.data
?.map(
(final Transaction transaction) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(transaction.name),
trailing: Text(
'${transaction.amount.abs().toStringAsFixed(2)}',
style: TextStyle(
color: transaction.amount == 0
? null
: (transaction.amount < 0
? Colors.green
: Colors.red),
fontWeight: FontWeight.bold,
),
),
),
),
)
.toList();
return Column(children: [...?recentTransactionsWidgetList]);
} else if (snapshot.hasError) {
return Column(
children: [
Icon(Icons.error, color: theme.colorScheme.error),
const Text('Fehler beim holen der letzten Transaktionen!'),
],
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}
}

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import '../../Controller/account_controller.dart'; import '../../Controller/account_controller.dart';
import '../../Controller/recurring_transaction_controller.dart';
import '../../Controller/transaction_controller.dart';
/// Ein Floating Action Button, der beim Klicken ein expandierendes Menü öffnet, /// Ein Floating Action Button, der beim Klicken ein expandierendes Menü öffnet,
/// um neue Transaktionen oder Konten anzulegen. /// um neue Transaktionen oder Konten anzulegen.
@@ -15,6 +17,10 @@ class FloatingCreationButton extends StatefulWidget {
class _FloatingCreationButtonState extends State<FloatingCreationButton> { class _FloatingCreationButtonState extends State<FloatingCreationButton> {
final AccountController _accountController = AccountController(); final AccountController _accountController = AccountController();
final RecurringTransactionController _recurringTransactionController =
RecurringTransactionController();
final TransactionController _transactionController = TransactionController();
final _key = GlobalKey<ExpandableFabState>(); final _key = GlobalKey<ExpandableFabState>();
@override @override
@@ -27,16 +33,22 @@ class _FloatingCreationButtonState extends State<FloatingCreationButton> {
childrenAnimation: ExpandableFabAnimation.none, childrenAnimation: ExpandableFabAnimation.none,
distance: 70, distance: 70,
children: <Widget>[ children: <Widget>[
_expandableButton(
label: 'Neue Transaktion',
icon: Icons.add,
onPressed: () {},
),
_expandableButton( _expandableButton(
label: 'Neues Konto', label: 'Neues Konto',
icon: Icons.account_balance_wallet, icon: Icons.account_balance_wallet,
onPressed: _accountController.newAccountHandler, onPressed: _accountController.newAccountHandler,
), ),
_expandableButton(
label: 'Neue Transaktion',
icon: Icons.swap_horiz,
onPressed: _transactionController.newTransactionHandler,
),
_expandableButton(
label: 'Neue wiederkehrende Transaktion',
icon: Icons.repeat,
onPressed:
_recurringTransactionController.newRecurringTransactionHandler,
),
], ],
); );

View File

@@ -0,0 +1,175 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../Controller/account_controller.dart';
import '../../Controller/transaction_controller.dart';
import '../../Repositories/transaction_repository.dart';
/// Stellt einen Chart des Monats-Kontostands dar
class MonthlyBalanceChart extends StatefulWidget {
/// Erstellt eine neue Instanz dieser Klasse
const MonthlyBalanceChart({
super.key,
this.amountMax,
this.amountMin,
this.dateFrom,
this.dateTo,
this.name,
});
/// Der Name der Transaktion, nach der gesucht werden soll
final String? name;
/// Der Mindestbetrag der Transaktion, nach der gesucht werden soll
final double? amountMin;
/// Der Maximalbetrag der Transaktion, nach der gesucht werden soll
final double? amountMax;
/// Das Datum der Transaktionen, ab dem gestartet wurde
final DateTime? dateFrom;
///Das Datum der Transaktionen, bis zu welchen beendet wurde
final DateTime? dateTo;
@override
State<StatefulWidget> createState() => _MonthlyBalanceChart();
}
class _MonthlyBalanceChart extends State<MonthlyBalanceChart> {
final TransactionRepository _transactionRepository = TransactionRepository();
final AccountController _accountController = AccountController();
final TransactionController _transactionController = TransactionController();
@override
void initState() {
_transactionRepository.monthlyBalances(
account: _accountController.selected.value,
name: widget.name,
amountMin: widget.amountMin,
amountMax: widget.amountMax,
dateFrom: widget.dateFrom,
dateTo: widget.dateTo,
);
super.initState();
_transactionController.transactions.addListener(() {
if (mounted) {
setState(() {});
}
});
}
@override
Widget build(final BuildContext context) => FutureBuilder(
future: _transactionRepository.monthlyBalances(
account: _accountController.selected.value,
name: widget.name,
amountMin: widget.amountMin,
amountMax: widget.amountMax,
dateFrom: widget.dateFrom,
dateTo: widget.dateTo,
),
builder:
(
final BuildContext context,
final AsyncSnapshot<List<Map<String, dynamic>>> snapshot,
) {
final ThemeData theme = Theme.of(context);
if (snapshot.hasData) {
final List<Map<String, dynamic>> monthlyBalances = snapshot.data!;
double maxBalance = 0;
double minBalance = 0;
for (final value in monthlyBalances) {
if (maxBalance < value['balance']) {
maxBalance = value['balance'];
}
if (minBalance > value['balance']) {
minBalance = value['balance'];
}
}
return SizedBox(
height: 180,
child: LineChart(
LineChartData(
minY: minBalance,
maxY: maxBalance,
titlesData: FlTitlesData(
topTitles: const AxisTitles(),
rightTitles: const AxisTitles(),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 1,
getTitlesWidget:
(final double value, final TitleMeta meta) {
final List<String> months = monthlyBalances.map((
final value,
) {
final DateTime date = value['date'];
final DateFormat format = DateFormat('MMMM');
return format.format(date);
}).toList();
if (value.toInt() >= 0 &&
value.toInt() < months.length) {
return Text(months[value.toInt()]);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 50,
getTitlesWidget:
(final double value, final TitleMeta meta) => Text(
'${value.toInt()}',
style: const TextStyle(fontSize: 12),
),
),
),
),
lineBarsData: <LineChartBarData>[
LineChartBarData(
spots: List<FlSpot>.generate(
monthlyBalances.length,
(final int index) => FlSpot(
index.toDouble(),
monthlyBalances[index]['balance'],
),
),
isCurved: true,
barWidth: 3,
color: theme.colorScheme.primary,
),
],
),
),
);
} else if (snapshot.hasError) {
return Center(
child: Column(
children: [
Icon(Icons.error, color: theme.colorScheme.error),
const Text('Fehler beim holen der Monatsübersicht!'),
],
),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}

View File

@@ -18,7 +18,7 @@ class RecurringTransactionRepository {
return find(id); return find(id);
} }
/// Aktualisiert ein Konto in der Datenbank /// Aktualisiert eine wiederkehrende Transaktion in der Datenbank
Future<bool> update( Future<bool> update(
final RecurringTransactionsCompanion recurringTransaction, final RecurringTransactionsCompanion recurringTransaction,
) => _db.update(_db.recurringTransactions).replace(recurringTransaction); ) => _db.update(_db.recurringTransactions).replace(recurringTransaction);
@@ -42,8 +42,6 @@ class RecurringTransactionRepository {
final DateTime? startDate, final DateTime? startDate,
final DateTime? startDateBefore, final DateTime? startDateBefore,
final DateTime? startDateAfter, final DateTime? startDateAfter,
final DateTime? startDateFrom,
final DateTime? startDateTo,
final TimeFrameEnum? timeFrame, final TimeFrameEnum? timeFrame,
final double? amount, final double? amount,
final double? amountMin, final double? amountMin,
@@ -77,12 +75,6 @@ class RecurringTransactionRepository {
query.where((final t) => t.startDate.isSmallerThanValue(startDateBefore)); query.where((final t) => t.startDate.isSmallerThanValue(startDateBefore));
} }
if (startDateFrom != null && startDateTo != null) {
query.where(
(final t) => t.startDate.isBetweenValues(startDateFrom, startDateTo),
);
}
if (timeFrame != null) { if (timeFrame != null) {
query.where((final t) => t.timeFrame.equals(timeFrame.index)); query.where((final t) => t.timeFrame.equals(timeFrame.index));
} }

View File

@@ -13,6 +13,10 @@ class TransactionRepository {
return find(id); return find(id);
} }
/// Aktualisiert eine Transaktion in der Datenbank
Future<bool> update(final TransactionsCompanion transaction) =>
_db.update(_db.transactions).replace(transaction);
/// Entfernt eine Transaktion aus der Datenbank /// Entfernt eine Transaktion aus der Datenbank
Future<int> remove(final Transaction transaction) => (_db.delete( Future<int> remove(final Transaction transaction) => (_db.delete(
_db.transactions, _db.transactions,
@@ -28,11 +32,14 @@ class TransactionRepository {
final int? id, final int? id,
final String? name, final String? name,
final DateTime? date, final DateTime? date,
final DateTime? dateBefore, final DateTime? dateFrom,
final DateTime? dateAfter, final DateTime? dateTo,
final double? amount, final double? amount,
final double? amountMin, final double? amountMin,
final double? amountMax, final double? amountMax,
final Account? account,
final int? limit,
final int? offset,
final String? orderBy, final String? orderBy,
}) { }) {
final SimpleSelectStatement<$TransactionsTable, Transaction> query = _db final SimpleSelectStatement<$TransactionsTable, Transaction> query = _db
@@ -50,12 +57,12 @@ class TransactionRepository {
query.where((final t) => t.date.equals(date)); query.where((final t) => t.date.equals(date));
} }
if (dateBefore != null) { if (dateFrom != null) {
query.where((final t) => t.date.isBiggerThanValue(dateBefore)); query.where((final t) => t.date.isBiggerThanValue(dateFrom));
} }
if (dateAfter != null) { if (dateTo != null) {
query.where((final t) => t.date.isSmallerThanValue(dateAfter)); query.where((final t) => t.date.isSmallerThanValue(dateTo));
} }
if (amount != null) { if (amount != null) {
@@ -70,6 +77,14 @@ class TransactionRepository {
query.where((final t) => t.amount.isSmallerThanValue(amountMax)); query.where((final t) => t.amount.isSmallerThanValue(amountMax));
} }
if (account != null) {
query.where((final t) => t.accountId.equals(account.id));
}
if (limit != null) {
query.limit(limit, offset: offset);
}
if (orderBy != null) { if (orderBy != null) {
switch (orderBy) { switch (orderBy) {
case 'nameAsc': case 'nameAsc':
@@ -89,4 +104,124 @@ class TransactionRepository {
return query.get(); return query.get();
} }
/// Gibt den Kontostand zurück
Future<double> balance({
final Account? account,
final DateTime? until,
}) async {
final JoinedSelectStatement<$TransactionsTable, Transaction> query =
_db.selectOnly(_db.transactions)
..addColumns([_db.transactions.amount.sum()]);
if (account != null) {
query.where(_db.transactions.accountId.equals(account.id));
}
if (until != null) {
query.where(_db.transactions.date.isSmallerOrEqualValue(until));
}
return (await query
.map((final row) => row.read(_db.transactions.amount.sum()) ?? 0)
.getSingle()) *
-1;
}
/// Gibt den Kontostand der letzten 12 Monate zurück
Future<List<Map<String, dynamic>>> monthlyBalances({
final Account? account,
final String? name,
final double? amountMin,
final double? amountMax,
DateTime? dateFrom,
DateTime? dateTo,
}) async {
final now = DateTime.now();
final monthStart = DateTime(now.year, now.month - 12);
final monthEnd = DateTime(now.year, now.month + 1, 0);
if (dateFrom == null || dateFrom.compareTo(monthStart) < 0) {
dateFrom = monthStart;
}
if (dateTo == null || dateTo.compareTo(monthEnd) > 0) {
dateTo = monthEnd;
}
final Expression<int> yearExpr = _db.transactions.date.year;
final Expression<int> monthExpr = _db.transactions.date.month;
final Expression<double> sumExpr = _db.transactions.amount.sum();
final JoinedSelectStatement<$TransactionsTable, Transaction> query =
_db.selectOnly(_db.transactions)
..addColumns([yearExpr, monthExpr, sumExpr])
..groupBy([yearExpr, monthExpr])
..orderBy([OrderingTerm.asc(yearExpr), OrderingTerm.asc(monthExpr)])
..where(_db.transactions.date.isBiggerOrEqualValue(dateFrom))
..where(_db.transactions.date.isSmallerOrEqualValue(dateTo));
if (account != null) {
query.where(_db.transactions.accountId.equals(account.id));
}
if (name != null && name.isNotEmpty) {
query.where(_db.transactions.name.like('%$name%'));
}
if (amountMin != null) {
query.where(_db.transactions.amount.isBiggerOrEqualValue(amountMin));
}
if (amountMax != null) {
query.where(_db.transactions.amount.isSmallerOrEqualValue(amountMax));
}
final List<Map<String, Object>> rows = (await query.get()).map((final row) {
final int year = row.read(yearExpr)!;
final int month = row.read(monthExpr)!;
return {
'date': DateTime(year, month),
'balance': (row.read(sumExpr) ?? 0) * -1,
};
}).toList();
double amount = await balance(account: account, until: dateFrom);
DateTime dateTimeLoop = dateFrom;
int loop = 0;
final List<Map<String, Object>> result = [];
while (dateTimeLoop.compareTo(monthEnd) < 0) {
Map<String, Object>? row;
for (final value in rows) {
final Object? rowDate = value['date'];
if (rowDate is DateTime) {
if (dateTimeLoop.compareTo(rowDate) == 0) {
row = value;
}
}
}
if (row == null) {
result.add({'date': dateTimeLoop, 'balance': 0.0});
} else {
result.add(row);
}
final double balance = double.parse(result[loop]['balance'].toString());
amount = balance + amount;
result[loop]['balance'] = amount;
loop = loop + 1;
dateTimeLoop = DateTime(dateTimeLoop.year, dateTimeLoop.month + 1);
}
return result;
}
} }