Feat: add logic history boarding pass
This commit is contained in:
parent
e8763bb85f
commit
d565bb59fa
|
@ -343,6 +343,12 @@
|
|||
"packageUri": "lib/",
|
||||
"languageVersion": "2.12"
|
||||
},
|
||||
{
|
||||
"name": "mobile_scanner",
|
||||
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/mobile_scanner-6.0.10",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "3.4"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.0",
|
||||
|
@ -632,7 +638,7 @@
|
|||
"languageVersion": "3.4"
|
||||
}
|
||||
],
|
||||
"generated": "2025-04-08T08:36:36.076309Z",
|
||||
"generated": "2025-04-22T06:48:38.993021Z",
|
||||
"generator": "pub",
|
||||
"generatorVersion": "3.5.0",
|
||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
||||
|
|
|
@ -214,6 +214,10 @@ meta
|
|||
2.12
|
||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.15.0/
|
||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.15.0/lib/
|
||||
mobile_scanner
|
||||
3.4
|
||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/mobile_scanner-6.0.10/
|
||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/mobile_scanner-6.0.10/lib/
|
||||
path
|
||||
3.0
|
||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.0/
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Permissions -->
|
||||
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
<!-- Permissions -->
|
||||
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="34" /> -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- Untuk Android 13 dan lebih tinggi -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
<application
|
||||
android:label="e_porter"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
dev.steenbakker.mobile_scanner.useUnbundled=true
|
||||
kotlin.incremental=false
|
||||
org.gradle.caching=false
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import 'dart:developer';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import '../../domain/models/transaction_model.dart';
|
||||
import '../../domain/repositories/history_repository.dart';
|
||||
|
||||
class HistoryRepositoryImpl implements HistoryRepository {
|
||||
final FirebaseDatabase _database = FirebaseDatabase.instance;
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
@override
|
||||
Stream<List<TransactionModel>> getTransactionsStream(String userId, String status) {
|
||||
final ref = _database.ref().child('transactions').child(userId);
|
||||
|
||||
return ref.onValue.map((event) {
|
||||
final snapshot = event.snapshot;
|
||||
// log('HistoryRepositoryImpl: snapshot diterima, exists: ${snapshot.exists}, has children: ${snapshot.children.isNotEmpty}');
|
||||
|
||||
if (!snapshot.exists || snapshot.value == null) {
|
||||
// log('HistoryRepositoryImpl: tidak ada data transaksi untuk userId: $userId');
|
||||
return <TransactionModel>[];
|
||||
}
|
||||
|
||||
try {
|
||||
final Map<dynamic, dynamic> userTransactions = snapshot.value as Map<dynamic, dynamic>;
|
||||
// log('HistoryRepositoryImpl: jumlah grup transaksi: ${userTransactions.length}');
|
||||
|
||||
List<TransactionModel> transactions = [];
|
||||
|
||||
userTransactions.forEach((groupKey, groupData) {
|
||||
if (groupData is Map<dynamic, dynamic>) {
|
||||
groupData.forEach((transactionKey, transactionData) {
|
||||
try {
|
||||
if (transactionData is Map<dynamic, dynamic>) {
|
||||
if (transactionData.containsKey('payment') &&
|
||||
transactionData['payment'] is Map &&
|
||||
transactionData['payment'].containsKey('status')) {
|
||||
final paymentStatus = transactionData['payment']['status'];
|
||||
|
||||
if (paymentStatus == status) {
|
||||
final payment = transactionData['payment'] as Map<dynamic, dynamic>;
|
||||
final Map<String, dynamic> processedData = {
|
||||
'id': payment['id'] ?? transactionKey,
|
||||
'idBooking': transactionData['idBooking'] ?? '',
|
||||
'ticketId': groupKey,
|
||||
'flightId': transactionData['flight']?['code'] ?? '',
|
||||
|
||||
// Data payment
|
||||
'amount': payment['amount'] ?? 0.0,
|
||||
'method': payment['method'] ?? '',
|
||||
'status': paymentStatus,
|
||||
'proofUrl': payment['proofUrl'],
|
||||
'createdAt': payment['createdAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(payment['createdAt'] as int)
|
||||
: DateTime.now(),
|
||||
'expiryTime': payment['expiryTime'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(payment['expiryTime'] as int)
|
||||
: DateTime.now().add(Duration(hours: 1)),
|
||||
|
||||
'flightDetails': Map<String, dynamic>.from(transactionData['flight'] ?? {}),
|
||||
'bandaraDetails': Map<String, dynamic>.from(transactionData['bandara'] ?? {}),
|
||||
'porterServiceDetails': transactionData['porterService'] != null
|
||||
? Map<String, dynamic>.from(transactionData['porterService'])
|
||||
: null,
|
||||
'userDetails': Map<String, dynamic>.from(transactionData['user'] ?? {}),
|
||||
|
||||
// Data tambahan
|
||||
'passenger': transactionData['passenger'] ?? 0,
|
||||
'passengerDetails': transactionData['passengerDetails'] ?? [],
|
||||
'numberSeat': transactionData['numberSeat'] ?? [],
|
||||
};
|
||||
|
||||
final transaction = TransactionModel.fromJson(processedData);
|
||||
transactions.add(transaction);
|
||||
|
||||
// log('HistoryRepositoryImpl: transaksi dengan id: $transactionKey ditambahkan ke daftar (status: $status)');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log('HistoryRepositoryImpl: error parsing transaksi dengan id: $transactionKey, error: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
log('HistoryRepositoryImpl: jumlah transaksi dengan status $status: ${transactions.length}');
|
||||
return transactions;
|
||||
} catch (e) {
|
||||
log('HistoryRepositoryImpl: error saat memproses snapshot: $e');
|
||||
return <TransactionModel>[];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId) async {
|
||||
try {
|
||||
log('HistoryRepositoryImpl: mengambil transaksi dari Firestore dengan ticketId: $ticketId, transactionId: $transactionId');
|
||||
|
||||
// Referensi ke dokumen transaksi di Firestore
|
||||
final docRef = _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId);
|
||||
|
||||
// Mengambil data dokumen
|
||||
final docSnapshot = await docRef.get();
|
||||
|
||||
if (!docSnapshot.exists) {
|
||||
log('HistoryRepositoryImpl: transaksi tidak ditemukan di Firestore');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mengambil data dari dokumen
|
||||
final data = docSnapshot.data();
|
||||
if (data == null) {
|
||||
log('HistoryRepositoryImpl: data transaksi kosong');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pastikan id dokumen tersimpan dalam data
|
||||
data['id'] = transactionId;
|
||||
data['ticketId'] = ticketId;
|
||||
|
||||
// Konversi ke model TransactionModel
|
||||
final transaction = TransactionModel.fromJson(data);
|
||||
log('HistoryRepositoryImpl: berhasil mengambil transaksi dari Firestore');
|
||||
return transaction;
|
||||
} catch (e) {
|
||||
log('HistoryRepositoryImpl: error saat mengambil transaksi dari Firestore: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:get/get.dart';
|
||||
|
||||
import '../../data/repositories/history_repository_impl.dart';
|
||||
import '../../presentation/controllers/history_controller.dart';
|
||||
import '../repositories/history_repository.dart';
|
||||
import '../usecases/history_usecase.dart';
|
||||
|
||||
class HistoryBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<HistoryRepository>(
|
||||
() => HistoryRepositoryImpl(),
|
||||
);
|
||||
|
||||
// Inject UseCase
|
||||
Get.lazyPut<HistoryUseCase>(
|
||||
() => HistoryUseCase(Get.find<HistoryRepository>()),
|
||||
);
|
||||
|
||||
// Inject Controller
|
||||
Get.lazyPut<HistoryController>(
|
||||
() => HistoryController(Get.find<HistoryUseCase>()),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import '../models/transaction_model.dart';
|
||||
|
||||
abstract class HistoryRepository {
|
||||
Stream<List<TransactionModel>> getTransactionsStream(String userId, String status);
|
||||
Future<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:e_porter/domain/repositories/history_repository.dart';
|
||||
|
||||
import '../models/transaction_model.dart';
|
||||
|
||||
class HistoryUseCase {
|
||||
final HistoryRepository _repository;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
Future<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId) {
|
||||
return _repository.getTransactionFromFirestore(ticketId, transactionId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:e_porter/domain/usecases/history_usecase.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../data/repositories/transaction_repository_impl.dart';
|
||||
import '../../domain/models/transaction_model.dart';
|
||||
|
||||
class HistoryController extends GetxController {
|
||||
final HistoryUseCase _historyUseCase;
|
||||
|
||||
final RxList<TransactionModel> pendingTransactions = <TransactionModel>[].obs;
|
||||
final RxList<TransactionModel> activeTransactions = <TransactionModel>[].obs;
|
||||
final Rx<TransactionModel?> selectedTransaction = Rx<TransactionModel?>(null);
|
||||
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxBool isCheckingExpiry = false.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
|
||||
StreamSubscription? _pendingSubscription;
|
||||
StreamSubscription? _activeSubscription;
|
||||
|
||||
String _currentUserId = '';
|
||||
|
||||
HistoryController(this._historyUseCase);
|
||||
|
||||
void initStreams(String userId) {
|
||||
isLoading.value = true;
|
||||
_currentUserId = userId;
|
||||
log('HistoryController: initStreams dipanggil dengan userId: $userId');
|
||||
|
||||
_cleanupStreams();
|
||||
|
||||
_pendingSubscription = _historyUseCase.getPendingTransactionsStream(userId).listen((transactions) {
|
||||
log('HistoryController: pending transactions updated, count: ${transactions.length}');
|
||||
|
||||
final sortedTransactions = _sortTransactionsByCreatedAt(transactions);
|
||||
pendingTransactions.value = sortedTransactions;
|
||||
|
||||
isLoading.value = false;
|
||||
}, onError: (error) {
|
||||
log('HistoryController: Error mendapatkan transaksi pending: $error');
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
_activeSubscription = _historyUseCase.getActiveTransactionsStream(userId).listen((transactions) {
|
||||
log('HistoryController: active transactions updated, count: ${transactions.length}');
|
||||
|
||||
final sortedTransactions = _sortTransactionsByCreatedAt(transactions);
|
||||
activeTransactions.value = sortedTransactions;
|
||||
|
||||
isLoading.value = false;
|
||||
}, onError: (error) {
|
||||
log('HistoryController: Error mendapatkan transaksi active: $error');
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
List<TransactionModel> _sortTransactionsByCreatedAt(List<TransactionModel> transactions) {
|
||||
final sortedList = List<TransactionModel>.from(transactions);
|
||||
|
||||
sortedList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
log('HistoryController: transaksi telah diurutkan berdasarkan createdAt (descending)');
|
||||
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
Future<void> checkExpiredPendingTransactions() async {
|
||||
final now = DateTime.now();
|
||||
final expiredTransactions = pendingTransactions.where((tx) => tx.expiryTime.isBefore(now)).toList();
|
||||
|
||||
if (expiredTransactions.isNotEmpty) {
|
||||
log('HistoryController: Ditemukan ${expiredTransactions.length} transaksi kedaluwarsa');
|
||||
|
||||
isCheckingExpiry.value = true;
|
||||
|
||||
for (var transaction in expiredTransactions) {
|
||||
log('HistoryController: Menghapus transaksi kedaluwarsa ${transaction.id} dari daftar pending');
|
||||
pendingTransactions.removeWhere((tx) => tx.id == transaction.id);
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(seconds: 2));
|
||||
await Get.find<TransactionRepositoryImpl>().checkAndCancelExpiredTransactions();
|
||||
|
||||
if (_currentUserId.isNotEmpty) {
|
||||
await refreshTransactions(_currentUserId);
|
||||
}
|
||||
|
||||
isCheckingExpiry.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TransactionModel?> getTransactionFromFirestore(String ticketId, String transactionId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
log('HistoryController: mencoba mengambil transaksi dengan ticketId: $ticketId, transactionId: $transactionId');
|
||||
final transaction = await _historyUseCase.getTransactionFromFirestore(ticketId, transactionId);
|
||||
|
||||
if (transaction != null) {
|
||||
selectedTransaction.value = transaction;
|
||||
} else {
|
||||
errorMessage.value = 'Transaksi tidak ditemukan';
|
||||
}
|
||||
|
||||
return transaction;
|
||||
} catch (e) {
|
||||
log('HistoryController: error saat mengambil transaksi dari Firestore: $e');
|
||||
errorMessage.value = 'Terjadi kesalahan: $e';
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshTransactions(String userId) async {
|
||||
log('HistoryController: Memperbarui data transaksi untuk userId: $userId');
|
||||
|
||||
pendingTransactions.clear();
|
||||
activeTransactions.clear();
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
_cleanupStreams();
|
||||
initStreams(userId);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
String getUserId() {
|
||||
if (_pendingSubscription != null || _activeSubscription != null) {
|
||||
return _currentUserId;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
void _cleanupStreams() {
|
||||
_pendingSubscription?.cancel();
|
||||
_activeSubscription?.cancel();
|
||||
_pendingSubscription = null;
|
||||
_activeSubscription = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_cleanupStreams();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void clearSelectedTransaction() {
|
||||
selectedTransaction.value = null;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:developer';
|
||||
import 'package:e_porter/presentation/screens/boarding_pass/component/payment_count_down_timer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
@ -9,9 +11,61 @@ import '../../../../_core/constants/typography.dart';
|
|||
|
||||
class CardBoardingPass extends StatelessWidget {
|
||||
final bool isActive;
|
||||
final String idBooking;
|
||||
final String? opsiFlight;
|
||||
final DateTime? expiryTime;
|
||||
final String? airlines;
|
||||
final String? codeAirlines;
|
||||
final String? logo;
|
||||
final List<String>? servicePorter;
|
||||
final String? flightClass;
|
||||
final int? passenger;
|
||||
final String? departureCity;
|
||||
final String? arrivalCity;
|
||||
final String? departureCode;
|
||||
final String? arrivalCode;
|
||||
final String? departurePlane;
|
||||
final String? arrivalPlane;
|
||||
final String? transitPlane;
|
||||
final String? transitStartDate;
|
||||
final String? transitEndDate;
|
||||
final String? departureTime;
|
||||
final String? arrivalTime;
|
||||
final String? departureDate;
|
||||
final String? arrivalDate;
|
||||
final String? duration;
|
||||
final String? stop;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const CardBoardingPass({Key? key, required this.isActive, this.onTap});
|
||||
const CardBoardingPass({
|
||||
Key? key,
|
||||
required this.isActive,
|
||||
required this.idBooking,
|
||||
this.opsiFlight,
|
||||
this.expiryTime,
|
||||
this.airlines,
|
||||
this.codeAirlines,
|
||||
this.logo,
|
||||
this.servicePorter,
|
||||
this.flightClass,
|
||||
this.passenger,
|
||||
this.departureCity,
|
||||
this.arrivalCity,
|
||||
this.departureCode,
|
||||
this.arrivalCode,
|
||||
this.departurePlane,
|
||||
this.arrivalPlane,
|
||||
this.transitPlane,
|
||||
this.transitStartDate,
|
||||
this.transitEndDate,
|
||||
this.departureTime,
|
||||
this.arrivalTime,
|
||||
this.departureDate,
|
||||
this.arrivalDate,
|
||||
this.duration,
|
||||
this.stop,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -20,7 +74,7 @@ class CardBoardingPass extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeaderStatus(),
|
||||
_buildHeaderStatus(idBooking: idBooking, opsiFlight: opsiFlight),
|
||||
CustomeShadowCotainner(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10.r),
|
||||
|
@ -38,57 +92,61 @@ class CardBoardingPass extends StatelessWidget {
|
|||
TypographyStyles.body('Kode Booking',
|
||||
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
SizedBox(width: 20.w),
|
||||
TypographyStyles.body('I2L8JRL', color: GrayColors.gray800),
|
||||
TypographyStyles.body(idBooking, color: GrayColors.gray800),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: GrayColors.gray100,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(width: 1.w, color: GrayColors.gray200),
|
||||
),
|
||||
child: TypographyStyles.caption('Keberangkatan', color: GrayColors.gray800),
|
||||
),
|
||||
if (opsiFlight != null && opsiFlight!.isNotEmpty)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: GrayColors.gray100,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(width: 1.w, color: GrayColors.gray200),
|
||||
),
|
||||
child: TypographyStyles.caption(opsiFlight!, color: GrayColors.gray800),
|
||||
)
|
||||
else
|
||||
SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
if (!isActive)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: RedColors.red200,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TypographyStyles.caption("Batas Pembayaran",
|
||||
color: RedColors.red600, fontWeight: FontWeight.w400),
|
||||
TypographyStyles.caption("01:07:12", color: RedColors.red600)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isActive) PaymentCountdownTimer(expiryTime: expiryTime!),
|
||||
SizedBox(height: 22.h),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TypographyStyles.caption("Citilink (103)", color: GrayColors.gray800),
|
||||
TypographyStyles.caption("${airlines} (${codeAirlines})", color: GrayColors.gray800),
|
||||
SizedBox(width: 10.w),
|
||||
SvgPicture.asset('assets/images/citilink.svg', width: 40.w, height: 10.h),
|
||||
logo != null && logo!.isNotEmpty
|
||||
? Image.network(
|
||||
logo!,
|
||||
width: 40.w,
|
||||
height: 26.h,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
log("Error loading image: $error");
|
||||
return Container(
|
||||
width: 40.w,
|
||||
height: 10.h,
|
||||
child: Center(child: Icon(Icons.error)),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
width: 40.w,
|
||||
height: 10.h,
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 1.0)),
|
||||
);
|
||||
},
|
||||
)
|
||||
: SvgPicture.asset('assets/images/citilink.svg', width: 10.w, height: 10.h),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Row(
|
||||
children: [
|
||||
TypographyStyles.small(
|
||||
'Fast Track (FT)',
|
||||
color: GrayColors.gray600,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
_buildText(context, text: 'Economy'),
|
||||
_buildText(context, text: '2 Dewasa'),
|
||||
],
|
||||
Wrap(
|
||||
spacing: 4.w,
|
||||
runSpacing: 4.h,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: _buildInfoItems(),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
SvgPicture.asset('assets/images/divider_custome.svg', width: 348.w),
|
||||
|
@ -98,13 +156,13 @@ class CardBoardingPass extends StatelessWidget {
|
|||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
||||
TypographyStyles.small("Sen, 27 Jan", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
TypographyStyles.caption("$departureTime", color: GrayColors.gray800),
|
||||
TypographyStyles.small("$departureDate", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
SizedBox(height: 20.h),
|
||||
TypographyStyles.small("5j 40m", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
TypographyStyles.small("$duration", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
SizedBox(height: 20.h),
|
||||
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
||||
TypographyStyles.small("Sen, 27 Jan", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
TypographyStyles.caption("$arrivalTime", color: GrayColors.gray800),
|
||||
TypographyStyles.small("$arrivalDate", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
|
@ -114,18 +172,18 @@ class CardBoardingPass extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.caption("Yogyakarta (YIA)", color: GrayColors.gray800),
|
||||
TypographyStyles.caption("$departureCity ($departureCode)", color: GrayColors.gray800),
|
||||
TypographyStyles.caption(
|
||||
"Bandar YIA, Terminal Domestic",
|
||||
"$departurePlane",
|
||||
color: GrayColors.gray600,
|
||||
fontWeight: FontWeight.w400,
|
||||
maxlines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 58.h),
|
||||
TypographyStyles.caption("Yogyakarta (YIA)", color: GrayColors.gray800),
|
||||
TypographyStyles.caption("$arrivalCity ($arrivalCode)", color: GrayColors.gray800),
|
||||
TypographyStyles.caption(
|
||||
"Bandar Zainuddin Abdul Madjid, Terminal Domestic",
|
||||
"$arrivalPlane",
|
||||
color: GrayColors.gray600,
|
||||
fontWeight: FontWeight.w400,
|
||||
maxlines: 2,
|
||||
|
@ -145,7 +203,7 @@ class CardBoardingPass extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderStatus() {
|
||||
Widget _buildHeaderStatus({required String idBooking, required String? opsiFlight}) {
|
||||
final Color headerColor = isActive ? GreenColors.green100 : GrayColors.gray500;
|
||||
if (isActive) {
|
||||
return Container(
|
||||
|
@ -173,35 +231,62 @@ class CardBoardingPass extends StatelessWidget {
|
|||
children: [
|
||||
TypographyStyles.body('Kode Booking', color: Colors.white, fontWeight: FontWeight.w400),
|
||||
SizedBox(width: 20.w),
|
||||
TypographyStyles.body('I2L8JRL', color: Colors.white),
|
||||
TypographyStyles.body(idBooking, color: Colors.white),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8.r)),
|
||||
child: TypographyStyles.caption('Keberangkatan', color: GrayColors.gray800),
|
||||
)
|
||||
if (opsiFlight != null && opsiFlight.isNotEmpty)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8.r)),
|
||||
child: TypographyStyles.caption(opsiFlight, color: GrayColors.gray800),
|
||||
)
|
||||
else
|
||||
SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildText(
|
||||
BuildContext context, {
|
||||
required String text,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(width: 10.w),
|
||||
CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)),
|
||||
SizedBox(width: 10.w),
|
||||
TypographyStyles.small(
|
||||
text,
|
||||
List<Widget> _buildInfoItems() {
|
||||
List<Widget> items = [];
|
||||
|
||||
if (servicePorter != null && servicePorter!.isNotEmpty) {
|
||||
for (int i = 0; i < servicePorter!.length; i++) {
|
||||
items.add(TypographyStyles.small(
|
||||
servicePorter![i],
|
||||
color: GrayColors.gray600,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
);
|
||||
));
|
||||
|
||||
if (i < servicePorter!.length - 1 || flightClass != null) {
|
||||
items.add(SizedBox(width: 5.w));
|
||||
items.add(CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)));
|
||||
items.add(SizedBox(width: 5.w));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flightClass != null && flightClass!.isNotEmpty) {
|
||||
items.add(TypographyStyles.small(
|
||||
flightClass!,
|
||||
color: GrayColors.gray600,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: FontWeight.w400,
|
||||
));
|
||||
|
||||
items.add(SizedBox(width: 5.w));
|
||||
items.add(CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)));
|
||||
items.add(SizedBox(width: 5.w));
|
||||
}
|
||||
|
||||
items.add(TypographyStyles.small(
|
||||
"${passenger ?? 1} Dewasa",
|
||||
color: GrayColors.gray600,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: FontWeight.w400,
|
||||
));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:e_porter/_core/component/appbar/appbar_component.dart';
|
||||
import 'package:e_porter/_core/constants/colors.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/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';
|
||||
import '../component/card_boarding_pass.dart';
|
||||
|
||||
class BoardingPassScreen extends StatefulWidget {
|
||||
|
@ -16,19 +23,109 @@ class BoardingPassScreen extends StatefulWidget {
|
|||
|
||||
class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final HistoryController _historyController = Get.find<HistoryController>();
|
||||
|
||||
Timer? _refreshTimer;
|
||||
|
||||
bool isLoading = true;
|
||||
String userId = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_loadUserData();
|
||||
|
||||
_refreshTimer = Timer.periodic(Duration(seconds: 10), (_) {
|
||||
_checkExpiredTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _checkExpiredTransactions() async {
|
||||
if (userId.isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
final expiredTransactions =
|
||||
_historyController.pendingTransactions.where((tx) => tx.expiryTime.isBefore(now)).toList();
|
||||
|
||||
if (expiredTransactions.isNotEmpty) {
|
||||
await _historyController.checkExpiredPendingTransactions();
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
final userData = await PreferencesService.getUserData();
|
||||
if (userData != null) {
|
||||
userId = userData.uid;
|
||||
_historyController.initStreams(userId);
|
||||
} else {
|
||||
_historyController.isLoading.value = false;
|
||||
}
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
String? _getPorterServiceInfo(TransactionModel transaction) {
|
||||
if (transaction.porterServiceDetails == null || transaction.porterServiceDetails!.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final porterService = transaction.porterServiceDetails!;
|
||||
|
||||
final keys = porterService.keys.toList();
|
||||
if (keys.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
keys.sort();
|
||||
if (keys.length == 1) {
|
||||
final type = _getPorterTypeInIndonesian(keys.first);
|
||||
return type;
|
||||
}
|
||||
final firstType = _getPorterTypeInIndonesian(keys.first);
|
||||
return "$firstType & ${keys.length - 1}";
|
||||
}
|
||||
|
||||
String _getPorterTypeInIndonesian(String type) {
|
||||
switch (type) {
|
||||
case 'arrival':
|
||||
return 'Kedatangan';
|
||||
case 'departure':
|
||||
return 'Keberangkatan';
|
||||
case 'transit':
|
||||
return 'Transit';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _getPorterServiceNames(TransactionModel transaction) {
|
||||
List<String> serviceNames = [];
|
||||
|
||||
if (transaction.porterServiceDetails == null || transaction.porterServiceDetails!.isEmpty) {
|
||||
return serviceNames;
|
||||
}
|
||||
|
||||
transaction.porterServiceDetails!.forEach((serviceType, serviceDetails) {
|
||||
if (serviceDetails is Map && serviceDetails.containsKey('name')) {
|
||||
String serviceName = serviceDetails['name'] as String? ?? '';
|
||||
if (serviceName.isNotEmpty) {
|
||||
serviceNames.add(serviceName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return serviceNames;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -56,40 +153,63 @@ class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTick
|
|||
],
|
||||
),
|
||||
),
|
||||
Obx(() => _historyController.isCheckingExpiry.value
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
color: Colors.amber.shade100,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.amber.shade800),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
"Memeriksa status transaksi...",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.amber.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink()),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
ListView.builder(
|
||||
itemCount: 1,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: CardBoardingPass(
|
||||
isActive: false,
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListView.builder(
|
||||
itemCount: 2,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: CardBoardingPass(
|
||||
isActive: true,
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.DETAILTICKET);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Obx(() {
|
||||
if (_historyController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_historyController.pendingTransactions.isEmpty && _historyController.activeTransactions.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Tidak ada transaksi",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: GrayColors.gray500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildTransactionList(_historyController.pendingTransactions, false),
|
||||
_buildTransactionList(_historyController.activeTransactions, true),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -97,4 +217,85 @@ class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTick
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionList(List<TransactionModel> transactions, bool isActive) {
|
||||
if (transactions.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Tidak ada transaksi",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: GrayColors.gray500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _historyController.refreshTransactions(userId);
|
||||
_historyController.checkExpiredPendingTransactions();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: transactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final transaction = transactions[index];
|
||||
|
||||
final flightDetails = transaction.flightDetails;
|
||||
final bandaraDetails = transaction.bandaraDetails;
|
||||
|
||||
final porterServiceInfo = _getPorterServiceInfo(transaction);
|
||||
final porterServiceNames = _getPorterServiceNames(transaction);
|
||||
|
||||
final departureTime = DateFormatterHelper.formatFlightTime(transaction.flightDetails['departureTime']);
|
||||
final arrivalTime = DateFormatterHelper.formatFlightTime(transaction.flightDetails['arrivalTime']);
|
||||
final departureDate = DateFormatterHelper.formatFlightDate(transaction.flightDetails['departureTime']);
|
||||
final arrivalDate = DateFormatterHelper.formatFlightDate(transaction.flightDetails['arrivalTime']);
|
||||
final duration = DateFormatterHelper.calculateFlightDuration(
|
||||
transaction.flightDetails['departureTime'],
|
||||
transaction.flightDetails['arrivalTime'],
|
||||
);
|
||||
|
||||
final departurePlane = MapHelper.getNestedValue(bandaraDetails, ['departure', 'name'], '');
|
||||
final arrivalPlane = MapHelper.getNestedValue(bandaraDetails, ['arrival', 'name'], '');
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: CardBoardingPass(
|
||||
isActive: isActive,
|
||||
idBooking: transaction.idBooking,
|
||||
opsiFlight: porterServiceInfo,
|
||||
expiryTime: transaction.expiryTime,
|
||||
airlines: flightDetails['airLines'],
|
||||
codeAirlines: flightDetails['code'],
|
||||
logo: flightDetails['airlineLogo'],
|
||||
servicePorter: porterServiceNames,
|
||||
flightClass: flightDetails['flightClass'],
|
||||
passenger: transaction.passenger,
|
||||
departureTime: departureTime,
|
||||
arrivalTime: arrivalTime,
|
||||
departureDate: departureDate,
|
||||
arrivalDate: arrivalDate,
|
||||
duration: duration,
|
||||
departureCity: flightDetails['cityDeparture'],
|
||||
arrivalCity: flightDetails['cityArrival'],
|
||||
departureCode: flightDetails['codeDeparture'],
|
||||
arrivalCode: flightDetails['codeArrival'],
|
||||
departurePlane: departurePlane,
|
||||
arrivalPlane: arrivalPlane,
|
||||
onTap: () {
|
||||
final argument = {
|
||||
'id_transaction': transaction.id,
|
||||
'id_ticket': transaction.ticketId,
|
||||
};
|
||||
log('ID Transaction: ${transaction.id}');
|
||||
log('ID Ticket: ${transaction.ticketId}');
|
||||
Get.toNamed(Routes.DETAILTICKET, arguments: argument);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import firebase_auth
|
|||
import firebase_core
|
||||
import firebase_database
|
||||
import firebase_storage
|
||||
import mobile_scanner
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
|
||||
|
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
||||
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
|
|
@ -448,6 +448,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
mobile_scanner:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mobile_scanner
|
||||
sha256: f536c5b8cadcf73d764bdce09c94744f06aa832264730f8971b21a60c5666826
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.10"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -59,6 +59,7 @@ dependencies:
|
|||
path: ^1.9.0
|
||||
permission_handler: ^11.4.0
|
||||
device_info_plus: ^11.3.0
|
||||
mobile_scanner: ^6.0.10
|
||||
# workmanager: ^0.5.2
|
||||
|
||||
# pin_code_fields: ^8.0.1
|
||||
|
|
Loading…
Reference in New Issue