Feat: Update transaction

This commit is contained in:
orangdeso 2025-04-20 14:17:30 +07:00
parent 606492e8a0
commit e8763bb85f
3 changed files with 167 additions and 9 deletions

View File

@ -6,8 +6,11 @@ import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:e_porter/domain/models/transaction_model.dart';
import 'package:e_porter/domain/repositories/transaction_repository.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import '../../presentation/controllers/history_controller.dart';
class TransactionRepositoryImpl implements TransactionRepository {
final FirebaseFirestore _firestore;
final FirebaseStorage _storage;
@ -52,6 +55,7 @@ class TransactionRepositoryImpl implements TransactionRepository {
final transactionData = {
'id': transactionId,
'idBooking': idBooking,
'ticketId': ticketId,
'flightId': flightId,
'amount': amount,
@ -355,11 +359,12 @@ class TransactionRepositoryImpl implements TransactionRepository {
.orderBy('createdAt', descending: false)
.get();
bool anyTransactionCancelled = false;
for (var doc in pendingTransactionsSnapshot.docs) {
final data = doc.data();
final expiryTime = (data['expiryTime'] as Timestamp).toDate();
// Jika sudah melewati waktu pembayaran
if (expiryTime.isBefore(now)) {
final ticketId = data['ticketId'];
final transactionId = doc.id;
@ -367,25 +372,27 @@ class TransactionRepositoryImpl implements TransactionRepository {
final flightId = data['flightId'];
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({
'status': 'cancelled',
'updatedAt': now,
});
// 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,
});
anyTransactionCancelled = true;
log('Transaksi $transactionId dibatalkan karena kedaluwarsa');
}
}
if (anyTransactionCancelled) {
await notifyTransactionUpdate();
}
} catch (e) {
if (e.toString().contains('failed-precondition') || e.toString().contains('requires an index')) {
log('[TransactionRepository] Indeks belum tersedia. Menggunakan metode alternatif.');
@ -496,6 +503,8 @@ class TransactionRepositoryImpl implements TransactionRepository {
final ticketsSnapshot = await _firestore.collection('tickets').get();
int count = 0;
bool anyTransactionCancelled = false;
for (var ticketDoc in ticketsSnapshot.docs) {
final ticketId = ticketDoc.id;
@ -543,15 +552,42 @@ class TransactionRepositoryImpl implements TransactionRepository {
});
}
anyTransactionCancelled = true;
count++;
}
}
}
}
if (anyTransactionCancelled) {
await notifyTransactionUpdate();
}
log('[TransactionRepository] Selesai memeriksa transaksi (metode alternatif), $count transaksi dibatalkan');
} catch (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');
}
}
}

View File

@ -11,11 +11,13 @@ class TransactionModel {
final String? proofUrl;
final DateTime createdAt;
final DateTime expiryTime;
final int passenger;
final Map<String, dynamic> flightDetails;
final Map<String, dynamic> bandaraDetails;
final Map<String, dynamic>? porterServiceDetails;
final Map<String, dynamic> userDetails;
final int passenger;
final List<Map<String, dynamic>> passengerDetails;
final List<String> numberSeat;
TransactionModel({
required this.id,
@ -28,11 +30,13 @@ class TransactionModel {
this.proofUrl,
required this.createdAt,
required this.expiryTime,
required this.passenger,
required this.flightDetails,
required this.bandaraDetails,
this.porterServiceDetails,
required this.userDetails,
required this.passenger,
required this.passengerDetails,
required this.numberSeat,
});
factory TransactionModel.fromJson(Map<String, dynamic> json) {
@ -42,10 +46,31 @@ class TransactionModel {
} else if (value is DateTime) {
return value;
} 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(
id: json['id'] ?? '',
idBooking: json['idBooking'] ?? '',
@ -57,11 +82,13 @@ class TransactionModel {
proofUrl: json['proofUrl'],
createdAt: getDateTime(json['createdAt']),
expiryTime: getDateTime(json['expiryTime']),
passenger: json['passenger'] ?? 0,
flightDetails: json['flightDetails'] ?? {},
bandaraDetails: json['bandaraDetails'] ?? {},
porterServiceDetails: json['porterServiceDetails'],
userDetails: json['userDetails'] ?? {},
passenger: json['passenger'] ?? 0,
passengerDetails: convertToMapList(json['passengerDetails']),
numberSeat: convertToStringList(json['numberSeat']),
);
}
@ -77,11 +104,13 @@ class TransactionModel {
'proofUrl': proofUrl,
'createdAt': createdAt,
'expiryTime': expiryTime,
'passenger': passenger,
'flightDetails': flightDetails,
'bandaraDetails': bandaraDetails,
'porterServiceDetails': porterServiceDetails,
'userDetails': userDetails,
'passenger': passenger,
'passengerDetails': passengerDetails,
'numberSeat': numberSeat,
};
}
}

View File

@ -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,
),
],
),
);
}
}