Feat: fix bug reassignments

This commit is contained in:
orangdeso 2025-05-05 05:07:20 +07:00
parent ec3435850b
commit 56b629b1d7
15 changed files with 2057 additions and 813 deletions

View File

@ -638,7 +638,7 @@
"languageVersion": "3.4" "languageVersion": "3.4"
} }
], ],
"generated": "2025-04-29T13:17:45.754727Z", "generated": "2025-04-30T16:03:15.794012Z",
"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",

View File

@ -0,0 +1,279 @@
import 'dart:async';
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
class TransactionReassignmentService {
final FirebaseFirestore _firestore;
Timer? _timer;
bool _isRunning = false;
TransactionReassignmentService({
FirebaseFirestore? firestore,
}) : _firestore = firestore ?? FirebaseFirestore.instance;
void startService({int intervalSeconds = 30}) {
log('[TransactionReassignmentService] Memulai service pengalihan transaksi dengan interval $intervalSeconds detik');
_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: intervalSeconds), (_) {
_checkRejectedTransactions();
});
}
void stopService() {
_timer?.cancel();
_timer = null;
log('[TransactionReassignmentService] Service pengalihan transaksi dihentikan');
}
Future<void> _checkRejectedTransactions() async {
// Mencegah eksekusi bersamaan
if (_isRunning) {
log('[TransactionReassignmentService] Proses pemeriksaan sebelumnya masih berjalan, melewati pemeriksaan ini');
return;
}
_isRunning = true;
try {
log('[TransactionReassignmentService] Memeriksa transaksi yang ditolak...');
// Query transaksi dengan status rejected di Firestore dengan kondisi yang lebih spesifik
// PERUBAHAN: menghapus filter porterOnlineId null agar semua transaksi rejected bisa dideteksi
final snapshot = await _firestore
.collection('porterTransactions')
.where('status', isEqualTo: 'rejected')
.where('isRejected', isEqualTo: true)
// .where('porterOnlineId', isNull: true) // DIHAPUS - tidak diperlukan dan dapat menyebabkan masalah
.where('reassignmentInfo', isNull: true) // Belum pernah dialihkan
.limit(5) // Batasi jumlah transaksi per batch
.get();
if (snapshot.docs.isEmpty) {
log('[TransactionReassignmentService] Tidak ada transaksi ditolak yang perlu dialihkan');
return;
}
log('[TransactionReassignmentService] Ditemukan ${snapshot.docs.length} transaksi ditolak yang perlu dialihkan');
// Proses setiap transaksi satu per satu
for (final doc in snapshot.docs) {
final transactionId = doc.id;
await _reassignTransaction(transactionId, doc.data());
// Berikan sedikit jeda untuk menghindari konflik
await Future.delayed(Duration(milliseconds: 500));
}
// Log setelah semua transaksi diproses
log('[TransactionReassignmentService] Selesai memproses ${snapshot.docs.length} transaksi ditolak');
} catch (e) {
log('[TransactionReassignmentService] Error memeriksa transaksi ditolak: $e');
} finally {
_isRunning = false;
}
}
Future<void> _reassignTransaction(String transactionId, Map<String, dynamic> txData) async {
try {
log('[TransactionReassignmentService] Memulai pengalihan transaksi: $transactionId');
// 1. Cari porter yang tersedia
final availablePorters = await _getAvailablePorters();
if (availablePorters.isEmpty) {
log('[TransactionReassignmentService] Tidak ada porter tersedia untuk transaksi: $transactionId');
return;
}
// 2. Pilih porter berdasarkan FIFO (yang sudah online paling lama didahulukan)
final newPorter = availablePorters.first;
final newPorterId = newPorter['id'];
final newPorterUserId = newPorter['userId'];
final newPorterLocation = newPorter['locationPorter'];
log('[TransactionReassignmentService] Porter baru dipilih: $newPorterId (userId: $newPorterUserId)');
// Ambil nilai yang diperlukan dari data transaksi
final passengerId = txData['idPassenger']?.toString() ?? '';
final oldPorterUserId = txData['porterUserId']?.toString() ?? '';
final now = DateTime.now();
// Ambil alasan penolakan jika ada
String rejectionReason = "Transaksi dialihkan otomatis";
if (txData.containsKey('rejectionInfo') && txData['rejectionInfo'] is Map) {
final rejInfo = txData['rejectionInfo'] as Map<String, dynamic>;
if (rejInfo.containsKey('reason') && rejInfo['reason'].toString().isNotEmpty) {
rejectionReason = "Dialihkan otomatis: ${rejInfo['reason']}";
}
}
// 3. Update data transaksi
log('[TransactionReassignmentService] Memperbarui data transaksi & porter dengan batch operation');
// TAMBAHAN: Double-check porter masih tersedia
final porterDoc = await _firestore.collection('porterOnline').doc(newPorterId).get();
if (!porterDoc.exists || porterDoc.data() == null || porterDoc.data()!['isAvailable'] != true) {
log('[TransactionReassignmentService] Porter $newPorterId tidak lagi tersedia, batalkan pengalihan');
return;
}
// Membuat Batch Operation
final batch = _firestore.batch();
// 4. Update porter baru - Set porter tidak tersedia dan hubungkan dengan transaksi
batch.update(_firestore.collection('porterOnline').doc(newPorterId), {
'isAvailable': false,
'idTransaction': transactionId,
'idUser': passengerId,
'onlineAt': now,
});
// 5. Update transaksi utama dengan info porter baru
batch.update(_firestore.collection('porterTransactions').doc(transactionId), {
'locationPorter': newPorterLocation,
'porterOnlineId': newPorterId,
'porterUserId': newPorterUserId,
'status': 'pending',
'updatedAt': now,
'reassignmentInfo': {
'previousPorterId': oldPorterUserId,
'reason': rejectionReason,
'timestamp': now,
'isAutomatic': true
},
'rejectionInfo': FieldValue.delete(), // Hapus info penolakan
'isRejected': false,
'hasAssignedPorter': true,
});
// 6. HAPUS entri transaksi lama dari porter sebelumnya jika ada
if (oldPorterUserId.isNotEmpty) {
log('[TransactionReassignmentService] Menghapus referensi dari porter lama: $oldPorterUserId');
final oldRef = _firestore
.collection('porterTransactionsByUser')
.doc(oldPorterUserId)
.collection('transactions')
.doc(transactionId);
batch.delete(oldRef);
}
// 7. Buat entri baru di porterTransactionsByUser untuk porter baru
if (newPorterUserId.isNotEmpty) {
log('[TransactionReassignmentService] Membuat referensi baru untuk porter: $newPorterUserId');
final newRef = _firestore
.collection('porterTransactionsByUser')
.doc(newPorterUserId)
.collection('transactions')
.doc(transactionId);
batch.set(newRef, {
'transactionId': transactionId,
'createdAt': now,
'status': 'pending',
'updatedAt': now,
'type': 'reassigned',
'passengerId': passengerId,
'reassigned': true,
'previousPorterUserId': oldPorterUserId,
'isAutomatic': true
});
}
// 8. Update porterRejections jika ada
final rejectionRef = _firestore
.collection('porterRejections')
.doc(oldPorterUserId)
.collection('transactionPorterRejection')
.doc(transactionId);
final rejectionDoc = await rejectionRef.get();
if (rejectionDoc.exists) {
batch.update(rejectionRef, {
'isReassigned': true,
'reassignedAt': now,
'newPorterId': newPorterId,
'newPorterUserId': newPorterUserId,
'automaticReassignment': true
});
}
// 9. Commit batch
await batch.commit();
log('[TransactionReassignmentService] ✓ Transaksi $transactionId berhasil dialihkan ke porter baru: $newPorterId');
} catch (e) {
log('[TransactionReassignmentService] Error mengalihkan transaksi $transactionId: $e');
}
}
Future<List<Map<String, dynamic>>> _getAvailablePorters() async {
try {
log('[TransactionReassignmentService] Mencari porter yang tersedia...');
// Query porter yang tersedia dan tidak sedang melayani transaksi
final query = _firestore
.collection('porterOnline')
.where('isAvailable', isEqualTo: true)
.orderBy('onlineAt', descending: false) // FIFO - yang online paling lama didahulukan
.limit(10); // Ambil lebih banyak untuk filtering
final snapshot = await query.get();
log('[TransactionReassignmentService] Query menemukan ${snapshot.docs.length} porter online');
final List<Map<String, dynamic>> availablePorters = [];
for (final doc in snapshot.docs) {
final data = Map<String, dynamic>.from(doc.data());
data['id'] = doc.id;
// PERUBAHAN: filter tambahan untuk memastikan porter benar-benar tersedia
if (data.containsKey('userId') &&
data.containsKey('locationPorter') &&
data['userId'] != null &&
data['userId'].toString().isNotEmpty &&
data['locationPorter'] != null &&
(data['idTransaction'] == null || data['idTransaction'].toString().isEmpty) &&
(data['idUser'] == null || data['idUser'].toString().isEmpty)) {
availablePorters.add(data);
log('[TransactionReassignmentService] Porter tersedia: ${doc.id}, userId: ${data['userId']}');
} else {
log('[TransactionReassignmentService] Porter tidak memenuhi syarat: ${doc.id}');
if (data['idTransaction'] != null) {
log('[TransactionReassignmentService] - Alasan: Sudah memiliki transaksi aktif');
} else if (data['userId'] == null || data['userId'].toString().isEmpty) {
log('[TransactionReassignmentService] - Alasan: Tidak memiliki userId valid');
} else if (data['locationPorter'] == null) {
log('[TransactionReassignmentService] - Alasan: Tidak memiliki lokasi porter');
}
}
}
if (availablePorters.isEmpty) {
log('[TransactionReassignmentService] Peringatan: Tidak ada porter yang tersedia setelah validasi');
} else {
log('[TransactionReassignmentService] Berhasil menemukan ${availablePorters.length} porter valid tersedia');
}
return availablePorters;
} catch (e) {
log('[TransactionReassignmentService] Error mendapatkan porter tersedia: $e');
return [];
}
}
// Opsional: Mengirim notifikasi ke porter baru
Future<void> _sendNotificationToNewPorter(
String porterUserId, String transactionId, Map<String, dynamic> txData) async {
try {
// Implementasi notifikasi bisa ditambahkan di sini
// Misal dengan Firebase Cloud Messaging atau menyimpan di collection notifications
} catch (e) {
log('[TransactionReassignmentService] Error mengirim notifikasi: $e');
}
}
Future<void> forceReassignmentCheck() async {
log('[TransactionReassignmentService] Memaksa pengecekan pengalihan transaksi');
await _checkRejectedTransactions();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
import 'package:e_porter/presentation/screens/boarding_pass/provider/porter_service_provider.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:e_porter/data/repositories/transaction_repository_impl.dart'; import 'package:e_porter/data/repositories/transaction_repository_impl.dart';
import 'package:e_porter/_core/service/transaction_expiry_service.dart'; import 'package:e_porter/_core/service/transaction_expiry_service.dart';
@ -11,5 +12,11 @@ class AppBinding extends Bindings {
// Inisialisasi dan mulai service // Inisialisasi dan mulai service
final repository = Get.find<TransactionRepositoryImpl>(); final repository = Get.find<TransactionRepositoryImpl>();
TransactionExpiryService().initialize(repository); TransactionExpiryService().initialize(repository);
// Inisialisasi dependency Porter
PorterServiceProvider.registerDependencies();
// Mulai layanan pengalihan transaksi
PorterServiceProvider.initServices();
} }
} }

View File

@ -17,6 +17,10 @@ class PorterTransactionModel {
final DateTime createdAt; final DateTime createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final RejectionInfo? rejectionInfo; final RejectionInfo? rejectionInfo;
final ReassignmentInfo? reassignmentInfo;
final String? previousTransactionId;
final bool? isRejected;
final bool? hasAssignedPorter;
PorterTransactionModel({ PorterTransactionModel({
required this.id, required this.id,
@ -32,6 +36,10 @@ class PorterTransactionModel {
required this.createdAt, required this.createdAt,
this.updatedAt, this.updatedAt,
this.rejectionInfo, this.rejectionInfo,
this.reassignmentInfo,
this.previousTransactionId,
this.isRejected,
this.hasAssignedPorter,
}) : normalizedStatus = _normalizeStatus(status, rejectionInfo); }) : normalizedStatus = _normalizeStatus(status, rejectionInfo);
static String _normalizeStatus(String status, RejectionInfo? rejectionInfo) { static String _normalizeStatus(String status, RejectionInfo? rejectionInfo) {
@ -85,6 +93,27 @@ class PorterTransactionModel {
} }
} }
ReassignmentInfo? reassignmentInfo;
if (json.containsKey('reassignmentInfo') && json['reassignmentInfo'] != null) {
try {
final Map<String, dynamic> reassignmentData;
if (json['reassignmentInfo'] is Map<String, dynamic>) {
reassignmentData = json['reassignmentInfo'];
} else if (json['reassignmentInfo'] is Map) {
// Convert dynamic map to string map
reassignmentData = {};
(json['reassignmentInfo'] as Map).forEach((key, value) {
reassignmentData[key.toString()] = value;
});
} else {
reassignmentData = {'reason': 'Format tidak valid', 'timestamp': DateTime.now().millisecondsSinceEpoch};
}
reassignmentInfo = ReassignmentInfo.fromJson(reassignmentData);
} catch (e) {
print('Error parsing reassignmentInfo: $e');
}
}
return PorterTransactionModel( return PorterTransactionModel(
id: id, id: id,
kodePorter: json['kodePorter'] ?? '', kodePorter: json['kodePorter'] ?? '',
@ -107,6 +136,10 @@ class PorterTransactionModel {
: (json['updatedAt'] is DateTime ? json['updatedAt'] : null)) : (json['updatedAt'] is DateTime ? json['updatedAt'] : null))
: null, : null,
rejectionInfo: rejectionInfo, rejectionInfo: rejectionInfo,
reassignmentInfo: reassignmentInfo,
previousTransactionId: json['previousTransactionId'],
isRejected: json['isRejected'] as bool? ?? false,
hasAssignedPorter: json['hasAssignedPorter'] as bool? ?? false,
); );
} }
@ -123,6 +156,11 @@ class PorterTransactionModel {
'ticketId': ticketId, 'ticketId': ticketId,
'transactionId': transactionId, 'transactionId': transactionId,
'createdAt': createdAt.millisecondsSinceEpoch, 'createdAt': createdAt.millisecondsSinceEpoch,
'rejectionInfo': rejectionInfo?.toJson(),
'reassignmentInfo': reassignmentInfo?.toJson(),
'previousTransactionId': previousTransactionId,
'isRejected': isRejected,
'hasAssignedPorter': hasAssignedPorter,
}; };
if (updatedAt != null) { if (updatedAt != null) {
@ -136,7 +174,6 @@ class PorterTransactionModel {
return data; return data;
} }
// Membuat salinan model dengan nilai baru
PorterTransactionModel copyWith({ PorterTransactionModel copyWith({
String? id, String? id,
String? kodePorter, String? kodePorter,
@ -195,3 +232,46 @@ class RejectionInfo {
}; };
} }
} }
class ReassignmentInfo {
final String? previousPorterId;
final String? rejectionId;
final String reason;
final DateTime timestamp;
ReassignmentInfo({
this.previousPorterId,
this.rejectionId,
required this.reason,
required this.timestamp,
});
factory ReassignmentInfo.fromJson(Map<String, dynamic> json) {
var rawTimestamp = json['timestamp'];
DateTime timestamp;
if (rawTimestamp is Timestamp) {
timestamp = rawTimestamp.toDate();
} else if (rawTimestamp is int) {
timestamp = DateTime.fromMillisecondsSinceEpoch(rawTimestamp);
} else {
timestamp = DateTime.now(); // Default fallback
}
return ReassignmentInfo(
previousPorterId: json['previousPorterId'],
rejectionId: json['rejectionId'],
reason: json['reason'] ?? 'Dialihkan ke porter baru',
timestamp: timestamp,
);
}
Map<String, dynamic> toJson() {
return {
'previousPorterId': previousPorterId,
'rejectionId': rejectionId,
'reason': reason,
'timestamp': timestamp.millisecondsSinceEpoch,
};
}
}

View File

