Feat: completed the implementation of transaction history for porter access rights

This commit is contained in:
orangdeso 2025-04-27 23:26:00 +07:00
parent b1f9d95907
commit 0b381d2571
10 changed files with 2114 additions and 0 deletions

View File

@ -0,0 +1,492 @@
import 'dart:async';
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_database/firebase_database.dart';
import '../../domain/models/transaction_porter_model.dart';
import '../../domain/repositories/transaction_porter_repository.dart';
class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
final FirebaseDatabase _database;
final FirebaseFirestore _firestore;
TransactionPorterRepositoryImpl({FirebaseDatabase? database, FirebaseFirestore? firestore})
: _database = database ?? FirebaseDatabase.instance,
_firestore = firestore ?? FirebaseFirestore.instance;
@override
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId) {
log('[TransactionPorterRepo] Memulai streaming transaksi porter: $porterId');
// Dapatkan dulu porterOnline document untuk mendapatkan userId
return _firestore.collection('porterOnline').doc(porterId).snapshots().asyncMap((snapshot) async {
String userId = '';
if (snapshot.exists && snapshot.data() != null) {
userId = snapshot.data()!['userId'] ?? '';
log('[TransactionPorterRepo] Ditemukan userId: $userId untuk porterId: $porterId');
} else {
// Jika porterOnline tidak ditemukan, gunakan porterId sebagai userId (fallback)
userId = porterId;
log('[TransactionPorterRepo] Porter tidak ditemukan di porterOnline, menggunakan porterId sebagai userId: $userId');
}
// Gabungkan transaksi dari kedua sumber
List<PorterTransactionModel> allTransactions = [];
try {
// 1. Ambil dari porterHistory berdasarkan userId
if (userId.isNotEmpty) {
log('[TransactionPorterRepo] Mengambil transaksi dari porterHistory/$userId');
final snapshot = await _database.ref().child('porterHistory/$userId').get();
if (snapshot.exists && snapshot.value != null) {
final Map<dynamic, dynamic> historyData = snapshot.value as Map<dynamic, dynamic>;
for (final entry in historyData.entries) {
final transactionId = entry.key.toString();
log('[TransactionPorterRepo] Memproses transactionId: $transactionId');
final txData = await getPorterTransactionById(transactionId);
if (txData != null) {
log('[TransactionPorterRepo] Transaksi ditemukan, menambahkan ke daftar');
allTransactions.add(PorterTransactionModel.fromJson(txData, transactionId));
} else {
log('[TransactionPorterRepo] Data transaksi tidak ditemukan untuk ID: $transactionId');
}
}
log('[TransactionPorterRepo] Total ${allTransactions.length} transaksi dari porterHistory/$userId');
} else {
log('[TransactionPorterRepo] Tidak ada data di porterHistory/$userId');
}
}
// 2. Juga ambil dari porterHistory berdasarkan porterId jika berbeda dari userId
if (porterId != userId && porterId.isNotEmpty) {
log('[TransactionPorterRepo] Mengambil transaksi dari porterHistory/$porterId');
final snapshot = await _database.ref().child('porterHistory/$porterId').get();
if (snapshot.exists && snapshot.value != null) {
final Map<dynamic, dynamic> historyData = snapshot.value as Map<dynamic, dynamic>;
for (final entry in historyData.entries) {
final transactionId = entry.key.toString();
// Cek apakah transaksi sudah ada di allTransactions
if (!allTransactions.any((tx) => tx.id == transactionId)) {
final txData = await getPorterTransactionById(transactionId);
if (txData != null) {
allTransactions.add(PorterTransactionModel.fromJson(txData, transactionId));
}
}
}
log('[TransactionPorterRepo] Menambahkan transaksi dari porterHistory/$porterId');
} else {
log('[TransactionPorterRepo] Tidak ada data di porterHistory/$porterId');
}
}
// 3. Cari transaksi yang memiliki porterUserId yang cocok dengan userId
final List<String> additionalTransactionIds = [];
final snapshotAll = await _database.ref().child('porterTransactions').get();
if (snapshotAll.exists && snapshotAll.value != null) {
final allTransactionsData = snapshotAll.value as Map<dynamic, dynamic>;
allTransactionsData.forEach((key, value) {
final transactionData = value as Map<dynamic, dynamic>;
if (transactionData.containsKey('porterUserId') && transactionData['porterUserId'] == userId) {
additionalTransactionIds.add(key.toString());
}
});
log('[TransactionPorterRepo] Menemukan ${additionalTransactionIds.length} transaksi tambahan dengan porterUserId: $userId');
for (final id in additionalTransactionIds) {
if (!allTransactions.any((tx) => tx.id == id)) {
final txData = await getPorterTransactionById(id);
if (txData != null) {
allTransactions.add(PorterTransactionModel.fromJson(txData, id));
}
}
}
}
// Sort by date (newest first)
allTransactions.sort((a, b) => b.createdAt.compareTo(a.createdAt));
log('[TransactionPorterRepo] Total ${allTransactions.length} transaksi gabungan berhasil dimuat');
return allTransactions;
} catch (e) {
log('[TransactionPorterRepo] Error saat memproses data riwayat porter: $e');
return <PorterTransactionModel>[];
}
});
}
@override
Stream<PorterTransactionModel?> watchTransactionById(String transactionId) {
log('[TransactionPorterRepo] Memantau transaksi dengan ID: $transactionId');
// Buat controller stream untuk mengirimkan update
final controller = StreamController<PorterTransactionModel?>();
// Buat subscription terhadap node porterTransactions/{transactionId}
final subscription = _database.ref().child('porterTransactions/$transactionId').onValue.listen((event) {
if (event.snapshot.exists && event.snapshot.value != null) {
try {
final data = Map<String, dynamic>.from(event.snapshot.value as Map);
final txModel = PorterTransactionModel.fromJson(data, transactionId);
controller.add(txModel);
log('[TransactionPorterRepo] Transaksi diperbarui: $transactionId, status: ${txModel.status}');
} catch (e) {
log('[TransactionPorterRepo] Error mengubah data: $e');
controller.addError(e);
}
} else {
controller.add(null);
}
}, onError: (e) {
log('[TransactionPorterRepo] Error streaming transaksi: $e');
controller.addError(e);
});
// Tutup controller dan batalkan subscription ketika stream ditutup
controller.onCancel = () {
subscription.cancel();
controller.close();
};
return controller.stream;
}
// @override
// Future<PorterTransactionModel?> getTransactionById(String transactionId) async {
// try {
// log('Fetching transaction by ID from Firestore: $transactionId');
// // Coba cari langsung dengan transactionId di Firestore
// final docSnapshot = await _firestore.collection('porterTransactions').doc(transactionId).get();
// if (docSnapshot.exists) {
// log('Transaction found in Firestore with data: ${docSnapshot.data()}');
// final data = docSnapshot.data();
// if (data != null) {
// return PorterTransactionModel.fromJson(data, transactionId);
// }
// } else {
// log('Transaction not found in Firestore with direct ID: $transactionId');
// }
// // Jika tidak ada di Firestore, coba cari dengan format "ticketId-transactionId"
// if (transactionId.contains('-')) {
// log('Trying with direct ID format: $transactionId');
// final combinedDocSnapshot = await _firestore.collection('porterTransactions').doc(transactionId).get();
// if (combinedDocSnapshot.exists) {
// log('Transaction found in Firestore with combined ID');
// final data = combinedDocSnapshot.data();
// if (data != null) {
// return PorterTransactionModel.fromJson(data, transactionId);
// }
// }
// }
// log('Transaction not found in Firestore with any ID format. Final attempt with Realtime DB...');
// // Mencoba cari di semua porterTransactions nodes di Realtime DB (sebagai fallback)
// // Ini inefficient, tapi bisa membantu menemukan data jika struktur tidak konsisten
// final dbRef = _database.ref().child('porterTransactions');
// final dataSnapshot = await dbRef.get();
// if (dataSnapshot.exists && dataSnapshot.value is Map) {
// final allData = dataSnapshot.value as Map<dynamic, dynamic>;
// // Iterasi semua porter IDs
// for (var porterId in allData.keys) {
// final porterData = allData[porterId];
// if (porterData is Map<dynamic, dynamic> && porterData.containsKey(transactionId)) {
// log('Transaction found in Realtime DB under porter: $porterId');
// final transactionData = porterData[transactionId] as Map<dynamic, dynamic>;
// return PorterTransactionModel.fromJson(transactionData, transactionId);
// }
// }
// }
// log('Transaction not found with ID: $transactionId in any database');
// return null;
// } catch (e) {
// log('Error fetching transaction by ID: $e');
// return null;
// }
// }
@override
Future<PorterTransactionModel?> getTransactionById(String transactionId) async {
try {
final txData = await getPorterTransactionById(transactionId);
if (txData != null) {
return PorterTransactionModel.fromJson(txData, transactionId);
}
return null;
} catch (e) {
log('Error mengambil transaksi porter: $e');
return null;
}
}
@override
Future<List<String>> getPorterTransactionIds(String userId) async {
try {
log('[TransactionPorterRepo] Mengambil ID transaksi untuk: $userId');
List<String> allTransactionIds = [];
// 1. Coba ambil dari porterHistory berdasarkan userId
final snapshot = await _database.ref().child('porterHistory/$userId').get();
if (snapshot.exists && snapshot.value != null) {
final Map<dynamic, dynamic> data = snapshot.value as Map<dynamic, dynamic>;
allTransactionIds.addAll(data.keys.map((key) => key.toString()));
log('[TransactionPorterRepo] Ditemukan ${allTransactionIds.length} transaksi dari porterHistory/$userId');
} else {
log('[TransactionPorterRepo] Tidak ada data di porterHistory/$userId');
}
// 2. Cari transaksi yang memiliki porterUserId yang cocok
final transactionsSnapshot = await _database.ref().child('porterTransactions').get();
if (transactionsSnapshot.exists && transactionsSnapshot.value != null) {
final allTransactions = transactionsSnapshot.value as Map<dynamic, dynamic>;
// Cari transaksi dengan porterUserId yang cocok
for (var key in allTransactions.keys) {
final value = allTransactions[key];
if (value is Map && value.containsKey('porterUserId') && value['porterUserId'] == userId) {
final id = key.toString();
if (!allTransactionIds.contains(id)) {
allTransactionIds.add(id);
}
}
}
log('[TransactionPorterRepo] Menambahkan transaksi berdasarkan porterUserId, total: ${allTransactionIds.length}');
}
return allTransactionIds;
} catch (e) {
log('[TransactionPorterRepo] Error mendapatkan ID transaksi: $e');
return [];
}
}
@override
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) async {
try {
log('[TransactionPorterRepo] Mengambil data transaksi untuk ID: $transactionId');
// Ambil langsung dari node porterTransactions/{transactionId}
final snapshot = await _database.ref().child('porterTransactions/$transactionId').get();
if (snapshot.exists && snapshot.value != null) {
final result = Map<String, dynamic>.from(snapshot.value as Map);
log('[TransactionPorterRepo] Data transaksi ditemukan untuk ID: $transactionId');
return result;
}
log('[TransactionPorterRepo] Data transaksi TIDAK ditemukan untuk ID: $transactionId');
// Jika tidak ditemukan di lokasi langsung, coba cari di semua transaksi
final allTransactions = await _database.ref().child('porterTransactions').get();
if (allTransactions.exists && allTransactions.value != null) {
final Map<dynamic, dynamic> allData = allTransactions.value as Map<dynamic, dynamic>;
// Transactionid mungkin tersimpan sebagai child dari porter ID lain
for (var key in allData.keys) {
if (key.toString() == transactionId) {
final result = Map<String, dynamic>.from(allData[key] as Map);
log('[TransactionPorterRepo] Data transaksi ditemukan di level atas');
return result;
}
}
}
return null;
} catch (e) {
log('[TransactionPorterRepo] Error mengambil data transaksi porter: $e');
return null;
}
}
@override
Future<void> updateTransactionStatus({
required String transactionId,
required String status,
}) async {
try {
final now = DateTime.now();
// Update di Firestore
await _firestore.collection('porterTransactions').doc(transactionId).update({
'status': status,
'updatedAt': now,
});
// Update di Realtime DB
await _database.ref().child('porterTransactions/$transactionId').update({
'status': status,
'updatedAt': now.millisecondsSinceEpoch,
});
log('Berhasil memperbarui status transaksi: $transactionId menjadi $status');
} catch (e) {
log('Error memperbarui status transaksi: $e');
throw Exception('Gagal memperbarui status transaksi: $e');
}
}
@override
Future<void> completePorterTransaction({
required String transactionId,
required String porterOnlineId,
}) async {
try {
final now = DateTime.now();
// Update status transaksi
await updateTransactionStatus(
transactionId: transactionId,
status: 'selesai',
);
// Reset status porter
await _firestore.collection('porterOnline').doc(porterOnlineId).update({
'idTransaction': null,
'idUser': null,
'isAvailable': true,
'onlineAt': now,
});
log('Transaksi porter berhasil diselesaikan');
} catch (e) {
log('Error menyelesaikan transaksi porter: $e');
throw Exception('Gagal menyelesaikan transaksi porter: $e');
}
}
// @override
// Future<void> updateTransactionStatus({
// required String ticketId,
// required String transactionId,
// required String status,
// }) async {
// try {
// final now = DateTime.now();
// log('Updating transaction status: $transactionId to $status');
// // Update di Firestore, coba dengan transactionId langsung
// try {
// await _firestore.collection('porterTransactions').doc(transactionId).update({
// 'status': status,
// 'updatedAt': now,
// });
// log('Successfully updated transaction in Firestore with direct ID');
// } catch (e) {
// log('Failed to update Firestore with direct ID: $e');
// // Coba dengan format ticketId-transactionId jika direct ID gagal
// if (ticketId.isNotEmpty) {
// final combinedId = '$ticketId-$transactionId';
// log('Trying update with combined ID: $combinedId');
// try {
// await _firestore.collection('porterTransactions').doc(combinedId).update({
// 'status': status,
// 'updatedAt': now,
// });
// log('Successfully updated transaction in Firestore with combined ID');
// } catch (e2) {
// log('Failed to update Firestore with combined ID: $e2');
// }
// }
// }
// // Mencari transaksi di Realtime DB
// // Perlu memeriksa semua porter untuk menemukan transaksi yang tepat
// final dbRef = _database.ref().child('porterTransactions');
// final dataSnapshot = await dbRef.get();
// if (dataSnapshot.exists && dataSnapshot.value is Map) {
// final allData = dataSnapshot.value as Map<dynamic, dynamic>;
// // Iterasi semua porter IDs
// for (var porterId in allData.keys) {
// final porterData = allData[porterId];
// if (porterData is Map<dynamic, dynamic> && porterData.containsKey(transactionId)) {
// log('Found transaction in Realtime DB under porter: $porterId');
// // Update status di Realtime DB
// final transactionRef =
// _database.ref().child('porterTransactions').child(porterId.toString()).child(transactionId);
// await transactionRef.update({
// 'status': status,
// 'updatedAt': now.millisecondsSinceEpoch,
// });
// log('Successfully updated transaction status in Realtime DB');
// break;
// }
// }
// }
// log('Status update process completed for transaction: $transactionId');
// } catch (e) {
// log('Error updating transaction status: $e');
// throw Exception('Failed to update transaction status: $e');
// }
// }
// @override
// Future<void> completePorterTransaction({
// required String ticketId,
// required String transactionId,
// required String porterOnlineId,
// }) async {
// try {
// final now = DateTime.now();
// log('Completing porter transaction: $transactionId and updating porterOnline: $porterOnlineId');
// await updateTransactionStatus(
// ticketId: ticketId,
// transactionId: transactionId,
// status: 'selesai',
// );
// try {
// await _firestore.collection('porterOnline').doc(porterOnlineId).update({
// 'idTransaction': null,
// 'idUser': null,
// 'isAvailable': true,
// 'onlineAt': now,
// });
// log('Successfully updated porterOnline data in Firestore');
// } catch (e) {
// log('Error updating porterOnline data in Firestore: $e');
// throw Exception('Gagal mengupdate data porterOnline di Firestore: $e');
// }
// // try {
// // final porterOnlineRef = _database.ref().child('porterOnline').child(porterOnlineId);
// // await porterOnlineRef.update({
// // 'idTransaction': null,
// // 'idUser': null,
// // 'isAvailable': true,
// // 'onlineAt': now.millisecondsSinceEpoch,
// // });
// // log('Successfully updated porterOnline data in Realtime DB');
// // } catch (e) {
// // log('Error updating porterOnline data in Realtime DB: $e');
// // }
// log('Porter transaction completion process finished successfully');
// } catch (e) {
// log('Error completing porter transaction: $e');
// throw Exception('Gagal menyelesaikan transaksi porter: $e');
// }
// }
}

