feat(settings): implement base settings controller and specific controllers for display mode, email, language, notifications, privacy, security, and text size

- Added BaseSettingsController to manage loading states and error handling.
- Created DisplayModeController to handle theme settings with persistence.
- Implemented EmailController for managing email preferences and verification.
- Developed LanguageController for language selection and persistence.
- Added NotificationsController to manage notification preferences.
- Created PrivacyController for privacy settings management.
- Implemented SecurityController for security settings including 2FA and backup codes.
- Developed TextSizeController to manage text size settings.
- Created SettingsController to aggregate all sub-controllers and manage user settings.
- Added UI components for displaying officer and user profile details.
This commit is contained in:
vergiLgood1 2025-05-27 07:10:04 +07:00
parent 61fb40bc0f
commit c6931619de
22 changed files with 2170 additions and 65 deletions

View File

@ -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<void> 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<void> 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(),
);
}
}

View File

@ -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<NavigationController>();
final theme = Theme.of(context);
return Scaffold(

View File

@ -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<Logger>();
// Get current user ID
String? get currentUserId => SupabaseService.instance.currentUserId;

View File

@ -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<Logger>()) {
Get.lazyPut<Logger>(() => 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>(() => UserRepository(), fenix: true);
Get.lazyPut<ProfileRepository>(() => ProfileRepository(), fenix: true);
Get.lazyPut<OfficerRepository>(() => OfficerRepository(), fenix: true);
}
void _registerProfileControllers() {
// Register profile-related controllers
Get.lazyPut<ProfileController>(() => ProfileController(), fenix: true);
Get.lazyPut<OfficerProfileController>(
() => OfficerProfileController(),
fenix: true,
);
}
void _registerSettingsControllers() {
// Register main settings controller
Get.lazyPut<SettingsController>(() => SettingsController(), fenix: true);
// Register individual settings feature controllers
Get.lazyPut<DisplayModeController>(
() => DisplayModeController(),
fenix: true,
);
Get.lazyPut<LanguageController>(() => LanguageController(), fenix: true);
Get.lazyPut<NotificationsController>(
() => NotificationsController(),
fenix: true,
);
Get.lazyPut<PrivacyController>(() => PrivacyController(), fenix: true);
Get.lazyPut<SecurityController>(() => SecurityController(), fenix: true);
Get.lazyPut<EmailController>(() => EmailController(), fenix: true);
Get.lazyPut<TextSizeController>(() => TextSizeController(), fenix: true);
}
}

View File

@ -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<FormState>();
// 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<bool> saveChanges();
void discardChanges();
Future<void> loadData();
}

View File

@ -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<OfficerRepository>();
// Observable state variables
final Rx<OfficerModel?> officer = Rx<OfficerModel?>(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<void> loadData() async {
await loadOfficerData();
if (isEditMode.value) {
_loadOfficerDataToForm();
}
}
// Load officer data
Future<void> loadOfficerData() async {
try {
setLoading(true);
// Get officer data from profile controller if available
final profileController = Get.find<ProfileController>();
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<void> refreshOfficerData() async {
await loadOfficerData();
}
// Get officer by ID
Future<OfficerModel?> 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<bool> 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;
}
}

View File

@ -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<UserRepository>()) {
Get.lazyPut(() => UserRepository());
}
if (!Get.isRegistered<ProfileRepository>()) {
Get.lazyPut(() => ProfileRepository());
}
if (!Get.isRegistered<OfficerRepository>()) {
Get.lazyPut(() => OfficerRepository());
}
// Register controllers
Get.lazyPut(() => ProfileController(), fenix: true);
Get.lazyPut(() => OfficerProfileController(), fenix: true);
}
}

View File

@ -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<UserRepository>();
final _profileRepository = Get.find<ProfileRepository>();
final _officerRepository = Get.find<OfficerRepository>();
// Observable state variables
final Rx<UserModel?> user = Rx<UserModel?>(null);
final Rx<ProfileModel?> profile = Rx<ProfileModel?>(null);
final Rx<OfficerModel?> officer = Rx<OfficerModel?>(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<DateTime?> birthDate = Rx<DateTime?>(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<void> loadData() async {
await fetchUserProfile();
if (isEditMode.value) {
_loadDataToFormFields();
}
}
// Reload profile data
Future<void> refreshProfile() async {
await fetchUserProfile();
}
// Fetch the user profile data
Future<void> 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<void> 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<void> 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<void> 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<String?> 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<bool> 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;
}
}

View File

@ -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<bool> saveSettings();
// Load settings from storage (to be implemented by subclasses)
Future<void> loadSettings();
}

View File

@ -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<String> 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<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save the theme setting to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load the theme setting from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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<bool> 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<bool> 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<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save email settings to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load email settings from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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<Map<String, String>> 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<String> localeParts = localeCode.split('_');
// Apply the language change
Get.updateLocale(
Locale(localeParts[0], localeParts.length > 1 ? localeParts[1] : ''),
);
saveSettings();
}
@override
Future<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save the language setting to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load the language setting from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save notification settings to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load notification settings from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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<String> visibilityOptions = ['Public', 'Friends', 'Private'];
// Messaging permissions
final RxString messagingPermission = 'Everyone'.obs;
final List<String> 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<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save privacy settings to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load privacy settings from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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<String> backupCodes = <String>[].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<bool> 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<String> 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<bool> 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<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save security settings to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load security settings from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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<UserModel?> currentUser = Rx<UserModel?>(null);
final Rx<ProfileModel?> userProfile = Rx<ProfileModel?>(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<DisplayModeController>();
languageController = Get.find<LanguageController>();
notificationsController = Get.find<NotificationsController>();
privacyController = Get.find<PrivacyController>();
securityController = Get.find<SecurityController>();
emailController = Get.find<EmailController>();
textSizeController = Get.find<TextSizeController>();
}
void changeTab(int index) {
selectedTabIndex.value = index;
}
// Log out user
Future<bool> 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<bool> 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<void> 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);
}
}
}

View File

@ -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<Map<String, dynamic>> 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<bool> saveSettings() async {
try {
setLoading(true);
// TODO: Save text size setting to persistent storage
final prefs = Get.find<dynamic>(); // 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<void> loadSettings() async {
try {
setLoading(true);
// TODO: Load text size setting from persistent storage
final prefs = Get.find<dynamic>(); // 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);
}
}
}

View File

@ -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
}
}

View File

@ -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)),
],
),
);
}
}

View File

@ -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)),
],
),
);
}
}

View File

@ -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<SettingsScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
// Use find instead of implicit creation with Get.put
final SettingsController _settingsController = Get.find<SettingsController>();
@override
void initState() {
@ -44,33 +49,45 @@ class _SettingsScreenState extends State<SettingsScreen>
),
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),
],
),
),
),

View File

@ -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<DisplayModeSettingsScreen> {
String selectedMode = 'System default';
// Use GetX controller
final DisplayModeController _controller = Get.find<DisplayModeController>();
@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<DisplayModeSettingsScreen> {
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<DisplayModeSettingsScreen> {
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<String>(
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<String>(
value: mode,
groupValue: _controller.selectedThemeMode.value,
activeColor: theme.primaryColor,
onChanged: (value) {
if (value != null) {
_controller.changeThemeMode(value);
}
},
),
],
),
),
),
);