@ -1,15 +1,15 @@
import '../models/transaction_porter_model.dart'; import '../models/transaction_porter_model.dart';
abstract class TransactionPorterRepository { abstract class TransactionPorterRepository {
Stream<List<PorterTransactionModel>> watchPorterTransactionsByUserId(String userId);
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId); Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId);
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); Stream<List<PorterTransactionModel>> watchRejectedTransactionsByPorter(String porterUserId);
Future<void> updateTransactionStatus({ Future<void> updateTransactionStatus({
required String transactionId, required String transactionId,

View File

@ -5,6 +5,10 @@ class TransactionPorterUsecase {
final TransactionPorterRepository _repository; final TransactionPorterRepository _repository;
TransactionPorterUsecase(this._repository); TransactionPorterUsecase(this._repository);
Stream<List<PorterTransactionModel>> watchPorterTransactionsByUserId(String userId) {
return _repository.watchPorterTransactionsByUserId(userId);
}
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId) { Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId) {
return _repository.watchPorterTransactions(porterId); return _repository.watchPorterTransactions(porterId);
} }
@ -13,16 +17,12 @@ 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);
} }
Future<List<String>> getPorterTransactionIds(String porterId) { Stream<List<PorterTransactionModel>> watchRejectedTransactionsByPorter(String porterUserId) {
return _repository.getPorterTransactionIds(porterId); return _repository.watchRejectedTransactionsByPorter(porterUserId);
} }
Future<void> updateTransactionStatus({ Future<void> updateTransactionStatus({

View File

@ -1,9 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:e_porter/_core/service/preferences_service.dart'; 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/screens/boarding_pass/provider/porter_service_provider.dart';
import 'package:flutter/material.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';
@ -15,21 +16,31 @@ class TransactionPorterController extends GetxController {
TransactionPorterController(this._useCase); TransactionPorterController(this._useCase);
final RxList<PorterTransactionModel> transactions = <PorterTransactionModel>[].obs; final RxList<PorterTransactionModel> transactions = <PorterTransactionModel>[].obs;
final RxList<PorterTransactionModel> rejectedTransactions = <PorterTransactionModel>[].obs;
final Rx<PorterTransactionModel?> currentTransaction = Rx<PorterTransactionModel?>(null); final Rx<PorterTransactionModel?> currentTransaction = Rx<PorterTransactionModel?>(null);
final Map<String, StreamSubscription<PorterTransactionModel?>> _transactionWatchers = {}; final Map<String, StreamSubscription<PorterTransactionModel?>> _transactionWatchers = {};
final RxString currentPorterId = ''.obs; final RxString currentPorterId = ''.obs;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxString error = ''.obs; final RxString error = ''.obs;
final RxBool isRejectionDialogVisible = false.obs;
final RxString rejectionReason = ''.obs;
final RxString statusFilter = 'pending'.obs;
final RxBool isRejecting = false.obs;
final RxBool needsRefresh = false.obs;
final TextEditingController rejectionReasonController = TextEditingController(); final TextEditingController rejectionReasonController = TextEditingController();
StreamSubscription<List<PorterTransactionModel>>? _subscription; StreamSubscription<List<PorterTransactionModel>>? _subscription;
StreamSubscription<PorterQueueModel?>? _porterSubscription; StreamSubscription<PorterQueueModel?>? _porterSubscription;
StreamSubscription<List<PorterTransactionModel>>? _rejectedSubscription;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_loadPorterData(); _loadPorterData();
_watchRejectedTransactions();
} }
Future<void> _loadPorterData() async { Future<void> _loadPorterData() async {
@ -40,105 +51,63 @@ class TransactionPorterController extends GetxController {
return; return;
} }
// Set userId untuk digunakan nanti
String userId = userData.uid; String userId = userData.uid;
final porterCtrl = Get.find<PorterQueueController>(); currentPorterId.value = userId; // Langsung gunakan userId
// Simpan user ID untuk memastikan bisa mengakses history jika porter tidak ditemukan log('Menggunakan userId dari preferences: $userId');
String userIdForHistory = userId; loadTransactionsFromUserId(userId); // Langsung gunakan userId
// Coba mendapatkan porter aktif
_porterSubscription = porterCtrl.watchPorter(userId).listen((porter) {
if (porter != null && porter.id != null) {
log('Porter aktif ditemukan: ${porter.id}');
currentPorterId.value = porter.id!;
// Gunakan userId untuk memastikan semua transaksi bisa diakses
loadTransactionsWithBothIDs(porter.id!, userIdForHistory);
} else {
// Porter tidak aktif, coba ambil riwayat dari userId
log('Porter aktif tidak ditemukan, mencoba ambil riwayat dari userId: $userId');
loadTransactionsFromUserId(userId);
}
}, onError: (e) {
log('Error memantau data porter: $e');
// Jika error, coba ambil riwayat dari userId
loadTransactionsFromUserId(userId);
});
} catch (e) { } catch (e) {
error.value = 'Error inisialisasi: $e'; error.value = 'Error inisialisasi: $e';
} }
} }
void loadTransactionsWithBothIDs(String porterId, String userId) { void loadTransactionsFromUserId(String userId) {
if (userId.isEmpty) {
error.value = 'User ID tidak boleh kosong';
return;
}
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
_subscription?.cancel(); _subscription?.cancel();
_subscription = _useCase.watchPorterTransactions(porterId).listen((transactionList) { _subscription = _useCase.watchPorterTransactionsByUserId(userId).listen(
log('Menerima ${transactionList.length} transaksi dari porterId'); (transactionList) {
log('[TransactionPorterController] Menerima ${transactionList.length} transaksi dari userId: $userId');
// Jika tidak ada transaksi dari porterId, coba dari userId if (transactionList.isEmpty) {
if (transactionList.isEmpty && userId != porterId) { log('[TransactionPorterController] Tidak ada transaksi ditemukan untuk userId: $userId');
log('Tidak ada transaksi dari porterId, mencoba dari userId: $userId'); if (transactions.isNotEmpty) {
loadTransactionsFromUserId(userId); transactions.clear();
}
} else { } else {
transactions.assignAll(transactionList); transactions.assignAll(transactionList);
isLoading.value = false;
}
}, onError: (e) {
log('Error streaming transaksi: $e');
// Jika terjadi error, coba dari userId sebagai fallback // Mulai memantau setiap transaksi individual untuk real-time updates
if (userId != porterId) { for (var transaction in transactionList) {
log('Error dengan porterId, mencoba dari userId: $userId'); watchTransaction(transaction.id);
loadTransactionsFromUserId(userId); }
} else { }
isLoading.value = false;
},
onError: (e) {
log('[TransactionPorterController] Error streaming transaksi: $e');
error.value = 'Gagal memuat transaksi: $e'; error.value = 'Gagal memuat transaksi: $e';
isLoading.value = false; isLoading.value = false;
} },
}); );
}
void loadTransactionsFromUserId(String userId) {
isLoading.value = true;
error.value = '';
log('[TransactionPorterController] Memuat transaksi untuk userId: $userId');
// Gunakan userId sebagai porterId di porterHistory
_useCase.getPorterTransactionIds(userId).then((transactionIds) {
log('[TransactionPorterController] Ditemukan ${transactionIds.length} ID transaksi untuk userId: $userId');
if (transactionIds.isEmpty) {
transactions.clear();
isLoading.value = false;
return;
}
// Ambil dan proses data transaksi
_processTransactionData(transactionIds);
}).catchError((e) {
log('[TransactionPorterController] Error mendapatkan ID transaksi: $e');
error.value = 'Gagal memuat riwayat transaksi';
isLoading.value = false;
});
} }
void watchTransaction(String transactionId) { void watchTransaction(String transactionId) {
// Batalkan subscription yang ada jika ada
_transactionWatchers[transactionId]?.cancel(); _transactionWatchers[transactionId]?.cancel();
// Mulai subscription baru
_transactionWatchers[transactionId] = _useCase.watchTransactionById(transactionId).listen( _transactionWatchers[transactionId] = _useCase.watchTransactionById(transactionId).listen(
(updatedTransaction) { (updatedTransaction) {
if (updatedTransaction != null) { if (updatedTransaction != null) {
// Update current transaction jika itu transaksi yang sedang aktif
if (currentTransaction.value?.id == transactionId) { if (currentTransaction.value?.id == transactionId) {
currentTransaction.value = updatedTransaction; currentTransaction.value = updatedTransaction;
} }
// Update transaksi di daftar
final index = transactions.indexWhere((tx) => tx.id == transactionId); final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) { if (index >= 0) {
transactions[index] = updatedTransaction; transactions[index] = updatedTransaction;
@ -152,30 +121,6 @@ class TransactionPorterController extends GetxController {
); );
} }
void _processTransactionData(List<String> transactionIds) async {
try {
List<PorterTransactionModel> txList = [];
for (var id in transactionIds) {
final txData = await _useCase.getPorterTransactionById(id);
if (txData != null) {
txList.add(PorterTransactionModel.fromJson(txData, id));
}
}
// Urutkan berdasarkan tanggal terbaru
txList.sort((a, b) => b.createdAt.compareTo(a.createdAt));
// Update transactions list
transactions.assignAll(txList);
isLoading.value = false;
} catch (e) {
log('Error memproses data transaksi: $e');
error.value = 'Gagal memproses data transaksi';
isLoading.value = false;
}
}
void loadTransactionsFromPorterId(String porterId) { void loadTransactionsFromPorterId(String porterId) {
if (porterId.isEmpty) { if (porterId.isEmpty) {
error.value = 'Porter ID tidak boleh kosong'; error.value = 'Porter ID tidak boleh kosong';
@ -190,7 +135,6 @@ class TransactionPorterController extends GetxController {
log('Menerima ${transactionList.length} transaksi'); log('Menerima ${transactionList.length} transaksi');
transactions.assignAll(transactionList); transactions.assignAll(transactionList);
// Mulai memantau setiap transaksi individual untuk real-time updates
for (var transaction in transactionList) { for (var transaction in transactionList) {
watchTransaction(transaction.id); watchTransaction(transaction.id);
} }
@ -203,28 +147,11 @@ class TransactionPorterController extends GetxController {
}); });
} }
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId) async {
try {
isLoading.value = true;
error.value = '';
return await _useCase.getPorterTransactionById(transactionId);
} catch (e) {
log('Error getting transaction data: $e');
error.value = 'Gagal mendapatkan data transaksi: $e';
return null;
} finally {
isLoading.value = false;
}
}
Future<PorterTransactionModel?> getTransactionById(String transactionId) async { Future<PorterTransactionModel?> getTransactionById(String transactionId) async {
try { try {
log('Getting transaction by ID: $transactionId'); log('Getting transaction by ID: $transactionId');
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
// Reset dulu current transaction agar UI bisa merespons ke loading state
currentTransaction.value = null; currentTransaction.value = null;
final transaction = await _useCase.getTransactionById(transactionId); final transaction = await _useCase.getTransactionById(transactionId);
@ -233,7 +160,6 @@ class TransactionPorterController extends GetxController {
log('Transaction found and set to current: ${transaction.id}'); log('Transaction found and set to current: ${transaction.id}');
currentTransaction.value = transaction; currentTransaction.value = transaction;
// Mulai memantau transaksi ini
watchTransaction(transactionId); watchTransaction(transactionId);
} else { } else {
log('Transaction not found with ID: $transactionId'); log('Transaction not found with ID: $transactionId');
@ -250,6 +176,109 @@ class TransactionPorterController extends GetxController {
} }
} }
void _watchRejectedTransactions() async {
try {
final userData = await PreferencesService.getUserData();
if (userData == null || userData.uid.isEmpty) {
log('[Controller] User data tidak ditemukan untuk watching rejected transactions');
return;
}
String porterUserId = userData.uid;
log('[Controller] Watching rejected transactions for porterUserId: $porterUserId');
_rejectedSubscription?.cancel();
_rejectedSubscription = _useCase.watchRejectedTransactionsByPorter(porterUserId).listen(
(transactions) {
log('[Controller] Received ${transactions.length} rejected transactions');
final uniqueTransactions = <String, PorterTransactionModel>{};
for (var tx in transactions) {
uniqueTransactions[tx.id] = tx;
}
rejectedTransactions.assignAll(uniqueTransactions.values.toList());
},
onError: (error) {
log('[Controller] Error watching rejected transactions: $error');
},
);
} catch (e) {
log('[Controller] Error setting up rejected transactions watcher: $e');
}
}
Future<PorterTransactionModel?> getRejectedTransactionById(String transactionId) async {
try {
log('[TransactionPorterController] Mencari transaksi ditolak dengan ID: $transactionId');
PorterTransactionModel? rejectedTransaction;
try {
rejectedTransaction = rejectedTransactions.firstWhere(
(tx) => tx.id == transactionId,
);
} catch (e) {
rejectedTransaction = null;
}
if (rejectedTransaction != null) {
// log('[TransactionPorterController] Transaksi ditolak ditemukan di cache');
currentTransaction.value = rejectedTransaction;
return rejectedTransaction;
}
final userData = await PreferencesService.getUserData();
if (userData != null && userData.uid.isNotEmpty) {
String porterUserId = userData.uid;
final completer = Completer<PorterTransactionModel?>();
final subscription = _useCase.watchRejectedTransactionsByPorter(porterUserId).listen(
(transactions) {
PorterTransactionModel? foundTransaction;
try {
foundTransaction = transactions.firstWhere(
(tx) => tx.id == transactionId,
);
} catch (e) {
foundTransaction = null;
}
if (foundTransaction != null) {
completer.complete(foundTransaction);
} else {
completer.complete(null);
}
},
onError: (error) {
log('[TransactionPorterController] Error mencari transaksi ditolak: $error');
completer.complete(null);
},
);
final transaction = await completer.future.timeout(
Duration(seconds: 3),
onTimeout: () => null,
);
subscription.cancel();
if (transaction != null) {
currentTransaction.value = transaction;
// log('[TransactionPorterController] Transaksi ditolak ditemukan: ${transaction.id}');
} else {
// log('[TransactionPorterController] Transaksi ditolak tidak ditemukan: $transactionId');
}
return transaction;
}
return null;
} catch (e) {
log('[TransactionPorterController] Error mendapatkan transaksi ditolak: $e');
return null;
}
}
Future<void> updateTransactionStatus({ Future<void> updateTransactionStatus({
required String transactionId, required String transactionId,
required String status, required String status,
@ -265,19 +294,14 @@ class TransactionPorterController extends GetxController {
status: status, status: status,
); );
// Dapatkan transaksi yang diperbarui
final updatedTransaction = await getTransactionById(transactionId); final updatedTransaction = await getTransactionById(transactionId);
// Update list transaksi yang ada dengan yang baru
if (updatedTransaction != null) { if (updatedTransaction != null) {
// Cari indeks transaksi dalam list yang ada
final index = transactions.indexWhere((tx) => tx.id == transactionId); final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) { if (index >= 0) {
// Update transaksi pada indeks yang ditemukan
transactions[index] = updatedTransaction; transactions[index] = updatedTransaction;
log('Transaksi di daftar utama diperbarui: $transactionId dengan status: $status'); log('Transaksi di daftar utama diperbarui: $transactionId dengan status: $status');
} else { } else {
// Jika tidak ditemukan, perbarui seluruh list
log('Transaksi tidak ditemukan di daftar, menyegarkan seluruh daftar'); log('Transaksi tidak ditemukan di daftar, menyegarkan seluruh daftar');
refreshTransactions(); refreshTransactions();
} }
@ -299,39 +323,19 @@ class TransactionPorterController extends GetxController {
}) async { }) async {
try { try {
isLoading.value = true; isLoading.value = true;
isRejecting.value = true;
error.value = ''; error.value = '';
log('Menolak transaksi: $transactionId dengan alasan: $reason'); log('Menolak transaksi: $transactionId dengan alasan: $reason');
// Proses penolakan transaksi
await _useCase.rejectTransaction( await _useCase.rejectTransaction(
transactionId: transactionId, transactionId: transactionId,
reason: reason.isEmpty ? 'Tidak ada alasan' : reason, reason: reason.isEmpty ? 'Tidak ada alasan' : reason,
); );
// Segera coba reassign ke porter lain needsRefresh.value = true;
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); final updatedTransaction = await getTransactionById(transactionId);
// Update list transaksi yang ada dengan yang baru
if (updatedTransaction != null) { if (updatedTransaction != null) {
final index = transactions.indexWhere((tx) => tx.id == transactionId); final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) { if (index >= 0) {
@ -342,20 +346,31 @@ class TransactionPorterController extends GetxController {
} }
} }
// Reset controller alasan await Future.delayed(Duration(milliseconds: 500));
rejectionReasonController.clear();
try {
log('[TransactionPorterController] Memaksa pengecekan reassignment...');
await PorterServiceProvider.forceReassignmentCheck();
} catch (reassignError) {
log('[TransactionPorterController] Error saat memaksa reassignment: $reassignError');
}
SnackbarHelper.showSuccess('Berhasil', 'Transaksi berhasil ditolak'); SnackbarHelper.showSuccess('Berhasil', 'Transaksi berhasil ditolak');
} catch (e) { } catch (e) {
log('Error menolak transaksi: $e'); log('Error menolak transaksi: $e');
error.value = 'Gagal menolak transaksi: $e'; error.value = 'Gagal menolak transaksi: $e';
SnackbarHelper.showError('Terjadi Kesalahan', 'Gagal menolak transaksi'); SnackbarHelper.showError('Gagal', 'Transaksi gagal ditolak: ${e.toString()}');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
isRejecting.value = false;
if (needsRefresh.value) {
await refreshTransactions();
needsRefresh.value = false;
}
} }
} }
// Metode serupa untuk completePorterTransaction
Future<void> completePorterTransaction({ Future<void> completePorterTransaction({
required String transactionId, required String transactionId,
}) async { }) async {
@ -380,10 +395,8 @@ class TransactionPorterController extends GetxController {
porterOnlineId: porterOnlineId, porterOnlineId: porterOnlineId,
); );
// Dapatkan transaksi yang diperbarui
final updatedTransaction = await getTransactionById(transactionId); final updatedTransaction = await getTransactionById(transactionId);
// Update list transaksi
if (updatedTransaction != null) { if (updatedTransaction != null) {
final index = transactions.indexWhere((tx) => tx.id == transactionId); final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) { if (index >= 0) {
@ -428,11 +441,104 @@ class TransactionPorterController extends GetxController {
loadTransactionsFromPorterId(currentPorterId.value); loadTransactionsFromPorterId(currentPorterId.value);
} }
Future<void> refreshRejectedTransactions() async {
try {
final userData = await PreferencesService.getUserData();
if (userData == null || userData.uid.isEmpty) {
log('[TransactionPorterController] User data tidak ditemukan untuk refresh rejected transactions');
return;
}
String porterUserId = userData.uid;
log('[TransactionPorterController] Refreshing rejected transactions untuk porterUserId: $porterUserId');
_rejectedSubscription?.cancel();
_rejectedSubscription = _useCase.watchRejectedTransactionsByPorter(porterUserId).listen(
(transactions) {
log('[TransactionPorterController] Refreshed: ${transactions.length} rejected transactions');
rejectedTransactions.assignAll(transactions);
},
onError: (error) {
log('[Controller] Error watching rejected transactions: $error');
if (error is FirebaseException) {
this.error.value = 'Error loading rejected transactions: ${error.message}';
} else {
this.error.value = 'Error loading rejected transactions: $error';
}
},
);
} catch (e) {
log('[TransactionPorterController] Error refresh rejected transactions: $e');
}
}
Future<void> forceReassignTransaction(String transactionId) async {
try {
isLoading.value = true;
error.value = '';
log('[TransactionPorterController] Mencoba mengalihkan transaksi: $transactionId');
final result = await _useCase.reassignRejectedTransaction(
transactionId: transactionId,
);
if (result != null) {
await Future.delayed(Duration(milliseconds: 500));
final updatedTransaction = await getTransactionById(transactionId);
if (updatedTransaction != null) {
final index = transactions.indexWhere((tx) => tx.id == transactionId);
if (index >= 0) {
transactions[index] = updatedTransaction;
}
}
await refreshTransactions();
SnackbarHelper.showSuccess('Berhasil', 'Transaksi berhasil dialihkan ke porter lain');
} else {
SnackbarHelper.showInfo('Info', 'Tidak ada porter tersedia saat ini, akan dicoba lagi nanti');
}
} catch (e) {
error.value = 'Gagal mengalihkan transaksi: $e';
SnackbarHelper.showError('Gagal', 'Transaksi gagal dialihkan: ${e.toString()}');
} finally {
isLoading.value = false;
}
}
List<PorterTransactionModel> getFilteredTransactions() {
return transactions.where((tx) {
if (statusFilter.value == 'pending') {
return tx.status == 'pending' && tx.porterOnlineId == currentPorterId.value;
}
if (statusFilter.value == 'rejected') {
return tx.status == 'rejected' && tx.porterOnlineId == currentPorterId.value;
}
return tx.normalizedStatus == statusFilter.value && tx.porterOnlineId == currentPorterId.value;
}).toList();
}
void updateStatusFilter(String status) {
statusFilter.value = status;
}
void showRejectDialog() {
rejectionReason.value = '';
rejectionReasonController.clear();
isRejectionDialogVisible.value = true;
}
void hideRejectDialog() {
isRejectionDialogVisible.value = false;
rejectionReasonController.clear();
}
@override @override
void onClose() { void onClose() {
rejectionReasonController.dispose(); rejectionReasonController.dispose();
_porterSubscription?.cancel(); _porterSubscription?.cancel();
_subscription?.cancel(); _subscription?.cancel();
_rejectedSubscription?.cancel();
for (var subscription in _transactionWatchers.values) { for (var subscription in _transactionWatchers.values) {
subscription.cancel(); subscription.cancel();
} }

View File

@ -52,8 +52,32 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
Future<void> _fetchTransactioPorterById() async { Future<void> _fetchTransactioPorterById() async {
try { try {
final args = Get.arguments as Map<String, dynamic>;
final fromRejected = args['fromRejected'] ?? false;
if (fromRejected) {
log('[Detail History Porter] Mencari transaksi di porterRejections...');
final rejectedTransaction = await _porterController.getRejectedTransactionById(porterTransactionId);
if (rejectedTransaction != null) {
log('[Detail History Porter] Transaksi found di porterRejections');
log('[Detail History Porter] Status: ${rejectedTransaction.status}, Norm: ${rejectedTransaction.normalizedStatus}');
} else {
log('[Detail History Porter] Transaksi tidak ditemukan di porterRejections');
}
} else {
await _porterController.getTransactionById(porterTransactionId); await _porterController.getTransactionById(porterTransactionId);
log('[Detail History Porter] Transaction fetched: ${_porterController.currentTransaction.value}'); final transaction = _porterController.currentTransaction.value;
if (transaction == null) {
log('[Detail History Porter] Transaksi tidak ditemukan di transactions, mengecek porterRejections...');
final rejectedTransaction = await _porterController.getRejectedTransactionById(porterTransactionId);
if (rejectedTransaction != null) {
log('[Detail History Porter] Transaksi ditemukan di porterRejections');
}
}
}
} catch (e) { } catch (e) {
log('[Detail History Porter] Error getTransaction $e'); log('[Detail History Porter] Error getTransaction $e');
} }
@ -61,12 +85,18 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
Future<void> _fetchTransactionData() async { Future<void> _fetchTransactionData() async {
try { try {
await _porterController.getTransactionById(porterTransactionId);
final porterTransaction = _porterController.currentTransaction.value; final porterTransaction = _porterController.currentTransaction.value;
if (porterTransaction != null && porterTransaction.ticketId != null && porterTransaction.transactionId != null) { if (porterTransaction != null && porterTransaction.ticketId != null && porterTransaction.transactionId != null) {
if (porterTransaction.ticketId.isNotEmpty && porterTransaction.transactionId.isNotEmpty) {
await _historyController.getTransactionFromFirestore( await _historyController.getTransactionFromFirestore(
porterTransaction.ticketId, porterTransaction.transactionId); porterTransaction.ticketId, porterTransaction.transactionId);
log('[Detail History Porter] Berhasil mengambil ticket transaction');
} else {
log('[Detail History Porter] ticketId atau transactionId kosong');
_historyController.selectedTransaction.value = null;
}
} }
} catch (e) { } catch (e) {
log('[Detail History Porter] Error fetching data: $e'); log('[Detail History Porter] Error fetching data: $e');
@ -127,6 +157,22 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
if (transaction == null || _porterController.isLoading.value) { if (transaction == null || _porterController.isLoading.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
if (transaction.normalizedStatus == 'rejected' &&
transaction.reassignmentInfo == null &&
transaction.rejectionInfo != null) {
return CustomeShadowCotainner(
child: ButtonFill(
text: 'Alihkan ke Porter Lain',
textColor: Colors.white,
backgroundColor: PrimaryColors.primary700,
onTap: () {
_porterController.forceReassignTransaction(porterTransactionId);
},
),
);
}
switch (transaction.normalizedStatus) { switch (transaction.normalizedStatus) {
case 'pending': case 'pending':
return CustomeShadowCotainner( return CustomeShadowCotainner(
@ -174,8 +220,6 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
case 'selesai': case 'selesai':
case 'rejected': case 'rejected':
return const SizedBox.shrink();
default: default:
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -184,6 +228,10 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
} }
Widget _buildStatusCard(PorterTransactionModel porterTransaction) { Widget _buildStatusCard(PorterTransactionModel porterTransaction) {
String displayStatus = porterTransaction.status;
if (porterTransaction.status == 'rejected' || porterTransaction.rejectionInfo != null) {
displayStatus = 'Ditolak';
}
return CustomeShadowCotainner( return CustomeShadowCotainner(
child: Row( child: Row(
children: [ children: [
@ -206,7 +254,7 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
TypographyStyles.h5( TypographyStyles.h5(
porterTransaction.status, displayStatus,
color: _getStatusColor(porterTransaction), color: _getStatusColor(porterTransaction),
), ),
], ],
@ -303,7 +351,6 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
} }
Widget _buildRejectionInfo(PorterTransactionModel transaction) { Widget _buildRejectionInfo(PorterTransactionModel transaction) {
// Tampilkan info penolakan jika tersedia
if (transaction.rejectionInfo == null) { if (transaction.rejectionInfo == null) {
return CustomeShadowCotainner( return CustomeShadowCotainner(
child: Column( child: Column(
@ -350,6 +397,24 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
_componentRowText(label: 'Tanggal Penolakan', value: rejectionDate), _componentRowText(label: 'Tanggal Penolakan', value: rejectionDate),
SizedBox(height: 6.h), SizedBox(height: 6.h),
_componentRowText(label: 'Waktu Penolakan', value: rejectionTime), _componentRowText(label: 'Waktu Penolakan', value: rejectionTime),
// Tambahkan informasi reassignment jika transaksi telah dialihkan
if (transaction.reassignmentInfo != null) ...[
SizedBox(height: 16.h),
const Divider(),
SizedBox(height: 8.h),
TypographyStyles.body(
'Transaksi Ini Telah Dialihkan',
color: PrimaryColors.primary800,
fontWeight: FontWeight.w600,
),
SizedBox(height: 8.h),
_componentRowText(
label: 'Waktu Pengalihan',
value: _dateFormat.format(transaction.reassignmentInfo!.timestamp) +
' ' +
_timeFormat.format(transaction.reassignmentInfo!.timestamp)),
],
], ],
), ),
); );
@ -401,21 +466,6 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
); );
} }
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;

View File

@ -179,92 +179,44 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
}); });
} }
// Daftar transaksi
Widget _buildTransactionList(String statusFilter) { Widget _buildTransactionList(String statusFilter) {
return Obx(() { return Obx(() {
// Jika sedang loading, tampilkan indikator
if (_porterController.isLoading.value) { if (_porterController.isLoading.value) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// Ambil semua transaksi dari controller List<PorterTransactionModel> allTransactions;
final allTransactions = _porterController.transactions;
// Log untuk debug
log('Semua transaksi: ${allTransactions.length}');
// 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') { if (statusFilter == 'rejected') {
return hasRejectionInfo || tx.normalizedStatus == 'rejected'; allTransactions = _porterController.rejectedTransactions;
log('Semua transaksi ditolak: ${allTransactions.length}');
} else {
allTransactions = _porterController.transactions;
log('Semua transaksi: ${allTransactions.length}');
} }
// Untuk tab lainnya, gunakan status normal final filteredTransactions =
return tx.normalizedStatus == statusFilter; filterTransactions(allTransactions, statusFilter, _porterController.currentPorterId.value);
}).toList();
log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}'); log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}');
// Jika tidak ada transaksi, tampilkan pesan kosong
if (filteredTransactions.isEmpty) { if (filteredTransactions.isEmpty) {
// Jika ada error tapi tidak ada transaksi
if (_porterController.error.value.contains('Porter tidak ditemukan') && if (_porterController.error.value.contains('Porter tidak ditemukan') &&
(statusFilter == 'selesai' || statusFilter == 'rejected')) { (statusFilter == 'selesai' || statusFilter == 'rejected')) {
// Tampilkan pesan yang lebih positif return _buildEmptyStateWithRefresh(statusFilter);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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 ${statusFilter == 'rejected' ? 'ditolak' : 'selesai'}',
color: GrayColors.gray600,
),
SizedBox(height: 8.h),
TypographyStyles.caption(
statusFilter == 'rejected'
? 'Riwayat akan muncul ketika Anda menolak permintaan porter'
: 'Riwayat akan muncul setelah Anda menyelesaikan transaksi',
color: GrayColors.gray500,
),
SizedBox(height: 16.h),
ElevatedButton.icon(
onPressed: () => _porterController.refreshTransactions(),
icon: Icon(Icons.refresh, size: 16.h),
label: const Text('Muat Ulang'),
style: ElevatedButton.styleFrom(
backgroundColor: PrimaryColors.primary800,
foregroundColor: Colors.white,
),
),
],
),
);
} }
return _buildEmptyTransactionMessage(statusFilter); return _buildEmptyTransactionMessage(statusFilter);
} }
// Jika ada transaksi, tampilkan daftar
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => _porterController.refreshTransactions(), onRefresh: () async {
if (statusFilter == 'rejected') {
await _porterController.refreshRejectedTransactions();
} else {
await _porterController.refreshTransactions();
}
},
child: ListView.separated( child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
itemCount: filteredTransactions.length, itemCount: filteredTransactions.length,
@ -275,21 +227,47 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
}); });
} }
// Pesan tidak ada transaksi
Widget _buildEmptyTransactionMessage(String statusFilter) { Widget _buildEmptyTransactionMessage(String statusFilter) {
// Ubah teks pesan sesuai dengan status filter
String statusText = statusFilter; String statusText = statusFilter;
if (statusFilter == 'rejected') { if (statusFilter == 'rejected') {
statusText = 'ditolak'; statusText = 'ditolak';
} else if (statusFilter == 'pending') {
statusText = 'pending';
} else if (statusFilter == 'proses') {
statusText = 'dalam proses';
} else if (statusFilter == 'selesai') {
statusText = 'selesai';
} }
return Center( return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(
_getStatusIcon(statusFilter),
size: 48.h,
color: GrayColors.gray400,
),
SizedBox(height: 16.h),
TypographyStyles.body( TypographyStyles.body(
'Tidak ada transaksi ${statusText.capitalizeFirst}', 'Tidak ada transaksi ${statusText.capitalizeFirst}',
color: GrayColors.gray600, color: GrayColors.gray400,
),
SizedBox(height: 8.h),
TypographyStyles.caption(
statusFilter == 'rejected'
? 'Riwayat akan muncul ketika Anda menolak permintaan porter'
: statusFilter == 'pending'
? 'Riwayat akan muncul ketika Anda menerima permintaan baru'
: statusFilter == 'proses'
? 'Riwayat akan muncul ketika Anda sedang menangani permintaan'
: 'Riwayat akan muncul setelah Anda menyelesaikan transaksi',
color: GrayColors.gray400,
maxlines: 2,
textAlign: TextAlign.center,
fontWeight: FontWeight.w400,
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
ElevatedButton.icon( ElevatedButton.icon(
@ -303,10 +281,10 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
), ),
], ],
), ),
),
); );
} }
// Item transaksi
Widget _buildTransactionItem(PorterTransactionModel transaction) { Widget _buildTransactionItem(PorterTransactionModel transaction) {
log('Building item for transaction: ${transaction.id}, status: ${transaction.status}'); log('Building item for transaction: ${transaction.id}, status: ${transaction.status}');
return FutureBuilder<TransactionModel?>( return FutureBuilder<TransactionModel?>(
@ -351,7 +329,6 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
} }
} }
// Modifikasi tampilan status untuk "rejected"
String displayStatus = transaction.normalizedStatus.capitalizeFirst!; String displayStatus = transaction.normalizedStatus.capitalizeFirst!;
if (transaction.normalizedStatus == 'rejected' || transaction.rejectionInfo != null) { if (transaction.normalizedStatus == 'rejected' || transaction.rejectionInfo != null) {
displayStatus = 'Ditolak'; displayStatus = 'Ditolak';
@ -370,11 +347,11 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
price: _priceFormatter.format(price), price: _priceFormatter.format(price),
statusColor: _getStatusColor(transaction.rejectionInfo != null ? 'rejected' : transaction.normalizedStatus), statusColor: _getStatusColor(transaction.rejectionInfo != null ? 'rejected' : transaction.normalizedStatus),
onTap: () { onTap: () {
log('ID Transaction Porter: ${transaction.id}');
Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: { Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: {
'transactionPorterId': transaction.id, 'transactionPorterId': transaction.id,
'ticketId': transaction.ticketId, 'ticketId': transaction.ticketId,
'ticketTransactionId': transaction.transactionId, 'ticketTransactionId': transaction.transactionId,
'fromRejected': transaction.status == 'rejected' || transaction.rejectionInfo != null,
}); });
}, },
); );
@ -382,6 +359,81 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
); );
} }
Widget _buildEmptyStateWithRefresh(String statusFilter) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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 ${statusFilter == 'rejected' ? 'ditolak' : 'selesai'}',
color: GrayColors.gray600,
),
SizedBox(height: 8.h),
TypographyStyles.caption(
statusFilter == 'rejected'
? 'Riwayat akan muncul ketika Anda menolak permintaan porter'
: 'Riwayat akan muncul setelah Anda menyelesaikan transaksi',
color: GrayColors.gray500,
),
SizedBox(height: 16.h),
ElevatedButton.icon(
onPressed: () => _porterController.refreshTransactions(),
icon: Icon(Icons.refresh, size: 16.h),
label: const Text('Muat Ulang'),
style: ElevatedButton.styleFrom(
backgroundColor: PrimaryColors.primary800,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
List<PorterTransactionModel> filterTransactions(
List<PorterTransactionModel> allTransactions, String statusFilter, String currentPorterId) {
// Buat map untuk menghilangkan duplikasi berdasarkan ID
final uniqueTransactions = <String, PorterTransactionModel>{};
for (var tx in allTransactions) {
if (tx.porterUserId != currentPorterId) {
continue;
}
final hasRejectionInfo = tx.rejectionInfo != null;
bool shouldInclude = false;
switch (statusFilter.toLowerCase()) {
case 'pending':
shouldInclude = tx.normalizedStatus == 'pending' && !hasRejectionInfo;
break;
case 'proses':
shouldInclude = tx.normalizedStatus == 'proses';
break;
case 'selesai':
shouldInclude = tx.normalizedStatus == 'selesai';
break;
case 'rejected':
shouldInclude = tx.status == 'rejected' || hasRejectionInfo;
break;
default:
shouldInclude = tx.normalizedStatus == statusFilter.toLowerCase();
}
if (shouldInclude) {
uniqueTransactions[tx.id] = tx;
}
}
return uniqueTransactions.values.toList();
}
// Warna status // Warna status
Color _getStatusColor(String status) { Color _getStatusColor(String status) {
switch (status) { switch (status) {
@ -397,4 +449,19 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
return GrayColors.gray400; return GrayColors.gray400;
} }
} }
IconData _getStatusIcon(String status) {
switch (status) {
case 'pending':
return Icons.hourglass_empty;
case 'proses':
return Icons.directions_run;
case 'selesai':
return Icons.check_circle_outline;
case 'rejected':
return Icons.cancel_outlined;
default:
return Icons.info_outline;
}
}
} }

