Feat: Macht die Dashboard-Seite funktional
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
250
lib/Controller/transaction_controller.dart
Normal file
250
lib/Controller/transaction_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
lib/Pages/Dashboard/current_balance.dart
Normal file
142
lib/Pages/Dashboard/current_balance.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
90
lib/Pages/Dashboard/recent_transactions_list.dart
Normal file
90
lib/Pages/Dashboard/recent_transactions_list.dart
Normal 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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
175
lib/Pages/Misc/monthly_balance_chart.dart
Normal file
175
lib/Pages/Misc/monthly_balance_chart.dart
Normal 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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user