View File

@ -0,0 +1,24 @@
import 'package:get/get.dart';
import '../../domain/repositories/transaction_porter_repository.dart';
import '../../domain/usecases/transaction_porter_usecase.dart';
import '../../data/repositories/transaction_porter_repository_impl.dart';
import '../../presentation/controllers/transaction_porter_controller.dart';
class TransactionPorterBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TransactionPorterRepository>(
() => TransactionPorterRepositoryImpl(),
);
// UseCase
Get.lazyPut<TransactionPorterUsecase>(
() => TransactionPorterUsecase(Get.find<TransactionPorterRepository>()),
);
// Controller
Get.lazyPut<TransactionPorterController>(
() => TransactionPorterController(Get.find<TransactionPorterUsecase>()),
);
}
}

View File

@ -0,0 +1,74 @@
// domain/models/transaction_porter_model.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class PorterTransactionModel {
final String id;
final String kodePorter;
final String porterUserId;
final String idPassenger;
final String locationPassenger;
final String locationPorter;
final String porterOnlineId;
final String status;
final String normalizedStatus;
final String ticketId;
final String transactionId;
final DateTime createdAt;
final DateTime? updatedAt;
PorterTransactionModel({
required this.id,
required this.kodePorter,
this.porterUserId = '',
required this.idPassenger,
required this.locationPassenger,
required this.locationPorter,
required this.porterOnlineId,
required this.status,
required this.ticketId,
required this.transactionId,
required this.createdAt,
this.updatedAt,
}) : normalizedStatus = _normalizeStatus(status);
// Simplify status normalization since we only have 3 possible statuses
static String _normalizeStatus(String status) {
final lowercaseStatus = status.toLowerCase().trim();
if (lowercaseStatus == "pending") return "pending";
if (lowercaseStatus == "proses") return "proses";
if (lowercaseStatus == "selesai") return "selesai";
// Default fallback logic for potentially inconsistent data
if (lowercaseStatus.contains("pend")) return "pending";
if (lowercaseStatus.contains("pros")) return "proses";
if (lowercaseStatus.contains("sele") || lowercaseStatus.contains("done")) return "selesai";
return "pending"; // Default to pending if unknown status
}
factory PorterTransactionModel.fromJson(Map<dynamic, dynamic> json, String id) {
return PorterTransactionModel(
id: id,
kodePorter: json['kodePorter'] ?? '',
porterUserId: json['porterUserId'] ?? '',
idPassenger: json['idPassenger'] as String? ?? '',
locationPassenger: json['locationPassenger'] as String? ?? '',
locationPorter: json['locationPorter'] as String? ?? '',
porterOnlineId: json['porterOnlineId'] as String? ?? '',
status: json['status'] as String? ?? 'Data Not Fount',
ticketId: json['ticketId'] as String? ?? '',
transactionId: json['transactionId'] as String? ?? '',
createdAt: json['createdAt'] is int
? DateTime.fromMillisecondsSinceEpoch(json['createdAt'] as int)
: (json['createdAt'] is Timestamp)
? (json['createdAt'] as Timestamp).toDate()
: DateTime.now(),
updatedAt: json['updatedAt'] != null
? (json['updatedAt'] is int
? DateTime.fromMillisecondsSinceEpoch(json['updatedAt'])
: (json['updatedAt'] is DateTime ? json['updatedAt'] : null))
: null,
);
}
}