View File

@ -229,15 +229,17 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
'Anda berhasil mendapatkan porter', 'Anda berhasil mendapatkan porter',
); );
// 4. Sukses: navigasi ke Processing log('[Scan QR] ID Transaction: $transactionId');
log('[Scan QR] ID Porter Online: ${result['porterId']}');
log('[Scan QR] ID Transaction Porter: ${result['transactionId']}');
Get.toNamed( Get.toNamed(
Routes.PROCESSING, Routes.PROCESSING,
arguments: { arguments: {
'location': rawLocation, 'location': rawLocation,
'ticketId': ticketId, 'ticketId': ticketId,
'transactionId': transactionId, 'transactionId': transactionId,
'porterId': result['porterId']!, 'porterOnlineId': result['porterId']!,
'porterTransactionId': result['transactionId']!, 'transactionPorterId': result['transactionId']!,
}, },
); );
} on Exception catch (e) { } on Exception catch (e) {

View File

@ -1,9 +1,13 @@
import 'dart:developer';
import 'package:e_porter/_core/component/button/button_fill.dart'; import 'package:e_porter/_core/component/button/button_fill.dart';
import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart'; import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart';
import 'package:e_porter/_core/constants/typography.dart'; import 'package:e_porter/_core/constants/typography.dart';
import 'package:e_porter/presentation/controllers/transaction_porter_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../../../_core/component/appbar/appbar_component.dart'; import '../../../../_core/component/appbar/appbar_component.dart';
import '../../../../_core/constants/colors.dart'; import '../../../../_core/constants/colors.dart';
@ -16,13 +20,58 @@ class ProcessingPorterScreen extends StatefulWidget {
} }
class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> { class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
final TransactionPorterController _porterController = Get.find<TransactionPorterController>();
late final String location;
late final String ticketId;
late final String transactionId;
late final String porterOnlineId;
late final String transactionPorterId;
final DateFormat _dateFormat = DateFormat('dd/MM/yyyy');
final DateFormat _timeFormat = DateFormat('HH:mm');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// final args = Get.arguments as Map<String, dynamic>; // final args = Get.arguments as Map<String, dynamic>;
// final location = args['location'] ?? ''; // final location = args['location'] ?? '';
// final ticketId = args['ticketId'] ?? ''; // final ticketId = args['ticketId'] ?? '';
// final transactionId = args['transactionId'] ?? ''; // final transactionId = args['transactionId'];
// final porterOnlineId = args['porterOnlineId'];
// final transactionPorterId = args['transactionPorterId'] ?? '';
_initializeData();
}
void _initializeData() {
final args = Get.arguments as Map<String, dynamic>;
location = args['location'] ?? '';
ticketId = args['ticketId'] ?? '';
transactionId = args['transactionId'] ?? '';
porterOnlineId = args['porterOnlineId'] ?? '';
transactionPorterId = args['transactionPorterId'] ?? '';
if (transactionPorterId.isEmpty) {
log('Error: transactionPorterId tidak tersedia dalam arguments');
Get.back();
return;
}
log('Memulai pemantauan transaksi porter: $transactionPorterId');
// Dapatkan detail transaksi dan mulai memantau perubahan
_porterController.getTransactionById(transactionPorterId).then((transaction) {
if (transaction == null) {
log('Transaksi tidak ditemukan: $transactionPorterId');
} else {
log('Transaksi ditemukan: ${transaction.id}, status: ${transaction.status}');
}
}).catchError((e) {
log('Error mendapatkan transaksi: $e');
});
// Mulai memantau transaksi secara real-time
_porterController.watchTransaction(transactionPorterId);
} }
@override @override
@ -37,8 +86,92 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
Get.back(); Get.back();
}, },
), ),
body: SafeArea( body: SafeArea(child: Obx(
child: Padding( () {
final transaction = _porterController.currentTransaction.value;
final isLoading = _porterController.isLoading.value;
final error = _porterController.error.value;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (error.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48.h, color: Colors.red),
SizedBox(height: 16.h),
TypographyStyles.body(
'Terjadi Kesalahan',
color: Colors.red,
),
SizedBox(height: 8.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 32.w),
child: TypographyStyles.caption(
error,
color: GrayColors.gray600,
textAlign: TextAlign.center,
),
),
SizedBox(height: 16.h),
ButtonFill(
text: 'Kembali',
textColor: Colors.white,
onTap: () => Get.back(),
),
],
),
);
}
if (transaction == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 48.h, color: GrayColors.gray400),
SizedBox(height: 16.h),
TypographyStyles.body(
'Transaksi tidak ditemukan',
color: GrayColors.gray600,
),
SizedBox(height: 16.h),
ButtonFill(
text: 'Kembali',
textColor: Colors.white,
onTap: () => Get.back(),
),
],
),
);
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
child: SingleChildScrollView(
child: Column(
children: [
_buildPorterStatusCard(transaction.status),
SizedBox(height: 20.h),
_buildPorterDetailsCard(transaction),
],
),
),
);
},
)),
bottomNavigationBar: CustomeShadowCotainner(
child: ButtonFill(
text: 'Kembali ke menu',
textColor: Colors.white,
onTap: () {},
),
),
);
}
Widget _buildDesignOld() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@ -76,30 +209,144 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
SizedBox(height: 10.h), SizedBox(height: 10.h),
TypographyStyles.body('Lokasi', color: GrayColors.gray800), TypographyStyles.body('Lokasi', color: GrayColors.gray800),
SizedBox(height: 10.h), SizedBox(height: 10.h),
_buildRowLocation(location: 'Gate Penerbangan', desc: 'Lokasi Anda'), // _buildRowLocation(location: 'Gate Penerbangan', desc: 'Lokasi Anda'),
SizedBox(height: 10.h), SizedBox(height: 10.h),
_buildRowLocation(location: 'Guyangan', desc: 'Lokasi Porter Anda'), // _buildRowLocation(location: 'Guyangan', desc: 'Lokasi Porter Anda'),
SizedBox(height: 10.h), SizedBox(height: 10.h),
_buildRowLocation(location: 'Porter menuju ke lokasi anda', desc: 'Porter bergerak'), // _buildRowLocation(location: 'Porter menuju ke lokasi anda', desc: 'Porter bergerak'),
], ],
), ),
), ),
], ],
), ),
), ),
),
),
bottomNavigationBar: CustomeShadowCotainner(
child: ButtonFill(
text: 'Kembali ke menu',
textColor: Colors.white,
onTap: () {},
),
),
); );
} }
Widget _buildRowLocation({required String location, required String desc}) { Widget _buildPorterStatusCard(String status) {
String statusText = '';
Widget statusIcon = const SizedBox.shrink();
switch (status.toLowerCase()) {
case 'pending':
statusText = 'Tunggu Portermu';
statusIcon = Icon(Icons.timelapse_outlined, color: Colors.orange);
break;
case 'proses':
statusText = 'Porter dalam Perjalanan';
statusIcon = Icon(Icons.directions_walk, color: PrimaryColors.primary800);
break;
case 'selesai':
statusText = 'Layanan Porter Selesai';
statusIcon = Icon(Icons.check_circle, color: Colors.green);
break;
default:
statusText = 'Status Porter Tidak Diketahui';
statusIcon = Icon(Icons.help_outline, color: GrayColors.gray400);
}
return CustomeShadowCotainner(
child: Column(
children: [
TypographyStyles.h6(statusText, color: GrayColors.gray800),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TypographyStyles.caption(
_getStatusDescription(status),
color: GrayColors.gray500,
fontWeight: FontWeight.w400,
),
SizedBox(width: 4.w),
statusIcon,
],
),
],
),
);
}
String _getStatusDescription(String status) {
switch (status.toLowerCase()) {
case 'pending':
return 'Tunggu dari pihak Porter merespon';
case 'proses':
return 'Porter sedang menuju lokasi Anda';
case 'selesai':
return 'Layanan porter telah selesai';
default:
return 'Status tidak diketahui';
}
}
Widget _buildPorterDetailsCard(dynamic transaction) {
final now = DateTime.now();
return CustomeShadowCotainner(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body('Informasi Porter', color: GrayColors.gray800),
SizedBox(height: 10.h),
Divider(thickness: 1, color: GrayColors.gray200),
SizedBox(height: 10.h),
TypographyStyles.body('Lokasi', color: GrayColors.gray800),
SizedBox(height: 10.h),
_buildRowLocation(
location: location.isNotEmpty ? location : transaction.locationPassenger,
desc: 'Lokasi Anda',
timestamp: transaction.createdAt,
),
SizedBox(height: 10.h),
_buildRowLocation(
location: transaction.locationPorter,
desc: 'Lokasi Porter Anda',
timestamp: transaction.createdAt,
),
SizedBox(height: 10.h),
_buildRowLocation(
location: _getLocationStatusText(transaction.status),
desc: 'Status Porter',
timestamp: now,
),
SizedBox(height: 10.h),
TypographyStyles.body('Kode Porter', color: GrayColors.gray800),
SizedBox(height: 4.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: PrimaryColors.primary100,
borderRadius: BorderRadius.circular(4.r),
),
child: TypographyStyles.h6(
transaction.kodePorter,
color: PrimaryColors.primary800,
),
),
],
),
);
}
String _getLocationStatusText(String status) {
switch (status.toLowerCase()) {
case 'pending':
return 'Menunggu konfirmasi porter';
case 'proses':
return 'Porter menuju ke lokasi anda';
case 'selesai':
return 'Layanan porter selesai';
default:
return 'Status tidak diketahui';
}
}
Widget _buildRowLocation({
required String location,
required String desc,
required DateTime timestamp,
}) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -107,19 +354,20 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
TypographyStyles.caption( TypographyStyles.caption(
'10/10/2025', _dateFormat.format(timestamp),
color: GrayColors.gray600, color: GrayColors.gray600,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
TypographyStyles.caption( TypographyStyles.caption(
'11:11', _timeFormat.format(timestamp),
color: GrayColors.gray600, color: GrayColors.gray600,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
], ],
), ),
SizedBox(width: 20.w), SizedBox(width: 20.w),
Column( Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TypographyStyles.caption( TypographyStyles.caption(
@ -134,6 +382,7 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
) )
], ],
), ),
),
], ],
); );
} }

