feat: Implement language selection feature in staff profile onboarding
This commit is contained in:
@@ -16,7 +16,7 @@ class StaffPaths {
|
|||||||
/// Generate child route based on the given route and parent route
|
/// Generate child route based on the given route and parent route
|
||||||
///
|
///
|
||||||
/// This is useful for creating nested routes within modules.
|
/// 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, '');
|
final String childPath = child.replaceFirst(parent, '');
|
||||||
|
|
||||||
// check if the child path is empty
|
// check if the child path is empty
|
||||||
@@ -107,8 +107,7 @@ class StaffPaths {
|
|||||||
/// Path format: `/worker-main/shift-details/{shiftId}`
|
/// Path format: `/worker-main/shift-details/{shiftId}`
|
||||||
///
|
///
|
||||||
/// Example: `/worker-main/shift-details/shift123`
|
/// Example: `/worker-main/shift-details/shift123`
|
||||||
static String shiftDetails(String shiftId) =>
|
static String shiftDetails(String shiftId) => '$shiftDetailsRoute/$shiftId';
|
||||||
'$shiftDetailsRoute/$shiftId';
|
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// ONBOARDING & PROFILE SECTIONS
|
// ONBOARDING & PROFILE SECTIONS
|
||||||
@@ -117,8 +116,17 @@ class StaffPaths {
|
|||||||
/// Personal information onboarding.
|
/// Personal information onboarding.
|
||||||
///
|
///
|
||||||
/// Collect basic personal information during staff onboarding.
|
/// Collect basic personal information during staff onboarding.
|
||||||
static const String onboardingPersonalInfo =
|
static const String onboardingPersonalInfo = '/worker-main/personal-info/';
|
||||||
'/worker-main/onboarding/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.
|
/// Emergency contact information.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -130,9 +130,6 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
// Support section
|
// Support section
|
||||||
const SupportSection(),
|
const SupportSection(),
|
||||||
|
|
||||||
// Settings section
|
|
||||||
const SettingsSection(),
|
|
||||||
|
|
||||||
// Logout button at the bottom
|
// Logout button at the bottom
|
||||||
const LogoutButton(),
|
const LogoutButton(),
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export 'compliance_section.dart';
|
export 'compliance_section.dart';
|
||||||
export 'finance_section.dart';
|
export 'finance_section.dart';
|
||||||
export 'onboarding_section.dart';
|
export 'onboarding_section.dart';
|
||||||
export 'settings_section.dart';
|
|
||||||
export 'support_section.dart';
|
export 'support_section.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<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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.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.
|
/// A form widget containing all personal information fields.
|
||||||
@@ -77,11 +79,73 @@ class PersonalInfoForm extends StatelessWidget {
|
|||||||
hint: i18n.locations_hint,
|
hint: i18n.locations_hint,
|
||||||
enabled: enabled,
|
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.
|
||||||
/// A label widget for form fields.
|
/// A label widget for form fields.
|
||||||
class _FieldLabel extends StatelessWidget {
|
class _FieldLabel extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import 'data/repositories/personal_info_repository_impl.dart';
|
import 'data/repositories/personal_info_repository_impl.dart';
|
||||||
import 'domain/repositories/personal_info_repository_interface.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 'domain/usecases/update_personal_info_usecase.dart';
|
||||||
import 'presentation/blocs/personal_info_bloc.dart';
|
import 'presentation/blocs/personal_info_bloc.dart';
|
||||||
import 'presentation/pages/personal_info_page.dart';
|
import 'presentation/pages/personal_info_page.dart';
|
||||||
|
import 'presentation/pages/language_selection_page.dart';
|
||||||
|
|
||||||
/// The entry module for the Staff Profile Info feature.
|
/// The entry module for the Staff Profile Info feature.
|
||||||
///
|
///
|
||||||
@@ -23,7 +25,8 @@ class StaffProfileInfoModule extends Module {
|
|||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repository
|
// Repository
|
||||||
i.addLazySingleton<PersonalInfoRepositoryInterface>(
|
i.addLazySingleton<PersonalInfoRepositoryInterface>(
|
||||||
PersonalInfoRepositoryImpl.new);
|
PersonalInfoRepositoryImpl.new,
|
||||||
|
);
|
||||||
|
|
||||||
// Use Cases - delegate business logic to repository
|
// Use Cases - delegate business logic to repository
|
||||||
i.addLazySingleton<GetPersonalInfoUseCase>(
|
i.addLazySingleton<GetPersonalInfoUseCase>(
|
||||||
@@ -45,13 +48,18 @@ class StaffProfileInfoModule extends Module {
|
|||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child(
|
r.child(
|
||||||
'/personal-info',
|
StaffPaths.childRoute(
|
||||||
|
StaffPaths.onboardingPersonalInfo,
|
||||||
|
StaffPaths.onboardingPersonalInfo,
|
||||||
|
),
|
||||||
child: (BuildContext context) => const PersonalInfoPage(),
|
child: (BuildContext context) => const PersonalInfoPage(),
|
||||||
);
|
);
|
||||||
// Alias with trailing slash to be tolerant of external deep links
|
|
||||||
r.child(
|
r.child(
|
||||||
'/personal-info/',
|
StaffPaths.childRoute(
|
||||||
child: (BuildContext context) => const PersonalInfoPage(),
|
StaffPaths.onboardingPersonalInfo,
|
||||||
|
StaffPaths.languageSelection,
|
||||||
|
),
|
||||||
|
child: (BuildContext context) => const LanguageSelectionPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class StaffMainModule extends Module {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
r.module(
|
r.module(
|
||||||
StaffPaths.childRoute(StaffPaths.main, StaffPaths.onboardingPersonalInfo).replaceFirst('/personal-info', ''),
|
StaffPaths.childRoute(StaffPaths.main, StaffPaths.onboardingPersonalInfo),
|
||||||
module: StaffProfileInfoModule(),
|
module: StaffProfileInfoModule(),
|
||||||
);
|
);
|
||||||
r.module(
|
r.module(
|
||||||
|
|||||||
Reference in New Issue
Block a user