Make the pie chart widget reusable

This commit is contained in:
2025-05-06 21:59:03 +02:00
parent d40d24f759
commit 63b5354b72
24 changed files with 2264 additions and 1924 deletions

View File

@@ -195,7 +195,8 @@ class OkaneNavigationLayout extends StatelessWidget {
},
),
if (p.showAccountName && state.activeAccountIndex != null)
if (p.showAccountName &&
state.activeAccountIndex != null)
Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
@@ -218,7 +219,8 @@ class OkaneNavigationLayout extends StatelessWidget {
),
ScreenSize.normal => Column(
children: [
if (p.showAccountName && state.activeAccountIndex != null)
if (p.showAccountName &&
state.activeAccountIndex != null)
AccountIndicator(
accountName:
state

View File

@@ -9,6 +9,8 @@ import 'package:okane/ui/pages/account/total_balance_card.dart';
import 'package:okane/ui/pages/account/upcoming_transactions_card.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 AccountListPage extends StatefulWidget {
final bool isPage;
@@ -102,18 +104,25 @@ class AccountListPageState extends State<AccountListPage> {
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: TotalBalanceCard(),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: UpcomingTransactionsCard(),
),
Row(
Wrap(
children: [
Padding(padding: EdgeInsets.all(16), child: BreakdownCard()),
Padding(
padding: EdgeInsets.all(8),
//child: BreakdownCard(),
child: PieChartCard(
titleText: "Spending Breakdown",
fallbackText: "No spending available",
items: [],
),
),
],
),
],

View File

@@ -8,6 +8,8 @@ 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";
@@ -71,101 +73,126 @@ class BreakdownCard extends StatelessWidget {
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<CoreCubit>();
return Card(
child: Padding(
padding: const EdgeInsets.all(8),
child: BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
if (bloc.activeAccount == null) {
return Text("No active account");
return BlocBuilder<CoreCubit, CoreState>(
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,
);
}
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)}",
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<CoreCubit, CoreState>(
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(),
);
},
);
},
),
);
}

View File

@@ -29,7 +29,7 @@ class TotalBalanceCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Total balance",
"Total balance",
style: Theme.of(context).textTheme.titleLarge,
),
FutureBuilder(

View File

@@ -23,44 +23,45 @@ class AddBudgetState extends State<AddBudgetPopup> {
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(
hintText: "Budget name",
),
decoration: InputDecoration(hintText: "Budget name"),
controller: _budgetNameEditController,
),
TextField(
decoration: InputDecoration(
hintText: "Income",
),
decoration: InputDecoration(hintText: "Income"),
controller: _budgetIncomeEditController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () async {
if (_budgetNameEditController.text.isEmpty || _budgetIncomeEditController.text.isEmpty) {
return;
}
onPressed: () async {
if (_budgetNameEditController.text.isEmpty ||
_budgetIncomeEditController.text.isEmpty) {
return;
}
final bloc = GetIt.I.get<CoreCubit>();
final budget = Budget()
..name = _budgetNameEditController.text
..period = BudgetPeriod.month
..includeOtherSpendings = false
..income = double.parse(_budgetIncomeEditController.text)
..account.value = bloc.activeAccount!;
await upsertBudget(budget);
widget.onDone();
},
child: Text("Add"),
final bloc = GetIt.I.get<CoreCubit>();
final budget =
Budget()
..name = _budgetNameEditController.text
..period = BudgetPeriod.month
..includeOtherSpendings = false
..income = double.parse(_budgetIncomeEditController.text)
..account.value = bloc.activeAccount!;
await upsertBudget(budget);
widget.onDone();
},
child: Text("Add"),
),
],
),
],
);
}
}
}

View File