View File

@ -0,0 +1,99 @@
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:e_porter/_core/service/transaction_reassignment_service.dart';
import 'package:e_porter/data/repositories/transaction_porter_repository_impl.dart';
import 'package:e_porter/domain/usecases/transaction_porter_usecase.dart';
import 'package:e_porter/presentation/controllers/transaction_porter_controller.dart';
import 'package:get/get.dart';
class PorterServiceProvider {
static TransactionReassignmentService? _reassignmentService;
static bool _isInitialized = false;
/// Inisialisasi layanan Porter
static void initServices() {
if (_isInitialized) {
log('[PorterServiceProvider] Layanan Porter sudah diinisialisasi sebelumnya');
return;
}
log('[PorterServiceProvider] Menginisialisasi layanan Porter');
// Inisialisasi service reassignment transaksi hanya dengan Firestore
_reassignmentService = TransactionReassignmentService(firestore: FirebaseFirestore.instance);
// Mulai service dengan interval 30 detik untuk pengujian
_reassignmentService!.startService(intervalSeconds: 30);
_isInitialized = true;
}
/// Menghentikan layanan Porter
static void stopServices() {
log('[PorterServiceProvider] Menghentikan layanan Porter');
if (_reassignmentService != null) {
_reassignmentService!.stopService();
_reassignmentService = null;
}
_isInitialized = false;
}
/// Inisialisasi controller dan repository
static void registerDependencies() {
log('[PorterServiceProvider] Mendaftarkan dependencies Porter');
// Register Repository jika belum terdaftar - hanya menggunakan Firestore
if (!Get.isRegistered<TransactionPorterRepositoryImpl>()) {
Get.lazyPut<TransactionPorterRepositoryImpl>(
() => TransactionPorterRepositoryImpl(firestore: FirebaseFirestore.instance),
fenix: true // Mempertahankan instance meskipun tidak ada yang menggunakan
);
}
// Register UseCase
if (!Get.isRegistered<TransactionPorterUsecase>()) {
Get.lazyPut<TransactionPorterUsecase>(() => TransactionPorterUsecase(Get.find<TransactionPorterRepositoryImpl>()),
fenix: true);
}
// Register Controller
if (!Get.isRegistered<TransactionPorterController>()) {
Get.lazyPut<TransactionPorterController>(() => TransactionPorterController(Get.find<TransactionPorterUsecase>()),
fenix: true);
}
log('[PorterServiceProvider] Dependencies Porter berhasil terdaftar');
}
/// Paksa reassignment untuk transaksi yang ditolak
static Future<void> forceReassignmentCheck() async {
log('[PorterServiceProvider] Memaksa pengecekan reassignment');
if (_reassignmentService == null) {
_reassignmentService = TransactionReassignmentService(firestore: FirebaseFirestore.instance);
}
try {
await _reassignmentService!.forceReassignmentCheck();
} catch (e) {
log('[PorterServiceProvider] Error saat memaksa reassignment: $e');
try {
resetReassignmentService();
await _reassignmentService!.forceReassignmentCheck();
} catch (fallbackError) {
log('[PorterServiceProvider] Fallback juga gagal: $fallbackError');
}
}
}
/// Reset TransactionReassignmentService jika diperlukan
static void resetReassignmentService() {
if (_reassignmentService != null) {
_reassignmentService!.stopService();
}
_reassignmentService = TransactionReassignmentService(firestore: FirebaseFirestore.instance);
_reassignmentService!.startService(intervalSeconds: 30);
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/domain/models/transaction_porter_model.dart';
class PorterTransactionHelper {
/// Filter transaksi berdasarkan status dan ID porter
static List<PorterTransactionModel> filterByStatus(
List<PorterTransactionModel> transactions,
String statusFilter,
String currentPorterId,
) {
return transactions.where((tx) {
// Pastikan hanya menampilkan transaksi untuk porter saat ini
if (tx.porterUserId != currentPorterId) {
return false;
}
// Cek apakah memiliki info penolakan atau penugasan ulang
final hasRejectionInfo = tx.rejectionInfo != null;
// final hasReassignmentInfo = tx.reassignmentInfo != null;
// Filter berdasarkan status
switch (statusFilter.toLowerCase()) {
case 'pending':
return tx.normalizedStatus == 'pending' && !hasRejectionInfo;
case 'proses':
return tx.normalizedStatus == 'proses';
case 'selesai':
return tx.normalizedStatus == 'selesai';
case 'rejected':
return hasRejectionInfo || tx.normalizedStatus == 'rejected';
default:
return tx.normalizedStatus == statusFilter.toLowerCase();
}
}).toList();
}
/// Mendapatkan warna status untuk UI
static Color getStatusColor(String status) {
switch (status) {
case 'pending':
return Colors.orange;
case 'proses':
return PrimaryColors.primary800;
case 'selesai':
return Colors.green;
case 'rejected':
return RedColors.red500;
default:
return GrayColors.gray400;
}
}
/// Mendapatkan ikon status untuk UI
static IconData getStatusIcon(String status) {
switch (status) {
case 'pending':
return Icons.hourglass_empty;
case 'proses':
return Icons.directions_run;
case 'selesai':
return Icons.check_circle_outline;
case 'rejected':
return Icons.cancel_outlined;
default:
return Icons.info_outline;
}
}
}

View File

@ -166,6 +166,7 @@ class AppRoutes {
GetPage( GetPage(
name: Routes.PROCESSING, name: Routes.PROCESSING,
page: () => ProcessingPorterScreen(), page: () => ProcessingPorterScreen(),
binding: TransactionPorterBinding(),
), ),
GetPage( GetPage(
name: Routes.HISTORYPORTER, name: Routes.HISTORYPORTER,