Allow including other spendings in a budget
This commit is contained in:
@@ -50,6 +50,7 @@ class AddBudgetState extends State<AddBudgetPopup> {
|
||||
final budget = Budget()
|
||||
..name = _budgetNameEditController.text
|
||||
..period = BudgetPeriod.month
|
||||
..includeOtherSpendings = false
|
||||
..income = double.parse(_budgetIncomeEditController.text)
|
||||
..account.value = bloc.activeAccount!;
|
||||
await upsertBudget(budget);
|
||||
|
||||
@@ -33,7 +33,7 @@ class BudgetDetailsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
body: ListView(
|
||||
children: [
|
||||
if (isPage)
|
||||
SizedBox(
|
||||
@@ -113,20 +113,34 @@ class BudgetDetailsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final categories =
|
||||
state.activeBudget!.items
|
||||
.map((i) => i.expenseCategory.value!.name)
|
||||
.toList();
|
||||
final spending = <String, double>{};
|
||||
for (final t in snapshot.data!) {
|
||||
if (t.expenseCategory.value == null) {
|
||||
continue;
|
||||
String categoryName;
|
||||
if (!categories.contains(t.expenseCategory.value?.name)) {
|
||||
if (!state.activeBudget!.includeOtherSpendings) {
|
||||
continue;
|
||||
}
|
||||
|
||||
categoryName = "Other";
|
||||
} else {
|
||||
categoryName = t.expenseCategory.value!.name;
|
||||
}
|
||||
|
||||
spending.update(
|
||||
t.expenseCategory.value!.name,
|
||||
categoryName,
|
||||
(value) => value + t.amount,
|
||||
ifAbsent: () => t.amount,
|
||||
);
|
||||
}
|
||||
|
||||
final totalSpent = spending.isEmpty ? 0 : spending.values.reduce((acc, val) => acc + val);
|
||||
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);
|
||||
@@ -179,24 +193,26 @@ class BudgetDetailsPage extends StatelessWidget {
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Budget left",
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge,
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
formatCurrency(budgetTotal + totalSpent),
|
||||
formatCurrency(
|
||||
budgetTotal + totalSpent,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge,
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -239,54 +255,97 @@ class BudgetDetailsPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
sectionsSpace: 0,
|
||||
centerSpaceRadius: 35,
|
||||
sections:
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
state.activeBudget!.items
|
||||
.map(
|
||||
(
|
||||
i,
|
||||
) => PieChartSectionData(
|
||||
value: i.amount.abs(),
|
||||
title: formatCurrency(
|
||||
i.amount.abs(),
|
||||
),
|
||||
titleStyle: TextStyle(
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
radius: 40,
|
||||
(i) => LegendItem(
|
||||
text:
|
||||
i
|
||||
.expenseCategory
|
||||
.value!
|
||||
.name,
|
||||
color: colorHash(
|
||||
i
|
||||
.expenseCategory
|
||||
@@ -298,44 +357,110 @@ class BudgetDetailsPage extends StatelessWidget {
|
||||
.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
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(),
|
||||
),
|
||||
)
|
||||
.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),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:okane/screen.dart';
|
||||
import 'package:okane/ui/pages/budgets/add_budget.dart';
|
||||
import 'package:okane/ui/pages/budgets/edit_budget.dart';
|
||||
import 'package:okane/ui/state/core.dart';
|
||||
import 'package:okane/ui/utils.dart';
|
||||
|
||||
@@ -42,14 +43,30 @@ class BudgetListPage extends StatelessWidget {
|
||||
(context, index) => ListTile(
|
||||
title: Text(state.budgets[index].name),
|
||||
selected: state.budgets[index] == state.activeBudget,
|
||||
trailing: IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
onPressed: () {
|
||||
// TODO
|
||||
},
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
showDialogOrModal(
|
||||
context: context,
|
||||
builder: (_) => EditBudgetPopup(
|
||||
budget: state.activeBudget!,
|
||||
onDone: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.redAccent),
|
||||
onPressed: () {
|
||||
// TODO
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GetIt.I.get<CoreCubit>().setActiveBudget(
|
||||
|
||||
77
lib/ui/pages/budgets/edit_budget.dart
Normal file
77
lib/ui/pages/budgets/edit_budget.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:okane/database/collections/budget.dart';
|
||||
import 'package:okane/database/database.dart';
|
||||
|
||||
class EditBudgetPopup extends StatefulWidget {
|
||||
final Budget budget;
|
||||
|
||||
final VoidCallback onDone;
|
||||
|
||||
const EditBudgetPopup({required this.budget, required this.onDone, super.key});
|
||||
|
||||
@override
|
||||
EditBudgetState createState() => EditBudgetState();
|
||||
}
|
||||
|
||||
class EditBudgetState extends State<EditBudgetPopup> {
|
||||
final _budgetNameEditController = TextEditingController();
|
||||
|
||||
late bool _includeOtherSpendings;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_budgetNameEditController.text = widget.budget.name;
|
||||
_includeOtherSpendings = widget.budget.includeOtherSpendings;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Name",
|
||||
),
|
||||
controller: _budgetNameEditController,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text("Include other spendings"),
|
||||
Switch(
|
||||
value: _includeOtherSpendings,
|
||||
onChanged: (value) {
|
||||
setState(() => _includeOtherSpendings = value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
if (_budgetNameEditController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (_budgetNameEditController.text == widget.budget.name && _includeOtherSpendings == widget.budget.includeOtherSpendings) {
|
||||
widget.onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
widget.budget
|
||||
..name = _budgetNameEditController.text
|
||||
..includeOtherSpendings = _includeOtherSpendings;
|
||||
await upsertBudget(widget.budget);
|
||||
widget.onDone();
|
||||
},
|
||||
child: Text("Save"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
class AccountIndicator extends StatelessWidget {
|
||||
final String accountName;
|
||||
|
||||
const AccountIndicator({super.key, required this.accountName});
|
||||
final Widget? trailing;
|
||||
|
||||
const AccountIndicator({super.key, this.trailing, required this.accountName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -12,12 +14,18 @@ class AccountIndicator extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
accountName,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Text(
|
||||
accountName,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (trailing != null)
|
||||
trailing!,
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user