View File

@ -0,0 +1,23 @@
import '../models/transaction_porter_model.dart';
abstract class TransactionPorterRepository {
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId);
Stream<PorterTransactionModel?> watchTransactionById(String transactionId);
Future<PorterTransactionModel?> getTransactionById(String transactionId);
Future<List<String>> getPorterTransactionIds(String porterId);
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId);
Future<void> updateTransactionStatus({
required String transactionId,
required String status,
});
Future<void> completePorterTransaction({
required String transactionId,
required String porterOnlineId,
});
}

View File

@ -0,0 +1,51 @@
import '../models/transaction_porter_model.dart';
import '../repositories/transaction_porter_repository.dart';
class TransactionPorterUsecase {
final TransactionPorterRepository _repository;
TransactionPorterUsecase(this._repository);
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId) {
return _repository.watchPorterTransactions(porterId);
}
Stream<PorterTransactionModel?> watchTransactionById(String transactionId) {
return _repository.watchTransactionById(transactionId);
}
Future<PorterTransactionModel?> getTransactionById(String transactionId) {
return _repository.getTransactionById(transactionId);
}
Future<List<String>> getPorterTransactionIds(String porterId) {
return _repository.getPorterTransactionIds(porterId);
}
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) {
return _repository.getPorterTransactionById(transactionId);
}
Future<void> updateTransactionStatus({
// required String ticketId,
required String transactionId,
required String status,
}) {
return _repository.updateTransactionStatus(
// ticketId: ticketId,
transactionId: transactionId,
status: status,
);
}
Future<void> completePorterTransaction({
// required String ticketId,
required String transactionId,
required String porterOnlineId,
}) {
return _repository.completePorterTransaction(
// ticketId: ticketId,
transactionId: transactionId,
porterOnlineId: porterOnlineId,
);
}
}

View File

