Feat: Update transaction
This commit is contained in:
parent
606492e8a0
commit
e8763bb85f
|
@ -6,8 +6,11 @@ import 'package:firebase_database/firebase_database.dart';
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
import 'package:e_porter/domain/models/transaction_model.dart';
|
import 'package:e_porter/domain/models/transaction_model.dart';
|
||||||
import 'package:e_porter/domain/repositories/transaction_repository.dart';
|
import 'package:e_porter/domain/repositories/transaction_repository.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import '../../presentation/controllers/history_controller.dart';
|
||||||
|
|
||||||
class TransactionRepositoryImpl implements TransactionRepository {
|
class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
final FirebaseFirestore _firestore;
|
final FirebaseFirestore _firestore;
|
||||||
final FirebaseStorage _storage;
|
final FirebaseStorage _storage;
|
||||||
|
@ -52,6 +55,7 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
|
|
||||||
final transactionData = {
|
final transactionData = {
|
||||||
'id': transactionId,
|
'id': transactionId,
|
||||||
|
'idBooking': idBooking,
|
||||||
'ticketId': ticketId,
|
'ticketId': ticketId,
|
||||||
'flightId': flightId,
|
'flightId': flightId,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
|
@ -355,11 +359,12 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
.orderBy('createdAt', descending: false)
|
.orderBy('createdAt', descending: false)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
bool anyTransactionCancelled = false;
|
||||||
|
|
||||||
for (var doc in pendingTransactionsSnapshot.docs) {
|
for (var doc in pendingTransactionsSnapshot.docs) {
|
||||||
final data = doc.data();
|
final data = doc.data();
|
||||||
final expiryTime = (data['expiryTime'] as Timestamp).toDate();
|
final expiryTime = (data['expiryTime'] as Timestamp).toDate();
|
||||||
|
|
||||||
// Jika sudah melewati waktu pembayaran
|
|
||||||
if (expiryTime.isBefore(now)) {
|
if (expiryTime.isBefore(now)) {
|
||||||
final ticketId = data['ticketId'];
|
final ticketId = data['ticketId'];
|
||||||
final transactionId = doc.id;
|
final transactionId = doc.id;
|
||||||
|
@ -367,25 +372,27 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
final flightId = data['flightId'];
|
final flightId = data['flightId'];
|
||||||
final numberSeat = (data['numberSeat'] as List).map((e) => e.toString()).toList();
|
final numberSeat = (data['numberSeat'] as List).map((e) => e.toString()).toList();
|
||||||
|
|
||||||
// Update status di Firestore
|
|
||||||
await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({
|
await _firestore.collection('tickets').doc(ticketId).collection('payments').doc(transactionId).update({
|
||||||
'status': 'cancelled',
|
'status': 'cancelled',
|
||||||
'updatedAt': now,
|
'updatedAt': now,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset status kursi
|
|
||||||
await _resetSeatStatus(ticketId, flightId, numberSeat);
|
await _resetSeatStatus(ticketId, flightId, numberSeat);
|
||||||
|
|
||||||
// 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': 'cancelled',
|
'status': 'cancelled',
|
||||||
'updatedAt': now.millisecondsSinceEpoch,
|
'updatedAt': now.millisecondsSinceEpoch,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
anyTransactionCancelled = true;
|
||||||
log('Transaksi $transactionId dibatalkan karena kedaluwarsa');
|
log('Transaksi $transactionId dibatalkan karena kedaluwarsa');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (anyTransactionCancelled) {
|
||||||
|
await notifyTransactionUpdate();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.toString().contains('failed-precondition') || e.toString().contains('requires an index')) {
|
if (e.toString().contains('failed-precondition') || e.toString().contains('requires an index')) {
|
||||||
log('[TransactionRepository] Indeks belum tersedia. Menggunakan metode alternatif.');
|
log('[TransactionRepository] Indeks belum tersedia. Menggunakan metode alternatif.');
|
||||||
|
@ -496,6 +503,8 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
final ticketsSnapshot = await _firestore.collection('tickets').get();
|
final ticketsSnapshot = await _firestore.collection('tickets').get();
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
bool anyTransactionCancelled = false;
|
||||||
|
|
||||||
for (var ticketDoc in ticketsSnapshot.docs) {
|
for (var ticketDoc in ticketsSnapshot.docs) {
|
||||||
final ticketId = ticketDoc.id;
|
final ticketId = ticketDoc.id;
|
||||||
|
|
||||||
|
@ -543,15 +552,42 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anyTransactionCancelled = true;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (anyTransactionCancelled) {
|
||||||
|
await notifyTransactionUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
log('[TransactionRepository] Selesai memeriksa transaksi (metode alternatif), $count transaksi dibatalkan');
|
log('[TransactionRepository] Selesai memeriksa transaksi (metode alternatif), $count transaksi dibatalkan');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('[TransactionRepository] Error pada metode alternatif: $e');
|
log('[TransactionRepository] Error pada metode alternatif: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> notifyTransactionUpdate() async {
|
||||||
|
try {
|
||||||
|
final historyController = Get.find<HistoryController>();
|
||||||
|
final userId = historyController.getUserId();
|
||||||
|
|
||||||
|
if (userId.isNotEmpty) {
|
||||||
|
// Jeda sedikit untuk memastikan database sudah diupdate
|
||||||
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
await historyController.refreshTransactions(userId);
|
||||||
|
|
||||||
|
// Pastikan UI menampilkan pembaruan
|
||||||
|
historyController.isCheckingExpiry.value = true;
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
historyController.isCheckingExpiry.value = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('[TransactionRepository] HistoryController tidak tersedia: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,13 @@ class TransactionModel {
|
||||||
final String? proofUrl;
|
final String? proofUrl;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime expiryTime;
|
final DateTime expiryTime;
|
||||||
|
final int passenger;
|
||||||
final Map<String, dynamic> flightDetails;
|
final Map<String, dynamic> flightDetails;
|
||||||
final Map<String, dynamic> bandaraDetails;
|
final Map<String, dynamic> bandaraDetails;
|
||||||
final Map<String, dynamic>? porterServiceDetails;
|
final Map<String, dynamic>? porterServiceDetails;
|
||||||
final Map<String, dynamic> userDetails;
|
final Map<String, dynamic> userDetails;
|
||||||
final int passenger;
|
final List<Map<String, dynamic>> passengerDetails;
|
||||||
|
final List<String> numberSeat;
|
||||||
|
|
||||||
TransactionModel({
|
TransactionModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -28,11 +30,13 @@ class TransactionModel {
|
||||||
this.proofUrl,
|
this.proofUrl,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.expiryTime,
|
required this.expiryTime,
|
||||||
|
required this.passenger,
|
||||||
required this.flightDetails,
|
required this.flightDetails,
|
||||||
required this.bandaraDetails,
|
required this.bandaraDetails,
|
||||||
this.porterServiceDetails,
|
this.porterServiceDetails,
|
||||||
required this.userDetails,
|
required this.userDetails,
|
||||||
required this.passenger,
|
required this.passengerDetails,
|
||||||
|
required this.numberSeat,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory TransactionModel.fromJson(Map<String, dynamic> json) {
|
factory TransactionModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -42,10 +46,31 @@ class TransactionModel {
|
||||||
} else if (value is DateTime) {
|
} else if (value is DateTime) {
|
||||||
return value;
|
return value;
|
||||||
} else {
|
} else {
|
||||||
return DateTime.now();
|
return DateTime.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> convertToMapList(dynamic list) {
|
||||||
|
if (list == null) return [];
|
||||||
|
if (list is List) {
|
||||||
|
return list.map((item) {
|
||||||
|
if (item is Map) {
|
||||||
|
return Map<String, dynamic>.from(item);
|
||||||
|
}
|
||||||
|
return <String, dynamic>{};
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> convertToStringList(dynamic list) {
|
||||||
|
if (list == null) return [];
|
||||||
|
if (list is List) {
|
||||||
|
return list.map((item) => item.toString()).toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return TransactionModel(
|
return TransactionModel(
|
||||||
id: json['id'] ?? '',
|
id: json['id'] ?? '',
|
||||||
idBooking: json['idBooking'] ?? '',
|
idBooking: json['idBooking'] ?? '',
|
||||||
|
@ -57,11 +82,13 @@ class TransactionModel {
|
||||||
proofUrl: json['proofUrl'],
|
proofUrl: json['proofUrl'],
|
||||||
createdAt: getDateTime(json['createdAt']),
|
createdAt: getDateTime(json['createdAt']),
|
||||||
expiryTime: getDateTime(json['expiryTime']),
|
expiryTime: getDateTime(json['expiryTime']),
|
||||||
|
passenger: json['passenger'] ?? 0,
|
||||||
flightDetails: json['flightDetails'] ?? {},
|
flightDetails: json['flightDetails'] ?? {},
|
||||||
bandaraDetails: json['bandaraDetails'] ?? {},
|
bandaraDetails: json['bandaraDetails'] ?? {},
|
||||||
porterServiceDetails: json['porterServiceDetails'],
|
porterServiceDetails: json['porterServiceDetails'],
|
||||||
userDetails: json['userDetails'] ?? {},
|
userDetails: json['userDetails'] ?? {},
|
||||||
passenger: json['passenger'] ?? 0,
|
passengerDetails: convertToMapList(json['passengerDetails']),
|
||||||
|
numberSeat: convertToStringList(json['numberSeat']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,11 +104,13 @@ class TransactionModel {
|
||||||
'proofUrl': proofUrl,
|
'proofUrl': proofUrl,
|
||||||
'createdAt': createdAt,
|
'createdAt': createdAt,
|
||||||
'expiryTime': expiryTime,
|
'expiryTime': expiryTime,
|
||||||
|
'passenger': passenger,
|
||||||
'flightDetails': flightDetails,
|
'flightDetails': flightDetails,
|
||||||
'bandaraDetails': bandaraDetails,
|
'bandaraDetails': bandaraDetails,
|
||||||
'porterServiceDetails': porterServiceDetails,
|
'porterServiceDetails': porterServiceDetails,
|
||||||
'userDetails': userDetails,
|
'userDetails': userDetails,
|
||||||
'passenger': passenger,
|
'passengerDetails': passengerDetails,
|
||||||
|
'numberSeat': numberSeat,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:e_porter/_core/constants/colors.dart';
|
||||||
|
import 'package:e_porter/_core/constants/typography.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
||||||
|
class PaymentCountdownTimer extends StatefulWidget {
|
||||||
|
final DateTime expiryTime;
|
||||||
|
|
||||||
|
const PaymentCountdownTimer({
|
||||||
|
Key? key,
|
||||||
|
required this.expiryTime,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PaymentCountdownTimer> createState() => _PaymentCountdownTimerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentCountdownTimerState extends State<PaymentCountdownTimer> {
|
||||||
|
late Timer _timer;
|
||||||
|
late Duration _remainingTime;
|
||||||
|
bool _isExpired = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_calculateRemainingTime();
|
||||||
|
_startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateRemainingTime() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (widget.expiryTime.isAfter(now)) {
|
||||||
|
_remainingTime = widget.expiryTime.difference(now);
|
||||||
|
} else {
|
||||||
|
_remainingTime = Duration.zero;
|
||||||
|
_isExpired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startTimer() {
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_calculateRemainingTime();
|
||||||
|
if (_remainingTime.inSeconds <= 0) {
|
||||||
|
_isExpired = true;
|
||||||
|
_timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration duration) {
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||||
|
final hours = twoDigits(duration.inHours);
|
||||||
|
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||||
|
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||||
|
return '$hours:$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RedColors.red200,
|
||||||
|
borderRadius: BorderRadius.circular(10.r),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.caption(
|
||||||
|
"Batas Pembayaran",
|
||||||
|
color: RedColors.red600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
TypographyStyles.caption(
|
||||||
|
_isExpired ? "Kedaluwarsa" : _formatDuration(_remainingTime),
|
||||||
|
color: RedColors.red600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue