From 6edf8d531ef1f0ccb426ec3be476d9f0f7492d74 Mon Sep 17 00:00:00 2001 From: orangdeso Date: Thu, 15 May 2025 01:51:13 +0700 Subject: [PATCH] Feat: Done for active transaction in boarding pass and features transaction history --- .dart_tool/package_config.json | 2 +- .../repositories/history_repository_impl.dart | 12 ++ lib/domain/models/ticket_model.dart | 4 + lib/domain/models/transaction_model.dart | 4 + .../repositories/history_repository.dart | 1 + lib/domain/usecases/history_usecase.dart | 6 +- .../controllers/history_controller.dart | 43 ++++- .../component/card_transaction.dart | 153 +++++++++++------- .../pages/boarding_pass_screen.dart | 1 - .../pages/print_boarding_pass_screen.dart | 2 +- .../pages/transaction_history.dart | 67 +++++++- .../pages/ticket_booking_step4_screen.dart | 1 + .../screens/routes/app_rountes.dart | 1 + 13 files changed, 220 insertions(+), 77 deletions(-) diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index 36826ab..f7b0c32 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -680,7 +680,7 @@ "languageVersion": "3.4" } ], - "generated": "2025-05-14T07:20:56.034581Z", + "generated": "2025-05-14T18:46:38.233635Z", "generator": "pub", "generatorVersion": "3.5.0", "flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0", diff --git a/lib/data/repositories/history_repository_impl.dart b/lib/data/repositories/history_repository_impl.dart index 124405e..6e29293 100644 --- a/lib/data/repositories/history_repository_impl.dart +++ b/lib/data/repositories/history_repository_impl.dart @@ -93,6 +93,18 @@ class HistoryRepositoryImpl implements HistoryRepository { }); } + @override + Stream> getHistoryTransactionsStream(String userId) { + return getTransactionsStream(userId, 'active').map((allPaid) { + final startOfToday = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + ); + return allPaid.where((tx) => tx.departureDate.isBefore(startOfToday)).toList(); + }); + } + @override Future getTransactionFromFirestore(String ticketId, String transactionId) async { try { diff --git a/lib/domain/models/ticket_model.dart b/lib/domain/models/ticket_model.dart index 4e53ff8..df1af04 100644 --- a/lib/domain/models/ticket_model.dart +++ b/lib/domain/models/ticket_model.dart @@ -52,6 +52,7 @@ class FlightModel { final String transitAirplane; final String stop; final int price; + final String gate; final String airlineLogo; final Map seat; @@ -73,6 +74,7 @@ class FlightModel { required this.transitAirplane, required this.stop, required this.price, + required this.gate, required this.airlineLogo, required this.seat, }); @@ -105,6 +107,7 @@ class FlightModel { transitAirplane: data['transitAirplane'] ?? '', stop: data['stop'] ?? '', price: data['price'] ?? 0, + gate: data['gate'] ?? '', airlineLogo: data['airlineLogo'] ?? '', seat: seatMap, ); @@ -128,6 +131,7 @@ class FlightModel { 'transitAirplane': transitAirplane, 'stop': stop, 'price': price, + 'gate': gate, 'airlineLogo': airlineLogo, 'seat': seat.map((key, value) => MapEntry(key, value.toMap())), }; diff --git a/lib/domain/models/transaction_model.dart b/lib/domain/models/transaction_model.dart index 0666a3f..6f6c8e7 100644 --- a/lib/domain/models/transaction_model.dart +++ b/lib/domain/models/transaction_model.dart @@ -39,6 +39,10 @@ class TransactionModel { required this.numberSeat, }); + DateTime get departureDate => (flightDetails['departureTime'] is int) + ? DateTime.fromMillisecondsSinceEpoch(flightDetails['departureTime'] as int) + : DateTime.now(); + factory TransactionModel.fromJson(Map json) { DateTime getDateTime(dynamic value) { if (value is Timestamp) { diff --git a/lib/domain/repositories/history_repository.dart b/lib/domain/repositories/history_repository.dart index 06f180d..56b10da 100644 --- a/lib/domain/repositories/history_repository.dart +++ b/lib/domain/repositories/history_repository.dart @@ -2,5 +2,6 @@ import '../models/transaction_model.dart'; abstract class HistoryRepository { Stream> getTransactionsStream(String userId, String status); + Stream> getHistoryTransactionsStream(String userId); Future getTransactionFromFirestore(String ticketId, String transactionId); } diff --git a/lib/domain/usecases/history_usecase.dart b/lib/domain/usecases/history_usecase.dart index 36761a0..c1fb797 100644 --- a/lib/domain/usecases/history_usecase.dart +++ b/lib/domain/usecases/history_usecase.dart @@ -7,16 +7,18 @@ class HistoryUseCase { HistoryUseCase(this._repository); - // Mendapatkan transaksi dengan status pending secara realtime Stream> getPendingTransactionsStream(String userId) { return _repository.getTransactionsStream(userId, 'pending'); } - // Mendapatkan transaksi dengan status active secara realtime Stream> getActiveTransactionsStream(String userId) { return _repository.getTransactionsStream(userId, 'active'); } + Stream> getHistoryTransactionsStream(String uid) { + return _repository.getHistoryTransactionsStream(uid); + } + Future getTransactionFromFirestore(String ticketId, String transactionId) { return _repository.getTransactionFromFirestore(ticketId, transactionId); } diff --git a/lib/presentation/controllers/history_controller.dart b/lib/presentation/controllers/history_controller.dart index 6efa014..7f9dff9 100644 --- a/lib/presentation/controllers/history_controller.dart +++ b/lib/presentation/controllers/history_controller.dart @@ -10,6 +10,7 @@ class HistoryController extends GetxController { final RxList pendingTransactions = [].obs; final RxList activeTransactions = [].obs; + final RxList historyTransactions = [].obs; final Rx selectedTransaction = Rx(null); final RxBool isLoading = true.obs; @@ -18,6 +19,7 @@ class HistoryController extends GetxController { StreamSubscription? _pendingSubscription; StreamSubscription? _activeSubscription; + StreamSubscription? _historySubscription; String _currentUserId = ''; @@ -42,15 +44,39 @@ class HistoryController extends GetxController { isLoading.value = false; }); - _activeSubscription = _historyUseCase.getActiveTransactionsStream(userId).listen((transactions) { - log('HistoryController: active transactions updated, count: ${transactions.length}'); + // _activeSubscription = _historyUseCase.getActiveTransactionsStream(userId).listen((transactions) { + // log('HistoryController: active transactions updated, count: ${transactions.length}'); - final sortedTransactions = _sortTransactionsByCreatedAt(transactions); - activeTransactions.value = sortedTransactions; + // final sortedTransactions = _sortTransactionsByCreatedAt(transactions); + // activeTransactions.value = sortedTransactions; + // isLoading.value = false; + // }, onError: (error) { + // log('HistoryController: Error mendapatkan transaksi active: $error'); + // isLoading.value = false; + // }); + + _activeSubscription = _historyUseCase.getActiveTransactionsStream(userId).map((list) { + final todayStart = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + ); + return list.where((tx) { + return tx.departureDate.isAtSameMomentAs(todayStart) || tx.departureDate.isAfter(todayStart); + }).toList(); + }).listen((filtered) { + activeTransactions.value = _sortTransactionsByCreatedAt(filtered); isLoading.value = false; - }, onError: (error) { - log('HistoryController: Error mendapatkan transaksi active: $error'); + }, onError: (e) { + log('Error mendapatkan transaksi active: $e'); + isLoading.value = false; + }); + + _historySubscription = _historyUseCase.getHistoryTransactionsStream(userId).listen((historyList) { + historyTransactions.value = _sortTransactionsByCreatedAt(historyList); + isLoading.value = false; + }, onError: (_) { isLoading.value = false; }); } @@ -119,6 +145,7 @@ class HistoryController extends GetxController { pendingTransactions.clear(); activeTransactions.clear(); + historyTransactions.clear(); isLoading.value = true; @@ -129,7 +156,7 @@ class HistoryController extends GetxController { } String getUserId() { - if (_pendingSubscription != null || _activeSubscription != null) { + if (_pendingSubscription != null || _activeSubscription != null || _historySubscription != null) { return _currentUserId; } return ''; @@ -138,8 +165,10 @@ class HistoryController extends GetxController { void _cleanupStreams() { _pendingSubscription?.cancel(); _activeSubscription?.cancel(); + _historySubscription?.cancel(); _pendingSubscription = null; _activeSubscription = null; + _historySubscription = null; } @override diff --git a/lib/presentation/screens/boarding_pass/component/card_transaction.dart b/lib/presentation/screens/boarding_pass/component/card_transaction.dart index 9ba33c3..25d2ef3 100644 --- a/lib/presentation/screens/boarding_pass/component/card_transaction.dart +++ b/lib/presentation/screens/boarding_pass/component/card_transaction.dart @@ -8,7 +8,28 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; class CardTransaction extends StatelessWidget { - const CardTransaction({super.key}); + final String bookingCode; + final String airlineLogo; + final String departureCode; + final String arrivalCode; + final String ticketDate; + final String flightClass; + final String servicePorter; + final String duration; + final String price; + + const CardTransaction({ + Key? key, + required this.bookingCode, + required this.airlineLogo, + required this.departureCode, + required this.arrivalCode, + required this.ticketDate, + required this.flightClass, + required this.servicePorter, + required this.duration, + required this.price, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -27,7 +48,7 @@ class CardTransaction extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ TypographyStyles.small("Kode Booking", color: Colors.white, fontWeight: FontWeight.w400), - TypographyStyles.body("I2L8JRL", color: Colors.white) + TypographyStyles.body(bookingCode, color: Colors.white) ], ), Container( @@ -46,66 +67,80 @@ class CardTransaction extends StatelessWidget { bottomLeft: Radius.circular(10.r), bottomRight: Radius.circular(10.r), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SvgPicture.asset('assets/images/citilink.svg', width: 40.w, height: 10.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.center, + child: Image.network(airlineLogo, width: 40.w), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + TypographyStyles.small( + ticketDate, + color: GrayColors.gray600, + fontWeight: FontWeight.w400, + ), + SizedBox(width: 10.w), + CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)), + SizedBox(width: 10.w), + TypographyStyles.small(duration, color: GrayColors.gray600, fontWeight: FontWeight.w400), + ], + ), + SizedBox(height: 6.h), + Row( + children: [ + TypographyStyles.body(departureCode, color: GrayColors.gray800), + SizedBox(width: 10.w), + SvgPicture.asset( + 'assets/icons/ic_right.svg', + color: GrayColors.gray800, + width: 14.w, + height: 14.h, + ), + SizedBox(width: 10.w), + TypographyStyles.body(arrivalCode, color: GrayColors.gray800), + ], + ), + SizedBox(height: 6.h), + Row( + children: [ + TypographyStyles.small( + flightClass, + color: GrayColors.gray600, + fontWeight: FontWeight.w400, + ), + SizedBox(width: 10.w), + CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)), + SizedBox(width: 10.w), + Expanded(child: TypographyStyles.small(servicePorter, color: GrayColors.gray600, fontWeight: FontWeight.w400)), + ], + ), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - TypographyStyles.small( - 'Sen, 27 Jan 2025 ', - color: GrayColors.gray600, - fontWeight: FontWeight.w400, - ), - SizedBox(width: 10.w), - CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)), - SizedBox(width: 10.w), - TypographyStyles.small('5j 40m', color: GrayColors.gray600, fontWeight: FontWeight.w400), + TypographyStyles.small('Total Harga', color: GrayColors.gray600, fontWeight: FontWeight.w400), + TypographyStyles.caption(price, color: PrimaryColors.primary800) ], ), - SizedBox(height: 6.h), - Row( - children: [ - TypographyStyles.body('YIA', color: GrayColors.gray800), - SizedBox(width: 10.w), - SvgPicture.asset( - 'assets/icons/ic_right.svg', - color: GrayColors.gray800, - width: 14.w, - height: 14.h, - ), - SizedBox(width: 10.w), - TypographyStyles.body('LOP', color: GrayColors.gray800), - ], - ), - SizedBox(height: 6.h), - Row( - children: [ - TypographyStyles.small( - 'Economy', - color: GrayColors.gray600, - fontWeight: FontWeight.w400, - ), - SizedBox(width: 10.w), - CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)), - SizedBox(width: 10.w), - TypographyStyles.small('Fast Track (FT)', color: GrayColors.gray600, fontWeight: FontWeight.w400), - ], - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - TypographyStyles.small('Total Harga', color: GrayColors.gray600, fontWeight: FontWeight.w400), - TypographyStyles.caption("Rp 1.410.000", color: PrimaryColors.primary800) - ], - ) - ], + ) + ], + ), ), ) ], diff --git a/lib/presentation/screens/boarding_pass/pages/boarding_pass_screen.dart b/lib/presentation/screens/boarding_pass/pages/boarding_pass_screen.dart index 1b9515c..a1ebe7e 100644 --- a/lib/presentation/screens/boarding_pass/pages/boarding_pass_screen.dart +++ b/lib/presentation/screens/boarding_pass/pages/boarding_pass_screen.dart @@ -7,7 +7,6 @@ import 'package:e_porter/presentation/screens/routes/app_rountes.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; - import '../../../../_core/service/preferences_service.dart'; import '../../../../_core/utils/map_helper.dart'; import '../../../../domain/models/transaction_model.dart'; diff --git a/lib/presentation/screens/boarding_pass/pages/print_boarding_pass_screen.dart b/lib/presentation/screens/boarding_pass/pages/print_boarding_pass_screen.dart index 333205d..394a6f8 100644 --- a/lib/presentation/screens/boarding_pass/pages/print_boarding_pass_screen.dart +++ b/lib/presentation/screens/boarding_pass/pages/print_boarding_pass_screen.dart @@ -210,7 +210,7 @@ class _PrintBoardingPassScreenState extends State { context: context, services: services, flightClass: transaction.flightDetails['flightClass'], - gate: 'Gate', + gate: transaction.flightDetails['gate'], seatNumber: seatNumber, ), ), diff --git a/lib/presentation/screens/boarding_pass/pages/transaction_history.dart b/lib/presentation/screens/boarding_pass/pages/transaction_history.dart index 5f4de2f..1f11369 100644 --- a/lib/presentation/screens/boarding_pass/pages/transaction_history.dart +++ b/lib/presentation/screens/boarding_pass/pages/transaction_history.dart @@ -1,8 +1,12 @@ import 'package:e_porter/_core/constants/colors.dart'; +import 'package:e_porter/_core/constants/typography.dart'; +import 'package:e_porter/_core/utils/formatter/date_helper.dart'; +import 'package:e_porter/presentation/controllers/history_controller.dart'; import 'package:e_porter/presentation/screens/boarding_pass/component/card_transaction.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import '../../../../_core/component/appbar/appbar_component.dart'; @@ -14,6 +18,8 @@ class transactionHistory extends StatefulWidget { } class _transactionHistoryState extends State { + final _historyController = Get.find(); + @override Widget build(BuildContext context) { return Scaffold( @@ -29,12 +35,61 @@ class _transactionHistoryState extends State { body: SafeArea( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), - child: ListView.builder( - itemCount: 5, - itemBuilder: (context, index) { - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: CardTransaction(), + child: Obx( + () { + final list = _historyController.historyTransactions; + if (list.isEmpty) { + return Center( + child: TypographyStyles.caption( + 'Tidak ada riwayat transaksi', + color: GrayColors.gray400, + fontWeight: FontWeight.w400, + ), + ); + } + return RefreshIndicator( + onRefresh: () => _historyController.refreshTransactions(_historyController.getUserId()), + child: ListView.builder( + itemCount: list.length, + itemBuilder: (context, index) { + final transaction = list[index]; + + final Map? svcMap = transaction.porterServiceDetails; + + final services = []; + if (svcMap?['departure'] != null) { + services.add(svcMap!['departure']['name'] as String); + } + if (svcMap?['transit'] != null) { + services.add(svcMap!['transit']['name'] as String); + } + if (svcMap?['arrival'] != null) { + services.add(svcMap!['arrival']['name'] as String); + } + + final departureDate = + DateFormatterHelper.formatFlightDate(transaction.flightDetails['departureTime']); + final duration = DateFormatterHelper.calculateFlightDuration( + transaction.flightDetails['departureTime'], transaction.flightDetails['arrivalTime']); + final price = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0) + .format(transaction.amount); + + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: CardTransaction( + bookingCode: transaction.idBooking, + airlineLogo: transaction.flightDetails['airlineLogo'], + departureCode: transaction.flightDetails['codeDeparture'], + arrivalCode: transaction.flightDetails['codeArrival'], + ticketDate: departureDate, + flightClass: transaction.flightDetails['flightClass'], + servicePorter: services.join(", "), + duration: duration, + price: price, + ), + ); + }, + ), ); }, ), diff --git a/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart b/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart index 648af02..14aeb16 100644 --- a/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart +++ b/lib/presentation/screens/home/pages/ticket_booking_step4_screen.dart @@ -312,6 +312,7 @@ class _TicketBookingStep4ScreenState extends State { 'stop': flightData?.stop, 'airlineLogo': flightData?.airlineLogo, 'price': flightData?.price, + 'gate': flightData?.gate, }; // Persiapkan data porter service jika ada diff --git a/lib/presentation/screens/routes/app_rountes.dart b/lib/presentation/screens/routes/app_rountes.dart index 12a33a7..7fb53de 100644 --- a/lib/presentation/screens/routes/app_rountes.dart +++ b/lib/presentation/screens/routes/app_rountes.dart @@ -159,6 +159,7 @@ class AppRoutes { GetPage( name: Routes.TRANSACTIONHISTORY, page: () => transactionHistory(), + binding: HistoryBinding(), ), GetPage( name: Routes.DETAILTICKET,