Feat: Done for active transaction in boarding pass and features transaction history
This commit is contained in:
parent
a9e1390a4a
commit
6edf8d531e
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())),
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -159,6 +159,7 @@ class AppRoutes {
|
|||
GetPage(
|
||||
name: Routes.TRANSACTIONHISTORY,
|
||||
page: () => transactionHistory(),
|
||||
binding: HistoryBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.DETAILTICKET,
|
||||
|
|
Loading…
Reference in New Issue