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:
parent
61fb40bc0f
commit
c6931619de
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,8 +49,19 @@ class _SettingsScreenState extends State<SettingsScreen>
|
|||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Profile Section
|
||||
Container(
|
||||
// Profile Section - Now clickable
|
||||
InkWell(
|
||||
onTap: () {
|
||||
// Navigate to the ProfileScreen when tapped
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => const ProfileScreen(isCurrentUser: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
child: Row(
|
||||
|
@ -73,6 +89,7 @@ class _SettingsScreenState extends State<SettingsScreen>
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Tab Bar
|
||||
Container(
|
||||
|
|
|
@ -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,23 +12,50 @@ 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(
|
||||
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: [
|
||||
_buildSectionHeader('Theme'),
|
||||
_buildRadioItem('System default'),
|
||||
_buildRadioItem('Light mode'),
|
||||
_buildRadioItem('Dark mode'),
|
||||
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)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
|
@ -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,12 +88,9 @@ class _DisplayModeSettingsScreenState extends State<DisplayModeSettingsScreen> {
|
|||
icon = Icons.brightness_auto;
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedMode = mode;
|
||||
});
|
||||
},
|
||||
return Obx(
|
||||
() => InkWell(
|
||||
onTap: () => _controller.changeThemeMode(mode),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Row(
|
||||
|
@ -73,17 +100,18 @@ class _DisplayModeSettingsScreenState extends State<DisplayModeSettingsScreen> {
|
|||
Expanded(child: Text(mode, style: theme.textTheme.bodyLarge)),
|
||||
Radio<String>(
|
||||
value: mode,
|
||||
groupValue: selectedMode,
|
||||
groupValue: _controller.selectedThemeMode.value,
|
||||
activeColor: theme.primaryColor,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedMode = value!;
|
||||
});
|
||||
if (value != null) {
|
||||
_controller.changeThemeMode(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue