Feat: fix bug after implementation rejected status
This commit is contained in:
parent
08331d69ca
commit
368788c85a
|
@ -638,7 +638,7 @@
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.4"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generated": "2025-04-27T19:47:24.712851Z",
|
"generated": "2025-04-29T13:17:45.754727Z",
|
||||||
"generator": "pub",
|
"generator": "pub",
|
||||||
"generatorVersion": "3.5.0",
|
"generatorVersion": "3.5.0",
|
||||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
||||||
|
|
|
@ -155,67 +155,6 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
|
||||||
|
|
||||||
return controller.stream;
|
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
|
@override
|
||||||
Future<PorterTransactionModel?> getTransactionById(String transactionId) async {
|
Future<PorterTransactionModel?> getTransactionById(String transactionId) async {
|
||||||
|
@ -282,28 +221,31 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
|
||||||
final snapshot = await _database.ref().child('porterTransactions/$transactionId').get();
|
final snapshot = await _database.ref().child('porterTransactions/$transactionId').get();
|
||||||
|
|
||||||
if (snapshot.exists && snapshot.value != null) {
|
if (snapshot.exists && snapshot.value != null) {
|
||||||
final result = Map<String, dynamic>.from(snapshot.value as Map);
|
// Safe cast: Konversi Map<Object?, Object?> ke Map<String, dynamic>
|
||||||
|
final Map<dynamic, dynamic> rawData = snapshot.value as Map;
|
||||||
|
final Map<String, dynamic> result = {};
|
||||||
|
|
||||||
|
// Proses setiap entry untuk memastikan keys sebagai string
|
||||||
|
rawData.forEach((key, value) {
|
||||||
|
String keyStr = key.toString();
|
||||||
|
|
||||||
|
// Perlakukan rejectionInfo secara khusus
|
||||||
|
if (keyStr == 'rejectionInfo' && value is Map) {
|
||||||
|
Map<String, dynamic> rejectionMap = {};
|
||||||
|
(value as Map).forEach((rKey, rValue) {
|
||||||
|
rejectionMap[rKey.toString()] = rValue;
|
||||||
|
});
|
||||||
|
result[keyStr] = rejectionMap;
|
||||||
|
} else {
|
||||||
|
result[keyStr] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
log('[TransactionPorterRepo] Data transaksi ditemukan untuk ID: $transactionId');
|
log('[TransactionPorterRepo] Data transaksi ditemukan untuk ID: $transactionId');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
log('[TransactionPorterRepo] Data transaksi TIDAK ditemukan untuk ID: $transactionId');
|
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;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('[TransactionPorterRepo] Error mengambil data transaksi porter: $e');
|
log('[TransactionPorterRepo] Error mengambil data transaksi porter: $e');
|
||||||
|
@ -338,6 +280,96 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> rejectTransaction({
|
||||||
|
required String transactionId,
|
||||||
|
required String reason,
|
||||||
|
}) async {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log('[TransactionPorterRepo] Menolak transaksi: $transactionId dengan alasan: $reason');
|
||||||
|
|
||||||
|
// 1. Dapatkan data transaksi sekali dan simpan dalam variabel
|
||||||
|
final transactionData = await getPorterTransactionById(transactionId);
|
||||||
|
if (transactionData == null) {
|
||||||
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Transaksi tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
final String porterOnlineId = transactionData['porterOnlineId'] ?? '';
|
||||||
|
if (porterOnlineId.isEmpty) {
|
||||||
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'ID Porter tidak ditemukan pada transaksi');
|
||||||
|
}
|
||||||
|
|
||||||
|
final rejectionId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
|
||||||
|
// Data yang akan digunakan untuk menyimpan di kedua tempat (Realtime Database & Firestore)
|
||||||
|
final rejectionData = {
|
||||||
|
'idPassenger': transactionData['idPassenger'] ?? '',
|
||||||
|
'kodePorter': transactionData['kodePorter'] ?? '',
|
||||||
|
'locationPassenger': transactionData['locationPassenger'] ?? '',
|
||||||
|
'locationPoter': transactionData['locationPorter'] ?? '',
|
||||||
|
'porterId': porterOnlineId,
|
||||||
|
'porterUserId': transactionData['porterUserId'] ?? '',
|
||||||
|
'ticketId': transactionData['ticketId'] ?? '',
|
||||||
|
'transactionId': transactionData['transactionId'] ?? '',
|
||||||
|
'status': 'rejected',
|
||||||
|
'reason': reason,
|
||||||
|
'timestamp': now
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Mulai Batch untuk mengelola multiple writes dengan lebih efisien di Firestore
|
||||||
|
final WriteBatch batch = _firestore.batch();
|
||||||
|
final transactionRef = _firestore.collection('porterTransactions').doc(transactionId);
|
||||||
|
final rejectionRef = _firestore.collection('porterRejections').doc(rejectionId);
|
||||||
|
|
||||||
|
// 3. Menyimpan riwayat penolakan di Firestore menggunakan Batch
|
||||||
|
batch.set(rejectionRef, rejectionData);
|
||||||
|
|
||||||
|
// 4. Update transaksi untuk tampilan di tab "Ditolak"
|
||||||
|
batch.update(transactionRef, {
|
||||||
|
'updatedAt': now,
|
||||||
|
'locationPorter': null,
|
||||||
|
'porterOnlineId': null,
|
||||||
|
'porterUserId': null,
|
||||||
|
'status': 'rejected',
|
||||||
|
'rejectionInfo': {'reason': reason, 'timestamp': now, 'status': 'rejected'},
|
||||||
|
});
|
||||||
|
|
||||||
|
await _database.ref().child('porterTransactions/$transactionId').update({
|
||||||
|
'updatedAt': now,
|
||||||
|
'locationPorter': null,
|
||||||
|
'porterOnlineId': null,
|
||||||
|
'porterUserId': null,
|
||||||
|
'status': 'rejected',
|
||||||
|
'rejectionInfo': {'reason': reason, 'timestamp': now, 'status': 'rejected'},
|
||||||
|
});
|
||||||
|
|
||||||
|
log('[Repository_impl] ID Transaction Porter: $transactionId');
|
||||||
|
|
||||||
|
// 5. Reset status porter supaya tersedia kembali
|
||||||
|
final porterRef = _firestore.collection('porterOnline').doc(porterOnlineId);
|
||||||
|
batch.update(porterRef, {
|
||||||
|
'idTransaction': null,
|
||||||
|
'idUser': null,
|
||||||
|
'isAvailable': true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Commit batch ke Firestore
|
||||||
|
await batch.commit();
|
||||||
|
|
||||||
|
log('[TransactionPorterRepo] Transaksi berhasil ditolak dan dikembalikan ke antrian di Firestore');
|
||||||
|
|
||||||
|
// 7. Simpan juga ke Realtime Database untuk pencatatan sementara
|
||||||
|
await _database.ref().child('porterRejections/$rejectionId').set(rejectionData);
|
||||||
|
|
||||||
|
log('[TransactionPorterRepo] Data penolakan berhasil disimpan di Realtime Database');
|
||||||
|
} catch (e) {
|
||||||
|
log('[TransactionPorterRepo] Error menolak transaksi: ${e.toString()}', level: 4);
|
||||||
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Gagal menolak transaksi: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> completePorterTransaction({
|
Future<void> completePorterTransaction({
|
||||||
required String transactionId,
|
required String transactionId,
|
||||||
|
@ -367,126 +399,157 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @override
|
@override
|
||||||
// Future<void> updateTransactionStatus({
|
Future<String?> reassignRejectedTransaction({
|
||||||
// required String ticketId,
|
required String transactionId,
|
||||||
// required String transactionId,
|
String? newPorterId,
|
||||||
// required String status,
|
}) async {
|
||||||
// }) async {
|
try {
|
||||||
// try {
|
log('[TransactionPorterRepo] Mencoba mengalihkan transaksi: $transactionId');
|
||||||
// final now = DateTime.now();
|
|
||||||
// log('Updating transaction status: $transactionId to $status');
|
|
||||||
|
|
||||||
// // Update di Firestore, coba dengan transactionId langsung
|
// 1. Dapatkan data transaksi yang ditolak
|
||||||
// try {
|
final rejectedTransaction = await getPorterTransactionById(transactionId);
|
||||||
// await _firestore.collection('porterTransactions').doc(transactionId).update({
|
if (rejectedTransaction == null) {
|
||||||
// 'status': status,
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Transaksi tidak ditemukan');
|
||||||
// 'updatedAt': now,
|
}
|
||||||
// });
|
|
||||||
// log('Successfully updated transaction in Firestore with direct ID');
|
// Verifikasi bahwa transaksi ini memang ditolak
|
||||||
// } catch (e) {
|
final status = rejectedTransaction['status'] as String? ?? '';
|
||||||
// log('Failed to update Firestore with direct ID: $e');
|
final hasRejectionInfo = rejectedTransaction.containsKey('rejectionInfo');
|
||||||
|
|
||||||
// // Coba dengan format ticketId-transactionId jika direct ID gagal
|
if (status != 'rejected' && !hasRejectionInfo) {
|
||||||
// if (ticketId.isNotEmpty) {
|
throw FirebaseException(
|
||||||
// final combinedId = '$ticketId-$transactionId';
|
plugin: 'TransactionPorterRepo',
|
||||||
// log('Trying update with combined ID: $combinedId');
|
message: 'Transaksi tidak dalam status ditolak'
|
||||||
|
);
|
||||||
// try {
|
}
|
||||||
// await _firestore.collection('porterTransactions').doc(combinedId).update({
|
|
||||||
// 'status': status,
|
// 2. Tandai transaksi lama sebagai sudah dialihkan
|
||||||
// 'updatedAt': now,
|
await _database.ref().child('porterTransactions/$transactionId').update({
|
||||||
// });
|
'isReassigned': true,
|
||||||
// log('Successfully updated transaction in Firestore with combined ID');
|
});
|
||||||
// } catch (e2) {
|
|
||||||
// log('Failed to update Firestore with combined ID: $e2');
|
// 3. Cari porter yang tersedia (jika tidak ada yang ditentukan)
|
||||||
// }
|
String selectedPorterId = '';
|
||||||
// }
|
String porterUserId = '';
|
||||||
// }
|
String porterLocation = '';
|
||||||
|
|
||||||
// // Mencari transaksi di Realtime DB
|
if (newPorterId != null && newPorterId.isNotEmpty) {
|
||||||
// // Perlu memeriksa semua porter untuk menemukan transaksi yang tepat
|
// Gunakan porter yang ditentukan
|
||||||
// final dbRef = _database.ref().child('porterTransactions');
|
final porterDoc = await _firestore.collection('porterOnline').doc(newPorterId).get();
|
||||||
// final dataSnapshot = await dbRef.get();
|
|
||||||
|
if (!porterDoc.exists) {
|
||||||
// if (dataSnapshot.exists && dataSnapshot.value is Map) {
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Porter yang ditentukan tidak ditemukan');
|
||||||
// final allData = dataSnapshot.value as Map<dynamic, dynamic>;
|
}
|
||||||
|
|
||||||
// // Iterasi semua porter IDs
|
final porterData = porterDoc.data();
|
||||||
// for (var porterId in allData.keys) {
|
if (porterData == null || porterData['isAvailable'] != true) {
|
||||||
// final porterData = allData[porterId];
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Porter yang ditentukan tidak tersedia');
|
||||||
|
}
|
||||||
// if (porterData is Map<dynamic, dynamic> && porterData.containsKey(transactionId)) {
|
|
||||||
// log('Found transaction in Realtime DB under porter: $porterId');
|
selectedPorterId = newPorterId;
|
||||||
|
porterUserId = porterData['userId'] ?? '';
|
||||||
// // Update status di Realtime DB
|
porterLocation = porterData['location'] ?? '';
|
||||||
// final transactionRef =
|
} else {
|
||||||
// _database.ref().child('porterTransactions').child(porterId.toString()).child(transactionId);
|
// Cari porter yang tersedia
|
||||||
|
final availablePortersSnapshot = await _firestore
|
||||||
// await transactionRef.update({
|
.collection('porterOnline')
|
||||||
// 'status': status,
|
.where('isAvailable', isEqualTo: true)
|
||||||
// 'updatedAt': now.millisecondsSinceEpoch,
|
.limit(1)
|
||||||
// });
|
.get();
|
||||||
|
|
||||||
// log('Successfully updated transaction status in Realtime DB');
|
if (availablePortersSnapshot.docs.isEmpty) {
|
||||||
// break;
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Tidak ada porter tersedia saat ini');
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// }
|
final porterDoc = availablePortersSnapshot.docs.first;
|
||||||
|
final porterData = porterDoc.data();
|
||||||
// log('Status update process completed for transaction: $transactionId');
|
|
||||||
// } catch (e) {
|
selectedPorterId = porterDoc.id;
|
||||||
// log('Error updating transaction status: $e');
|
porterUserId = porterData['userId'] ?? '';
|
||||||
// throw Exception('Failed to update transaction status: $e');
|
porterLocation = porterData['location'] ?? '';
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
log('[TransactionPorterRepo] Porter baru dipilih: $selectedPorterId (userId: $porterUserId)');
|
||||||
// @override
|
|
||||||
// Future<void> completePorterTransaction({
|
// 4. Buat transaksi baru dengan referensi ke transaksi lama
|
||||||
// required String ticketId,
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
// required String transactionId,
|
final newTransactionId = '${now}_${100000 + (now % 900000)}';
|
||||||
// required String porterOnlineId,
|
|
||||||
// }) async {
|
// Data untuk transaksi baru
|
||||||
// try {
|
final newTransactionData = <String, dynamic>{
|
||||||
// final now = DateTime.now();
|
'transactionId': rejectedTransaction['transactionId'] ?? '',
|
||||||
// log('Completing porter transaction: $transactionId and updating porterOnline: $porterOnlineId');
|
'ticketId': rejectedTransaction['ticketId'] ?? '',
|
||||||
|
'idPassenger': rejectedTransaction['idPassenger'] ?? '',
|
||||||
// await updateTransactionStatus(
|
'locationPassenger': rejectedTransaction['locationPassenger'] ?? '',
|
||||||
// ticketId: ticketId,
|
'kodePorter': rejectedTransaction['kodePorter'] ?? '',
|
||||||
// transactionId: transactionId,
|
'status': 'pending',
|
||||||
// status: 'selesai',
|
'createdAt': now,
|
||||||
// );
|
'porterOnlineId': selectedPorterId,
|
||||||
|
'porterUserId': porterUserId,
|
||||||
// try {
|
'locationPorter': porterLocation,
|
||||||
// await _firestore.collection('porterOnline').doc(porterOnlineId).update({
|
'previousTransactionId': transactionId, // Referensi ke transaksi yang ditolak
|
||||||
// 'idTransaction': null,
|
};
|
||||||
// 'idUser': null,
|
|
||||||
// 'isAvailable': true,
|
// Jika ada rejectionInfo, tambahkan alasan pengalihan
|
||||||
// 'onlineAt': now,
|
if (hasRejectionInfo && rejectedTransaction['rejectionInfo'] is Map) {
|
||||||
// });
|
final rejectionInfoMap = rejectedTransaction['rejectionInfo'] as Map<dynamic, dynamic>;
|
||||||
// log('Successfully updated porterOnline data in Firestore');
|
final reason = rejectionInfoMap['reason']?.toString() ?? 'Ditolak oleh porter sebelumnya';
|
||||||
// } catch (e) {
|
|
||||||
// log('Error updating porterOnline data in Firestore: $e');
|
newTransactionData['reassignmentInfo'] = {
|
||||||
// throw Exception('Gagal mengupdate data porterOnline di Firestore: $e');
|
'previousTransactionId': transactionId,
|
||||||
// }
|
'reason': reason,
|
||||||
|
'timestamp': now,
|
||||||
// // try {
|
};
|
||||||
// // final porterOnlineRef = _database.ref().child('porterOnline').child(porterOnlineId);
|
}
|
||||||
// // await porterOnlineRef.update({
|
|
||||||
// // 'idTransaction': null,
|
// 5. Simpan transaksi baru ke Realtime Database dan Firestore
|
||||||
// // 'idUser': null,
|
final batch = _firestore.batch();
|
||||||
// // 'isAvailable': true,
|
|
||||||
// // 'onlineAt': now.millisecondsSinceEpoch,
|
// Ke Realtime Database
|
||||||
// // });
|
await _database.ref().child('porterTransactions/$newTransactionId').set(newTransactionData);
|
||||||
// // log('Successfully updated porterOnline data in Realtime DB');
|
|
||||||
// // } catch (e) {
|
// Firestore - update porter
|
||||||
// // log('Error updating porterOnline data in Realtime DB: $e');
|
final porterRef = _firestore.collection('porterOnline').doc(selectedPorterId);
|
||||||
// // }
|
batch.update(porterRef, {
|
||||||
|
'isAvailable': false,
|
||||||
// log('Porter transaction completion process finished successfully');
|
'idTransaction': newTransactionId,
|
||||||
// } catch (e) {
|
'idUser': rejectedTransaction['idPassenger'] ?? '',
|
||||||
// log('Error completing porter transaction: $e');
|
});
|
||||||
// throw Exception('Gagal menyelesaikan transaksi porter: $e');
|
|
||||||
// }
|
// Commit Firestore batch
|
||||||
// }
|
await batch.commit();
|
||||||
|
|
||||||
|
// 6. Tambahkan ke porterHistory
|
||||||
|
if (porterUserId.isNotEmpty) {
|
||||||
|
await _database.ref().child('porterHistory/$porterUserId/$newTransactionId').set({
|
||||||
|
'timestamp': now,
|
||||||
|
'transactionId': newTransactionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan ke history porterId juga
|
||||||
|
if (selectedPorterId.isNotEmpty) {
|
||||||
|
await _database.ref().child('porterHistory/$selectedPorterId/$newTransactionId').set({
|
||||||
|
'timestamp': now,
|
||||||
|
'transactionId': newTransactionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Tambahkan ke passengerHistory
|
||||||
|
final passengerId = rejectedTransaction['idPassenger'] ?? '';
|
||||||
|
if (passengerId.isNotEmpty) {
|
||||||
|
await _database.ref().child('passengerHistory/$passengerId/$newTransactionId').set({
|
||||||
|
'timestamp': now,
|
||||||
|
'transactionId': newTransactionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log('[TransactionPorterRepo] Transaksi berhasil dialihkan ke porter baru. ID baru: $newTransactionId');
|
||||||
|
return newTransactionId;
|
||||||
|
} catch (e) {
|
||||||
|
log('[TransactionPorterRepo] Error mengalihkan transaksi: $e', level: 4);
|
||||||
|
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Gagal mengalihkan transaksi: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// domain/models/transaction_porter_model.dart
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
class PorterTransactionModel {
|
class PorterTransactionModel {
|
||||||
|
@ -15,6 +16,7 @@ class PorterTransactionModel {
|
||||||
final String transactionId;
|
final String transactionId;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
final RejectionInfo? rejectionInfo;
|
||||||
|
|
||||||
PorterTransactionModel({
|
PorterTransactionModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -29,25 +31,60 @@ class PorterTransactionModel {
|
||||||
required this.transactionId,
|
required this.transactionId,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
}) : normalizedStatus = _normalizeStatus(status);
|
this.rejectionInfo,
|
||||||
|
}) : normalizedStatus = _normalizeStatus(status, rejectionInfo);
|
||||||
|
|
||||||
|
static String _normalizeStatus(String status, RejectionInfo? rejectionInfo) {
|
||||||
|
if (rejectionInfo != null) {
|
||||||
|
return "rejected";
|
||||||
|
}
|
||||||
|
|
||||||
// Simplify status normalization since we only have 3 possible statuses
|
|
||||||
static String _normalizeStatus(String status) {
|
|
||||||
final lowercaseStatus = status.toLowerCase().trim();
|
final lowercaseStatus = status.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Status standar
|
||||||
if (lowercaseStatus == "pending") return "pending";
|
if (lowercaseStatus == "pending") return "pending";
|
||||||
if (lowercaseStatus == "proses") return "proses";
|
if (lowercaseStatus == "proses") return "proses";
|
||||||
if (lowercaseStatus == "selesai") return "selesai";
|
if (lowercaseStatus == "selesai") return "selesai";
|
||||||
|
if (lowercaseStatus == "rejected" || lowercaseStatus == "ditolak") return "rejected";
|
||||||
|
|
||||||
// Default fallback logic for potentially inconsistent data
|
// Logic fallback untuk menangani potensi status yang tidak konsisten
|
||||||
if (lowercaseStatus.contains("pend")) return "pending";
|
if (lowercaseStatus.contains("pend")) return "pending";
|
||||||
if (lowercaseStatus.contains("pros")) return "proses";
|
if (lowercaseStatus.contains("pros")) return "proses";
|
||||||
if (lowercaseStatus.contains("sele") || lowercaseStatus.contains("done")) return "selesai";
|
if (lowercaseStatus.contains("sele") || lowercaseStatus.contains("done")) return "selesai";
|
||||||
|
if (lowercaseStatus.contains("rej") || lowercaseStatus.contains("tol")) return "rejected";
|
||||||
|
|
||||||
return "pending"; // Default to pending if unknown status
|
return "pending";
|
||||||
}
|
}
|
||||||
|
|
||||||
factory PorterTransactionModel.fromJson(Map<dynamic, dynamic> json, String id) {
|
factory PorterTransactionModel.fromJson(Map<dynamic, dynamic> json, String id) {
|
||||||
|
RejectionInfo? rejectionInfo;
|
||||||
|
if (json.containsKey('rejectionInfo')) {
|
||||||
|
try {
|
||||||
|
final rejectionData = json['rejectionInfo'];
|
||||||
|
if (rejectionData is Map) {
|
||||||
|
final Map<String, dynamic> safeRejectionData =
|
||||||
|
Map<String, dynamic>.from(rejectionData.map((key, value) => MapEntry(key.toString(), value)));
|
||||||
|
|
||||||
|
rejectionInfo = RejectionInfo(
|
||||||
|
reason: safeRejectionData['reason']?.toString() ?? 'Tidak ada alasan',
|
||||||
|
timestamp: safeRejectionData.containsKey('timestamp')
|
||||||
|
? (safeRejectionData['timestamp'] is int
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(safeRejectionData['timestamp'])
|
||||||
|
: DateTime.now())
|
||||||
|
: DateTime.now(),
|
||||||
|
status: safeRejectionData['status']?.toString() ?? 'rejected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('[Transaction Porter Model] Error parsing rejectionInfo: $e');
|
||||||
|
rejectionInfo = RejectionInfo(
|
||||||
|
reason: 'Data tidak valid',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
status: 'rejected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return PorterTransactionModel(
|
return PorterTransactionModel(
|
||||||
id: id,
|
id: id,
|
||||||
kodePorter: json['kodePorter'] ?? '',
|
kodePorter: json['kodePorter'] ?? '',
|
||||||
|
@ -56,7 +93,7 @@ class PorterTransactionModel {
|
||||||
locationPassenger: json['locationPassenger'] as String? ?? '',
|
locationPassenger: json['locationPassenger'] as String? ?? '',
|
||||||
locationPorter: json['locationPorter'] as String? ?? '',
|
locationPorter: json['locationPorter'] as String? ?? '',
|
||||||
porterOnlineId: json['porterOnlineId'] as String? ?? '',
|
porterOnlineId: json['porterOnlineId'] as String? ?? '',
|
||||||
status: json['status'] as String? ?? 'Data Not Fount',
|
status: json['status'] as String? ?? 'Data Not Found',
|
||||||
ticketId: json['ticketId'] as String? ?? '',
|
ticketId: json['ticketId'] as String? ?? '',
|
||||||
transactionId: json['transactionId'] as String? ?? '',
|
transactionId: json['transactionId'] as String? ?? '',
|
||||||
createdAt: json['createdAt'] is int
|
createdAt: json['createdAt'] is int
|
||||||
|
@ -69,6 +106,92 @@ class PorterTransactionModel {
|
||||||
? DateTime.fromMillisecondsSinceEpoch(json['updatedAt'])
|
? DateTime.fromMillisecondsSinceEpoch(json['updatedAt'])
|
||||||
: (json['updatedAt'] is DateTime ? json['updatedAt'] : null))
|
: (json['updatedAt'] is DateTime ? json['updatedAt'] : null))
|
||||||
: null,
|
: null,
|
||||||
|
rejectionInfo: rejectionInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = {
|
||||||
|
'id': id,
|
||||||
|
'kodePorter': kodePorter,
|
||||||
|
'porterUserId': porterUserId,
|
||||||
|
'idPassenger': idPassenger,
|
||||||
|
'locationPassenger': locationPassenger,
|
||||||
|
'locationPorter': locationPorter,
|
||||||
|
'porterOnlineId': porterOnlineId,
|
||||||
|
'status': status,
|
||||||
|
'ticketId': ticketId,
|
||||||
|
'transactionId': transactionId,
|
||||||
|
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updatedAt != null) {
|
||||||
|
data['updatedAt'] = updatedAt!.millisecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rejectionInfo != null) {
|
||||||
|
data['rejectionInfo'] = rejectionInfo!.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Membuat salinan model dengan nilai baru
|
||||||
|
PorterTransactionModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? kodePorter,
|
||||||
|
String? porterUserId,
|
||||||
|
String? idPassenger,
|
||||||
|
String? locationPassenger,
|
||||||
|
String? locationPorter,
|
||||||
|
String? porterOnlineId,
|
||||||
|
String? status,
|
||||||
|
String? ticketId,
|
||||||
|
String? transactionId,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
RejectionInfo? rejectionInfo,
|
||||||
|
}) {
|
||||||
|
return PorterTransactionModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
kodePorter: kodePorter ?? this.kodePorter,
|
||||||
|
porterUserId: porterUserId ?? this.porterUserId,
|
||||||
|
idPassenger: idPassenger ?? this.idPassenger,
|
||||||
|
locationPassenger: locationPassenger ?? this.locationPassenger,
|
||||||
|
locationPorter: locationPorter ?? this.locationPorter,
|
||||||
|
porterOnlineId: porterOnlineId ?? this.porterOnlineId,
|
||||||
|
status: status ?? this.status,
|
||||||
|
ticketId: ticketId ?? this.ticketId,
|
||||||
|
transactionId: transactionId ?? this.transactionId,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
rejectionInfo: rejectionInfo ?? this.rejectionInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RejectionInfo {
|
||||||
|
final String reason;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
RejectionInfo({required this.reason, required this.timestamp, this.status = 'rejected'});
|
||||||
|
|
||||||
|
factory RejectionInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RejectionInfo(
|
||||||
|
reason: json['reason'] ?? 'Tidak ada alasan',
|
||||||
|
timestamp: json['timestamp'] is Timestamp
|
||||||
|
? (json['timestamp'] as Timestamp).toDate()
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] ?? 0),
|
||||||
|
status: json['status'] ?? 'rejected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'reason': reason,
|
||||||
|
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||||
|
'status': status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,19 +5,29 @@ abstract class TransactionPorterRepository {
|
||||||
|
|
||||||
Stream<PorterTransactionModel?> watchTransactionById(String transactionId);
|
Stream<PorterTransactionModel?> watchTransactionById(String transactionId);
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId);
|
||||||
|
|
||||||
Future<PorterTransactionModel?> getTransactionById(String transactionId);
|
Future<PorterTransactionModel?> getTransactionById(String transactionId);
|
||||||
|
|
||||||
Future<List<String>> getPorterTransactionIds(String porterId);
|
Future<List<String>> getPorterTransactionIds(String porterId);
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId);
|
|
||||||
|
|
||||||
Future<void> updateTransactionStatus({
|
Future<void> updateTransactionStatus({
|
||||||
required String transactionId,
|
required String transactionId,
|
||||||
required String status,
|
required String status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<void> rejectTransaction({
|
||||||
|
required String transactionId,
|
||||||
|
required String reason,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> completePorterTransaction({
|
Future<void> completePorterTransaction({
|
||||||
required String transactionId,
|
required String transactionId,
|
||||||
required String porterOnlineId,
|
required String porterOnlineId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<String?> reassignRejectedTransaction({
|
||||||
|
required String transactionId,
|
||||||
|
String? newPorterId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,10 @@ class TransactionPorterUsecase {
|
||||||
return _repository.watchTransactionById(transactionId);
|
return _repository.watchTransactionById(transactionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) {
|
||||||
|
return _repository.getPorterTransactionById(transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<PorterTransactionModel?> getTransactionById(String transactionId) {
|
Future<PorterTransactionModel?> getTransactionById(String transactionId) {
|
||||||
return _repository.getTransactionById(transactionId);
|
return _repository.getTransactionById(transactionId);
|
||||||
}
|
}
|
||||||
|
@ -21,31 +25,43 @@ class TransactionPorterUsecase {
|
||||||
return _repository.getPorterTransactionIds(porterId);
|
return _repository.getPorterTransactionIds(porterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) {
|
|
||||||
return _repository.getPorterTransactionById(transactionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateTransactionStatus({
|
Future<void> updateTransactionStatus({
|
||||||
// required String ticketId,
|
|
||||||
required String transactionId,
|
required String transactionId,
|
||||||
required String status,
|
required String status,
|
||||||
}) {
|
}) {
|
||||||
return _repository.updateTransactionStatus(
|
return _repository.updateTransactionStatus(
|
||||||
// ticketId: ticketId,
|
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
status: status,
|
status: status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> rejectTransaction({
|
||||||
|
required String transactionId,
|
||||||
|
required String reason,
|
||||||
|
}) {
|
||||||
|
return _repository.rejectTransaction(
|
||||||
|
transactionId: transactionId,
|
||||||
|
reason: reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> completePorterTransaction({
|
Future<void> completePorterTransaction({
|
||||||
// required String ticketId,
|
|
||||||
required String transactionId,
|
required String transactionId,
|
||||||
required String porterOnlineId,
|
required String porterOnlineId,
|
||||||
}) {
|
}) {
|
||||||
return _repository.completePorterTransaction(
|
return _repository.completePorterTransaction(
|
||||||
// ticketId: ticketId,
|
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
porterOnlineId: porterOnlineId,
|
porterOnlineId: porterOnlineId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> reassignRejectedTransaction({
|
||||||
|
required String transactionId,
|
||||||
|
String? newPorterId,
|
||||||
|
}) {
|
||||||
|
return _repository.reassignRejectedTransaction(
|
||||||
|
transactionId: transactionId,
|
||||||
|
newPorterId: newPorterId,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:e_porter/_core/service/preferences_service.dart';
|
||||||
import 'package:e_porter/_core/utils/snackbar/snackbar_helper.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/domain/models/porter_queue_model.dart';
|
||||||
import 'package:e_porter/presentation/controllers/porter_queue_controller.dart';
|
import 'package:e_porter/presentation/controllers/porter_queue_controller.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../domain/models/transaction_porter_model.dart';
|
import '../../domain/models/transaction_porter_model.dart';
|
||||||
import '../../domain/usecases/transaction_porter_usecase.dart';
|
import '../../domain/usecases/transaction_porter_usecase.dart';
|
||||||
|
@ -20,6 +21,8 @@ class TransactionPorterController extends GetxController {
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxString error = ''.obs;
|
final RxString error = ''.obs;
|
||||||
|
|
||||||
|
final TextEditingController rejectionReasonController = TextEditingController();
|
||||||
|
|
||||||
StreamSubscription<List<PorterTransactionModel>>? _subscription;
|
StreamSubscription<List<PorterTransactionModel>>? _subscription;
|
||||||
StreamSubscription<PorterQueueModel?>? _porterSubscription;
|
StreamSubscription<PorterQueueModel?>? _porterSubscription;
|
||||||
|
|
||||||
|
@ -290,6 +293,68 @@ class TransactionPorterController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> rejectTransaction({
|
||||||
|
required String transactionId,
|
||||||
|
required String reason,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
|
||||||
|
log('Menolak transaksi: $transactionId dengan alasan: $reason');
|
||||||
|
|
||||||
|
// Proses penolakan transaksi
|
||||||
|
await _useCase.rejectTransaction(
|
||||||
|
transactionId: transactionId,
|
||||||
|
reason: reason.isEmpty ? 'Tidak ada alasan' : reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Segera coba reassign ke porter lain
|
||||||
|
try {
|
||||||
|
log('Mencoba mengalihkan transaksi yang ditolak ke porter baru...');
|
||||||
|
final newTransactionId = await _useCase.reassignRejectedTransaction(
|
||||||
|
transactionId: transactionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newTransactionId != null) {
|
||||||
|
log('Transaksi berhasil dialihkan ke ID baru: $newTransactionId');
|
||||||
|
SnackbarHelper.showSuccess('Berhasil', 'Transaksi dialihkan ke porter lain');
|
||||||
|
} else {
|
||||||
|
// Jika tidak ada porter tersedia saat ini, service di background akan mencoba lagi nanti
|
||||||
|
log('Tidak ada porter tersedia saat ini, akan dicoba lagi nanti oleh service');
|
||||||
|
}
|
||||||
|
} catch (reassignError) {
|
||||||
|
log('Error saat mencoba mengalihkan transaksi: $reassignError');
|
||||||
|
// Tidak perlu menampilkan error ke user, service akan mencoba lagi nanti
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dapatkan transaksi yang diperbarui
|
||||||
|
final updatedTransaction = await getTransactionById(transactionId);
|
||||||
|
|
||||||
|
// Update list transaksi yang ada dengan yang baru
|
||||||
|
if (updatedTransaction != null) {
|
||||||
|
final index = transactions.indexWhere((tx) => tx.id == transactionId);
|
||||||
|
if (index >= 0) {
|
||||||
|
transactions[index] = updatedTransaction;
|
||||||
|
log('Transaksi di daftar utama diperbarui menjadi ditolak: $transactionId');
|
||||||
|
} else {
|
||||||
|
refreshTransactions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset controller alasan
|
||||||
|
rejectionReasonController.clear();
|
||||||
|
|
||||||
|
SnackbarHelper.showSuccess('Berhasil', 'Transaksi berhasil ditolak');
|
||||||
|
} catch (e) {
|
||||||
|
log('Error menolak transaksi: $e');
|
||||||
|
error.value = 'Gagal menolak transaksi: $e';
|
||||||
|
SnackbarHelper.showError('Terjadi Kesalahan', 'Gagal menolak transaksi');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Metode serupa untuk completePorterTransaction
|
// Metode serupa untuk completePorterTransaction
|
||||||
Future<void> completePorterTransaction({
|
Future<void> completePorterTransaction({
|
||||||
required String transactionId,
|
required String transactionId,
|
||||||
|
@ -365,6 +430,7 @@ class TransactionPorterController extends GetxController {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
rejectionReasonController.dispose();
|
||||||
_porterSubscription?.cancel();
|
_porterSubscription?.cancel();
|
||||||
_subscription?.cancel();
|
_subscription?.cancel();
|
||||||
for (var subscription in _transactionWatchers.values) {
|
for (var subscription in _transactionWatchers.values) {
|
||||||
|
@ -373,4 +439,51 @@ class TransactionPorterController extends GetxController {
|
||||||
_transactionWatchers.clear();
|
_transactionWatchers.clear();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showRejectionDialog(String transactionId, BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Tolak Permintaan Porter'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Masukkan alasan penolakan:'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextField(
|
||||||
|
controller: rejectionReasonController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Contoh: Sedang melayani penumpang lain',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
rejectionReasonController.clear();
|
||||||
|
},
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final reason = rejectionReasonController.text.trim();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
rejectTransaction(
|
||||||
|
transactionId: transactionId,
|
||||||
|
reason: reason,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Tolak'),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,16 +27,14 @@ class DetailHistoryPorterScreen extends StatefulWidget {
|
||||||
class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
final TransactionPorterController _porterController = Get.find<TransactionPorterController>();
|
final TransactionPorterController _porterController = Get.find<TransactionPorterController>();
|
||||||
final HistoryController _historyController = Get.find<HistoryController>();
|
final HistoryController _historyController = Get.find<HistoryController>();
|
||||||
final RxBool _isLoadingTicket = false.obs;
|
|
||||||
|
|
||||||
PorterTransactionModel? porterTransaction;
|
PorterTransactionModel? porterTransaction;
|
||||||
|
|
||||||
late final String porterTransactionId;
|
late final String porterTransactionId;
|
||||||
|
|
||||||
// Formatters
|
final RxBool _isLoadingTicket = false.obs;
|
||||||
final DateFormat _dateFormat = DateFormat('dd MMMM yyyy', 'en_US');
|
final DateFormat _dateFormat = DateFormat('dd MMMM yyyy', 'en_US');
|
||||||
final DateFormat _timeFormat = DateFormat.jm();
|
final DateFormat _timeFormat = DateFormat.jm();
|
||||||
// final NumberFormat _priceFormatter = NumberFormat.decimalPattern('id_ID');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -114,6 +112,10 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
_buildLocationPassenger(porterTransaction),
|
_buildLocationPassenger(porterTransaction),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
_buildDetailsOrder(ticketTransaction),
|
_buildDetailsOrder(ticketTransaction),
|
||||||
|
if (porterTransaction.normalizedStatus == 'rejected') ...[
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
_buildRejectionInfo(porterTransaction),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -128,8 +130,22 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
switch (transaction.normalizedStatus) {
|
switch (transaction.normalizedStatus) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return CustomeShadowCotainner(
|
return CustomeShadowCotainner(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
child: ButtonFill(
|
child: ButtonFill(
|
||||||
text: 'Terima Orderan',
|
text: 'Tolak',
|
||||||
|
textColor: Colors.white,
|
||||||
|
backgroundColor: RedColors.red500,
|
||||||
|
onTap: () {
|
||||||
|
_porterController.showRejectionDialog(porterTransactionId, context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: ButtonFill(
|
||||||
|
text: 'Terima',
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_porterController.updateTransactionStatus(
|
_porterController.updateTransactionStatus(
|
||||||
|
@ -138,6 +154,9 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'proses':
|
case 'proses':
|
||||||
|
@ -154,6 +173,7 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'selesai':
|
case 'selesai':
|
||||||
|
case 'rejected':
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -282,6 +302,59 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildRejectionInfo(PorterTransactionModel transaction) {
|
||||||
|
// Tampilkan info penolakan jika tersedia
|
||||||
|
if (transaction.rejectionInfo == null) {
|
||||||
|
return CustomeShadowCotainner(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_componentHeaderText(
|
||||||
|
text: 'Informasi Penolakan',
|
||||||
|
svgIcon: 'assets/icons/ic_info.svg',
|
||||||
|
),
|
||||||
|
TypographyStyles.body(
|
||||||
|
'Tidak ada informasi penolakan',
|
||||||
|
color: GrayColors.gray500,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rejectionDate = _dateFormat.format(transaction.rejectionInfo!.timestamp);
|
||||||
|
final rejectionTime = _timeFormat.format(transaction.rejectionInfo!.timestamp);
|
||||||
|
|
||||||
|
return CustomeShadowCotainner(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, color: Colors.red, size: 24.sp),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
TypographyStyles.body(
|
||||||
|
'Informasi Penolakan',
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 10.h),
|
||||||
|
child: Divider(thickness: 1, color: GrayColors.gray200),
|
||||||
|
),
|
||||||
|
_componentRowText(label: 'Alasan', value: transaction.rejectionInfo!.reason),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
_componentRowText(label: 'Tanggal Penolakan', value: rejectionDate),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
_componentRowText(label: 'Waktu Penolakan', value: rejectionTime),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _componentHeaderText({required String text, required String svgIcon}) {
|
Widget _componentHeaderText({required String text, required String svgIcon}) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -328,7 +401,21 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper untuk warna status
|
String _getStatusText(PorterTransactionModel transaction) {
|
||||||
|
switch (transaction.normalizedStatus) {
|
||||||
|
case 'pending':
|
||||||
|
return 'Menunggu';
|
||||||
|
case 'proses':
|
||||||
|
return 'Dalam Proses';
|
||||||
|
case 'selesai':
|
||||||
|
return 'Selesai';
|
||||||
|
case 'rejected':
|
||||||
|
return 'Ditolak';
|
||||||
|
default:
|
||||||
|
return transaction.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Color _getStatusColor(PorterTransactionModel? transaction) {
|
Color _getStatusColor(PorterTransactionModel? transaction) {
|
||||||
if (transaction == null) return GrayColors.gray400;
|
if (transaction == null) return GrayColors.gray400;
|
||||||
|
|
||||||
|
@ -339,12 +426,13 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
return PrimaryColors.primary800;
|
return PrimaryColors.primary800;
|
||||||
case 'selesai':
|
case 'selesai':
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
|
case 'rejected':
|
||||||
|
return Colors.red;
|
||||||
default:
|
default:
|
||||||
return GrayColors.gray400;
|
return GrayColors.gray400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper untuk icon status
|
|
||||||
IconData _getStatusIcon(PorterTransactionModel? transaction) {
|
IconData _getStatusIcon(PorterTransactionModel? transaction) {
|
||||||
if (transaction == null) return Icons.info_outline;
|
if (transaction == null) return Icons.info_outline;
|
||||||
|
|
||||||
|
@ -355,6 +443,8 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
return Icons.directions_run;
|
return Icons.directions_run;
|
||||||
case 'selesai':
|
case 'selesai':
|
||||||
return Icons.check_circle_outline;
|
return Icons.check_circle_outline;
|
||||||
|
case 'rejected':
|
||||||
|
return Icons.cancel_outlined;
|
||||||
default:
|
default:
|
||||||
return Icons.info_outline;
|
return Icons.info_outline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,30 +33,6 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
decimalDigits: 0,
|
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 {
|
Future<TransactionModel?> _loadTicketTransaction(String ticketId, String transactionId) async {
|
||||||
final cacheKey = "$ticketId-$transactionId";
|
final cacheKey = "$ticketId-$transactionId";
|
||||||
if (_ticketTransactionCache.containsKey(cacheKey)) {
|
if (_ticketTransactionCache.containsKey(cacheKey)) {
|
||||||
|
@ -84,7 +60,7 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 4,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: GrayColors.gray50,
|
backgroundColor: GrayColors.gray50,
|
||||||
appBar: SimpleAppbarComponent(
|
appBar: SimpleAppbarComponent(
|
||||||
|
@ -102,6 +78,7 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
_buildTransactionList('pending'),
|
_buildTransactionList('pending'),
|
||||||
_buildTransactionList('proses'),
|
_buildTransactionList('proses'),
|
||||||
_buildTransactionList('selesai'),
|
_buildTransactionList('selesai'),
|
||||||
|
_buildTransactionList('rejected'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -129,11 +106,12 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
labelColor: PrimaryColors.primary800,
|
labelColor: PrimaryColors.primary800,
|
||||||
unselectedLabelColor: GrayColors.gray400,
|
unselectedLabelColor: GrayColors.gray400,
|
||||||
indicatorColor: PrimaryColors.primary800,
|
indicatorColor: PrimaryColors.primary800,
|
||||||
indicatorWeight: 3,
|
indicatorWeight: 4,
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(text: 'Pending'),
|
Tab(text: 'Pending'),
|
||||||
Tab(text: 'Proses'),
|
Tab(text: 'Proses'),
|
||||||
Tab(text: 'Selesai'),
|
Tab(text: 'Selesai'),
|
||||||
|
Tab(text: 'Ditolak'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -184,7 +162,7 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, color: Colors.red, size: 20.w),
|
Icon(Icons.error_outline, color: RedColors.red500, size: 20.w),
|
||||||
SizedBox(width: 8.w),
|
SizedBox(width: 8.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -215,29 +193,55 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
// Log untuk debug
|
// Log untuk debug
|
||||||
log('Semua transaksi: ${allTransactions.length}');
|
log('Semua transaksi: ${allTransactions.length}');
|
||||||
|
|
||||||
// Filter berdasarkan status
|
// Filter berdasarkan status dengan logika yang diperbaiki
|
||||||
final filteredTransactions = allTransactions.where((tx) => tx.normalizedStatus == statusFilter).toList();
|
final filteredTransactions = allTransactions.where((tx) {
|
||||||
|
// Cek apakah transaksi memiliki rejectionInfo
|
||||||
|
final hasRejectionInfo = tx.rejectionInfo != null;
|
||||||
|
|
||||||
|
// Transaksi yang memiliki rejectionInfo harus masuk ke tab "rejected" saja
|
||||||
|
if (hasRejectionInfo && statusFilter != 'rejected') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaksi yang tidak memiliki rejectionInfo tapi statusnya "rejected"
|
||||||
|
// juga harus masuk ke tab "rejected" saja
|
||||||
|
if (!hasRejectionInfo && tx.normalizedStatus == 'rejected' && statusFilter != 'rejected') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untuk tab "rejected", tampilkan semua transaksi yang ditolak
|
||||||
|
if (statusFilter == 'rejected') {
|
||||||
|
return hasRejectionInfo || tx.normalizedStatus == 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untuk tab lainnya, gunakan status normal
|
||||||
|
return tx.normalizedStatus == statusFilter;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}');
|
log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}');
|
||||||
|
|
||||||
// Jika tidak ada transaksi, tampilkan pesan kosong
|
// Jika tidak ada transaksi, tampilkan pesan kosong
|
||||||
if (filteredTransactions.isEmpty) {
|
if (filteredTransactions.isEmpty) {
|
||||||
// Jika ada error tapi tidak ada transaksi
|
// Jika ada error tapi tidak ada transaksi
|
||||||
if (_porterController.error.value.contains('Porter tidak ditemukan') && statusFilter == 'selesai') {
|
if (_porterController.error.value.contains('Porter tidak ditemukan') &&
|
||||||
|
(statusFilter == 'selesai' || statusFilter == 'rejected')) {
|
||||||
// Tampilkan pesan yang lebih positif
|
// Tampilkan pesan yang lebih positif
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.history, size: 48.h, color: Colors.grey[400]),
|
Icon(statusFilter == 'rejected' ? Icons.block_outlined : Icons.history,
|
||||||
|
size: 48.h, color: Colors.grey[400]),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
TypographyStyles.body(
|
TypographyStyles.body(
|
||||||
'Tidak ada riwayat transaksi selesai',
|
'Tidak ada riwayat transaksi ${statusFilter == 'rejected' ? 'ditolak' : 'selesai'}',
|
||||||
color: GrayColors.gray600,
|
color: GrayColors.gray600,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.h),
|
SizedBox(height: 8.h),
|
||||||
TypographyStyles.caption(
|
TypographyStyles.caption(
|
||||||
'Riwayat akan muncul setelah Anda menyelesaikan transaksi',
|
statusFilter == 'rejected'
|
||||||
|
? 'Riwayat akan muncul ketika Anda menolak permintaan porter'
|
||||||
|
: 'Riwayat akan muncul setelah Anda menyelesaikan transaksi',
|
||||||
color: GrayColors.gray500,
|
color: GrayColors.gray500,
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
|
@ -273,17 +277,22 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
|
|
||||||
// Pesan tidak ada transaksi
|
// Pesan tidak ada transaksi
|
||||||
Widget _buildEmptyTransactionMessage(String statusFilter) {
|
Widget _buildEmptyTransactionMessage(String statusFilter) {
|
||||||
|
// Ubah teks pesan sesuai dengan status filter
|
||||||
|
String statusText = statusFilter;
|
||||||
|
if (statusFilter == 'rejected') {
|
||||||
|
statusText = 'ditolak';
|
||||||
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.body(
|
TypographyStyles.body(
|
||||||
'Tidak ada transaksi ${statusFilter.capitalizeFirst}',
|
'Tidak ada transaksi ${statusText.capitalizeFirst}',
|
||||||
color: GrayColors.gray600,
|
color: GrayColors.gray600,
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
// Perbaiki ini untuk memanggil refreshTransactions di controller
|
|
||||||
onPressed: () => _porterController.refreshTransactions(),
|
onPressed: () => _porterController.refreshTransactions(),
|
||||||
icon: Icon(Icons.refresh, size: 16.h),
|
icon: Icon(Icons.refresh, size: 16.h),
|
||||||
label: const Text('Refresh'),
|
label: const Text('Refresh'),
|
||||||
|
@ -342,18 +351,24 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modifikasi tampilan status untuk "rejected"
|
||||||
|
String displayStatus = transaction.normalizedStatus.capitalizeFirst!;
|
||||||
|
if (transaction.normalizedStatus == 'rejected' || transaction.rejectionInfo != null) {
|
||||||
|
displayStatus = 'Ditolak';
|
||||||
|
}
|
||||||
|
|
||||||
return CardHistoryPorter(
|
return CardHistoryPorter(
|
||||||
namePassenger: passengerName,
|
namePassenger: passengerName,
|
||||||
tlpnPassenger: passengerPhone,
|
tlpnPassenger: passengerPhone,
|
||||||
lokasiPassenger: transaction.locationPassenger,
|
lokasiPassenger: transaction.locationPassenger,
|
||||||
status: transaction.normalizedStatus.capitalizeFirst!,
|
status: displayStatus,
|
||||||
date: _dateFormat.format(transaction.createdAt),
|
date: _dateFormat.format(transaction.createdAt),
|
||||||
time: _timeFormat.format(transaction.createdAt),
|
time: _timeFormat.format(transaction.createdAt),
|
||||||
porter1: porter1,
|
porter1: porter1,
|
||||||
porter2: porter2,
|
porter2: porter2,
|
||||||
porter3: porter3,
|
porter3: porter3,
|
||||||
price: _priceFormatter.format(price),
|
price: _priceFormatter.format(price),
|
||||||
statusColor: _getStatusColor(transaction.normalizedStatus),
|
statusColor: _getStatusColor(transaction.rejectionInfo != null ? 'rejected' : transaction.normalizedStatus),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
log('ID Transaction Porter: ${transaction.id}');
|
log('ID Transaction Porter: ${transaction.id}');
|
||||||
Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: {
|
Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: {
|
||||||
|
@ -376,6 +391,8 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
||||||
return PrimaryColors.primary800;
|
return PrimaryColors.primary800;
|
||||||
case 'selesai':
|
case 'selesai':
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
|
case 'rejected':
|
||||||
|
return RedColors.red500;
|
||||||
default:
|
default:
|
||||||
return GrayColors.gray400;
|
return GrayColors.gray400;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue