Feat: Macht die Dashboard-Seite funktional
This commit is contained in:
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 '../Misc/monthly_balance_chart.dart';
|
||||
import 'current_balance.dart';
|
||||
import 'recent_transactions_list.dart';
|
||||
|
||||
/// Eine Seite, die das Dashboard der App darstellt.
|
||||
///
|
||||
/// Diese Seite zeigt eine Übersicht über den aktuellen Kontostand,
|
||||
@@ -13,227 +16,33 @@ class Dashboard extends StatelessWidget {
|
||||
/// Baut das Dashboard-Widget auf.
|
||||
/// [context] ist der Build-Kontext
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
const currentBalance = 4820.75;
|
||||
const double previousMonthBalance = 4300;
|
||||
Widget build(final BuildContext context) => const Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
CurrentBalance(),
|
||||
SizedBox(height: 32),
|
||||
|
||||
final monthlyBalance = <String, double>{
|
||||
'Jan': 1200.0,
|
||||
'Feb': 900.0,
|
||||
'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,
|
||||
Text(
|
||||
'Kontostand pro Monat',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
MonthlyBalanceChart(),
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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: 32),
|
||||
Text(
|
||||
'Letzte Transaktionen',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
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());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user