feat: Implement language selection feature in staff profile onboarding

This commit is contained in:
Achintha Isuru
2026-02-19 15:45:24 -05:00
parent 5cf0c91ebe
commit 4d935cd80c
7 changed files with 209 additions and 19 deletions

View File

@@ -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.
///

View File

@@ -130,9 +130,6 @@ class StaffProfilePage extends StatelessWidget {
// Support section
const SupportSection(),
// Settings section
const SettingsSection(),
// Logout button at the bottom
const LogoutButton(),

View File

@@ -1,5 +1,5 @@
export 'compliance_section.dart';
export 'finance_section.dart';
export 'onboarding_section.dart';
export 'settings_section.dart';
export 'support_section.dart';

View File

@@ -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<LanguageSelectionPage> createState() => _LanguageSelectionPageState();
}
class _LanguageSelectionPageState extends State<LanguageSelectionPage> {
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<LocaleBloc, LocaleState>(
builder: (BuildContext context, LocaleState state) {
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
_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<LocaleBloc>().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: <Widget>[
Text(
label,
style: isSelected
? UiTypography.body1b.copyWith(color: UiColors.primary)
: UiTypography.body1r,
),
if (isSelected) const Icon(UiIcons.check, color: UiColors.primary),
],
),
),
);
}
}

View File

@@ -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: <Widget>[
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 {

View File

@@ -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<PersonalInfoRepositoryInterface>(
PersonalInfoRepositoryImpl.new);
PersonalInfoRepositoryImpl.new,
);
// Use Cases - delegate business logic to repository
i.addLazySingleton<GetPersonalInfoUseCase>(
@@ -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(),
);
}
}

View File

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