Feat: done logic transaction with upload file

This commit is contained in:
orangdeso 2025-04-09 23:23:21 +07:00
parent ea3e1080e0
commit 7b40cbace4
12 changed files with 444 additions and 172 deletions

View File

@ -632,7 +632,7 @@
"languageVersion": "3.4" "languageVersion": "3.4"
} }
], ],
"generated": "2025-04-01T21:50:03.833642Z", "generated": "2025-04-08T08:36:36.076309Z",
"generator": "pub", "generator": "pub",
"generatorVersion": "3.5.0", "generatorVersion": "3.5.0",
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0", "flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",

View File

@ -73,54 +73,43 @@ class TransactionRepositoryImpl implements TransactionRepository {
if (seatData.exists) { if (seatData.exists) {
Map<String, dynamic>? data = seatData.data() as Map<String, dynamic>?; Map<String, dynamic>? data = seatData.data() as Map<String, dynamic>?;
// Fungsi untuk mendapatkan kelas seat dari nomor kursi (misalnya "A2" -> "a")
String getSeatClass(String seatNumber) { String getSeatClass(String seatNumber) {
// Ambil huruf pertama dan ubah ke lowercase
if (seatNumber.isNotEmpty) { if (seatNumber.isNotEmpty) {
return seatNumber[0].toLowerCase(); return seatNumber[0].toLowerCase();
} }
return "a"; // Default ke "a" jika format tidak sesuai return "a";
} }
// Fungsi untuk mendapatkan indeks seat dari nomor kursi (misalnya "A2" -> 2)
int getSeatIndex(String seatNumber) { int getSeatIndex(String seatNumber) {
if (seatNumber.length > 1) { if (seatNumber.length > 1) {
try { try {
return int.parse(seatNumber.substring(1)) - 1; // Konversi ke berbasis-0 return int.parse(seatNumber.substring(1)) - 1;
} catch (e) { } catch (e) {
log("Error parsing seat index: $e"); log("Error parsing seat index: $e");
} }
} }
return 0; // Default ke 0 jika format tidak sesuai return 0;
} }
// Perbarui status kursi untuk setiap seat yang dipilih
for (String seatNumber in numberSeat) { for (String seatNumber in numberSeat) {
String seatClass = getSeatClass(seatNumber); String seatClass = getSeatClass(seatNumber);
int seatIndex = getSeatIndex(seatNumber); int seatIndex = getSeatIndex(seatNumber);
log("Processing seat: $seatNumber, class: $seatClass, index: $seatIndex"); log("Processing seat: $seatNumber, class: $seatClass, index: $seatIndex");
// Pastikan struktur data seat tersedia
if (data != null && data.containsKey('seat')) { if (data != null && data.containsKey('seat')) {
Map<String, dynamic> seatData = Map<String, dynamic>.from(data['seat']); Map<String, dynamic> seatData = Map<String, dynamic>.from(data['seat']);
// Pastikan kelas kursi (a-f) tersedia
if (seatData.containsKey(seatClass)) { if (seatData.containsKey(seatClass)) {
Map<String, dynamic> classSeatData = Map<String, dynamic>.from(seatData[seatClass]); Map<String, dynamic> classSeatData = Map<String, dynamic>.from(seatData[seatClass]);
// Pastikan array isTaken tersedia
if (classSeatData.containsKey('isTaken')) { if (classSeatData.containsKey('isTaken')) {
List<dynamic> isTaken = List<dynamic>.from(classSeatData['isTaken'] ?? []); List<dynamic> isTaken = List<dynamic>.from(classSeatData['isTaken'] ?? []);
// Perbarui array isTaken
while (isTaken.length <= seatIndex) { while (isTaken.length <= seatIndex) {
isTaken.add(false); // Tambahkan seat yang belum ada dengan false isTaken.add(false);
} }
isTaken[seatIndex] = true; // Set kursi yang dipilih sebagai 'taken' isTaken[seatIndex] = true;
// Update Firestore untuk kelas kursi tertentu
await _firestore await _firestore
.collection('tickets') .collection('tickets')
.doc(ticketId) .doc(ticketId)
@ -130,7 +119,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
log("Successfully updated seat $seatNumber in class $seatClass at index $seatIndex"); log("Successfully updated seat $seatNumber in class $seatClass at index $seatIndex");
} else { } else {
// Jika 'isTaken' tidak ada, buat array baru
List<dynamic> isTaken = List.filled(seatIndex + 1, false); List<dynamic> isTaken = List.filled(seatIndex + 1, false);
isTaken[seatIndex] = true; isTaken[seatIndex] = true;
@ -144,7 +132,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
log("Created new isTaken array for seat $seatNumber in class $seatClass"); log("Created new isTaken array for seat $seatNumber in class $seatClass");
} }
} else { } else {
// Jika kelas kursi tidak ada, buat struktur baru
List<dynamic> isTaken = List.filled(seatIndex + 1, false); List<dynamic> isTaken = List.filled(seatIndex + 1, false);
isTaken[seatIndex] = true; isTaken[seatIndex] = true;
@ -155,7 +142,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
log("Created new seat class $seatClass for seat $seatNumber"); log("Created new seat class $seatClass for seat $seatNumber");
} }
} else { } else {
// Jika struktur 'seat' tidak ada, buat struktur lengkap
List<dynamic> isTaken = List.filled(seatIndex + 1, false); List<dynamic> isTaken = List.filled(seatIndex + 1, false);
isTaken[seatIndex] = true; isTaken[seatIndex] = true;
@ -221,13 +207,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
'updatedAt': now, 'updatedAt': now,
}); });
// Update status dokumen tiket utama
await _firestore.collection('tickets').doc(ticketId).update({
'status': status == 'paid' ? 'awaiting_verification' : status,
'lastUpdated': now,
});
// Update status di Realtime Database
final databaseRef = FirebaseDatabase.instance.ref(); final databaseRef = FirebaseDatabase.instance.ref();
await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({ await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({
'status': status, 'status': status,
@ -239,36 +218,38 @@ class TransactionRepositoryImpl implements TransactionRepository {
} }
@override @override
Future<void> uploadPaymentProof( Future<void> uploadPaymentProof({
{required String ticketId, required String transactionId, required File proofImage}) async { required String ticketId,
required String transactionId,
required File proofImage,
required String userId,
}) async {
try { try {
final fileName = final fileName =
'payment_proof_${transactionId}_${DateTime.now().millisecondsSinceEpoch}${path.extension(proofImage.path)}'; 'payment_proof_${transactionId}_${DateTime.now().millisecondsSinceEpoch}${path.extension(proofImage.path)}';
final storageRef = _storage.ref().child('payment/$fileName'); final storageRef = _storage.ref().child('payment/$fileName');
// Upload file ke Firebase Storage // Tambahkan listener untuk progress upload
final uploadTask = await storageRef.putFile(proofImage); final uploadTask = storageRef.putFile(proofImage);
final downloadUrl = await uploadTask.ref.getDownloadURL();
// Update di Firestore dengan URL bukti pembayaran // Upload file ke Firebase Storage
final snapshot = await uploadTask;
final downloadUrl = await snapshot.ref.getDownloadURL();
final now = DateTime.now();
// Update Firestore
await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({ await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({
'proofUrl': downloadUrl, 'proofUrl': downloadUrl,
'status': 'paid', 'status': 'active',
'paidAt': DateTime.now(), 'paidAt': now,
}); });
// Update status dokumen tiket utama // Update Realtime Database
await _firestore.collection('tickets').doc(ticketId).update({
'status': 'awaiting_verification',
'lastUpdated': DateTime.now(),
});
// Update di Realtime Database
final databaseRef = FirebaseDatabase.instance.ref(); final databaseRef = FirebaseDatabase.instance.ref();
await databaseRef.child('transactions/$ticketId/payment').update({ await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({
'proofUrl': downloadUrl, 'proofUrl': downloadUrl,
'status': 'paid', 'status': 'active',
'paidAt': DateTime.now().millisecondsSinceEpoch, 'paidAt': now.millisecondsSinceEpoch,
}); });
} catch (e) { } catch (e) {
throw Exception('Failed to upload payment proof: $e'); throw Exception('Failed to upload payment proof: $e');
@ -278,12 +259,9 @@ class TransactionRepositoryImpl implements TransactionRepository {
@override @override
Future<List<TransactionModel>> getTransactionsByUserId(String userId) async { Future<List<TransactionModel>> getTransactionsByUserId(String userId) async {
try { try {
// Mencari tiket berdasarkan userId
final ticketQuerySnapshot = await _firestore.collection('tickets').where('userId', isEqualTo: userId).get(); final ticketQuerySnapshot = await _firestore.collection('tickets').where('userId', isEqualTo: userId).get();
final List<TransactionModel> transactions = []; final List<TransactionModel> transactions = [];
// Untuk setiap tiket, ambil data payment
for (var ticketDoc in ticketQuerySnapshot.docs) { for (var ticketDoc in ticketQuerySnapshot.docs) {
final ticketId = ticketDoc.id; final ticketId = ticketDoc.id;
final paymentsSnapshot = await _firestore.collection('tickets').doc(ticketId).collection('payments').get(); final paymentsSnapshot = await _firestore.collection('tickets').doc(ticketId).collection('payments').get();
@ -368,40 +346,194 @@ class TransactionRepositoryImpl implements TransactionRepository {
Future<void> checkAndCancelExpiredTransactions() async { Future<void> checkAndCancelExpiredTransactions() async {
try { try {
log('[TransactionRepository] Mulai memeriksa transaksi kedaluwarsa');
final now = DateTime.now(); final now = DateTime.now();
final pendingTransactionsSnapshot =
await _firestore.collectionGroup('payments').where('status', isEqualTo: 'pending').get();
for (var doc in pendingTransactionsSnapshot.docs) { final ticketsCheck = await _firestore.collection('tickets').limit(1).get();
final data = doc.data(); if (ticketsCheck.docs.isEmpty) {
final expiryTime = (data['expiryTime'] as Timestamp).toDate(); log('[TransactionRepository] Tidak ada tiket untuk diperiksa, melewati pengecekan');
return;
}
if (expiryTime.isBefore(now)) { try {
final ticketId = data['ticketId']; final pendingTransactionsSnapshot = await _firestore
final transactionId = doc.id; .collectionGroup('payments')
final userId = data['userDetails']['uid']; .where('status', isEqualTo: 'pending')
.orderBy('createdAt', descending: false)
.get();
await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({ for (var doc in pendingTransactionsSnapshot.docs) {
'status': 'cancelled', final data = doc.data();
'updatedAt': now, final expiryTime = (data['expiryTime'] as Timestamp).toDate();
});
await _firestore.collection('tickets').doc(ticketId).update({ // Jika sudah melewati waktu pembayaran
'status': 'cancelled', if (expiryTime.isBefore(now)) {
'lastUpdated': now, final ticketId = data['ticketId'];
}); final transactionId = doc.id;
final userId = data['userDetails']['uid'];
final flightId = data['flightId'];
final numberSeat = (data['numberSeat'] as List).map((e) => e.toString()).toList();
final databaseRef = FirebaseDatabase.instance.ref(); // Update status di Firestore
await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({ await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({
'status': 'cancelled', 'status': 'cancelled',
'updatedAt': now.millisecondsSinceEpoch, 'updatedAt': now,
}); });
log('Transaction $transactionId cancelled due to expiry'); // Reset status kursi
await _resetSeatStatus(ticketId, flightId, numberSeat);
// Update status di Realtime Database
final databaseRef = FirebaseDatabase.instance.ref();
await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({
'status': 'cancelled',
'updatedAt': now.millisecondsSinceEpoch,
});
log('Transaksi $transactionId dibatalkan karena kedaluwarsa');
}
}
} catch (e) {
if (e.toString().contains('failed-precondition') || e.toString().contains('requires an index')) {
log('[TransactionRepository] Indeks belum tersedia. Menggunakan metode alternatif.');
// Gunakan metode alternatif yang tidak memerlukan indeks khusus
await checkAndCancelExpiredTransactionsAlternative();
} else {
rethrow;
} }
} }
} catch (e) { } catch (e) {
log('Error checking expired transactions: $e'); log('[TransactionRepository] Error memeriksa transaksi kedaluwarsa: $e');
rethrow;
}
}
// Metode helper untuk mereset status kursi
Future<void> _resetSeatStatus(String ticketId, String flightId, List<String> numberSeat) async {
try {
DocumentSnapshot seatData =
await _firestore.collection('tickets').doc(ticketId).collection('flights').doc(flightId).get();
if (seatData.exists) {
Map<String, dynamic>? data = seatData.data() as Map<String, dynamic>?;
for (String seatNumber in numberSeat) {
String seatClass = getSeatClass(seatNumber);
int seatIndex = getSeatIndex(seatNumber);
if (data != null && data.containsKey('seat')) {
Map<String, dynamic> seatData = Map<String, dynamic>.from(data['seat']);
if (seatData.containsKey(seatClass)) {
Map<String, dynamic> classSeatData = Map<String, dynamic>.from(seatData[seatClass]);
if (classSeatData.containsKey('isTaken')) {
List<dynamic> isTaken = List<dynamic>.from(classSeatData['isTaken'] ?? []);
if (seatIndex < isTaken.length) {
isTaken[seatIndex] = false; // Reset status kursi
await _firestore
.collection('tickets')
.doc(ticketId)
.collection('flights')
.doc(flightId)
.update({'seat.$seatClass.isTaken': isTaken});
log("Reset kursi $seatNumber di kelas $seatClass pada indeks $seatIndex");
}
}
}
}
}
}
} catch (e) {
log("Error mereset status kursi: $e");
}
}
String getSeatClass(String seatNumber) {
if (seatNumber.isNotEmpty) {
return seatNumber[0].toLowerCase();
}
return "a";
}
int getSeatIndex(String seatNumber) {
if (seatNumber.length > 1) {
try {
return int.parse(seatNumber.substring(1)) - 1;
} catch (e) {
log("Error parsing seat index: $e");
}
}
return 0;
}
Future<void> checkAndCancelExpiredTransactionsAlternative() async {
try {
final now = DateTime.now();
log('[TransactionRepository] Menggunakan metode alternatif untuk memeriksa transaksi');
final ticketsSnapshot = await _firestore.collection('tickets').get();
int count = 0;
for (var ticketDoc in ticketsSnapshot.docs) {
final ticketId = ticketDoc.id;
final paymentsSnapshot = await _firestore
.collection('tickets')
.doc(ticketId)
.collection('payments')
.where('status', isEqualTo: 'pending')
.get();
for (var doc in paymentsSnapshot.docs) {
final data = doc.data();
if (data.containsKey('expiryTime')) {
final expiryTime = (data['expiryTime'] as Timestamp).toDate();
if (expiryTime.isBefore(now)) {
final transactionId = doc.id;
final userId = data['userDetails']?['uid'];
final flightId = data['flightId'];
final List<String> numberSeat = [];
if (data.containsKey('numberSeat')) {
numberSeat.addAll((data['numberSeat'] as List).map((e) => e.toString()));
}
log('[TransactionRepository] Memproses transaksi kedaluwarsa alternatif: $transactionId');
// Update status transaksi
await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({
'status': 'cancelled',
'updatedAt': now,
});
// Reset seat status
if (numberSeat.isNotEmpty) {
await _resetSeatStatus(ticketId, flightId, numberSeat);
}
// Update Realtime Database jika userId tersedia
if (userId != null) {
final databaseRef = FirebaseDatabase.instance.ref();
await databaseRef.child('transactions/$userId/$ticketId/$transactionId/payment').update({
'status': 'cancelled',
'updatedAt': now.millisecondsSinceEpoch,
});
}
count++;
}
}
}
}
log('[TransactionRepository] Selesai memeriksa transaksi (metode alternatif), $count transaksi dibatalkan');
} catch (e) {
log('[TransactionRepository] Error pada metode alternatif: $e');
} }
} }
} }

View File

@ -0,0 +1,15 @@
import 'package:get/get.dart';
import 'package:e_porter/data/repositories/transaction_repository_impl.dart';
import 'package:e_porter/_core/service/transaction_expiry_service.dart';
class AppBinding extends Bindings {
@override
void dependencies() {
// Inisialisasi TransactionExpiryService
Get.put<TransactionRepositoryImpl>(TransactionRepositoryImpl(), permanent: true);
// Inisialisasi dan mulai service
final repository = Get.find<TransactionRepositoryImpl>();
TransactionExpiryService().initialize(repository);
}
}

View File

@ -4,22 +4,29 @@ import 'package:e_porter/data/repositories/transaction_repository_impl.dart';
import 'package:e_porter/domain/usecases/transaction_usecase.dart'; import 'package:e_porter/domain/usecases/transaction_usecase.dart';
import 'package:e_porter/presentation/controllers/transaction_controller.dart'; import 'package:e_porter/presentation/controllers/transaction_controller.dart';
import '../../_core/service/transaction_expiry_service.dart';
class TransactionBinding extends Bindings { class TransactionBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Repository
Get.lazyPut<TransactionRepository>( Get.lazyPut<TransactionRepository>(
() => TransactionRepositoryImpl(), () => TransactionRepositoryImpl(),
fenix: true
);
Get.lazyPut<TransactionRepositoryImpl>(
() => TransactionRepositoryImpl(),
fenix: true,
); );
// UseCase
Get.lazyPut<TransactionUseCase>( Get.lazyPut<TransactionUseCase>(
() => TransactionUseCase(Get.find<TransactionRepository>()), () => TransactionUseCase(Get.find<TransactionRepository>()),
); );
// Controller
Get.lazyPut<TransactionController>( Get.lazyPut<TransactionController>(
() => TransactionController(Get.find<TransactionUseCase>()), () => TransactionController(Get.find<TransactionUseCase>()),
); );
TransactionExpiryService().initialize(Get.find<TransactionRepositoryImpl>());
} }
} }

View File

@ -28,6 +28,7 @@ abstract class TransactionRepository {
required String ticketId, required String ticketId,
required String transactionId, required String transactionId,
required File proofImage, required File proofImage,
required String userId,
}); });
Future<List<TransactionModel>> getTransactionsByUserId(String userId); Future<List<TransactionModel>> getTransactionsByUserId(String userId);

View File

@ -55,11 +55,13 @@ class TransactionUseCase {
required String ticketId, required String ticketId,
required String transactionId, required String transactionId,
required File proofImage, required File proofImage,
required String userId,
}) { }) {
return _repository.uploadPaymentProof( return _repository.uploadPaymentProof(
ticketId: ticketId, ticketId: ticketId,
transactionId: transactionId, transactionId: transactionId,
proofImage: proofImage, proofImage: proofImage,
userId: userId,
); );
} }

View File

@ -1,19 +1,14 @@
import 'dart:developer'; import 'dart:developer';
import 'package:e_porter/data/repositories/transaction_repository_impl.dart'; import 'package:e_porter/domain/bindings/app_binding.dart';
import 'package:e_porter/presentation/screens/routes/app_rountes.dart'; import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.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 '_core/service/transaction_expiry_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); await Firebase.initializeApp();
final transactionRepo = Get.put(TransactionRepositoryImpl());
TransactionExpiryService().initialize(transactionRepo);
log("Firebase Initialized Successfully!"); log("Firebase Initialized Successfully!");
runApp(MyApp(initialRoute: Routes.SPLASH)); runApp(MyApp(initialRoute: Routes.SPLASH));
@ -32,6 +27,7 @@ class MyApp extends StatelessWidget {
return GetMaterialApp( return GetMaterialApp(
debugShowCheckedModeBanner: true, debugShowCheckedModeBanner: true,
initialRoute: initialRoute, initialRoute: initialRoute,
initialBinding: AppBinding(),
getPages: AppRoutes.routes, getPages: AppRoutes.routes,
); );
}, },

View File

@ -1,8 +1,10 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:e_porter/domain/models/transaction_model.dart'; import 'package:e_porter/domain/models/transaction_model.dart';
import 'package:e_porter/domain/usecases/transaction_usecase.dart'; import 'package:e_porter/domain/usecases/transaction_usecase.dart';
import 'package:e_porter/_core/service/logger_service.dart';
import '../../data/repositories/transaction_repository_impl.dart';
class TransactionController extends GetxController { class TransactionController extends GetxController {
final TransactionUseCase _transactionUseCase; final TransactionUseCase _transactionUseCase;
@ -12,6 +14,8 @@ class TransactionController extends GetxController {
final Rx<TransactionModel?> currentTransaction = Rx<TransactionModel?>(null); final Rx<TransactionModel?> currentTransaction = Rx<TransactionModel?>(null);
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxString error = ''.obs; final RxString error = ''.obs;
final RxBool isUploading = false.obs;
final RxDouble uploadProgress = 0.0.obs;
Future<String> createTransaction({ Future<String> createTransaction({
required String ticketId, required String ticketId,
@ -46,7 +50,6 @@ class TransactionController extends GetxController {
numberSeat: numberSeat, numberSeat: numberSeat,
); );
// Ambil data transaksi setelah berhasil dibuat
final transaction = await _transactionUseCase.getTransactionById( final transaction = await _transactionUseCase.getTransactionById(
ticketId: ticketId, ticketId: ticketId,
transactionId: transactionId, transactionId: transactionId,
@ -55,7 +58,7 @@ class TransactionController extends GetxController {
currentTransaction.value = transaction; currentTransaction.value = transaction;
return transactionId; return transactionId;
} catch (e) { } catch (e) {
logger.e('Gagal membuat transaksi: $e'); log('Gagal membuat transaksi: $e');
error.value = 'Gagal membuat transaksi: $e'; error.value = 'Gagal membuat transaksi: $e';
throw Exception('Gagal membuat transaksi: $e'); throw Exception('Gagal membuat transaksi: $e');
} finally { } finally {
@ -63,26 +66,45 @@ class TransactionController extends GetxController {
} }
} }
Future<void> uploadPaymentProof( Future<void> uploadPaymentProof({
{required String ticketId, required String transactionId, required File proofImage}) async { required String ticketId,
required String transactionId,
required File proofImage,
required String userId,
}) async {
try { try {
isLoading.value = true; isUploading.value = true;
error.value = ''; error.value = '';
uploadProgress.value = 0.0;
await _transactionUseCase.uploadPaymentProof( await _transactionUseCase.uploadPaymentProof(
ticketId: ticketId, transactionId: transactionId, proofImage: proofImage); ticketId: ticketId,
transactionId: transactionId,
// Refresh data transaksi setelah bukti pembayaran diunggah proofImage: proofImage,
final transaction = userId: userId,
await _transactionUseCase.getTransactionById(ticketId: ticketId, transactionId: transactionId); );
// Update status transaksi setelah upload berhasil
await _transactionUseCase.updateTransactionStatus(
ticketId: ticketId,
transactionId: transactionId,
status: 'active',
userId: userId,
);
// Refresh data transaksi
final transaction = await _transactionUseCase.getTransactionById(
ticketId: ticketId,
transactionId: transactionId,
);
currentTransaction.value = transaction; currentTransaction.value = transaction;
} catch (e) { } catch (e) {
logger.e('Gagal mengunggah bukti pembayaran: $e'); error.value = 'Gagal mengupload bukti pembayaran: $e';
error.value = 'Gagal mengunggah bukti pembayaran: $e'; throw Exception('Gagal mengupload bukti pembayaran: $e');
throw Exception('Gagal mengunggah bukti pembayaran: $e');
} finally { } finally {
isLoading.value = false; isUploading.value = false;
} }
} }
@ -94,7 +116,7 @@ class TransactionController extends GetxController {
final transactions = await _transactionUseCase.getTransactionsByUserId(userId); final transactions = await _transactionUseCase.getTransactionsByUserId(userId);
return transactions; return transactions;
} catch (e) { } catch (e) {
logger.e('Gagal mendapatkan daftar transaksi: $e'); log('Gagal mendapatkan daftar transaksi: $e');
error.value = 'Gagal mendapatkan daftar transaksi: $e'; error.value = 'Gagal mendapatkan daftar transaksi: $e';
throw Exception('Gagal mendapatkan daftar transaksi: $e'); throw Exception('Gagal mendapatkan daftar transaksi: $e');
} finally { } finally {
@ -112,7 +134,7 @@ class TransactionController extends GetxController {
currentTransaction.value = transaction; currentTransaction.value = transaction;
} catch (e) { } catch (e) {
logger.e('Gagal mendapatkan detail transaksi: $e'); log('Gagal mendapatkan detail transaksi: $e');
error.value = 'Gagal mendapatkan detail transaksi: $e'; error.value = 'Gagal mendapatkan detail transaksi: $e';
throw Exception('Gagal mendapatkan detail transaksi: $e'); throw Exception('Gagal mendapatkan detail transaksi: $e');
} finally { } finally {
@ -127,12 +149,12 @@ class TransactionController extends GetxController {
currentTransaction.value = transaction; currentTransaction.value = transaction;
}, },
onError: (e) { onError: (e) {
logger.e('Error mendengarkan perubahan transaksi: $e'); log('Error mendengarkan perubahan transaksi: $e');
error.value = 'Error mendengarkan perubahan transaksi: $e'; error.value = 'Error mendengarkan perubahan transaksi: $e';
}, },
); );
} catch (e) { } catch (e) {
logger.e('Gagal memantau transaksi: $e'); log('Gagal memantau transaksi: $e');
error.value = 'Gagal memantau transaksi: $e'; error.value = 'Gagal memantau transaksi: $e';
} }
} }
@ -162,11 +184,28 @@ class TransactionController extends GetxController {
currentTransaction.value = transaction; currentTransaction.value = transaction;
} catch (e) { } catch (e) {
logger.e('Gagal mengupdate status transaksi: $e'); log('Gagal mengupdate status transaksi: $e');
error.value = 'Gagal mengupdate status transaksi: $e'; error.value = 'Gagal mengupdate status transaksi: $e';
throw Exception('Gagal mengupdate status transaksi: $e'); throw Exception('Gagal mengupdate status transaksi: $e');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> checkExpiredTransactions() async {
try {
isLoading.value = true;
error.value = '';
final repository = Get.find<TransactionRepositoryImpl>();
await repository.checkAndCancelExpiredTransactions();
log('Berhasil memeriksa transaksi kedaluwarsa');
} catch (e) {
log('Gagal memeriksa transaksi kedaluwarsa: $e');
error.value = 'Gagal memeriksa transaksi kedaluwarsa: $e';
} finally {
isLoading.value = false;
}
}
} }

View File

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:e_porter/_core/component/appbar/appbar_component.dart'; import 'package:e_porter/_core/component/appbar/appbar_component.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/colors.dart'; import 'package:e_porter/_core/constants/colors.dart';
@ -125,7 +127,14 @@ class _PaymentScreenState extends State<PaymentScreen> {
text: 'Lanjutkan', text: 'Lanjutkan',
textColor: Colors.white, textColor: Colors.white,
onTap: () { onTap: () {
Get.toNamed(Routes.UPLOADFILE); final argument = {
'ticketId': ticketId,
'transactionId': Get.arguments['transactionId'], // Ambil dari argumen yang diteruskan
'flightData': flightData,
'totalAll': totalAll,
};
log('Transaction ID: $argument');
Get.toNamed(Routes.UPLOADFILE, arguments: argument);
}, },
), ),
), ),

View File

@ -65,6 +65,7 @@ class _TicketBookingStep4ScreenState extends State<TicketBookingStep4Screen> {
selectedPorterServices = args['selectedPorterServices'] ?? {}; selectedPorterServices = args['selectedPorterServices'] ?? {};
fetchDataFlight(); fetchDataFlight();
transactionController.checkExpiredTransactions();
} }
Future<void> fetchDataFlight() async { Future<void> fetchDataFlight() async {
@ -238,7 +239,7 @@ class _TicketBookingStep4ScreenState extends State<TicketBookingStep4Screen> {
// Persiapkan data expiry time // Persiapkan data expiry time
final DateTime currentTime = DateTime.now(); final DateTime currentTime = DateTime.now();
final DateTime expiryTime = currentTime.add(Duration(seconds: 5)); final DateTime expiryTime = currentTime.add(Duration(days: 1));
// Persiapkan data bandara // Persiapkan data bandara
final bandaraData = { final bandaraData = {

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:e_porter/_core/constants/colors.dart'; import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart'; import 'package:e_porter/_core/constants/typography.dart';
import 'package:e_porter/_core/service/permission_service.dart'; import 'package:e_porter/_core/service/permission_service.dart';
@ -11,7 +13,10 @@ import 'package:zoom_tap_animation/zoom_tap_animation.dart';
import '../../../../_core/component/appbar/appbar_component.dart'; import '../../../../_core/component/appbar/appbar_component.dart';
import '../../../../_core/component/button/button_fill.dart'; import '../../../../_core/component/button/button_fill.dart';
import '../../../../_core/component/card/custome_shadow_cotainner.dart'; import '../../../../_core/component/card/custome_shadow_cotainner.dart';
import '../../../../_core/service/preferences_service.dart';
import '../../../../domain/models/upload_file_model.dart'; import '../../../../domain/models/upload_file_model.dart';
import '../../../controllers/transaction_controller.dart';
import '../../routes/app_rountes.dart';
class UploadFileScreen extends StatefulWidget { class UploadFileScreen extends StatefulWidget {
const UploadFileScreen({super.key}); const UploadFileScreen({super.key});
@ -22,8 +27,100 @@ class UploadFileScreen extends StatefulWidget {
class _UploadFileScreenState extends State<UploadFileScreen> { class _UploadFileScreenState extends State<UploadFileScreen> {
final List<UploadFileModel> uploadedFiles = []; final List<UploadFileModel> uploadedFiles = [];
final TransactionController _transactionController = Get.find<TransactionController>();
bool isUploading = false; bool isUploading = false;
late String ticketId;
late String transactionId;
late String userId = '';
@override
void initState() {
super.initState();
final args = Get.arguments as Map<String, dynamic>;
ticketId = args['ticketId'] ?? '';
transactionId = args['transactionId'] ?? '';
_loadUserData();
}
Future<void> _loadUserData() async {
final userData = await PreferencesService.getUserData();
if (userData != null) {
setState(() {
userId = userData.uid;
});
}
}
void _uploadToServer() async {
try {
// Validasi data transaksi
if (ticketId.isEmpty || transactionId.isEmpty || userId.isEmpty) {
Get.snackbar(
'Error',
'Data transaksi tidak lengkap',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
if (uploadedFiles.isEmpty || uploadedFiles.first.status != FileUploadStatus.completed) {
Get.snackbar(
'Error',
'Silakan pilih dan selesaikan proses file terlebih dahulu',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
setState(() {
isUploading = true;
});
Get.snackbar(
'Info',
'Mengupload bukti pembayaran...',
backgroundColor: PrimaryColors.primary600,
colorText: Colors.white,
);
final fileToUpload = uploadedFiles.first;
final File proofImage = File(fileToUpload.filePath);
await _transactionController.uploadPaymentProof(
ticketId: ticketId,
transactionId: transactionId,
proofImage: proofImage,
userId: userId,
);
Get.snackbar(
'Sukses',
'Bukti pembayaran berhasil diupload',
backgroundColor: Colors.green,
colorText: Colors.white,
);
Get.offAllNamed(Routes.NAVBAR);
} catch (e) {
Get.snackbar(
'Error',
'Gagal mengupload bukti pembayaran: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) {
setState(() {
isUploading = false;
});
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -225,52 +322,51 @@ class _UploadFileScreenState extends State<UploadFileScreen> {
} }
Future<void> _pickFile() async { Future<void> _pickFile() async {
bool permissionGranted = await PermissionHelper.requestStoragePermission(); bool permissionGranted = await PermissionHelper.requestStoragePermission();
if (!permissionGranted) { if (!permissionGranted) {
Get.snackbar( Get.snackbar(
'Permission Denied', 'Permission Denied',
'Storage permission is required to select files.', 'Storage permission is required to select files.',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
return; return;
} }
try { try {
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'zip', 'mp4'], allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'],
);
if (result != null) {
final file = result.files.first;
final newFile = UploadFileModel(
fileName: file.name,
filePath: file.path!,
fileSize: file.size,
progress: 0,
remainingTime: '0',
status: FileUploadStatus.pending,
); );
setState(() { if (result != null) {
uploadedFiles.add(newFile); final file = result.files.first;
}); final newFile = UploadFileModel(
fileName: file.name,
filePath: file.path!,
fileSize: file.size,
progress: 0,
remainingTime: '0',
status: FileUploadStatus.pending,
);
// Simulasikan proses loading lokal setState(() {
_simulateLocalLoading(newFile); uploadedFiles.add(newFile);
});
// Simulasikan proses loading lokal
_simulateLocalLoading(newFile);
}
} catch (e) {
print('Error picking file: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat memilih file',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} }
} catch (e) {
print('Error picking file: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat memilih file',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} }
}
void _simulateLocalLoading(UploadFileModel file) { void _simulateLocalLoading(UploadFileModel file) {
setState(() { setState(() {
@ -349,31 +445,4 @@ class _UploadFileScreenState extends State<UploadFileScreen> {
_uploadToServer(); _uploadToServer();
} }
} }
void _uploadToServer() {
// Simulate uploading to server
Get.snackbar(
'Info',
'Mengupload bukti pembayaran...',
backgroundColor: PrimaryColors.primary600,
colorText: Colors.white,
);
// Here you would actually send the files to your server
// For now, let's just simulate a successful upload
Future.delayed(Duration(seconds: 2), () {
Get.snackbar(
'Sukses',
'Bukti pembayaran berhasil diupload',
backgroundColor: Colors.green,
colorText: Colors.white,
);
// Navigate back or to a success screen
Future.delayed(Duration(seconds: 1), () {
// Get.offNamed(Routes.SUCCESS_SCREEN);
Get.back();
});
});
}
} }

View File

@ -59,6 +59,7 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
permission_handler: ^11.4.0 permission_handler: ^11.4.0
device_info_plus: ^11.3.0 device_info_plus: ^11.3.0
# workmanager: ^0.5.2
# pin_code_fields: ^8.0.1 # pin_code_fields: ^8.0.1
# dio: ^5.8.0+1 # dio: ^5.8.0+1