diff --git a/lib/data/repository/transaction_repository_stub.dart b/lib/data/repository/transaction_repository_stub.dart index 23564125..a78b76d2 100644 --- a/lib/data/repository/transaction_repository_stub.dart +++ b/lib/data/repository/transaction_repository_stub.dart @@ -1,10 +1,17 @@ +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; + + TransactionRepositoryStub(this.service); + @override Future addTransaction({ required double amount, @@ -30,8 +37,17 @@ class TransactionRepositoryStub implements TransactionRepository { } @override - Future deleteTransaction(int id) async { - throw UnimplementedError('deleteTransaction not implemented'); + Future deleteTransaction(String id) async { + final client = await service.getClient(); + + final response = await client.rpc( + 'delete_transaction', + params: {'p_id': id}, + ); + print("respinse of deleting is : $response"); + if(response == null || response['id'] == null){ + throw Exception("cannot delete transaction with id: $id "); + } } @override @@ -44,8 +60,63 @@ class TransactionRepositoryStub implements TransactionRepository { } @override - Future getTransactionDetails(int id) async { - throw UnimplementedError('getTransactionDetails not implemented'); + Future getTransactionDetails(String id) async { + final client = await service.getClient(); + final response = await client.rpc( + 'get_transaction_details', + params: {'p_id': id}, + ); + final data = response as Map; + if (data.isEmpty) { + throw Exception("no transaction with that id: $id"); + } + final transactionType = ((data['transaction_type'] as int) == 1) + ? TransactionType.income + : TransactionType.expense; + return Transaction( + id: 0, + amount: (data['amount'] as num).toDouble(), + currency: await _getCurrencyAbbreviation(data['currency_id'] as int), + type: transactionType, + date: DateTime.parse(data['created_at']).toLocal(), + category: TransactionCategory( + id: data['category_id'] as int, + name: await _getCategoryName((data['category_id'] as int).toString()), + ), + note: data['note'] as String, + ); + } + + Future _getCurrencyAbbreviation(int currencyId) async { + final client = await service.getClient(); + + final response = await client + .from('currencies') + .select('abbreviation') + .eq('id', currencyId) + .maybeSingle(); + + if (response == null) { + throw Exception("No currency found with id: $currencyId"); + } + + return response['abbreviation'] as String; + } + + Future _getCategoryName(String categoryId) async { + final client = await service.getClient(); + + final response = await client + .from('categories') + .select('name') + .eq('id', categoryId) + .maybeSingle(); + + if (response == null) { + throw Exception("No category found with id: $categoryId"); + } + + return response['name'] as String; } @override diff --git a/lib/data/repository/utils/fake_data.dart b/lib/data/repository/utils/fake_data.dart index 6c39e87f..00c1de5b 100644 --- a/lib/data/repository/utils/fake_data.dart +++ b/lib/data/repository/utils/fake_data.dart @@ -1,4 +1,6 @@ +import '../../../domain/entity/transaction.dart'; import '../../../domain/entity/transaction_category.dart'; +import '../../../domain/entity/transaction_type.dart'; import '../../../domain/repository/model/top_spending_category.dart'; List getFakeTopSpendingCategories() { @@ -25,4 +27,30 @@ List getFakeTopSpendingCategories() { percentage: 10.0, ), ]; -} \ No newline at end of file +} + +final fakeTransactionExpense = Transaction( + id: 1, + amount: 128.50, + currency: "USD", + type: TransactionType.expense, + date: DateTime.now(), + category: TransactionCategory( + id: 101, + name: "Food & Drinks", + ), + note: "Lunch at café", +); + +final fakeTransactionIncome = Transaction( + id: 2, + amount: 500.00, + currency: "USD", + type: TransactionType.income, + date: DateTime.now(), + category: TransactionCategory( + id: 201, + name: "Salary", + ), + note: "Monthly paycheck", +); \ No newline at end of file diff --git a/lib/di/injection.dart b/lib/di/injection.dart index cef2fab7..ea680194 100644 --- a/lib/di/injection.dart +++ b/lib/di/injection.dart @@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart'; import 'package:moneyplus/domain/repository/transaction_repository.dart'; import 'package:moneyplus/presentation/account_setup/cubit/account_setup_cubit.dart'; import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:moneyplus/presentation/trasnaction_details/trasnaction_details_cubit.dart'; import '../data/repository/account_repository.dart'; import '../data/repository/authentication_repository.dart'; @@ -56,7 +57,7 @@ void initDI() { ); getIt.registerLazySingleton( - () => TransactionRepositoryStub(), + () => TransactionRepositoryStub(getIt()), ); getIt.registerLazySingleton(() => AccountSetupCubit(getIt())); @@ -69,4 +70,8 @@ void initDI() { () => TransactionCubit(transactionRepository: getIt()), ); + + getIt.registerFactory( + () => TransactionDetailsCubit(transactionRepository: getIt()) + ); } diff --git a/lib/domain/repository/transaction_repository.dart b/lib/domain/repository/transaction_repository.dart index d141ac09..4d8fc0c8 100644 --- a/lib/domain/repository/transaction_repository.dart +++ b/lib/domain/repository/transaction_repository.dart @@ -21,7 +21,7 @@ abstract class TransactionRepository { String? note, }); - Future deleteTransaction(int id); + Future deleteTransaction(String id); Future> getTransactions({ TransactionType? type, @@ -29,7 +29,7 @@ abstract class TransactionRepository { DateTime? date, }); - Future getTransactionDetails(int id); + Future getTransactionDetails(String id); Future getTotalAmount({TransactionType? type}); diff --git a/lib/presentation/navigation/routes.dart b/lib/presentation/navigation/routes.dart index f0dc325e..61f304cf 100644 --- a/lib/presentation/navigation/routes.dart +++ b/lib/presentation/navigation/routes.dart @@ -7,6 +7,7 @@ import 'package:moneyplus/presentation/login/screen/login_screen.dart'; import '../../di/injection.dart'; import '../login/cubit/login_cubit.dart'; +import '../trasnaction_details/transaction_details_screen.dart'; part 'routes.g.dart'; @@ -55,3 +56,15 @@ class HomeRoute extends GoRouteData with $HomeRoute { return HomeScreen(); } } + +@TypedGoRoute(path: '/transaction_details') +@immutable +class TransactionDetailsRoute extends GoRouteData with $TransactionDetailsRoute { + final String transactionId; + TransactionDetailsRoute(this.transactionId); + + @override + Widget build(BuildContext context, GoRouterState state) { + return TransactionDetailsScreen(transactionId: transactionId); + } +} diff --git a/lib/presentation/navigation/routes.g.dart b/lib/presentation/navigation/routes.g.dart index 90f3bb29..c147a2cd 100644 --- a/lib/presentation/navigation/routes.g.dart +++ b/lib/presentation/navigation/routes.g.dart @@ -6,7 +6,12 @@ part of 'routes.dart'; // GoRouterGenerator // ************************************************************************** -List get $appRoutes => [$onBoardingRoute, $loginRoute, $homeRoute]; +List get $appRoutes => [ + $onBoardingRoute, + $loginRoute, + $homeRoute, + $transactionDetailsRoute, +]; RouteBase get $onBoardingRoute => GoRouteData.$route(path: '/', factory: $OnBoardingRoute._fromState); @@ -77,3 +82,34 @@ mixin $HomeRoute on GoRouteData { @override void replace(BuildContext context) => context.replace(location); } + +RouteBase get $transactionDetailsRoute => GoRouteData.$route( + path: '/transaction_details', + factory: $TransactionDetailsRoute._fromState, +); + +mixin $TransactionDetailsRoute on GoRouteData { + static TransactionDetailsRoute _fromState(GoRouterState state) => + TransactionDetailsRoute(state.uri.queryParameters['transaction-id']!); + + TransactionDetailsRoute get _self => this as TransactionDetailsRoute; + + @override + String get location => GoRouteData.$location( + '/transaction_details', + queryParams: {'transaction-id': _self.transactionId}, + ); + + @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); +} diff --git a/lib/presentation/trasnaction_details/transaction_details_screen.dart b/lib/presentation/trasnaction_details/transaction_details_screen.dart index 964ae6a2..acc259cb 100644 --- a/lib/presentation/trasnaction_details/transaction_details_screen.dart +++ b/lib/presentation/trasnaction_details/transaction_details_screen.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:moneyplus/design_system/assets/app_assets.dart'; import 'package:moneyplus/design_system/theme/money_colors.dart'; import 'package:moneyplus/design_system/theme/money_extension_context.dart'; import 'package:moneyplus/design_system/widgets/app_bar.dart'; import 'package:moneyplus/design_system/widgets/buttons/button/default_button.dart'; +import 'package:moneyplus/design_system/widgets/snack_bar.dart'; +import 'package:moneyplus/di/injection.dart'; import 'package:moneyplus/presentation/trasnaction_details/transactionDetailsComponent.dart'; +import 'package:moneyplus/presentation/trasnaction_details/trasnaction_details_cubit.dart'; import 'package:svg_flutter/svg.dart'; import '../../design_system/widgets/buttons/error/default_error_button.dart'; @@ -14,51 +18,113 @@ import '../../domain/entity/transaction_category.dart'; import '../../domain/entity/transaction_type.dart'; class TransactionDetailsScreen extends StatelessWidget { - const TransactionDetailsScreen({super.key}); + final String transactionId; + + const TransactionDetailsScreen({super.key, required this.transactionId}); @override Widget build(BuildContext context) { - final colors = context.colors; - return Scaffold( - appBar: CustomAppBar( - backgroundColor: colors.surfaceLow, - leading: _circleIcon(AppAssets.icArrowLeft,context), - title: "Transaction details", - trailing: _circleIcon(AppAssets.icShare,context), - ), - body: Container( - height: double.infinity, - color: colors.surface, - child: Padding( - padding: const EdgeInsets.only(top: 20), - child: SingleChildScrollView( - child: Center(child: TransactionDetailsComponent(transaction: fakeTransactionExpense)), - ), - ), + return BlocProvider( + create: (context) => + getIt() + ..getTransactionDetails(transactionId), + child: BlocBuilder( + builder: (context, state) { + return switch (state) { + TransactionDetailsLoading() => _loadingContent(), + TransactionDetailsLoaded() => _loadedContent(context, state), + TransactionDetailsError() => _errorContent(state.errorMsg), + }; + }, ), - bottomNavigationBar: _bottomBar(context), ); } } -Widget _circleIcon(String iconPath, BuildContext context){ - return GestureDetector( - onTap: (){ - GoRouter.of(context).pop(); +Widget _loadingContent() { + return Scaffold( + body: Center( + child: CircularProgressIndicator(color: MoneyColors.light.primary), + ), + ); +} + +Widget _errorContent(String errorMsg) { + return Scaffold(body: Center(child: Text(errorMsg))); +} + +Widget _loadedContent(BuildContext context, TransactionDetailsLoaded state) { + final colors = context.colors; + final cubit = context.read(); + return Scaffold( + appBar: CustomAppBar( + backgroundColor: colors.surfaceLow, + leading: _circleIcon(AppAssets.icArrowLeft, context), + title: "Transaction details", + trailing: _circleIcon(AppAssets.icShare, context), + ), + body: Container( + height: double.infinity, + color: colors.surface, + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: SingleChildScrollView( + child: Center( + child: TransactionDetailsComponent( + transaction: state.transactionDetails, + ), + ), + ), + ), + ), + bottomNavigationBar: _bottomBar( + context: context, + onClickDelete: () { + cubit.deleteTransaction().then((success) { + if (success) { + MSnackBar.success( + message: "Transaction deleted successfully", + title: "Success", + ).showSnackBar(context: context); + } else { + MSnackBar.error( + message: "Failed to delete transaction", + title: "Error", + ).showSnackBar(context: context); + } + }); }, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: MoneyColors.light.surface,), - alignment: Alignment.center, - child: SvgPicture.asset(iconPath, width: 20, height: 20, matchTextDirection: true), + ), + ); +} + +Widget _circleIcon(String iconPath, BuildContext context) { + return GestureDetector( + onTap: () { + GoRouter.of(context).pop(); + }, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MoneyColors.light.surface, ), - ); + alignment: Alignment.center, + child: SvgPicture.asset( + iconPath, + width: 20, + height: 20, + matchTextDirection: true, + ), + ), + ); } -Widget _bottomBar(BuildContext context){ +Widget _bottomBar({ + required BuildContext context, + required Function onClickDelete, +}) { final localizations = context.localizations; return Container( width: double.infinity, @@ -68,42 +134,21 @@ Widget _bottomBar(BuildContext context){ right: false, left: false, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), child: Column( mainAxisSize: MainAxisSize.min, spacing: 12, children: [ DefaultButton(text: localizations.edit), - DefaultErrorButton(text: localizations.delete) + DefaultErrorButton( + text: localizations.delete, + onPressed: () { + onClickDelete(); + }, + ), ], ), ), ), ); } - -final fakeTransactionExpense = Transaction( - id: 1, - amount: 125.50, - currency: "USD", - type: TransactionType.expense, - date: DateTime.now(), - category: TransactionCategory( - id: 101, - name: "Food & Drinks", - ), - note: "Lunch at café", -); - -final fakeTransactionIncome = Transaction( - id: 2, - amount: 500.00, - currency: "USD", - type: TransactionType.income, - date: DateTime.now(), - category: TransactionCategory( - id: 201, - name: "Salary", - ), - note: "Monthly paycheck", -); \ No newline at end of file diff --git a/lib/presentation/trasnaction_details/trasnaction_details_cubit.dart b/lib/presentation/trasnaction_details/trasnaction_details_cubit.dart new file mode 100644 index 00000000..89dd4486 --- /dev/null +++ b/lib/presentation/trasnaction_details/trasnaction_details_cubit.dart @@ -0,0 +1,40 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:moneyplus/domain/entity/transaction.dart'; +import 'package:moneyplus/domain/repository/transaction_repository.dart'; + +import '../utils/safe_call.dart'; + +part 'trasnaction_details_state.dart'; + +class TransactionDetailsCubit extends Cubit { + final TransactionRepository transactionRepository; + + TransactionDetailsCubit({required this.transactionRepository}) + : super(TransactionDetailsLoading()); + + void getTransactionDetails(String id) async { + safeCall( + function: () { + return transactionRepository.getTransactionDetails(id); + }, + onSuccess: (details) { + emit(TransactionDetailsLoaded(transactionDetails: details,transactionId: id)); + }, + onError: (e) { + emit(TransactionDetailsError(errorMsg: "Error, cannot get Translation details")); + print("error in TransactionDetailsCubit: $e"); + }, + ); + + } + + Future deleteTransaction() async { + try{ + await transactionRepository.deleteTransaction((state as TransactionDetailsLoaded).transactionId.toString()); + return true; + }catch(e){ + return false; + } + } +} diff --git a/lib/presentation/trasnaction_details/trasnaction_details_state.dart b/lib/presentation/trasnaction_details/trasnaction_details_state.dart new file mode 100644 index 00000000..8973c430 --- /dev/null +++ b/lib/presentation/trasnaction_details/trasnaction_details_state.dart @@ -0,0 +1,34 @@ +part of 'trasnaction_details_cubit.dart'; + +@immutable +sealed class TransactionDetailsState {} + +final class TransactionDetailsLoading extends TransactionDetailsState {} + +final class TransactionDetailsLoaded extends TransactionDetailsState { + final Transaction transactionDetails; + final String transactionId; + + + TransactionDetailsLoaded({ + required this.transactionDetails, + required this.transactionId, + }); + + TransactionDetailsLoaded copyWith({ + Transaction? transactionDetails, + String? transactionId, + + }) { + return TransactionDetailsLoaded( + transactionDetails: transactionDetails ?? this.transactionDetails, + transactionId: transactionId ?? this.transactionId, + ); + } +} + +final class TransactionDetailsError extends TransactionDetailsState { + final String errorMsg; + + TransactionDetailsError({required this.errorMsg}); +} diff --git a/lib/presentation/utils/safe_call.dart b/lib/presentation/utils/safe_call.dart new file mode 100644 index 00000000..f24bd126 --- /dev/null +++ b/lib/presentation/utils/safe_call.dart @@ -0,0 +1,12 @@ +Future safeCall({ + required Future Function() function, + required void Function(T) onSuccess, + required void Function(Exception?) onError, +}) async { + try { + var result = await function(); + onSuccess(result); + } catch (e) { + onError(e is Exception ? e : Exception(e.toString())); + } +} \ No newline at end of file