Files
bumrent_app/lib/app/modules/warga/controllers/pembayaran_sewa_controller.dart
Andreas Malvino 8284c93aa5 fitur petugas
2025-06-22 09:25:58 +07:00

1254 lines
40 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../../data/providers/aset_provider.dart';
import '../../../services/navigation_service.dart';
// Custom class for web platform to handle image URLs
class WebImageFile {
final String imageUrl;
String id = ''; // Database ID for the foto_pembayaran record (UUID string)
WebImageFile(this.imageUrl);
}
class PembayaranSewaController extends GetxController
with GetSingleTickerProviderStateMixin {
// Dependencies
final NavigationService navigationService = Get.find<NavigationService>();
final AsetProvider asetProvider = Get.find<AsetProvider>();
// Direct access to Supabase client for storage operations
final SupabaseClient client = Supabase.instance.client;
// Tab controller
late TabController tabController;
// Order details
final orderId = ''.obs;
final orderDetails = Rx<Map<String, dynamic>>({});
// Sewa Aset details with related aset info
final sewaAsetDetails = Rx<Map<String, dynamic>>({});
// Tagihan Sewa details
final tagihanSewa = Rx<Map<String, dynamic>>({});
// Payment details
final paymentMethod = ''.obs;
final selectedPaymentType = ''.obs;
final isLoading = false.obs;
final currentStep = 0.obs;
// Payment proof images for tagihan awal
final RxList<dynamic> paymentProofImagesTagihanAwal = <dynamic>[].obs;
// Payment proof images for denda
final RxList<dynamic> paymentProofImagesDenda = <dynamic>[].obs;
// Track original images loaded from database
final RxList<WebImageFile> originalImages = <WebImageFile>[].obs;
// Track images marked for deletion
final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
// Flag to track if there are changes that need to be saved
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
final RxBool hasUnsavedChangesDenda = false.obs;
// Get image widget for a specific image
Widget getImageWidget(dynamic imageFile) {
// Check if it's a WebImageFile (for existing images loaded from URLs)
if (imageFile is WebImageFile) {
return Image.network(
imageFile.imageUrl,
height: 120,
width: 120,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 120,
width: 120,
color: Colors.grey[300],
child: const Center(child: Text('Error')),
);
},
);
}
// Check if running on web with a File object
else if (kIsWeb && imageFile is File) {
// For web, we need to use Image.network with the path
return Image.network(
imageFile.path,
height: 120,
width: 120,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 120,
width: 120,
color: Colors.grey[300],
child: const Center(child: Text('Error')),
);
},
);
}
// For mobile with a File object
else if (imageFile is File) {
return Image.file(imageFile, height: 120, width: 120, fit: BoxFit.cover);
}
// Fallback for any other type
else {
return Container(
height: 120,
width: 120,
color: Colors.grey[300],
child: const Center(child: Text('Invalid image')),
);
}
}
// Remove an image from the list
void removeImage(dynamic image) {
if (selectedPaymentType.value == 'denda') {
// Untuk denda
if (image is WebImageFile && image.id.isNotEmpty) {
imagesToDeleteDenda.add(image);
debugPrint(
'🗑️ Marked image for deletion (denda): \\${image.imageUrl} (ID: \\${image.id})',
);
}
paymentProofImagesDenda.remove(image);
} else {
// Default/tagihan awal
if (image is WebImageFile && image.id.isNotEmpty) {
imagesToDeleteTagihanAwal.add(image);
debugPrint(
'🗑️ Marked image for deletion: \\${image.imageUrl} (ID: \\${image.id})',
);
}
paymentProofImagesTagihanAwal.remove(image);
}
_checkForChanges();
update();
}
// Show image in full screen when tapped
void showFullScreenImage(dynamic image) {
String imageUrl;
if (image is WebImageFile) {
imageUrl = image.imageUrl;
} else if (image is File) {
imageUrl = image.path;
} else {
debugPrint('❌ Cannot display image: Unknown image type');
return;
}
debugPrint('📷 Showing full screen image: $imageUrl');
// Show full screen image dialog
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
alignment: Alignment.center,
children: [
// Image with pinch to zoom
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child:
kIsWeb
? Image.network(
imageUrl,
fit: BoxFit.contain,
height: Get.height,
width: Get.width,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Text('Error loading image'),
);
},
)
: Image.file(
File(imageUrl),
fit: BoxFit.contain,
height: Get.height,
width: Get.width,
),
),
// Close button
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Get.back(),
),
),
],
),
),
barrierDismissible: true,
);
}
// Check if there are any changes to save (new images added or existing images removed)
void _checkForChanges() {
bool hasChangesTagihanAwal = false;
bool hasChangesDenda = false;
if (imagesToDeleteTagihanAwal.isNotEmpty) {
hasChangesTagihanAwal = true;
}
if (imagesToDeleteDenda.isNotEmpty) {
hasChangesDenda = true;
}
for (dynamic image in paymentProofImagesTagihanAwal) {
if (image is File) {
hasChangesTagihanAwal = true;
break;
}
}
for (dynamic image in paymentProofImagesDenda) {
if (image is File) {
hasChangesDenda = true;
break;
}
}
hasUnsavedChangesTagihanAwal.value = hasChangesTagihanAwal;
hasUnsavedChangesDenda.value = hasChangesDenda;
debugPrint(
'💾 Has unsaved changes (tagihan awal): $hasChangesTagihanAwal, (denda): $hasChangesDenda',
);
}
final isUploading = false.obs;
final uploadProgress = 0.0.obs;
// Timer countdown
final remainingTime = ''.obs;
Timer? _countdownTimer;
final int paymentTimeLimit = 3600; // 1 hour in seconds
final timeRemaining = 0.obs;
// Bank accounts for transfer
final bankAccounts = RxList<Map<String, dynamic>>([]);
@override
void onInit() {
super.onInit();
tabController = TabController(length: 3, vsync: this);
// Get order ID and rental data from arguments
if (Get.arguments != null) {
if (Get.arguments['orderId'] != null) {
orderId.value = Get.arguments['orderId'];
// If rental data is passed, use it directly
if (Get.arguments['rentalData'] != null) {
Map<String, dynamic> rentalData = Get.arguments['rentalData'];
debugPrint('Received rental data: $rentalData');
// Pre-populate order details with rental data
orderDetails.value = {
'id': rentalData['id'] ?? '',
'item_name': rentalData['name'] ?? 'Aset',
'quantity': rentalData['jumlahUnit'] ?? 0,
'rental_period': rentalData['waktuSewa'] ?? '',
'duration': rentalData['duration'] ?? '',
'price_per_unit': 0, // This might not be available in rental data
'total_price':
rentalData['totalPrice'] != null
? int.tryParse(
rentalData['totalPrice'].toString().replaceAll(
RegExp(r'[^0-9]'),
'',
),
) ??
0
: 0,
'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN',
'created_at': DateTime.now().toString(),
'denda': 0, // Default value
'keterangan': '', // Default value
'image_url': rentalData['imageUrl'],
'waktu_mulai': rentalData['waktuMulai'],
'waktu_selesai': rentalData['waktuSelesai'],
'rentang_waktu': rentalData['rentangWaktu'],
};
// Still load additional details from the database
checkSewaAsetTableStructure();
loadTagihanSewaDetails().then((_) {
// Load existing payment proof images after tagihan_sewa details are loaded
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
});
loadSewaAsetDetails();
loadBankAccounts(); // Load bank accounts data
} else {
// If no rental data is passed, load everything from the database
checkSewaAsetTableStructure();
loadOrderDetails();
loadTagihanSewaDetails().then((_) {
// Load existing payment proof images after tagihan_sewa details are loaded
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
});
loadSewaAsetDetails();
loadBankAccounts(); // Load bank accounts data
}
}
}
}
@override
void onClose() {
_countdownTimer?.cancel();
tabController.dispose();
super.onClose();
}
// Load order details
void loadOrderDetails() {
isLoading.value = true;
// Simulating API call
Future.delayed(Duration(seconds: 1), () {
// Mock data
orderDetails.value = {
'id': orderId.value,
'item_name': 'Sewa Kursi Taman',
'quantity': 5,
'rental_period': '24 April 2023, 10:00 - 12:00',
'duration': '2 jam',
'price_per_unit': 10000,
'total_price': 50000,
'status': 'MENUNGGU PEMBAYARAN',
'created_at':
DateTime.now().toString(), // Use this for countdown calculation
'denda': 20000, // Dummy data for denda
'keterangan':
'Terjadi kerusakan pada bagian kaki', // Dummy keterangan for denda
};
// Update the current step based on the status
updateCurrentStepBasedOnStatus();
isLoading.value = false;
startCountdownTimer();
});
}
// Load sewa_aset details with aset data
void loadSewaAsetDetails() {
isLoading.value = true;
debugPrint(
'🔍 Starting to load sewa_aset details for orderId: ${orderId.value}',
);
asetProvider
.getSewaAsetWithAsetData(orderId.value)
.then((data) {
if (data != null) {
// Use actual data without adding dummy values
sewaAsetDetails.value = data;
debugPrint(
'✅ Sewa aset details loaded: ${sewaAsetDetails.value['id']}',
);
// Debug all fields in the sewaAsetDetails
debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):');
data.forEach((key, value) {
debugPrint(' $key: $value');
});
// Specifically debug waktu_mulai and waktu_selesai
debugPrint('⏰ WAKTU DETAILS:');
debugPrint(' waktu_mulai: ${data['waktu_mulai']}');
debugPrint(' waktu_selesai: ${data['waktu_selesai']}');
debugPrint(' denda: ${data['denda']}');
debugPrint(' keterangan: ${data['keterangan']}');
// If aset_detail exists, debug it too
if (data['aset_detail'] != null) {
debugPrint('🏢 ASET DETAILS:');
(data['aset_detail'] as Map<String, dynamic>).forEach((
key,
value,
) {
debugPrint(' $key: $value');
});
}
// Update order details based on sewa_aset data
orderDetails.update((val) {
if (data['aset_detail'] != null) {
val?['item_name'] = data['aset_detail']['nama'] ?? 'Aset Sewa';
}
val?['quantity'] = data['kuantitas'] ?? 1;
val?['denda'] =
data['denda'] ?? 0; // Use data from API or default to 0
val?['keterangan'] = data['keterangan'] ?? '';
if (data['status'] != null &&
data['status'].toString().isNotEmpty) {
val?['status'] = data['status'];
debugPrint(
'📊 Order status from sewa_aset: \\${data['status']}',
);
}
// Tambahkan mapping updated_at
if (data['updated_at'] != null) {
val?['updated_at'] = data['updated_at'];
}
// Format rental period
if (data['waktu_mulai'] != null &&
data['waktu_selesai'] != null) {
try {
final startTime = DateTime.parse(data['waktu_mulai']);
final endTime = DateTime.parse(data['waktu_selesai']);
val?['rental_period'] =
'\\${startTime.day}/\\${startTime.month}/\\${startTime.year}, \\${startTime.hour}:\\${startTime.minute.toString().padLeft(2, '0')} - \\${endTime.hour}:\\${endTime.minute.toString().padLeft(2, '0')}';
debugPrint(
'✅ Successfully formatted rental period: \\${val?['rental_period']}',
);
} catch (e) {
debugPrint('❌ Error parsing date: \\${e}');
}
} else {
debugPrint(
'⚠️ Missing waktu_mulai or waktu_selesai for formatting rental period',
);
}
});
// Update the current step based on the status
updateCurrentStepBasedOnStatus();
} else {
debugPrint(
'⚠️ No sewa_aset details found for order: ${orderId.value}',
);
// Add dummy data when no real data is available
sewaAsetDetails.value = {
'id': orderId.value,
'denda': 20000,
'keterangan': 'Terjadi kerusakan pada bagian kaki',
};
}
isLoading.value = false;
})
.catchError((error) {
debugPrint('❌ Error loading sewa_aset details: $error');
// Add dummy data in case of error
sewaAsetDetails.value = {
'id': orderId.value,
'denda': 20000,
'keterangan': 'Terjadi kerusakan pada bagian kaki',
};
isLoading.value = false;
});
}
// Load tagihan sewa details
Future<void> loadTagihanSewaDetails() {
isLoading.value = true;
// Use the AsetProvider to fetch the tagihan_sewa data
return asetProvider
.getTagihanSewa(orderId.value)
.then((data) {
if (data != null) {
tagihanSewa.value = data;
debugPrint('✅ Tagihan sewa loaded: ${tagihanSewa.value['id']}');
// Debug the tagihan_sewa data
debugPrint('📋 TAGIHAN SEWA DETAILS:');
data.forEach((key, value) {
debugPrint(' $key: $value');
});
// Specifically debug denda, keterangan, and foto_kerusakan
debugPrint('💰 DENDA DETAILS:');
debugPrint(' denda: ${data['denda']}');
debugPrint(' keterangan: ${data['keterangan']}');
debugPrint(' foto_kerusakan: ${data['foto_kerusakan']}');
} else {
debugPrint('⚠️ No tagihan sewa found for order: ${orderId.value}');
// Initialize with empty data instead of mock data
tagihanSewa.value = {
'id': '',
'sewa_aset_id': orderId.value,
'denda': 0,
'keterangan': '',
'foto_kerusakan': '',
};
}
isLoading.value = false;
})
.catchError((error) {
debugPrint('❌ Error loading tagihan sewa: $error');
// Initialize with empty data instead of mock data
tagihanSewa.value = {
'id': '',
'sewa_aset_id': orderId.value,
'denda': 0,
'keterangan': '',
'foto_kerusakan': '',
};
isLoading.value = false;
});
}
// Start countdown timer (1 hour)
void startCountdownTimer() {
timeRemaining.value = paymentTimeLimit;
_countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (timeRemaining.value <= 0) {
timer.cancel();
handlePaymentTimeout();
} else {
timeRemaining.value--;
updateRemainingTimeDisplay();
}
});
}
// Update the time display in format HH:MM:SS
void updateRemainingTimeDisplay() {
int hours = timeRemaining.value ~/ 3600;
int minutes = (timeRemaining.value % 3600) ~/ 60;
int seconds = timeRemaining.value % 60;
remainingTime.value =
'${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
// Handle payment timeout - change status to DIBATALKAN
void handlePaymentTimeout() {
if (orderDetails.value['status'] == 'MENUNGGU PEMBAYARAN') {
orderDetails.update((val) {
val?['status'] = 'DIBATALKAN';
});
Get.snackbar(
'Pesanan Dibatalkan',
'Batas waktu pembayaran telah berakhir',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: Duration(seconds: 5),
);
}
}
// Change payment method
void selectPaymentMethod(String method) {
paymentMethod.value = method;
update();
}
// Select payment type (tagihan_awal or denda)
void selectPaymentType(String type) {
selectedPaymentType.value = type;
if (type == 'tagihan_awal') {
loadExistingPaymentProofImages(jenisPembayaran: 'tagihan awal');
} else if (type == 'denda') {
loadExistingPaymentProofImages(jenisPembayaran: 'denda');
}
update();
}
// Take photo using camera
Future<void> takePhoto() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image != null) {
if (selectedPaymentType.value == 'denda') {
paymentProofImagesDenda.add(File(image.path));
} else {
paymentProofImagesTagihanAwal.add(File(image.path));
}
_checkForChanges();
update();
}
} catch (e) {
debugPrint('❌ Error taking photo: $e');
Get.snackbar(
'Error',
'Gagal mengambil foto: \\${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Select photo from gallery
Future<void> selectPhotoFromGallery() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (image != null) {
if (selectedPaymentType.value == 'denda') {
paymentProofImagesDenda.add(File(image.path));
} else {
paymentProofImagesTagihanAwal.add(File(image.path));
}
_checkForChanges();
update();
}
} catch (e) {
debugPrint('❌ Error selecting photo from gallery: $e');
Get.snackbar(
'Error',
'Gagal memilih foto dari galeri: \\${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Upload payment proof to Supabase storage and save to foto_pembayaran table
Future<void> uploadPaymentProof({required String jenisPembayaran}) async {
final paymentProofImages =
jenisPembayaran == 'tagihan awal'
? paymentProofImagesTagihanAwal
: paymentProofImagesDenda;
final imagesToDelete =
jenisPembayaran == 'tagihan awal'
? imagesToDeleteTagihanAwal
: imagesToDeleteDenda;
final hasUnsavedChanges =
jenisPembayaran == 'tagihan awal'
? hasUnsavedChangesTagihanAwal
: hasUnsavedChangesDenda;
// If there are no images and none marked for deletion, show error
if (paymentProofImages.isEmpty && imagesToDelete.isEmpty) {
Get.snackbar(
'Error',
'Mohon unggah bukti pembayaran terlebih dahulu',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// If there are no changes, no need to do anything
if (!hasUnsavedChanges.value) {
Get.snackbar(
'Info',
'Tidak ada perubahan yang perlu disimpan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.blue,
colorText: Colors.white,
);
return;
}
try {
isUploading.value = true;
uploadProgress.value = 0.0;
// Set up upload progress listener
final progressNotifier = StreamController<double>();
progressNotifier.stream.listen((progress) {
uploadProgress.value = progress;
});
// First, delete any images marked for deletion
if (imagesToDelete.isNotEmpty) {
debugPrint(
'🗑️ Deleting ${imagesToDelete.length} images from database and storage',
);
for (WebImageFile image in imagesToDelete) {
// Delete the record from the foto_pembayaran table
if (image.id.isNotEmpty) {
debugPrint('🗑️ Deleting record with ID: ${image.id}');
try {
// Delete the record using the UUID string
final result = await client
.from('foto_pembayaran')
.delete()
.eq('id', image.id); // ID is already a string UUID
debugPrint('🗑️ Delete result: $result');
} catch (e) {
debugPrint('❌ Error deleting record: $e');
throw e; // Re-throw so the main catch block handles it
}
debugPrint('🗑️ Deleted database record with ID: ${image.id}');
// Extract the file name from the URL to delete from storage
try {
// Parse the URL to get the filename more reliably
Uri uri = Uri.parse(image.imageUrl);
String path = uri.path;
// The filename is the last part of the path after the last '/'
final String fileName = path.substring(path.lastIndexOf('/') + 1);
debugPrint(
'🗑️ Attempting to delete file from storage: $fileName',
);
// Delete the file from storage
await client.storage.from('bukti.pembayaran').remove([fileName]);
debugPrint(
'🗑️ Successfully deleted file from storage: $fileName',
);
} catch (e) {
debugPrint('⚠️ Error deleting file from storage: $e');
// Continue even if file deletion fails - we've at least deleted from the database
}
}
}
// Clear the deleted images list
imagesToDelete.clear();
}
// Upload each new image to Supabase Storage and save to database
debugPrint(
'🔄 Uploading new payment proof images to Supabase storage...',
);
List<String> uploadedUrls = [];
List<dynamic> newImagesToUpload = [];
List<String> existingImageUrls = [];
// Separate existing WebImageFile objects from new File objects that need uploading
for (final image in paymentProofImages) {
if (image is WebImageFile) {
// This is an existing image, no need to upload again
existingImageUrls.add(image.imageUrl);
} else if (image is File) {
// This is a new image that needs to be uploaded
newImagesToUpload.add(image);
}
}
debugPrint(
'🔄 Found ${existingImageUrls.length} existing images and ${newImagesToUpload.length} new images to upload',
);
// If there are new images to upload
if (newImagesToUpload.isNotEmpty) {
// Calculate progress increment per image
final double progressIncrement = 1.0 / newImagesToUpload.length;
double currentProgress = 0.0;
// Upload each new image
for (int i = 0; i < newImagesToUpload.length; i++) {
final dynamic imageFile = newImagesToUpload[i];
final String fileName =
'${DateTime.now().millisecondsSinceEpoch}_${orderId.value}_$i.jpg';
// Create a sub-progress tracker for this image
final subProgressNotifier = StreamController<double>();
subProgressNotifier.stream.listen((subProgress) {
// Calculate overall progress
progressNotifier.add(
currentProgress + (subProgress * progressIncrement),
);
});
// Upload to Supabase Storage
final String? imageUrl = await _uploadToSupabaseStorage(
imageFile,
fileName,
subProgressNotifier,
);
if (imageUrl == null) {
throw Exception('Failed to upload image $i to storage');
}
debugPrint('✅ Image $i uploaded successfully: $imageUrl');
uploadedUrls.add(imageUrl);
// Update progress for next image
currentProgress += progressIncrement;
}
} else {
// If there are only existing images, set progress to 100%
progressNotifier.add(1.0);
}
// Save all new URLs to foto_pembayaran table
for (String imageUrl in uploadedUrls) {
await _saveToFotoPembayaranTable(imageUrl, jenisPembayaran);
}
// Reload the existing images to get fresh data with new IDs
await loadExistingPaymentProofImages(jenisPembayaran: jenisPembayaran);
// Update order status in orderDetails
orderDetails.update((val) {
if (jenisPembayaran == 'denda' &&
val?['status'] == 'PEMBAYARAN DENDA') {
val?['status'] = 'PERIKSA PEMBAYARAN DENDA';
} else {
val?['status'] = 'MEMERIKSA PEMBAYARAN';
}
});
// Also update the status in the sewa_aset table
try {
// Get the sewa_aset_id from the tagihanSewa data
final dynamic sewaAsetId = tagihanSewa.value['sewa_aset_id'];
if (sewaAsetId != null && sewaAsetId.toString().isNotEmpty) {
debugPrint(
'🔄 Updating status in sewa_aset table for ID: $sewaAsetId',
);
// Update the status in the sewa_aset table
final updateResult = await client
.from('sewa_aset')
.update({
'status':
(jenisPembayaran == 'denda' &&
orderDetails.value['status'] ==
'PERIKSA PEMBAYARAN DENDA')
? 'PERIKSA PEMBAYARAN DENDA'
: 'PERIKSA PEMBAYARAN',
})
.eq('id', sewaAsetId.toString());
debugPrint('✅ Status updated in sewa_aset table: $updateResult');
} else {
debugPrint(
'⚠️ Could not update sewa_aset status: No valid sewa_aset_id found',
);
}
} catch (e) {
// Don't fail the entire operation if this update fails
debugPrint('❌ Error updating status in sewa_aset table: $e');
}
// Update current step based on status
updateCurrentStepBasedOnStatus();
// Cancel countdown timer as payment has been submitted
_countdownTimer?.cancel();
// Reset change tracking
hasUnsavedChanges.value = false;
// Show success message
Get.snackbar(
'Sukses',
'Bukti pembayaran berhasil diunggah',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
debugPrint('❌ Error uploading payment proof: $e');
Get.snackbar(
'Error',
'Gagal mengunggah bukti pembayaran: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isUploading.value = false;
uploadProgress.value = 0.0;
}
}
// Go to next step
void nextStep() {
if (currentStep.value < 7) {
currentStep.value++;
updateOrderStatusBasedOnStep();
}
}
// Update order status based on current step
void updateOrderStatusBasedOnStep() {
String newStatus;
switch (currentStep.value) {
case 0:
newStatus = 'MENUNGGU PEMBAYARAN';
break;
case 1:
newStatus = 'MEMERIKSA PEMBAYARAN';
break;
case 2:
newStatus = 'DITERIMA';
break;
case 3:
newStatus = 'PENGEMBALIAN';
break;
case 4:
newStatus = 'PEMBAYARAN DENDA';
break;
case 5:
newStatus = 'MEMERIKSA PEMBAYARAN DENDA';
break;
case 6:
newStatus = 'SELESAI';
break;
default:
newStatus = 'MENUNGGU PEMBAYARAN';
}
orderDetails.update((val) {
val?['status'] = newStatus;
});
}
// Update currentStep based on order status
void updateCurrentStepBasedOnStatus() {
final status = orderDetails.value['status']?.toString().toUpperCase() ?? '';
debugPrint('📊 Updating current step based on status: $status');
switch (status) {
case 'MENUNGGU PEMBAYARAN':
currentStep.value = 0;
break;
case 'MEMERIKSA PEMBAYARAN':
currentStep.value = 1;
break;
case 'DITERIMA':
currentStep.value = 2;
break;
case 'AKTIF':
currentStep.value = 3;
break;
case 'PENGEMBALIAN':
currentStep.value = 4;
break;
case 'PEMBAYARAN DENDA':
currentStep.value = 5;
break;
case 'PERIKSA PEMBAYARAN DENDA':
currentStep.value = 6;
break;
case 'SELESAI':
currentStep.value = 7;
break;
case 'DIBATALKAN':
currentStep.value = 8;
break;
default:
currentStep.value = 0;
break;
}
debugPrint('📊 Current step updated to: ${currentStep.value}');
}
// This method has been moved and improved above
// Submit cash payment
void submitCashPayment() {
// Update order status
orderDetails.update((val) {
val?['status'] = 'MEMERIKSA PEMBAYARAN';
});
// Cancel countdown timer as payment has been submitted
_countdownTimer?.cancel();
// Show success message
Get.snackbar(
'Sukses',
'Pembayaran tunai berhasil disubmit',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// Update step
currentStep.value = 1;
}
// Cancel payment
void cancelPayment() {
Get.back();
}
// Debug function to check sewa_aset table structure
void checkSewaAsetTableStructure() {
try {
debugPrint('🔍 DEBUG: Checking sewa_aset table structure');
final client = asetProvider.client;
// Get a single record to check field names
client
.from('sewa_aset')
.select('*')
.limit(1)
.then((response) {
if (response.isNotEmpty) {
final record = response.first;
debugPrint('📋 SEWA_ASET TABLE STRUCTURE:');
debugPrint('Available fields in sewa_aset table:');
record.forEach((key, value) {
debugPrint(
' $key: (${value != null ? value.runtimeType : 'null'})',
);
});
// Specifically check for time fields
final timeFields = [
'waktu_mulai',
'waktu_selesai',
'start_time',
'end_time',
];
for (final field in timeFields) {
debugPrint(
' Field "$field" exists: ${record.containsKey(field)}',
);
if (record.containsKey(field)) {
debugPrint(' Field "$field" value: ${record[field]}');
}
}
} else {
debugPrint('⚠️ No records found in sewa_aset table');
}
})
.catchError((e) {
debugPrint('❌ Error checking sewa_aset table: $e');
});
} catch (e) {
debugPrint('❌ Error in checkSewaAsetTableStructure: $e');
}
}
// Load bank accounts from akun_bank table
Future<void> loadBankAccounts() async {
debugPrint('Loading bank accounts from akun_bank table...');
try {
final data = await asetProvider.getBankAccounts();
if (data.isNotEmpty) {
bankAccounts.assignAll(data);
debugPrint(
'✅ Bank accounts loaded: ${bankAccounts.length} accounts found',
);
// Debug the bank accounts data
debugPrint('📋 BANK ACCOUNTS DETAILS:');
for (var account in bankAccounts) {
debugPrint(
' Bank: ${account['nama_bank']}, Account: ${account['nama_akun']}, Number: ${account['no_rekening']}',
);
}
} else {
debugPrint('⚠️ No bank accounts found in akun_bank table');
}
} catch (e) {
debugPrint('❌ Error loading bank accounts: $e');
}
}
// Helper method to upload image to Supabase storage
Future<String?> _uploadToSupabaseStorage(
dynamic imageFile,
String fileName,
StreamController<double> progressNotifier,
) async {
try {
debugPrint('🔄 Uploading image to Supabase storage: $fileName');
// If it's already a WebImageFile, just return the URL
if (imageFile is WebImageFile) {
progressNotifier.add(1.0); // No upload needed
return imageFile.imageUrl;
}
// Handle File objects
if (imageFile is File) {
// Get file bytes
List<int> fileBytes = await imageFile.readAsBytes();
// Upload to Supabase Storage
await client.storage
.from('bukti.pembayaran')
.uploadBinary(
fileName,
Uint8List.fromList(fileBytes),
fileOptions: const FileOptions(
cacheControl: '3600',
upsert: false,
),
);
// Get public URL
final String publicUrl = client.storage
.from('bukti.pembayaran')
.getPublicUrl(fileName);
debugPrint('✅ Upload successful: $publicUrl');
progressNotifier.add(1.0); // Upload complete
return publicUrl;
}
// If we get here, we don't know how to handle this type
throw Exception('Unsupported image type: ${imageFile.runtimeType}');
} catch (e) {
debugPrint('❌ Error uploading to Supabase storage: $e');
return null;
} finally {
progressNotifier.close();
}
}
// Helper method to save image URL to foto_pembayaran table
Future<void> _saveToFotoPembayaranTable(
String imageUrl,
String jenisPembayaran,
) async {
try {
debugPrint('🔄 Saving image URL to foto_pembayaran table...');
// Get the tagihan_sewa_id from the tagihanSewa object
final dynamic tagihanSewaId = tagihanSewa.value['id'];
if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) {
throw Exception('tagihan_sewa_id not found in tagihanSewa data');
}
debugPrint('🔄 Using tagihan_sewa_id: $tagihanSewaId');
// Prepare the data to insert
final Map<String, dynamic> data = {
'tagihan_sewa_id': tagihanSewaId,
'foto_pembayaran': imageUrl,
'jenis_pembayaran': jenisPembayaran,
'created_at': DateTime.now().toIso8601String(),
};
// Insert data into the foto_pembayaran table
final response =
await client.from('foto_pembayaran').insert(data).select().single();
debugPrint(
'✅ Image URL saved to foto_pembayaran table: ${response['id']}',
);
} catch (e) {
debugPrint('❌ Error in _saveToFotoPembayaranTable: $e');
throw Exception('Failed to save image URL to database: $e');
}
}
// Load existing payment proof images for a specific jenis_pembayaran
Future<void> loadExistingPaymentProofImages({
required String jenisPembayaran,
}) async {
try {
debugPrint(
'🔄 Loading existing payment proof images for tagihan_sewa_id: \\${tagihanSewa.value['id']} dan jenis_pembayaran: $jenisPembayaran',
);
final dynamic tagihanSewaId = tagihanSewa.value['id'];
if (tagihanSewaId == null || tagihanSewaId.toString().isEmpty) {
debugPrint('⚠️ No valid tagihan_sewa_id found, skipping image load');
return;
}
final List<dynamic> response = await client
.from('foto_pembayaran')
.select()
.eq('tagihan_sewa_id', tagihanSewaId)
.eq('jenis_pembayaran', jenisPembayaran)
.order('created_at', ascending: false);
debugPrint(
'🔄 Found \\${response.length} existing payment proof images for $jenisPembayaran',
);
final targetList =
jenisPembayaran == 'tagihan awal'
? paymentProofImagesTagihanAwal
: paymentProofImagesDenda;
targetList.clear();
for (final item in response) {
final String imageUrl = item['foto_pembayaran'];
String imageId = '';
try {
if (item.containsKey('id')) {
final dynamic rawId = item['id'];
if (rawId != null) {
imageId = rawId.toString();
}
debugPrint('🔄 Image ID: $imageId');
}
} catch (e) {
debugPrint('❌ Error getting image ID: $e');
}
final webImageFile = WebImageFile(imageUrl);
webImageFile.id = imageId;
targetList.add(webImageFile);
debugPrint('✅ Added image: $imageUrl with ID: $imageId');
}
update();
} catch (e) {
debugPrint('❌ Error loading payment proof images: $e');
}
}
// Refresh all data
Future<void> refreshData() async {
debugPrint('Refreshing payment page data...');
isLoading.value = true;
try {
// Reload all data
await Future.delayed(
const Duration(milliseconds: 500),
); // Small delay for better UX
loadOrderDetails();
loadTagihanSewaDetails();
loadSewaAsetDetails();
loadBankAccounts(); // Load bank accounts data
// Explicitly update the current step based on the status
// This ensures the progress timeline is always in sync with the actual status
updateCurrentStepBasedOnStatus();
// Restart countdown timer if needed
if (orderDetails.value['status'] == 'MENUNGGU PEMBAYARAN') {
_countdownTimer?.cancel();
startCountdownTimer();
}
debugPrint('Data refresh completed');
} catch (e) {
debugPrint('Error refreshing data: $e');
} finally {
isLoading.value = false;
}
return Future.value();
}
}