diff --git a/lib/data/repository/transaction_repository.dart b/lib/data/repository/transaction_repository.dart index 801368a..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; @@ -73,8 +72,22 @@ class TransactionRepositoryImpl 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?.toIso8601String(), + 'p_category_ids': categoriesId, + 'p_transaction_type_id': type?.value, + 'p_page': page, + }, + ); + return (response as List) + .map((transaction) => Transaction.fromJson(transaction)) + .toList(); } @override @@ -174,94 +187,10 @@ class TransactionRepositoryImpl 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 { static String deleteTransaction = 'delete_transaction'; static String getTransactionDetails = 'get_transaction_details'; + static String getTransactions = 'get_transactions'; } diff --git a/lib/domain/entity/transaction.dart b/lib/domain/entity/transaction.dart index 0fb106d..b6d83ac 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: json['category_id'], name: json['category']), + note: json['note'] ?? '', ); } } diff --git a/lib/domain/repository/transaction_repository.dart b/lib/domain/repository/transaction_repository.dart index 9737066..c510f01 100644 --- a/lib/domain/repository/transaction_repository.dart +++ b/lib/domain/repository/transaction_repository.dart @@ -31,9 +31,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}); @@ -46,8 +48,4 @@ abstract class TransactionRepository { Future addExpenseCategory(String name); Future editExpenseCategory({required int id, required String name}); - - Future> getAllTransactions(); - - Future> getAllTransactionsByType(TransactionType type,); } diff --git a/lib/presentation/transactions/cubit/transaction_cubit.dart b/lib/presentation/transactions/cubit/transaction_cubit.dart index 5af5b07..441c6bd 100644 --- a/lib/presentation/transactions/cubit/transaction_cubit.dart +++ b/lib/presentation/transactions/cubit/transaction_cubit.dart @@ -13,37 +13,57 @@ class TransactionCubit extends Cubit { void loadData() async { emit(state.copyWith(status: TransactionStatus.loading)); - final result = await transactionRepository.getAllTransactions(); final now = DateTime.now(); - emit( - state.copyWith( - allTransactions: result, - filteredTransactions: _filterTransaction(now.month, now.year, result), - selectedYear: now.year, - selectedMonth: now.month, - status: TransactionStatus.success, - ), - ); + 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 { if (state.selectedTab == tab) return; - emit(state.copyWith(status: TransactionStatus.loading, selectedTab: tab)); - - final result = await _getTransactionsByTab(tab); - emit( state.copyWith( - status: TransactionStatus.success, - allTransactions: result, - filteredTransactions: _filterTransaction( - state.selectedMonth, - state.selectedYear, - result, - ), + status: TransactionStatus.loading, + selectedTab: tab, + currentPage: 1, + hasMore: true, ), ); + + 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 { @@ -54,39 +74,74 @@ class TransactionCubit extends Cubit { selectedMonth: month, selectedYear: year, status: TransactionStatus.loading, + currentPage: 1, + hasMore: true, ), ); - emit( - state.copyWith( - status: TransactionStatus.success, - filteredTransactions: _filterTransaction( - month, - year, - state.allTransactions, + 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)); + } } - 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), - }; + void loadMore() async { + if (!state.hasMore || state.isLoadingMore) return; + + emit(state.copyWith(isLoadingMore: true)); + + final nextPage = state.currentPage + 1; + + 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)); + } } - List _filterTransaction( - int month, - int year, - List transactions, - ) { - return transactions.where((transaction) { - return transaction.date.year == year && transaction.date.month == month; - }).toList(); + 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 + : tab == TransactionTabs.incomes + ? TransactionType.income + : null, + date: DateTime(year, month), + ); } } diff --git a/lib/presentation/transactions/cubit/transaction_state.dart b/lib/presentation/transactions/cubit/transaction_state.dart index 5ffd7a2..61f5498 100644 --- a/lib/presentation/transactions/cubit/transaction_state.dart +++ b/lib/presentation/transactions/cubit/transaction_state.dart @@ -10,20 +10,24 @@ 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; + final int currentPage; + final bool hasMore; + final bool isLoadingMore; 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, + this.currentPage = 1, + this.hasMore = true, + this.isLoadingMore = false }); factory TransactionState.initial() => @@ -32,20 +36,24 @@ class TransactionState { TransactionState copyWith({ TransactionStatus? status, ErrorModel? error, - List? filteredTransactions, - List? allTransactions, + List? transactions, TransactionTabs? selectedTab, int? selectedYear, int? selectedMonth, + int? currentPage, + bool? hasMore, + bool? isLoadingMore }) { 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, + 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 b4e9733..ec9abc0 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,52 +10,10 @@ 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( + return SafeArea( + child: 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.filteredTransactions.isEmpty - ? SliverFillRemaining(child: EmptyTransactions()) - : TransactionsList(transactions: state.filteredTransactions), - ], - ); - }, - ), + child: TransactionScreenContent(), ), ); } diff --git a/lib/presentation/transactions/widget/empty_transactions.dart b/lib/presentation/transactions/widget/empty_transactions.dart index c6032ae..37ebe2c 100644 --- a/lib/presentation/transactions/widget/empty_transactions.dart +++ b/lib/presentation/transactions/widget/empty_transactions.dart @@ -46,7 +46,7 @@ class EmptyTransactions extends StatelessWidget { ), SizedBox(height: 24), IntrinsicWidth( - child: DefaultButton(text: localizations.add_transaction), + child: DefaultButton(text: localizations.add_transaction, onPressed: (){},), ), ], ); 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 new file mode 100644 index 0000000..8b49857 --- /dev/null +++ b/lib/presentation/transactions/widget/transaction_screen_content.dart @@ -0,0 +1,97 @@ +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 Column( + children: [ + TransactionAppBar( + year: state.selectedYear, + month: state.selectedMonth, + onDatePick: context.read().setSelectedDate, + onFilterClicked: () {}, + ), + + Padding( + padding: const EdgeInsets.only(bottom: 16, left: 16, top: 16), + child: TabsRow( + selectedTab: state.selectedTab, + onTabSelected: context.read().onTabSelected, + ), + ), + + 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()), + ], + ), + ), + ], + ); + }, + ), + ); + } +}