@ -0,0 +1,376 @@
import 'dart:async';
import 'dart:developer';
import 'package:e_porter/_core/service/preferences_service.dart';
import 'package:e_porter/_core/utils/snackbar/snackbar_helper.dart';
import 'package:e_porter/domain/models/porter_queue_model.dart';
import 'package:e_porter/presentation/controllers/porter_queue_controller.dart';
import 'package:get/get.dart';
import '../../domain/models/transaction_porter_model.dart';
import '../../domain/usecases/transaction_porter_usecase.dart';
class TransactionPorterController extends GetxController {
final TransactionPorterUsecase _useCase;
TransactionPorterController(this._useCase);
final RxList<PorterTransactionModel> transactions = <PorterTransactionModel>[].obs;
final Rx<PorterTransactionModel?> currentTransaction = Rx<PorterTransactionModel?>(null);
final Map<String, StreamSubscription<PorterTransactionModel?>> _transactionWatchers = {};
final RxString currentPorterId = ''.obs;
final RxBool isLoading = false.obs;
final RxString error = ''.obs;
StreamSubscription<List<PorterTransactionModel>>? _subscription;
StreamSubscription<PorterQueueModel?>? _porterSubscription;
@override
void onInit() {
super.onInit();
_loadPorterData();
}
Future<void> _loadPorterData() async {
try {
final userData = await PreferencesService.getUserData();
if (userData == null || userData.uid.isEmpty) {
error.value = 'Data user tidak tersedia';
return;
}
// Set userId untuk digunakan nanti
String userId = userData.uid;
final porterCtrl = Get.find<PorterQueueController>();
// Simpan user ID untuk memastikan bisa mengakses history jika porter tidak ditemukan
String userIdForHistory = userId;
// Coba mendapatkan porter aktif
_porterSubscription = porterCtrl.watchPorter(userId).listen((porter) {
if (porter != null && porter.id != null) {
log('Porter aktif ditemukan: ${porter.id}');
currentPorterId.value = porter.id!;
// Gunakan userId untuk memastikan semua transaksi bisa diakses
loadTransactionsWithBothIDs(porter.id!, userIdForHistory);
} else {
// Porter tidak aktif, coba ambil riwayat dari userId
log('Porter aktif tidak ditemukan, mencoba ambil riwayat dari userId: $userId');
loadTransactionsFromUserId(userId);
}
}, onError: (e) {
log('Error memantau data porter: $e');
// Jika error, coba ambil riwayat dari userId
loadTransactionsFromUserId(userId);
});
} catch (e) {
error.value = 'Error inisialisasi: $e';
}
}
void loadTransactionsWithBothIDs(String porterId, String userId) {
isLoading.value = true;
error.value = '';
_subscription?.cancel();
_subscription = _useCase.watchPorterTransactions(porterId).listen((transactionList) {
log('Menerima ${transactionList.length} transaksi dari porterId');
// Jika tidak ada transaksi dari porterId, coba dari userId
if (transactionList.isEmpty && userId != porterId) {
log('Tidak ada transaksi dari porterId, mencoba dari userId: $userId');
loadTransactionsFromUserId(userId);
} else {
transactions.assignAll(transactionList);
isLoading.value = false;
}
}, onError: (e) {
log('Error streaming transaksi: $e');
// Jika terjadi error, coba dari userId sebagai fallback
if (userId != porterId) {
log('Error dengan porterId, mencoba dari userId: $userId');
loadTransactionsFromUserId(userId);
} else {
error.value = 'Gagal memuat transaksi: $e';
isLoading.value = false;
}
});
}
void loadTransactionsFromUserId(String userId) {
isLoading.value = true;
error.value = '';
log('[TransactionPorterController] Memuat transaksi untuk userId: $userId');
// Gunakan userId sebagai porterId di porterHistory
_useCase.getPorterTransactionIds(userId).then((transactionIds) {
log('[TransactionPorterController] Ditemukan ${transactionIds.length} ID transaksi untuk userId: $userId');
if (transactionIds.isEmpty) {
transactions.clear();
isLoading.value = false;
return;
}
// Ambil dan proses data transaksi
_processTransactionData(transactionIds);
}).catchError((e) {
log('[TransactionPorterController] Error mendapatkan ID transaksi: $e');
error.value = 'Gagal memuat riwayat transaksi';
isLoading.value = false;
});
}
void watchTransaction(String transactionId) {
// Batalkan subscription yang ada jika ada
_transactionWatchers[transactionId]?.cancel();
// Mulai subscription baru
_transactionWatchers[transactionId] = _useCase.watchTransactionById(transactionId).listen(
(updatedTransaction) {
if (updatedTransaction != null) {
// Update current transaction jika itu transaksi yang sedang aktif
if (currentTransaction.value?.id == transactionId) {
currentTransaction.value = updatedTransaction;
}
// Update transaksi di daftar
final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) {
transactions[index] = updatedTransaction;
log('Transaksi diperbarui secara real-time: $transactionId, status: ${updatedTransaction.status}');
}
}
},
onError: (error) {
log('Error memantau transaksi: $error');
},
);
}
void _processTransactionData(List<String> transactionIds) async {
try {
List<PorterTransactionModel> txList = [];
for (var id in transactionIds) {
final txData = await _useCase.getPorterTransactionById(id);
if (txData != null) {
txList.add(PorterTransactionModel.fromJson(txData, id));
}
}
// Urutkan berdasarkan tanggal terbaru
txList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
// Update transactions list
transactions.assignAll(txList);
isLoading.value = false;
} catch (e) {
log('Error memproses data transaksi: $e');
error.value = 'Gagal memproses data transaksi';
isLoading.value = false;
}
}
void loadTransactionsFromPorterId(String porterId) {
if (porterId.isEmpty) {
error.value = 'Porter ID tidak boleh kosong';
return;
}
isLoading.value = true;
error.value = '';
_subscription?.cancel();
_subscription = _useCase.watchPorterTransactions(porterId).listen((transactionList) {
log('Menerima ${transactionList.length} transaksi');
transactions.assignAll(transactionList);
// Mulai memantau setiap transaksi individual untuk real-time updates
for (var transaction in transactionList) {
watchTransaction(transaction.id);
}
isLoading.value = false;
}, onError: (e) {
log('Error streaming transaksi: $e');
error.value = 'Gagal memuat transaksi: $e';
isLoading.value = false;
});
}
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) async {
try {
isLoading.value = true;
error.value = '';
return await _useCase.getPorterTransactionById(transactionId);
} catch (e) {
log('Error getting transaction data: $e');
error.value = 'Gagal mendapatkan data transaksi: $e';
return null;
} finally {
isLoading.value = false;
}
}
Future<PorterTransactionModel?> getTransactionById(String transactionId) async {
try {
log('Getting transaction by ID: $transactionId');
isLoading.value = true;
error.value = '';
// Reset dulu current transaction agar UI bisa merespons ke loading state
currentTransaction.value = null;
final transaction = await _useCase.getTransactionById(transactionId);
if (transaction != null) {
log('Transaction found and set to current: ${transaction.id}');
currentTransaction.value = transaction;
// Mulai memantau transaksi ini
watchTransaction(transactionId);
} else {
log('Transaction not found with ID: $transactionId');
error.value = 'Transaksi tidak ditemukan';
}
return transaction;
} catch (e) {
log('Error getting transaction by ID: $e');
error.value = 'Gagal mendapatkan detail transaksi: $e';
return null;
} finally {
isLoading.value = false;
}
}
Future<void> updateTransactionStatus({
required String transactionId,
required String status,
}) async {
try {
isLoading.value = true;
error.value = '';
log('Memperbarui status transaksi: $transactionId menjadi $status');
await _useCase.updateTransactionStatus(
transactionId: transactionId,
status: status,
);
// Dapatkan transaksi yang diperbarui
final updatedTransaction = await getTransactionById(transactionId);
// Update list transaksi yang ada dengan yang baru
if (updatedTransaction != null) {
// Cari indeks transaksi dalam list yang ada
final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) {
// Update transaksi pada indeks yang ditemukan
transactions[index] = updatedTransaction;
log('Transaksi di daftar utama diperbarui: $transactionId dengan status: $status');
} else {
// Jika tidak ditemukan, perbarui seluruh list
log('Transaksi tidak ditemukan di daftar, menyegarkan seluruh daftar');
refreshTransactions();
}
}
SnackbarHelper.showSuccess('Berhasil', 'Status transaksi berhasil diperbarui');
} catch (e) {
log('Error memperbarui status: $e');
error.value = 'Gagal memperbarui status transaksi: $e';
SnackbarHelper.showError('Terjadi Kesalahan', 'Status transaksi gagal diperbarui');
} finally {
isLoading.value = false;
}
}
// Metode serupa untuk completePorterTransaction
Future<void> completePorterTransaction({
required String transactionId,
}) async {
try {
isLoading.value = true;
error.value = '';
final transaction = currentTransaction.value;
if (transaction == null) {
throw Exception('Tidak dapat menemukan data transaksi');
}
final porterOnlineId = transaction.porterOnlineId;
if (porterOnlineId.isEmpty) {
throw Exception('ID Porter Online tidak ditemukan');
}
log('Menyelesaikan transaksi: $transactionId dengan porter: $porterOnlineId');
await _useCase.completePorterTransaction(
transactionId: transactionId,
porterOnlineId: porterOnlineId,
);
// Dapatkan transaksi yang diperbarui
final updatedTransaction = await getTransactionById(transactionId);
// Update list transaksi
if (updatedTransaction != null) {
final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) {
transactions[index] = updatedTransaction;
log('Transaksi di daftar utama diperbarui menjadi selesai: $transactionId');
} else {
refreshTransactions();
}
}
SnackbarHelper.showSuccess('Transaksi Selesai', 'Transaksi porter berhasil diselesaikan');
} catch (e) {
log('Error menyelesaikan transaksi: $e');
error.value = 'Gagal menyelesaikan transaksi: $e';
SnackbarHelper.showError('Terjadi Kesalahan', 'Gagal menyelesaikan transaksi');
} finally {
isLoading.value = false;
}
}
Future<void> refreshTransactions() async {
final id = currentPorterId.value;
if (id.isEmpty) {
try {
final userData = await PreferencesService.getUserData();
if (userData != null && userData.uid.isNotEmpty) {
currentPorterId.value = userData.uid;
log('Retrieved porter ID from preferences during refresh: ${userData.uid}');
} else {
error.value = 'ID porter tidak tersedia';
return;
}
} catch (e) {
log('Error getting user data for refresh: $e');
error.value = 'Gagal memperoleh ID porter: $e';
return;
}
}
log('Refreshing transactions for porter: ${currentPorterId.value}');
isLoading.value = true;
loadTransactionsFromPorterId(currentPorterId.value);
}
@override
void onClose() {
_porterSubscription?.cancel();
_subscription?.cancel();
for (var subscription in _transactionWatchers.values) {
subscription.cancel();
}
_transactionWatchers.clear();
super.onClose();
}
}

