import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:okane/database/collections/budget.dart'; import 'package:okane/database/database.dart'; import 'package:okane/ui/pages/account/breakdown_card.dart'; import 'package:okane/ui/pages/budgets/add_budget_item.dart'; import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/utils.dart'; class BudgetDetailsPage extends StatelessWidget { final bool isPage; const BudgetDetailsPage({this.isPage = false, super.key}); static MaterialPageRoute get mobileRoute => MaterialPageRoute(builder: (_) => BudgetDetailsPage(isPage: true)); void _addBudgetItem(BuildContext context, CoreState state) { showDialogOrModal( context: context, builder: (_) => AddBudgetItemPopup( budget: state.activeBudget!, onDone: () { Navigator.of(context).pop(); }, ), ); } @override Widget build(BuildContext context) { return Scaffold( body: ListView( children: [ if (isPage) SizedBox( height: 50, child: Row( children: [ IconButton( icon: Icon(Icons.arrow_back), onPressed: () { Navigator.of(context).pop(); }, ), ], ), ), BlocBuilder( builder: (context, state) { if (state.activeBudget == null) { return Text("No budget selected"); } if (state.activeBudget!.items.isEmpty) { return Row( children: [ Text("No budget items added"), Padding( padding: EdgeInsets.only(left: 16), child: IconButton( onPressed: () => _addBudgetItem(context, state), icon: Icon(Icons.add), ), ), ], ); } final bloc = GetIt.I.get(); final today = DateTime.now(); return FutureBuilder( future: getTransactionsInTimeframe( bloc.activeAccount!, today, TransactionQueryDateOption.thisMonth, ), builder: (context, snapshot) { final daysLeft = switch (state.activeBudget!.period) { BudgetPeriod.month => monthEnding(today).difference(today).inDays, }; if (!snapshot.hasData) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text( "Budget items", style: Theme.of(context).textTheme.titleMedium, ), ListView.builder( shrinkWrap: true, itemCount: state.activeBudget!.items.length, itemBuilder: (context, index) { final item = state.activeBudget!.items.elementAt( index, ); final amount = formatCurrency(item.amount); return ListTile( title: Text( "${item.expenseCategory.value!.name} ($amount)", ), subtitle: Text("..."), ); }, ), ], ); } final categories = state.activeBudget!.items .map((i) => i.expenseCategory.value!.name) .toList(); final spending = {}; for (final t in snapshot.data!) { String categoryName; if (!categories.contains(t.expenseCategory.value?.name)) { if (!state.activeBudget!.includeOtherSpendings) { continue; } categoryName = "Other"; } else { categoryName = t.expenseCategory.value!.name; } spending.update( categoryName, (value) => value + t.amount, ifAbsent: () => t.amount, ); } final totalSpent = spending.isEmpty ? 0 : spending.values.reduce((acc, val) => acc + val); final budgetTotal = state.activeBudget!.items .map((i) => i.amount) .reduce((acc, val) => acc + val); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 100, child: ListView( scrollDirection: Axis.horizontal, children: [ Padding( padding: EdgeInsets.all(8), child: SizedBox( height: 100, width: 150, child: Card( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Days left", textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.titleLarge, ), Text( daysLeft.toString(), textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.bodyLarge, ), ], ), ), ), ), Padding( padding: EdgeInsets.all(8), child: SizedBox( height: 100, width: 150, child: Card( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Budget left", textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.titleLarge, ), Text( formatCurrency( budgetTotal + totalSpent, ), textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.bodyLarge, ), ], ), ), ), ), Padding( padding: EdgeInsets.all(8), child: SizedBox( height: 100, width: 150, child: Card( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Budget total", textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.titleLarge, ), Text( formatCurrency(budgetTotal), textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.bodyLarge, ), ], ), ), ), ), ], ), ), Wrap( children: [ Padding( padding: EdgeInsets.all(8), child: SizedBox( child: Card( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: EdgeInsets.only(top: 8), child: Text( "Budget breakdown", style: Theme.of( context, ).textTheme.titleLarge, textAlign: TextAlign.center, ), ), Row( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.all(16), child: SizedBox( width: 150, height: 150, child: AspectRatio( aspectRatio: 1, child: PieChart( PieChartData( borderData: FlBorderData( show: false, ), sectionsSpace: 0, centerSpaceRadius: 35, sections: state .activeBudget! .items .map( ( i, ) => PieChartSectionData( value: i.amount .abs(), title: formatCurrency( i.amount .abs(), ), titleStyle: TextStyle( fontWeight: FontWeight .bold, ), radius: 40, color: colorHash( i .expenseCategory .value! .name, ), ), ) .toList(), ), ), ), ), ), Padding( padding: EdgeInsets.symmetric( horizontal: 8, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: state.activeBudget!.items .map( (i) => LegendItem( text: i .expenseCategory .value! .name, color: colorHash( i .expenseCategory .value! .name, ), ), ) .toList(), ), ), ], ), ], ), ), ), ), Padding( padding: EdgeInsets.all(8), child: SizedBox( child: Card( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: EdgeInsets.only(top: 8), child: Text( "Spending breakdown", style: Theme.of( context, ).textTheme.titleLarge, textAlign: TextAlign.center, ), ), Row( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.all(16), child: SizedBox( width: 150, height: 150, child: AspectRatio( aspectRatio: 1, child: PieChart( PieChartData( borderData: FlBorderData( show: false, ), sectionsSpace: 0, centerSpaceRadius: 35, sections: spending.entries .map( ( e, ) => PieChartSectionData( value: e.value .abs(), title: formatCurrency( e.value .abs(), ), titleStyle: TextStyle( fontWeight: FontWeight .bold, ), radius: 40, color: colorHash( e.key, ), ), ) .toList(), ), ), ), ), ), Padding( padding: EdgeInsets.symmetric( horizontal: 8, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: spending.keys .map( (k) => LegendItem( text: k, color: colorHash(k), ), ) .toList(), ), ), ], ), ], ), ), ), ), ], ), Padding( padding: EdgeInsets.all(8), child: Row( children: [ Text( "Budget items", style: Theme.of(context).textTheme.titleMedium, ), Padding( padding: EdgeInsets.only(left: 16), child: IconButton( icon: Icon(Icons.add), onPressed: () => _addBudgetItem(context, state), ), ), ], ), ), Padding( padding: EdgeInsets.all(8), child: ListView.builder( shrinkWrap: true, itemCount: state.activeBudget!.items.length, itemBuilder: (context, index) { final item = state.activeBudget!.items.elementAt( index, ); final amount = formatCurrency(item.amount); final spent = spending[item.expenseCategory.value!.name]; final left = spent == null ? item.amount : item.amount + spent; final subtitleText = left < 0 ? "${formatCurrency(left)} over" : "${formatCurrency(left)} left"; return ListTile( title: Text( "${item.expenseCategory.value!.name} ($amount)", ), subtitle: Text( subtitleText, style: TextStyle( color: left < 0 ? Colors.red : Colors.green, ), ), trailing: IconButton( icon: Icon(Icons.delete), onPressed: () {}, ), ); }, ), ), ], ); }, ); }, ), ], ), ); } }