Feat: fix bug after implementation rejected status

This commit is contained in:
orangdeso 2025-04-30 13:53:27 +07:00
parent 08331d69ca
commit 368788c85a
8 changed files with 692 additions and 260 deletions

View File

@ -638,7 +638,7 @@
"languageVersion": "3.4"
}
],
"generated": "2025-04-27T19:47:24.712851Z",
"generated": "2025-04-29T13:17:45.754727Z",
"generator": "pub",
"generatorVersion": "3.5.0",
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",

View File

@ -155,67 +155,6 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
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 {
@ -282,28 +221,31 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
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);
// 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');
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');
@ -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
Future<void> completePorterTransaction({
required String transactionId,
@ -367,126 +399,157 @@ class TransactionPorterRepositoryImpl implements TransactionPorterRepository {
}
}
// @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');
@override
Future<String?> reassignRejectedTransaction({
required String transactionId,
String? newPorterId,
}) async {
try {
log('[TransactionPorterRepo] Mencoba mengalihkan transaksi: $transactionId');
// // 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');
// 1. Dapatkan data transaksi yang ditolak
final rejectedTransaction = await getPorterTransactionById(transactionId);
if (rejectedTransaction == null) {
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Transaksi tidak ditemukan');
}
// // Coba dengan format ticketId-transactionId jika direct ID gagal
// if (ticketId.isNotEmpty) {
// final combinedId = '$ticketId-$transactionId';
// log('Trying update with combined ID: $combinedId');
// Verifikasi bahwa transaksi ini memang ditolak
final status = rejectedTransaction['status'] as String? ?? '';
final hasRejectionInfo = rejectedTransaction.containsKey('rejectionInfo');
if (status != 'rejected' && !hasRejectionInfo) {
throw FirebaseException(
plugin: 'TransactionPorterRepo',
message: 'Transaksi tidak dalam status ditolak'
);
}
// 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');
// }
// }
// }
// 2. Tandai transaksi lama sebagai sudah dialihkan
await _database.ref().child('porterTransactions/$transactionId').update({
'isReassigned': true,
});
// // 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();
// 3. Cari porter yang tersedia (jika tidak ada yang ditentukan)
String selectedPorterId = '';
String porterUserId = '';
String porterLocation = '';
if (newPorterId != null && newPorterId.isNotEmpty) {
// Gunakan porter yang ditentukan
final porterDoc = await _firestore.collection('porterOnline').doc(newPorterId).get();
if (!porterDoc.exists) {
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Porter yang ditentukan tidak ditemukan');
}
final porterData = porterDoc.data();
if (porterData == null || porterData['isAvailable'] != true) {
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Porter yang ditentukan tidak tersedia');
}
selectedPorterId = newPorterId;
porterUserId = porterData['userId'] ?? '';
porterLocation = porterData['location'] ?? '';
} else {
// Cari porter yang tersedia
final availablePortersSnapshot = await _firestore
.collection('porterOnline')
.where('isAvailable', isEqualTo: true)
.limit(1)
.get();
if (availablePortersSnapshot.docs.isEmpty) {
throw FirebaseException(plugin: 'TransactionPorterRepo', message: 'Tidak ada porter tersedia saat ini');
}
final porterDoc = availablePortersSnapshot.docs.first;
final porterData = porterDoc.data();
selectedPorterId = porterDoc.id;
porterUserId = porterData['userId'] ?? '';
porterLocation = porterData['location'] ?? '';
}
log('[TransactionPorterRepo] Porter baru dipilih: $selectedPorterId (userId: $porterUserId)');
// if (dataSnapshot.exists && dataSnapshot.value is Map) {
// final allData = dataSnapshot.value as Map<dynamic, dynamic>;
// 4. Buat transaksi baru dengan referensi ke transaksi lama
final now = DateTime.now().millisecondsSinceEpoch;
final newTransactionId = '${now}_${100000 + (now % 900000)}';
// Data untuk transaksi baru
final newTransactionData = <String, dynamic>{
'transactionId': rejectedTransaction['transactionId'] ?? '',
'ticketId': rejectedTransaction['ticketId'] ?? '',
'idPassenger': rejectedTransaction['idPassenger'] ?? '',
'locationPassenger': rejectedTransaction['locationPassenger'] ?? '',
'kodePorter': rejectedTransaction['kodePorter'] ?? '',
'status': 'pending',
'createdAt': now,
'porterOnlineId': selectedPorterId,
'porterUserId': porterUserId,
'locationPorter': porterLocation,
'previousTransactionId': transactionId, // Referensi ke transaksi yang ditolak
};
// Jika ada rejectionInfo, tambahkan alasan pengalihan
if (hasRejectionInfo && rejectedTransaction['rejectionInfo'] is Map) {
final rejectionInfoMap = rejectedTransaction['rejectionInfo'] as Map<dynamic, dynamic>;
final reason = rejectionInfoMap['reason']?.toString() ?? 'Ditolak oleh porter sebelumnya';
newTransactionData['reassignmentInfo'] = {
'previousTransactionId': transactionId,
'reason': reason,
'timestamp': now,
};
}
// // Iterasi semua porter IDs
// for (var porterId in allData.keys) {
// final porterData = allData[porterId];
// 5. Simpan transaksi baru ke Realtime Database dan Firestore
final batch = _firestore.batch();
// Ke Realtime Database
await _database.ref().child('porterTransactions/$newTransactionId').set(newTransactionData);
// Firestore - update porter
final porterRef = _firestore.collection('porterOnline').doc(selectedPorterId);
batch.update(porterRef, {
'isAvailable': false,
'idTransaction': newTransactionId,
'idUser': rejectedTransaction['idPassenger'] ?? '',
});
// Commit Firestore batch
await batch.commit();
// if (porterData is Map<dynamic, dynamic> && porterData.containsKey(transactionId)) {
// log('Found transaction in Realtime DB under porter: $porterId');
// 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,
});
}
// // Update status di Realtime DB
// final transactionRef =
// _database.ref().child('porterTransactions').child(porterId.toString()).child(transactionId);
// 7. Tambahkan ke passengerHistory
final passengerId = rejectedTransaction['idPassenger'] ?? '';
if (passengerId.isNotEmpty) {
await _database.ref().child('passengerHistory/$passengerId/$newTransactionId').set({
'timestamp': now,
'transactionId': newTransactionId,
});
}
// 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');
// }
// }
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');
}
}
}

