import 'dart:math'; 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/transaction.dart'; import 'package:okane/database/database.dart'; import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/utils.dart'; const CATEGORY_INCOME = "Income"; const CATEGORY_OTHER = "Other"; typedef LegendData = ({Map expenses, Map colors, double usable}); class LegendItem extends StatelessWidget { final String text; final Color color; const LegendItem({required this.text, required this.color, super.key}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ DecoratedBox( decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(5), ), child: SizedBox(width: 10, height: 10), ), Padding(padding: EdgeInsets.only(left: 8), child: Text(text)), ], ); } } class BreakdownCard extends StatelessWidget { const BreakdownCard({super.key}); LegendData _computeSections(List transactions) { Map expenses = {}; Map colors = {}; double usableMoney = 0; transactions.forEach((t) { String category; if (t.amount > 0) { category = CATEGORY_INCOME; colors[CATEGORY_INCOME] = Colors.green; } else { if (t.expenseCategory.value?.name == null) { category = CATEGORY_OTHER; colors[category] = Colors.red; } else { category = t.expenseCategory.value!.name; colors[category] = colorHash(t.expenseCategory.value!.name); } } expenses.update( category, (value) => value + t.amount.abs(), ifAbsent: () => t.amount.abs(), ); usableMoney += t.amount; }); return (expenses: expenses, colors: colors, usable: usableMoney); } @override Widget build(BuildContext context) { final bloc = GetIt.I.get(); return Card( child: Padding( padding: const EdgeInsets.all(8), child: BlocBuilder( builder: (context, state) { if (bloc.activeAccount == null) { return Text("No active account"); } return FutureBuilder( future: getLastTransactions( bloc.activeAccount!, DateTime.now(), 30, ), builder: (context, snapshot) { final title = Padding( padding: EdgeInsets.only(bottom: 16), child: Text("Expense Breakdown"), ); if (!snapshot.hasData) { return Column(children: [title, CircularProgressIndicator()]); } if (snapshot.data!.isEmpty) { return Column(children: [title, Text("No transactions")]); } final data = _computeSections(snapshot.data!); final sectionData = data.expenses.entries .map( (entry) => PieChartSectionData( value: entry.value, title: formatCurrency(entry.value, precise: false), titleStyle: TextStyle(fontWeight: FontWeight.bold), radius: 40, color: data.colors[entry.key]!, ), ) .toList(); return Column( children: [ title, Row( children: [ SizedBox( width: 150, height: 150, child: AspectRatio( aspectRatio: 1, child: PieChart( PieChartData( borderData: FlBorderData(show: false), sectionsSpace: 0, centerSpaceRadius: 35, sections: sectionData, ), ), ), ), Padding( padding: EdgeInsets.only(left: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: data.expenses.keys .map( (key) => LegendItem( text: key, color: data.colors[key]!, ), ) .toList(), ), ), ], ), Padding( padding: EdgeInsets.only(top: 16), child: Text( "Available money: ${formatCurrency(data.usable)}", ), ), ], ); }, ); }, ), ), ); } }