From 78917a5f840e475732489b62a363b9794a5fdef9 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Wed, 21 Jan 2026 19:41:13 -0500 Subject: [PATCH] refactor: extract UI components into dedicated widgets for the client settings page and update repository constructor. --- .../client/settings/lib/client_settings.dart | 2 +- .../settings_repository_impl.dart | 14 +- .../settings_repository_interface.dart | 6 +- .../src/domain/usecases/sign_out_usecase.dart | 4 + .../pages/client_settings_page.dart | 189 +----------------- .../settings_actions.dart | 41 ++++ .../settings_profile_header.dart | 91 +++++++++ .../settings_quick_links.dart | 95 +++++++++ 8 files changed, 256 insertions(+), 186 deletions(-) create mode 100644 apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart create mode 100644 apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_quick_links.dart diff --git a/apps/packages/features/client/settings/lib/client_settings.dart b/apps/packages/features/client/settings/lib/client_settings.dart index 16b05949..1ea5cb98 100644 --- a/apps/packages/features/client/settings/lib/client_settings.dart +++ b/apps/packages/features/client/settings/lib/client_settings.dart @@ -15,7 +15,7 @@ class ClientSettingsModule extends Module { void binds(Injector i) { // Repositories i.addLazySingleton( - () => SettingsRepositoryImpl(i.get()), + () => SettingsRepositoryImpl(mock: i.get()), ); // UseCases 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 index a5c8b4ab..fdd5d922 100644 --- 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 @@ -1,15 +1,19 @@ import 'package:krow_data_connect/krow_data_connect.dart'; import '../../domain/repositories/settings_repository_interface.dart'; -/// Implementation of [SettingsRepositoryInterface] that delegates to [AuthRepositoryMock]. +/// Implementation of [SettingsRepositoryInterface]. +/// +/// This implementation delegates data access to the [AuthRepositoryMock] +/// from the `data_connect` package. class SettingsRepositoryImpl implements SettingsRepositoryInterface { - final AuthRepositoryMock _authMock; + /// The auth mock from data connect. + final AuthRepositoryMock mock; - /// Creates a [SettingsRepositoryImpl]. - SettingsRepositoryImpl(this._authMock); + /// Creates a [SettingsRepositoryImpl] with the required [mock]. + SettingsRepositoryImpl({required this.mock}); @override Future signOut() { - return _authMock.signOut(); + return mock.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 index 27cac004..4c936d68 100644 --- 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 @@ -1,5 +1,7 @@ -/// Interface for the Client Settings Repository. +/// Interface for the Client Settings repository. +/// +/// This repository handles settings-related operations such as user sign out. abstract interface class SettingsRepositoryInterface { - /// Signs out the current user. + /// Signs out the current user from the application. 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 index 68d5fde4..3f050dfc 100644 --- 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 @@ -2,10 +2,14 @@ import 'package:krow_core/core.dart'; import '../repositories/settings_repository_interface.dart'; /// Use case handles the user sign out process. +/// +/// This use case delegates the sign out logic to the [SettingsRepositoryInterface]. class SignOutUseCase implements NoInputUseCase { final SettingsRepositoryInterface _repository; /// Creates a [SignOutUseCase]. + /// + /// Requires a [SettingsRepositoryInterface] to perform the sign out operation. SignOutUseCase(this._repository); @override 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 index cb6a9699..f4ae4ae4 100644 --- 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 @@ -1,19 +1,23 @@ -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'; +import '../widgets/client_settings_page/settings_actions.dart'; +import '../widgets/client_settings_page/settings_profile_header.dart'; +import '../widgets/client_settings_page/settings_quick_links.dart'; /// Page for client settings and profile management. +/// +/// This page follows the KROW architecture by being a [StatelessWidget] +/// and delegating its state management to [ClientSettingsBloc] and its +/// UI sections to specialized sub-widgets. 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( @@ -27,188 +31,17 @@ class ClientSettingsPage extends StatelessWidget { ).showSnackBar(SnackBar(content: Text(state.message))); } }, - child: Scaffold( + child: const 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: () {}, - ), - ], - ), - ), - ), - ]), - ), - ), + SettingsProfileHeader(), + SettingsActions(), + SettingsQuickLinks(), ], ), ), ), ); } - - 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/lib/src/presentation/widgets/client_settings_page/settings_actions.dart b/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart new file mode 100644 index 00000000..1375b271 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart @@ -0,0 +1,41 @@ +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 '../../blocs/client_settings_bloc.dart'; + +/// A widget that displays the primary actions for the settings page. +class SettingsActions extends StatelessWidget { + /// Creates a [SettingsActions]. + const SettingsActions({super.key}); + + @override + Widget build(BuildContext context) { + final labels = t.client_settings.profile; + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), + sliver: SliverList( + delegate: SliverChildListDelegate([ + const SizedBox(height: UiConstants.space5), + 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()), + ); + }, + ), + ]), + ), + ); + } +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart new file mode 100644 index 00000000..8ef63b89 --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -0,0 +1,91 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +/// A widget that displays the profile header with avatar and company info. +class SettingsProfileHeader extends StatelessWidget { + /// Creates a [SettingsProfileHeader]. + const SettingsProfileHeader({super.key}); + + @override + Widget build(BuildContext context) { + final labels = t.client_settings.profile; + + return 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), + 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), + ), + ); + } +} diff --git a/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_quick_links.dart b/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_quick_links.dart new file mode 100644 index 00000000..5e33a33b --- /dev/null +++ b/apps/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_quick_links.dart @@ -0,0 +1,95 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A widget that displays a list of quick links in a card. +class SettingsQuickLinks extends StatelessWidget { + /// Creates a [SettingsQuickLinks]. + const SettingsQuickLinks({super.key}); + + @override + Widget build(BuildContext context) { + final labels = t.client_settings.profile; + + return SliverPadding( + padding: const EdgeInsets.all(UiConstants.space5), + sliver: SliverToBoxAdapter( + child: 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), + _QuickLinkItem( + icon: UiIcons.nfc, + title: labels.clock_in_hubs, + onTap: () {}, + ), + _QuickLinkItem( + icon: UiIcons.building, + title: labels.billing_payments, + onTap: () {}, + ), + ], + ), + ), + ), + ), + ); + } +} + +/// Internal widget for a single quick link item. +class _QuickLinkItem extends StatelessWidget { + final IconData icon; + final String title; + final VoidCallback onTap; + + const _QuickLinkItem({ + required this.icon, + required this.title, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + 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, + ), + ], + ), + ), + ); + } +}