feat: Implement profile completion feature with repository and use case
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user