View File

@ -0,0 +1,322 @@
import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:zoom_tap_animation/zoom_tap_animation.dart';
class CardHistoryPorter extends StatelessWidget {
final String namePassenger;
final String tlpnPassenger;
final String? porter1;
final String? porter2;
final String? porter3;
final String lokasiPassenger;
final String status;
final String date;
final String time;
final Color? statusColor;
final String? price;
final VoidCallback? onTap;
final String? bookingId;
const CardHistoryPorter({
Key? key,
required this.namePassenger,
required this.tlpnPassenger,
this.porter1,
this.porter2,
this.porter3,
required this.lokasiPassenger,
required this.status,
required this.date,
required this.time,
this.statusColor,
this.price,
this.onTap,
this.bookingId,
}) : super(key: key);
bool get _shouldShowPorters {
if (porter1 == null && porter2 == null && porter3 == null) return false;
return (porter1?.isNotEmpty ?? false) || (porter2?.isNotEmpty ?? false) || (porter3?.isNotEmpty ?? false);
}
@override
Widget build(BuildContext context) {
return ZoomTapAnimation(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.r),
child: CustomeShadowCotainner(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
Divider(height: 16.h, thickness: 1, color: GrayColors.gray100),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStatusAvatar(),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPassengerInfo(),
SizedBox(height: 6.h),
if (_shouldShowPorters) ...[
_buildPorterInfo(),
SizedBox(height: 6.h),
],
_buildLocationInfo(),
SizedBox(height: 6.h),
_buildPriceInfo(),
SizedBox(height: 6.h),
_buildDateAndTimeInfo(),
],
),
),
],
),
],
),
),
),
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (bookingId != null) ...[
Row(
children: [
Icon(
Icons.confirmation_number_outlined,
size: 14.sp,
color: GrayColors.gray500,
),
SizedBox(width: 4.w),
TypographyStyles.caption(
'ID: $bookingId',
color: GrayColors.gray600,
fontWeight: FontWeight.w600,
),
],
),
] else ...[
SizedBox(width: 4.w),
],
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: statusColor?.withOpacity(0.1) ?? Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getStatusIcon(),
size: 12.sp,
color: statusColor,
),
SizedBox(width: 4.w),
TypographyStyles.caption(
status,
color: statusColor,
fontWeight: FontWeight.w600,
),
],
),
),
],
);
}
Widget _buildStatusAvatar() {
return CircleAvatar(
radius: 20.r,
backgroundColor: statusColor?.withOpacity(0.1) ?? Colors.grey.withOpacity(0.1),
child: Icon(
_getStatusIcon(),
color: statusColor,
size: 20.sp,
),
);
}
Widget _buildPassengerInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body(
namePassenger,
color: GrayColors.gray800,
fontWeight: FontWeight.w600,
),
SizedBox(height: 2.h),
Row(
children: [
Icon(
Icons.phone_outlined,
size: 14.sp,
color: GrayColors.gray400,
),
SizedBox(width: 4.w),
TypographyStyles.small(
tlpnPassenger,
color: GrayColors.gray500,
fontWeight: FontWeight.w400,
),
],
),
],
);
}
Widget _buildPorterInfo() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
color: PrimaryColors.primary50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: PrimaryColors.primary100),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.caption(
'Layanan Porter:',
color: PrimaryColors.primary800,
fontWeight: FontWeight.w600,
),
SizedBox(height: 4.h),
Wrap(
spacing: 8.w,
runSpacing: 4.h,
children: [
if (porter1 != null && porter1!.isNotEmpty) _buildPorterChip(porter1!),
if (porter2 != null && porter2!.isNotEmpty) _buildPorterChip(porter2!),
if (porter3 != null && porter3!.isNotEmpty) _buildPorterChip(porter3!),
],
),
],
),
);
}
Widget _buildPorterChip(String porterName) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: PrimaryColors.primary200),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.luggage_outlined,
size: 12.sp,
color: PrimaryColors.primary700,
),
SizedBox(width: 4.w),
TypographyStyles.caption(
porterName,
color: PrimaryColors.primary700,
fontWeight: FontWeight.w500,
),
],
),
);
}
Widget _buildLocationInfo() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.location_on_outlined,
size: 24.sp,
color: GrayColors.gray500,
),
SizedBox(width: 4.w),
TypographyStyles.caption(
lokasiPassenger,
color: GrayColors.gray600,
fontWeight: FontWeight.w500,
),
],
);
}
// Info Harga
Widget _buildPriceInfo() {
if (price == null) return SizedBox.shrink();
return Row(
children: [
Icon(
Icons.paid_outlined,
size: 14.sp,
color: GrayColors.gray500,
),
SizedBox(width: 4.w),
TypographyStyles.body(
price!,
color: PrimaryColors.primary800,
fontWeight: FontWeight.w600,
),
],
);
}
// Info Tanggal dan Waktu
Widget _buildDateAndTimeInfo() {
return Row(
children: [
Icon(
Icons.calendar_today_outlined,
size: 14.sp,
color: GrayColors.gray400,
),
SizedBox(width: 4.w),
TypographyStyles.small(
date,
color: GrayColors.gray500,
fontWeight: FontWeight.w400,
),
SizedBox(width: 8.w),
Icon(
Icons.access_time_outlined,
size: 14.sp,
color: GrayColors.gray400,
),
SizedBox(width: 4.w),
TypographyStyles.small(
time,
color: GrayColors.gray500,
fontWeight: FontWeight.w400,
),
],
);
}
// Mendapatkan icon berdasarkan status
IconData _getStatusIcon() {
switch (status.toLowerCase()) {
case 'pending':
return Icons.hourglass_empty;
case 'proses':
return Icons.directions_run;
case 'selesai':
return Icons.check_circle_outline;
default:
return Icons.info_outline;
}
}
}

