diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/data/repositories/profile_completion_repository_impl.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/data/repositories/profile_completion_repository_impl.dart new file mode 100644 index 00000000..c72c30a2 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/data/repositories/profile_completion_repository_impl.dart @@ -0,0 +1,55 @@ +import 'package:krow_data_connect/krow_data_connect.dart'; + +import 'package:staff_main/src/domain/repositories/profile_completion_repository.dart'; + +/// Implementation of [ProfileCompletionRepositoryInterface]. +/// +/// Fetches profile completion status from the Data Connect backend. +class ProfileCompletionRepositoryImpl implements ProfileCompletionRepositoryInterface { + /// Creates a new [ProfileCompletionRepositoryImpl]. + /// + /// Requires a [DataConnectService] instance for backend communication. + ProfileCompletionRepositoryImpl({ + DataConnectService? service, + }) : _service = service ?? DataConnectService.instance; + + final DataConnectService _service; + + @override + Future getProfileCompletion() async { + return _service.run(() async { + final String staffId = await _service.getStaffId(); + + final response = await _service.connector + .getStaffProfileCompletion(id: staffId) + .execute(); + + final staff = response.data.staff; + final emergencyContacts = response.data.emergencyContacts; + final taxForms = response.data.taxForms; + + return _isProfileComplete(staff, emergencyContacts, taxForms); + }); + } + + /// Checks if staff has experience data (skills or industries). + bool _hasExperience(dynamic staff) { + if (staff == null) return false; + final skills = staff.skills; + final industries = staff.industries; + return (skills is List && skills.isNotEmpty) || + (industries is List && industries.isNotEmpty); + } + + /// Determines if the profile is complete based on all sections. + bool _isProfileComplete( + dynamic staff, + List emergencyContacts, + List taxForms, + ) { + return staff != null && + emergencyContacts.isNotEmpty && + taxForms.isNotEmpty && + _hasExperience(staff); + } +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/domain/repositories/profile_completion_repository.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/domain/repositories/profile_completion_repository.dart new file mode 100644 index 00000000..33a53169 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/domain/repositories/profile_completion_repository.dart @@ -0,0 +1,13 @@ +/// Repository interface for profile completion status queries. +/// +/// This interface defines the contract for accessing profile completion data. +/// Implementations should fetch this data from the backend via Data Connect. +abstract interface class ProfileCompletionRepositoryInterface { + /// Fetches whether the profile is complete for the current staff member. + /// + /// Returns true if all required profile sections have been completed, + /// false otherwise. + /// + /// Throws an exception if the query fails. + Future getProfileCompletion(); +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/domain/usecases/get_profile_completion_usecase.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/domain/usecases/get_profile_completion_usecase.dart new file mode 100644 index 00000000..6d09e6e1 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/domain/usecases/get_profile_completion_usecase.dart @@ -0,0 +1,24 @@ +import '../repositories/profile_completion_repository.dart'; + +/// Use case for retrieving profile completion status. +/// +/// This use case encapsulates the business logic for determining whether +/// a staff member's profile is complete. It delegates to the repository +/// for data access. +class GetProfileCompletionUsecase { + /// Creates a [GetProfileCompletionUsecase]. + /// + /// Requires a [ProfileCompletionRepositoryInterface] for data access. + GetProfileCompletionUsecase({ + required ProfileCompletionRepositoryInterface repository, + }) : _repository = repository; + + final ProfileCompletionRepositoryInterface _repository; + + /// Executes the use case to get profile completion status. + /// + /// Returns true if the profile is complete, false otherwise. + /// + /// Throws an exception if the operation fails. + Future call() => _repository.getProfileCompletion(); +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart index 9f33afb1..004cdfae 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart @@ -1,14 +1,22 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:staff_main/src/domain/usecases/get_profile_completion_usecase.dart'; import 'package:staff_main/src/presentation/blocs/staff_main_state.dart'; class StaffMainCubit extends Cubit implements Disposable { - StaffMainCubit() : super(const StaffMainState()) { + StaffMainCubit({ + required GetProfileCompletionUsecase getProfileCompletionUsecase, + }) : _getProfileCompletionUsecase = getProfileCompletionUsecase, + super(const StaffMainState()) { Modular.to.addListener(_onRouteChanged); _onRouteChanged(); + _loadProfileCompletion(); } + final GetProfileCompletionUsecase _getProfileCompletionUsecase; + void _onRouteChanged() { if (isClosed) return; final String path = Modular.to.path; @@ -32,6 +40,22 @@ class StaffMainCubit extends Cubit implements Disposable { } } + /// Loads the profile completion status. + Future _loadProfileCompletion() async { + try { + final isComplete = await _getProfileCompletionUsecase(); + if (!isClosed) { + emit(state.copyWith(isProfileComplete: isComplete)); + } + } catch (e) { + // If there's an error, allow access to all features + debugPrint('Error loading profile completion: $e'); + if (!isClosed) { + emit(state.copyWith(isProfileComplete: true)); + } + } + } + void navigateToTab(int index) { if (index == state.currentIndex) return; diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart index 68175302..0903b877 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart @@ -3,14 +3,19 @@ import 'package:equatable/equatable.dart'; class StaffMainState extends Equatable { const StaffMainState({ this.currentIndex = 2, // Default to Home + this.isProfileComplete = false, }); final int currentIndex; + final bool isProfileComplete; - StaffMainState copyWith({int? currentIndex}) { - return StaffMainState(currentIndex: currentIndex ?? this.currentIndex); + StaffMainState copyWith({int? currentIndex, bool? isProfileComplete}) { + return StaffMainState( + currentIndex: currentIndex ?? this.currentIndex, + isProfileComplete: isProfileComplete ?? this.isProfileComplete, + ); } @override - List get props => [currentIndex]; + List get props => [currentIndex, isProfileComplete]; } diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart index 30ea3405..176719ed 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart @@ -2,6 +2,9 @@ import 'dart:ui'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart'; +import 'package:staff_main/src/presentation/blocs/staff_main_state.dart'; import 'package:staff_main/src/utils/index.dart'; /// A custom bottom navigation bar for the Staff app. @@ -10,6 +13,10 @@ import 'package:staff_main/src/utils/index.dart'; /// and follows the KROW Design System guidelines. It displays five tabs: /// Shifts, Payments, Home, Clock In, and Profile. /// +/// Navigation items are gated by profile completion status. Items marked with +/// [StaffNavItem.requireProfileCompletion] are only visible when the profile +/// is complete. +/// /// The widget uses: /// - [UiColors] for all color values /// - [UiTypography] for text styling @@ -41,48 +48,55 @@ class StaffMainBottomBar extends StatelessWidget { const Color activeColor = UiColors.primary; const Color inactiveColor = UiColors.textInactive; - return Stack( - clipBehavior: Clip.none, - children: [ - // Glassmorphic background with blur effect - Positioned.fill( - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - decoration: BoxDecoration( - color: UiColors.white.withValues(alpha: 0.85), - border: Border( - top: BorderSide( - color: UiColors.black.withValues(alpha: 0.1), + return BlocBuilder( + builder: (BuildContext context, StaffMainState state) { + final bool isProfileComplete = state.isProfileComplete; + + return Stack( + clipBehavior: Clip.none, + children: [ + // Glassmorphic background with blur effect + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.85), + border: Border( + top: BorderSide( + color: UiColors.black.withValues(alpha: 0.1), + ), + ), ), ), ), ), ), - ), - ), - // Navigation items - Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + UiConstants.space2, - top: UiConstants.space4, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ...defaultStaffNavItems.map( - (item) => _buildNavItem( - item: item, - activeColor: activeColor, - inactiveColor: inactiveColor, - ), + // Navigation items + Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + UiConstants.space2, + top: UiConstants.space4, ), - ], - ), - ), - ], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ...defaultStaffNavItems.map( + (item) => _buildNavItem( + item: item, + activeColor: activeColor, + inactiveColor: inactiveColor, + isProfileComplete: isProfileComplete, + ), + ), + ], + ), + ), + ], + ); + }, ); } @@ -94,13 +108,19 @@ class StaffMainBottomBar extends StatelessWidget { /// - Typography uses [UiTypography.footnote2m] /// - Colors are passed as parameters from design system /// - /// The [item.requireProfileCompletion] flag can be used to conditionally - /// disable or style the item based on profile completion status. + /// Items with [item.requireProfileCompletion] = true are hidden when + /// [isProfileComplete] is false. Widget _buildNavItem({ required StaffNavItem item, required Color activeColor, required Color inactiveColor, + required bool isProfileComplete, }) { + // Hide item if profile completion is required but not complete + if (item.requireProfileCompletion && !isProfileComplete) { + return const SizedBox.shrink(); + } + final bool isSelected = currentIndex == item.index; return Expanded( child: GestureDetector( 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 fd5ddc74..27a3484f 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 @@ -8,7 +8,11 @@ import 'package:staff_certificates/staff_certificates.dart'; import 'package:staff_clock_in/staff_clock_in.dart'; import 'package:staff_documents/staff_documents.dart'; import 'package:staff_emergency_contact/staff_emergency_contact.dart'; +import 'package:staff_faqs/staff_faqs.dart'; import 'package:staff_home/staff_home.dart'; +import 'package:staff_main/src/data/repositories/profile_completion_repository_impl.dart'; +import 'package:staff_main/src/domain/repositories/profile_completion_repository.dart'; +import 'package:staff_main/src/domain/usecases/get_profile_completion_usecase.dart'; import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart'; import 'package:staff_main/src/presentation/pages/staff_main_page.dart'; import 'package:staff_payments/staff_payements.dart'; @@ -18,13 +22,24 @@ import 'package:staff_profile_experience/staff_profile_experience.dart'; import 'package:staff_profile_info/staff_profile_info.dart'; import 'package:staff_shifts/staff_shifts.dart'; import 'package:staff_tax_forms/staff_tax_forms.dart'; -import 'package:staff_faqs/staff_faqs.dart'; import 'package:staff_time_card/staff_time_card.dart'; class StaffMainModule extends Module { @override void binds(Injector i) { - i.addSingleton(StaffMainCubit.new); + i.addSingleton( + ProfileCompletionRepositoryImpl.new, + ); + i.addSingleton( + () => GetProfileCompletionUsecase( + repository: i.get(), + ), + ); + i.addSingleton( + () => StaffMainCubit( + getProfileCompletionUsecase: i.get(), + ), + ); } @override diff --git a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml index f31d21a8..91c0b8a4 100644 --- a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml +++ b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml @@ -21,7 +21,9 @@ dependencies: core_localization: path: ../../../core_localization krow_core: - path: ../../../krow_core + path: ../../../core + krow_data_connect: + path: ../../../data_connect # Features staff_home: diff --git a/docs/MOBILE/00-agent-development-rules.md b/docs/MOBILE/00-agent-development-rules.md index c7322cfc..5ef0a8b7 100644 --- a/docs/MOBILE/00-agent-development-rules.md +++ b/docs/MOBILE/00-agent-development-rules.md @@ -111,7 +111,7 @@ If a user request is vague: * **DO NOT** add 3rd party packages without checking `apps/mobile/packages/core` first. * **DO NOT** add `firebase_auth` or `firebase_data_connect` to any Feature package. They belong in `data_connect` only. * **Service Locator**: Use `DataConnectService.instance` for singleton access to backend operations. -* **Dependency Injection**: Use Flutter Modular for BLoC and UseCase injection in `Module.routes()`. +* **Dependency Injection**: Use Flutter Modular for BLoC (never use `addSingleton` for Blocs, always use `add` method) and UseCase injection in `Module.routes()`. ## 8. Error Handling diff --git a/docs/MOBILE/01-architecture-principles.md b/docs/MOBILE/01-architecture-principles.md index c24a8295..40bcb623 100644 --- a/docs/MOBILE/01-architecture-principles.md +++ b/docs/MOBILE/01-architecture-principles.md @@ -64,7 +64,7 @@ graph TD ### 2.2 Features (`apps/mobile/packages/features//`) - **Role**: Vertical slices of user-facing functionality. - **Internal Structure**: - - `domain/`: Feature-specific Use Cases and Repository Interfaces. + - `domain/`: Feature-specific Use Cases(always extend the apps/mobile/packages/core/lib/src/domain/usecases/usecase.dart abstract clas) and Repository Interfaces. - `data/`: Repository Implementations. - `presentation/`: - Pages, BLoCs, Widgets.