View File

@ -1,4 +1,5 @@
// domain/models/transaction_porter_model.dart
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
class PorterTransactionModel {
@ -15,6 +16,7 @@ class PorterTransactionModel {
final String transactionId;
final DateTime createdAt;
final DateTime? updatedAt;
final RejectionInfo? rejectionInfo;
PorterTransactionModel({
required this.id,
@ -29,25 +31,60 @@ class PorterTransactionModel {
required this.transactionId,
required this.createdAt,
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();
// Status standar
if (lowercaseStatus == "pending") return "pending";
if (lowercaseStatus == "proses") return "proses";
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("pros")) return "proses";
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) {
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(
id: id,
kodePorter: json['kodePorter'] ?? '',
@ -56,7 +93,7 @@ class PorterTransactionModel {
locationPassenger: json['locationPassenger'] as String? ?? '',
locationPorter: json['locationPorter'] 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? ?? '',
transactionId: json['transactionId'] as String? ?? '',
createdAt: json['createdAt'] is int
@ -69,6 +106,92 @@ class PorterTransactionModel {
? DateTime.fromMillisecondsSinceEpoch(json['updatedAt'])
: (json['updatedAt'] is DateTime ? json['updatedAt'] : 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,
};
}
}

View File

@ -5,19 +5,29 @@ abstract class TransactionPorterRepository {
Stream<PorterTransactionModel?> watchTransactionById(String transactionId);
Future<Map<String, dynamic>?> getPorterTransactionById(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> rejectTransaction({
required String transactionId,
required String reason,
});
Future<void> completePorterTransaction({
required String transactionId,
required String porterOnlineId,
});
Future<String?> reassignRejectedTransaction({
required String transactionId,
String? newPorterId,
});
}

View File

@ -13,6 +13,10 @@ class TransactionPorterUsecase {
return _repository.watchTransactionById(transactionId);
}
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) {
return _repository.getPorterTransactionById(transactionId);
}
Future<PorterTransactionModel?> getTransactionById(String transactionId) {
return _repository.getTransactionById(transactionId);
}
@ -21,31 +25,43 @@ class TransactionPorterUsecase {
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> rejectTransaction({
required String transactionId,
required String reason,
}) {
return _repository.rejectTransaction(
transactionId: transactionId,
reason: reason,
);
}
Future<void> completePorterTransaction({
// required String ticketId,
required String transactionId,
required String porterOnlineId,
}) {
return _repository.completePorterTransaction(
// ticketId: ticketId,
transactionId: transactionId,
porterOnlineId: porterOnlineId,
);
}
Future<String?> reassignRejectedTransaction({
required String transactionId,
String? newPorterId,
}) {
return _repository.reassignRejectedTransaction(
transactionId: transactionId,
newPorterId: newPorterId,
);
}
}

View File

@ -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/domain/models/porter_queue_model.dart';
import 'package:e_porter/presentation/controllers/porter_queue_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../domain/models/transaction_porter_model.dart';
import '../../domain/usecases/transaction_porter_usecase.dart';
@ -20,6 +21,8 @@ class TransactionPorterController extends GetxController {
final RxBool isLoading = false.obs;
final RxString error = ''.obs;
final TextEditingController rejectionReasonController = TextEditingController();
StreamSubscription<List<PorterTransactionModel>>? _subscription;
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
Future<void> completePorterTransaction({
required String transactionId,
@ -365,6 +430,7 @@ class TransactionPorterController extends GetxController {
@override
void onClose() {
rejectionReasonController.dispose();
_porterSubscription?.cancel();
_subscription?.cancel();
for (var subscription in _transactionWatchers.values) {
@ -373,4 +439,51 @@ class TransactionPorterController extends GetxController {
_transactionWatchers.clear();
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),
),
],
);
},
);
}
}

View File

@ -27,16 +27,14 @@ class DetailHistoryPorterScreen extends StatefulWidget {
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 RxBool _isLoadingTicket = false.obs;
final DateFormat _dateFormat = DateFormat('dd MMMM yyyy', 'en_US');
final DateFormat _timeFormat = DateFormat.jm();
// final NumberFormat _priceFormatter = NumberFormat.decimalPattern('id_ID');
@override
void initState() {
@ -114,6 +112,10 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
_buildLocationPassenger(porterTransaction),
SizedBox(height: 20.h),
_buildDetailsOrder(ticketTransaction),
if (porterTransaction.normalizedStatus == 'rejected') ...[
SizedBox(height: 20.h),
_buildRejectionInfo(porterTransaction),
],
],
),
),
@ -128,15 +130,32 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
switch (transaction.normalizedStatus) {
case 'pending':
return CustomeShadowCotainner(
child: ButtonFill(
text: 'Terima Orderan',
textColor: Colors.white,
onTap: () {
_porterController.updateTransactionStatus(
transactionId: porterTransactionId,
status: 'proses',
);
},
child: Row(
children: [
Expanded(
child: ButtonFill(
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,
onTap: () {
_porterController.updateTransactionStatus(
transactionId: porterTransactionId,
status: 'proses',
);
},
),
),
],
),
);
@ -154,6 +173,7 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
);
case 'selesai':
case 'rejected':
return const SizedBox.shrink();
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}) {
return Column(
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) {
if (transaction == null) return GrayColors.gray400;
@ -339,12 +426,13 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
return PrimaryColors.primary800;
case 'selesai':
return Colors.green;
case 'rejected':
return Colors.red;
default:
return GrayColors.gray400;
}
}
// Helper untuk icon status
IconData _getStatusIcon(PorterTransactionModel? transaction) {
if (transaction == null) return Icons.info_outline;
@ -355,6 +443,8 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
return Icons.directions_run;
case 'selesai':
return Icons.check_circle_outline;
case 'rejected':
return Icons.cancel_outlined;
default:
return Icons.info_outline;
}

View File

@ -33,30 +33,6 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
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)) {
@ -84,7 +60,7 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
length: 4,
child: Scaffold(
backgroundColor: GrayColors.gray50,
appBar: SimpleAppbarComponent(
@ -102,6 +78,7 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
_buildTransactionList('pending'),
_buildTransactionList('proses'),
_buildTransactionList('selesai'),
_buildTransactionList('rejected'),
],
),
),
@ -129,11 +106,12 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
labelColor: PrimaryColors.primary800,
unselectedLabelColor: GrayColors.gray400,
indicatorColor: PrimaryColors.primary800,
indicatorWeight: 3,
indicatorWeight: 4,
tabs: const [
Tab(text: 'Pending'),
Tab(text: 'Proses'),
Tab(text: 'Selesai'),
Tab(text: 'Ditolak'),
],
),
);
@ -184,7 +162,7 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
),
child: Row(
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),
Expanded(
child: Text(
@ -215,29 +193,55 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
// Log untuk debug
log('Semua transaksi: ${allTransactions.length}');
// Filter berdasarkan status
final filteredTransactions = allTransactions.where((tx) => tx.normalizedStatus == statusFilter).toList();
// Filter berdasarkan status dengan logika yang diperbaiki
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}');
// 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') {
if (_porterController.error.value.contains('Porter tidak ditemukan') &&
(statusFilter == 'selesai' || statusFilter == 'rejected')) {
// Tampilkan pesan yang lebih positif
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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),
TypographyStyles.body(
'Tidak ada riwayat transaksi selesai',
'Tidak ada riwayat transaksi ${statusFilter == 'rejected' ? 'ditolak' : 'selesai'}',
color: GrayColors.gray600,
),
SizedBox(height: 8.h),
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,
),
SizedBox(height: 16.h),
@ -273,17 +277,22 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
// Pesan tidak ada transaksi
Widget _buildEmptyTransactionMessage(String statusFilter) {
// Ubah teks pesan sesuai dengan status filter
String statusText = statusFilter;
if (statusFilter == 'rejected') {
statusText = 'ditolak';
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TypographyStyles.body(
'Tidak ada transaksi ${statusFilter.capitalizeFirst}',
'Tidak ada transaksi ${statusText.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'),
@ -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(
namePassenger: passengerName,
tlpnPassenger: passengerPhone,
lokasiPassenger: transaction.locationPassenger,
status: transaction.normalizedStatus.capitalizeFirst!,
status: displayStatus,
date: _dateFormat.format(transaction.createdAt),
time: _timeFormat.format(transaction.createdAt),
porter1: porter1,
porter2: porter2,
porter3: porter3,
price: _priceFormatter.format(price),
statusColor: _getStatusColor(transaction.normalizedStatus),
statusColor: _getStatusColor(transaction.rejectionInfo != null ? 'rejected' : transaction.normalizedStatus),
onTap: () {
log('ID Transaction Porter: ${transaction.id}');
Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: {
@ -376,6 +391,8 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
return PrimaryColors.primary800;
case 'selesai':
return Colors.green;
case 'rejected':
return RedColors.red500;
default:
return GrayColors.gray400;
}