From 4d935cd80c698161b55ebef336684ff81dcbb007 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 19 Feb 2026 15:45:24 -0500 Subject: [PATCH] feat: Implement language selection feature in staff profile onboarding --- .../lib/src/routing/staff/route_paths.dart | 26 ++-- .../pages/staff_profile_page.dart | 3 - .../presentation/widgets/sections/index.dart | 2 +- .../pages/language_selection_page.dart | 113 ++++++++++++++++++ .../widgets/personal_info_form.dart | 64 ++++++++++ .../lib/src/staff_profile_info_module.dart | 18 ++- .../staff_main/lib/src/staff_main_module.dart | 2 +- 7 files changed, 209 insertions(+), 19 deletions(-) create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/language_selection_page.dart diff --git a/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart b/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart index 97badf3c..bcb0a472 100644 --- a/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart +++ b/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart @@ -16,14 +16,14 @@ class StaffPaths { /// Generate child route based on the given route and parent route /// /// This is useful for creating nested routes within modules. - static String childRoute(String parent, String child) { + static String childRoute(String parent, String child) { final String childPath = child.replaceFirst(parent, ''); - + // check if the child path is empty if (childPath.isEmpty) { return '/'; - } - + } + // ensure the child path starts with a '/' if (!childPath.startsWith('/')) { return '/$childPath'; @@ -31,7 +31,7 @@ class StaffPaths { return childPath; } - + // ========================================================================== // AUTHENTICATION // ========================================================================== @@ -107,8 +107,7 @@ class StaffPaths { /// Path format: `/worker-main/shift-details/{shiftId}` /// /// Example: `/worker-main/shift-details/shift123` - static String shiftDetails(String shiftId) => - '$shiftDetailsRoute/$shiftId'; + static String shiftDetails(String shiftId) => '$shiftDetailsRoute/$shiftId'; // ========================================================================== // ONBOARDING & PROFILE SECTIONS @@ -117,8 +116,17 @@ class StaffPaths { /// Personal information onboarding. /// /// Collect basic personal information during staff onboarding. - static const String onboardingPersonalInfo = - '/worker-main/onboarding/personal-info/'; + static const String onboardingPersonalInfo = '/worker-main/personal-info/'; + + // ========================================================================== + // PERSONAL INFORMATION & PREFERENCES + // ========================================================================== + + /// Language selection page. + /// + /// Allows staff to select their preferred language for the app interface. + static const String languageSelection = + '/worker-main/personal-info/language-selection/'; /// Emergency contact information. /// diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart index 23dbc84c..49767da9 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart @@ -130,9 +130,6 @@ class StaffProfilePage extends StatelessWidget { // Support section const SupportSection(), - // Settings section - const SettingsSection(), - // Logout button at the bottom const LogoutButton(), diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/index.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/index.dart index 967a4dac..6295bcba 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/index.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/sections/index.dart @@ -1,5 +1,5 @@ export 'compliance_section.dart'; export 'finance_section.dart'; export 'onboarding_section.dart'; -export 'settings_section.dart'; export 'support_section.dart'; + diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/language_selection_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/language_selection_page.dart new file mode 100644 index 00000000..3c157413 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/language_selection_page.dart @@ -0,0 +1,113 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +/// Language selection page for staff profile. +/// +/// Displays available languages and allows the user to select their preferred +/// language. Changes are applied immediately via [LocaleBloc] and persisted. +/// Shows a snackbar when the language is successfully changed. +class LanguageSelectionPage extends StatefulWidget { + /// Creates a [LanguageSelectionPage]. + const LanguageSelectionPage({super.key}); + + @override + State createState() => _LanguageSelectionPageState(); +} + +class _LanguageSelectionPageState extends State { + void _showLanguageChangedSnackbar(String languageName) { + UiSnackbar.show( + context, + message: 'Language changed to $languageName', + type: UiSnackbarType.success, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: UiAppBar( + title: 'Select Language', + showBackButton: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container(color: UiColors.border, height: 1.0), + ), + ), + body: SafeArea( + child: BlocBuilder( + builder: (BuildContext context, LocaleState state) { + return ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + _buildLanguageOption( + context, + label: 'English', + locale: AppLocale.en, + ), + const SizedBox(height: UiConstants.space4), + _buildLanguageOption( + context, + label: 'Español', + locale: AppLocale.es, + ), + ], + ); + }, + ), + ), + ); + } + + Widget _buildLanguageOption( + BuildContext context, { + required String label, + required AppLocale locale, + }) { + // Check if this option is currently selected. + final AppLocale currentLocale = LocaleSettings.currentLocale; + final bool isSelected = currentLocale == locale; + + return InkWell( + onTap: () { + // Only proceed if selecting a different language + if (currentLocale != locale) { + Modular.get().add(ChangeLocale(locale.flutterLocale)); + _showLanguageChangedSnackbar(label); + } + }, + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + horizontal: UiConstants.space4, + ), + decoration: BoxDecoration( + color: isSelected + ? UiColors.primary.withValues(alpha: 0.1) + : UiColors.background, + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + border: Border.all( + color: isSelected ? UiColors.primary : UiColors.border, + width: isSelected ? 2 : 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: isSelected + ? UiTypography.body1b.copyWith(color: UiColors.primary) + : UiTypography.body1r, + ), + if (isSelected) const Icon(UiIcons.check, color: UiColors.primary), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart index 6ae1fc46..06f145fb 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; /// A form widget containing all personal information fields. @@ -77,11 +79,73 @@ class PersonalInfoForm extends StatelessWidget { hint: i18n.locations_hint, enabled: enabled, ), + const SizedBox(height: UiConstants.space4), + + _FieldLabel(text: 'Language'), + const SizedBox(height: UiConstants.space2), + _LanguageSelector( + enabled: enabled, + ), ], ); } } +/// A language selector widget that displays the current language and navigates to language selection page. +class _LanguageSelector extends StatelessWidget { + const _LanguageSelector({ + this.enabled = true, + }); + + final bool enabled; + + String _getLanguageLabel(AppLocale locale) { + switch (locale) { + case AppLocale.en: + return 'English'; + case AppLocale.es: + return 'Español'; + } + } + + @override + Widget build(BuildContext context) { + final AppLocale currentLocale = LocaleSettings.currentLocale; + final String currentLanguage = _getLanguageLabel(currentLocale); + + return GestureDetector( + onTap: enabled + ? () => Modular.to.pushNamed(StaffPaths.languageSelection) + : null, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + border: Border.all(color: UiColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + currentLanguage, + style: UiTypography.body2r.textPrimary, + ), + Icon( + UiIcons.chevronRight, + color: UiColors.textSecondary, + ), + ], + ), + ), + ); + } +} + /// A label widget for form fields. /// A label widget for form fields. class _FieldLabel extends StatelessWidget { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart index 47c80748..f949fa72 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; import 'data/repositories/personal_info_repository_impl.dart'; import 'domain/repositories/personal_info_repository_interface.dart'; @@ -7,6 +8,7 @@ import 'domain/usecases/get_personal_info_usecase.dart'; import 'domain/usecases/update_personal_info_usecase.dart'; import 'presentation/blocs/personal_info_bloc.dart'; import 'presentation/pages/personal_info_page.dart'; +import 'presentation/pages/language_selection_page.dart'; /// The entry module for the Staff Profile Info feature. /// @@ -23,7 +25,8 @@ class StaffProfileInfoModule extends Module { void binds(Injector i) { // Repository i.addLazySingleton( - PersonalInfoRepositoryImpl.new); + PersonalInfoRepositoryImpl.new, + ); // Use Cases - delegate business logic to repository i.addLazySingleton( @@ -45,13 +48,18 @@ class StaffProfileInfoModule extends Module { @override void routes(RouteManager r) { r.child( - '/personal-info', + StaffPaths.childRoute( + StaffPaths.onboardingPersonalInfo, + StaffPaths.onboardingPersonalInfo, + ), child: (BuildContext context) => const PersonalInfoPage(), ); - // Alias with trailing slash to be tolerant of external deep links r.child( - '/personal-info/', - child: (BuildContext context) => const PersonalInfoPage(), + StaffPaths.childRoute( + StaffPaths.onboardingPersonalInfo, + StaffPaths.languageSelection, + ), + child: (BuildContext context) => const LanguageSelectionPage(), ); } } diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index 0fb79b75..21493654 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -73,7 +73,7 @@ class StaffMainModule extends Module { ], ); r.module( - StaffPaths.childRoute(StaffPaths.main, StaffPaths.onboardingPersonalInfo).replaceFirst('/personal-info', ''), + StaffPaths.childRoute(StaffPaths.main, StaffPaths.onboardingPersonalInfo), module: StaffProfileInfoModule(), ); r.module(