Feat: fix bug reassignments
This commit is contained in:
parent
ec3435850b
commit
56b629b1d7
|
@ -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",
|
||||||
|
|
|
@ -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: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();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
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;
|
isLoading.value = false;
|
||||||
}
|
},
|
||||||
}, onError: (e) {
|
onError: (e) {
|
||||||
log('Error streaming transaksi: $e');
|
log('[TransactionPorterController] Error streaming transaksi: $e');
|
||||||
|
|
||||||
// Jika terjadi error, coba dari userId sebagai fallback
|
|
||||||
if (userId != porterId) {
|
|
||||||
log('Error dengan porterId, mencoba dari userId: $userId');
|
|
||||||
loadTransactionsFromUserId(userId);
|
|
||||||
} else {
|
|
||||||
error.value = 'Gagal memuat transaksi: $e';
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,8 +52,32 @@ class _DetailHistoryPorterScreenState extends State<DetailHistoryPorterScreen> {
|
||||||
|
|
||||||
Future<void> _fetchTransactioPorterById() async {
|
Future<void> _fetchTransactioPorterById() async {
|
||||||
try {
|
try {
|
||||||
await _porterController.getTransactionById(porterTransactionId);
|
final args = Get.arguments as Map<String, dynamic>;
|
||||||
log('[Detail History Porter] Transaction fetched: ${_porterController.currentTransaction.value}');
|
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) {
|
} 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) {
|
||||||
await _historyController.getTransactionFromFirestore(
|
if (porterTransaction.ticketId.isNotEmpty && porterTransaction.transactionId.isNotEmpty) {
|
||||||
porterTransaction.ticketId, porterTransaction.transactionId);
|
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) {
|
} 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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
if (statusFilter == 'rejected') {
|
||||||
log('Semua transaksi: ${allTransactions.length}');
|
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 =
|
||||||
final filteredTransactions = allTransactions.where((tx) {
|
filterTransactions(allTransactions, statusFilter, _porterController.currentPorterId.value);
|
||||||
// Cek apakah transaksi memiliki rejectionInfo
|
|
||||||
final hasRejectionInfo = tx.rejectionInfo != null;
|
|
||||||
|
|
||||||
// Transaksi yang memiliki rejectionInfo harus masuk ke tab "rejected" saja
|
|
||||||
if (hasRejectionInfo && statusFilter != 'rejected') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaksi yang tidak memiliki rejectionInfo tapi statusnya "rejected"
|
|
||||||
// juga harus masuk ke tab "rejected" saja
|
|
||||||
if (!hasRejectionInfo && tx.normalizedStatus == 'rejected' && statusFilter != 'rejected') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untuk tab "rejected", tampilkan semua transaksi yang ditolak
|
|
||||||
if (statusFilter == 'rejected') {
|
|
||||||
return hasRejectionInfo || tx.normalizedStatus == 'rejected';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untuk tab lainnya, gunakan status normal
|
|
||||||
return tx.normalizedStatus == statusFilter;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}');
|
log('Transaksi dengan status $statusFilter: ${filteredTransactions.length}');
|
||||||
|
|
||||||
// Jika tidak ada transaksi, tampilkan pesan kosong
|
|
||||||
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,38 +227,64 @@ 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(
|
||||||
child: Column(
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Center(
|
||||||
children: [
|
child: Column(
|
||||||
TypographyStyles.body(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
'Tidak ada transaksi ${statusText.capitalizeFirst}',
|
children: [
|
||||||
color: GrayColors.gray600,
|
Icon(
|
||||||
),
|
_getStatusIcon(statusFilter),
|
||||||
SizedBox(height: 16.h),
|
size: 48.h,
|
||||||
ElevatedButton.icon(
|
color: GrayColors.gray400,
|
||||||
onPressed: () => _porterController.refreshTransactions(),
|
|
||||||
icon: Icon(Icons.refresh, size: 16.h),
|
|
||||||
label: const Text('Refresh'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: PrimaryColors.primary800,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
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) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,58 +86,80 @@ class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(child: Obx(
|
||||||
child: Padding(
|
() {
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
final transaction = _porterController.currentTransaction.value;
|
||||||
child: SingleChildScrollView(
|
final isLoading = _porterController.isLoading.value;
|
||||||
child: Column(
|
final error = _porterController.error.value;
|
||||||
children: [
|
|
||||||
TypographyStyles.h1('Ilustrasi'),
|
if (isLoading) {
|
||||||
SizedBox(height: 32.h),
|
return const Center(child: CircularProgressIndicator());
|
||||||
CustomeShadowCotainner(
|
}
|
||||||
child: Column(
|
if (error.isNotEmpty) {
|
||||||
children: [
|
return Center(
|
||||||
TypographyStyles.h6('Tunggu Portermu', color: GrayColors.gray800),
|
child: Column(
|
||||||
SizedBox(height: 4.h),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Icon(Icons.error_outline, size: 48.h, color: Colors.red),
|
||||||
children: [
|
SizedBox(height: 16.h),
|
||||||
TypographyStyles.caption(
|
TypographyStyles.body(
|
||||||
'Tunngu dari pihak Porter merespon',
|
'Terjadi Kesalahan',
|
||||||
color: GrayColors.gray500,
|
color: Colors.red,
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
Icon(Icons.timelapse_outlined)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 8.h),
|
||||||
SizedBox(height: 20.h),
|
Padding(
|
||||||
CustomeShadowCotainner(
|
padding: EdgeInsets.symmetric(horizontal: 32.w),
|
||||||
child: Column(
|
child: TypographyStyles.caption(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
error,
|
||||||
children: [
|
color: GrayColors.gray600,
|
||||||
TypographyStyles.body('Ahmad Choirul Umam', color: GrayColors.gray800),
|
textAlign: TextAlign.center,
|
||||||
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: 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(
|
bottomNavigationBar: CustomeShadowCotainner(
|
||||||
child: ButtonFill(
|
child: ButtonFill(
|
||||||
text: 'Kembali ke menu',
|
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(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -107,32 +354,34 @@ 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(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
TypographyStyles.caption(
|
children: [
|
||||||
location,
|
TypographyStyles.caption(
|
||||||
color: GrayColors.gray800,
|
location,
|
||||||
fontWeight: FontWeight.w600,
|
color: GrayColors.gray800,
|
||||||
),
|
fontWeight: FontWeight.w600,
|
||||||
TypographyStyles.small(
|
),
|
||||||
desc,
|
TypographyStyles.small(
|
||||||
color: GrayColors.gray600,
|
desc,
|
||||||
fontWeight: FontWeight.w400,
|
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(
|
GetPage(
|
||||||
name: Routes.PROCESSING,
|
name: Routes.PROCESSING,
|
||||||
page: () => ProcessingPorterScreen(),
|
page: () => ProcessingPorterScreen(),
|
||||||
|
binding: TransactionPorterBinding(),
|
||||||
),
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.HISTORYPORTER,
|
name: Routes.HISTORYPORTER,
|
||||||
|
|
Loading…
Reference in New Issue