diff --git a/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart b/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart index 760cb183eb..ec266dada4 100644 --- a/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart +++ b/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart @@ -47,51 +47,52 @@ class _LocationSelectionScreenState extends State UserPreferencesModel? userPreferences; @override -void initState() { - super.initState(); - loggy.info('initState called'); + void initState() { + super.initState(); + loggy.info('initState called'); - _initializeUserData(); + _initializeUserData(); - googlePlacesBloc = context.read() - ..add(ResetGooglePlaces()); + googlePlacesBloc = context.read() + ..add(ResetGooglePlaces()); - loggy.info('Checking dashboard state'); - final dashboardBloc = context.read(); - final currentState = dashboardBloc.state; - loggy.info('Current dashboard state: ${currentState.runtimeType}'); + loggy.info('Checking dashboard state'); + final dashboardBloc = context.read(); + final currentState = dashboardBloc.state; + loggy.info('Current dashboard state: ${currentState.runtimeType}'); + + if (currentState is DashboardLoaded) { + loggy.info('Dashboard already loaded, populating measurements'); + if (currentState.response.measurements != null) { + loggy.info( + 'Found ${currentState.response.measurements!.length} measurements in loaded state'); + _populateMeasurements(currentState.response.measurements!); + + // IMPORTANT ADDITION: Pre-select existing locations from the current DashboardState + if (currentState.userPreferences != null && + currentState.userPreferences!.selectedSites.isNotEmpty) { + final existingIds = currentState.userPreferences!.selectedSites + .map((site) => site.id) + .toSet(); - if (currentState is DashboardLoaded) { - loggy.info('Dashboard already loaded, populating measurements'); - if (currentState.response.measurements != null) { - loggy.info( - 'Found ${currentState.response.measurements!.length} measurements in loaded state'); - _populateMeasurements(currentState.response.measurements!); - - // IMPORTANT ADDITION: Pre-select existing locations from the current DashboardState - if (currentState.userPreferences != null && - currentState.userPreferences!.selectedSites.isNotEmpty) { - final existingIds = currentState.userPreferences!.selectedSites - .map((site) => site.id) - .toSet(); - - loggy.info('Pre-selecting ${existingIds.length} existing locations from dashboard state'); + loggy.info( + 'Pre-selecting ${existingIds.length} existing locations from dashboard state'); + setState(() { + selectedLocations = existingIds; + }); + } + } else { + loggy.warning('No measurements in loaded state'); setState(() { - selectedLocations = existingIds; + isLoading = false; + errorMessage = "No measurements available in loaded state"; }); } } else { - loggy.warning('No measurements in loaded state'); - setState(() { - isLoading = false; - errorMessage = "No measurements available in loaded state"; - }); + loggy.info('Dispatching LoadDashboard event'); + dashboardBloc.add(LoadDashboard()); } - } else { - loggy.info('Dispatching LoadDashboard event'); - dashboardBloc.add(LoadDashboard()); } -} Future _initializeUserData() async { try { @@ -174,100 +175,99 @@ void initState() { // File: src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart // Update the _saveSelectedLocations method in the LocationSelectionScreen -Future _saveSelectedLocations() async { - loggy.info( - 'Save button pressed with ${selectedLocations.length} selected locations'); + Future _saveSelectedLocations() async { + loggy.info( + 'Save button pressed with ${selectedLocations.length} selected locations'); + + // Debug token + await AuthHelper.debugToken(); + + // Check auth state from the bloc + final authState = context.read().state; + final isLoggedIn = authState is AuthLoaded; + + loggy.info('Current auth state: ${authState.runtimeType}'); + loggy.info('Is user logged in? $isLoggedIn'); + + if (!isLoggedIn) { + loggy.warning('❌ User not logged in, cannot save'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please log in to save your locations')), + ); + return; + } - // Debug token - await AuthHelper.debugToken(); + // Use enhanced token checker + final isExpired = await TokenDebugger.checkTokenExpiration(); + + if (isExpired) { + loggy.warning('❌ Token is expired, cannot save'); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Your session has expired. Please log in again.'), + duration: const Duration(seconds: 8), + action: SnackBarAction( + label: 'Log In', + onPressed: () { + // Navigate directly to login screen + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const LoginPage(), + ), + (route) => false, + ); + }, + ), + ), + ); + return; + } - // Check auth state from the bloc - final authState = context.read().state; - final isLoggedIn = authState is AuthLoaded; + setState(() { + isSaving = true; + }); - loggy.info('Current auth state: ${authState.runtimeType}'); - loggy.info('Is user logged in? $isLoggedIn'); + try { + // IMPORTANT CHANGE: Instead of creating a new preference, we dispatch + // the UpdateSelectedLocations event to the DashboardBloc, which will + // merge these with existing locations - if (!isLoggedIn) { - loggy.warning('❌ User not logged in, cannot save'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please log in to save your locations')), - ); - return; - } + final dashboardBloc = context.read(); - // Use enhanced token checker - final isExpired = await TokenDebugger.checkTokenExpiration(); - - if (isExpired) { - loggy.warning('❌ Token is expired, cannot save'); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Your session has expired. Please log in again.'), - duration: const Duration(seconds: 8), - action: SnackBarAction( - label: 'Log In', - onPressed: () { - // Navigate directly to login screen - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (context) => const LoginPage(), - ), - (route) => false, - ); - }, - ), - ), - ); - return; - } - - setState(() { - isSaving = true; - }); - - try { - // IMPORTANT CHANGE: Instead of creating a new preference, we dispatch - // the UpdateSelectedLocations event to the DashboardBloc, which will - // merge these with existing locations - - final dashboardBloc = context.read(); - - // Convert the Set to a List - final locationIdsList = selectedLocations.toList(); - - loggy.info('Dispatching UpdateSelectedLocations with ${locationIdsList.length} locations'); - - // Dispatch the event - dashboardBloc.add(UpdateSelectedLocations(locationIdsList)); - - // Show success message - loggy.info('✅ Successfully dispatched update event'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Locations saved successfully')), - ); - - // Return to previous screen with the selected locations - Navigator.pop(context, locationIdsList); - } catch (e) { - loggy.error('❌ Error saving locations: $e'); - loggy.error('Stack trace: ${StackTrace.current}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'An error occurred while saving locations: ${e.toString()}')), - ); - } finally { - if (mounted) { - setState(() { - isSaving = false; - }); - } - } -} + // Convert the Set to a List + final locationIdsList = selectedLocations.toList(); + + loggy.info( + 'Dispatching UpdateSelectedLocations with ${locationIdsList.length} locations'); + // Dispatch the event + dashboardBloc.add(UpdateSelectedLocations(locationIdsList)); + // Show success message + loggy.info('✅ Successfully dispatched update event'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Locations saved successfully')), + ); + + // Return to previous screen with the selected locations + Navigator.pop(context, locationIdsList); + } catch (e) { + loggy.error('❌ Error saving locations: $e'); + loggy.error('Stack trace: ${StackTrace.current}'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'An error occurred while saving locations: ${e.toString()}')), + ); + } finally { + if (mounted) { + setState(() { + isSaving = false; + }); + } + } + } Future _loadUserPreferences(String userId) async { try { diff --git a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart index 07c1d50a60..c2b13b9573 100644 --- a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart +++ b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart @@ -11,10 +11,10 @@ import '../../shared/widgets/loading_widget.dart'; class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { const DashboardAppBar({super.key}); - + @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); - + @override Widget build(BuildContext context) { return AppBar( @@ -34,7 +34,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildThemeToggle(BuildContext context) { final themeBloc = context.read(); return GestureDetector( @@ -48,7 +48,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildUserAvatar(BuildContext context) { return BlocBuilder( builder: (context, authState) { @@ -60,7 +60,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { }, ); } - + Widget _buildGuestAvatar(BuildContext context) { return GestureDetector( onTap: () { @@ -84,12 +84,13 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildUserProfileAvatar(BuildContext context) { return BlocBuilder( builder: (context, userState) { if (userState is UserLoaded) { - String firstName = userState.model.users[0].firstName[0].toUpperCase(); + String firstName = + userState.model.users[0].firstName[0].toUpperCase(); String lastName = userState.model.users[0].lastName[0].toUpperCase(); return GestureDetector( onTap: () { @@ -118,4 +119,4 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { }, ); } -} \ No newline at end of file +} diff --git a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart index 94d682a560..02b4d3d2bf 100644 --- a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart +++ b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart @@ -13,22 +13,19 @@ class DashboardHeader extends StatelessWidget { Widget build(BuildContext context) { return PagePadding( padding: 16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16), - _buildGreeting(context), - Text( - "Today's Air Quality • ${DateFormat.MMMMd().format(DateTime.now())}", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.headlineMedium?.color, - ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 16), + _buildGreeting(context), + Text( + "Today's Air Quality • ${DateFormat.MMMMd().format(DateTime.now())}", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.headlineMedium?.color, ), - SizedBox(height: 16) - ] - ), + ), + SizedBox(height: 16) + ]), ); } @@ -97,4 +94,4 @@ class DashboardHeader extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart index 2ed1712d42..c98c6ae05a 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart @@ -38,7 +38,8 @@ class _EditProfileState extends State { } bool _validateEmail(String email) { - final emailRegExp = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); + final emailRegExp = RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); return emailRegExp.hasMatch(email); } @@ -49,28 +50,28 @@ class _EditProfileState extends State { ); return false; } - + if (_lastNameController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Last name cannot be empty')), ); return false; } - + if (_emailController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Email cannot be empty')), ); return false; } - + if (!_validateEmail(_emailController.text.trim())) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Please enter a valid email address')), ); return false; } - + return true; } @@ -79,7 +80,7 @@ class _EditProfileState extends State { setState(() { _isLoading = false; }); - + _loadingTimeout?.cancel(); _loadingTimeout = null; } @@ -88,7 +89,7 @@ class _EditProfileState extends State { void _updateProfile() { // Only update if form has changed and not already loading if (!_formChanged || _isLoading) return; - + // Validate form fields if (!_validateForm()) return; @@ -149,28 +150,28 @@ class _EditProfileState extends State { return BlocConsumer( listener: (context, state) { print('Current state: $state'); - + if (state is UserUpdateSuccess) { _resetLoadingState(); setState(() { _formChanged = false; }); - + // Load the user profile to ensure UI is up-to-date context.read().add(LoadUser()); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Profile updated successfully'), backgroundColor: Colors.green, ), ); - + // Navigate back after successful update Navigator.of(context).pop(); } else if (state is UserUpdateError) { _resetLoadingState(); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error updating profile: ${state.message}'), @@ -186,7 +187,7 @@ class _EditProfileState extends State { if (state is UserUpdating && !_isLoading) { _isLoading = true; } - + return Scaffold( appBar: AppBar( leading: IconButton( @@ -198,7 +199,8 @@ class _EditProfileState extends State { context: context, builder: (context) => AlertDialog( title: Text('Discard Changes?'), - content: Text('You have unsaved changes. Are you sure you want to go back?'), + content: Text( + 'You have unsaved changes. Are you sure you want to go back?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -240,7 +242,9 @@ class _EditProfileState extends State { : Text( 'Done', style: TextStyle( - color: _formChanged ? Colors.white : Colors.white.withOpacity(0.5), + color: _formChanged + ? Colors.white + : Colors.white.withOpacity(0.5), fontWeight: FontWeight.bold, fontSize: 16.0, ), @@ -353,4 +357,4 @@ class _EditProfileState extends State { _loadingTimeout?.cancel(); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart index 905a3f5689..f3502b408a 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -85,25 +85,24 @@ class _ProfilePageState extends State { borderRadius: BorderRadius.circular(200)), child: Center( - - child: InkWell( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - EditProfile())), - child: Text( - "Edit your profile", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Colors.white, + child: InkWell( + onTap: () => Navigator.of(context) + .push(MaterialPageRoute( + builder: (context) => + EditProfile())), + child: Text( + "Edit your profile", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), ), - ), )), ), SizedBox(width: 8), CircleAvatar( - backgroundColor: - Theme.of(context).highlightColor, + backgroundColor: Theme.of(context) + .highlightColor, radius: 26, child: SvgPicture.asset( "assets/icons/notification.svg")) @@ -138,9 +137,7 @@ class _ProfilePageState extends State { label: "Settings")), ]), Expanded( - child: TabBarView(children: [ - SettingsWidget() - ]), + child: TabBarView(children: [SettingsWidget()]), ) ], )), diff --git a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart index 8b5cdd3e6d..92edb716b1 100644 --- a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart +++ b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart @@ -21,64 +21,63 @@ class UserImpl extends UserRepository { if (userId == null) { throw Exception("User ID not found"); } - + http.Response profileResponse = await createAuthenticatedGetRequest("/api/v2/users/$userId", {}); - + ProfileResponseModel model = profileResponseModelFromJson(profileResponse.body); return model; } -@override -Future updateUserProfile({ - required String firstName, - required String lastName, - required String email, -}) async { - final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); - if (userId == null) { - throw Exception("User ID not found"); - } - - // Prepare the request body with the updated fields - final Map requestBody = { - "firstName": firstName, - "lastName": lastName, - "email": email, - }; - - http.Response updateResponse = await createAuthenticatedPutRequest( - path: "/api/v2/users/$userId", - data: requestBody, - ); - - print("https://api.airqo.net/api/v2/users/$userId"); - print(updateResponse.statusCode); - - // Print the full response body for debugging - print("Full response body: ${updateResponse.body}"); - - try { - final responseBody = json.decode(updateResponse.body); - print("Response structure: ${responseBody.keys}"); - - // Print details of each key to identify null values - responseBody.forEach((key, value) { - print("Key: $key, Value: $value, Type: ${value?.runtimeType}"); - }); - - // Print user object structure if it exists - if (responseBody.containsKey('user') && responseBody['user'] != null) { - print("User object keys: ${(responseBody['user'] as Map).keys}"); + @override + Future updateUserProfile({ + required String firstName, + required String lastName, + required String email, + }) async { + final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); + if (userId == null) { + throw Exception("User ID not found"); + } + + // Prepare the request body with the updated fields + final Map requestBody = { + "firstName": firstName, + "lastName": lastName, + "email": email, + }; + + http.Response updateResponse = await createAuthenticatedPutRequest( + path: "/api/v2/users/$userId", + data: requestBody, + ); + + print("https://api.airqo.net/api/v2/users/$userId"); + print(updateResponse.statusCode); + + // Print the full response body for debugging + print("Full response body: ${updateResponse.body}"); + + try { + final responseBody = json.decode(updateResponse.body); + print("Response structure: ${responseBody.keys}"); + + // Print details of each key to identify null values + responseBody.forEach((key, value) { + print("Key: $key, Value: $value, Type: ${value?.runtimeType}"); + }); + + // Print user object structure if it exists + if (responseBody.containsKey('user') && responseBody['user'] != null) { + print("User object keys: ${(responseBody['user'] as Map).keys}"); + } + + // Try a simpler approach - just reload the profile + return await loadUserProfile(); + } catch (e) { + print("Error parsing update response: $e"); + throw Exception("Failed to update profile: $e"); } - - // Try a simpler approach - just reload the profile - return await loadUserProfile(); - - } catch (e) { - print("Error parsing update response: $e"); - throw Exception("Failed to update profile: $e"); } } -} \ No newline at end of file