feat: Implement profile completion feature with repository and use case

This commit is contained in:
Achintha Isuru
2026-02-19 10:56:04 -05:00
parent c48dab6786
commit a162824887
10 changed files with 205 additions and 47 deletions

View File

@@ -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<bool> 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<dynamic> emergencyContacts,
List<dynamic> taxForms,
) {
return staff != null &&
emergencyContacts.isNotEmpty &&
taxForms.isNotEmpty &&
_hasExperience(staff);
}
}

View File

@@ -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<bool> getProfileCompletion();
}

View File

@@ -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<bool> call() => _repository.getProfileCompletion();
}

View File

@@ -1,14 +1,22 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:flutter_modular/flutter_modular.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'; import 'package:staff_main/src/presentation/blocs/staff_main_state.dart';
class StaffMainCubit extends Cubit<StaffMainState> implements Disposable { class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
StaffMainCubit() : super(const StaffMainState()) { StaffMainCubit({
required GetProfileCompletionUsecase getProfileCompletionUsecase,
}) : _getProfileCompletionUsecase = getProfileCompletionUsecase,
super(const StaffMainState()) {
Modular.to.addListener(_onRouteChanged); Modular.to.addListener(_onRouteChanged);
_onRouteChanged(); _onRouteChanged();
_loadProfileCompletion();
} }
final GetProfileCompletionUsecase _getProfileCompletionUsecase;
void _onRouteChanged() { void _onRouteChanged() {
if (isClosed) return; if (isClosed) return;
final String path = Modular.to.path; final String path = Modular.to.path;
@@ -32,6 +40,22 @@ class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
} }
} }
/// Loads the profile completion status.
Future<void> _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) { void navigateToTab(int index) {
if (index == state.currentIndex) return; if (index == state.currentIndex) return;

View File

@@ -3,14 +3,19 @@ import 'package:equatable/equatable.dart';
class StaffMainState extends Equatable { class StaffMainState extends Equatable {
const StaffMainState({ const StaffMainState({
this.currentIndex = 2, // Default to Home this.currentIndex = 2, // Default to Home
this.isProfileComplete = false,
}); });
final int currentIndex; final int currentIndex;
final bool isProfileComplete;
StaffMainState copyWith({int? currentIndex}) { StaffMainState copyWith({int? currentIndex, bool? isProfileComplete}) {
return StaffMainState(currentIndex: currentIndex ?? this.currentIndex); return StaffMainState(
currentIndex: currentIndex ?? this.currentIndex,
isProfileComplete: isProfileComplete ?? this.isProfileComplete,
);
} }
@override @override
List<Object> get props => <Object>[currentIndex]; List<Object> get props => <Object>[currentIndex, isProfileComplete];
} }

View File

@@ -2,6 +2,9 @@ import 'dart:ui';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.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'; import 'package:staff_main/src/utils/index.dart';
/// A custom bottom navigation bar for the Staff app. /// 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: /// and follows the KROW Design System guidelines. It displays five tabs:
/// Shifts, Payments, Home, Clock In, and Profile. /// 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: /// The widget uses:
/// - [UiColors] for all color values /// - [UiColors] for all color values
/// - [UiTypography] for text styling /// - [UiTypography] for text styling
@@ -41,6 +48,10 @@ class StaffMainBottomBar extends StatelessWidget {
const Color activeColor = UiColors.primary; const Color activeColor = UiColors.primary;
const Color inactiveColor = UiColors.textInactive; const Color inactiveColor = UiColors.textInactive;
return BlocBuilder<StaffMainCubit, StaffMainState>(
builder: (BuildContext context, StaffMainState state) {
final bool isProfileComplete = state.isProfileComplete;
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: <Widget>[ children: <Widget>[
@@ -77,6 +88,7 @@ class StaffMainBottomBar extends StatelessWidget {
item: item, item: item,
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
isProfileComplete: isProfileComplete,
), ),
), ),
], ],
@@ -84,6 +96,8 @@ class StaffMainBottomBar extends StatelessWidget {
), ),
], ],
); );
},
);
} }
/// Builds a single navigation item. /// Builds a single navigation item.
@@ -94,13 +108,19 @@ class StaffMainBottomBar extends StatelessWidget {
/// - Typography uses [UiTypography.footnote2m] /// - Typography uses [UiTypography.footnote2m]
/// - Colors are passed as parameters from design system /// - Colors are passed as parameters from design system
/// ///
/// The [item.requireProfileCompletion] flag can be used to conditionally /// Items with [item.requireProfileCompletion] = true are hidden when
/// disable or style the item based on profile completion status. /// [isProfileComplete] is false.
Widget _buildNavItem({ Widget _buildNavItem({
required StaffNavItem item, required StaffNavItem item,
required Color activeColor, required Color activeColor,
required Color inactiveColor, 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; final bool isSelected = currentIndex == item.index;
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(

View File

@@ -8,7 +8,11 @@ import 'package:staff_certificates/staff_certificates.dart';
import 'package:staff_clock_in/staff_clock_in.dart'; import 'package:staff_clock_in/staff_clock_in.dart';
import 'package:staff_documents/staff_documents.dart'; import 'package:staff_documents/staff_documents.dart';
import 'package:staff_emergency_contact/staff_emergency_contact.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_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/blocs/staff_main_cubit.dart';
import 'package:staff_main/src/presentation/pages/staff_main_page.dart'; import 'package:staff_main/src/presentation/pages/staff_main_page.dart';
import 'package:staff_payments/staff_payements.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_profile_info/staff_profile_info.dart';
import 'package:staff_shifts/staff_shifts.dart'; import 'package:staff_shifts/staff_shifts.dart';
import 'package:staff_tax_forms/staff_tax_forms.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'; import 'package:staff_time_card/staff_time_card.dart';
class StaffMainModule extends Module { class StaffMainModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
i.addSingleton(StaffMainCubit.new); i.addSingleton<ProfileCompletionRepositoryInterface>(
ProfileCompletionRepositoryImpl.new,
);
i.addSingleton(
() => GetProfileCompletionUsecase(
repository: i.get<ProfileCompletionRepositoryInterface>(),
),
);
i.addSingleton(
() => StaffMainCubit(
getProfileCompletionUsecase: i.get<GetProfileCompletionUsecase>(),
),
);
} }
@override @override

View File

@@ -21,7 +21,9 @@ dependencies:
core_localization: core_localization:
path: ../../../core_localization path: ../../../core_localization
krow_core: krow_core:
path: ../../../krow_core path: ../../../core
krow_data_connect:
path: ../../../data_connect
# Features # Features
staff_home: staff_home:

View File

@@ -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 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. * **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. * **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 ## 8. Error Handling

View File

@@ -64,7 +64,7 @@ graph TD
### 2.2 Features (`apps/mobile/packages/features/<APP_NAME>/<FEATURE_NAME>`) ### 2.2 Features (`apps/mobile/packages/features/<APP_NAME>/<FEATURE_NAME>`)
- **Role**: Vertical slices of user-facing functionality. - **Role**: Vertical slices of user-facing functionality.
- **Internal Structure**: - **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. - `data/`: Repository Implementations.
- `presentation/`: - `presentation/`:
- Pages, BLoCs, Widgets. - Pages, BLoCs, Widgets.