Feat: completed the implementation of transaction history for porter access rights
This commit is contained in:
parent
b1f9d95907
commit
0b381d2571
|
@ -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');
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -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>()),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue