import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/main.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/drawer.dart'; import 'package:prasule/util/graphs.dart'; import 'package:prasule/util/utils.dart'; import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/setup.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:wheel_chooser/wheel_chooser.dart'; /// Shows data from a [Wallet] in graphs class GraphView extends StatefulWidget { /// Shows data from a [Wallet] in graphs const GraphView({super.key}); @override State createState() => _GraphViewState(); } class _GraphViewState extends State { var _selectedDate = DateTime.now(); Wallet? selectedWallet; List wallets = []; String? locale; bool yearly = true; @override void didChangeDependencies() { super.didChangeDependencies(); locale ??= Localizations.localeOf(context).languageCode; } List generateChartData(EntryType type) { final d = _selectedDate.add(const Duration(days: 31)); final data = List.filled( yearly ? 12 : DateTime(d.year, d.month, 0).day, 0, ); if (selectedWallet == null) return []; for (var i = 0; i < data.length; i++) { final entriesForRange = selectedWallet!.entries.where( (element) => ((!yearly) ? element.date.month == _selectedDate.month && element.date.year == _selectedDate.year && element.date.day == i + 1 : element.date.month == i + 1 && element.date.year == _selectedDate.year) && element.type == type, ); var sum = 0.0; for (final e in entriesForRange) { sum += e.data.amount; } data[i] = sum; } return data; } final availableYears = >[]; Future loadWallet() async { wallets = await WalletManager.listWallets(); if (wallets.isEmpty && mounted) { unawaited( Navigator.of(context) .pushReplacement(platformRoute((c) => const SetupView())), ); return; } selectedWallet = wallets.first; availableYears.clear(); for (final entry in selectedWallet!.entries) { if (!availableYears.any((element) => element.value == entry.date.year)) { availableYears.add( WheelChoice( value: entry.date.year, title: entry.date.year.toString(), ), ); } } setState(() {}); } int? chartType; @override void initState() { super.initState(); loadWallet(); SharedPreferences.getInstance().then((s) { chartType = s.getInt("monthlygraph") ?? 2; setState(() {}); }); } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( floatingActionButton: Tooltip( message: AppLocalizations.of(context).changeDate, child: FloatingActionButton( child: const Icon(Icons.calendar_month), onPressed: () async { var selectedYear = _selectedDate.year; var selectedMonth = _selectedDate.month; await showAdaptiveDialog( context: context, builder: (c) => AlertDialog.adaptive( title: Text( yearly ? AppLocalizations.of(context).selectYear : AppLocalizations.of(context).selectMonth, ), content: LimitedBox( maxHeight: MediaQuery.of(context).size.width * 0.7, maxWidth: MediaQuery.of(context).size.width * 0.8, child: Wrap( alignment: WrapAlignment.center, spacing: 5, children: [ if (!yearly) SizedBox( width: 120, height: 100, child: WheelChooser.choices( onChoiceChanged: (v) { selectedMonth = v as int; }, startPosition: _selectedDate.month - 1, choices: List>.generate( 12, (index) => WheelChoice( value: index + 1, title: DateFormat.MMMM(locale ?? "en").format( DateTime( _selectedDate.year, index + 1, ), ), ), ), ), ), SizedBox( height: 100, width: 80, child: WheelChooser.choices( startPosition: availableYears.indexWhere( (element) => element.value == _selectedDate.year, ), onChoiceChanged: (v) { selectedYear = v as int; }, choices: availableYears, ), ), ], ), ), actions: [ TextButton( onPressed: () { _selectedDate = DateTime(selectedYear, selectedMonth); Navigator.of(c).pop(); }, child: Text(AppLocalizations.of(context).ok), ), ], ), ); setState(() {}); }, ), ), appBar: AppBar( bottom: TabBar( tabs: [ Tab( child: Text(AppLocalizations.of(context).expenses), ), Tab( child: Text(AppLocalizations.of(context).incomePlural), ), ], ), title: DropdownButton( value: (selectedWallet == null) ? -1 : wallets.indexOf( wallets.where((w) => w.name == selectedWallet!.name).first, ), items: [ ...wallets.map( (e) => DropdownMenuItem( value: wallets.indexOf( e, ), child: Text(e.name), ), ), DropdownMenuItem( value: -1, child: Text(AppLocalizations.of(context).newWallet), ), ], onChanged: (v) async { if (v == null || v == -1) { await Navigator.of(context).push( platformRoute( (c) => const SetupView( newWallet: true, ), ), ); wallets = await WalletManager.listWallets(); logger.i(wallets.length); selectedWallet = wallets.last; setState(() {}); return; } selectedWallet = wallets[v]; setState(() {}); }, ), actions: [ PopupMenuButton( itemBuilder: (context) => [ AppLocalizations.of(context).settings, AppLocalizations.of(context).about, ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), onSelected: (value) { if (value == AppLocalizations.of(context).settings) { Navigator.of(context) .push( platformRoute( (context) => const SettingsView(), ), ) .then((value) async { selectedWallet = await WalletManager.loadWallet(selectedWallet!.name); final s = await SharedPreferences.getInstance(); chartType = s.getInt("monthlygraph") ?? 2; setState(() {}); }); } else if (value == AppLocalizations.of(context).about) { showAbout(context); } }, ), ], ), drawer: makeDrawer(context, 2), body: TabBarView( children: [ // EXPENSE TAB SingleChildScrollView( child: Center( child: (selectedWallet == null) ? const CircularProgressIndicator( strokeWidth: 5, ) : SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 200, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( AppLocalizations.of(context).monthly, style: const TextStyle( fontWeight: FontWeight.bold, ), ), Switch.adaptive( value: yearly, onChanged: (v) async { yearly = v; final s = await SharedPreferences.getInstance(); chartType = yearly ? (s.getInt("yearlygraph") ?? 1) : (s.getInt("monthlygraph") ?? 2); setState(() {}); }, ), Text( AppLocalizations.of(context).yearly, style: const TextStyle( fontWeight: FontWeight.bold, ), ), ], ), ), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: (MediaQuery.of(context) .platformBrightness == Brightness.light) ? [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 3, blurRadius: 7, offset: const Offset( 0, 3, ), ), ] : null, color: (MediaQuery.of(context) .platformBrightness == Brightness.dark) ? Theme.of(context) .colorScheme .secondaryContainer : Theme.of(context).colorScheme.surface, ), child: Padding( padding: const EdgeInsets.all(8), child: Column( children: [ Text( yearly ? AppLocalizations.of(context) .expensesPerYear( _selectedDate.year, ) : AppLocalizations.of(context) .expensesPerMonth( DateFormat.yMMMM(locale) .format(_selectedDate), ), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox( height: 15, ), SizedBox( width: MediaQuery.of(context).size.width * 0.9, height: MediaQuery.of(context).size.height * 0.35, child: (chartType == null) ? const CircularProgressIndicator() : (chartType == 1) ? ExpensesBarChart( currency: selectedWallet!.currency, date: _selectedDate, locale: locale ?? "en", yearly: yearly, expenseData: generateChartData( EntryType.expense, ), incomeData: const [], ) : Padding( padding: const EdgeInsets.all(8), child: ExpensesLineChart( currency: selectedWallet! .currency, date: _selectedDate, locale: locale ?? "en", yearly: yearly, expenseData: generateChartData( EntryType.expense, ), incomeData: const [], ), ), ), ], ), ), ), const SizedBox( height: 25, ), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: (MediaQuery.of(context) .platformBrightness == Brightness.light) ? [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 3, blurRadius: 7, offset: const Offset( 0, 3, ), ), ] : null, color: (MediaQuery.of(context) .platformBrightness == Brightness.dark) ? Theme.of(context) .colorScheme .secondaryContainer : Theme.of(context).colorScheme.surface, ), width: MediaQuery.of(context).size.width * 0.95, height: MediaQuery.of(context).size.height * 0.4, child: Column( children: [ const SizedBox( height: 10, ), Flexible( child: Text( textAlign: TextAlign.center, yearly ? AppLocalizations.of(context) .expensesPerYearCategory( _selectedDate.year, ) : AppLocalizations.of(context) .expensesPerMonthCategory( DateFormat.yMMMM(locale) .format(_selectedDate), ), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), Padding( padding: const EdgeInsets.all(6), child: CategoriesPieChart( // TODO: better size adaptivity without overflow locale: locale ?? "en", symbol: selectedWallet!.currency.symbol, entries: selectedWallet!.entries .where( (element) => ((!yearly) ? element.date.month == _selectedDate .month && element.date.year == _selectedDate.year : element.date.year == _selectedDate.year) && element.type == EntryType.expense, ) .toList(), categories: selectedWallet!.categories, ), ), ], ), ), ], ), ), ), ), // Expense Tab END SingleChildScrollView( child: Center( child: (selectedWallet == null) ? const CircularProgressIndicator( strokeWidth: 5, ) : SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 200, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( AppLocalizations.of(context).monthly, style: const TextStyle( fontWeight: FontWeight.bold, ), ), Switch.adaptive( value: yearly, onChanged: (v) async { yearly = v; final s = await SharedPreferences.getInstance(); chartType = yearly ? (s.getInt("yearlygraph") ?? 1) : (s.getInt("monthlygraph") ?? 2); setState(() {}); }, ), Text( AppLocalizations.of(context).yearly, style: const TextStyle( fontWeight: FontWeight.bold, ), ), ], ), ), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: (MediaQuery.of(context) .platformBrightness == Brightness.light) ? [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 3, blurRadius: 7, offset: const Offset( 0, 3, ), ), ] : null, color: (MediaQuery.of(context) .platformBrightness == Brightness.dark) ? Theme.of(context) .colorScheme .secondaryContainer : Theme.of(context).colorScheme.surface, ), child: Padding( padding: const EdgeInsets.all(8), child: Column( children: [ Text( yearly ? AppLocalizations.of(context) .incomePerYear( _selectedDate.year, ) : AppLocalizations.of(context) .incomePerMonth( DateFormat.yMMMM(locale) .format(_selectedDate), ), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox( height: 15, ), SizedBox( width: MediaQuery.of(context).size.width * 0.9, height: MediaQuery.of(context).size.height * 0.35, child: (chartType == null) ? const CircularProgressIndicator() : (chartType == 1) ? ExpensesBarChart( currency: selectedWallet!.currency, date: _selectedDate, locale: locale ?? "en", yearly: yearly, expenseData: const [], incomeData: generateChartData( EntryType.income, ), ) : Padding( padding: const EdgeInsets.all(8), child: ExpensesLineChart( currency: selectedWallet! .currency, date: _selectedDate, locale: locale ?? "en", yearly: yearly, expenseData: const [], incomeData: generateChartData( EntryType.income, ), ), ), ), ], ), ), ), const SizedBox( height: 25, ), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: (MediaQuery.of(context) .platformBrightness == Brightness.light) ? [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 3, blurRadius: 7, offset: const Offset( 0, 3, ), ), ] : null, color: (MediaQuery.of(context) .platformBrightness == Brightness.dark) ? Theme.of(context) .colorScheme .secondaryContainer : Theme.of(context).colorScheme.surface, ), width: MediaQuery.of(context).size.width * 0.95, height: MediaQuery.of(context).size.height * 0.4, child: Column( children: [ const SizedBox( height: 10, ), Flexible( child: Text( yearly ? AppLocalizations.of(context) .incomePerYearCategory( _selectedDate.year, ) : AppLocalizations.of(context) .incomePerMonthCategory( DateFormat.yMMMM(locale) .format(_selectedDate), ), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), Padding( padding: const EdgeInsets.all(6), child: CategoriesPieChart( locale: locale ?? "en", symbol: selectedWallet!.currency.symbol, entries: selectedWallet!.entries .where( (element) => ((!yearly) ? element.date.month == _selectedDate .month && element.date.year == _selectedDate.year : element.date.year == _selectedDate.year) && element.type == EntryType.income, ) .toList(), categories: selectedWallet!.categories, ), ), ], ), ), ], ), ), ), ), // Income Tab END ], ), ), ); } }