From eace8a66afa788e422aa7a08c235f179cd741380 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Wed, 21 Jan 2026 19:37:34 -0500 Subject: [PATCH] feat: Implement client settings and profile management feature with sign-out functionality. --- apps/apps/client/lib/main.dart | 7 + apps/apps/client/pubspec.yaml | 2 + .../lib/src/l10n/en.i18n.json | 11 + .../lib/src/l10n/es.i18n.json | 11 + .../lib/src/l10n/strings.g.dart | 4 +- .../lib/src/l10n/strings_en.g.dart | 53 ++++- .../lib/src/l10n/strings_es.g.dart | 36 ++- .../design_system/lib/src/ui_icons.dart | 3 + .../navigation/client_home_navigator.dart | 15 +- .../presentation/pages/client_home_page.dart | 6 +- .../client/settings/lib/client_settings.dart | 34 +++ .../settings_repository_impl.dart | 15 ++ .../settings_repository_interface.dart | 5 + .../src/domain/usecases/sign_out_usecase.dart | 15 ++ .../blocs/client_settings_bloc.dart | 31 +++ .../blocs/client_settings_event.dart | 12 + .../blocs/client_settings_state.dart | 29 +++ .../navigation/client_settings_navigator.dart | 10 + .../pages/client_settings_page.dart | 214 ++++++++++++++++++ .../features/client/settings/pubspec.yaml | 37 +++ apps/pubspec.yaml | 1 + 21 files changed, 536 insertions(+), 15 deletions(-) create mode 100644 apps/packages/features/client/settings/lib/client_settings.dart create mode 100644 apps/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart create mode 100644 apps/packages/features/client/settings/lib/src/domain/repositories/settings_repository_interface.dart create mode 100644 apps/packages/features/client/settings/lib/src/domain/usecases/sign_out_usecase.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_bloc.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_event.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_state.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/navigation/client_settings_navigator.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart create mode 100644 apps/packages/features/client/settings/pubspec.yaml diff --git a/apps/apps/client/lib/main.dart b/apps/apps/client/lib/main.dart index 8e8cf837..f23426e2 100644 --- a/apps/apps/client/lib/main.dart +++ b/apps/apps/client/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:client_authentication/client_authentication.dart' as client_authentication; import 'package:client_home/client_home.dart' as client_home; +import 'package:client_settings/client_settings.dart' as client_settings; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -25,6 +26,12 @@ class AppModule extends Module { // Client home route r.module('/client-home', module: client_home.ClientHomeModule()); + + // Client settings route + r.module( + '/client-settings', + module: client_settings.ClientSettingsModule(), + ); } } diff --git a/apps/apps/client/pubspec.yaml b/apps/apps/client/pubspec.yaml index 8550ff66..e969060a 100644 --- a/apps/apps/client/pubspec.yaml +++ b/apps/apps/client/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: path: ../../packages/features/client/authentication client_home: path: ../../packages/features/client/home + client_settings: + path: ../../packages/features/client/settings core_localization: path: ../../packages/core_localization flutter_modular: ^6.3.2 diff --git a/apps/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/packages/core_localization/lib/src/l10n/en.i18n.json index 63aa509e..1d556eab 100644 --- a/apps/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/packages/core_localization/lib/src/l10n/en.i18n.json @@ -185,6 +185,17 @@ "hourly_rate": "Hourly Rate (\\$) *", "post_shift": "Post Shift" } + }, + "client_settings": { + "profile": { + "title": "Profile", + "edit_profile": "Edit Profile", + "hubs": "Hubs", + "log_out": "Log Out", + "quick_links": "Quick Links", + "clock_in_hubs": "Clock-In Hubs", + "billing_payments": "Billing & Payments" + } } } diff --git a/apps/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/packages/core_localization/lib/src/l10n/es.i18n.json index dd1dfb8b..c596400d 100644 --- a/apps/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/packages/core_localization/lib/src/l10n/es.i18n.json @@ -185,5 +185,16 @@ "hourly_rate": "Tarifa por hora (\\$) *", "post_shift": "Publicar Turno" } + }, + "client_settings": { + "profile": { + "title": "Perfil", + "edit_profile": "Editar Perfil", + "hubs": "Hubs", + "log_out": "Cerrar sesión", + "quick_links": "Enlaces rápidos", + "clock_in_hubs": "Hubs de Marcaje", + "billing_payments": "Facturación y Pagos" + } } } diff --git a/apps/packages/core_localization/lib/src/l10n/strings.g.dart b/apps/packages/core_localization/lib/src/l10n/strings.g.dart index 1860222f..9fb8794c 100644 --- a/apps/packages/core_localization/lib/src/l10n/strings.g.dart +++ b/apps/packages/core_localization/lib/src/l10n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 276 (138 per locale) +/// Strings: 288 (144 per locale) /// -/// Built on 2026-01-21 at 18:21 UTC +/// Built on 2026-01-22 at 00:33 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/packages/core_localization/lib/src/l10n/strings_en.g.dart b/apps/packages/core_localization/lib/src/l10n/strings_en.g.dart index 25bf923a..e02e5085 100644 --- a/apps/packages/core_localization/lib/src/l10n/strings_en.g.dart +++ b/apps/packages/core_localization/lib/src/l10n/strings_en.g.dart @@ -45,6 +45,7 @@ class Translations with BaseTranslations { late final TranslationsStaffAuthenticationEn staff_authentication = TranslationsStaffAuthenticationEn._(_root); late final TranslationsClientAuthenticationEn client_authentication = TranslationsClientAuthenticationEn._(_root); late final TranslationsClientHomeEn client_home = TranslationsClientHomeEn._(_root); + late final TranslationsClientSettingsEn client_settings = TranslationsClientSettingsEn._(_root); } // Path: common @@ -120,10 +121,6 @@ class TranslationsClientHomeEn { final Translations _root; // ignore: unused_field // Translations - - /// en: 'Shift order submitted successfully' - String get shift_created_success => 'Shift order submitted successfully'; - late final TranslationsClientHomeDashboardEn dashboard = TranslationsClientHomeDashboardEn._(_root); late final TranslationsClientHomeWidgetsEn widgets = TranslationsClientHomeWidgetsEn._(_root); late final TranslationsClientHomeActionsEn actions = TranslationsClientHomeActionsEn._(_root); @@ -131,6 +128,16 @@ class TranslationsClientHomeEn { late final TranslationsClientHomeFormEn form = TranslationsClientHomeFormEn._(_root); } +// Path: client_settings +class TranslationsClientSettingsEn { + TranslationsClientSettingsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsClientSettingsProfileEn profile = TranslationsClientSettingsProfileEn._(_root); +} + // Path: staff_authentication.get_started_page class TranslationsStaffAuthenticationGetStartedPageEn { TranslationsStaffAuthenticationGetStartedPageEn._(this._root); @@ -541,6 +548,36 @@ class TranslationsClientHomeFormEn { String get post_shift => 'Post Shift'; } +// Path: client_settings.profile +class TranslationsClientSettingsProfileEn { + TranslationsClientSettingsProfileEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Profile' + String get title => 'Profile'; + + /// en: 'Edit Profile' + String get edit_profile => 'Edit Profile'; + + /// en: 'Hubs' + String get hubs => 'Hubs'; + + /// en: 'Log Out' + String get log_out => 'Log Out'; + + /// en: 'Quick Links' + String get quick_links => 'Quick Links'; + + /// en: 'Clock-In Hubs' + String get clock_in_hubs => 'Clock-In Hubs'; + + /// en: 'Billing & Payments' + String get billing_payments => 'Billing & Payments'; +} + // Path: staff_authentication.profile_setup_page.steps class TranslationsStaffAuthenticationProfileSetupPageStepsEn { TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root); @@ -816,7 +853,6 @@ extension on Translations { 'client_authentication.sign_up_page.social_google' => 'Sign Up with Google', 'client_authentication.sign_up_page.has_account' => 'Already have an account? ', 'client_authentication.sign_up_page.sign_in_link' => 'Sign In', - 'client_home.shift_created_success' => 'Shift order submitted successfully', 'client_home.dashboard.welcome_back' => 'Welcome back', 'client_home.dashboard.edit_mode_active' => 'Edit Mode Active', 'client_home.dashboard.drag_instruction' => 'Drag to reorder, toggle visibility', @@ -855,6 +891,13 @@ extension on Translations { 'client_home.form.workers_needed' => 'Workers Needed *', 'client_home.form.hourly_rate' => 'Hourly Rate (\$) *', 'client_home.form.post_shift' => 'Post Shift', + 'client_settings.profile.title' => 'Profile', + 'client_settings.profile.edit_profile' => 'Edit Profile', + 'client_settings.profile.hubs' => 'Hubs', + 'client_settings.profile.log_out' => 'Log Out', + 'client_settings.profile.quick_links' => 'Quick Links', + 'client_settings.profile.clock_in_hubs' => 'Clock-In Hubs', + 'client_settings.profile.billing_payments' => 'Billing & Payments', _ => null, }; } diff --git a/apps/packages/core_localization/lib/src/l10n/strings_es.g.dart b/apps/packages/core_localization/lib/src/l10n/strings_es.g.dart index 10fd7a44..7d2dd850 100644 --- a/apps/packages/core_localization/lib/src/l10n/strings_es.g.dart +++ b/apps/packages/core_localization/lib/src/l10n/strings_es.g.dart @@ -42,6 +42,7 @@ class TranslationsEs with BaseTranslations implements T @override late final _TranslationsStaffAuthenticationEs staff_authentication = _TranslationsStaffAuthenticationEs._(_root); @override late final _TranslationsClientAuthenticationEs client_authentication = _TranslationsClientAuthenticationEs._(_root); @override late final _TranslationsClientHomeEs client_home = _TranslationsClientHomeEs._(_root); + @override late final _TranslationsClientSettingsEs client_settings = _TranslationsClientSettingsEs._(_root); } // Path: common @@ -103,7 +104,6 @@ class _TranslationsClientHomeEs implements TranslationsClientHomeEn { final TranslationsEs _root; // ignore: unused_field // Translations - @override String get shift_created_success => 'Orden de turno enviada con éxito'; @override late final _TranslationsClientHomeDashboardEs dashboard = _TranslationsClientHomeDashboardEs._(_root); @override late final _TranslationsClientHomeWidgetsEs widgets = _TranslationsClientHomeWidgetsEs._(_root); @override late final _TranslationsClientHomeActionsEs actions = _TranslationsClientHomeActionsEs._(_root); @@ -111,6 +111,16 @@ class _TranslationsClientHomeEs implements TranslationsClientHomeEn { @override late final _TranslationsClientHomeFormEs form = _TranslationsClientHomeFormEs._(_root); } +// Path: client_settings +class _TranslationsClientSettingsEs implements TranslationsClientSettingsEn { + _TranslationsClientSettingsEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final _TranslationsClientSettingsProfileEs profile = _TranslationsClientSettingsProfileEs._(_root); +} + // Path: staff_authentication.get_started_page class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn { _TranslationsStaffAuthenticationGetStartedPageEs._(this._root); @@ -334,6 +344,22 @@ class _TranslationsClientHomeFormEs implements TranslationsClientHomeFormEn { @override String get post_shift => 'Publicar Turno'; } +// Path: client_settings.profile +class _TranslationsClientSettingsProfileEs implements TranslationsClientSettingsProfileEn { + _TranslationsClientSettingsProfileEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Perfil'; + @override String get edit_profile => 'Editar Perfil'; + @override String get hubs => 'Hubs'; + @override String get log_out => 'Cerrar sesión'; + @override String get quick_links => 'Enlaces rápidos'; + @override String get clock_in_hubs => 'Hubs de Marcaje'; + @override String get billing_payments => 'Facturación y Pagos'; +} + // Path: staff_authentication.profile_setup_page.steps class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn { _TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root); @@ -534,7 +560,6 @@ extension on TranslationsEs { 'client_authentication.sign_up_page.social_google' => 'Regístrate con Google', 'client_authentication.sign_up_page.has_account' => '¿Ya tienes una cuenta? ', 'client_authentication.sign_up_page.sign_in_link' => 'Iniciar sesión', - 'client_home.shift_created_success' => 'Orden de turno enviada con éxito', 'client_home.dashboard.welcome_back' => 'Bienvenido de nuevo', 'client_home.dashboard.edit_mode_active' => 'Modo Edición Activo', 'client_home.dashboard.drag_instruction' => 'Arrastra para reordenar, cambia la visibilidad', @@ -573,6 +598,13 @@ extension on TranslationsEs { 'client_home.form.workers_needed' => 'Trabajadores Necesarios *', 'client_home.form.hourly_rate' => 'Tarifa por hora (\$) *', 'client_home.form.post_shift' => 'Publicar Turno', + 'client_settings.profile.title' => 'Perfil', + 'client_settings.profile.edit_profile' => 'Editar Perfil', + 'client_settings.profile.hubs' => 'Hubs', + 'client_settings.profile.log_out' => 'Cerrar sesión', + 'client_settings.profile.quick_links' => 'Enlaces rápidos', + 'client_settings.profile.clock_in_hubs' => 'Hubs de Marcaje', + 'client_settings.profile.billing_payments' => 'Facturación y Pagos', _ => null, }; } diff --git a/apps/packages/design_system/lib/src/ui_icons.dart b/apps/packages/design_system/lib/src/ui_icons.dart index 9b3252cf..99e8b1f9 100644 --- a/apps/packages/design_system/lib/src/ui_icons.dart +++ b/apps/packages/design_system/lib/src/ui_icons.dart @@ -171,4 +171,7 @@ class UiIcons { /// Google icon static const IconData google = _IconLib2.google; + + /// NFC icon + static const IconData nfc = _IconLib.nfc; } diff --git a/apps/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart b/apps/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart index d3ad6e8e..a973363e 100644 --- a/apps/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart +++ b/apps/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart @@ -1,10 +1,15 @@ import 'package:flutter_modular/flutter_modular.dart'; -/// Navigation extension for the Client Home feature. +/// Extension on [IModularNavigator] to provide strongly-typed navigation +/// for the client home feature. extension ClientHomeNavigator on IModularNavigator { - /// Navigates to the Client Home page. - void navigateClientHome() => navigate('/client-home/'); + /// Navigates to the client home page. + void pushClientHome() { + pushNamed('/client/home/'); + } - /// Pushes the Client Home page. - Future pushClientHome() => pushNamed('/client-home/'); + /// Navigates to the settings page. + void pushSettings() { + pushNamed('/client-settings/'); + } } diff --git a/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart b/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart index eef72a62..83bbcf91 100644 --- a/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart +++ b/apps/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_modular/flutter_modular.dart'; import '../blocs/client_home_bloc.dart'; import '../blocs/client_home_event.dart'; import '../blocs/client_home_state.dart'; +import '../navigation/client_home_navigator.dart'; import '../widgets/actions_widget.dart'; import '../widgets/coverage_widget.dart'; import '../widgets/live_activity_widget.dart'; @@ -176,7 +177,10 @@ class ClientHomePage extends StatelessWidget { onTap: () {}, ), const SizedBox(width: UiConstants.space2), - _HeaderIconButton(icon: UiIcons.settings, onTap: () {}), + _HeaderIconButton( + icon: UiIcons.settings, + onTap: () => Modular.to.pushSettings(), + ), ], ), ], diff --git a/apps/packages/features/client/settings/lib/client_settings.dart b/apps/packages/features/client/settings/lib/client_settings.dart new file mode 100644 index 00000000..16b05949 --- /dev/null +++ b/apps/packages/features/client/settings/lib/client_settings.dart @@ -0,0 +1,34 @@ +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'src/data/repositories_impl/settings_repository_impl.dart'; +import 'src/domain/repositories/settings_repository_interface.dart'; +import 'src/domain/usecases/sign_out_usecase.dart'; +import 'src/presentation/blocs/client_settings_bloc.dart'; +import 'src/presentation/pages/client_settings_page.dart'; + +/// A [Module] for the client settings feature. +class ClientSettingsModule extends Module { + @override + List get imports => [DataConnectModule()]; + + @override + void binds(Injector i) { + // Repositories + i.addLazySingleton( + () => SettingsRepositoryImpl(i.get()), + ); + + // UseCases + i.addLazySingleton(SignOutUseCase.new); + + // BLoCs + i.add( + () => ClientSettingsBloc(signOutUseCase: i.get()), + ); + } + + @override + void routes(r) { + r.child('/', child: (_) => const ClientSettingsPage()); + } +} diff --git a/apps/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart b/apps/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart new file mode 100644 index 00000000..a5c8b4ab --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart @@ -0,0 +1,15 @@ +import 'package:krow_data_connect/krow_data_connect.dart'; +import '../../domain/repositories/settings_repository_interface.dart'; + +/// Implementation of [SettingsRepositoryInterface] that delegates to [AuthRepositoryMock]. +class SettingsRepositoryImpl implements SettingsRepositoryInterface { + final AuthRepositoryMock _authMock; + + /// Creates a [SettingsRepositoryImpl]. + SettingsRepositoryImpl(this._authMock); + + @override + Future signOut() { + return _authMock.signOut(); + } +} diff --git a/apps/packages/features/client/settings/lib/src/domain/repositories/settings_repository_interface.dart b/apps/packages/features/client/settings/lib/src/domain/repositories/settings_repository_interface.dart new file mode 100644 index 00000000..27cac004 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/domain/repositories/settings_repository_interface.dart @@ -0,0 +1,5 @@ +/// Interface for the Client Settings Repository. +abstract interface class SettingsRepositoryInterface { + /// Signs out the current user. + Future signOut(); +} diff --git a/apps/packages/features/client/settings/lib/src/domain/usecases/sign_out_usecase.dart b/apps/packages/features/client/settings/lib/src/domain/usecases/sign_out_usecase.dart new file mode 100644 index 00000000..68d5fde4 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/domain/usecases/sign_out_usecase.dart @@ -0,0 +1,15 @@ +import 'package:krow_core/core.dart'; +import '../repositories/settings_repository_interface.dart'; + +/// Use case handles the user sign out process. +class SignOutUseCase implements NoInputUseCase { + final SettingsRepositoryInterface _repository; + + /// Creates a [SignOutUseCase]. + SignOutUseCase(this._repository); + + @override + Future call() { + return _repository.signOut(); + } +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_bloc.dart b/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_bloc.dart new file mode 100644 index 00000000..25298faa --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_bloc.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/usecases/sign_out_usecase.dart'; + +part 'client_settings_event.dart'; +part 'client_settings_state.dart'; + +/// BLoC to manage client settings and profile state. +class ClientSettingsBloc + extends Bloc { + final SignOutUseCase _signOutUseCase; + + ClientSettingsBloc({required SignOutUseCase signOutUseCase}) + : _signOutUseCase = signOutUseCase, + super(const ClientSettingsInitial()) { + on(_onSignOutRequested); + } + + Future _onSignOutRequested( + ClientSettingsSignOutRequested event, + Emitter emit, + ) async { + emit(const ClientSettingsLoading()); + try { + await _signOutUseCase(); + emit(const ClientSettingsSignOutSuccess()); + } catch (e) { + emit(ClientSettingsError(e.toString())); + } + } +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_event.dart b/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_event.dart new file mode 100644 index 00000000..c32f8ad3 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_event.dart @@ -0,0 +1,12 @@ +part of 'client_settings_bloc.dart'; + +abstract class ClientSettingsEvent extends Equatable { + const ClientSettingsEvent(); + + @override + List get props => []; +} + +class ClientSettingsSignOutRequested extends ClientSettingsEvent { + const ClientSettingsSignOutRequested(); +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_state.dart b/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_state.dart new file mode 100644 index 00000000..5588fa0a --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/blocs/client_settings_state.dart @@ -0,0 +1,29 @@ +part of 'client_settings_bloc.dart'; + +abstract class ClientSettingsState extends Equatable { + const ClientSettingsState(); + + @override + List get props => []; +} + +class ClientSettingsInitial extends ClientSettingsState { + const ClientSettingsInitial(); +} + +class ClientSettingsLoading extends ClientSettingsState { + const ClientSettingsLoading(); +} + +class ClientSettingsSignOutSuccess extends ClientSettingsState { + const ClientSettingsSignOutSuccess(); +} + +class ClientSettingsError extends ClientSettingsState { + final String message; + + const ClientSettingsError(this.message); + + @override + List get props => [message]; +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/navigation/client_settings_navigator.dart b/apps/packages/features/client/settings/lib/src/presentation/navigation/client_settings_navigator.dart new file mode 100644 index 00000000..b5b454f5 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/navigation/client_settings_navigator.dart @@ -0,0 +1,10 @@ +import 'package:flutter_modular/flutter_modular.dart'; + +/// Extension on [IModularNavigator] to provide strongly-typed navigation +/// for the client settings feature. +extension ClientSettingsNavigator on IModularNavigator { + /// Navigates to the client settings page. + void pushClientSettings() { + pushNamed('/client/settings/'); + } +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart b/apps/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart new file mode 100644 index 00000000..cb6a9699 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/pages/client_settings_page.dart @@ -0,0 +1,214 @@ +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'; +import '../blocs/client_settings_bloc.dart'; + +/// Page for client settings and profile management. +class ClientSettingsPage extends StatelessWidget { + /// Creates a [ClientSettingsPage]. + const ClientSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + final labels = t.client_settings.profile; + + return BlocProvider( + create: (context) => Modular.get(), + child: BlocListener( + listener: (context, state) { + if (state is ClientSettingsSignOutSuccess) { + Modular.to.navigate('/'); + } + if (state is ClientSettingsError) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.message))); + } + }, + child: Scaffold( + backgroundColor: UiColors.background, + body: CustomScrollView( + slivers: [ + SliverAppBar( + backgroundColor: UiColors.primary, + expandedHeight: 200, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [UiColors.primary, Color(0xFF0047FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: UiConstants.space5 * 2, + ), // Adjust for SafeArea + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: UiColors.white.withValues(alpha: 0.24), + width: 4, + ), + color: UiColors.white, + ), + child: const Center( + child: Text( + 'C', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: UiColors.primary, + ), + ), + ), + ), + const SizedBox(height: UiConstants.space3), + Text( + 'Your Company', + style: UiTypography.body1b.copyWith( + color: UiColors.white, + ), + ), + const SizedBox(height: UiConstants.space1), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + UiIcons.mail, + size: 14, + color: UiColors.white.withValues(alpha: 0.7), + ), + const SizedBox(width: UiConstants.space2), + Text( + 'client@example.com', + style: UiTypography.footnote1r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + ], + ), + ], + ), + ), + ), + leading: IconButton( + icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), + onPressed: () => Modular.to.pop(), + ), + title: Text( + labels.title, + style: UiTypography.body1b.copyWith(color: UiColors.white), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(UiConstants.space5), + sliver: SliverList( + delegate: SliverChildListDelegate([ + UiButton.primary( + text: labels.edit_profile, + onPressed: () {}, + ), + const SizedBox(height: UiConstants.space4), + UiButton.primary(text: labels.hubs, onPressed: () {}), + const SizedBox(height: UiConstants.space4), + BlocBuilder( + builder: (context, state) { + return UiButton.secondary( + text: labels.log_out, + onPressed: state is ClientSettingsLoading + ? null + : () => BlocProvider.of( + context, + ).add(const ClientSettingsSignOutRequested()), + ); + }, + ), + const SizedBox(height: UiConstants.space5), + Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + side: const BorderSide(color: UiColors.border), + ), + color: UiColors.white, + child: Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + labels.quick_links, + style: UiTypography.footnote1b.textPrimary, + ), + const SizedBox(height: UiConstants.space3), + _buildQuickLink( + context, + icon: UiIcons.nfc, + title: labels.clock_in_hubs, + onTap: () {}, + ), + _buildQuickLink( + context, + icon: UiIcons.building, + title: labels.billing_payments, + onTap: () {}, + ), + ], + ), + ), + ), + ]), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildQuickLink( + BuildContext context, { + required IconData icon, + required String title, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: UiConstants.radiusMd, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space3, + horizontal: UiConstants.space2, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon, size: 20, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space3), + Text(title, style: UiTypography.footnote1m.textPrimary), + ], + ), + const Icon( + UiIcons.chevronRight, + size: 20, + color: UiColors.iconThird, + ), + ], + ), + ), + ); + } +} diff --git a/apps/packages/features/client/settings/pubspec.yaml b/apps/packages/features/client/settings/pubspec.yaml new file mode 100644 index 00000000..67077c9a --- /dev/null +++ b/apps/packages/features/client/settings/pubspec.yaml @@ -0,0 +1,37 @@ +name: client_settings +description: Settings and profile screen for the client application. +version: 0.0.1 +publish_to: none +resolution: workspace + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_bloc: ^8.1.0 + flutter_modular: ^6.3.0 + equatable: ^2.0.5 + lucide_icons: ^0.257.0 + + design_system: + path: ../../../design_system + core_localization: + path: ../../../core_localization + krow_core: + path: ../../../core + krow_domain: + path: ../../../domain + krow_data_connect: + path: ../../../data_connect + +dev_dependencies: + flutter_test: + sdk: flutter + bloc_test: ^9.1.0 + mocktail: ^1.0.0 + +flutter: + uses-material-design: true diff --git a/apps/pubspec.yaml b/apps/pubspec.yaml index 9ff2ff69..d9c97690 100644 --- a/apps/pubspec.yaml +++ b/apps/pubspec.yaml @@ -12,6 +12,7 @@ workspace: - packages/features/staff/authentication - packages/features/client/authentication - packages/features/client/home + - packages/features/client/settings - apps/staff - apps/client - apps/design_system_viewer