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'; import 'package:okane/ui/widgets/piechart.dart'; import 'package:okane/ui/widgets/piechart_card.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.symmetric(horizontal: 8), child: PieChartCard( fallbackText: "", valueConverter: formatCurrency, items: state.activeBudget!.items .map( (i) => ( title: i.expenseCategory.value!.name, value: i.amount, color: colorHash( i.expenseCategory.value!.name, ), ), ) .toList(), titleText: "Budget breakdown", ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: PieChartCard( fallbackText: "No spending available", valueConverter: formatCurrency, items: spending.entries .map( (e) => ( title: e.key, value: e.value.abs(), color: colorHash(e.key), ), ) .toList(), titleText: "Spending Breakdown", ), ), ], ), 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: () {}, ), ); }, ), ), ], ); }, ); }, ), ], ), ); } }