diff --git a/assets/icons/ic_add_transaction_expense.svg b/assets/icons/ic_add_transaction_expense.svg
new file mode 100644
index 0000000..5ebb2cc
--- /dev/null
+++ b/assets/icons/ic_add_transaction_expense.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/ic_add_transaction_income.svg b/assets/icons/ic_add_transaction_income.svg
new file mode 100644
index 0000000..b4f61a2
--- /dev/null
+++ b/assets/icons/ic_add_transaction_income.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/core/l10n/app_ar.arb b/lib/core/l10n/app_ar.arb
index e337729..a0ff716 100644
--- a/lib/core/l10n/app_ar.arb
+++ b/lib/core/l10n/app_ar.arb
@@ -137,5 +137,11 @@
"add" : "اضافة",
"transaction_details": "تفاصيل المعاملة",
"income_details": "تفاصيل الدخل",
- "expense_details": "تفاصيل المصروف"
+ "expense_details": "تفاصيل المصروف",
+ "add_transaction_sheet_subtitle": "اختر نوع المعاملة التي تريد إضافتها",
+ "make_expense": "تسجيل مصروف",
+ "continueButton": "متابعة",
+ "categoriesFilterTitle": "تصفية الفئات",
+ "categoriesFilterClear": "مسح",
+ "categoriesFilterApply": "تطبيق"
}
diff --git a/lib/core/l10n/app_en.arb b/lib/core/l10n/app_en.arb
index 32ca868..5658786 100644
--- a/lib/core/l10n/app_en.arb
+++ b/lib/core/l10n/app_en.arb
@@ -154,5 +154,11 @@
"expense_details": "Expense details",
"whereDoYouUsuallySpendYourMoney": "Where do you usually spend your money?",
"suggestions": "Suggestions:",
- "selectedCategories": "Selected Categories:"
+ "selectedCategories": "Selected Categories:",
+ "add_transaction_sheet_subtitle": "Select transaction type you want to add it",
+ "make_expense": "Make expense",
+ "continueButton": "Continue",
+ "categoriesFilterTitle": "Categories filter",
+ "categoriesFilterClear": "Clear",
+ "categoriesFilterApply": "Apply"
}
diff --git a/lib/design_system/assets/app_assets.dart b/lib/design_system/assets/app_assets.dart
index 40468a6..a264b63 100644
--- a/lib/design_system/assets/app_assets.dart
+++ b/lib/design_system/assets/app_assets.dart
@@ -69,4 +69,6 @@ class AppAssets {
static const String icEmptyTransactionImage = '$_images/empty_transaction_image.png';
static const String icEmptyTransactionPattern = '$_icons/empty_transaction_pattern.svg';
static const String icFilter = "$_icons/ic_filter.svg";
+ static const String icAddAmount = "$_icons/ic_add_transaction_income.svg";
+ static const String icAddExpense = "$_icons/ic_add_transaction_expense.svg";
}
diff --git a/lib/design_system/constants/design_constants.dart b/lib/design_system/constants/design_constants.dart
index 9780691..9f0321b 100644
--- a/lib/design_system/constants/design_constants.dart
+++ b/lib/design_system/constants/design_constants.dart
@@ -19,6 +19,7 @@ class DesignConstants {
// Icon Sizes
static const double iconSizeSmall = 14.0;
static const double iconSizeMedium = 16.0;
+ static const double iconSizeMed = 24.0;
static const double iconSizeLarge = 28.0;
// Component Sizes
diff --git a/lib/design_system/widgets/bottom_sheet.dart b/lib/design_system/widgets/bottom_sheet.dart
index 66b3d1b..ea655e4 100644
--- a/lib/design_system/widgets/bottom_sheet.dart
+++ b/lib/design_system/widgets/bottom_sheet.dart
@@ -1,9 +1,13 @@
import 'dart:ui';
import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/assets/app_assets.dart';
import 'package:moneyplus/design_system/theme/money_colors.dart';
+import 'package:svg_flutter/svg.dart';
import 'package:moneyplus/design_system/theme/money_typography.dart';
+import '../theme/money_extension_context.dart';
+
class MBottomSheet extends StatelessWidget {
final String title;
final Widget content;
@@ -19,9 +23,9 @@ class MBottomSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.only(
+ decoration: BoxDecoration(
+ color: context.colors.surface,
+ borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
@@ -42,20 +46,7 @@ class MBottomSheet extends StatelessWidget {
),
GestureDetector(
onTap: () => Navigator.pop(context),
- child: Container(
- width: 28,
- height: 28,
- decoration: BoxDecoration(
- color: const Color(0xFFE5E7EB),
- borderRadius: BorderRadius.circular(14),
- border: Border.all(color: MoneyColors.light.body, width: 1),
- ),
- child: Icon(
- Icons.close,
- size: 16,
- color: MoneyColors.light.body,
- ),
- ),
+ child: SvgPicture.asset(AppAssets.iconCancel, width: 20, height: 20),
),
],
),
diff --git a/lib/presentation/transactions/cubit/transaction_cubit.dart b/lib/presentation/transactions/cubit/transaction_cubit.dart
index 5af5b07..6a251a4 100644
--- a/lib/presentation/transactions/cubit/transaction_cubit.dart
+++ b/lib/presentation/transactions/cubit/transaction_cubit.dart
@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter/foundation.dart';
import 'package:moneyplus/domain/entity/transaction.dart';
import 'package:moneyplus/domain/entity/transaction_type.dart';
import 'package:moneyplus/domain/repository/transaction_repository.dart';
@@ -10,6 +11,17 @@ class TransactionCubit extends Cubit {
TransactionCubit({required this.transactionRepository})
: super(TransactionState.initial());
+ static const List _mockCategories = [
+ 'Food',
+ 'Transport',
+ 'Shopping',
+ 'Bills',
+ 'Health',
+ 'Entertainment',
+ 'Salary',
+ 'Gifts',
+ ];
+
void loadData() async {
emit(state.copyWith(status: TransactionStatus.loading));
@@ -18,7 +30,13 @@ class TransactionCubit extends Cubit {
emit(
state.copyWith(
allTransactions: result,
- filteredTransactions: _filterTransaction(now.month, now.year, result),
+ availableCategories: _mockCategories,
+ filteredTransactions: _filterTransaction(
+ month: now.month,
+ year: now.year,
+ transactions: result,
+ selectedCategories: state.selectedCategories,
+ ),
selectedYear: now.year,
selectedMonth: now.month,
status: TransactionStatus.success,
@@ -38,9 +56,10 @@ class TransactionCubit extends Cubit {
status: TransactionStatus.success,
allTransactions: result,
filteredTransactions: _filterTransaction(
- state.selectedMonth,
- state.selectedYear,
- result,
+ month: state.selectedMonth,
+ year: state.selectedYear,
+ transactions: result,
+ selectedCategories: state.selectedCategories,
),
),
);
@@ -61,9 +80,26 @@ class TransactionCubit extends Cubit {
state.copyWith(
status: TransactionStatus.success,
filteredTransactions: _filterTransaction(
- month,
- year,
- state.allTransactions,
+ month: month,
+ year: year,
+ transactions: state.allTransactions,
+ selectedCategories: state.selectedCategories,
+ ),
+ ),
+ );
+ }
+
+ void setSelectedCategories(Set categories) {
+ if (setEquals(state.selectedCategories, categories)) return;
+
+ emit(
+ state.copyWith(
+ selectedCategories: Set.from(categories),
+ filteredTransactions: _filterTransaction(
+ month: state.selectedMonth,
+ year: state.selectedYear,
+ transactions: state.allTransactions,
+ selectedCategories: categories,
),
),
);
@@ -81,12 +117,20 @@ class TransactionCubit extends Cubit {
}
List _filterTransaction(
- int month,
- int year,
- List transactions,
+ {
+ required int month,
+ required int year,
+ required List transactions,
+ required Set selectedCategories,
+ }
) {
return transactions.where((transaction) {
- return transaction.date.year == year && transaction.date.month == month;
+ final matchesDate =
+ transaction.date.year == year && transaction.date.month == month;
+ if (!matchesDate) return false;
+
+ if (selectedCategories.isEmpty) return true;
+ return selectedCategories.contains(transaction.category.name);
}).toList();
}
}
diff --git a/lib/presentation/transactions/cubit/transaction_state.dart b/lib/presentation/transactions/cubit/transaction_state.dart
index 5ffd7a2..d3e7efc 100644
--- a/lib/presentation/transactions/cubit/transaction_state.dart
+++ b/lib/presentation/transactions/cubit/transaction_state.dart
@@ -15,6 +15,8 @@ class TransactionState {
final TransactionTabs selectedTab;
final int selectedMonth;
final int selectedYear;
+ final List availableCategories;
+ final Set selectedCategories;
const TransactionState({
required this.status,
@@ -24,6 +26,8 @@ class TransactionState {
this.selectedTab = TransactionTabs.all,
this.selectedYear = 2026,
this.selectedMonth = 1,
+ this.availableCategories = const [],
+ this.selectedCategories = const {},
});
factory TransactionState.initial() =>
@@ -37,6 +41,8 @@ class TransactionState {
TransactionTabs? selectedTab,
int? selectedYear,
int? selectedMonth,
+ List? availableCategories,
+ Set? selectedCategories,
}) {
return TransactionState(
status: status ?? this.status,
@@ -46,6 +52,8 @@ class TransactionState {
selectedTab: selectedTab ?? this.selectedTab,
selectedYear: selectedYear ?? this.selectedYear,
selectedMonth: selectedMonth ?? this.selectedMonth,
+ availableCategories: availableCategories ?? this.availableCategories,
+ selectedCategories: selectedCategories ?? this.selectedCategories,
);
}
}
diff --git a/lib/presentation/transactions/screen/transactions_screen.dart b/lib/presentation/transactions/screen/transactions_screen.dart
index b4e9733..9ed86b3 100644
--- a/lib/presentation/transactions/screen/transactions_screen.dart
+++ b/lib/presentation/transactions/screen/transactions_screen.dart
@@ -6,6 +6,7 @@ import 'package:moneyplus/design_system/widgets/snack_bar.dart';
import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart';
import 'package:moneyplus/presentation/transactions/cubit/transaction_state.dart';
import 'package:moneyplus/presentation/transactions/widget/empty_transactions.dart';
+import 'package:moneyplus/presentation/transactions/widget/categories_filter_bottom_sheet.dart';
import 'package:moneyplus/presentation/transactions/widget/loading_view.dart';
import 'package:moneyplus/presentation/transactions/widget/tabs_row.dart';
import 'package:moneyplus/presentation/transactions/widget/transaction_app_bar.dart';
@@ -40,7 +41,17 @@ class TransactionsScreen extends StatelessWidget {
year: state.selectedYear,
month: state.selectedMonth,
onDatePick: context.read().setSelectedDate,
- onFilterClicked: (){},
+ onFilterClicked: () async {
+ final result = await showCategoriesFilterBottomSheet(
+ context: context,
+ categories: state.availableCategories,
+ initialSelectedCategories: state.selectedCategories,
+ );
+ if (result == null) return;
+ context
+ .read()
+ .setSelectedCategories(result.toSet());
+ },
),
),
SliverPadding(
diff --git a/lib/presentation/transactions/widget/add_transaction_bottom_sheet.dart b/lib/presentation/transactions/widget/add_transaction_bottom_sheet.dart
new file mode 100644
index 0000000..e54a0d0
--- /dev/null
+++ b/lib/presentation/transactions/widget/add_transaction_bottom_sheet.dart
@@ -0,0 +1,102 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/core/l10n/app_localizations.dart';
+import 'package:moneyplus/design_system/assets/app_assets.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+import 'package:moneyplus/design_system/widgets/bottom_sheet.dart';
+import 'package:moneyplus/design_system/widgets/buttons/button/default_button.dart';
+import 'package:moneyplus/domain/entity/transaction_type.dart';
+import 'package:moneyplus/presentation/navigation/routes.dart';
+import 'package:moneyplus/presentation/transactions/widget/transaction_type_card.dart';
+import 'package:moneyplus/utils/extenstions/show_bottom_sheet.dart';
+
+class AddTransactionBottomSheet extends StatefulWidget {
+ final BuildContext parentContext;
+
+ const AddTransactionBottomSheet({
+ super.key,
+ required this.parentContext,
+ });
+
+ @override
+ State createState() =>
+ _AddTransactionBottomSheetState();
+}
+
+class _AddTransactionBottomSheetState extends State {
+ TransactionType? _selectedType;
+
+ void _selectType(TransactionType type) {
+ setState(() {
+ _selectedType = type;
+ });
+ }
+
+ void _onContinue() {
+ final selected = _selectedType;
+ if (selected == null) return;
+
+ Navigator.of(context).pop();
+ if (selected == TransactionType.income) {
+ AddIncomeRoute().push(widget.parentContext);
+ } else {
+ AddExpenseRoute().push(widget.parentContext);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final typography = context.typography;
+ final colors = context.colors;
+ final l10n = AppLocalizations.of(context)!;
+
+ return MBottomSheet(
+ title: l10n.add_transaction,
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.add_transaction_sheet_subtitle,
+ style: typography.body.small.copyWith(color: colors.body),
+ ),
+ const SizedBox(height: DesignConstants.spacingLarge),
+ Row(
+ children: [
+ Expanded(
+ child: TransactionTypeCard(
+ label: l10n.addIncome,
+ iconPath: AppAssets.icAddAmount,
+ selected: _selectedType == TransactionType.income,
+ onTap: () => _selectType(TransactionType.income),
+ ),
+ ),
+ const SizedBox(width: DesignConstants.spacingSmall),
+ Expanded(
+ child: TransactionTypeCard(
+ label: l10n.make_expense,
+ iconPath: AppAssets.icAddExpense,
+ selected: _selectedType == TransactionType.expense,
+ onTap: () => _selectType(TransactionType.expense),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ actionButtons: [
+ DefaultButton(
+ text: l10n.continueButton,
+ isEnabled: _selectedType != null,
+ onPressed: _selectedType != null ? _onContinue : null,
+ ),
+ ],
+ );
+ }
+}
+
+void showAddTransactionBottomSheet(BuildContext context) {
+ context.showBlurBottomSheet(
+ AddTransactionBottomSheet(parentContext: context),
+ );
+}
+
diff --git a/lib/presentation/transactions/widget/categories_filter_bottom_sheet.dart b/lib/presentation/transactions/widget/categories_filter_bottom_sheet.dart
new file mode 100644
index 0000000..ca2c7cf
--- /dev/null
+++ b/lib/presentation/transactions/widget/categories_filter_bottom_sheet.dart
@@ -0,0 +1,108 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/widgets/bottom_sheet.dart';
+import 'package:moneyplus/design_system/widgets/buttons/button/default_button.dart';
+import 'package:moneyplus/design_system/widgets/buttons/secondary/defult_secondary_button.dart';
+import 'package:moneyplus/design_system/widgets/chip.dart';
+import 'package:moneyplus/utils/extenstions/show_bottom_sheet.dart';
+
+import '../../../core/l10n/app_localizations.dart';
+
+Future?> showCategoriesFilterBottomSheet({
+ required BuildContext context,
+ required List categories,
+ Set? initialSelectedCategories,
+}) {
+ return context.showBlurBottomSheet>(
+ CategoriesFilterBottomSheet(
+ categories: categories,
+ initialSelectedCategories: initialSelectedCategories,
+ ),
+ );
+}
+
+class CategoriesFilterBottomSheet extends StatefulWidget {
+ final List categories;
+ final Set? initialSelectedCategories;
+
+ const CategoriesFilterBottomSheet({
+ super.key,
+ required this.categories,
+ this.initialSelectedCategories,
+ });
+
+ @override
+ State createState() =>
+ _CategoriesFilterBottomSheetState();
+}
+
+class _CategoriesFilterBottomSheetState extends State {
+ late Set _selected;
+
+ @override
+ void initState() {
+ super.initState();
+ _selected = Set.from(widget.initialSelectedCategories ?? const {});
+ }
+
+ void _toggle(String category) {
+ setState(() {
+ if (_selected.contains(category)) {
+ _selected.remove(category);
+ } else {
+ _selected.add(category);
+ }
+ });
+ }
+
+ void _clear() {
+ setState(() {
+ _selected.clear();
+ });
+ }
+
+ void _apply() {
+ final selectedInOrder = widget.categories
+ .where(_selected.contains)
+ .toList(growable: false);
+ Navigator.of(context).pop(selectedInOrder);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
+
+ return MBottomSheet(
+ title: l10n.categoriesFilterTitle,
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Wrap(
+ spacing: DesignConstants.spacingSmall,
+ runSpacing: DesignConstants.spacingSmall,
+ children: [
+ for (final category in widget.categories)
+ MChip(
+ label: category,
+ selected: _selected.contains(category),
+ onTap: () => _toggle(category),
+ ),
+ ],
+ ),
+ ],
+ ),
+ actionButtons: [
+ DefaultSecondaryButton(
+ text: l10n.categoriesFilterClear,
+ isEnabled: _selected.isNotEmpty,
+ onPressed: _selected.isNotEmpty ? _clear : null,
+ ),
+ DefaultButton(
+ text: l10n.categoriesFilterApply,
+ onPressed: _apply,
+ ),
+ ],
+ );
+ }
+}
+
diff --git a/lib/presentation/transactions/widget/empty_transactions.dart b/lib/presentation/transactions/widget/empty_transactions.dart
index c6032ae..7fbea25 100644
--- a/lib/presentation/transactions/widget/empty_transactions.dart
+++ b/lib/presentation/transactions/widget/empty_transactions.dart
@@ -4,6 +4,7 @@ import 'package:moneyplus/design_system/assets/app_assets.dart';
import 'package:moneyplus/design_system/theme/money_extension_context.dart';
import 'package:moneyplus/design_system/widgets/buttons/button/default_button.dart';
import 'package:svg_flutter/svg.dart';
+import 'package:moneyplus/presentation/transactions/widget/add_transaction_bottom_sheet.dart';
class EmptyTransactions extends StatelessWidget {
const EmptyTransactions({super.key});
@@ -46,7 +47,10 @@ class EmptyTransactions extends StatelessWidget {
),
SizedBox(height: 24),
IntrinsicWidth(
- child: DefaultButton(text: localizations.add_transaction),
+ child: DefaultButton(
+ text: localizations.add_transaction,
+ onPressed: () => showAddTransactionBottomSheet(context),
+ ),
),
],
);
diff --git a/lib/presentation/transactions/widget/transaction_app_bar.dart b/lib/presentation/transactions/widget/transaction_app_bar.dart
index ad94b65..169adf9 100644
--- a/lib/presentation/transactions/widget/transaction_app_bar.dart
+++ b/lib/presentation/transactions/widget/transaction_app_bar.dart
@@ -9,7 +9,7 @@ import '../../../design_system/assets/app_assets.dart';
class TransactionAppBar extends StatelessWidget {
final Function(int month, int year) onDatePick;
- final Function onFilterClicked;
+ final VoidCallback onFilterClicked;
final int year;
final int month;
@@ -37,7 +37,7 @@ class TransactionAppBar extends StatelessWidget {
}, year: year, month: month),
SizedBox(width: 8,),
GestureDetector(
- onTap: () {},
+ onTap: onFilterClicked,
child: Container(
height: 40,
width: 40,
diff --git a/lib/presentation/transactions/widget/transaction_type_card.dart b/lib/presentation/transactions/widget/transaction_type_card.dart
new file mode 100644
index 0000000..645cf35
--- /dev/null
+++ b/lib/presentation/transactions/widget/transaction_type_card.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+import 'package:svg_flutter/svg.dart';
+
+class TransactionTypeCard extends StatelessWidget {
+ final String label;
+ final String iconPath;
+ final bool selected;
+ final VoidCallback onTap;
+
+ const TransactionTypeCard({
+ super.key,
+ required this.label,
+ required this.iconPath,
+ required this.selected,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = context.colors;
+ final typography = context.typography;
+
+ final Color borderColor = selected ? colors.primary : Colors.transparent;
+ final Color backgroundColor = selected
+ ? colors.primary.withValues(alpha: 0.08)
+ : colors.surfaceLow;
+ final Color contentColor = selected ? colors.primary : colors.body;
+
+ return GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ curve: Curves.easeInOut,
+ padding: const EdgeInsets.symmetric(
+ vertical: DesignConstants.spacingMedium,
+ horizontal: DesignConstants.spacingLarge,
+ ),
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ borderRadius: BorderRadius.circular(DesignConstants.radiusMedium),
+ border: Border.all(color: borderColor, width: 1),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SvgPicture.asset(
+ iconPath,
+ width: DesignConstants.iconSizeMed,
+ height: DesignConstants.iconSizeMed,
+ colorFilter: ColorFilter.mode(contentColor, BlendMode.srcIn),
+ ),
+ const SizedBox(height: DesignConstants.spacingSmall),
+ Text(
+ label,
+ style: typography.label.medium.copyWith(
+ color: contentColor,
+ fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
+ ),
+ textAlign: TextAlign.start,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}