Skip to content

Commit

Permalink
feat: Category selector improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
enrique-lozano committed Feb 6, 2024
1 parent 5c2e7a9 commit 4eb95bb
Show file tree
Hide file tree
Showing 17 changed files with 244 additions and 1,035 deletions.
2 changes: 2 additions & 0 deletions lib/app/budgets/budget_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:monekin/app/budgets/components/budget_evolution_chart.dart';
import 'package:monekin/app/transactions/widgets/transaction_list.dart';
import 'package:monekin/core/database/services/budget/budget_service.dart';
import 'package:monekin/core/models/budget/budget.dart';
import 'package:monekin/core/models/transaction/transaction.dart';
import 'package:monekin/core/models/transaction/transaction_status.dart';
import 'package:monekin/core/presentation/widgets/animated_progress_bar.dart';
import 'package:monekin/core/presentation/widgets/card_with_header.dart';
Expand Down Expand Up @@ -265,6 +266,7 @@ class _BudgetDetailsPageState extends State<BudgetDetailsPage> {
TransactionStatus.pending,
TransactionStatus.voided
}),
transactionTypes: [TransactionType.expense],
minDate: budget.currentDateRange.$1,
maxDate: budget.currentDateRange.$2,
includeParentCategoriesInSearch: true,
Expand Down
78 changes: 37 additions & 41 deletions lib/app/budgets/budget_form_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class _BudgetFormPageState extends State<BudgetFormPage> {

TextEditingController nameController = TextEditingController();

List<Category> categories = [];
List<Account> accounts = [];
List<Category>? categories;
List<Account>? accounts;

Periodicity? intervalPeriod = Periodicity.month;

Expand Down Expand Up @@ -99,8 +99,8 @@ class _BudgetFormPageState extends State<BudgetFormPage> {
intervalPeriod: intervalPeriod,
startDate: intervalPeriod == null ? startDate : null,
endDate: intervalPeriod == null ? endDate : null,
categories: categories.map((e) => e.id).toList(),
accounts: accounts.map((e) => e.id).toList(),
categories: categories?.map((e) => e.id).toList(),
accounts: accounts?.map((e) => e.id).toList(),
);

if (isEditMode) {
Expand Down Expand Up @@ -129,7 +129,7 @@ class _BudgetFormPageState extends State<BudgetFormPage> {
}
}

fillForm(Budget budget) {
fillForm(Budget budget) async {
nameController.text = budget.name;
valueController.text = budget.limitAmount.abs().toString();

Expand All @@ -138,31 +138,25 @@ class _BudgetFormPageState extends State<BudgetFormPage> {
endDate = budget.endDate;
}

CategoryService.instance
.getCategories(
predicate: (p0, p1) => p0.id.isIn(budget.categories),
)
.first
.then((value) {
setState(() {
categories = value;
});
});

AccountService.instance
.getAccounts(
predicate: (p0, p1) => p0.id.isIn(budget.accounts),
)
.first
.then((value) {
setState(() {
accounts = value;
});
});

setState(() {
intervalPeriod = budget.intervalPeriod;
});
categories = budget.categories == null
? null
: await CategoryService.instance
.getCategories(
predicate: (p0, p1) => p0.id.isIn(budget.categories!),
)
.first;

accounts = budget.accounts == null
? null
: await AccountService.instance
.getAccounts(
predicate: (p0, p1) => p0.id.isIn(budget.accounts!),
)
.first;

intervalPeriod = budget.intervalPeriod;

setState(() {});
}

@override
Expand All @@ -176,13 +170,15 @@ class _BudgetFormPageState extends State<BudgetFormPage> {
persistentFooterButtons: [
PersistentFooterButton(
child: FilledButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();

submitForm();
}
},
onPressed: categories != null && categories!.isEmpty
? null
: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();

submitForm();
}
},
icon: const Icon(Icons.save),
label: Text(
isEditMode ? t.budgets.form.edit : t.budgets.form.create),
Expand Down Expand Up @@ -245,16 +241,16 @@ class _BudgetFormPageState extends State<BudgetFormPage> {
const SizedBox(height: 16),
selector(
title: '${t.general.accounts} *',
inputValue: accounts.isNotEmpty
? accounts.map((e) => e.name).join(', ')
inputValue: accounts != null && accounts!.isNotEmpty
? accounts!.map((e) => e.name).join(', ')
: null,
onClick: () async {
final modalRes = await showAccountSelectorBottomSheet(
context,
AccountSelector(
allowMultiSelection: true,
filterSavingAccounts: false,
selectedAccounts: accounts,
selectedAccounts: accounts ?? [],
));

if (modalRes != null) {
Expand All @@ -278,7 +274,7 @@ class _BudgetFormPageState extends State<BudgetFormPage> {
selectedCategories: categories,
onChange: (selection) {
setState(() {
categories = selection ?? [];
categories = selection;
});
},
);
Expand Down
5 changes: 4 additions & 1 deletion lib/app/categories/categories_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ class _CategoriesListState extends State<CategoriesList> {
child: CategorySelector(
availableCategories: categoriesToDisplay,
selectedCategories: selectedCategories,
multiSelection: false,
direction: Axis.vertical,
onChange: (selectedItems) async {
final category = selectedItems?.elementAt(0);
final category = selectedItems?.elementAtOrNull(0);

if (category == null) {
return;
Expand Down Expand Up @@ -135,6 +136,8 @@ class _CategoriesListState extends State<CategoriesList> {
}

Navigator.of(context).pop([modalRes]);
} else {
selectedCategories = [...widget.selectedCategories];
}
}

Expand Down
81 changes: 50 additions & 31 deletions lib/app/categories/category_selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import 'dart:ui';

import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:monekin/core/models/category/category.dart';
import 'package:monekin/core/models/supported-icon/icon_displayer.dart';
import 'package:monekin/core/presentation/app_colors.dart';
import 'package:monekin/core/presentation/widgets/wrap/wrap_extended.dart';
import 'package:monekin/core/utils/color_utils.dart';
import 'package:monekin/i18n/translations.g.dart';

Expand All @@ -20,7 +20,7 @@ class CategorySelector extends StatefulWidget {
required this.availableCategories,
this.extraHeaderButtons,
this.multiSelection = true,
this.iconSize = 38,
this.iconSize = 48,
this.iconPadding = 8,
});

Expand Down Expand Up @@ -58,20 +58,25 @@ class _CategorySelectorState extends State<CategorySelector> {
for (final (index, categoryToDisplay)
in widget.availableCategories!.indexed) ...[
Builder(builder: (context) {
final isCategorySelected = selectedCategories != null &&
final isCategorySelected = selectedCategories == null ||
selectedCategories!.any((cat) => cat.id == categoryToDisplay.id);

return CategoryButtonSelector(
maxTextSize: widget.iconSize * 1.2,
iconWidget: IconDisplayer.fromCategory(
context,
category: categoryToDisplay,
size: widget.iconSize,
padding: widget.iconPadding,
isOutline: isCategorySelected,
onTap: () {
HapticFeedback.lightImpact();

if (!widget.multiSelection) {
selectedCategories = [categoryToDisplay];

print(selectedCategories);

setState(() {});

if (widget.onChange != null) {
Expand All @@ -85,15 +90,17 @@ class _CategorySelectorState extends State<CategorySelector> {
selectedCategories = [categoryToDisplay];
} else {
selectedCategories!.add(categoryToDisplay);

if (selectedCategories!.length ==
widget.availableCategories!.length) {
selectedCategories = null;
}
}
} else {
selectedCategories ??= [...widget.availableCategories!];

selectedCategories!.removeWhere(
(element) => element.id == categoryToDisplay.id);

if (selectedCategories != null &&
selectedCategories!.isEmpty) {
selectedCategories = null;
}
}

setState(() {});
Expand All @@ -112,13 +119,14 @@ class _CategorySelectorState extends State<CategorySelector> {
];
}

buildSelectAllButton(
Widget buildSelectAllButton(
BuildContext context, {
required List<Category>? selectedCategories,
}) {
final t = Translations.of(context);

return CategoryButtonSelector(
maxTextSize: widget.iconSize * 1.2,
iconWidget: IconDisplayer(
icon: Icons.select_all,
size: widget.iconSize,
Expand All @@ -129,9 +137,17 @@ class _CategorySelectorState extends State<CategorySelector> {
),
mainColor: AppColors.of(context).onBackground,
onTap: () {
setState(() {
print(selectedCategories);

if (selectedCategories == null) {
selectedCategories = [];
} else {
selectedCategories = null;
});
}

setState(() {});

HapticFeedback.lightImpact();

if (widget.onChange != null) {
widget.onChange!(selectedCategories);
Expand All @@ -157,29 +173,27 @@ class _CategorySelectorState extends State<CategorySelector> {
)
.toList();

if (selectedCategories != null && selectedCategories.isEmpty) {
selectedCategories = null;
}

return Builder(builder: (context) {
if (widget.availableCategories == null) {
return Container();
}

if (widget.direction == Axis.vertical) {
return WrapSuper(
wrapType: WrapType.balanced,
wrapFit: WrapFit.min,
lineSpacing: 12,
spacing: 24,
alignment: WrapSuperAlignment.center,
children: [
if (extraHeaderButtonsWithSameSize != null)
...extraHeaderButtonsWithSameSize,
...buildCategoriesOptions(
selectedCategories: selectedCategories,
)
],
return Align(
alignment: Alignment.center,
heightFactor: 1,
child: Wrap(
runAlignment: WrapAlignment.center,
runSpacing: 12,
spacing: 24,
children: [
if (extraHeaderButtonsWithSameSize != null)
...extraHeaderButtonsWithSameSize,
...buildCategoriesOptions(
selectedCategories: selectedCategories,
)
],
),
);
}

Expand Down Expand Up @@ -229,25 +243,30 @@ class _CategorySelectorState extends State<CategorySelector> {
@CopyWith()
class CategoryButtonSelector extends StatelessWidget {
const CategoryButtonSelector(
{super.key, required this.iconWidget, required this.label});
{super.key,
required this.iconWidget,
required this.label,
required this.maxTextSize});

final IconDisplayer iconWidget;
final String label;

final double maxTextSize;

@override
Widget build(BuildContext context) {
return Column(
children: [
iconWidget,
const SizedBox(height: 4),
SizedBox(
width: 48,
width: maxTextSize,
child: Text(
label,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall,
style: Theme.of(context).textTheme.labelMedium,
),
),
],
Expand Down
Loading

0 comments on commit 4eb95bb

Please sign in to comment.