Feat: fix bug reassignments
This commit is contained in:
parent
ec3435850b
commit
56b629b1d7
|
@ -638,7 +638,7 @@
|
|||
"languageVersion": "3.4"
|
||||
}
|
||||
],
|
||||
"generated": "2025-04-29T13:17:45.754727Z",
|
||||
"generated": "2025-04-30T16:03:15.794012Z",
|
||||
"generator": "pub",
|
||||
"generatorVersion": "3.5.0",
|
||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
||||
|
|
|
@ -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
|
@ -1,3 +1,4 @@
|
|||
import 'package:e_porter/presentation/screens/boarding_pass/provider/porter_service_provider.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:e_porter/data/repositories/transaction_repository_impl.dart';
|
||||
import 'package:e_porter/_core/service/transaction_expiry_service.dart';
|
||||
|
@ -7,9 +8,15 @@ class AppBinding extends Bindings {
|
|||
void dependencies() {
|
||||
// Inisialisasi TransactionExpiryService
|
||||
Get.put<TransactionRepositoryImpl>(TransactionRepositoryImpl(), permanent: true);
|
||||
|
||||
|
||||
// Inisialisasi dan mulai service
|
||||
final repository = Get.find<TransactionRepositoryImpl>();
|
||||
TransactionExpiryService().initialize(repository);
|
||||
|
||||
// Inisialisasi dependency Porter
|
||||
PorterServiceProvider.registerDependencies();
|
||||
|
||||
// Mulai layanan pengalihan transaksi
|
||||
PorterServiceProvider.initServices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ class PorterTransactionModel {
|
|||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final RejectionInfo? rejectionInfo;
|
||||
final ReassignmentInfo? reassignmentInfo;
|
||||
final String? previousTransactionId;
|
||||
final bool? isRejected;
|
||||
final bool? hasAssignedPorter;
|
||||
|
||||
PorterTransactionModel({
|
||||
required this.id,
|
||||
|
@ -32,6 +36,10 @@ class PorterTransactionModel {
|
|||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
this.rejectionInfo,
|
||||
this.reassignmentInfo,
|
||||
this.previousTransactionId,
|
||||
this.isRejected,
|
||||
this.hasAssignedPorter,
|
||||
}) : normalizedStatus = _normalizeStatus(status, 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(
|
||||
id: id,
|
||||
kodePorter: json['kodePorter'] ?? '',
|
||||
|
@ -107,6 +136,10 @@ class PorterTransactionModel {
|
|||
: (json['updatedAt'] is DateTime ? json['updatedAt'] : null))
|
||||
: null,
|
||||
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,
|
||||
'transactionId': transactionId,
|
||||
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||
'rejectionInfo': rejectionInfo?.toJson(),
|
||||
'reassignmentInfo': reassignmentInfo?.toJson(),
|
||||
'previousTransactionId': previousTransactionId,
|
||||
'isRejected': isRejected,
|
||||
'hasAssignedPorter': hasAssignedPorter,
|
||||
};
|
||||
|
||||
if (updatedAt != null) {
|
||||
|
@ -136,7 +174,6 @@ class PorterTransactionModel {
|
|||
return data;
|
||||
}
|
||||
|
||||
// Membuat salinan model dengan nilai baru
|
||||
PorterTransactionModel copyWith({
|
||||
String? id,
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import '../models/transaction_porter_model.dart';
|
||||
|
||||
abstract class TransactionPorterRepository {
|
||||
Stream<List<PorterTransactionModel>> watchPorterTransactionsByUserId(String userId);
|
||||
|
||||
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId);
|
||||
|
||||
Stream<PorterTransactionModel?> watchTransactionById(String transactionId);
|
||||
|
||||
Future<Map<String, dynamic>?> getPorterTransactionById(String transactionId);
|
||||
|
||||
Future<PorterTransactionModel?> getTransactionById(String transactionId);
|
||||
|
||||
Future<List<String>> getPorterTransactionIds(String porterId);
|
||||
Stream<List<PorterTransactionModel>> watchRejectedTransactionsByPorter(String porterUserId);
|
||||
|
||||
Future<void> updateTransactionStatus({
|
||||
required String transactionId,
|
||||
|
|
|
@ -5,6 +5,10 @@ class TransactionPorterUsecase {
|
|||
final TransactionPorterRepository _repository;
|
||||
TransactionPorterUsecase(this._repository);
|
||||
|
||||
Stream<List<PorterTransactionModel>> watchPorterTransactionsByUserId(String userId) {
|
||||
return _repository.watchPorterTransactionsByUserId(userId);
|
||||
}
|
||||
|
||||
Stream<List<PorterTransactionModel>> watchPorterTransactions(String porterId) {
|
||||
return _repository.watchPorterTransactions(porterId);
|
||||
}
|
||||
|
@ -13,16 +17,12 @@ 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);
|
||||
}
|
||||
|
||||
Future<List<String>> getPorterTransactionIds(String porterId) {
|
||||
return _repository.getPorterTransactionIds(porterId);
|
||||
Stream<List<PorterTransactionModel>> watchRejectedTransactionsByPorter(String porterUserId) {
|
||||
return _repository.watchRejectedTransactionsByPorter(porterUserId);
|
||||
}
|
||||
|
||||
Future<void> updateTransactionStatus({
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
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:e_porter/presentation/screens/boarding_pass/provider/porter_service_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../domain/models/transaction_porter_model.dart';
|
||||
|
@ -15,21 +16,31 @@ class TransactionPorterController extends GetxController {
|
|||
TransactionPorterController(this._useCase);
|
||||
|
||||
final RxList<PorterTransactionModel> transactions = <PorterTransactionModel>[].obs;
|
||||
final RxList<PorterTransactionModel> rejectedTransactions = <PorterTransactionModel>[].obs;
|
||||
final Rx<PorterTransactionModel?> currentTransaction = Rx<PorterTransactionModel?>(null);
|
||||
final Map<String, StreamSubscription<PorterTransactionModel?>> _transactionWatchers = {};
|
||||
final RxString currentPorterId = ''.obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString error = ''.obs;
|
||||
|
||||
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();
|
||||
|
||||
StreamSubscription<List<PorterTransactionModel>>? _subscription;
|
||||
StreamSubscription<PorterQueueModel?>? _porterSubscription;
|
||||
StreamSubscription<List<PorterTransactionModel>>? _rejectedSubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadPorterData();
|
||||
_watchRejectedTransactions();
|
||||
}
|
||||
|
||||
Future<void> _loadPorterData() async {
|
||||
|
@ -40,105 +51,63 @@ class TransactionPorterController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Set userId untuk digunakan nanti
|
||||
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
|
||||
String userIdForHistory = userId;
|
||||
|
||||
// Coba mendapatkan porter aktif
|
||||
_porterSubscription = porterCtrl.watchPorter(userId).listen((porter) {
|
||||
if (porter != null && porter.id != null) {
|
||||
log('Porter aktif ditemukan: ${porter.id}');
|
||||
currentPorterId.value = porter.id!;
|
||||
|
||||
// Gunakan userId untuk memastikan semua transaksi bisa diakses
|
||||
loadTransactionsWithBothIDs(porter.id!, userIdForHistory);
|
||||
} else {
|
||||
// Porter tidak aktif, coba ambil riwayat dari userId
|
||||
log('Porter aktif tidak ditemukan, mencoba ambil riwayat dari userId: $userId');
|
||||
loadTransactionsFromUserId(userId);
|
||||
}
|
||||
}, onError: (e) {
|
||||
log('Error memantau data porter: $e');
|
||||
// Jika error, coba ambil riwayat dari userId
|
||||
loadTransactionsFromUserId(userId);
|
||||
});
|
||||
log('Menggunakan userId dari preferences: $userId');
|
||||
loadTransactionsFromUserId(userId); // Langsung gunakan userId
|
||||
} catch (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;
|
||||
error.value = '';
|
||||
|
||||
_subscription?.cancel();
|
||||
_subscription = _useCase.watchPorterTransactions(porterId).listen((transactionList) {
|
||||
log('Menerima ${transactionList.length} transaksi dari porterId');
|
||||
_subscription = _useCase.watchPorterTransactionsByUserId(userId).listen(
|
||||
(transactionList) {
|
||||
log('[TransactionPorterController] Menerima ${transactionList.length} transaksi dari userId: $userId');
|
||||
|
||||
if (transactionList.isEmpty) {
|
||||
log('[TransactionPorterController] Tidak ada transaksi ditemukan untuk userId: $userId');
|
||||
if (transactions.isNotEmpty) {
|
||||
transactions.clear();
|
||||
}
|
||||
} else {
|
||||
transactions.assignAll(transactionList);
|
||||
|
||||
// Mulai memantau setiap transaksi individual untuk real-time updates
|
||||
for (var transaction in transactionList) {
|
||||
watchTransaction(transaction.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak ada transaksi dari porterId, coba dari userId
|
||||
if (transactionList.isEmpty && userId != porterId) {
|
||||
log('Tidak ada transaksi dari porterId, mencoba dari userId: $userId');
|
||||
loadTransactionsFromUserId(userId);
|
||||
} else {
|
||||
transactions.assignAll(transactionList);
|
||||
isLoading.value = false;
|
||||
}
|
||||
}, onError: (e) {
|
||||
log('Error streaming transaksi: $e');
|
||||
|
||||
// Jika terjadi error, coba dari userId sebagai fallback
|
||||
if (userId != porterId) {
|
||||
log('Error dengan porterId, mencoba dari userId: $userId');
|
||||
loadTransactionsFromUserId(userId);
|
||||
} else {
|
||||
},
|
||||
onError: (e) {
|
||||
log('[TransactionPorterController] Error streaming transaksi: $e');
|
||||
error.value = 'Gagal memuat transaksi: $e';
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void loadTransactionsFromUserId(String userId) {
|
||||
isLoading.value = true;
|
||||
error.value = '';
|
||||
|
||||
log('[TransactionPorterController] Memuat transaksi untuk userId: $userId');
|
||||
|
||||
// Gunakan userId sebagai porterId di porterHistory
|
||||
_useCase.getPorterTransactionIds(userId).then((transactionIds) {
|
||||
log('[TransactionPorterController] Ditemukan ${transactionIds.length} ID transaksi untuk userId: $userId');
|
||||
|
||||
if (transactionIds.isEmpty) {
|
||||
transactions.clear();
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ambil dan proses data transaksi
|
||||
_processTransactionData(transactionIds);
|
||||
}).catchError((e) {
|
||||
log('[TransactionPorterController] Error mendapatkan ID transaksi: $e');
|
||||
error.value = 'Gagal memuat riwayat transaksi';
|
||||
isLoading.value = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void watchTransaction(String transactionId) {
|
||||
// Batalkan subscription yang ada jika ada
|
||||
_transactionWatchers[transactionId]?.cancel();
|
||||
|
||||
// Mulai subscription baru
|
||||
_transactionWatchers[transactionId] = _useCase.watchTransactionById(transactionId).listen(
|
||||
(updatedTransaction) {
|
||||
if (updatedTransaction != null) {
|
||||
// Update current transaction jika itu transaksi yang sedang aktif
|
||||
if (currentTransaction.value?.id == transactionId) {
|
||||
currentTransaction.value = updatedTransaction;
|
||||
}
|
||||
|
||||
// Update transaksi di daftar
|
||||
final index = transactions.indexWhere((tx) => tx.id == transactionId);
|
||||
if (index >= 0) {
|
||||
transactions[index] = updatedTransaction;
|
||||
|
@ -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) {
|
||||
if (porterId.isEmpty) {
|
||||
error.value = 'Porter ID tidak boleh kosong';
|
||||
|
@ -190,7 +135,6 @@ class TransactionPorterController extends GetxController {
|
|||
log('Menerima ${transactionList.length} transaksi');
|
||||
transactions.assignAll(transactionList);
|
||||
|
||||
// Mulai memantau setiap transaksi individual untuk real-time updates
|
||||
for (var transaction in transactionList) {
|
||||
watchTransaction(transaction.id);
|
||||
}
|
||||
|
@ -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 {
|
||||
try {
|
||||
log('Getting transaction by ID: $transactionId');
|
||||
isLoading.value = true;
|
||||
error.value = '';
|
||||
|
||||
// Reset dulu current transaction agar UI bisa merespons ke loading state
|
||||
currentTransaction.value = null;
|
||||
|
||||
final transaction = await _useCase.getTransactionById(transactionId);
|
||||
|
@ -233,7 +160,6 @@ class TransactionPorterController extends GetxController {
|
|||
log('Transaction found and set to current: ${transaction.id}');
|
||||
currentTransaction.value = transaction;
|
||||
|
||||
// Mulai memantau transaksi ini
|
||||
watchTransaction(transactionId);
|
||||
} else {
|
||||
log('Transaction not found with ID: $transactionId');
|
||||
|
@ -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({
|
||||
required String transactionId,
|
||||
required String status,
|
||||
|
@ -265,19 +294,14 @@ class TransactionPorterController extends GetxController {
|
|||
status: status,
|
||||
);
|
||||
|
||||
// Dapatkan transaksi yang diperbarui
|
||||
final updatedTransaction = await getTransactionById(transactionId);
|
||||
|
||||
// Update list transaksi yang ada dengan yang baru
|
||||
if (updatedTransaction != null) {
|
||||
// Cari indeks transaksi dalam list yang ada
|
||||
final index = transactions.indexWhere((tx) => tx.id == transactionId);
|
||||
if (index >= 0) {
|
||||
// Update transaksi pada indeks yang ditemukan
|
||||
transactions[index] = updatedTransaction;
|
||||
log('Transaksi di daftar utama diperbarui: $transactionId dengan status: $status');
|
||||
} else {
|
||||
// Jika tidak ditemukan, perbarui seluruh list
|
||||
log('Transaksi tidak ditemukan di daftar, menyegarkan seluruh daftar');
|
||||
refreshTransactions();
|
||||
}
|
||||
|
@ -299,39 +323,19 @@ class TransactionPorterController extends GetxController {
|
|||
}) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
isRejecting.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,
|
||||
);
|
||||
needsRefresh.value = true;
|
||||
|
||||
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) {
|
||||
|
@ -342,20 +346,31 @@ class TransactionPorterController extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
// Reset controller alasan
|
||||
rejectionReasonController.clear();
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
|
||||
try {
|
||||
log('[TransactionPorterController] Memaksa pengecekan reassignment...');
|
||||
await PorterServiceProvider.forceReassignmentCheck();
|
||||
} catch (reassignError) {
|
||||
log('[TransactionPorterController] Error saat memaksa reassignment: $reassignError');
|
||||
}
|
||||
|
||||
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');
|
||||
SnackbarHelper.showError('Gagal', 'Transaksi gagal ditolak: ${e.toString()}');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isRejecting.value = false;
|
||||
|
||||
if (needsRefresh.value) {
|
||||
await refreshTransactions();
|
||||
needsRefresh.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Metode serupa untuk completePorterTransaction
|
||||
Future<void> completePorterTransaction({
|
||||
required String transactionId,
|
||||
}) async {
|
||||
|
@ -380,10 +395,8 @@ class TransactionPorterController extends GetxController {
|
|||
porterOnlineId: porterOnlineId,
|
||||
);
|
||||
|
||||
// Dapatkan transaksi yang diperbarui
|
||||
final updatedTransaction = await getTransactionById(transactionId);
|
||||
|
||||
// Update list transaksi
|
||||
if (updatedTransaction != null) {
|
||||
final index = transactions.indexWhere((tx) => tx.id == transactionId);
|
||||
if (index >= 0) {
|
||||
|
@ -428,11 +441,104 @@ class TransactionPorterController extends GetxController {
|
|||
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
|
||||
void onClose() {
|
||||
rejectionReasonController.dispose();
|
||||
_porterSubscription?.cancel();
|
||||
_subscription?.cancel();
|
||||
_rejectedSubscription?.cancel();
|
||||
for (var subscription in _transactionWatchers.values) {
|
||||
subscription.cancel();
|
||||
}
|
||||
|
|
|
@ -52,8 +52,32 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
|
||||
Future<void> _fetchTransactioPorterById() async {
|
||||
try {
|
||||
await _porterController.getTransactionById(porterTransactionId);
|
||||
log('[Detail History Porter] Transaction fetched: ${_porterController.currentTransaction.value}');
|
||||
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);
|
||||
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) {
|
||||
log('[Detail History Porter] Error getTransaction $e');
|
||||
}
|
||||
|
@ -61,12 +85,18 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
|
||||
Future<void> _fetchTransactionData() async {
|
||||
try {
|
||||
await _porterController.getTransactionById(porterTransactionId);
|
||||
final porterTransaction = _porterController.currentTransaction.value;
|
||||
|
||||
if (porterTransaction != null && porterTransaction.ticketId != null && porterTransaction.transactionId != null) {
|
||||
await _historyController.getTransactionFromFirestore(
|
||||
porterTransaction.ticketId, porterTransaction.transactionId);
|
||||
if (porterTransaction.ticketId.isNotEmpty && porterTransaction.transactionId.isNotEmpty) {
|
||||
await _historyController.getTransactionFromFirestore(
|
||||
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) {
|
||||
log('[Detail History Porter] Error fetching data: $e');
|
||||
|
@ -127,6 +157,22 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
if (transaction == null || _porterController.isLoading.value) {
|
||||
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) {
|
||||
case 'pending':
|
||||
return CustomeShadowCotainner(
|
||||
|
@ -174,8 +220,6 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
|
||||
case 'selesai':
|
||||
case 'rejected':
|
||||
return const SizedBox.shrink();
|
||||
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
@ -184,6 +228,10 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
}
|
||||
|
||||
Widget _buildStatusCard(PorterTransactionModel porterTransaction) {
|
||||
String displayStatus = porterTransaction.status;
|
||||
if (porterTransaction.status == 'rejected' || porterTransaction.rejectionInfo != null) {
|
||||
displayStatus = 'Ditolak';
|
||||
}
|
||||
return CustomeShadowCotainner(
|
||||
child: Row(
|
||||
children: [
|
||||
|
@ -206,7 +254,7 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
TypographyStyles.h5(
|
||||
porterTransaction.status,
|
||||
displayStatus,
|
||||
color: _getStatusColor(porterTransaction),
|
||||
),
|
||||
],
|
||||
|
@ -303,7 +351,6 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
}
|
||||
|
||||
Widget _buildRejectionInfo(PorterTransactionModel transaction) {
|
||||
// Tampilkan info penolakan jika tersedia
|
||||
if (transaction.rejectionInfo == null) {
|
||||
return CustomeShadowCotainner(
|
||||
child: Column(
|
||||
|
@ -350,6 +397,24 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
|||
_componentRowText(label: 'Tanggal Penolakan', value: rejectionDate),
|
||||
SizedBox(height: 6.h),
|
||||
_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) {
|
||||
if (transaction == null) return GrayColors.gray400;
|
||||
|
||||
|
|
|
@ -179,92 +179,44 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
// Daftar transaksi
|
||||
Widget _buildTransactionList(String statusFilter) {
|
||||
return Obx(() {
|
||||
// Jika sedang loading, tampilkan indikator
|
||||
if (_porterController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Ambil semua transaksi dari controller
|
||||
final allTransactions = _porterController.transactions;
|
||||
List<PorterTransactionModel> allTransactions;
|
||||
|
||||
// Log untuk debug
|
||||
log('Semua transaksi: ${allTransactions.length}');
|
||||
if (statusFilter == 'rejected') {
|
||||
allTransactions = _porterController.rejectedTransactions;
|
||||
log('Semua transaksi ditolak: ${allTransactions.length}');
|
||||
} else {
|
||||
allTransactions = _porterController.transactions;
|
||||
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') {
|
||||
return hasRejectionInfo || tx.normalizedStatus == 'rejected';
|
||||
}
|
||||
|
||||
// Untuk tab lainnya, gunakan status normal
|
||||
return tx.normalizedStatus == statusFilter;
|
||||
}).toList();
|
||||
final filteredTransactions =
|
||||
filterTransactions(allTransactions, statusFilter, _porterController.currentPorterId.value);
|
||||
|
||||
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' || statusFilter == 'rejected')) {
|
||||
// Tampilkan pesan yang lebih positif
|
||||
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 _buildEmptyStateWithRefresh(statusFilter);
|
||||
}
|
||||
|
||||
return _buildEmptyTransactionMessage(statusFilter);
|
||||
}
|
||||
|
||||
// Jika ada transaksi, tampilkan daftar
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _porterController.refreshTransactions(),
|
||||
onRefresh: () async {
|
||||
if (statusFilter == 'rejected') {
|
||||
await _porterController.refreshRejectedTransactions();
|
||||
} else {
|
||||
await _porterController.refreshTransactions();
|
||||
}
|
||||
},
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
itemCount: filteredTransactions.length,
|
||||
|
@ -275,38 +227,64 @@ 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';
|
||||
} else if (statusFilter == 'pending') {
|
||||
statusText = 'pending';
|
||||
} else if (statusFilter == 'proses') {
|
||||
statusText = 'dalam proses';
|
||||
} else if (statusFilter == 'selesai') {
|
||||
statusText = 'selesai';
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TypographyStyles.body(
|
||||
'Tidak ada transaksi ${statusText.capitalizeFirst}',
|
||||
color: GrayColors.gray600,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _porterController.refreshTransactions(),
|
||||
icon: Icon(Icons.refresh, size: 16.h),
|
||||
label: const Text('Refresh'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: PrimaryColors.primary800,
|
||||
foregroundColor: Colors.white,
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_getStatusIcon(statusFilter),
|
||||
size: 48.h,
|
||||
color: GrayColors.gray400,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 16.h),
|
||||
TypographyStyles.body(
|
||||
'Tidak ada transaksi ${statusText.capitalizeFirst}',
|
||||
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),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _porterController.refreshTransactions(),
|
||||
icon: Icon(Icons.refresh, size: 16.h),
|
||||
label: const Text('Refresh'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: PrimaryColors.primary800,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Item transaksi
|
||||
Widget _buildTransactionItem(PorterTransactionModel transaction) {
|
||||
log('Building item for transaction: ${transaction.id}, status: ${transaction.status}');
|
||||
return FutureBuilder<TransactionModel?>(
|
||||
|
@ -351,7 +329,6 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
// Modifikasi tampilan status untuk "rejected"
|
||||
String displayStatus = transaction.normalizedStatus.capitalizeFirst!;
|
||||
if (transaction.normalizedStatus == 'rejected' || transaction.rejectionInfo != null) {
|
||||
displayStatus = 'Ditolak';
|
||||
|
@ -370,11 +347,11 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
|||
price: _priceFormatter.format(price),
|
||||
statusColor: _getStatusColor(transaction.rejectionInfo != null ? 'rejected' : transaction.normalizedStatus),
|
||||
onTap: () {
|
||||
log('ID Transaction Porter: ${transaction.id}');
|
||||
Get.toNamed(Routes.DETAILHISTORYPORTER, arguments: {
|
||||
'transactionPorterId': transaction.id,
|
||||
'ticketId': transaction.ticketId,
|
||||
'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
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status) {
|
||||
|
@ -397,4 +449,19 @@ class _HistoryPorterScreenState extends State<HistoryPorterScreen> {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,15 +229,17 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
|
|||
'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(
|
||||
Routes.PROCESSING,
|
||||
arguments: {
|
||||
'location': rawLocation,
|
||||
'ticketId': ticketId,
|
||||
'transactionId': transactionId,
|
||||
'porterId': result['porterId']!,
|
||||
'porterTransactionId': result['transactionId']!,
|
||||
'porterOnlineId': result['porterId']!,
|
||||
'transactionPorterId': result['transactionId']!,
|
||||
},
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:e_porter/_core/component/button/button_fill.dart';
|
||||
import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart';
|
||||
import 'package:e_porter/_core/constants/typography.dart';
|
||||
import 'package:e_porter/presentation/controllers/transaction_porter_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../_core/component/appbar/appbar_component.dart';
|
||||
import '../../../../_core/constants/colors.dart';
|
||||
|
@ -16,13 +20,58 @@ class ProcessingPorterScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
// final args = Get.arguments as Map<String, dynamic>;
|
||||
// final location = args['location'] ?? '';
|
||||
// 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
|
||||
|
@ -37,58 +86,80 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
|
|||
Get.back();
|
||||
},
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
TypographyStyles.h1('Ilustrasi'),
|
||||
SizedBox(height: 32.h),
|
||||
CustomeShadowCotainner(
|
||||
child: Column(
|
||||
children: [
|
||||
TypographyStyles.h6('Tunggu Portermu', color: GrayColors.gray800),
|
||||
SizedBox(height: 4.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TypographyStyles.caption(
|
||||
'Tunngu dari pihak Porter merespon',
|
||||
color: GrayColors.gray500,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Icon(Icons.timelapse_outlined)
|
||||
],
|
||||
),
|
||||
],
|
||||
body: SafeArea(child: Obx(
|
||||
() {
|
||||
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: 20.h),
|
||||
CustomeShadowCotainner(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.body('Ahmad Choirul Umam', 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: 'Gate Penerbangan', desc: 'Lokasi Anda'),
|
||||
SizedBox(height: 10.h),
|
||||
_buildRowLocation(location: 'Guyangan', desc: 'Lokasi Porter Anda'),
|
||||
SizedBox(height: 10.h),
|
||||
_buildRowLocation(location: 'Porter menuju ke lokasi anda', desc: 'Porter bergerak'),
|
||||
],
|
||||
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',
|
||||
|
@ -99,7 +170,183 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildRowLocation({required String location, required String desc}) {
|
||||
Widget _buildDesignOld() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
TypographyStyles.h1('Ilustrasi'),
|
||||
SizedBox(height: 32.h),
|
||||
CustomeShadowCotainner(
|
||||
child: Column(
|
||||
children: [
|
||||
TypographyStyles.h6('Tunggu Portermu', color: GrayColors.gray800),
|
||||
SizedBox(height: 4.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TypographyStyles.caption(
|
||||
'Tunngu dari pihak Porter merespon',
|
||||
color: GrayColors.gray500,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Icon(Icons.timelapse_outlined)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
CustomeShadowCotainner(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.body('Ahmad Choirul Umam', 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: 'Gate Penerbangan', desc: 'Lokasi Anda'),
|
||||
SizedBox(height: 10.h),
|
||||
// _buildRowLocation(location: 'Guyangan', desc: 'Lokasi Porter Anda'),
|
||||
SizedBox(height: 10.h),
|
||||
// _buildRowLocation(location: 'Porter menuju ke lokasi anda', desc: 'Porter bergerak'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -107,32 +354,34 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
TypographyStyles.caption(
|
||||
'10/10/2025',
|
||||
_dateFormat.format(timestamp),
|
||||
color: GrayColors.gray600,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
TypographyStyles.caption(
|
||||
'11:11',
|
||||
_timeFormat.format(timestamp),
|
||||
color: GrayColors.gray600,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.caption(
|
||||
location,
|
||||
color: GrayColors.gray800,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
TypographyStyles.small(
|
||||
desc,
|
||||
color: GrayColors.gray600,
|
||||
fontWeight: FontWeight.w400,
|
||||
)
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.caption(
|
||||
location,
|
||||
color: GrayColors.gray800,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
TypographyStyles.small(
|
||||
desc,
|
||||
color: GrayColors.gray600,
|
||||
fontWeight: FontWeight.w400,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -166,6 +166,7 @@ class AppRoutes {
|
|||
GetPage(
|
||||
name: Routes.PROCESSING,
|
||||
page: () => ProcessingPorterScreen(),
|
||||
binding: TransactionPorterBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.HISTORYPORTER,
|
||||
|
|
Loading…
Reference in New Issue