@@ -11,7 +11,11 @@ class AddBudgetItemPopup extends StatefulWidget {
final VoidCallback onDone;
final Budget budget;
const AddBudgetItemPopup({super.key, required this.onDone, required this.budget});
const AddBudgetItemPopup({
super.key,
required this.onDone,
required this.budget,
});
@override
AddBudgetItemState createState() => AddBudgetItemState();
@@ -31,55 +35,67 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
Text("Expense category"),
OutlinedButton(
onPressed: () async {
final category = await showDialogOrModal(
context: context,
builder: (_) => AddExpenseCategory(),
);
if (category == null) {
return;
}
onPressed: () async {
final category = await showDialogOrModal(
context: context,
builder: (_) => AddExpenseCategory(),
);
if (category == null) {
return;
}
setState(() => _expenseCategory = category);
},
child: Text(_expenseCategory?.name ?? "None"),
setState(() => _expenseCategory = category);
},
child: Text(_expenseCategory?.name ?? "None"),
),
],
),
TextField(
decoration: InputDecoration(
hintText: "Amount",
),
decoration: InputDecoration(hintText: "Amount"),
controller: _budgetItemAmountEditController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () async {
if (_budgetItemAmountEditController.text.isEmpty || _expenseCategory == null) {
return;
}
if (widget.budget.items.where((i) => i.expenseCategory.value!.name == _expenseCategory!.name).firstOrNull != null) {
return;
}
onPressed: () async {
if (_budgetItemAmountEditController.text.isEmpty ||
_expenseCategory == null) {
return;
}
if (widget.budget.items
.where(
(i) =>
i.expenseCategory.value!.name ==
_expenseCategory!.name,
)
.firstOrNull !=
null) {
return;
}
final item = BudgetItem()
..expenseCategory.value = _expenseCategory
..amount = double.parse(_budgetItemAmountEditController.text);
await upsertBudgetItem(item);
widget.budget.items.add(item);
await upsertBudget(widget.budget);
widget.onDone();
},
child: Text("Add"),
final item =
BudgetItem()
..expenseCategory.value = _expenseCategory
..amount = double.parse(
_budgetItemAmountEditController.text,
);
await upsertBudgetItem(item);
widget.budget.items.add(item);
await upsertBudget(widget.budget);
widget.onDone();
},
child: Text("Add"),
),
],
),
],
);
}
}
}

View File

@@ -8,6 +8,8 @@ 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;
@@ -256,211 +258,48 @@ class BudgetDetailsPage extends StatelessWidget {
),
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,
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,
),
),
),
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(),
),
),
),
),
),
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,
),
),
)
.toList(),
),
),
],
),
],
),
),
),
)
.toList(),
titleText: "Budget breakdown",
),
),
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: PieChartCard(
fallbackText: "No spending available",
valueConverter: formatCurrency,
items:
spending.entries
.map(
(e) => (
title: e.key,
value: e.value.abs(),
color: colorHash(e.key),
),
),
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(),
),
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 8,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children:
spending.keys
.map(
(k) => LegendItem(
text: k,
color: colorHash(k),
),
)
.toList(),
),
),
],
),
],
),
),
),
)
.toList(),
titleText: "Spending Breakdown",
),
],
),
),
],
),
Padding(
padding: EdgeInsets.all(8),

View File

@@ -19,9 +19,7 @@ class BudgetListPage extends StatelessWidget {
if (state.budgets.isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("No budgets"),
],
children: [Text("No budgets")],
);
}
@@ -74,18 +72,18 @@ class BudgetListPage extends StatelessWidget {
right: 16,
bottom: 16,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialogOrModal(
context: context,
builder:
(_) => AddBudgetPopup(
onDone: () {
Navigator.of(context).pop();
},
),
);
},
child: Icon(Icons.add),
onPressed: () {
showDialogOrModal(
context: context,
builder:
(_) => AddBudgetPopup(
onDone: () {
Navigator.of(context).pop();
},
),
);
},
),
),
],

View File

