From d3804865d8abdfa90fa4b5ebec3244e84b14ffd5 Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Thu, 1 Jan 2026 17:05:10 +0100 Subject: [PATCH] Feat: Macht die Trend-Seite funktional --- lib/Pages/Dialog/dynamic_dialog.dart | 2 +- .../Misc/InputFields/date_range_picker.dart | 111 ++++++ .../dynamic_date_time_field.dart | 1 + lib/Pages/Misc/monthly_balance_chart.dart | 9 - lib/Pages/Trend/input_fields.dart | 159 ++++++++ lib/Pages/Trend/transaction_list.dart | 156 ++++++++ lib/Pages/Trend/trend.dart | 358 ++---------------- 7 files changed, 469 insertions(+), 327 deletions(-) create mode 100644 lib/Pages/Misc/InputFields/date_range_picker.dart rename lib/Pages/Misc/{ => InputFields}/dynamic_date_time_field.dart (97%) create mode 100644 lib/Pages/Trend/input_fields.dart create mode 100644 lib/Pages/Trend/transaction_list.dart diff --git a/lib/Pages/Dialog/dynamic_dialog.dart b/lib/Pages/Dialog/dynamic_dialog.dart index a741517..1eff527 100644 --- a/lib/Pages/Dialog/dynamic_dialog.dart +++ b/lib/Pages/Dialog/dynamic_dialog.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../../Services/navigation_service.dart'; import '../../Services/theme_service.dart'; -import '../Misc/dynamic_date_time_field.dart'; +import '../Misc/InputFields/dynamic_date_time_field.dart'; import 'dialog_action.dart'; import 'dialog_input_field.dart'; import 'dialog_input_field_select_item.dart'; diff --git a/lib/Pages/Misc/InputFields/date_range_picker.dart b/lib/Pages/Misc/InputFields/date_range_picker.dart new file mode 100644 index 0000000..c1005df --- /dev/null +++ b/lib/Pages/Misc/InputFields/date_range_picker.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; + +import '../../Dialog/dialog_action.dart'; +import '../../Dialog/dialog_input_field.dart'; +import '../../Dialog/dialog_input_field_type_enum.dart'; +import '../../Dialog/dynamic_dialog.dart'; + +/// Stellt einen DateRange-Picker als Input-Feld dar +class DateRangePicker extends StatefulWidget { + /// Initialisiert eine neue Instanz dieser Klasse + const DateRangePicker({ + this.autofocus = false, + super.key, + this.decoration, + this.initialValue, + this.onChanged, + }); + + /// Der Initiale Wert des Input-Feldes + final DateTimeRange? initialValue; + + /// Ob das Feld automatisch ausgewählt werden soll + final bool autofocus; + + /// Die Funktion, die bei Veränderung des Wertes aufgerufen wird + final Function(DateTimeRange?)? onChanged; + + /// Die Dekoration, wie das Feld aussehen soll + final InputDecoration? decoration; + + @override + State createState() => _DateRangePicker(); +} + +class _DateRangePicker extends State { + final ValueNotifier _value = ValueNotifier(null); + + @override + void initState() { + super.initState(); + + _value.value = widget.initialValue; + _value.addListener(() { + widget.onChanged?.call(_value.value); + }); + } + + @override + Widget build(final BuildContext context) => TextField( + readOnly: true, + controller: TextEditingController( + text: widget.initialValue != null + ? '${widget.initialValue!.start.day}' + '.${widget.initialValue!.start.month}' + '.${widget.initialValue!.start.year}' + ' - ' + '${widget.initialValue!.end.day}' + '.${widget.initialValue!.end.month}' + '.${widget.initialValue!.end.year}' + : null, + ), + decoration: widget.decoration, + onTap: _pickDateRange, + ); + + Future _pickDateRange() async { + await DynamicDialog( + title: 'Zeitraum auswählen', + inputFields: [ + DialogInputField( + id: 'dateFrom', + label: 'Startdatum', + keyboardType: TextInputType.datetime, + inputType: DialogInputFieldTypeEnum.date, + initialValue: _value.value?.start, + ), + DialogInputField( + id: 'dateTo', + label: 'Enddatum', + keyboardType: TextInputType.datetime, + inputType: DialogInputFieldTypeEnum.date, + initialValue: _value.value?.end, + ), + ], + actions: [ + DialogAction(label: 'Abbrechen'), + DialogAction( + label: 'Leeren', + onPressed: (_) { + _value.value = null; + }, + ), + DialogAction( + label: 'Auswählen', + isPrimary: true, + onPressed: (final Map values) { + if (values['dateFrom'] != null && + values['dateFrom'] is DateTime && + values['dateTo'] != null && + values['dateTo'] is DateTime) { + _value.value = DateTimeRange( + start: values['dateFrom'], + end: values['dateTo'], + ); + } + }, + ), + ], + ).show(); + } +} diff --git a/lib/Pages/Misc/dynamic_date_time_field.dart b/lib/Pages/Misc/InputFields/dynamic_date_time_field.dart similarity index 97% rename from lib/Pages/Misc/dynamic_date_time_field.dart rename to lib/Pages/Misc/InputFields/dynamic_date_time_field.dart index 0a6724e..bf91e81 100644 --- a/lib/Pages/Misc/dynamic_date_time_field.dart +++ b/lib/Pages/Misc/InputFields/dynamic_date_time_field.dart @@ -14,6 +14,7 @@ class DynamicDateTimeField extends StatefulWidget { super.key, }); + /// Der Initiale Wert des Input-Feldes final DateTime? initialValue; /// Der Modus des Datums-Feldes diff --git a/lib/Pages/Misc/monthly_balance_chart.dart b/lib/Pages/Misc/monthly_balance_chart.dart index faf9697..1765041 100644 --- a/lib/Pages/Misc/monthly_balance_chart.dart +++ b/lib/Pages/Misc/monthly_balance_chart.dart @@ -45,15 +45,6 @@ class _MonthlyBalanceChart extends State { @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(() { diff --git a/lib/Pages/Trend/input_fields.dart b/lib/Pages/Trend/input_fields.dart new file mode 100644 index 0000000..769a30e --- /dev/null +++ b/lib/Pages/Trend/input_fields.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:routemaster/routemaster.dart'; + +import '../Misc/InputFields/date_range_picker.dart'; + +/// Stellt die Inputfelder für die Verlaufsübersicht dar +class InputFields extends StatefulWidget { + /// Erstellt eine neue Instanz dieser Klasse + const InputFields({ + super.key, + this.amountMax, + this.amountMin, + this.dateFrom, + this.dateTo, + this.name, + this.onChanged, + }); + + /// 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; + + /// Die Funktion, die bei Veränderung eines Wertes aufgerufen wird + final Function()? onChanged; + + @override + State createState() => _InputFields(); +} + +class _InputFields extends State { + String? _name; + double? _amountMin; + double? _amountMax; + DateTimeRange? _dateTimeRange; + + @override + void initState() { + super.initState(); + + _name = widget.name; + _amountMin = widget.amountMin; + _amountMax = widget.amountMax; + + if (widget.dateFrom != null && widget.dateTo != null) { + _dateTimeRange = DateTimeRange( + start: widget.dateFrom!, + end: widget.dateTo!, + ); + } + } + + @override + Widget build(final BuildContext context) { + final ThemeData theme = Theme.of(context); + + return Row( + children: [ + Expanded( + child: TextField( + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + onChanged: (final String value) { + _name = value; + _updateUrl(); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + decoration: const InputDecoration( + labelText: 'Min Betrag €', + border: OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + onChanged: (final String value) { + _amountMin = double.tryParse(value); + _updateUrl(); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + decoration: const InputDecoration( + labelText: 'Max Betrag €', + border: OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + onChanged: (final String value) { + _amountMax = double.tryParse(value); + _updateUrl(); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: DateRangePicker( + initialValue: _dateTimeRange, + onChanged: (final DateTimeRange? value) { + _dateTimeRange = value; + _updateUrl(); + }, + decoration: InputDecoration( + labelText: 'Zeitraum', + border: const OutlineInputBorder(), + suffixIcon: Icon( + Icons.calendar_month, + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () { + _name = null; + _amountMin = null; + _amountMax = null; + _dateTimeRange = null; + + _updateUrl(); + }, + icon: const Icon(Icons.clear), + ), + ], + ); + } + + void _updateUrl() { + final params = { + if (_name != null && _name != '') 'name': _name!, + if (_amountMin != null) 'amountMin': _amountMin!.toString(), + if (_amountMax != null) 'amountMax': _amountMax!.toString(), + }; + + if (_dateTimeRange != null) { + params['dateFrom'] = _dateTimeRange!.start.toIso8601String(); + params['dateTo'] = _dateTimeRange!.end.toIso8601String(); + } + + Routemaster.of(context).replace('/trend', queryParameters: params); + + widget.onChanged?.call(); + } +} diff --git a/lib/Pages/Trend/transaction_list.dart b/lib/Pages/Trend/transaction_list.dart new file mode 100644 index 0000000..591e9a2 --- /dev/null +++ b/lib/Pages/Trend/transaction_list.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:intl/intl.dart'; + +import '../../Controller/account_controller.dart'; +import '../../Controller/transaction_controller.dart'; +import '../../Entities/drift_database.dart'; +import '../../Repositories/transaction_repository.dart'; + +/// Stellt eine filterbare Liste der Transaktionen dar +class TransactionList extends StatefulWidget { + /// Erstellt eine neue Instanz dieser Klasse + const TransactionList({ + 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 createState() => _TransactionListState(); +} + +class _TransactionListState extends State { + 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: _transactionRepository.findBy( + name: widget.name, + amountMin: widget.amountMin, + amountMax: widget.amountMax, + dateFrom: widget.dateFrom, + dateTo: widget.dateTo, + account: _accountController.selected.value, + ), + builder: + ( + final BuildContext context, + final AsyncSnapshot> snapshot, + ) { + final ThemeData theme = Theme.of(context); + + if (snapshot.hasData) { + final transactionsByMonth = >{}; + for (final Transaction transaction in snapshot.data!) { + final DateTime date = transaction.date!; + final DateFormat format = DateFormat('MMMM'); + + final monthName = '${format.format(date)} ${date.year}'; + transactionsByMonth + .putIfAbsent(monthName, () => []) + .add(transaction); + } + + return Expanded( + child: CustomScrollView( + slivers: transactionsByMonth.entries + .map( + ( + final MapEntry> entry, + ) => SliverStickyHeader( + header: Container( + color: theme.colorScheme.surface, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + alignment: Alignment.centerLeft, + child: Text( + entry.key, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate(( + final BuildContext context, + final int index, + ) { + final Transaction transaction = entry.value[index]; + + return ListTile( + contentPadding: EdgeInsets.zero, + title: Text(transaction.name), + subtitle: Text( + '${transaction.date?.day}' + '.${transaction.date?.month}' + '.${transaction.date?.year}', + ), + trailing: Text( + '${transaction.amount.abs().toStringAsFixed(2)}' + ' €', + style: TextStyle( + color: transaction.amount >= 0 + ? Colors.red + : Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ); + }, childCount: entry.value.length), + ), + ), + ) + .toList(), + ), + ); + } else if (snapshot.hasError) { + return Center( + child: Column( + children: [ + Icon(Icons.error, color: theme.colorScheme.error), + const Text('Fehler beim holen der Transaktionen!'), + ], + ), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); +} diff --git a/lib/Pages/Trend/trend.dart b/lib/Pages/Trend/trend.dart index 7f75424..8f34039 100644 --- a/lib/Pages/Trend/trend.dart +++ b/lib/Pages/Trend/trend.dart @@ -1,10 +1,10 @@ -import 'dart:math'; - -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:routemaster/routemaster.dart'; +import '../Misc/monthly_balance_chart.dart'; +import 'input_fields.dart'; +import 'transaction_list.dart'; + /// Eine Seite, die den Trend von Transaktionen zeigt. /// /// Die Seite zeigt einen Liniendiagramm für den Kontostand über die Zeit, @@ -19,153 +19,17 @@ class Trend extends StatefulWidget { } class _TrendState extends State { - final Map monthlyBalance = { - for (int i in List.generate(24, (final int i) => i + 1)) - 'M${i + 1}': Random().nextDouble() * 2000 + 500, - }; - - final List> transactions = - List>.generate( - 50, - (final int i) => { - 'date': DateTime.now().subtract(Duration(days: i)), - 'name': 'Transaction ${i + 1}', - 'amount': (Random().nextDouble() * 2000 - 1000), - }, - ); - - late TextEditingController nameController; - late TextEditingController minController; - late TextEditingController maxController; - - DateTimeRange? selectedRange; - bool _initialized = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!_initialized) { - final Map query = Routemaster.of( - context, - ).currentRoute.queryParameters; - - nameController = TextEditingController(text: query['name'] ?? ''); - minController = TextEditingController(text: query['min'] ?? ''); - maxController = TextEditingController(text: query['max'] ?? ''); - - if (query['start'] != null && query['end'] != null) { - final DateTime? start = DateTime.tryParse(query['start']!); - final DateTime? end = DateTime.tryParse(query['end']!); - if (start != null && end != null) { - selectedRange = DateTimeRange(start: start, end: end); - } - } else { - selectedRange = null; - } - - _initialized = true; - } - } - - @override - void dispose() { - nameController.dispose(); - minController.dispose(); - maxController.dispose(); - super.dispose(); - } - - Future _pickDateRange() async { - final now = DateTime.now(); - final DateTimeRange? picked = await showDateRangePicker( - context: context, - firstDate: DateTime(now.year - 5), - lastDate: DateTime(now.year + 1), - initialDateRange: selectedRange, - ); - - if (picked != null) { - setState(() => selectedRange = picked); - _updateUrl(); - } - } - - void _updateUrl() { - final params = { - if (nameController.text.isNotEmpty) 'name': nameController.text, - if (minController.text.isNotEmpty) 'min': minController.text, - if (maxController.text.isNotEmpty) 'max': maxController.text, - if (selectedRange != null) - 'start': selectedRange!.start.toIso8601String(), - if (selectedRange != null) 'end': selectedRange!.end.toIso8601String(), - }; - Routemaster.of(context).replace('/trend', queryParameters: params); - setState(() {}); - } - - String _monthName(final int month) { - const names = [ - '', - 'Januar', - 'Februar', - 'März', - 'April', - 'Mai', - 'Juni', - 'Juli', - 'August', - 'September', - 'Oktober', - 'November', - 'Dezember', - ]; - return names[month]; - } - @override Widget build(final BuildContext context) { - final ThemeData theme = Theme.of(context); - final Map query = Routemaster.of( context, ).currentRoute.queryParameters; - final String searchName = query['name'] ?? ''; - final double? minAmount = double.tryParse(query['min'] ?? ''); - final double? maxAmount = double.tryParse(query['max'] ?? ''); - final List> filteredTransactions = - transactions.where((final Map tx) { - final date = tx['date']! as DateTime; - - var rangeMatch = true; - if (selectedRange != null) { - rangeMatch = - !date.isBefore(selectedRange!.start) && - !date.isAfter(selectedRange!.end); - } - - final bool nameMatch = - searchName.isEmpty || - ((tx['name'] ?? '') as String).contains(searchName); - final amount = (tx['amount'] ?? 0) as double; - final bool amountMatch = - (minAmount == null || amount >= minAmount) && - (maxAmount == null || amount <= maxAmount); - - return rangeMatch && nameMatch && amountMatch; - }).toList()..sort( - (final Map a, final Map b) => - (b['date']! as DateTime).compareTo(a['date']! as DateTime), - ); - - final transactionsByMonth = >>{}; - for (final tx in filteredTransactions) { - final date = tx['date']! as DateTime; - final monthName = '${_monthName(date.month)} ${date.year}'; - transactionsByMonth - .putIfAbsent(monthName, () => >[]) - .add(tx); - } + final String? name = query['name']; + final double? amountMin = double.tryParse(query['amountMin'] ?? ''); + final double? amountMax = double.tryParse(query['amountMax'] ?? ''); + final DateTime? dateFrom = DateTime.tryParse(query['dateFrom'] ?? ''); + final DateTime? dateTo = DateTime.tryParse(query['dateTo'] ?? ''); return Scaffold( body: SafeArea( @@ -174,10 +38,29 @@ class _TrendState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _inputFields(theme), + InputFields( + name: name, + amountMin: amountMin, + amountMax: amountMax, + dateFrom: dateFrom, + dateTo: dateTo, + onChanged: () { + setState(() {}); + }, + ), const SizedBox(height: 24), - _lineChart(theme), + MonthlyBalanceChart( + name: name, + amountMin: amountMin, + amountMax: amountMax, + dateFrom: (dateFrom != null) + ? DateTime(dateFrom.year, dateFrom.month, dateFrom.day - 1) + : null, + dateTo: (dateTo != null) + ? DateTime(dateTo.year, dateTo.month, dateTo.day + 1) + : null, + ), const SizedBox(height: 24), const Text( @@ -185,180 +68,21 @@ class _TrendState extends State { style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), - _transactions(theme, transactionsByMonth), + TransactionList( + name: name, + amountMin: amountMin, + amountMax: amountMax, + dateFrom: (dateFrom != null) + ? DateTime(dateFrom.year, dateFrom.month, dateFrom.day - 1) + : null, + dateTo: (dateTo != null) + ? DateTime(dateTo.year, dateTo.month, dateTo.day + 1) + : null, + ), ], ), ), ), ); } - - Widget _inputFields(final ThemeData theme) => Row( - children: [ - Expanded( - child: TextField( - controller: nameController, - decoration: const InputDecoration( - labelText: 'Name', - border: OutlineInputBorder(), - ), - onChanged: (_) => _updateUrl(), - ), - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: minController, - decoration: const InputDecoration( - labelText: 'Min Betrag', - border: OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: (_) => _updateUrl(), - ), - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: maxController, - decoration: const InputDecoration( - labelText: 'Max Betrag', - border: OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: (_) => _updateUrl(), - ), - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - readOnly: true, - controller: TextEditingController( - text: selectedRange != null - ? '${selectedRange!.start.day}.${selectedRange!.start.month}.${selectedRange!.start.year} - ${selectedRange!.end.day}.${selectedRange!.end.month}.${selectedRange!.end.year}' - : '', - ), - decoration: InputDecoration( - labelText: 'Zeitraum', - border: const OutlineInputBorder(), - suffixIcon: Icon( - Icons.calendar_month, - color: theme.colorScheme.onSurface, - ), - ), - onTap: _pickDateRange, - ), - ), - ], - ); - - Widget _lineChart(final ThemeData theme) => SizedBox( - height: 180, - child: LineChart( - LineChartData( - minY: 0, - maxY: - monthlyBalance.values.reduce( - (final double a, final double b) => a > b ? a : b, - ) + - 500, - 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 months = monthlyBalance.keys.toList(); - if (value.toInt() >= 0 && value.toInt() < months.length) { - return Text( - months[value.toInt()], - style: theme.textTheme.bodySmall, - ); - } - return const Text(''); - }, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 50, - interval: 500, - getTitlesWidget: (final double value, final TitleMeta meta) => - Text('${value.toInt()} €', style: theme.textTheme.bodySmall), - ), - ), - ), - lineBarsData: [ - LineChartBarData( - spots: List.generate( - monthlyBalance.length, - (final int i) => - FlSpot(i.toDouble(), monthlyBalance.values.elementAt(i)), - ), - isCurved: true, - barWidth: 3, - color: theme.colorScheme.primary, - ), - ], - ), - ), - ); - - Widget _transactions( - final ThemeData theme, - final Map>> transactionsByMonth, - ) => Expanded( - child: CustomScrollView( - slivers: transactionsByMonth.entries - .map( - (final MapEntry>> entry) => - SliverStickyHeader( - header: Container( - color: theme.colorScheme.surface, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - alignment: Alignment.centerLeft, - child: Text( - entry.key, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - sliver: SliverList( - delegate: SliverChildBuilderDelegate(( - final BuildContext context, - final int index, - ) { - final Map tx = entry.value[index]; - final amount = (tx['amount'] ?? 0) as double; - final date = tx['date']! as DateTime; - return ListTile( - contentPadding: EdgeInsets.zero, - title: Text((tx['name'] ?? '') as String), - subtitle: Text( - '${date.day}.${date.month}.${date.year}', - ), - trailing: Text( - '${amount.toStringAsFixed(2)} €', - style: TextStyle( - color: amount >= 0 ? Colors.green : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ); - }, childCount: entry.value.length), - ), - ), - ) - .toList(), - ), - ); }