semua fitur selesai
This commit is contained in:
@ -1,9 +1,14 @@
|
||||
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
|
||||
@ -19,6 +24,10 @@ class WargaDashboardController extends GetxController {
|
||||
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
|
||||
|
||||
@ -90,6 +99,18 @@ class WargaDashboardController extends GetxController {
|
||||
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');
|
||||
}
|
||||
@ -330,4 +351,369 @@ class WargaDashboardController extends GetxController {
|
||||
print('Error fetching profile from warga_desa: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update user profile data in warga_desa table
|
||||
Future<bool> 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<bool> 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<bool> 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<String?> 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<XFile?> 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.BOTTOM,
|
||||
backgroundColor: Colors.red.shade700,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to show image source selection dialog
|
||||
Future<void> 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<void> 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<bool> 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<XFile?> tempPickedFile = Rx<XFile?>(null);
|
||||
final Rx<Uint8List?> tempAvatarBytes = Rx<Uint8List?>(null);
|
||||
}
|
||||
|
Reference in New Issue
Block a user