From 169669315c6dc08d468f808ce9caa6b808146cdc Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Tue, 29 Apr 2025 20:18:36 +0200 Subject: [PATCH] feat: add pickable daterange to report screen --- lib/main.dart | 7 + lib/screens/report_screen.dart | 272 +++++++++++++++++++++++++++++---- pubspec.lock | 9 +- pubspec.yaml | 5 +- 4 files changed, 258 insertions(+), 35 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f5c288c..7743e0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:timetracker/screens/main_screen.dart'; import 'package:timetracker/src/rust/api.dart'; @@ -68,6 +69,12 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Rust Time Tracker', + supportedLocales: const [Locale('de', 'DE'), Locale('en', '')], + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), home: const InitializerWidget(), ); diff --git a/lib/screens/report_screen.dart b/lib/screens/report_screen.dart index b24c37e..dcae7ae 100644 --- a/lib/screens/report_screen.dart +++ b/lib/screens/report_screen.dart @@ -1,9 +1,11 @@ import 'dart:developer'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:timetracker/src/rust/api.dart'; @@ -56,7 +58,7 @@ extension ReportDataFormatting on ReportData { } } -enum ReportPeriod { day, week, month, year } +enum ReportPeriod { day, week, month, year, custom } class ReportScreen extends StatefulWidget { const ReportScreen({super.key}); @@ -68,6 +70,8 @@ class ReportScreen extends StatefulWidget { class _ReportScreenState extends State { final DateTime _selectedDate = DateTime.now(); ReportPeriod _selectedPeriod = ReportPeriod.day; + DateTime _customStartDate = DateTime.now(); + DateTime _customEndDate = DateTime.now(); Tag? _selectedTag; ReportData? _reportData; bool _isLoadingReport = false; @@ -85,6 +89,9 @@ class _ReportScreenState extends State { @override void initState() { super.initState(); + final now = DateTime.now(); + _customStartDate = DateTime(now.year, now.month, now.day); + _customEndDate = DateTime(now.year, now.month, now.day); _generateReport(); } @@ -330,7 +337,7 @@ class _ReportScreenState extends State { ); } - (DateTime, DateTime) _calculateDateRange() { + (DateTime, DateTime) _calculateDateRange(ReportPeriod period) { final nowLocal = DateTime.now().toLocal(); final startOfDay = DateTime(nowLocal.year, nowLocal.month, nowLocal.day); @@ -352,6 +359,13 @@ class _ReportScreenState extends State { final startOfYear = DateTime(nowLocal.year, 1, 1); final endOfYear = DateTime(nowLocal.year + 1, 1, 1); return (startOfYear, endOfYear); + case ReportPeriod.custom: + final endExclusive = DateTime( + _customEndDate.year, + _customEndDate.month, + _customEndDate.day, + ).add(const Duration(days: 1)); + return (_customStartDate, endExclusive); } } @@ -363,12 +377,33 @@ class _ReportScreenState extends State { }); final timeService = context.read(); - final (start, end) = _calculateDateRange(); final tagId = _selectedTag?.id; - log("Generating report for TagID: $tagId, Start: $start, End: $end"); + late DateTime reportStartDate; + late DateTime reportEndDateExclusive; - final data = await timeService.flGetReport(tagId, start, end); + if (_selectedPeriod == ReportPeriod.custom) { + reportStartDate = _customStartDate; + reportEndDateExclusive = DateTime( + _customEndDate.year, + _customEndDate.month, + _customEndDate.day, + ).add(const Duration(days: 1)); + } else { + final (start, end) = _calculateDateRange(_selectedPeriod); + reportStartDate = start; + reportEndDateExclusive = end; + } + + log( + "Generating report for Period: $_selectedPeriod, TagID: $tagId, Start: $reportStartDate, End (Exclusive): $reportEndDateExclusive", + ); + + final data = await timeService.flGetReport( + tagId, + reportStartDate, + reportEndDateExclusive, + ); if (mounted) { setState(() { @@ -378,6 +413,29 @@ class _ReportScreenState extends State { } } + // Future _generateReport() async { + // if (!mounted) return; + // setState(() { + // _isLoadingReport = true; + // _reportData = null; + // }); + + // final timeService = context.read(); + // final (start, end) = _calculateDateRange(); + // final tagId = _selectedTag?.id; + + // log("Generating report for TagID: $tagId, Start: $start, End: $end"); + + // final data = await timeService.flGetReport(tagId, start, end); + + // if (mounted) { + // setState(() { + // _reportData = data; + // _isLoadingReport = false; + // }); + // } + // } + @override Widget build(BuildContext context) { final tags = context.watch().tags; @@ -405,7 +463,7 @@ class _ReportScreenState extends State { ), ], ) - : _buildReportView(), + : _buildReportView(_reportData!), ), ), ], @@ -415,33 +473,71 @@ class _ReportScreenState extends State { Widget _buildFilterControls(List tagOptions) { final DateFormat formatter = DateFormat('dd.MM.yyyy'); - final (start, end) = _calculateDateRange(); + final String dateRangeButtonText = + '${formatter.format(_customStartDate)} - ${formatter.format(_customEndDate)}'; return Padding( padding: const EdgeInsets.all(8.0), child: Wrap( spacing: 8.0, runSpacing: 4.0, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - DropdownButton( - value: _selectedPeriod, - onChanged: (ReportPeriod? newValue) { - if (newValue != null) { - setState(() { - _selectedPeriod = newValue; - }); - _generateReport(); - } - }, - items: - ReportPeriod.values.map((ReportPeriod period) { - return DropdownMenuItem( - value: period, - child: PlatformText( - period.toString().split('.').last.toUpperCase(), - ), - ); - }).toList(), + PlatformWidget( + material: + (_, __) => DropdownButton( + value: _selectedPeriod, + items: [ + ...ReportPeriod.values.map((period) { + String text; + switch (period) { + case ReportPeriod.day: + text = 'Tag'; + break; + case ReportPeriod.week: + text = 'Woche'; + break; + case ReportPeriod.month: + text = 'Monat'; + break; + case ReportPeriod.year: + text = 'Jahr'; + break; + case ReportPeriod.custom: + text = 'Benutzerdefiniert'; + break; + } + return DropdownMenuItem( + value: period, + child: PlatformText(text), + ); + }), + ], + onChanged: (ReportPeriod? newValue) { + if (newValue != null && newValue != _selectedPeriod) { + setState(() { + _selectedPeriod = newValue; + if (_selectedPeriod != ReportPeriod.custom) { + _generateReport(); + } else { + // Optional: Direkt den Picker öffnen, wenn "Custom" gewählt wird? + // WidgetsBinding.instance.addPostFrameCallback((_) => _selectDateRange()); + } + }); + } + }, + ), + // TODO: Cupertino Dropdown Alternative (komplexer, oft wird Button+Picker genutzt) + // Fürs Erste verwenden wir auch auf iOS das Material Dropdown + cupertino: + (_, __) => CupertinoButton( + padding: EdgeInsets.zero, + child: Text(_selectedPeriod.toString().split('.').last), + onPressed: () { + /* Hier müsste ein Picker geöffnet werden */ + }, + ), ), DropdownButton( @@ -462,23 +558,135 @@ class _ReportScreenState extends State { }).toList(), ), - Chip( - label: PlatformText( - '${formatter.format(start)} - ${formatter.format(end.subtract(const Duration(seconds: 1)))}', + if (_selectedPeriod == ReportPeriod.custom) + PlatformElevatedButton( + onPressed: _selectDateRange, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(FontAwesomeIcons.calendar), // PlatformIcons(context)), + const SizedBox(width: 8), + PlatformText(dateRangeButtonText), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), - ), ], ), ); } - Widget _buildReportView() { + // Widget _buildFilterControls(List tagOptions) { + // final DateFormat formatter = DateFormat('dd.MM.yyyy'); + // final (start, end) = _calculateDateRange(); + + // return Padding( + // padding: const EdgeInsets.all(8.0), + // child: Wrap( + // spacing: 8.0, + // runSpacing: 4.0, + // alignment: WrapAlignment.center, + // children: [ + // DropdownButton( + // value: _selectedPeriod, + // onChanged: (ReportPeriod? newValue) { + // if (newValue != null) { + // setState(() { + // _selectedPeriod = newValue; + // }); + // _generateReport(); + // } + // }, + // items: + // ReportPeriod.values.map((ReportPeriod period) { + // return DropdownMenuItem( + // value: period, + // child: PlatformText( + // period.toString().split('.').last.toUpperCase(), + // ), + // ); + // }).toList(), + // ), + + // DropdownButton( + // value: _selectedTag, + // hint: PlatformText("Alle Tags"), + // onChanged: (Tag? newValue) { + // setState(() { + // _selectedTag = newValue; + // }); + // _generateReport(); + // }, + // items: + // tagOptions.map((Tag? tag) { + // return DropdownMenuItem( + // value: tag, + // child: PlatformText(tag?.name ?? "Alle Tags"), + // ); + // }).toList(), + // ), + + // Chip( + // label: PlatformText( + // '${formatter.format(start)} - ${formatter.format(end.subtract(const Duration(seconds: 1)))}', + // ), + // ), + // ], + // ), + // ); + // } + + Future _selectDateRange() async { + final DateTimeRange initialRange = DateTimeRange( + start: _customStartDate, + end: _customEndDate, + ); + + final DateTimeRange? picked = await showDateRangePicker( + context: context, + locale: const Locale('de', 'DE'), + initialDateRange: initialRange, + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365)), + helpText: 'Zeitraum auswählen', + cancelText: 'Abbrechen', + confirmText: 'Ok', + saveText: 'Speichern', + ); + + if (picked != null) { + if (picked.start != _customStartDate || + picked.end != _customEndDate || + _selectedPeriod != ReportPeriod.custom) { + setState(() { + _customStartDate = DateTime( + picked.start.year, + picked.start.month, + picked.start.day, + ); + _customEndDate = DateTime( + picked.end.year, + picked.end.month, + picked.end.day, + ); + _selectedPeriod = ReportPeriod.custom; + }); + _generateReport(); + } + } + } + + Widget _buildReportView(ReportData reportData) { if (_reportData == null) { return Center( child: PlatformText('Keine Reportdaten geladen oder Fehler.'), ); } - final (startDate, endDate) = _calculateDateRange(); + final (startDate, endDate) = _calculateDateRange( + _selectedPeriod == ReportPeriod.custom + ? ReportPeriod.custom + : _selectedPeriod, + ); return ListView( children: [ diff --git a/pubspec.lock b/pubspec.lock index eb05bdd..77a96e6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -123,6 +123,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_platform_widgets: dependency: "direct main" description: @@ -174,10 +179,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b4bb4df..b081108 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.8 rust_lib_timetracker: @@ -17,7 +19,8 @@ dependencies: flutter_rust_bridge: 2.9.0 ffi: ^2.1.0 path_provider: ^2.1.1 - intl: ^0.20.2 + # intl: ^0.20.2 + intl: ^0.19.0 provider: ^6.1.1 fl_chart: ^0.70.2 flutter_platform_widgets: ^8.0.0