View File

@ -0,0 +1,362 @@
// ignore_for_file: deprecated_member_use, unnecessary_null_comparison
import 'dart:developer';
import 'package:e_porter/_core/component/appbar/appbar_component.dart';
import 'package:e_porter/_core/component/button/button_fill.dart';
import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart';
import 'package:e_porter/domain/models/transaction_model.dart';
import 'package:e_porter/domain/models/transaction_porter_model.dart';
import 'package:e_porter/presentation/controllers/history_controller.dart';
import 'package:e_porter/presentation/controllers/transaction_porter_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
class DetailHistoryPorterScreen extends StatefulWidget {
const DetailHistoryPorterScreen({super.key});
@override
State<DetailHistoryPorterScreen> createState() => _DetailHistoryPorterScreenState();
}
class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
final TransactionPorterController _porterController = Get.find<TransactionPorterController>();
final HistoryController _historyController = Get.find<HistoryController>();
final RxBool _isLoadingTicket = false.obs;
PorterTransactionModel? porterTransaction;
late final String porterTransactionId;
// Formatters
final DateFormat _dateFormat = DateFormat('dd MMMM yyyy', 'en_US');
final DateFormat _timeFormat = DateFormat.jm();
// final NumberFormat _priceFormatter = NumberFormat.decimalPattern('id_ID');
@override
void initState() {
super.initState();
final args = Get.arguments as Map<String, dynamic>;
porterTransactionId = args['transactionPorterId'];
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchTransactioPorterById();
_fetchTransactionData();
});
log('[Detail History Porter] ID Transaction Porter : $porterTransactionId');
}
Future<void> _fetchTransactioPorterById() async {
try {
await _porterController.getTransactionById(porterTransactionId);
log('[Detail History Porter] Transaction fetched: ${_porterController.currentTransaction.value}');
} catch (e) {
log('[Detail History Porter] Error getTransaction $e');
}
}
Future<void> _fetchTransactionData() async {
try {
await _porterController.getTransactionById(porterTransactionId);
final porterTransaction = _porterController.currentTransaction.value;
if (porterTransaction != null && porterTransaction.ticketId != null && porterTransaction.transactionId != null) {
await _historyController.getTransactionFromFirestore(
porterTransaction.ticketId, porterTransaction.transactionId);
}
} catch (e) {
log('[Detail History Porter] Error fetching data: $e');
} finally {
_isLoadingTicket.value = false;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: GrayColors.gray50,
appBar: DefaultAppbarComponent(
title: 'Detail Riwayat',
textColor: Colors.white,
backgroundColors: PrimaryColors.primary800,
onTab: () {
Get.back();
},
),
body: Obx(() {
final porterTransaction = _porterController.currentTransaction.value;
final ticketTransaction = _historyController.selectedTransaction.value;
final isLoadingPorter = _porterController.isLoading.value;
final isLoadingTicket = _isLoadingTicket.value;
if (isLoadingPorter || isLoadingTicket) {
return const Center(child: CircularProgressIndicator());
} else if (porterTransaction == null) {
return const Center(child: Text('Data transaksi tidak ditemukan'));
}
return SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
child: SingleChildScrollView(
child: Column(
children: [
_buildStatusCard(porterTransaction),
SizedBox(height: 20.h),
_buildInfoPassenger(ticketTransaction),
SizedBox(height: 20.h),
_buildLocationPassenger(porterTransaction),
SizedBox(height: 20.h),
_buildDetailsOrder(ticketTransaction),
],
),
),
),
);
}),
bottomNavigationBar: Obx(() {
final transaction = _porterController.currentTransaction.value;
if (transaction == null || _porterController.isLoading.value) {
return const SizedBox.shrink();
}
switch (transaction.normalizedStatus) {
case 'pending':
return CustomeShadowCotainner(
child: ButtonFill(
text: 'Terima Orderan',
textColor: Colors.white,
onTap: () {
_porterController.updateTransactionStatus(
transactionId: porterTransactionId,
status: 'proses',
);
},
),
);
case 'proses':
return CustomeShadowCotainner(
child: ButtonFill(
text: 'Selesaikan Orderan',
textColor: Colors.white,
onTap: () {
_porterController.completePorterTransaction(
transactionId: porterTransactionId,
);
},
),
);
case 'selesai':
return const SizedBox.shrink();
default:
return const SizedBox.shrink();
}
}),
);
}
Widget _buildStatusCard(PorterTransactionModel porterTransaction) {
return CustomeShadowCotainner(
child: Row(
children: [
CircleAvatar(
radius: 24.r,
backgroundColor: _getStatusColor(porterTransaction).withOpacity(0.1),
child: Icon(
_getStatusIcon(porterTransaction),
color: _getStatusColor(porterTransaction),
size: 24.sp,
),
),
SizedBox(width: 20.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.caption(
'Status Transaksi',
color: GrayColors.gray500,
fontWeight: FontWeight.w500,
),
TypographyStyles.h5(
porterTransaction.status,
color: _getStatusColor(porterTransaction),
),
],
),
],
),
);
}
Widget _buildInfoPassenger(TransactionModel? ticketTransaction) {
return CustomeShadowCotainner(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_componentHeaderText(text: 'Informasi Penumpang', svgIcon: 'assets/icons/ic_account.svg'),
if (ticketTransaction != null && ticketTransaction.passengerDetails.isNotEmpty)
..._buildPassengerDetailsList(ticketTransaction)
else
TypographyStyles.body(
'Data penumpang tidak tersedia',
color: GrayColors.gray500,
fontWeight: FontWeight.w600,
),
],
),
);
}
List<Widget> _buildPassengerDetailsList(TransactionModel ticketTransaction) {
final List<Widget> passengerWidgets = [];
for (int i = 0; i < ticketTransaction.passengerDetails.length; i++) {
final passenger = ticketTransaction.passengerDetails[i];
passengerWidgets.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body('Penumpang ${i + 1}', color: GrayColors.gray800, fontWeight: FontWeight.w600),
SizedBox(height: 8.h),
_componentRowText(label: 'Nama', value: passenger['name'] ?? 'N/A'),
SizedBox(height: 6.h),
_componentRowText(label: 'Jenis Kelamin', value: passenger['gender'] ?? 'N/A'),
if (i < ticketTransaction.passengerDetails.length - 1) SizedBox(height: 16.h),
],
),
);
}
return passengerWidgets;
}
Widget _buildLocationPassenger(PorterTransactionModel porterTransaction) {
final orderDate = _dateFormat.format(porterTransaction.createdAt);
final orderTime = _timeFormat.format(porterTransaction.createdAt);
return CustomeShadowCotainner(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_componentHeaderText(text: 'Lokasi Layanan', svgIcon: 'assets/icons/ic_account.svg'),
_componentRowText(label: 'Lokasi Penumpang', value: porterTransaction.locationPassenger),
SizedBox(height: 6.h),
_componentRowText(label: 'Lokasi Anda', value: porterTransaction.locationPorter),
SizedBox(height: 6.h),
_componentRowText(label: 'Tanggal Order', value: orderDate),
SizedBox(height: 6.h),
_componentRowText(label: 'Waktu Order', value: orderTime),
],
),
);
}
Widget _buildDetailsOrder(TransactionModel? ticketTransaction) {
return CustomeShadowCotainner(
child: Column(
children: [
_componentHeaderText(text: 'Rincian Pesanan', svgIcon: 'assets/icons/ic_account.svg'),
_componentRowText(
label: 'Keberangkatan',
value: '${ticketTransaction?.porterServiceDetails?['departure']?['name'] ?? '-'}',
),
_componentRowText(
label: 'Kedatangan',
value: '${ticketTransaction?.porterServiceDetails?['arrival']?['name'] ?? '-'}',
),
_componentRowText(
label: 'Transit',
value: '${ticketTransaction?.porterServiceDetails?['transit']?['name'] ?? '-'}',
),
],
),
);
}
Widget _componentHeaderText({required String text, required String svgIcon}) {
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SvgPicture.asset(svgIcon, color: PrimaryColors.primary800, width: 24.w, height: 24.h),
SizedBox(width: 10.w),
TypographyStyles.body(
text,
color: PrimaryColors.primary800,
fontWeight: FontWeight.w600,
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.h),
child: Divider(thickness: 1, color: GrayColors.gray200),
),
],
);
}
Widget _componentRowText({required String label, required String value}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.height * 0.16,
child: TypographyStyles.caption(
label,
color: GrayColors.gray500,
fontWeight: FontWeight.w400,
),
),
Expanded(
child: TypographyStyles.body(
value,
color: GrayColors.gray800,
fontWeight: FontWeight.w600,
),
),
],
);
}
// Helper untuk warna status
Color _getStatusColor(PorterTransactionModel? transaction) {
if (transaction == null) return GrayColors.gray400;
switch (transaction.normalizedStatus) {
case 'pending':
return Colors.orange;
case 'proses':
return PrimaryColors.primary800;
case 'selesai':
return Colors.green;
default:
return GrayColors.gray400;
}
}
// Helper untuk icon status
IconData _getStatusIcon(PorterTransactionModel? transaction) {
if (transaction == null) return Icons.info_outline;
switch (transaction.normalizedStatus) {
case 'pending':
return Icons.hourglass_empty;
case 'proses':
return Icons.directions_run;
case 'selesai':
return Icons.check_circle_outline;
default:
return Icons.info_outline;
}
}
}

