import 'package:get/get.dart'; import 'package:flutter/material.dart'; import '../../../data/providers/auth_provider.dart'; import '../../../routes/app_routes.dart'; import '../../../services/navigation_service.dart'; import '../../../data/providers/aset_provider.dart'; import '../../../theme/app_colors.dart'; import 'package:intl/intl.dart'; import 'package:flutter/foundation.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:image_picker/image_picker.dart'; class WargaDashboardController extends GetxController { // Dependency injection final AuthProvider _authProvider = Get.find(); final NavigationService navigationService = Get.find(); final AsetProvider _asetProvider = Get.find(); // User data final userName = 'Pengguna Warga'.obs; final userRole = 'Warga'.obs; final userAvatar = Rx(null); final userEmail = ''.obs; final userNik = ''.obs; final userPhone = ''.obs; final userAddress = ''.obs; final userTanggalLahir = ''.obs; final userRtRw = ''.obs; final userKelurahanDesa = ''.obs; final userKecamatan = ''.obs; // Navigation state is now managed by NavigationService // Sample data (would be loaded from API) final activeRentals = >[].obs; // Active bills final activeBills = >[].obs; // Active penalties final activePenalties = >[].obs; // Summary counts final diterimaCount = 0.obs; final tagihanAktifCount = 0.obs; final dendaAktifCount = 0.obs; @override void onInit() async { super.onInit(); // Set navigation index to Home (0) navigationService.setNavIndex(0); // Check if navigation is coming from login final args = Get.arguments; final bool isFromLogin = args != null && args['from_login'] == true; if (isFromLogin) { print('onInit: Navigation from login detected, prioritizing data fetch'); } // Verifikasi bahwa pengguna sudah login sebelum melakukan fetch data if (_authProvider.currentUser != null) { // Prioritize loading user profile data first await fetchProfileFromWargaDesa(); // If the profile data was not loaded successfully, try again after a short delay if (userName.value == 'Pengguna Warga' || userNik.value.isEmpty) { print('onInit: Profile data not loaded, retrying after delay'); await Future.delayed(const Duration(milliseconds: 800)); await fetchProfileFromWargaDesa(); } // Load other user data await _loadUserData(); // Load other data in parallel to speed up the dashboard initialization Future.wait([ _loadSampleData(), loadDummyData(), loadUnpaidRentals(), _debugCountSewaAset(), loadActiveRentals(), ]).then((_) => print('onInit: All data loaded successfully')); // If coming from login, make sure UI is updated if (isFromLogin) { update(); } } else { print('onInit: User not logged in, skipping data fetch'); } } Future _loadUserData() async { try { // Get the full name from warga_desa table final fullName = await _authProvider.getUserFullName(); if (fullName != null && fullName.isNotEmpty) { userName.value = fullName; } // Get the avatar URL final avatar = await _authProvider.getUserAvatar(); userAvatar.value = avatar; // Get the role name final roleId = await _authProvider.getUserRoleId(); if (roleId != null) { final roleName = await _authProvider.getRoleName(roleId); if (roleName != null) { userRole.value = roleName; } } // Load additional user data // In a real app, these would come from the API/database userEmail.value = await _authProvider.getUserEmail() ?? ''; userNik.value = await _authProvider.getUserNIK() ?? ''; userPhone.value = await _authProvider.getUserPhone() ?? ''; userAddress.value = await _authProvider.getUserAddress() ?? ''; // Load additional profile data final tanggalLahir = await _authProvider.getUserTanggalLahir(); final rtRw = await _authProvider.getUserRtRw(); final kelurahanDesa = await _authProvider.getUserKelurahanDesa(); final kecamatan = await _authProvider.getUserKecamatan(); // Set values for additional profile data userTanggalLahir.value = tanggalLahir ?? 'Tidak tersedia'; userRtRw.value = rtRw ?? 'Tidak tersedia'; userKelurahanDesa.value = kelurahanDesa ?? 'Tidak tersedia'; userKecamatan.value = kecamatan ?? 'Tidak tersedia'; } catch (e) { print('Error loading user data: $e'); } } Future _loadSampleData() async { // Clear any existing data activeRentals.clear(); // Load active rentals from API // For now, using sample data activeRentals.add({ 'id': '1', 'name': 'Kursi', 'time': '24 April 2023, 10:00 - 12:00', 'duration': '2 jam', 'price': 'Rp50.000', 'can_extend': true, }); } void extendRental(String rentalId) { // Implementasi untuk memperpanjang sewa // Seharusnya melakukan API call ke backend } void endRental(String rentalId) { // Implementasi untuk mengakhiri sewa // Seharusnya melakukan API call ke backend } void navigateToRentals() { // Navigate to SewaAset using the navigation service navigationService.toSewaAset(); } Future refreshData() async { print('refreshData: Refreshing dashboard data'); try { // First fetch profile data await fetchProfileFromWargaDesa(); await _loadUserData(); // Then load all other data in parallel await Future.wait([ _loadSampleData(), loadDummyData(), loadUnpaidRentals(), loadActiveRentals(), _debugCountSewaAset(), ]); // Update UI update(); print('refreshData: Dashboard data refreshed successfully'); } catch (e) { print('refreshData: Error refreshing data: $e'); // Show error message to user Get.snackbar( 'Perhatian', 'Terjadi kesalahan saat memuat data', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.shade100, colorText: Colors.red.shade900, duration: const Duration(seconds: 3), ); } } void onNavItemTapped(int index) { if (navigationService.currentNavIndex.value == index) { return; // Don't do anything if same tab } navigationService.setNavIndex(index); switch (index) { case 0: // Already on Home tab break; case 1: // Navigate to Sewa page, tab Aktif toWargaSewaTabAktif(); break; } } void toWargaSewaTabAktif() { // Navigasi ke halaman warga sewa dan tab Aktif (index 3) Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3}); } Future logout() async { print('logout: Logging out user'); await _authProvider.signOut(); navigationService.toLogin(); print('logout: User logged out and redirected to login screen'); } Future loadDummyData() async { // Dummy active bills activeBills.clear(); activeBills.add({ 'id': '1', 'title': 'Tagihan Air', 'due_date': '30 Apr 2023', 'amount': 'Rp 125.000', }); activeBills.add({ 'id': '2', 'title': 'Sewa Aula Desa', 'due_date': '15 Apr 2023', 'amount': 'Rp 350.000', }); // Dummy active penalties activePenalties.clear(); activePenalties.add({ 'id': '1', 'title': 'Keterlambatan Sewa Traktor', 'days_late': '7', 'amount': 'Rp 75.000', }); } Future loadUnpaidRentals() async { try { final results = await _authProvider.getSewaAsetByStatus([ 'MENUNGGU PEMBAYARAN', 'PEMBAYARANAN DENDA', ]); activeBills.value = results; } catch (e) { print('Error loading unpaid rentals: $e'); } } Future _debugCountSewaAset() async { diterimaCount.value = await _asetProvider.countSewaAsetByStatus([ 'DITERIMA', ]); tagihanAktifCount.value = await _asetProvider.countSewaAsetByStatus([ 'MENUNGGU PEMBAYARAN', 'PERIKSA PEMBAYARAN', ]); dendaAktifCount.value = await _asetProvider.countSewaAsetByStatus([ 'PEMBAYARAN DENDA', 'PERIKSA PEMBAYARAN DENDA', ]); print('[DEBUG] Jumlah sewa diterima: ${diterimaCount.value}'); print('[DEBUG] Jumlah tagihan aktif: ${tagihanAktifCount.value}'); print('[DEBUG] Jumlah denda aktif: ${dendaAktifCount.value}'); } Future loadActiveRentals() async { try { activeRentals.clear(); final sewaAsetList = await _authProvider.getSewaAsetByStatus(['AKTIF']); for (var sewaAset in sewaAsetList) { String assetName = 'Aset'; String? imageUrl; String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam'; if (sewaAset['aset_id'] != null) { final asetData = await _asetProvider.getAsetById(sewaAset['aset_id']); if (asetData != null) { assetName = asetData.nama; imageUrl = asetData.imageUrl; } } DateTime? waktuMulai; DateTime? waktuSelesai; String waktuSewa = ''; String tanggalSewa = ''; String jamMulai = ''; String jamSelesai = ''; String rentangWaktu = ''; if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) { waktuMulai = DateTime.parse(sewaAset['waktu_mulai']); waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']); final formatTanggal = DateFormat('dd-MM-yyyy'); final formatWaktu = DateFormat('HH:mm'); final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID'); tanggalSewa = formatTanggalLengkap.format(waktuMulai); jamMulai = formatWaktu.format(waktuMulai); jamSelesai = formatWaktu.format(waktuSelesai); if (namaSatuanWaktu.toLowerCase() == 'jam') { rentangWaktu = '$jamMulai - $jamSelesai'; } else if (namaSatuanWaktu.toLowerCase() == 'hari') { final tanggalMulai = formatTanggalLengkap.format(waktuMulai); final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai); rentangWaktu = '$tanggalMulai - $tanggalSelesai'; } else { rentangWaktu = '$jamMulai - $jamSelesai'; } waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - ' '${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}'; } String totalPrice = 'Rp 0'; if (sewaAset['total'] != null) { final formatter = NumberFormat.currency( locale: 'id', symbol: 'Rp ', decimalDigits: 0, ); totalPrice = formatter.format(sewaAset['total']); } String duration = '-'; final tagihan = await _asetProvider.getTagihanSewa(sewaAset['id']); if (tagihan != null) { final durasiTagihan = tagihan['durasi'] ?? sewaAset['durasi']; final satuanTagihan = tagihan['nama_satuan_waktu'] ?? namaSatuanWaktu; duration = '${durasiTagihan ?? '-'} ${satuanTagihan ?? ''}'; } else { duration = '${sewaAset['durasi'] ?? '-'} ${namaSatuanWaktu ?? ''}'; } activeRentals.add({ 'id': sewaAset['id'] ?? '', 'name': assetName, 'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg', 'jumlahUnit': sewaAset['kuantitas'] ?? 0, 'waktuSewa': waktuSewa, 'duration': duration, 'status': sewaAset['status'] ?? 'AKTIF', 'totalPrice': totalPrice, 'tanggalSewa': tanggalSewa, 'jamMulai': jamMulai, 'jamSelesai': jamSelesai, 'rentangWaktu': rentangWaktu, 'namaSatuanWaktu': namaSatuanWaktu, 'waktuMulai': sewaAset['waktu_mulai'], 'waktuSelesai': sewaAset['waktu_selesai'], 'can_extend': sewaAset['can_extend'] == true, }); } } catch (e) { print('Error loading active rentals: $e'); } } void toSewaAsetTabPaket() { // Navigasi ke halaman sewa_aset tab Paket (index 1) Get.toNamed(Routes.SEWA_ASET, arguments: {'tab': 1}); } Future fetchProfileFromWargaDesa() async { try { final user = _authProvider.currentUser; if (user == null) { print( 'fetchProfileFromWargaDesa: No current user found, skipping fetch', ); return; // Exit early if no user is logged in } final userId = user.id; print('fetchProfileFromWargaDesa: Fetching data for user: $userId'); final data = await _authProvider.client .from('warga_desa') .select('nik, alamat, email, nama_lengkap, no_hp, avatar') .eq('user_id', userId) .maybeSingle(); if (data != null) { print('fetchProfileFromWargaDesa: Data retrieved successfully'); userNik.value = data['nik']?.toString() ?? ''; userAddress.value = data['alamat']?.toString() ?? ''; userEmail.value = data['email']?.toString() ?? ''; userName.value = data['nama_lengkap']?.toString() ?? ''; userPhone.value = data['no_hp']?.toString() ?? ''; userAvatar.value = data['avatar']?.toString() ?? ''; // Trigger UI refresh update(); print('fetchProfileFromWargaDesa: Profile data updated'); } else { print('fetchProfileFromWargaDesa: No data found for user: $userId'); } } catch (e) { print('Error fetching profile from warga_desa: $e'); // If it fails, try again after a delay await Future.delayed(const Duration(seconds: 1)); try { await _retryFetchProfile(); } catch (retryError) { print('Retry error fetching profile: $retryError'); } } } // Helper method to retry fetching profile Future _retryFetchProfile() async { final user = _authProvider.currentUser; if (user == null) { print('_retryFetchProfile: No current user found, skipping retry'); return; // Exit early if no user is logged in } print('_retryFetchProfile: Retrying fetch for user: ${user.id}'); final data = await _authProvider.client .from('warga_desa') .select('nik, alamat, email, nama_lengkap, no_hp, avatar') .eq('user_id', user.id) .maybeSingle(); if (data != null) { print('_retryFetchProfile: Data retrieved successfully on retry'); userNik.value = data['nik']?.toString() ?? ''; userAddress.value = data['alamat']?.toString() ?? ''; userEmail.value = data['email']?.toString() ?? ''; userName.value = data['nama_lengkap']?.toString() ?? ''; userPhone.value = data['no_hp']?.toString() ?? ''; userAvatar.value = data['avatar']?.toString() ?? ''; update(); print('_retryFetchProfile: Profile data updated'); } } // Method to update user profile data in warga_desa table Future updateUserProfile({ required String namaLengkap, required String noHp, }) async { try { final user = _authProvider.currentUser; if (user == null) { print('Cannot update profile: No current user'); return false; } final userId = user.id; // Update data in warga_desa table await _authProvider.client .from('warga_desa') .update({'nama_lengkap': namaLengkap, 'no_hp': noHp}) .eq('user_id', userId); // Update local values userName.value = namaLengkap; userPhone.value = noHp; print('Profile updated successfully for user: $userId'); return true; } catch (e) { print('Error updating user profile: $e'); return false; } } // Method to delete user avatar Future deleteUserAvatar() async { try { final user = _authProvider.currentUser; if (user == null) { print('Cannot delete avatar: No current user'); return false; } final userId = user.id; final currentAvatarUrl = userAvatar.value; // If there's an avatar URL, delete it from storage if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) { try { print('Attempting to delete avatar from URL: $currentAvatarUrl'); // Extract filename from URL // The URL format is typically: // https://[project-ref].supabase.co/storage/v1/object/public/warga/[filename] final uri = Uri.parse(currentAvatarUrl); final path = uri.path; // Find the filename after the last slash final filename = path.substring(path.lastIndexOf('/') + 1); if (filename.isNotEmpty) { print('Extracted filename: $filename'); // Delete from storage bucket 'warga' final response = await _authProvider.client.storage .from('warga') .remove([filename]); print('Storage deletion response: $response'); } else { print('Failed to extract filename from avatar URL'); } } catch (e) { print('Error deleting avatar from storage: $e'); // Continue with database update even if storage delete fails } } // Update warga_desa table to set avatar to null await _authProvider.client .from('warga_desa') .update({'avatar': null}) .eq('user_id', userId); // Update local value userAvatar.value = ''; print('Avatar deleted successfully for user: $userId'); return true; } catch (e) { print('Error deleting user avatar: $e'); return false; } } // Method to update user avatar URL Future updateUserAvatar(String avatarUrl) async { try { final user = _authProvider.currentUser; if (user == null) { print('Cannot update avatar: No current user'); return false; } final userId = user.id; // Update data in warga_desa table await _authProvider.client .from('warga_desa') .update({'avatar': avatarUrl}) .eq('user_id', userId); // Update local value userAvatar.value = avatarUrl; print('Avatar updated successfully for user: $userId'); return true; } catch (e) { print('Error updating user avatar: $e'); return false; } } // Method to upload avatar image to Supabase storage Future uploadAvatar(Uint8List fileBytes, String fileName) async { try { final user = _authProvider.currentUser; if (user == null) { print('Cannot upload avatar: No current user'); return null; } // Generate a unique filename using timestamp and user ID final timestamp = DateTime.now().millisecondsSinceEpoch; final extension = fileName.split('.').last; final uniqueFileName = 'avatar_${user.id}_$timestamp.$extension'; // Upload to 'warga' bucket final response = await _authProvider.client.storage .from('warga') .uploadBinary( uniqueFileName, fileBytes, fileOptions: const FileOptions(cacheControl: '3600', upsert: true), ); // Get the public URL final publicUrl = _authProvider.client.storage .from('warga') .getPublicUrl(uniqueFileName); print('Avatar uploaded successfully: $publicUrl'); return publicUrl; } catch (e) { print('Error uploading avatar: $e'); return null; } } // Method to handle image picking from camera or gallery Future pickImage(ImageSource source) async { try { // Pick image directly without permission checks final ImagePicker picker = ImagePicker(); final XFile? pickedFile = await picker.pickImage( source: source, maxWidth: 800, maxHeight: 800, imageQuality: 85, ); if (pickedFile != null) { print('Image picked: ${pickedFile.path}'); } return pickedFile; } catch (e) { print('Error picking image: $e'); // Show error message if there's an issue Get.snackbar( 'Gagal', 'Tidak dapat mengakses ${source == ImageSource.camera ? 'kamera' : 'galeri'}', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.shade700, colorText: Colors.white, duration: const Duration(seconds: 3), ); return null; } } // Method to show image source selection dialog Future showImageSourceDialog() async { await Get.bottomSheet( Container( padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), const Text( 'Pilih Sumber Gambar', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildImageSourceOption( icon: Icons.camera_alt_rounded, label: 'Kamera', onTap: () async { Get.back(); final pickedFile = await pickImage(ImageSource.camera); if (pickedFile != null) { await processPickedImage(pickedFile); } }, ), _buildImageSourceOption( icon: Icons.photo_library_rounded, label: 'Galeri', onTap: () async { Get.back(); final pickedFile = await pickImage(ImageSource.gallery); if (pickedFile != null) { await processPickedImage(pickedFile); } }, ), ], ), const SizedBox(height: 20), ], ), ), isDismissible: true, enableDrag: true, ); } // Helper method to build image source option Widget _buildImageSourceOption({ required IconData icon, required String label, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(icon, color: AppColors.primary, size: 32), ), const SizedBox(height: 8), Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade800, ), ), ], ), ); } // Method to process picked image (temporary preview before saving) Future processPickedImage(XFile pickedFile) async { try { // Read file as bytes final bytes = await pickedFile.readAsBytes(); // Store the picked file temporarily for later use when saving tempPickedFile.value = pickedFile; // Update UI with temporary avatar preview tempAvatarBytes.value = bytes; print('Image processed for preview'); } catch (e) { print('Error processing picked image: $e'); } } // Method to save the picked image to Supabase and update profile Future saveNewAvatar() async { try { if (tempPickedFile.value == null || tempAvatarBytes.value == null) { print('No temporary image to save'); return false; } final pickedFile = tempPickedFile.value!; final bytes = tempAvatarBytes.value!; // First delete the old avatar if exists final currentAvatarUrl = userAvatar.value; if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) { try { await deleteUserAvatar(); } catch (e) { print('Error deleting old avatar: $e'); // Continue with upload even if delete fails } } // Upload new avatar final newAvatarUrl = await uploadAvatar(bytes, pickedFile.name); if (newAvatarUrl == null) { print('Failed to upload new avatar'); return false; } // Update avatar URL in database final success = await updateUserAvatar(newAvatarUrl); if (success) { // Clear temporary data tempPickedFile.value = null; tempAvatarBytes.value = null; print('Avatar updated successfully'); } return success; } catch (e) { print('Error saving new avatar: $e'); return false; } } // Method to cancel avatar change void cancelAvatarChange() { tempPickedFile.value = null; tempAvatarBytes.value = null; print('Avatar change canceled'); } // Temporary storage for picked image final Rx tempPickedFile = Rx(null); final Rx tempAvatarBytes = Rx(null); }