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: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<StaffMainState> 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<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) {
if (index == state.currentIndex) return;

View File

@@ -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<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: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: <Widget>[
// 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<StaffMainCubit, StaffMainState>(
builder: (BuildContext context, StaffMainState state) {
final bool isProfileComplete = state.isProfileComplete;
return Stack(
clipBehavior: Clip.none,
children: <Widget>[
// 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: <Widget>[
...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: <Widget>[
...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(

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_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<ProfileCompletionRepositoryInterface>(
ProfileCompletionRepositoryImpl.new,
);
i.addSingleton(
() => GetProfileCompletionUsecase(
repository: i.get<ProfileCompletionRepositoryInterface>(),
),
);
i.addSingleton(
() => StaffMainCubit(
getProfileCompletionUsecase: i.get<GetProfileCompletionUsecase>(),
),
);
}
@override

View File

@@ -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:

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 `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

View File

@@ -64,7 +64,7 @@ graph TD
### 2.2 Features (`apps/mobile/packages/features/<APP_NAME>/<FEATURE_NAME>`)
- **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.