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'; import 'package:okane/ui/widgets/piechart.dart'; import 'package:okane/ui/widgets/piechart_card.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); } Widget _buildCard(Widget child, String? subtitle) { return ResponsiveCard( titleText: "Expense Breakdown", subtitleText: subtitle, child: child, ); } Widget _buildCenterText(String text) { return _buildCard(Center(child: Text(text)), null); } @override Widget build(BuildContext context) { final bloc = GetIt.I.get(); return BlocBuilder( builder: (context, state) { if (bloc.activeAccount == null) { return _buildCenterText("No account active"); } return FutureBuilder( future: getLastTransactions(bloc.activeAccount!, DateTime.now(), 30), builder: (context, snapshot) { if (!snapshot.hasData) { return _buildCard( Padding( padding: EdgeInsets.all(16), child: SizedBox( width: 150 - 16 * 2, height: 150 - 16 * 2, child: CircularProgressIndicator(), ), ), null, ); } 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(); if (sectionData.isEmpty) { return _buildCenterText("No expenses available"); } return OkanePieChart( items: data.expenses.entries .map( (e) => ( title: e.key, value: e.value, color: colorHash(e.key), ), ) .toList(), ); }, ); }, ); return ResponsiveCard( titleText: "Expense Breakdown", 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) { if (!snapshot.hasData) { return CircularProgressIndicator(); } 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(); if (sectionData.isEmpty) { return Center(child: Text("No expenses")); } return OkanePieChart( items: data.expenses.entries .map( (e) => ( title: e.key, value: e.value, color: colorHash(e.key), ), ) .toList(), ); }, ); }, ), ); } }