Feat: Done for active transaction in boarding pass and features transaction history

This commit is contained in:
orangdeso 2025-05-15 01:51:13 +07:00
parent a9e1390a4a
commit 6edf8d531e
13 changed files with 220 additions and 77 deletions

View File

@ -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",

View File

@ -93,6 +93,18 @@ class HistoryRepositoryImpl implements HistoryRepository {
});
}
@override
Stream<List<TransactionModel>> 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<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId) async {
try {

View File

@ -52,6 +52,7 @@ class FlightModel {
final String transitAirplane;
final String stop;
final int price;
final String gate;
final String airlineLogo;
final Map<String, SeatInfo> 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())),
};

View File

@ -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<String, dynamic> json) {
DateTime getDateTime(dynamic value) {
if (value is Timestamp) {

View File

@ -2,5 +2,6 @@ import '../models/transaction_model.dart';
abstract class HistoryRepository {
Stream<List<TransactionModel>> getTransactionsStream(String userId, String status);
Stream<List<TransactionModel>> getHistoryTransactionsStream(String userId);
Future<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId);
}

View File

@ -7,16 +7,18 @@ class HistoryUseCase {
HistoryUseCase(this._repository);
// Mendapatkan transaksi dengan status pending secara realtime
Stream<List<TransactionModel>> getPendingTransactionsStream(String userId) {
return _repository.getTransactionsStream(userId, 'pending');
}
// Mendapatkan transaksi dengan status active secara realtime
Stream<List<TransactionModel>> getActiveTransactionsStream(String userId) {
return _repository.getTransactionsStream(userId, 'active');
}
Stream<List<TransactionModel>> getHistoryTransactionsStream(String uid) {
return _repository.getHistoryTransactionsStream(uid);
}
Future<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId) {
return _repository.getTransactionFromFirestore(ticketId, transactionId);
}

View File

@ -10,6 +10,7 @@ class HistoryController extends GetxController {
final RxList<TransactionModel> pendingTransactions = <TransactionModel>[].obs;
final RxList<TransactionModel> activeTransactions = <TransactionModel>[].obs;
final RxList<TransactionModel> historyTransactions = <TransactionModel>[].obs;
final Rx<TransactionModel?> selectedTransaction = Rx<TransactionModel?>(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

View File

@ -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)
],
)
],
)
],
),
),
)
],

View File

@ -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';

View File

@ -210,7 +210,7 @@ class _PrintBoardingPassScreenState extends State<PrintBoardingPassScreen> {
context: context,
services: services,
flightClass: transaction.flightDetails['flightClass'],
gate: 'Gate',
gate: transaction.flightDetails['gate'],
seatNumber: seatNumber,
),
),

View File

@ -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<transactionHistory> {
final _historyController = Get.find<HistoryController>();
@override
Widget build(BuildContext context) {
return Scaffold(
@ -29,12 +35,61 @@ class _transactionHistoryState extends State<transactionHistory> {
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<String, dynamic>? svcMap = transaction.porterServiceDetails;
final services = <String>[];
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,
),
);
},
),
);
},
),

View File

@ -312,6 +312,7 @@ class _TicketBookingStep4ScreenState extends State<TicketBookingStep4Screen> {
'stop': flightData?.stop,
'airlineLogo': flightData?.airlineLogo,
'price': flightData?.price,
'gate': flightData?.gate,
};
// Persiapkan data porter service jika ada

View File

@ -159,6 +159,7 @@ class AppRoutes {
GetPage(
name: Routes.TRANSACTIONHISTORY,
page: () => transactionHistory(),
binding: HistoryBinding(),
),
GetPage(
name: Routes.DETAILTICKET,