From 91bdb67bad01f501bb28430d28ee09d26b3acc91 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 13:06:49 +0200 Subject: [PATCH 01/13] feat: add implementation for get transactions --- .../transaction_repository_stub.dart | 35 +++++++++++++++++-- .../repository/transaction_repository.dart | 4 ++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/data/repository/transaction_repository_stub.dart b/lib/data/repository/transaction_repository_stub.dart index 4c13ea7..13c5398 100644 --- a/lib/data/repository/transaction_repository_stub.dart +++ b/lib/data/repository/transaction_repository_stub.dart @@ -1,13 +1,11 @@ import 'package:moneyplus/core/errors/error_model.dart'; import 'package:moneyplus/core/errors/result.dart'; -import 'package:moneyplus/data/repository/utils/fake_data.dart'; import 'package:moneyplus/data/service/supabase_service.dart'; import 'package:moneyplus/domain/entity/transaction.dart'; import 'package:moneyplus/domain/entity/transaction_category.dart'; import 'package:moneyplus/domain/entity/transaction_type.dart'; import 'package:moneyplus/domain/repository/model/top_spending_category.dart'; import 'package:moneyplus/domain/repository/transaction_repository.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; class TransactionRepositoryStub implements TransactionRepository { final SupabaseService service; @@ -56,8 +54,38 @@ class TransactionRepositoryStub implements TransactionRepository { TransactionType? type, TransactionCategory? category, DateTime? date, + List? categoriesId, + required int page, }) async { - throw UnimplementedError('getTransactions not implemented'); + final client = await service.getClient(); + final response = await client.rpc( + RpcString.getTransactions, + params: { + 'p_timestamp': (date ?? DateTime.now()).toIso8601String(), + 'p_category_ids': categoriesId, + 'p_transaction_type_id': type == TransactionType.income + ? 1 + : type == TransactionType.expense + ? 2 + : null, + 'p_page': page, + }, + ); + return (response as List) + .map( + (e) => Transaction( + id: e['id'] ?? 0, + amount: (e['amount'] as num).toDouble(), + currency: e['currency'] ?? '', + type: e['transaction_type'] == 'income' + ? TransactionType.income + : TransactionType.expense, + date: DateTime.parse(e['date']), + category: TransactionCategory(id: 1, name: e['category']), + note: e['note'] ?? '', + ), + ) + .toList(); } @override @@ -241,4 +269,5 @@ class TransactionRepositoryStub implements TransactionRepository { class RpcString { static String deleteTransaction = 'delete_transaction'; static String getTransactionDetails = 'get_transaction_details'; + static String getTransactions = 'get_transactions'; } diff --git a/lib/domain/repository/transaction_repository.dart b/lib/domain/repository/transaction_repository.dart index c5d0d46..cf8b773 100644 --- a/lib/domain/repository/transaction_repository.dart +++ b/lib/domain/repository/transaction_repository.dart @@ -29,9 +29,11 @@ abstract class TransactionRepository { TransactionType? type, TransactionCategory? category, DateTime? date, + List categoriesId = const[], + required int page, }); - Future> getTransactionDetails(String id); + Future> getTransactionDetails(String id); Future getTotalAmount({TransactionType? type}); From 20b6b3d692010f4844e57447c09417f3dd462a82 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 13:17:25 +0200 Subject: [PATCH 02/13] feat: use get transaction in transaction_cubit --- .../transactions/cubit/transaction_cubit.dart | 60 +++++++------------ .../transactions/cubit/transaction_state.dart | 12 ++-- .../screen/transactions_screen.dart | 4 +- 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/lib/presentation/transactions/cubit/transaction_cubit.dart b/lib/presentation/transactions/cubit/transaction_cubit.dart index 5af5b07..f983ea3 100644 --- a/lib/presentation/transactions/cubit/transaction_cubit.dart +++ b/lib/presentation/transactions/cubit/transaction_cubit.dart @@ -1,5 +1,4 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:moneyplus/domain/entity/transaction.dart'; import 'package:moneyplus/domain/entity/transaction_type.dart'; import 'package:moneyplus/domain/repository/transaction_repository.dart'; import 'package:moneyplus/presentation/transactions/cubit/transaction_state.dart'; @@ -13,12 +12,11 @@ class TransactionCubit extends Cubit { void loadData() async { emit(state.copyWith(status: TransactionStatus.loading)); - final result = await transactionRepository.getAllTransactions(); final now = DateTime.now(); + final result = await transactionRepository.getTransactions(page: 1, date: now); emit( state.copyWith( - allTransactions: result, - filteredTransactions: _filterTransaction(now.month, now.year, result), + transactions: result, selectedYear: now.year, selectedMonth: now.month, status: TransactionStatus.success, @@ -31,17 +29,20 @@ class TransactionCubit extends Cubit { emit(state.copyWith(status: TransactionStatus.loading, selectedTab: tab)); - final result = await _getTransactionsByTab(tab); + final result = await transactionRepository.getTransactions( + page: 1, + type: tab == TransactionTabs.expenses + ? TransactionType.expense + : tab == TransactionTabs.incomes + ? TransactionType.income + : null, + date: DateTime(state.selectedYear, state.selectedMonth) + ); emit( state.copyWith( status: TransactionStatus.success, - allTransactions: result, - filteredTransactions: _filterTransaction( - state.selectedMonth, - state.selectedYear, - result, - ), + transactions: result, ), ); } @@ -57,36 +58,21 @@ class TransactionCubit extends Cubit { ), ); + final result = await transactionRepository.getTransactions( + page: 1, + type: state.selectedTab == TransactionTabs.expenses + ? TransactionType.expense + : state.selectedTab == TransactionTabs.incomes + ? TransactionType.income + : null, + date: DateTime(year, month) + ); + emit( state.copyWith( status: TransactionStatus.success, - filteredTransactions: _filterTransaction( - month, - year, - state.allTransactions, - ), + transactions: result, ), ); } - - Future> _getTransactionsByTab(TransactionTabs tab) async { - return await switch (tab) { - TransactionTabs.all => transactionRepository.getAllTransactions(), - TransactionTabs.incomes => transactionRepository.getAllTransactionsByType( - TransactionType.income, - ), - TransactionTabs.expenses => - transactionRepository.getAllTransactionsByType(TransactionType.expense), - }; - } - - List _filterTransaction( - int month, - int year, - List transactions, - ) { - return transactions.where((transaction) { - return transaction.date.year == year && transaction.date.month == month; - }).toList(); - } } diff --git a/lib/presentation/transactions/cubit/transaction_state.dart b/lib/presentation/transactions/cubit/transaction_state.dart index 5ffd7a2..d6db9f1 100644 --- a/lib/presentation/transactions/cubit/transaction_state.dart +++ b/lib/presentation/transactions/cubit/transaction_state.dart @@ -10,8 +10,7 @@ enum TransactionTabs { all, incomes, expenses } class TransactionState { final TransactionStatus status; final ErrorModel? error; - final List filteredTransactions; - final List allTransactions; + final List transactions; final TransactionTabs selectedTab; final int selectedMonth; final int selectedYear; @@ -19,8 +18,7 @@ class TransactionState { const TransactionState({ required this.status, this.error, - this.filteredTransactions = const [], - this.allTransactions = const [], + this.transactions = const [], this.selectedTab = TransactionTabs.all, this.selectedYear = 2026, this.selectedMonth = 1, @@ -32,8 +30,7 @@ class TransactionState { TransactionState copyWith({ TransactionStatus? status, ErrorModel? error, - List? filteredTransactions, - List? allTransactions, + List? transactions, TransactionTabs? selectedTab, int? selectedYear, int? selectedMonth, @@ -41,8 +38,7 @@ class TransactionState { return TransactionState( status: status ?? this.status, error: error, - filteredTransactions: filteredTransactions ?? this.filteredTransactions, - allTransactions: allTransactions ?? this.allTransactions, + transactions: transactions ?? this.transactions, selectedTab: selectedTab ?? this.selectedTab, selectedYear: selectedYear ?? this.selectedYear, selectedMonth: selectedMonth ?? this.selectedMonth, diff --git a/lib/presentation/transactions/screen/transactions_screen.dart b/lib/presentation/transactions/screen/transactions_screen.dart index b4e9733..bd1dff7 100644 --- a/lib/presentation/transactions/screen/transactions_screen.dart +++ b/lib/presentation/transactions/screen/transactions_screen.dart @@ -56,9 +56,9 @@ class TransactionsScreen extends StatelessWidget { ), state.status == TransactionStatus.loading ? SliverFillRemaining(child: LoadingView()) - : state.filteredTransactions.isEmpty + : state.transactions.isEmpty ? SliverFillRemaining(child: EmptyTransactions()) - : TransactionsList(transactions: state.filteredTransactions), + : TransactionsList(transactions: state.transactions), ], ); }, From ff023e7ee4244b8fb62fe194d0253e143735f64c Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 13:19:57 +0200 Subject: [PATCH 03/13] feat: remove unused getAllTransaction function in repository --- .../transaction_repository_stub.dart | 87 +------------------ .../repository/transaction_repository.dart | 4 - 2 files changed, 1 insertion(+), 90 deletions(-) diff --git a/lib/data/repository/transaction_repository_stub.dart b/lib/data/repository/transaction_repository_stub.dart index 13c5398..f818590 100644 --- a/lib/data/repository/transaction_repository_stub.dart +++ b/lib/data/repository/transaction_repository_stub.dart @@ -61,7 +61,7 @@ class TransactionRepositoryStub implements TransactionRepository { final response = await client.rpc( RpcString.getTransactions, params: { - 'p_timestamp': (date ?? DateTime.now()).toIso8601String(), + 'p_timestamp': date?.toIso8601String(), 'p_category_ids': categoriesId, 'p_transaction_type_id': type == TransactionType.income ? 1 @@ -179,91 +179,6 @@ class TransactionRepositoryStub implements TransactionRepository { }) async { throw UnimplementedError('editExpenseCategory not implemented'); } - - @override - Future> getAllTransactions() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - Transaction( - id: 1, - amount: 50000, - currency: "IQD", - type: TransactionType.expense, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - Transaction( - id: 4, - amount: 5040, - currency: "IQD", - type: TransactionType.income, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - Transaction( - id: 2, - amount: 230000, - currency: "IQD", - type: TransactionType.income, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - Transaction( - id: 3, - amount: 530000, - currency: "IQD", - type: TransactionType.expense, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - ]; - } - - @override - Future> getAllTransactionsByType( - TransactionType type, - ) async { - await Future.delayed(const Duration(milliseconds: 500)); - if (type == TransactionType.income) { - return [ - Transaction( - id: 4, - amount: 5040, - currency: "IQD", - type: TransactionType.income, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - Transaction( - id: 2, - amount: 230000, - currency: "IQD", - type: TransactionType.income, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - ]; - } else { - return [ - Transaction( - id: 1, - amount: 50000, - currency: "IQD", - type: TransactionType.expense, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - Transaction( - id: 3, - amount: 530000, - currency: "IQD", - type: TransactionType.expense, - date: DateTime(2024, 12, 2), - category: TransactionCategory(id: 1, name: "shopping"), - ), - ]; - } - } } class RpcString { diff --git a/lib/domain/repository/transaction_repository.dart b/lib/domain/repository/transaction_repository.dart index cf8b773..920ae48 100644 --- a/lib/domain/repository/transaction_repository.dart +++ b/lib/domain/repository/transaction_repository.dart @@ -46,8 +46,4 @@ abstract class TransactionRepository { Future addExpenseCategory(String name); Future editExpenseCategory({required int id, required String name}); - - Future> getAllTransactions(); - - Future> getAllTransactionsByType(TransactionType type,); } From 3cf36555b7441abe9696d50afca1d3bcb822d9ff Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 14:08:03 +0200 Subject: [PATCH 04/13] feat: add pagination behaviour in transaction screen --- .../transactions/cubit/transaction_cubit.dart | 52 +++++++++- .../transactions/cubit/transaction_state.dart | 12 +++ .../screen/transactions_screen.dart | 61 +----------- .../widget/transaction_screen_content.dart | 95 +++++++++++++++++++ 4 files changed, 160 insertions(+), 60 deletions(-) create mode 100644 lib/presentation/transactions/widget/transaction_screen_content.dart diff --git a/lib/presentation/transactions/cubit/transaction_cubit.dart b/lib/presentation/transactions/cubit/transaction_cubit.dart index f983ea3..91253ac 100644 --- a/lib/presentation/transactions/cubit/transaction_cubit.dart +++ b/lib/presentation/transactions/cubit/transaction_cubit.dart @@ -13,13 +13,17 @@ class TransactionCubit extends Cubit { emit(state.copyWith(status: TransactionStatus.loading)); final now = DateTime.now(); - final result = await transactionRepository.getTransactions(page: 1, date: now); + final result = await transactionRepository.getTransactions( + page: 1, + date: now, + ); emit( state.copyWith( transactions: result, selectedYear: now.year, selectedMonth: now.month, status: TransactionStatus.success, + hasMore: result.length == 20, ), ); } @@ -27,7 +31,14 @@ class TransactionCubit extends Cubit { void onTabSelected(TransactionTabs tab) async { if (state.selectedTab == tab) return; - emit(state.copyWith(status: TransactionStatus.loading, selectedTab: tab)); + emit( + state.copyWith( + status: TransactionStatus.loading, + selectedTab: tab, + currentPage: 1, + hasMore: true, + ), + ); final result = await transactionRepository.getTransactions( page: 1, @@ -36,13 +47,15 @@ class TransactionCubit extends Cubit { : tab == TransactionTabs.incomes ? TransactionType.income : null, - date: DateTime(state.selectedYear, state.selectedMonth) + date: DateTime(state.selectedYear, state.selectedMonth), ); emit( state.copyWith( status: TransactionStatus.success, transactions: result, + currentPage: 1, + hasMore: result.length == 20, ), ); } @@ -55,6 +68,8 @@ class TransactionCubit extends Cubit { selectedMonth: month, selectedYear: year, status: TransactionStatus.loading, + currentPage: 1, + hasMore: true, ), ); @@ -65,13 +80,42 @@ class TransactionCubit extends Cubit { : state.selectedTab == TransactionTabs.incomes ? TransactionType.income : null, - date: DateTime(year, month) + date: DateTime(year, month), ); emit( state.copyWith( status: TransactionStatus.success, transactions: result, + currentPage: 1, + hasMore: result.length == 20, + ), + ); + } + + void loadMore() async { + if (!state.hasMore || state.isLoadingMore) return; + + emit(state.copyWith(isLoadingMore: true)); + + final nextPage = state.currentPage + 1; + + final result = await transactionRepository.getTransactions( + page: nextPage, + type: state.selectedTab == TransactionTabs.expenses + ? TransactionType.expense + : state.selectedTab == TransactionTabs.incomes + ? TransactionType.income + : null, + date: DateTime(state.selectedYear, state.selectedMonth), + ); + + emit( + state.copyWith( + transactions: [...state.transactions, ...result], + currentPage: nextPage, + hasMore: result.length == 20, + isLoadingMore: false, ), ); } diff --git a/lib/presentation/transactions/cubit/transaction_state.dart b/lib/presentation/transactions/cubit/transaction_state.dart index d6db9f1..61f5498 100644 --- a/lib/presentation/transactions/cubit/transaction_state.dart +++ b/lib/presentation/transactions/cubit/transaction_state.dart @@ -14,6 +14,9 @@ class TransactionState { final TransactionTabs selectedTab; final int selectedMonth; final int selectedYear; + final int currentPage; + final bool hasMore; + final bool isLoadingMore; const TransactionState({ required this.status, @@ -22,6 +25,9 @@ class TransactionState { this.selectedTab = TransactionTabs.all, this.selectedYear = 2026, this.selectedMonth = 1, + this.currentPage = 1, + this.hasMore = true, + this.isLoadingMore = false }); factory TransactionState.initial() => @@ -34,6 +40,9 @@ class TransactionState { TransactionTabs? selectedTab, int? selectedYear, int? selectedMonth, + int? currentPage, + bool? hasMore, + bool? isLoadingMore }) { return TransactionState( status: status ?? this.status, @@ -42,6 +51,9 @@ class TransactionState { selectedTab: selectedTab ?? this.selectedTab, selectedYear: selectedYear ?? this.selectedYear, selectedMonth: selectedMonth ?? this.selectedMonth, + currentPage: currentPage ?? this.currentPage, + hasMore: hasMore ?? this.hasMore, + isLoadingMore: isLoadingMore ?? this.isLoadingMore ); } } diff --git a/lib/presentation/transactions/screen/transactions_screen.dart b/lib/presentation/transactions/screen/transactions_screen.dart index bd1dff7..70168c1 100644 --- a/lib/presentation/transactions/screen/transactions_screen.dart +++ b/lib/presentation/transactions/screen/transactions_screen.dart @@ -1,15 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:moneyplus/core/l10n/app_localizations.dart'; -import 'package:moneyplus/design_system/theme/money_extension_context.dart'; -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/loading_view.dart'; -import 'package:moneyplus/presentation/transactions/widget/tabs_row.dart'; -import 'package:moneyplus/presentation/transactions/widget/transaction_app_bar.dart'; -import 'package:moneyplus/presentation/transactions/widget/transactions_list.dart'; +import 'package:moneyplus/presentation/transactions/widget/transaction_screen_content.dart'; + import '../../../core/di/injection.dart'; class TransactionsScreen extends StatelessWidget { @@ -17,53 +10,9 @@ class TransactionsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final colors = context.colors; - final localizations = AppLocalizations.of(context)!; - return Scaffold( - backgroundColor: colors.surface, - body: BlocProvider( - create: (_) => getIt()..loadData(), - child: BlocConsumer( - listener: (context, state) { - if (state.status == TransactionStatus.failure) { - MSnackBar.error( - message: localizations.transaction_error_content, - title: localizations.transaction_error_title, - ).showSnackBar(context: context); - } - }, - builder: (context, state) { - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: TransactionAppBar( - year: state.selectedYear, - month: state.selectedMonth, - onDatePick: context.read().setSelectedDate, - onFilterClicked: (){}, - ), - ), - SliverPadding( - padding: const EdgeInsets.only(bottom: 16, left: 16, top: 16), - sliver: SliverToBoxAdapter( - child: TabsRow( - selectedTab: state.selectedTab, - onTabSelected: context - .read() - .onTabSelected, - ), - ), - ), - state.status == TransactionStatus.loading - ? SliverFillRemaining(child: LoadingView()) - : state.transactions.isEmpty - ? SliverFillRemaining(child: EmptyTransactions()) - : TransactionsList(transactions: state.transactions), - ], - ); - }, - ), - ), + return BlocProvider( + create: (_) => getIt()..loadData(), + child: TransactionScreenContent(), ); } } diff --git a/lib/presentation/transactions/widget/transaction_screen_content.dart b/lib/presentation/transactions/widget/transaction_screen_content.dart new file mode 100644 index 0000000..5dda772 --- /dev/null +++ b/lib/presentation/transactions/widget/transaction_screen_content.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:moneyplus/core/l10n/app_localizations.dart'; +import 'package:moneyplus/design_system/theme/money_extension_context.dart'; +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/loading_view.dart'; +import 'package:moneyplus/presentation/transactions/widget/tabs_row.dart'; +import 'package:moneyplus/presentation/transactions/widget/transaction_app_bar.dart'; +import 'package:moneyplus/presentation/transactions/widget/transactions_list.dart'; + +class TransactionScreenContent extends StatefulWidget { + const TransactionScreenContent({super.key}); + + @override + State createState() => + _TransactionsScreenContentState(); +} + +class _TransactionsScreenContentState extends State { + final ScrollController _controller = ScrollController(); + + @override + void initState() { + super.initState(); + + _controller.addListener(() { + if (_controller.position.pixels >= + _controller.position.maxScrollExtent - 200) { + context.read().loadMore(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final localizations = AppLocalizations.of(context)!; + return Scaffold( + backgroundColor: colors.surface, + body: BlocConsumer( + listener: (context, state) { + if (state.status == TransactionStatus.failure) { + MSnackBar.error( + message: localizations.transaction_error_content, + title: localizations.transaction_error_title, + ).showSnackBar(context: context); + } + }, + builder: (context, state) { + return CustomScrollView( + controller: _controller, + slivers: [ + SliverToBoxAdapter( + child: TransactionAppBar( + year: state.selectedYear, + month: state.selectedMonth, + onDatePick: context.read().setSelectedDate, + onFilterClicked: () {}, + ), + ), + SliverPadding( + padding: const EdgeInsets.only(bottom: 16, left: 16, top: 16), + sliver: SliverToBoxAdapter( + child: TabsRow( + selectedTab: state.selectedTab, + onTabSelected: context + .read() + .onTabSelected, + ), + ), + ), + state.status == TransactionStatus.loading + ? SliverFillRemaining(child: LoadingView()) + : state.transactions.isEmpty + ? SliverFillRemaining(child: EmptyTransactions()) + : TransactionsList(transactions: state.transactions), + + if (state.isLoadingMore) + const SliverToBoxAdapter(child: LoadingView()), + ], + ); + }, + ), + ); + } +} From d253a23eca54865ab4c9fee42c7c0b2e8d4da6f3 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 20:51:18 +0200 Subject: [PATCH 05/13] feat: add safe area --- .../transactions/screen/transactions_screen.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/presentation/transactions/screen/transactions_screen.dart b/lib/presentation/transactions/screen/transactions_screen.dart index 70168c1..ec9abc0 100644 --- a/lib/presentation/transactions/screen/transactions_screen.dart +++ b/lib/presentation/transactions/screen/transactions_screen.dart @@ -10,9 +10,11 @@ class TransactionsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => getIt()..loadData(), - child: TransactionScreenContent(), + return SafeArea( + child: BlocProvider( + create: (_) => getIt()..loadData(), + child: TransactionScreenContent(), + ), ); } } From 4bd67c3e5dfbac40423730baf30235b4353963f6 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 20:51:43 +0200 Subject: [PATCH 06/13] refactor: remove duplicate di --- lib/core/di/injection.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index c076e5d..6f2dcd6 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -4,7 +4,6 @@ import 'package:moneyplus/presentation/account_setup/cubit/account_setup_cubit.d import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart'; import '../../data/repository/account_repository.dart'; import '../../data/repository/authentication_repository.dart'; -import '../../data/repository/fake_statistics_repository.dart'; import '../../data/repository/transaction_repository.dart'; import '../../data/repository/statistics_repository_impl.dart'; import '../../data/repository/user_money_repository.dart'; @@ -62,10 +61,6 @@ void initDI() { () => TransactionRepositoryImpl(service: getIt()), ); - getIt.registerLazySingleton( - () => AccountSetupCubit(getIt()), - ); - getIt.registerLazySingleton( () => AccountSetupCubit(getIt()), ); From b2e27438b8d26e0371d102846efc77136b33d49b Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Fri, 20 Feb 2026 20:58:40 +0200 Subject: [PATCH 07/13] feat: navigate to add income from empty transactions screen --- lib/presentation/navigation/routes.g.dart | 71 +++++++++++++------ .../widget/empty_transactions.dart | 3 +- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/lib/presentation/navigation/routes.g.dart b/lib/presentation/navigation/routes.g.dart index dd948d4..3665d30 100644 --- a/lib/presentation/navigation/routes.g.dart +++ b/lib/presentation/navigation/routes.g.dart @@ -11,9 +11,10 @@ List get $appRoutes => [ $loginRoute, $mainRoute, $transactionDetailsRoute, - $statisticsRoute, $forgetPasswordRoute, $updatePasswordRoute, + $addIncomeRoute, + $addExpenseRoute, ]; RouteBase get $onBoardingRoute => @@ -117,17 +118,17 @@ mixin $TransactionDetailsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -RouteBase get $statisticsRoute => GoRouteData.$route( - path: '/statistics', - factory: $StatisticsRoute._fromState, +RouteBase get $forgetPasswordRoute => GoRouteData.$route( + path: '/forget_password', + factory: $ForgetPasswordRoute._fromState, ); -mixin $StatisticsRoute on GoRouteData { - static StatisticsRoute _fromState(GoRouterState state) => - const StatisticsRoute(); +mixin $ForgetPasswordRoute on GoRouteData { + static ForgetPasswordRoute _fromState(GoRouterState state) => + const ForgetPasswordRoute(); @override - String get location => GoRouteData.$location('/statistics'); + String get location => GoRouteData.$location('/forget_password'); @override void go(BuildContext context) => context.go(location); @@ -143,17 +144,17 @@ mixin $StatisticsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -RouteBase get $forgetPasswordRoute => GoRouteData.$route( - path: '/forget_password', - factory: $ForgetPasswordRoute._fromState, +RouteBase get $updatePasswordRoute => GoRouteData.$route( + path: '/update_password', + factory: $UpdatePasswordRoute._fromState, ); -mixin $ForgetPasswordRoute on GoRouteData { - static ForgetPasswordRoute _fromState(GoRouterState state) => - const ForgetPasswordRoute(); +mixin $UpdatePasswordRoute on GoRouteData { + static UpdatePasswordRoute _fromState(GoRouterState state) => + UpdatePasswordRoute(); @override - String get location => GoRouteData.$location('/forget_password'); + String get location => GoRouteData.$location('/update_password'); @override void go(BuildContext context) => context.go(location); @@ -169,17 +170,43 @@ mixin $ForgetPasswordRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -RouteBase get $updatePasswordRoute => GoRouteData.$route( - path: '/update_password', - factory: $UpdatePasswordRoute._fromState, +RouteBase get $addIncomeRoute => GoRouteData.$route( + path: '/add-income', + factory: $AddIncomeRoute._fromState, ); -mixin $UpdatePasswordRoute on GoRouteData { - static UpdatePasswordRoute _fromState(GoRouterState state) => - UpdatePasswordRoute(); +mixin $AddIncomeRoute on GoRouteData { + static AddIncomeRoute _fromState(GoRouterState state) => + const AddIncomeRoute(); @override - String get location => GoRouteData.$location('/update_password'); + String get location => GoRouteData.$location('/add-income'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $addExpenseRoute => GoRouteData.$route( + path: '/add-expense', + factory: $AddExpenseRoute._fromState, +); + +mixin $AddExpenseRoute on GoRouteData { + static AddExpenseRoute _fromState(GoRouterState state) => + const AddExpenseRoute(); + + @override + String get location => GoRouteData.$location('/add-expense'); @override void go(BuildContext context) => context.go(location); diff --git a/lib/presentation/transactions/widget/empty_transactions.dart b/lib/presentation/transactions/widget/empty_transactions.dart index c6032ae..d2b11c1 100644 --- a/lib/presentation/transactions/widget/empty_transactions.dart +++ b/lib/presentation/transactions/widget/empty_transactions.dart @@ -3,6 +3,7 @@ import 'package:moneyplus/core/l10n/app_localizations.dart'; 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:moneyplus/presentation/navigation/routes.dart'; import 'package:svg_flutter/svg.dart'; class EmptyTransactions extends StatelessWidget { @@ -46,7 +47,7 @@ class EmptyTransactions extends StatelessWidget { ), SizedBox(height: 24), IntrinsicWidth( - child: DefaultButton(text: localizations.add_transaction), + child: DefaultButton(text: localizations.add_transaction, onPressed: (){AddIncomeRoute().push(context);},), ), ], ); From 40d52d64370af87cb85be0e50a661770a9a91470 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Mon, 23 Feb 2026 20:59:13 +0200 Subject: [PATCH 08/13] refactor: use meaningful name --- lib/data/repository/transaction_repository.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/data/repository/transaction_repository.dart b/lib/data/repository/transaction_repository.dart index a5050eb..945afa4 100644 --- a/lib/data/repository/transaction_repository.dart +++ b/lib/data/repository/transaction_repository.dart @@ -92,16 +92,16 @@ class TransactionRepositoryImpl implements TransactionRepository { ); return (response as List) .map( - (e) => Transaction( - id: e['id'] ?? 0, - amount: (e['amount'] as num).toDouble(), - currency: e['currency'] ?? '', - type: e['transaction_type'] == 'income' + (transaction) => Transaction( + id: transaction['id'] ?? 0, + amount: (transaction['amount'] as num).toDouble(), + currency: transaction['currency'] ?? '', + type: transaction['transaction_type'] == 'income' ? TransactionType.income : TransactionType.expense, - date: DateTime.parse(e['date']), - category: TransactionCategory(id: 1, name: e['category']), - note: e['note'] ?? '', + date: DateTime.parse(transaction['date']), + category: TransactionCategory(id: 1, name: transaction['category']), + note: transaction['note'] ?? '', ), ) .toList(); From b462575055f95de7a023b41d95326889033264ca Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Mon, 23 Feb 2026 21:09:11 +0200 Subject: [PATCH 09/13] refactor: make app bar and tabs row fixed and not scrolling --- .../transactions/widget/loading_view.dart | 3 +- .../widget/transaction_screen_content.dart | 52 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/presentation/transactions/widget/loading_view.dart b/lib/presentation/transactions/widget/loading_view.dart index 115f58f..2e550f0 100644 --- a/lib/presentation/transactions/widget/loading_view.dart +++ b/lib/presentation/transactions/widget/loading_view.dart @@ -18,7 +18,8 @@ class LoadingView extends StatelessWidget { color: context.colors.primary, ), ), - ) + ), + SizedBox(height: 16,), ], ); } diff --git a/lib/presentation/transactions/widget/transaction_screen_content.dart b/lib/presentation/transactions/widget/transaction_screen_content.dart index 5dda772..8b49857 100644 --- a/lib/presentation/transactions/widget/transaction_screen_content.dart +++ b/lib/presentation/transactions/widget/transaction_screen_content.dart @@ -56,36 +56,38 @@ class _TransactionsScreenContentState extends State { } }, builder: (context, state) { - return CustomScrollView( - controller: _controller, - slivers: [ - SliverToBoxAdapter( - child: TransactionAppBar( - year: state.selectedYear, - month: state.selectedMonth, - onDatePick: context.read().setSelectedDate, - onFilterClicked: () {}, - ), + return Column( + children: [ + TransactionAppBar( + year: state.selectedYear, + month: state.selectedMonth, + onDatePick: context.read().setSelectedDate, + onFilterClicked: () {}, ), - SliverPadding( + + Padding( padding: const EdgeInsets.only(bottom: 16, left: 16, top: 16), - sliver: SliverToBoxAdapter( - child: TabsRow( - selectedTab: state.selectedTab, - onTabSelected: context - .read() - .onTabSelected, - ), + child: TabsRow( + selectedTab: state.selectedTab, + onTabSelected: context.read().onTabSelected, ), ), - state.status == TransactionStatus.loading - ? SliverFillRemaining(child: LoadingView()) - : state.transactions.isEmpty - ? SliverFillRemaining(child: EmptyTransactions()) - : TransactionsList(transactions: state.transactions), - if (state.isLoadingMore) - const SliverToBoxAdapter(child: LoadingView()), + Expanded( + child: CustomScrollView( + controller: _controller, + slivers: [ + state.status == TransactionStatus.loading + ? SliverFillRemaining(child: LoadingView()) + : state.transactions.isEmpty + ? SliverFillRemaining(child: EmptyTransactions()) + : TransactionsList(transactions: state.transactions), + + if (state.isLoadingMore) + const SliverToBoxAdapter(child: LoadingView()), + ], + ), + ), ], ); }, From 3648c4a22efaec9b585600fb8aa7cc134a7b524d Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Wed, 25 Feb 2026 20:48:59 +0200 Subject: [PATCH 10/13] refactor: add error handling for api request --- .../transactions/cubit/transaction_cubit.dart | 151 ++++++++++-------- 1 file changed, 88 insertions(+), 63 deletions(-) diff --git a/lib/presentation/transactions/cubit/transaction_cubit.dart b/lib/presentation/transactions/cubit/transaction_cubit.dart index 91253ac..441c6bd 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:moneyplus/domain/entity/transaction.dart'; import 'package:moneyplus/domain/entity/transaction_type.dart'; import 'package:moneyplus/domain/repository/transaction_repository.dart'; import 'package:moneyplus/presentation/transactions/cubit/transaction_state.dart'; @@ -13,19 +14,23 @@ class TransactionCubit extends Cubit { emit(state.copyWith(status: TransactionStatus.loading)); final now = DateTime.now(); - final result = await transactionRepository.getTransactions( - page: 1, - date: now, - ); - emit( - state.copyWith( - transactions: result, - selectedYear: now.year, - selectedMonth: now.month, - status: TransactionStatus.success, - hasMore: result.length == 20, - ), - ); + try { + final result = await transactionRepository.getTransactions( + page: 1, + date: now, + ); + emit( + state.copyWith( + transactions: result, + selectedYear: now.year, + selectedMonth: now.month, + status: TransactionStatus.success, + hasMore: result.length == 20, + ), + ); + } catch (_) { + emit(state.copyWith(status: TransactionStatus.failure)); + } } void onTabSelected(TransactionTabs tab) async { @@ -40,24 +45,25 @@ class TransactionCubit extends Cubit { ), ); - final result = await transactionRepository.getTransactions( - page: 1, - type: tab == TransactionTabs.expenses - ? TransactionType.expense - : tab == TransactionTabs.incomes - ? TransactionType.income - : null, - date: DateTime(state.selectedYear, state.selectedMonth), - ); - - emit( - state.copyWith( - status: TransactionStatus.success, - transactions: result, - currentPage: 1, - hasMore: result.length == 20, - ), - ); + try { + final result = await _getTransaction( + page: 1, + tab: tab, + year: state.selectedYear, + month: state.selectedMonth, + ); + + emit( + state.copyWith( + status: TransactionStatus.success, + transactions: result, + currentPage: 1, + hasMore: result.length == 20, + ), + ); + } catch (_) { + emit(state.copyWith(status: TransactionStatus.failure)); + } } void setSelectedDate(int month, int year) async { @@ -73,24 +79,25 @@ class TransactionCubit extends Cubit { ), ); - final result = await transactionRepository.getTransactions( - page: 1, - type: state.selectedTab == TransactionTabs.expenses - ? TransactionType.expense - : state.selectedTab == TransactionTabs.incomes - ? TransactionType.income - : null, - date: DateTime(year, month), - ); - - emit( - state.copyWith( - status: TransactionStatus.success, - transactions: result, - currentPage: 1, - hasMore: result.length == 20, - ), - ); + try { + final result = await _getTransaction( + page: 1, + tab: state.selectedTab, + year: year, + month: month, + ); + + emit( + state.copyWith( + status: TransactionStatus.success, + transactions: result, + currentPage: 1, + hasMore: result.length == 20, + ), + ); + } catch (_) { + emit(state.copyWith(status: TransactionStatus.failure)); + } } void loadMore() async { @@ -100,23 +107,41 @@ class TransactionCubit extends Cubit { final nextPage = state.currentPage + 1; - final result = await transactionRepository.getTransactions( - page: nextPage, - type: state.selectedTab == TransactionTabs.expenses + try { + final result = await _getTransaction( + page: nextPage, + tab: state.selectedTab, + year: state.selectedYear, + month: state.selectedMonth, + ); + + emit( + state.copyWith( + transactions: [...state.transactions, ...result], + currentPage: nextPage, + hasMore: result.length == 20, + isLoadingMore: false, + ), + ); + } catch (_) { + emit(state.copyWith(status: TransactionStatus.failure)); + } + } + + Future> _getTransaction({ + required int page, + required TransactionTabs tab, + required int year, + required int month, + }) async { + return await transactionRepository.getTransactions( + page: page, + type: tab == TransactionTabs.expenses ? TransactionType.expense - : state.selectedTab == TransactionTabs.incomes + : tab == TransactionTabs.incomes ? TransactionType.income : null, - date: DateTime(state.selectedYear, state.selectedMonth), - ); - - emit( - state.copyWith( - transactions: [...state.transactions, ...result], - currentPage: nextPage, - hasMore: result.length == 20, - isLoadingMore: false, - ), + date: DateTime(year, month), ); } } From 6beb1d6ad48bb16ebf799f72bcdbc3318ac06382 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Wed, 25 Feb 2026 20:55:16 +0200 Subject: [PATCH 11/13] refactor: add transaction mapping in transaction entity --- .../repository/transaction_repository.dart | 21 ++----------------- lib/domain/entity/transaction.dart | 16 +++++++++++++- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/lib/data/repository/transaction_repository.dart b/lib/data/repository/transaction_repository.dart index 945afa4..0528ce8 100644 --- a/lib/data/repository/transaction_repository.dart +++ b/lib/data/repository/transaction_repository.dart @@ -9,7 +9,6 @@ import 'package:moneyplus/domain/repository/transaction_repository.dart'; import '../../domain/entity/currency.dart'; - class TransactionRepositoryImpl implements TransactionRepository { final SupabaseService service; @@ -82,28 +81,12 @@ class TransactionRepositoryImpl implements TransactionRepository { params: { 'p_timestamp': date?.toIso8601String(), 'p_category_ids': categoriesId, - 'p_transaction_type_id': type == TransactionType.income - ? 1 - : type == TransactionType.expense - ? 2 - : null, + 'p_transaction_type_id': type?.value, 'p_page': page, }, ); return (response as List) - .map( - (transaction) => Transaction( - id: transaction['id'] ?? 0, - amount: (transaction['amount'] as num).toDouble(), - currency: transaction['currency'] ?? '', - type: transaction['transaction_type'] == 'income' - ? TransactionType.income - : TransactionType.expense, - date: DateTime.parse(transaction['date']), - category: TransactionCategory(id: 1, name: transaction['category']), - note: transaction['note'] ?? '', - ), - ) + .map((transaction) => Transaction.fromJson(transaction)) .toList(); } diff --git a/lib/domain/entity/transaction.dart b/lib/domain/entity/transaction.dart index 0fb106d..ed962e0 100644 --- a/lib/domain/entity/transaction.dart +++ b/lib/domain/entity/transaction.dart @@ -36,7 +36,21 @@ class Transaction { type: type ?? this.type, date: date ?? this.date, category: category ?? this.category, - note: note ?? this.note + note: note ?? this.note, + ); + } + + factory Transaction.fromJson(Map json) { + return Transaction( + id: json['id'] ?? 0, + amount: (json['amount'] as num).toDouble(), + currency: json['currency'] ?? '', + type: json['transaction_type'] == 'income' + ? TransactionType.income + : TransactionType.expense, + date: DateTime.parse(json['date']), + category: TransactionCategory(id: 1, name: json['category']), + note: json['note'] ?? '', ); } } From 0db85aad02dc4867c673e87941b93bf5e70d4985 Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Wed, 25 Feb 2026 21:03:14 +0200 Subject: [PATCH 12/13] refactor: remove on pressed in add transaction btn --- lib/presentation/transactions/widget/empty_transactions.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/presentation/transactions/widget/empty_transactions.dart b/lib/presentation/transactions/widget/empty_transactions.dart index d2b11c1..37ebe2c 100644 --- a/lib/presentation/transactions/widget/empty_transactions.dart +++ b/lib/presentation/transactions/widget/empty_transactions.dart @@ -3,7 +3,6 @@ import 'package:moneyplus/core/l10n/app_localizations.dart'; 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:moneyplus/presentation/navigation/routes.dart'; import 'package:svg_flutter/svg.dart'; class EmptyTransactions extends StatelessWidget { @@ -47,7 +46,7 @@ class EmptyTransactions extends StatelessWidget { ), SizedBox(height: 24), IntrinsicWidth( - child: DefaultButton(text: localizations.add_transaction, onPressed: (){AddIncomeRoute().push(context);},), + child: DefaultButton(text: localizations.add_transaction, onPressed: (){},), ), ], ); From 1404c4ca5aa5fe6d277cac7d4f3270a792ce8acc Mon Sep 17 00:00:00 2001 From: nourelhodaahmed Date: Wed, 25 Feb 2026 21:31:31 +0200 Subject: [PATCH 13/13] refactor: get category id from remote --- lib/domain/entity/transaction.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/domain/entity/transaction.dart b/lib/domain/entity/transaction.dart index ed962e0..b6d83ac 100644 --- a/lib/domain/entity/transaction.dart +++ b/lib/domain/entity/transaction.dart @@ -49,7 +49,7 @@ class Transaction { ? TransactionType.income : TransactionType.expense, date: DateTime.parse(json['date']), - category: TransactionCategory(id: 1, name: json['category']), + category: TransactionCategory(id: json['category_id'], name: json['category']), note: json['note'] ?? '', ); }