View File

@ -0,0 +1,383 @@
import 'dart:developer';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart';
import 'package:e_porter/domain/models/transaction_model.dart';
import 'package:e_porter/domain/models/transaction_porter_model.dart';
import 'package:e_porter/presentation/controllers/history_controller.dart';
import 'package:e_porter/presentation/controllers/transaction_porter_controller.dart';
import 'package:e_porter/presentation/screens/boarding_pass/component/card_history_porter.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 'package:intl/intl.dart';
import '../../../../_core/component/appbar/appbar_component.dart';
class HistoryPorterScreen extends StatefulWidget {
const HistoryPorterScreen({super.key});
@override
State<HistoryPorterScreen> createState() => _HistoryPorterScreenState();
}
class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
final TransactionPorterController _porterController = Get.find<TransactionPorterController>();
final HistoryController _historyController = Get.find<HistoryController>();
final Map<String, TransactionModel> _ticketTransactionCache = {};
final DateFormat _dateFormat = DateFormat('dd/MM/yyyy');
final DateFormat _timeFormat = DateFormat('HH:mm');
final NumberFormat _priceFormatter = NumberFormat.currency(
locale: 'id_ID',
symbol: 'Rp ',
decimalDigits: 0,
);
// Future<List<PorterTransactionModel>> _loadPorterTransactionHistory() async {
// try {
// if (_userId.isEmpty) return [];
// // Dapatkan ID semua transaksi porter
// final transactionIds = await _porterController.getPorterTransactionIds(_userId);
// // Muat data untuk setiap ID transaksi
// final transactions = <PorterTransactionModel>[];
// for (final id in transactionIds) {
// final txData = await _porterController.getPorterTransactionById(id);
// if (txData != null) {
// transactions.add(PorterTransactionModel.fromJson(txData, id));
// }
// }
// return transactions;
// } catch (e) {
// log('Error loading porter transaction history: $e');
// return [];
// }
// }
Future<TransactionModel?> _loadTicketTransaction(String ticketId, String transactionId) async {
final cacheKey = "$ticketId-$transactionId";
if (_ticketTransactionCache.containsKey(cacheKey)) {
return _ticketTransactionCache[cacheKey];
}
try {
if (ticketId.isEmpty || transactionId.isEmpty) {
return null;
}
final transaction = await _historyController.getTransactionFromFirestore(ticketId, transactionId);
if (transaction != null) {
_ticketTransactionCache[cacheKey] = transaction;
}
return transaction;
} catch (e) {
log('Error loading ticket transaction: $e');
return null;
}
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
backgroundColor: GrayColors.gray50,
appBar: SimpleAppbarComponent(
title: 'Riwayat Transaksi',
subTitle: 'Semua aktivitas transaksi aktif ditampilkan dihalaman ini',
),
body: Column(
children: [
_buildLoadingIndicator(),
_buildTabBar(),
_buildErrorMessage(),
Expanded(
child: TabBarView(
children: [
_buildTransactionList('pending'),
_buildTransactionList('proses'),
_buildTransactionList('selesai'),
],
),
),
],
),
),
);
}
// Indikator loading
Widget _buildLoadingIndicator() {
return Obx(() => _porterController.isLoading.value
? LinearProgressIndicator(
backgroundColor: PrimaryColors.primary100,
valueColor: AlwaysStoppedAnimation<Color>(PrimaryColors.primary800),
)
: const SizedBox.shrink());
}
// Tab bar
Widget _buildTabBar() {
return Container(
color: Colors.white,
child: TabBar(
labelColor: PrimaryColors.primary800,
unselectedLabelColor: GrayColors.gray400,
indicatorColor: PrimaryColors.primary800,
indicatorWeight: 3,
tabs: const [
Tab(text: 'Pending'),
Tab(text: 'Proses'),
Tab(text: 'Selesai'),
],
),
);
}
// Pesan error
Widget _buildErrorMessage() {
return Obx(() {
if (_porterController.error.value.isEmpty) {
return const SizedBox.shrink();
}
// Customisasi pesan error untuk lebih user-friendly
String errorMessage = _porterController.error.value;
if (errorMessage.contains('Porter tidak ditemukan')) {
return Container(
padding: EdgeInsets.all(8.w),
margin: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.orange[50],
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.orange, size: 20.w),
SizedBox(width: 8.w),
Expanded(
child: Text(
'Menampilkan riwayat transaksi yang telah selesai',
style: TextStyle(
color: Colors.orange[800],
fontSize: 14.sp,
),
),
),
],
),
);
}
// Error lainnya
return Container(
padding: EdgeInsets.all(8.w),
margin: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 20.w),
SizedBox(width: 8.w),
Expanded(
child: Text(
errorMessage,
style: TextStyle(
color: Colors.red[800],
fontSize: 14.sp,
),
),
),
],
),
);
});
}
// Daftar transaksi
Widget _buildTransactionList(String statusFilter) {
return Obx(() {
// Jika sedang loading, tampilkan indikator
if (_porterController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
// Ambil semua transaksi dari controller
final allTransactions = _porterController.transactions;
// Log untuk debug
log('Semua transaksi: ${allTransactions.length}');
// Filter berdasarkan status
final filteredTransactions = allTransactions.where((tx) => tx.normalizedStatus == statusFilter).toList();
log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}');
// Jika tidak ada transaksi, tampilkan pesan kosong
if (filteredTransactions.isEmpty) {
// Jika ada error tapi tidak ada transaksi
if (_porterController.error.value.contains('Porter tidak ditemukan') && statusFilter == 'selesai') {
// Tampilkan pesan yang lebih positif
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 48.h, color: Colors.grey[400]),
SizedBox(height: 16.h),
TypographyStyles.body(
'Tidak ada riwayat transaksi selesai',
color: GrayColors.gray600,
),
SizedBox(height: 8.h),
TypographyStyles.caption(
'Riwayat akan muncul setelah Anda menyelesaikan transaksi',
color: GrayColors.gray500,
),
SizedBox(height: 16.h),
ElevatedButton.icon(
onPressed: () => _porterController.refreshTransactions(),
icon: Icon(Icons.refresh, size: 16.h),
label: const Text('Muat Ulang'),
style: ElevatedButton.styleFrom(
backgroundColor: PrimaryColors.primary800,
foregroundColor: Colors.white,
),
),
],
),
);
}
return _buildEmptyTransactionMessage(statusFilter);
}
// Jika ada transaksi, tampilkan daftar
return RefreshIndicator(
onRefresh: () => _porterController.refreshTransactions(),
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
itemCount: filteredTransactions.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) => _buildTransactionItem(filteredTransactions[index]),
),
);
});
}
// Pesan tidak ada transaksi
Widget _buildEmptyTransactionMessage(String statusFilter) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TypographyStyles.body(
'Tidak ada transaksi ${statusFilter.capitalizeFirst}',
color: GrayColors.gray600,
),
SizedBox(height: 16.h),
ElevatedButton.icon(
// Perbaiki ini untuk memanggil refreshTransactions di controller
onPressed: () => _porterController.refreshTransactions(),
icon: Icon(Icons.refresh, size: 16.h),
label: const Text('Refresh'),
style: ElevatedButton.styleFrom(
backgroundColor: PrimaryColors.primary800,
foregroundColor: Colors.white,
),
),
],
),
);
}
// Item transaksi
Widget _buildTransactionItem(PorterTransactionModel transaction) {
log('Building item for transaction: ${transaction.id}, status: ${transaction.status}');
return FutureBuilder<TransactionModel?>(
future: _loadTicketTransaction(transaction.ticketId, transaction.transactionId),
builder: (context, snapshot) {
String passengerName = transaction.idPassenger;
String passengerPhone = '-';
double price = 0;
String porter1 = 'Porter';
String? porter2 = null;
String? porter3 = null;
if (snapshot.hasData && snapshot.data != null) {
final ticketTransaction = snapshot.data!;
passengerName = ticketTransaction.userDetails['name'] ?? transaction.idPassenger;
passengerPhone = ticketTransaction.userDetails['phone'] ?? '-';
if (ticketTransaction.porterServiceDetails != null) {
price = 0;
if (ticketTransaction.porterServiceDetails!.containsKey('arrival') &&
ticketTransaction.porterServiceDetails!['arrival'] is Map<String, dynamic> &&
ticketTransaction.porterServiceDetails!['arrival'].containsKey('price')) {
price += (ticketTransaction.porterServiceDetails!['arrival']['price'] as num).toDouble();
porter1 = ticketTransaction.porterServiceDetails!['arrival']['name'] ?? 'Porter';
}
if (ticketTransaction.porterServiceDetails!.containsKey('departure') &&
ticketTransaction.porterServiceDetails!['departure'] is Map<String, dynamic> &&
ticketTransaction.porterServiceDetails!['departure'].containsKey('price')) {
price += (ticketTransaction.porterServiceDetails!['departure']['price'] as num).toDouble();
porter2 = ticketTransaction.porterServiceDetails!['departure']['name'];
}
if (ticketTransaction.porterServiceDetails!.containsKey('transit') &&
ticketTransaction.porterServiceDetails!['transit'] is Map<String, dynamic> &&
ticketTransaction.porterServiceDetails!['transit'].containsKey('price')) {
price += (ticketTransaction.porterServiceDetails!['transit']['price'] as num).toDouble();
porter3 = ticketTransaction.porterServiceDetails!['transit']['name'];
}
log('Total porter price: $price');
}
}
return CardHistoryPorter(
namePassenger: passengerName,
tlpnPassenger: passengerPhone,
lokasiPassenger: transaction.locationPassenger,
status: transaction.normalizedStatus.capitalizeFirst!,
date: _dateFormat.format(transaction.createdAt),
time: _timeFormat.format(transaction.createdAt),
porter1: porter1,
porter2: porter2,
porter3: porter3,
price: _priceFormatter.format(price),
statusColor: _getStatusColor(transaction.normalizedStatus),
onTap: () {
log('ID Transaction Porter: ${transaction.id}');
Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: {
'transactionPorterId': transaction.id,
'ticketId': transaction.ticketId,
'ticketTransactionId': transaction.transactionId,
});
},
);
},
);
}
// Warna status
Color _getStatusColor(String status) {
switch (status) {
case 'pending':
return Colors.orange;
case 'proses':
return PrimaryColors.primary800;
case 'selesai':
return Colors.green;
default:
return GrayColors.gray400;
}
}
}

View File

@ -13,6 +13,7 @@ import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/state_succes_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/boarding_pass_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/detail_history_porter_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/detail_ticket_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/history_porter_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/print_boarding_pass_screen.dart';
@ -170,6 +171,11 @@ class AppRoutes {
name: Routes.HISTORYPORTER,
page: () => HistoryPorterScreen(),
),
GetPage(
name: Routes.DETAILHISTORYPORTER,
page: () => DetailHistoryPorterScreen(),
binding: TransactionPorterBinding()
),
GetPage(
name: Routes.ADDPASSENGER,
page: () => AddPassengerScreen(),
@ -206,6 +212,7 @@ class Routes {
static const SCANQR = '/scan_qr';
static const PROCESSING = '/processing';
static const HISTORYPORTER = '/history_porter';
static const DETAILHISTORYPORTER = '/detail_history_porter';
static const ADDPASSENGER = '/add_passenger';
}