@@ -6,45 +6,47 @@ class EditBudgetPopup extends StatefulWidget {
final Budget budget;
final VoidCallback onDone;
const EditBudgetPopup({required this.budget, required this.onDone, super.key});
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",
),
decoration: InputDecoration(hintText: "Name"),
controller: _budgetNameEditController,
),
Row(
children: [
Text("Include other spendings"),
Switch(
value: _includeOtherSpendings,
onChanged: (value) {
setState(() => _includeOtherSpendings = value);
},
value: _includeOtherSpendings,
onChanged: (value) {
setState(() => _includeOtherSpendings = value);
},
),
],
),
@@ -52,26 +54,28 @@ class EditBudgetState extends State<EditBudgetPopup> {
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);
onPressed: () async {
if (_budgetNameEditController.text.isEmpty) {
return;
}
if (_budgetNameEditController.text == widget.budget.name &&
_includeOtherSpendings ==
widget.budget.includeOtherSpendings) {
widget.onDone();
},
child: Text("Save"),
return;
}
widget.budget
..name = _budgetNameEditController.text
..includeOtherSpendings = _includeOtherSpendings;
await upsertBudget(widget.budget);
widget.onDone();
},
child: Text("Save"),
),
],
),
],
);
}
}
}

View File

@@ -99,9 +99,7 @@ class CoreCubit extends Cubit<CoreState> {
emit(state.copyWith(expenseCategories: await getExpenseCategories()));
});
_budgetsStreamSubscription?.cancel();
_budgetsStreamSubscription = watchBudgets(activeAccount!).listen((
_,
) async {
_budgetsStreamSubscription = watchBudgets(activeAccount!).listen((_) async {
emit(state.copyWith(budgets: await getBudgets(activeAccount!)));
});
}

View File

@@ -12,7 +12,8 @@ part of 'core.dart';
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$CoreState {
@@ -41,18 +42,19 @@ abstract class $CoreStateCopyWith<$Res> {
factory $CoreStateCopyWith(CoreState value, $Res Function(CoreState) then) =
_$CoreStateCopyWithImpl<$Res, CoreState>;
@useResult
$Res call(
{OkanePage activePage,
int? activeAccountIndex,
Transaction? activeTransaction,
List<Account> accounts,
List<RecurringTransaction> recurringTransactions,
List<Transaction> transactions,
List<TransactionTemplate> transactionTemplates,
List<Beneficiary> beneficiaries,
List<ExpenseCategory> expenseCategories,
List<Budget> budgets,
Budget? activeBudget});
$Res call({
OkanePage activePage,
int? activeAccountIndex,
Transaction? activeTransaction,
List<Account> accounts,
List<RecurringTransaction> recurringTransactions,
List<Transaction> transactions,
List<TransactionTemplate> transactionTemplates,
List<Beneficiary> beneficiaries,
List<ExpenseCategory> expenseCategories,
List<Budget> budgets,
Budget? activeBudget,
});
}
/// @nodoc
@@ -80,52 +82,66 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
Object? budgets = null,
Object? activeBudget = freezed,
}) {
return _then(_value.copyWith(
activePage: null == activePage
? _value.activePage
: activePage // ignore: cast_nullable_to_non_nullable
as OkanePage,
activeAccountIndex: freezed == activeAccountIndex
? _value.activeAccountIndex
: activeAccountIndex // ignore: cast_nullable_to_non_nullable
as int?,
activeTransaction: freezed == activeTransaction
? _value.activeTransaction
: activeTransaction // ignore: cast_nullable_to_non_nullable
as Transaction?,
accounts: null == accounts
? _value.accounts
: accounts // ignore: cast_nullable_to_non_nullable
as List<Account>,
recurringTransactions: null == recurringTransactions
? _value.recurringTransactions
: recurringTransactions // ignore: cast_nullable_to_non_nullable
as List<RecurringTransaction>,
transactions: null == transactions
? _value.transactions
: transactions // ignore: cast_nullable_to_non_nullable
as List<Transaction>,
transactionTemplates: null == transactionTemplates
? _value.transactionTemplates
: transactionTemplates // ignore: cast_nullable_to_non_nullable
as List<TransactionTemplate>,
beneficiaries: null == beneficiaries
? _value.beneficiaries
: beneficiaries // ignore: cast_nullable_to_non_nullable
as List<Beneficiary>,
expenseCategories: null == expenseCategories
? _value.expenseCategories
: expenseCategories // ignore: cast_nullable_to_non_nullable
as List<ExpenseCategory>,
budgets: null == budgets
? _value.budgets
: budgets // ignore: cast_nullable_to_non_nullable
as List<Budget>,
activeBudget: freezed == activeBudget
? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?,
) as $Val);
return _then(
_value.copyWith(
activePage:
null == activePage
? _value.activePage
: activePage // ignore: cast_nullable_to_non_nullable
as OkanePage,
activeAccountIndex:
freezed == activeAccountIndex
? _value.activeAccountIndex
: activeAccountIndex // ignore: cast_nullable_to_non_nullable
as int?,
activeTransaction:
freezed == activeTransaction
? _value.activeTransaction
: activeTransaction // ignore: cast_nullable_to_non_nullable
as Transaction?,
accounts:
null == accounts
? _value.accounts
: accounts // ignore: cast_nullable_to_non_nullable
as List<Account>,
recurringTransactions:
null == recurringTransactions
? _value.recurringTransactions
: recurringTransactions // ignore: cast_nullable_to_non_nullable
as List<RecurringTransaction>,
transactions:
null == transactions
? _value.transactions
: transactions // ignore: cast_nullable_to_non_nullable
as List<Transaction>,
transactionTemplates:
null == transactionTemplates
? _value.transactionTemplates
: transactionTemplates // ignore: cast_nullable_to_non_nullable
as List<TransactionTemplate>,
beneficiaries:
null == beneficiaries
? _value.beneficiaries
: beneficiaries // ignore: cast_nullable_to_non_nullable
as List<Beneficiary>,
expenseCategories:
null == expenseCategories
? _value.expenseCategories
: expenseCategories // ignore: cast_nullable_to_non_nullable
as List<ExpenseCategory>,
budgets:
null == budgets
? _value.budgets
: budgets // ignore: cast_nullable_to_non_nullable
as List<Budget>,
activeBudget:
freezed == activeBudget
? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?,
)
as $Val,
);
}
}
@@ -133,22 +149,24 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
abstract class _$$CoreStateImplCopyWith<$Res>
implements $CoreStateCopyWith<$Res> {
factory _$$CoreStateImplCopyWith(
_$CoreStateImpl value, $Res Function(_$CoreStateImpl) then) =
__$$CoreStateImplCopyWithImpl<$Res>;
_$CoreStateImpl value,
$Res Function(_$CoreStateImpl) then,
) = __$$CoreStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{OkanePage activePage,
int? activeAccountIndex,
Transaction? activeTransaction,
List<Account> accounts,
List<RecurringTransaction> recurringTransactions,
List<Transaction> transactions,
List<TransactionTemplate> transactionTemplates,
List<Beneficiary> beneficiaries,
List<ExpenseCategory> expenseCategories,
List<Budget> budgets,
Budget? activeBudget});
$Res call({
OkanePage activePage,
int? activeAccountIndex,
Transaction? activeTransaction,
List<Account> accounts,
List<RecurringTransaction> recurringTransactions,
List<Transaction> transactions,
List<TransactionTemplate> transactionTemplates,
List<Beneficiary> beneficiaries,
List<ExpenseCategory> expenseCategories,
List<Budget> budgets,
Budget? activeBudget,
});
}
/// @nodoc
@@ -156,8 +174,9 @@ class __$$CoreStateImplCopyWithImpl<$Res>
extends _$CoreStateCopyWithImpl<$Res, _$CoreStateImpl>
implements _$$CoreStateImplCopyWith<$Res> {
__$$CoreStateImplCopyWithImpl(
_$CoreStateImpl _value, $Res Function(_$CoreStateImpl) _then)
: super(_value, _then);
_$CoreStateImpl _value,
$Res Function(_$CoreStateImpl) _then,
) : super(_value, _then);
@pragma('vm:prefer-inline')
@override
@@ -174,77 +193,90 @@ class __$$CoreStateImplCopyWithImpl<$Res>
Object? budgets = null,
Object? activeBudget = freezed,
}) {
return _then(_$CoreStateImpl(
activePage: null == activePage
? _value.activePage
: activePage // ignore: cast_nullable_to_non_nullable
as OkanePage,
activeAccountIndex: freezed == activeAccountIndex
? _value.activeAccountIndex
: activeAccountIndex // ignore: cast_nullable_to_non_nullable
as int?,
activeTransaction: freezed == activeTransaction
? _value.activeTransaction
: activeTransaction // ignore: cast_nullable_to_non_nullable
as Transaction?,
accounts: null == accounts
? _value._accounts
: accounts // ignore: cast_nullable_to_non_nullable
as List<Account>,
recurringTransactions: null == recurringTransactions
? _value._recurringTransactions
: recurringTransactions // ignore: cast_nullable_to_non_nullable
as List<RecurringTransaction>,
transactions: null == transactions
? _value._transactions
: transactions // ignore: cast_nullable_to_non_nullable
as List<Transaction>,
transactionTemplates: null == transactionTemplates
? _value._transactionTemplates
: transactionTemplates // ignore: cast_nullable_to_non_nullable
as List<TransactionTemplate>,
beneficiaries: null == beneficiaries
? _value._beneficiaries
: beneficiaries // ignore: cast_nullable_to_non_nullable
as List<Beneficiary>,
expenseCategories: null == expenseCategories
? _value._expenseCategories
: expenseCategories // ignore: cast_nullable_to_non_nullable
as List<ExpenseCategory>,
budgets: null == budgets
? _value._budgets
: budgets // ignore: cast_nullable_to_non_nullable
as List<Budget>,
activeBudget: freezed == activeBudget
? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?,
));
return _then(
_$CoreStateImpl(
activePage:
null == activePage
? _value.activePage
: activePage // ignore: cast_nullable_to_non_nullable
as OkanePage,
activeAccountIndex:
freezed == activeAccountIndex
? _value.activeAccountIndex
: activeAccountIndex // ignore: cast_nullable_to_non_nullable
as int?,
activeTransaction:
freezed == activeTransaction
? _value.activeTransaction
: activeTransaction // ignore: cast_nullable_to_non_nullable
as Transaction?,
accounts:
null == accounts
? _value._accounts
: accounts // ignore: cast_nullable_to_non_nullable
as List<Account>,
recurringTransactions:
null == recurringTransactions
? _value._recurringTransactions
: recurringTransactions // ignore: cast_nullable_to_non_nullable
as List<RecurringTransaction>,
transactions:
null == transactions
? _value._transactions
: transactions // ignore: cast_nullable_to_non_nullable
as List<Transaction>,
transactionTemplates:
null == transactionTemplates
? _value._transactionTemplates
: transactionTemplates // ignore: cast_nullable_to_non_nullable
as List<TransactionTemplate>,
beneficiaries:
null == beneficiaries
? _value._beneficiaries
: beneficiaries // ignore: cast_nullable_to_non_nullable
as List<Beneficiary>,
expenseCategories:
null == expenseCategories
? _value._expenseCategories
: expenseCategories // ignore: cast_nullable_to_non_nullable
as List<ExpenseCategory>,
budgets:
null == budgets
? _value._budgets
: budgets // ignore: cast_nullable_to_non_nullable
as List<Budget>,
activeBudget:
freezed == activeBudget
? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?,
),
);
}
}
/// @nodoc
class _$CoreStateImpl implements _CoreState {
const _$CoreStateImpl(
{this.activePage = OkanePage.accounts,
this.activeAccountIndex,
this.activeTransaction = null,
final List<Account> accounts = const [],
final List<RecurringTransaction> recurringTransactions = const [],
final List<Transaction> transactions = const [],
final List<TransactionTemplate> transactionTemplates = const [],
final List<Beneficiary> beneficiaries = const [],
final List<ExpenseCategory> expenseCategories = const [],
final List<Budget> budgets = const [],
this.activeBudget = null})
: _accounts = accounts,
_recurringTransactions = recurringTransactions,
_transactions = transactions,
_transactionTemplates = transactionTemplates,
_beneficiaries = beneficiaries,
_expenseCategories = expenseCategories,
_budgets = budgets;
const _$CoreStateImpl({
this.activePage = OkanePage.accounts,
this.activeAccountIndex,
this.activeTransaction = null,
final List<Account> accounts = const [],
final List<RecurringTransaction> recurringTransactions = const [],
final List<Transaction> transactions = const [],
final List<TransactionTemplate> transactionTemplates = const [],
final List<Beneficiary> beneficiaries = const [],
final List<ExpenseCategory> expenseCategories = const [],
final List<Budget> budgets = const [],
this.activeBudget = null,
}) : _accounts = accounts,
_recurringTransactions = recurringTransactions,
_transactions = transactions,
_transactionTemplates = transactionTemplates,
_beneficiaries = beneficiaries,
_expenseCategories = expenseCategories,
_budgets = budgets;
@override
@JsonKey()
@@ -341,16 +373,26 @@ class _$CoreStateImpl implements _CoreState {
(identical(other.activeTransaction, activeTransaction) ||
other.activeTransaction == activeTransaction) &&
const DeepCollectionEquality().equals(other._accounts, _accounts) &&
const DeepCollectionEquality()
.equals(other._recurringTransactions, _recurringTransactions) &&
const DeepCollectionEquality()
.equals(other._transactions, _transactions) &&
const DeepCollectionEquality()
.equals(other._transactionTemplates, _transactionTemplates) &&
const DeepCollectionEquality()
.equals(other._beneficiaries, _beneficiaries) &&
const DeepCollectionEquality()
.equals(other._expenseCategories, _expenseCategories) &&
const DeepCollectionEquality().equals(
other._recurringTransactions,
_recurringTransactions,
) &&
const DeepCollectionEquality().equals(
other._transactions,
_transactions,
) &&
const DeepCollectionEquality().equals(
other._transactionTemplates,
_transactionTemplates,
) &&
const DeepCollectionEquality().equals(
other._beneficiaries,
_beneficiaries,
) &&
const DeepCollectionEquality().equals(
other._expenseCategories,
_expenseCategories,
) &&
const DeepCollectionEquality().equals(other._budgets, _budgets) &&
(identical(other.activeBudget, activeBudget) ||
other.activeBudget == activeBudget));
@@ -358,18 +400,19 @@ class _$CoreStateImpl implements _CoreState {
@override
int get hashCode => Object.hash(
runtimeType,
activePage,
activeAccountIndex,
activeTransaction,
const DeepCollectionEquality().hash(_accounts),
const DeepCollectionEquality().hash(_recurringTransactions),
const DeepCollectionEquality().hash(_transactions),
const DeepCollectionEquality().hash(_transactionTemplates),
const DeepCollectionEquality().hash(_beneficiaries),
const DeepCollectionEquality().hash(_expenseCategories),
const DeepCollectionEquality().hash(_budgets),
activeBudget);
runtimeType,
activePage,
activeAccountIndex,
activeTransaction,
const DeepCollectionEquality().hash(_accounts),
const DeepCollectionEquality().hash(_recurringTransactions),
const DeepCollectionEquality().hash(_transactions),
const DeepCollectionEquality().hash(_transactionTemplates),
const DeepCollectionEquality().hash(_beneficiaries),
const DeepCollectionEquality().hash(_expenseCategories),
const DeepCollectionEquality().hash(_budgets),
activeBudget,
);
@JsonKey(ignore: true)
@override
@@ -379,18 +422,19 @@ class _$CoreStateImpl implements _CoreState {
}
abstract class _CoreState implements CoreState {
const factory _CoreState(
{final OkanePage activePage,
final int? activeAccountIndex,
final Transaction? activeTransaction,
final List<Account> accounts,
final List<RecurringTransaction> recurringTransactions,
final List<Transaction> transactions,
final List<TransactionTemplate> transactionTemplates,
final List<Beneficiary> beneficiaries,
final List<ExpenseCategory> expenseCategories,
final List<Budget> budgets,
final Budget? activeBudget}) = _$CoreStateImpl;
const factory _CoreState({
final OkanePage activePage,
final int? activeAccountIndex,
final Transaction? activeTransaction,
final List<Account> accounts,
final List<RecurringTransaction> recurringTransactions,
final List<Transaction> transactions,
final List<TransactionTemplate> transactionTemplates,
final List<Beneficiary> beneficiaries,
final List<ExpenseCategory> expenseCategories,
final List<Budget> budgets,
final Budget? activeBudget,
}) = _$CoreStateImpl;
@override
OkanePage get activePage;

View File

@@ -17,7 +17,7 @@ Future<T?> showDialogOrModal<T>({
builder:
(context) => Padding(
padding: EdgeInsets.only(
bottom: 32 + MediaQuery.of(context).viewInsets.bottom,
bottom: 32 + MediaQuery.of(context).viewInsets.bottom,
),
child: builder(context),
),
@@ -74,12 +74,5 @@ String formatCurrency(double amount, {bool precise = true}) {
}
DateTime monthEnding(DateTime now) {
return DateTime(
now.year,
now.month,
32,
23,
59,
59,
);
}
return DateTime(now.year, now.month, 32, 23, 59, 59);
}

View File

@@ -22,8 +22,7 @@ class AccountIndicator extends StatelessWidget {
),
),
const Spacer(),
if (trailing != null)
trailing!,
if (trailing != null) trailing!,
],
),
);

