Feat: add logic history boarding pass
This commit is contained in:
parent
e8763bb85f
commit
d565bb59fa
|
@ -343,6 +343,12 @@
|
||||||
"packageUri": "lib/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "2.12"
|
"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",
|
"name": "path",
|
||||||
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.0",
|
"rootUri": "file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.0",
|
||||||
|
@ -632,7 +638,7 @@
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.4"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generated": "2025-04-08T08:36:36.076309Z",
|
"generated": "2025-04-22T06:48:38.993021Z",
|
||||||
"generator": "pub",
|
"generator": "pub",
|
||||||
"generatorVersion": "3.5.0",
|
"generatorVersion": "3.5.0",
|
||||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
||||||
|
|
|
@ -214,6 +214,10 @@ meta
|
||||||
2.12
|
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/
|
||||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.15.0/lib/
|
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
|
path
|
||||||
3.0
|
3.0
|
||||||
file:///C:/Users/ASUS/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.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">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="34" /> -->
|
android:maxSdkVersion="34" /> -->
|
||||||
<uses-permission android:name="android.permission.READ_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.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_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
|
<application
|
||||||
android:label="e_porter"
|
android:label="e_porter"
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
@ -9,9 +11,61 @@ import '../../../../_core/constants/typography.dart';
|
||||||
|
|
||||||
class CardBoardingPass extends StatelessWidget {
|
class CardBoardingPass extends StatelessWidget {
|
||||||
final bool isActive;
|
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;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -20,7 +74,7 @@ class CardBoardingPass extends StatelessWidget {
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildHeaderStatus(),
|
_buildHeaderStatus(idBooking: idBooking, opsiFlight: opsiFlight),
|
||||||
CustomeShadowCotainner(
|
CustomeShadowCotainner(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(10.r),
|
bottomLeft: Radius.circular(10.r),
|
||||||
|
@ -38,9 +92,10 @@ class CardBoardingPass extends StatelessWidget {
|
||||||
TypographyStyles.body('Kode Booking',
|
TypographyStyles.body('Kode Booking',
|
||||||
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
SizedBox(width: 20.w),
|
SizedBox(width: 20.w),
|
||||||
TypographyStyles.body('I2L8JRL', color: GrayColors.gray800),
|
TypographyStyles.body(idBooking, color: GrayColors.gray800),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (opsiFlight != null && opsiFlight!.isNotEmpty)
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -48,47 +103,50 @@ class CardBoardingPass extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
borderRadius: BorderRadius.circular(8.r),
|
||||||
border: Border.all(width: 1.w, color: GrayColors.gray200),
|
border: Border.all(width: 1.w, color: GrayColors.gray200),
|
||||||
),
|
),
|
||||||
child: TypographyStyles.caption('Keberangkatan', color: GrayColors.gray800),
|
child: TypographyStyles.caption(opsiFlight!, color: GrayColors.gray800),
|
||||||
),
|
)
|
||||||
|
else
|
||||||
|
SizedBox.shrink(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!isActive)
|
if (!isActive) PaymentCountdownTimer(expiryTime: expiryTime!),
|
||||||
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)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 22.h),
|
SizedBox(height: 22.h),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.caption("Citilink (103)", color: GrayColors.gray800),
|
TypographyStyles.caption("${airlines} (${codeAirlines})", color: GrayColors.gray800),
|
||||||
SizedBox(width: 10.w),
|
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),
|
SizedBox(height: 4.w),
|
||||||
Row(
|
Wrap(
|
||||||
children: [
|
spacing: 4.w,
|
||||||
TypographyStyles.small(
|
runSpacing: 4.h,
|
||||||
'Fast Track (FT)',
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
color: GrayColors.gray600,
|
children: _buildInfoItems(),
|
||||||
letterSpacing: 0.2,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
_buildText(context, text: 'Economy'),
|
|
||||||
_buildText(context, text: '2 Dewasa'),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
SvgPicture.asset('assets/images/divider_custome.svg', width: 348.w),
|
SvgPicture.asset('assets/images/divider_custome.svg', width: 348.w),
|
||||||
|
@ -98,13 +156,13 @@ class CardBoardingPass extends StatelessWidget {
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
TypographyStyles.caption("$departureTime", color: GrayColors.gray800),
|
||||||
TypographyStyles.small("Sen, 27 Jan", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
TypographyStyles.small("$departureDate", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
SizedBox(height: 20.h),
|
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),
|
SizedBox(height: 20.h),
|
||||||
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
TypographyStyles.caption("$arrivalTime", color: GrayColors.gray800),
|
||||||
TypographyStyles.small("Sen, 27 Jan", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
TypographyStyles.small("$arrivalDate", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 20.w),
|
SizedBox(width: 20.w),
|
||||||
|
@ -114,18 +172,18 @@ class CardBoardingPass extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.caption("Yogyakarta (YIA)", color: GrayColors.gray800),
|
TypographyStyles.caption("$departureCity ($departureCode)", color: GrayColors.gray800),
|
||||||
TypographyStyles.caption(
|
TypographyStyles.caption(
|
||||||
"Bandar YIA, Terminal Domestic",
|
"$departurePlane",
|
||||||
color: GrayColors.gray600,
|
color: GrayColors.gray600,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
maxlines: 2,
|
maxlines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
SizedBox(height: 58.h),
|
SizedBox(height: 58.h),
|
||||||
TypographyStyles.caption("Yogyakarta (YIA)", color: GrayColors.gray800),
|
TypographyStyles.caption("$arrivalCity ($arrivalCode)", color: GrayColors.gray800),
|
||||||
TypographyStyles.caption(
|
TypographyStyles.caption(
|
||||||
"Bandar Zainuddin Abdul Madjid, Terminal Domestic",
|
"$arrivalPlane",
|
||||||
color: GrayColors.gray600,
|
color: GrayColors.gray600,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
maxlines: 2,
|
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;
|
final Color headerColor = isActive ? GreenColors.green100 : GrayColors.gray500;
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return Container(
|
return Container(
|
||||||
|
@ -173,35 +231,62 @@ class CardBoardingPass extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.body('Kode Booking', color: Colors.white, fontWeight: FontWeight.w400),
|
TypographyStyles.body('Kode Booking', color: Colors.white, fontWeight: FontWeight.w400),
|
||||||
SizedBox(width: 20.w),
|
SizedBox(width: 20.w),
|
||||||
TypographyStyles.body('I2L8JRL', color: Colors.white),
|
TypographyStyles.body(idBooking, color: Colors.white),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (opsiFlight != null && opsiFlight.isNotEmpty)
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8.r)),
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8.r)),
|
||||||
child: TypographyStyles.caption('Keberangkatan', color: GrayColors.gray800),
|
child: TypographyStyles.caption(opsiFlight, color: GrayColors.gray800),
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
SizedBox.shrink()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildText(
|
List<Widget> _buildInfoItems() {
|
||||||
BuildContext context, {
|
List<Widget> items = [];
|
||||||
required String text,
|
|
||||||
}) {
|
if (servicePorter != null && servicePorter!.isNotEmpty) {
|
||||||
return Row(
|
for (int i = 0; i < servicePorter!.length; i++) {
|
||||||
children: [
|
items.add(TypographyStyles.small(
|
||||||
SizedBox(width: 10.w),
|
servicePorter![i],
|
||||||
CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)),
|
|
||||||
SizedBox(width: 10.w),
|
|
||||||
TypographyStyles.small(
|
|
||||||
text,
|
|
||||||
color: GrayColors.gray600,
|
color: GrayColors.gray600,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
fontWeight: FontWeight.w400,
|
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/component/appbar/appbar_component.dart';
|
||||||
import 'package:e_porter/_core/constants/colors.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:e_porter/presentation/screens/routes/app_rountes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.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';
|
import '../component/card_boarding_pass.dart';
|
||||||
|
|
||||||
class BoardingPassScreen extends StatefulWidget {
|
class BoardingPassScreen extends StatefulWidget {
|
||||||
|
@ -16,19 +23,109 @@ class BoardingPassScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTickerProviderStateMixin {
|
class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
|
final HistoryController _historyController = Get.find<HistoryController>();
|
||||||
|
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
|
||||||
|
bool isLoading = true;
|
||||||
|
String userId = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
|
_loadUserData();
|
||||||
|
|
||||||
|
_refreshTimer = Timer.periodic(Duration(seconds: 10), (_) {
|
||||||
|
_checkExpiredTransactions();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
super.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -56,45 +153,149 @@ class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTick
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Obx(() => _historyController.isCheckingExpiry.value
|
||||||
child: Padding(
|
? Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
width: double.infinity,
|
||||||
child: TabBarView(
|
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||||
controller: _tabController,
|
color: Colors.amber.shade100,
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListView.builder(
|
SizedBox(
|
||||||
itemCount: 1,
|
width: 16.w,
|
||||||
itemBuilder: (context, index) {
|
height: 16.h,
|
||||||
return Padding(
|
child: CircularProgressIndicator(
|
||||||
padding: EdgeInsets.only(bottom: 16.h),
|
strokeWidth: 2,
|
||||||
child: CardBoardingPass(
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.amber.shade800),
|
||||||
isActive: false,
|
|
||||||
onTap: () {},
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListView.builder(
|
SizedBox(width: 8.w),
|
||||||
itemCount: 2,
|
Text(
|
||||||
itemBuilder: (context, index) {
|
"Memeriksa status transaksi...",
|
||||||
return Padding(
|
style: TextStyle(
|
||||||
padding: EdgeInsets.only(bottom: 16.h),
|
fontSize: 12.sp,
|
||||||
child: CardBoardingPass(
|
color: Colors.amber.shade800,
|
||||||
isActive: true,
|
|
||||||
onTap: () {
|
|
||||||
Get.toNamed(Routes.DETAILTICKET);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: SizedBox.shrink()),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_core
|
||||||
import firebase_database
|
import firebase_database
|
||||||
import firebase_storage
|
import firebase_storage
|
||||||
|
import mobile_scanner
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
||||||
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
|
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
|
||||||
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -448,6 +448,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
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:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -59,6 +59,7 @@ dependencies:
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
permission_handler: ^11.4.0
|
permission_handler: ^11.4.0
|
||||||
device_info_plus: ^11.3.0
|
device_info_plus: ^11.3.0
|
||||||
|
mobile_scanner: ^6.0.10
|
||||||
# workmanager: ^0.5.2
|
# workmanager: ^0.5.2
|
||||||
|
|
||||||
# pin_code_fields: ^8.0.1
|
# pin_code_fields: ^8.0.1
|
||||||
|
|
Loading…
Reference in New Issue