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(); final AsetProvider asetProvider = Get.find(); // 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>({}); // Sewa Aset details with related aset info final sewaAsetDetails = Rx>({}); // Tagihan Sewa details final tagihanSewa = Rx>({}); // Payment details final paymentMethod = ''.obs; final selectedPaymentType = ''.obs; final isLoading = false.obs; final currentStep = 0.obs; // Payment proof images for tagihan awal final RxList paymentProofImagesTagihanAwal = [].obs; // Payment proof images for denda final RxList paymentProofImagesDenda = [].obs; // Track original images loaded from database final RxList originalImages = [].obs; // Track images marked for deletion final RxList imagesToDeleteTagihanAwal = [].obs; final RxList imagesToDeleteDenda = [].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>([]); @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 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).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 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 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 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 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(); 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 uploadedUrls = []; List newImagesToUpload = []; List 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(); 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 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 _uploadToSupabaseStorage( dynamic imageFile, String fileName, StreamController 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 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 _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 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 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 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 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(); } }