View File

@@ -0,0 +1,84 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:okane/ui/pages/account/breakdown_card.dart';
typedef OkanePieChartSection = ({String title, double value, Color color});
typedef OkanePieChartValueConverter = String Function(double);
String numToString(double input) {
return input.toString();
}
class OkanePieChart extends StatelessWidget {
// Width of the pie chart
final double width;
// Height of the pie chart
final double height;
final List<OkanePieChartSection> items;
final OkanePieChartValueConverter valueConverter;
const OkanePieChart({
required this.items,
this.valueConverter = numToString,
this.width = 150,
this.height = 150,
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: width,
height: height,
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
borderData: FlBorderData(show: false),
sectionsSpace: 5,
centerSpaceRadius: 35,
sections:
items
.map(
(i) => PieChartSectionData(
value: i.value,
title: valueConverter(i.value),
titleStyle: TextStyle(
fontWeight: FontWeight.bold,
),
radius: 40,
color: i.color,
),
)
.toList(),
),
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children:
items
.map((i) => LegendItem(text: i.title, color: i.color))
.toList(),
),
),
],
);
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:okane/screen.dart';
import 'package:okane/ui/widgets/piechart.dart';
class ResponsiveCard extends StatelessWidget {
final String titleText;
final String? subtitleText;
final Widget child;
const ResponsiveCard({
super.key,
required this.titleText,
required this.child,
this.subtitleText,
});
@override
Widget build(BuildContext context) {
final screenSize = getScreenSize(context);
final card = Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(top: 8, left: 8, right: 8),
child: Text(
titleText,
style: Theme.of(
context,
).textTheme.titleLarge!.copyWith(fontWeight: FontWeight.bold),
),
),
child,
if (subtitleText != null) Text(subtitleText!),
],
),
);
return switch (screenSize) {
ScreenSize.small => Row(children: [Expanded(child: card)]),
ScreenSize.normal => Container(
constraints: BoxConstraints(maxWidth: ScreenSize.normal.size),
child: card,
),
};
}
}
class PieChartCard extends StatelessWidget {
final String titleText;
// Text to display when items is empty.
final String fallbackText;
final OkanePieChartValueConverter valueConverter;
final List<OkanePieChartSection> items;
const PieChartCard({
super.key,
this.valueConverter = numToString,
required this.items,
required this.fallbackText,
required this.titleText,
});
@override
Widget build(BuildContext context) {
final child =
items.isEmpty
? SizedBox(
width: 150,
height: 150,
child: Center(child: Text(fallbackText)),
)
: OkanePieChart(valueConverter: valueConverter, items: items);
return ResponsiveCard(titleText: titleText, child: child);
}
}