okane/lib/ui/pages/budgets/budget_details.dart

371 lines
15 KiB
Dart

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<void> 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<CoreCubit, CoreState>(
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<CoreCubit>();
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 = <String, double>{};
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: () {},
),
);
},
),
),
],
);
},
);
},
),
],
),
);
}
}