diff --git a/sigap-mobile/lib/main.dart b/sigap-mobile/lib/main.dart index 44778cf..f208464 100644 --- a/sigap-mobile/lib/main.dart +++ b/sigap-mobile/lib/main.dart @@ -1,13 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; -import 'package:sigap/app.dart'; +import 'package:sigap/navigation_menu.dart'; +import 'package:sigap/src/features/personalization/presentasion/bindings/personalization_bindings.dart'; +import 'package:sigap/src/utils/theme/theme.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future main() async { - final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + // Make sure to initialize bindings first + WidgetsFlutterBinding.ensureInitialized(); + + // Register navigation controller early since it's needed for NavigationMenu + Get.put(NavigationController(), permanent: true); // Make sure status bar is properly set SystemChrome.setSystemUIOverlayStyle( @@ -41,6 +48,22 @@ Future main() async { MapboxOptions.setAccessToken(mapboxAccesToken); - runApp(const App()); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + title: 'SIGAP', + debugShowCheckedModeBanner: false, + theme: TAppTheme.lightTheme, + darkTheme: TAppTheme.darkTheme, + themeMode: ThemeMode.system, + initialBinding: PersonalizationBindings(), + home: const NavigationMenu(), + ); + } } - \ No newline at end of file diff --git a/sigap-mobile/lib/navigation_menu.dart b/sigap-mobile/lib/navigation_menu.dart index c206ffc..ee78d01 100644 --- a/sigap-mobile/lib/navigation_menu.dart +++ b/sigap-mobile/lib/navigation_menu.dart @@ -9,8 +9,8 @@ class NavigationMenu extends StatelessWidget { @override Widget build(BuildContext context) { - // Using GetX controller to manage navigation state - final controller = Get.put(NavigationController()); + // Ensure NavigationController is registered in a binding first, then use find + final controller = Get.find(); final theme = Theme.of(context); return Scaffold( diff --git a/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart b/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart index b416285..34c6700 100644 --- a/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart +++ b/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart @@ -14,7 +14,7 @@ class UserRepository extends GetxController { static UserRepository get instance => Get.find(); final _supabase = SupabaseService.instance.client; - final _logger = Get.put(Logger()); + final _logger = Get.find(); // Get current user ID String? get currentUserId => SupabaseService.instance.currentUserId; diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/bindings/personalization_bindings.dart b/sigap-mobile/lib/src/features/personalization/presentasion/bindings/personalization_bindings.dart index e69de29..92bd807 100644 --- a/sigap-mobile/lib/src/features/personalization/presentasion/bindings/personalization_bindings.dart +++ b/sigap-mobile/lib/src/features/personalization/presentasion/bindings/personalization_bindings.dart @@ -0,0 +1,80 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart'; +import 'package:sigap/src/features/personalization/data/repositories/profile_repository.dart'; +import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/profile_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/email_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/language_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/security_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/settings_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart'; + +class PersonalizationBindings extends Bindings { + @override + void dependencies() { + // Register Logger if not already registered + if (!Get.isRegistered()) { + Get.lazyPut(() => Logger(), fenix: true); + } + + // Register repositories + _registerRepositories(); + + // Register profile controllers + _registerProfileControllers(); + + // Register settings controllers + _registerSettingsControllers(); + } + + void _registerRepositories() { + // Register repositories with fenix: true to keep them alive when not in use + // but recreate them if they were destroyed + Get.lazyPut(() => UserRepository(), fenix: true); + + Get.lazyPut(() => ProfileRepository(), fenix: true); + + Get.lazyPut(() => OfficerRepository(), fenix: true); + } + + void _registerProfileControllers() { + // Register profile-related controllers + Get.lazyPut(() => ProfileController(), fenix: true); + + Get.lazyPut( + () => OfficerProfileController(), + fenix: true, + ); + } + + void _registerSettingsControllers() { + // Register main settings controller + Get.lazyPut(() => SettingsController(), fenix: true); + + // Register individual settings feature controllers + Get.lazyPut( + () => DisplayModeController(), + fenix: true, + ); + + Get.lazyPut(() => LanguageController(), fenix: true); + + Get.lazyPut( + () => NotificationsController(), + fenix: true, + ); + + Get.lazyPut(() => PrivacyController(), fenix: true); + + Get.lazyPut(() => SecurityController(), fenix: true); + + Get.lazyPut(() => EmailController(), fenix: true); + + Get.lazyPut(() => TextSizeController(), fenix: true); + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart new file mode 100644 index 0000000..59a11ad --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +abstract class BaseProfileController extends GetxController { + // Common state variables + final RxBool isLoading = false.obs; + final RxString errorMessage = ''.obs; + final RxBool hasChanges = false.obs; + + // Form key for validation + final formKey = GlobalKey(); + + // Loading state management + void setLoading(bool loading) { + isLoading.value = loading; + } + + // Error handling + void setError(String message) { + errorMessage.value = message; + } + + // Clear error + void clearError() { + errorMessage.value = ''; + } + + // Show error dialog + void showError(String title, String message) { + Get.snackbar( + title, + message, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 4), + backgroundColor: Colors.red.withOpacity(0.8), + colorText: Colors.white, + ); + } + + // Show success message + void showSuccess(String title, String message) { + Get.snackbar( + title, + message, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + backgroundColor: Colors.green.withOpacity(0.8), + colorText: Colors.white, + ); + } + + // Common validation methods + String? validateRequiredField(String? value, String fieldName) { + if (value == null || value.isEmpty) { + return '$fieldName is required'; + } + return null; + } + + String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Email is required'; + } + if (!GetUtils.isEmail(value)) { + return 'Enter a valid email address'; + } + return null; + } + + String? validatePhone(String? value) { + if (value == null || value.isEmpty) { + return 'Phone number is required'; + } + if (!GetUtils.isPhoneNumber(value)) { + return 'Enter a valid phone number'; + } + return null; + } + + String? validateName(String? value) { + if (value == null || value.isEmpty) { + return 'Name is required'; + } + if (value.length < 2) { + return 'Name is too short'; + } + return null; + } + + // Abstract methods to be implemented by subclasses + Future saveChanges(); + void discardChanges(); + Future loadData(); +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart new file mode 100644 index 0000000..dab46a6 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; +import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/profile_controller.dart'; + +class OfficerProfileController extends BaseProfileController { + // Use find to get previously registered repository + final _officerRepository = Get.find(); + + // Observable state variables + final Rx officer = Rx(null); + final RxString unitName = ''.obs; + final RxString patrolUnitName = ''.obs; + final RxBool isValidOfficer = true.obs; // Track if officer profile is valid + final RxBool isEditMode = false.obs; + + // Form controllers for edit mode + late TextEditingController rankController; + late TextEditingController positionController; + late TextEditingController nrpController; + late TextEditingController phoneController; + + // Original officer data for change tracking + OfficerModel? originalOfficer; + + @override + void onInit() { + super.onInit(); + + // Initialize text controllers + rankController = TextEditingController(); + positionController = TextEditingController(); + nrpController = TextEditingController(); + phoneController = TextEditingController(); + + loadData(); + } + + @override + void onClose() { + rankController.dispose(); + positionController.dispose(); + nrpController.dispose(); + phoneController.dispose(); + super.onClose(); + } + + // Load officer data + @override + Future loadData() async { + await loadOfficerData(); + if (isEditMode.value) { + _loadOfficerDataToForm(); + } + } + + // Load officer data + Future loadOfficerData() async { + try { + setLoading(true); + + // Get officer data from profile controller if available + final profileController = Get.find(); + if (profileController.officer.value != null) { + officer.value = profileController.officer.value; + originalOfficer = profileController.officer.value; + } else { + // Fetch officer data directly + final officerData = await _officerRepository.getOfficerData(); + officer.value = officerData; + originalOfficer = officerData; + } + + // Additional metadata could be fetched here, like unit name + // This would require additional repository methods + + if (officer.value != null) { + // Example: You might fetch unit name from another repository + // unitName.value = await _unitsRepository.getUnitName(officer.value!.unitId); + unitName.value = officer.value?.unitId ?? 'Unknown Unit'; + patrolUnitName.value = officer.value?.patrolUnitId ?? 'No Patrol Unit'; + } + + isValidOfficer.value = _validateOfficerProfile(); + } catch (e) { + setError('Failed to load officer data: ${e.toString()}'); + isValidOfficer.value = false; + } finally { + setLoading(false); + } + } + + // Toggle edit mode + void toggleEditMode() { + isEditMode.value = !isEditMode.value; + if (isEditMode.value) { + _loadOfficerDataToForm(); + } else { + hasChanges.value = false; + } + } + + // Load officer data to form controllers + void _loadOfficerDataToForm() { + if (officer.value != null) { + rankController.text = officer.value!.rank ?? ''; + positionController.text = officer.value!.position ?? ''; + nrpController.text = officer.value!.nrp?.toString() ?? ''; + phoneController.text = officer.value!.phone ?? ''; + } + + // Setup listeners + _setupTextChangeListeners(); + } + + // Set up listeners to track changes + void _setupTextChangeListeners() { + void onTextChanged() { + checkForChanges(); + } + + rankController.addListener(onTextChanged); + positionController.addListener(onTextChanged); + nrpController.addListener(onTextChanged); + phoneController.addListener(onTextChanged); + } + + // Check for changes in form fields + void checkForChanges() { + if (officer.value == null) { + hasChanges.value = false; + return; + } + + hasChanges.value = + rankController.text != (officer.value!.rank ?? '') || + positionController.text != (officer.value!.position ?? '') || + nrpController.text != (officer.value!.nrp?.toString() ?? '') || + phoneController.text != (officer.value!.phone ?? ''); + } + + // Validate officer profile has required fields + bool _validateOfficerProfile() { + if (officer.value == null) return false; + + // Check required fields + return officer.value!.nrp != null && + officer.value!.name != null && + officer.value!.unitId != null; + } + + // Refresh officer data + Future refreshOfficerData() async { + await loadOfficerData(); + } + + // Get officer by ID + Future getOfficerById(String officerId) async { + try { + setLoading(true); + return await _officerRepository.getOfficerById(officerId); + } catch (e) { + setError('Failed to get officer: ${e.toString()}'); + return null; + } finally { + setLoading(false); + } + } + + // Validate NRP for officers + String? validateNRP(String? value) { + if (value == null || value.isEmpty) { + return 'NRP is required'; + } + return null; + } + + // Save officer changes + @override + Future saveChanges() async { + if (!formKey.currentState!.validate()) { + return false; + } + + try { + setLoading(true); + + if (officer.value != null) { + dynamic nrpValue = nrpController.text.trim(); + // Try to convert to int if it's a number + if (int.tryParse(nrpValue) != null) { + nrpValue = int.parse(nrpValue); + } + + // Create updated officer model + final updatedOfficer = officer.value!.copyWith( + rank: rankController.text.trim(), + position: positionController.text.trim(), + nrp: nrpValue, + phone: phoneController.text.trim(), + ); + + // Save to repository + final result = await _officerRepository.updateOfficer(updatedOfficer); + if (result != null) { + officer.value = result; + originalOfficer = result; + } + + await refreshOfficerData(); + showSuccess('Success', 'Officer profile updated successfully'); + hasChanges.value = false; + isEditMode.value = false; + return true; + } else { + showError('Error', 'No officer profile to update'); + return false; + } + } catch (e) { + setError('Failed to save changes: ${e.toString()}'); + showError( + 'Error', + 'Could not save officer profile changes. ${e.toString()}', + ); + return false; + } finally { + setLoading(false); + } + } + + // Discard changes + @override + void discardChanges() { + _loadOfficerDataToForm(); + hasChanges.value = false; + isEditMode.value = false; + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/profile_binding.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/profile_binding.dart new file mode 100644 index 0000000..0ca3d75 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/profile_binding.dart @@ -0,0 +1,28 @@ +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart'; +import 'package:sigap/src/features/personalization/data/repositories/profile_repository.dart'; +import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/officer_profile_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/profile_controller.dart'; + +class ProfileBinding extends Bindings { + @override + void dependencies() { + // Register repositories if they aren't already + if (!Get.isRegistered()) { + Get.lazyPut(() => UserRepository()); + } + + if (!Get.isRegistered()) { + Get.lazyPut(() => ProfileRepository()); + } + + if (!Get.isRegistered()) { + Get.lazyPut(() => OfficerRepository()); + } + + // Register controllers + Get.lazyPut(() => ProfileController(), fenix: true); + Get.lazyPut(() => OfficerProfileController(), fenix: true); + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/profile_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/profile_controller.dart new file mode 100644 index 0000000..fc4e5be --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/profile/profile_controller.dart @@ -0,0 +1,321 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/users_model.dart'; +import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart'; +import 'package:sigap/src/features/personalization/data/repositories/profile_repository.dart'; +import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/profile/base_profile_controller.dart'; + +class ProfileController extends BaseProfileController { + // Repositories + final _userRepository = Get.find(); + final _profileRepository = Get.find(); + final _officerRepository = Get.find(); + + // Observable state variables + final Rx user = Rx(null); + final Rx profile = Rx(null); + final Rx officer = Rx(null); + final RxBool isOfficer = false.obs; + final RxBool isInitialized = false.obs; + final RxBool isEditMode = false.obs; + + // Form controllers for edit mode + late TextEditingController firstNameController; + late TextEditingController lastNameController; + late TextEditingController bioController; + late TextEditingController phoneController; + late TextEditingController birthPlaceController; + late Rx birthDate = Rx(null); + + // Original data for comparison + ProfileModel? originalProfile; + + @override + void onInit() { + super.onInit(); + + // Initialize text controllers + firstNameController = TextEditingController(); + lastNameController = TextEditingController(); + bioController = TextEditingController(); + phoneController = TextEditingController(); + birthPlaceController = TextEditingController(); + + // Load initial data + loadData(); + } + + @override + void onClose() { + // Dispose controllers + firstNameController.dispose(); + lastNameController.dispose(); + bioController.dispose(); + phoneController.dispose(); + birthPlaceController.dispose(); + super.onClose(); + } + + // Load data from repositories + @override + Future loadData() async { + await fetchUserProfile(); + if (isEditMode.value) { + _loadDataToFormFields(); + } + } + + // Reload profile data + Future refreshProfile() async { + await fetchUserProfile(); + } + + // Fetch the user profile data + Future fetchUserProfile() async { + try { + setLoading(true); + clearError(); + + // Check if user is an officer + isOfficer.value = await _userRepository.isCurrentUserOfficer(); + + // Fetch the appropriate data based on the user type + if (isOfficer.value) { + await fetchOfficerData(); + } else { + await fetchRegularUserData(); + } + + isInitialized.value = true; + } catch (e) { + setError('Failed to load profile: ${e.toString()}'); + showError( + 'Profile Error', + 'Could not load profile data. ${e.toString()}', + ); + } finally { + setLoading(false); + } + } + + // Fetch data for regular users + Future fetchRegularUserData() async { + try { + // Get user and profile data + final userData = await _userRepository.getCurrentUserData(); + user.value = userData; + + // If profile exists in user data, use it + if (userData.profile != null) { + profile.value = userData.profile; + originalProfile = userData.profile; + } else { + // Otherwise, fetch profile separately + final profileData = await _profileRepository.getProfileData(); + profile.value = profileData; + originalProfile = profileData; + } + } catch (e) { + setError('Failed to load user data: ${e.toString()}'); + } + } + + // Fetch data for officers + Future fetchOfficerData() async { + try { + // Get officer data + final officerData = await _officerRepository.getOfficerData(); + officer.value = officerData; + + // Get additional user data + final userData = await _userRepository.getCurrentUserData(); + user.value = userData; + profile.value = userData.profile; + originalProfile = userData.profile; + } catch (e) { + setError('Failed to load officer data: ${e.toString()}'); + } + } + + // Toggle edit mode + void toggleEditMode() { + isEditMode.value = !isEditMode.value; + if (isEditMode.value) { + _loadDataToFormFields(); + } else { + hasChanges.value = false; + } + } + + // Load current data into form fields + void _loadDataToFormFields() { + if (profile.value != null) { + firstNameController.text = profile.value!.firstName ?? ''; + lastNameController.text = profile.value!.lastName ?? ''; + bioController.text = profile.value!.bio ?? ''; + birthPlaceController.text = profile.value!.placeOfBirth ?? ''; + birthDate.value = profile.value!.birthDate; + } + + if (user.value != null) { + phoneController.text = user.value!.phone ?? ''; + } + + // Setup listeners to track changes + _setupTextChangeListeners(); + } + + // Setup listeners for text controllers to track changes + void _setupTextChangeListeners() { + void onTextChanged() { + _checkForChanges(); + } + + firstNameController.addListener(onTextChanged); + lastNameController.addListener(onTextChanged); + bioController.addListener(onTextChanged); + phoneController.addListener(onTextChanged); + birthPlaceController.addListener(onTextChanged); + } + + // Check for changes in form data + void _checkForChanges() { + if (profile.value == null) { + hasChanges.value = false; + return; + } + + // Check profile fields + bool profileChanged = + firstNameController.text != (profile.value!.firstName ?? '') || + lastNameController.text != (profile.value!.lastName ?? '') || + bioController.text != (profile.value!.bio ?? '') || + birthPlaceController.text != (profile.value!.placeOfBirth ?? '') || + birthDate.value != profile.value!.birthDate; + + // Check phone + bool phoneChanged = false; + if (user.value != null) { + phoneChanged = phoneController.text != (user.value!.phone ?? ''); + } + + hasChanges.value = profileChanged || phoneChanged; + } + + // Set birth date + void setBirthDate(DateTime? date) { + birthDate.value = date; + _checkForChanges(); + } + + // Fetch profile by user ID + Future fetchProfileByUserId(String userId) async { + try { + setLoading(true); + clearError(); + + // Get user data + final userData = await _userRepository.getUserById(userId); + user.value = userData; + profile.value = userData.profile; + originalProfile = userData.profile; + + // Check if user is an officer + final isUserOfficer = userData.isOfficer; + isOfficer.value = isUserOfficer; + + // If user is an officer, fetch officer data + if (isUserOfficer) { + try { + final officerData = await _officerRepository.getOfficerById(userId); + officer.value = officerData; + } catch (e) { + // Handle case when officer data doesn't exist yet + officer.value = null; + } + } + } catch (e) { + setError('Failed to load profile: ${e.toString()}'); + showError( + 'Profile Error', + 'Could not load user profile. ${e.toString()}', + ); + } finally { + setLoading(false); + } + } + + // Upload avatar + Future uploadAvatar(String imagePath) async { + try { + setLoading(true); + final avatarUrl = await _profileRepository.uploadAvatar(imagePath); + await refreshProfile(); // Reload profile to get the new avatar + return avatarUrl; + } catch (e) { + setError('Failed to upload avatar: ${e.toString()}'); + showError( + 'Upload Failed', + 'Could not upload profile picture. ${e.toString()}', + ); + return null; + } finally { + setLoading(false); + } + } + + // Save changes + @override + Future saveChanges() async { + if (!formKey.currentState!.validate()) { + return false; + } + + try { + setLoading(true); + + // Update profile data + if (profile.value != null) { + final updatedProfile = profile.value!.copyWith( + firstName: firstNameController.text.trim(), + lastName: lastNameController.text.trim(), + bio: bioController.text.trim(), + placeOfBirth: birthPlaceController.text.trim(), + birthDate: birthDate.value, + ); + + await _profileRepository.updateProfile(updatedProfile); + profile.value = updatedProfile; + } + + // Update phone if changed + final phone = phoneController.text.trim(); + if (user.value != null && phone != user.value!.phone) { + await _userRepository.updateUserPhone(phone); + } + + await refreshProfile(); + hasChanges.value = false; + isEditMode.value = false; + showSuccess('Success', 'Profile updated successfully'); + return true; + } catch (e) { + setError('Failed to save changes: ${e.toString()}'); + showError('Error', 'Could not save profile changes. ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + // Discard changes + @override + void discardChanges() { + _loadDataToFormFields(); + hasChanges.value = false; + isEditMode.value = false; + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart new file mode 100644 index 0000000..d1ec528 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +abstract class BaseSettingsController extends GetxController { + final RxBool isLoading = false.obs; + final RxString errorMessage = ''.obs; + + // Method to handle loading state + void setLoading(bool loading) { + isLoading.value = loading; + } + + // Method to handle errors + void setError(String message) { + errorMessage.value = message; + } + + // Method to clear errors + void clearError() { + errorMessage.value = ''; + } + + // Save settings to storage (to be implemented by subclasses) + Future saveSettings(); + + // Load settings from storage (to be implemented by subclasses) + Future loadSettings(); +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart new file mode 100644 index 0000000..36161cf --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class DisplayModeController extends BaseSettingsController { + // Available theme modes + final List themeOptions = [ + 'System default', + 'Light mode', + 'Dark mode', + ]; + + // Current selected theme mode + final RxString selectedThemeMode = 'System default'.obs; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Change theme mode + void changeThemeMode(String mode) { + if (!themeOptions.contains(mode)) return; + + selectedThemeMode.value = mode; + _applyTheme(mode); + saveSettings(); + } + + // Apply the theme based on selected mode + void _applyTheme(String mode) { + ThemeMode themeMode; + + switch (mode) { + case 'Light mode': + themeMode = ThemeMode.light; + break; + case 'Dark mode': + themeMode = ThemeMode.dark; + break; + default: + themeMode = ThemeMode.system; + } + + Get.changeThemeMode(themeMode); + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save the theme setting to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setString('theme_mode', selectedThemeMode.value); + + return true; + } catch (e) { + setError('Failed to save theme settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load the theme setting from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // final savedTheme = await prefs.getString('theme_mode') ?? 'System default'; + // selectedThemeMode.value = savedTheme; + + // Apply the loaded theme + _applyTheme(selectedThemeMode.value); + } catch (e) { + setError('Failed to load theme settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/email_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/email_controller.dart new file mode 100644 index 0000000..9276a1d --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/email_controller.dart @@ -0,0 +1,119 @@ +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class EmailController extends BaseSettingsController { + // Email settings + final RxString currentEmail = "anitarose@example.com".obs; + final RxBool receiveNewsletter = true.obs; + final RxBool receivePromotions = false.obs; + + // Email verification status + final RxBool isEmailVerified = true.obs; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Toggle email preference + void toggleEmailPreference(String preference, bool value) { + switch (preference) { + case 'receive_newsletter': + receiveNewsletter.value = value; + break; + case 'receive_promotions': + receivePromotions.value = value; + break; + } + + saveSettings(); + } + + // Change primary email + Future changeEmail(String newEmail, String password) async { + try { + setLoading(true); + + // TODO: Implement email change logic with API + // Normally would validate password and update email + + // Simulate API call + await Future.delayed(const Duration(seconds: 1)); + + if (newEmail.isEmpty || !GetUtils.isEmail(newEmail)) { + setError('Please enter a valid email address.'); + return false; + } + + currentEmail.value = newEmail; + isEmailVerified.value = false; // New email needs verification + await saveSettings(); + + return true; + } catch (e) { + setError('Failed to change email: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + // Send verification email + Future sendVerificationEmail() async { + try { + setLoading(true); + + // TODO: Implement send verification email logic with API + + // Simulate API call + await Future.delayed(const Duration(seconds: 1)); + + return true; + } catch (e) { + setError('Failed to send verification email: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save email settings to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setString('current_email', currentEmail.value); + // await prefs.setBool('receive_newsletter', receiveNewsletter.value); + // await prefs.setBool('receive_promotions', receivePromotions.value); + // await prefs.setBool('is_email_verified', isEmailVerified.value); + + return true; + } catch (e) { + setError('Failed to save email settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load email settings from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // currentEmail.value = prefs.getString('current_email') ?? "anitarose@example.com"; + // receiveNewsletter.value = prefs.getBool('receive_newsletter') ?? true; + // receivePromotions.value = prefs.getBool('receive_promotions') ?? false; + // isEmailVerified.value = prefs.getBool('is_email_verified') ?? true; + } catch (e) { + setError('Failed to load email settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/language_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/language_controller.dart new file mode 100644 index 0000000..c88f057 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/language_controller.dart @@ -0,0 +1,82 @@ +import 'dart:ui'; + +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class LanguageController extends BaseSettingsController { + // Available languages with their locale codes + final List> availableLanguages = [ + {'name': 'English (US)', 'code': 'en_US'}, + {'name': 'Bahasa Indonesia', 'code': 'id_ID'}, + {'name': 'Español', 'code': 'es_ES'}, + {'name': 'Français', 'code': 'fr_FR'}, + {'name': '日本語', 'code': 'ja_JP'}, + {'name': '한국어', 'code': 'ko_KR'}, + ]; + + // Current selected language + final RxString selectedLanguage = 'English (US)'.obs; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Change language + void changeLanguage(String language) { + final languageItem = availableLanguages.firstWhere( + (lang) => lang['name'] == language, + orElse: () => {'name': 'English (US)', 'code': 'en_US'}, + ); + + selectedLanguage.value = language; + final String localeCode = languageItem['code'] ?? 'en_US'; + final List localeParts = localeCode.split('_'); + + // Apply the language change + Get.updateLocale( + Locale(localeParts[0], localeParts.length > 1 ? localeParts[1] : ''), + ); + saveSettings(); + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save the language setting to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setString('selected_language', selectedLanguage.value); + + return true; + } catch (e) { + setError('Failed to save language settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load the language setting from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // final savedLanguage = await prefs.getString('selected_language') ?? 'English (US)'; + // selectedLanguage.value = savedLanguage; + + // Apply the loaded language + if (selectedLanguage.value != 'English (US)') { + changeLanguage(selectedLanguage.value); + } + } catch (e) { + setError('Failed to load language settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart new file mode 100644 index 0000000..60d1e48 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart @@ -0,0 +1,81 @@ +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class NotificationsController extends BaseSettingsController { + // Notification settings + final RxBool messagesNotify = true.obs; + final RxBool commentsNotify = true.obs; + final RxBool followersNotify = true.obs; + final RxBool mentionsNotify = false.obs; + final RxBool systemNotify = true.obs; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Toggle a notification setting + void toggleNotification(String type, bool value) { + switch (type) { + case 'messages': + messagesNotify.value = value; + break; + case 'comments': + commentsNotify.value = value; + break; + case 'followers': + followersNotify.value = value; + break; + case 'mentions': + mentionsNotify.value = value; + break; + case 'system': + systemNotify.value = value; + break; + } + + saveSettings(); + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save notification settings to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setBool('notify_messages', messagesNotify.value); + // await prefs.setBool('notify_comments', commentsNotify.value); + // await prefs.setBool('notify_followers', followersNotify.value); + // await prefs.setBool('notify_mentions', mentionsNotify.value); + // await prefs.setBool('notify_system', systemNotify.value); + + return true; + } catch (e) { + setError('Failed to save notification settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load notification settings from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // messagesNotify.value = prefs.getBool('notify_messages') ?? true; + // commentsNotify.value = prefs.getBool('notify_comments') ?? true; + // followersNotify.value = prefs.getBool('notify_followers') ?? true; + // mentionsNotify.value = prefs.getBool('notify_mentions') ?? false; + // systemNotify.value = prefs.getBool('notify_system') ?? true; + } catch (e) { + setError('Failed to load notification settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart new file mode 100644 index 0000000..04f1818 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart @@ -0,0 +1,103 @@ +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class PrivacyController extends BaseSettingsController { + // Privacy settings + final RxBool showActivityStatus = true.obs; + final RxBool allowComments = false.obs; + final RxBool shareUsageData = true.obs; + final RxBool allowPersonalizedAds = false.obs; + + // Profile visibility options + final RxString profileVisibility = 'Public'.obs; + final List visibilityOptions = ['Public', 'Friends', 'Private']; + + // Messaging permissions + final RxString messagingPermission = 'Everyone'.obs; + final List messagingOptions = ['Everyone', 'Friends', 'Nobody']; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Toggle privacy setting + void togglePrivacySetting(String setting, bool value) { + switch (setting) { + case 'activity_status': + showActivityStatus.value = value; + break; + case 'allow_comments': + allowComments.value = value; + break; + case 'share_usage_data': + shareUsageData.value = value; + break; + case 'personalized_ads': + allowPersonalizedAds.value = value; + break; + } + + saveSettings(); + } + + // Change profile visibility + void changeProfileVisibility(String visibility) { + if (visibilityOptions.contains(visibility)) { + profileVisibility.value = visibility; + saveSettings(); + } + } + + // Change messaging permissions + void changeMessagingPermission(String permission) { + if (messagingOptions.contains(permission)) { + messagingPermission.value = permission; + saveSettings(); + } + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save privacy settings to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setBool('show_activity_status', showActivityStatus.value); + // await prefs.setBool('allow_comments', allowComments.value); + // await prefs.setBool('share_usage_data', shareUsageData.value); + // await prefs.setBool('allow_personalized_ads', allowPersonalizedAds.value); + // await prefs.setString('profile_visibility', profileVisibility.value); + // await prefs.setString('messaging_permission', messagingPermission.value); + + return true; + } catch (e) { + setError('Failed to save privacy settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load privacy settings from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // showActivityStatus.value = prefs.getBool('show_activity_status') ?? true; + // allowComments.value = prefs.getBool('allow_comments') ?? false; + // shareUsageData.value = prefs.getBool('share_usage_data') ?? true; + // allowPersonalizedAds.value = prefs.getBool('allow_personalized_ads') ?? false; + // profileVisibility.value = prefs.getString('profile_visibility') ?? 'Public'; + // messagingPermission.value = prefs.getString('messaging_permission') ?? 'Everyone'; + } catch (e) { + setError('Failed to load privacy settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/security_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/security_controller.dart new file mode 100644 index 0000000..06464bd --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/security_controller.dart @@ -0,0 +1,141 @@ +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class SecurityController extends BaseSettingsController { + // Security settings + final RxBool requireBiometric = true.obs; + final RxBool enable2FA = false.obs; + final RxBool sendEmailAlerts = true.obs; + final RxBool sendPushNotification = false.obs; + + // Passcode information + final RxBool hasPasscode = false.obs; + final RxString passcodeLastChanged = ''.obs; + + // 2FA backup codes + final RxList backupCodes = [].obs; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Toggle security setting + void toggleSecuritySetting(String setting, bool value) { + switch (setting) { + case 'require_biometric': + requireBiometric.value = value; + break; + case 'enable_2fa': + enable2FA.value = value; + break; + case 'send_email_alerts': + sendEmailAlerts.value = value; + break; + case 'send_push_notification': + sendPushNotification.value = value; + break; + } + + saveSettings(); + } + + // Generate new backup codes (normally would call an API) + Future generateBackupCodes() async { + try { + setLoading(true); + + // Simulate API call to generate backup codes + await Future.delayed(const Duration(seconds: 1)); + + // Generate random codes for demo purposes + final List codes = List.generate( + 8, + (_) => List.generate( + 6, + (_) => + (0 + (9 - 0) * (DateTime.now().microsecondsSinceEpoch % 10)) + .toString(), + ).join(''), + ); + + backupCodes.value = codes; + return true; + } catch (e) { + setError('Failed to generate backup codes: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + // Change passcode + Future changePasscode( + String currentPasscode, + String newPasscode, + ) async { + try { + setLoading(true); + + // TODO: Implement passcode change logic + // Normally would validate the current passcode and save the new one + + hasPasscode.value = true; + passcodeLastChanged.value = DateTime.now().toIso8601String(); + await saveSettings(); + + return true; + } catch (e) { + setError('Failed to change passcode: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save security settings to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setBool('require_biometric', requireBiometric.value); + // await prefs.setBool('enable_2fa', enable2FA.value); + // await prefs.setBool('send_email_alerts', sendEmailAlerts.value); + // await prefs.setBool('send_push_notification', sendPushNotification.value); + // await prefs.setBool('has_passcode', hasPasscode.value); + // await prefs.setString('passcode_last_changed', passcodeLastChanged.value); + // await prefs.setStringList('backup_codes', backupCodes); + + return true; + } catch (e) { + setError('Failed to save security settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load security settings from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // requireBiometric.value = prefs.getBool('require_biometric') ?? true; + // enable2FA.value = prefs.getBool('enable_2fa') ?? false; + // sendEmailAlerts.value = prefs.getBool('send_email_alerts') ?? true; + // sendPushNotification.value = prefs.getBool('send_push_notification') ?? false; + // hasPasscode.value = prefs.getBool('has_passcode') ?? false; + // passcodeLastChanged.value = prefs.getString('passcode_last_changed') ?? ''; + // backupCodes.value = prefs.getStringList('backup_codes') ?? []; + } catch (e) { + setError('Failed to load security settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/settings_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/settings_controller.dart new file mode 100644 index 0000000..f464a83 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/settings_controller.dart @@ -0,0 +1,116 @@ +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/users_model.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/email_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/language_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/notifications_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/privacy_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/security_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart'; + +class SettingsController extends BaseSettingsController { + // User info + final Rx currentUser = Rx(null); + final Rx userProfile = Rx(null); + + // Current tab index + final RxInt selectedTabIndex = 0.obs; + + // References to sub-controllers + late final DisplayModeController displayModeController; + late final LanguageController languageController; + late final NotificationsController notificationsController; + late final PrivacyController privacyController; + late final SecurityController securityController; + late final EmailController emailController; + late final TextSizeController textSizeController; + + @override + void onInit() { + super.onInit(); + initializeControllers(); + loadSettings(); + } + + void initializeControllers() { + // Initialize all sub-controllers + displayModeController = Get.find(); + languageController = Get.find(); + notificationsController = Get.find(); + privacyController = Get.find(); + securityController = Get.find(); + emailController = Get.find(); + textSizeController = Get.find(); + } + + void changeTab(int index) { + selectedTabIndex.value = index; + } + + // Log out user + Future logout() async { + try { + setLoading(true); + + // TODO: Implement logout logic + // Clear user session, tokens, etc. + + // Simulate API call + await Future.delayed(const Duration(seconds: 1)); + + // Clear user data + currentUser.value = null; + userProfile.value = null; + + return true; + } catch (e) { + setError('Failed to log out: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // Save all sub-controller settings + await displayModeController.saveSettings(); + await languageController.saveSettings(); + await notificationsController.saveSettings(); + await privacyController.saveSettings(); + await securityController.saveSettings(); + await emailController.saveSettings(); + await textSizeController.saveSettings(); + + return true; + } catch (e) { + setError('Failed to save settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load user data + // Normally would fetch from an API or local storage + + // For demo purposes, create a dummy user + // currentUser.value = UserModel(/* ... */); + // userProfile.value = ProfileModel(/* ... */); + } catch (e) { + setError('Failed to load user settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart new file mode 100644 index 0000000..47a421f --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/controllers/settings/text_size_controller.dart @@ -0,0 +1,87 @@ +import 'package:flutter/widgets.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/base_settings_controller.dart'; + +class TextSizeController extends BaseSettingsController { + // Text size settings + final RxDouble textScaleFactor = 1.0.obs; + + // Min and max scale values + final double minTextScale = 0.8; + final double maxTextScale = 1.4; + + // Preset text sizes + final List> textSizePresets = [ + {'label': 'Small', 'scale': 0.8}, + {'label': 'Normal', 'scale': 1.0}, + {'label': 'Large', 'scale': 1.2}, + {'label': 'Extra Large', 'scale': 1.4}, + ]; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + // Change text size + void changeTextSize(double scale) { + if (scale < minTextScale) scale = minTextScale; + if (scale > maxTextScale) scale = maxTextScale; + + textScaleFactor.value = scale; + _applyTextScale(); + saveSettings(); + } + + // Apply text scale to app + void _applyTextScale() { + // In a real app, you would need to update the MediaQuery data to affect the entire app + // This is a simplified version for demonstration + + // MediaQuery approach (would require wrapping MaterialApp with a builder) + // final MediaQueryData mediaQuery = MediaQuery.of(Get.context!); + // final MediaQueryData updatedMediaQuery = mediaQuery.copyWith(textScaleFactor: textScaleFactor.value); + + // Since we can't directly modify MediaQuery here, we'll use this as a placeholder + // The actual implementation would depend on your app's architecture + debugPrint('Applied text scale factor: ${textScaleFactor.value}'); + } + + @override + Future saveSettings() async { + try { + setLoading(true); + + // TODO: Save text size setting to persistent storage + final prefs = Get.find(); // Replace with your storage solution + // await prefs.setDouble('text_scale_factor', textScaleFactor.value); + + return true; + } catch (e) { + setError('Failed to save text size settings: ${e.toString()}'); + return false; + } finally { + setLoading(false); + } + } + + @override + Future loadSettings() async { + try { + setLoading(true); + + // TODO: Load text size setting from persistent storage + final prefs = Get.find(); // Replace with your storage solution + // final savedScale = prefs.getDouble('text_scale_factor') ?? 1.0; + // textScaleFactor.value = savedScale; + + // Apply the loaded text scale + _applyTextScale(); + } catch (e) { + setError('Failed to load text size settings: ${e.toString()}'); + } finally { + setLoading(false); + } + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/profile_screen.dart b/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/profile_screen.dart index e69de29..05b9143 100644 --- a/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/profile_screen.dart +++ b/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/profile_screen.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/users_model.dart'; +import 'package:sigap/src/features/personalization/presentasion/pages/profile/widgets/officer_profile_detail.dart'; +import 'package:sigap/src/features/personalization/presentasion/pages/profile/widgets/user_profile_detail.dart'; + +class ProfileScreen extends StatelessWidget { + final UserModel? user; + final ProfileModel? profile; + final OfficerModel? officer; + final bool isCurrentUser; + + const ProfileScreen({ + super.key, + this.user, + this.profile, + this.officer, + this.isCurrentUser = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenHeight = MediaQuery.of(context).size.height; + + // For demo purposes, using dummy data if no data is provided + final displayName = + profile?.fullName ?? user?.profile?.fullName ?? 'Anita Rose'; + final location = _getLocationString(); + final avatarUrl = profile?.avatar ?? user?.profile?.avatar; + final bio = + profile?.bio ?? + 'I am currently pursuing a major in Management Economics and Finance at the University of Guelph, Ontario, Canada. Please let me know if I can help you in any way'; + + return Scaffold( + backgroundColor: const Color( + 0xFFF2F2F7, + ), // Light gray iOS-like background + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: theme.iconTheme.color), + onPressed: () => Navigator.pop(context), + ), + actions: [ + CircleAvatar( + backgroundColor: theme.primaryColor.withOpacity(0.1), + child: IconButton( + icon: Icon(Icons.person_outline, color: theme.primaryColor), + onPressed: () { + // Show profile options + }, + ), + ), + const SizedBox(width: 16), + ], + ), + body: Stack( + children: [ + // Top section with avatar, name, and location + Column( + children: [ + SizedBox(height: screenHeight * 0.05), + // Profile Avatar + Center( + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: CircleAvatar( + radius: 50, + backgroundColor: Colors.grey.shade300, + backgroundImage: + avatarUrl != null ? NetworkImage(avatarUrl) : null, + child: + avatarUrl == null + ? const Icon( + Icons.person, + size: 50, + color: Colors.white, + ) + : null, + ), + ), + ), + const SizedBox(height: 16), + // Name + Text( + displayName, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + // Location with icon + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.location_on_outlined, + size: 16, + color: theme.hintColor, + ), + const SizedBox(width: 4), + Text( + location, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + ), + ), + ], + ), + ], + ), + + // Bottom sheet with profile details + Positioned( + left: 0, + right: 0, + bottom: 0, + top: screenHeight * 0.32, // Adjust position to show top content + child: Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(25), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + // Center gray line indicator for draggable bottom sheet look + Center( + child: Container( + height: 4, + width: 40, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.3), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + const SizedBox(height: 16), + + // About Section + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('About', style: theme.textTheme.titleLarge), + const SizedBox(height: 8), + Text( + bio, + style: theme.textTheme.bodyMedium?.copyWith( + height: 1.5, + color: theme.textTheme.bodyMedium?.color + ?.withOpacity(0.8), + ), + ), + ], + ), + ), + + // User-specific information + officer != null + ? OfficerProfileDetail(officer: officer!) + : UserProfileDetail(user: user, profile: profile), + ], + ), + ), + ), + ), + ], + ), + ); + } + + String _getLocationString() { + if (officer?.placeOfBirth != null) { + return officer!.placeOfBirth!; + } else if (profile?.placeOfBirth != null) { + return profile!.placeOfBirth!; + } + + // Fallback to address if available + if (profile?.address != null) { + final address = profile!.address!; + final city = address['city']; + final country = address['country']; + + if (city != null && country != null) { + return '$city, $country'; + } else if (city != null) { + return city; + } else if (country != null) { + return country; + } + } + + return 'Ontario, Canada'; // Default fallback + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/widgets/officer_profile_detail.dart b/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/widgets/officer_profile_detail.dart new file mode 100644 index 0000000..e17cc40 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/widgets/officer_profile_detail.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; + +class OfficerProfileDetail extends StatelessWidget { + final OfficerModel officer; + + const OfficerProfileDetail({super.key, required this.officer}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + + Text('Official Information', style: theme.textTheme.titleLarge), + + const SizedBox(height: 16), + + // Officer information + _buildInfoRow( + context, + 'NRP', + officer.nrp?.toString() ?? 'Not Available', + ), + + if (officer.rank != null) + _buildInfoRow(context, 'Rank', officer.rank!), + + if (officer.position != null) + _buildInfoRow(context, 'Position', officer.position!), + + _buildInfoRow(context, 'Email', officer.email ?? 'Not Available'), + + if (officer.phone != null) + _buildInfoRow(context, 'Phone', officer.phone!), + + if (officer.dateOfBirth != null) + _buildInfoRow( + context, + 'Date of Birth', + DateFormat('dd MMMM yyyy').format(officer.dateOfBirth!), + ), + + if (officer.placeOfBirth != null) + _buildInfoRow(context, 'Place of Birth', officer.placeOfBirth!), + + if (officer.unitId != null) + _buildInfoRow(context, 'Unit ID', officer.unitId!), + + if (officer.validUntil != null) + _buildInfoRow( + context, + 'Valid Until', + DateFormat('dd MMMM yyyy').format(officer.validUntil!), + ), + + if (officer.role != null) + _buildInfoRow(context, 'Role', officer.role!.name ?? 'Officer'), + + // Status information if banned + if (officer.isBanned) ...[ + const SizedBox(height: 24), + + Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.red, size: 20), + const SizedBox(width: 8), + Text('Status Information', style: theme.textTheme.titleLarge), + ], + ), + + const SizedBox(height: 16), + + _buildInfoRow(context, 'Status', 'Banned', valueColor: Colors.red), + + _buildInfoRow( + context, + 'Reason', + officer.bannedReason ?? 'No reason provided', + ), + + if (officer.bannedUntil != null) + _buildInfoRow( + context, + 'Banned Until', + DateFormat('dd MMMM yyyy, HH:mm').format(officer.bannedUntil!), + ), + ], + ], + ), + ); + } + + Widget _buildInfoRow( + BuildContext context, + String label, + String value, { + Color? valueColor, + }) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: theme.textTheme.titleMedium?.copyWith(color: valueColor), + ), + const SizedBox(height: 8), + Divider(height: 1, color: theme.dividerColor.withOpacity(0.3)), + ], + ), + ); + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/widgets/user_profile_detail.dart b/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/widgets/user_profile_detail.dart new file mode 100644 index 0000000..19da0a7 --- /dev/null +++ b/sigap-mobile/lib/src/features/personalization/presentasion/pages/profile/widgets/user_profile_detail.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:sigap/src/features/personalization/data/models/models/profile_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/users_model.dart'; + +class UserProfileDetail extends StatelessWidget { + final UserModel? user; + final ProfileModel? profile; + + const UserProfileDetail({super.key, this.user, this.profile}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text('Personal Information', style: theme.textTheme.titleLarge), + const SizedBox(height: 16), + + // User information + if (profile?.nik != null) + _buildInfoRow(context, 'NIK', profile!.nik!), + + _buildInfoRow(context, 'Email', user?.email ?? 'Not Available'), + + if (user?.phone != null) + _buildInfoRow(context, 'Phone', user!.phone!), + + if (profile?.birthDate != null) + _buildInfoRow( + context, + 'Birth Date', + DateFormat('dd MMMM yyyy').format(profile!.birthDate!), + ), + + if (profile?.placeOfBirth != null) + _buildInfoRow(context, 'Place of Birth', profile!.placeOfBirth!), + + if (user?.lastSignInAt != null) + _buildInfoRow( + context, + 'Last Sign In', + DateFormat('dd MMM yyyy, HH:mm').format(user!.lastSignInAt!), + ), + + if (user?.role != null) + _buildInfoRow(context, 'Role', user!.role!.name ?? 'User'), + ], + ), + ); + } + + Widget _buildInfoRow(BuildContext context, String label, String value) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text(value, style: theme.textTheme.titleMedium), + const SizedBox(height: 8), + Divider(height: 1, color: theme.dividerColor.withOpacity(0.3)), + ], + ), + ); + } +} diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/setting_screen.dart b/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/setting_screen.dart index 54d7393..5eec4a4 100644 --- a/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/setting_screen.dart +++ b/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/setting_screen.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/settings_controller.dart'; +import 'package:sigap/src/features/personalization/presentasion/pages/profile/profile_screen.dart'; import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/contact_screen.dart'; import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart'; import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/email_setting.dart'; @@ -19,6 +22,8 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; + // Use find instead of implicit creation with Get.put + final SettingsController _settingsController = Get.find(); @override void initState() { @@ -44,33 +49,45 @@ class _SettingsScreenState extends State ), body: Column( children: [ - // Profile Section - Container( - padding: const EdgeInsets.all(20), - color: theme.scaffoldBackgroundColor, - child: Row( - children: [ - CircleAvatar( - radius: 30, - backgroundColor: theme.hintColor, - child: Icon( - Icons.person, - size: 35, - color: theme.scaffoldBackgroundColor, - ), + // Profile Section - Now clickable + InkWell( + onTap: () { + // Navigate to the ProfileScreen when tapped + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => const ProfileScreen(isCurrentUser: true), ), - const SizedBox(width: 15), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Anita Rose', style: theme.textTheme.titleLarge), - Text('anitarose', style: theme.textTheme.bodySmall), - ], + ); + }, + child: Container( + padding: const EdgeInsets.all(20), + color: theme.scaffoldBackgroundColor, + child: Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: theme.hintColor, + child: Icon( + Icons.person, + size: 35, + color: theme.scaffoldBackgroundColor, + ), ), - ), - Icon(Icons.chevron_right, color: theme.hintColor), - ], + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Anita Rose', style: theme.textTheme.titleLarge), + Text('anitarose', style: theme.textTheme.bodySmall), + ], + ), + ), + Icon(Icons.chevron_right, color: theme.hintColor), + ], + ), ), ), diff --git a/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart b/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart index 5b16d83..306c223 100644 --- a/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart +++ b/sigap-mobile/lib/src/features/personalization/presentasion/pages/settings/widgets/display_mode_setting.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/personalization/presentasion/controllers/settings/display_mode_controller.dart'; import 'package:sigap/src/features/personalization/presentasion/pages/settings/widgets/base_detail_screen.dart'; class DisplayModeSettingsScreen extends StatefulWidget { @@ -10,20 +12,47 @@ class DisplayModeSettingsScreen extends StatefulWidget { } class _DisplayModeSettingsScreenState extends State { - String selectedMode = 'System default'; + // Use GetX controller + final DisplayModeController _controller = Get.find(); @override Widget build(BuildContext context) { return BaseDetailScreen( title: 'Display Mode', - content: ListView( - children: [ - _buildSectionHeader('Theme'), - _buildRadioItem('System default'), - _buildRadioItem('Light mode'), - _buildRadioItem('Dark mode'), - ], - ), + content: Obx(() { + // Show loading indicator if controller is loading + if (_controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + // Show error message if there is one + if (_controller.errorMessage.value.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Error: ${_controller.errorMessage.value}', + style: TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ElevatedButton( + onPressed: () => _controller.loadSettings(), + child: Text('Retry'), + ), + ], + ), + ); + } + + // Show normal content + return ListView( + children: [ + _buildSectionHeader('Theme'), + ..._controller.themeOptions.map((mode) => _buildRadioItem(mode)), + ], + ); + }), ); } @@ -44,6 +73,7 @@ class _DisplayModeSettingsScreenState extends State { Widget _buildRadioItem(String mode) { final theme = Theme.of(context); IconData icon; + switch (mode) { case 'System default': icon = Icons.brightness_auto; @@ -58,30 +88,28 @@ class _DisplayModeSettingsScreenState extends State { icon = Icons.brightness_auto; } - return InkWell( - onTap: () { - setState(() { - selectedMode = mode; - }); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - Icon(icon, size: 24, color: theme.iconTheme.color), - const SizedBox(width: 15), - Expanded(child: Text(mode, style: theme.textTheme.bodyLarge)), - Radio( - value: mode, - groupValue: selectedMode, - activeColor: theme.primaryColor, - onChanged: (value) { - setState(() { - selectedMode = value!; - }); - }, - ), - ], + return Obx( + () => InkWell( + onTap: () => _controller.changeThemeMode(mode), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Icon(icon, size: 24, color: theme.iconTheme.color), + const SizedBox(width: 15), + Expanded(child: Text(mode, style: theme.textTheme.bodyLarge)), + Radio( + value: mode, + groupValue: _controller.selectedThemeMode.value, + activeColor: theme.primaryColor, + onChanged: (value) { + if (value != null) { + _controller.changeThemeMode(value); + } + }, + ), + ], + ), ), ), );