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: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;
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,48 +48,55 @@ 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 Stack(
|
return BlocBuilder<StaffMainCubit, StaffMainState>(
|
||||||
clipBehavior: Clip.none,
|
builder: (BuildContext context, StaffMainState state) {
|
||||||
children: <Widget>[
|
final bool isProfileComplete = state.isProfileComplete;
|
||||||
// Glassmorphic background with blur effect
|
|
||||||
Positioned.fill(
|
return Stack(
|
||||||
child: ClipRect(
|
clipBehavior: Clip.none,
|
||||||
child: BackdropFilter(
|
children: <Widget>[
|
||||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
// Glassmorphic background with blur effect
|
||||||
child: Container(
|
Positioned.fill(
|
||||||
decoration: BoxDecoration(
|
child: ClipRect(
|
||||||
color: UiColors.white.withValues(alpha: 0.85),
|
child: BackdropFilter(
|
||||||
border: Border(
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
top: BorderSide(
|
child: Container(
|
||||||
color: UiColors.black.withValues(alpha: 0.1),
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.85),
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: UiColors.black.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Navigation items
|
||||||
),
|
Container(
|
||||||
// Navigation items
|
padding: EdgeInsets.only(
|
||||||
Container(
|
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space2,
|
||||||
padding: EdgeInsets.only(
|
top: UiConstants.space4,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
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]
|
/// - 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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user